Compare commits

..

1 Commits

Author SHA1 Message Date
7a7a83c835 path to node progress 2024-01-25 09:53:14 +11:00
242 changed files with 5583 additions and 48230 deletions

View File

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

View File

@ -1,2 +1 @@
src/wasm-lib/* src/wasm-lib/*
*.typegen.ts

View File

@ -17,12 +17,12 @@
"never" "never"
], ],
"react-hooks/exhaustive-deps": "off", "react-hooks/exhaustive-deps": "off",
"@typescript-eslint/no-floating-promises": "warn"
}, },
"overrides": [ "overrides": [
{ {
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure "files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
"rules": { "rules": {
"@typescript-eslint/no-floating-promises": "warn",
"testing-library/prefer-screen-queries": "off" "testing-library/prefer-screen-queries": "off"
} }
} }

View File

@ -1,29 +0,0 @@
name: Announce Release
on:
release:
types: [published]
jobs:
announce_release:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Announce Release
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
RELEASE_VERSION: ${{ github.event.release.tag_name }}
RELEASE_BODY: ${{ github.event.release.body}}
run: python public/announce_release.py

View File

@ -46,7 +46,6 @@ jobs:
workspaces: './src/wasm-lib' workspaces: './src/wasm-lib'
- run: yarn build:wasm - run: yarn build:wasm
- run: yarn xstate:typegen
- run: yarn tsc - run: yarn tsc
@ -126,7 +125,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [macos-14, ubuntu-latest, windows-latest] os: [macos-latest, ubuntu-latest, windows-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -189,10 +188,10 @@ jobs:
- name: Fix format - name: Fix format
run: yarn fmt run: yarn fmt
- name: Install x86 target for Universal builds (MacOS only) - name: Install Universal target (MacOS only)
if: matrix.os == 'macos-14' if: matrix.os == 'macos-latest'
run: | run: |
rustup target add x86_64-apple-darwin rustup target add aarch64-apple-darwin
- name: Prepare certificate and variables (Windows only) - name: Prepare certificate and variables (Windows only)
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }} if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
@ -226,7 +225,7 @@ jobs:
with: with:
includeRelease: false includeRelease: false
includeDebug: true includeDebug: true
args: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }} args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
- name: Build the app (release) and sign - name: Build the app (release) and sign
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
@ -242,12 +241,12 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}" TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
with: with:
args: "${{ matrix.os == 'macos-14' && '--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@v3
if: matrix.os != 'ubuntu-latest' if: matrix.os != 'ubuntu-latest'
env: env:
PREFIX: ${{ matrix.os == 'macos-14' && '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' }}
with: with:
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*" path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
@ -255,7 +254,7 @@ jobs:
- name: Run e2e tests (linux only) - name: Run e2e tests (linux only)
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
run: | run: |
cargo install tauri-driver@0.1.3 cargo install tauri-driver
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }} source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
export VITE_KC_API_BASE_URL export VITE_KC_API_BASE_URL
xvfb-run yarn test:e2e:tauri xvfb-run yarn test:e2e:tauri
@ -338,17 +337,17 @@ jobs:
cat last_download.json cat last_download.json
- name: Authenticate to Google Cloud - name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.1.1' uses: 'google-github-actions/auth@v2.0.0'
with: with:
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@v2.1.0 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@v2.1.0 uses: google-github-actions/upload-cloud-storage@v2.0.0
with: with:
path: artifact path: artifact
glob: '*/Zoo*' glob: '*/Zoo*'
@ -356,13 +355,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@v2.1.0 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@v2.1.0 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 }}

View File

@ -79,7 +79,7 @@ jobs:
playwright-macos: playwright-macos:
timeout-minutes: 60 timeout-minutes: 60
runs-on: macos-14 runs-on: macos-latest
needs: playwright-ubuntu needs: playwright-ubuntu
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

4
.gitignore vendored
View File

@ -50,7 +50,3 @@ e2e/playwright/export-snapshots/*embedded.gltf
/playwright-report/ /playwright-report/
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
## generated files
src/**/*.typegen.ts

View File

@ -10,4 +10,4 @@ src/wasm-lib/kcl/bindings
e2e/playwright/export-snapshots e2e/playwright/export-snapshots
# XState generated files # XState generated files
src/machines/**.typegen.ts src/machines/modelingMachine.typegen.ts

View File

@ -182,7 +182,7 @@ For more information on fuzzing you can check out
First time running plawright locally, you'll need to add the secrets file First time running plawright locally, you'll need to add the secrets file
```bash ```bash
touch ./e2e/playwright/playwright-secrets.env touch ./e2e/playwright/playwright-secrets.env
printf 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets.env echo 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets2.env
``` ```
then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens

View File

@ -1,16 +0,0 @@
# Known Issues
The following are bugs that are not in modeling-app or kcl itself. These bugs
once fixed in engine will just start working here with no language changes.
- **Sketch on Face**: If your sketch is outside the edges of the face (on which you
are sketching) you will get multiple models returned instead of one single
model for that sketch and its underlying 3D object.
- **Patterns**: If you try and pass a pattern to `hole` currently only the first
item in the pattern is being subtracted. This is an engine bug that is being
worked on.
- **Import**: Right now you can import a file, even if that file has brep data
you cannot edit it. You also cannot move or transform the imported objects at
all. In the future, after v1, the engine will account for this.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -498,7 +498,7 @@
} }
], ],
"asset": { "asset": {
"generator": "zoo.dev", "generator": "kittycad.io",
"version": "2.0" "version": "2.0"
}, },
"buffers": [ "buffers": [
@ -882,7 +882,7 @@
"loops": [ "loops": [
[ [
14, 14,
1 -1
] ]
] ]
}, },
@ -1953,17 +1953,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
-1,
0, 0,
1,
0 0
], ],
"yAxis": [ "point": [
0,
0,
-1
],
"origin": [
0, 0,
-0.0127, -0.0127,
0.050800000000000005 0.050800000000000005
@ -1973,17 +1968,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
1,
0, 0,
0 -1,
-0
], ],
"yAxis": [ "point": [
-0,
0,
1
],
"origin": [
0.039310000000000005, 0.039310000000000005,
-0.0254, -0.0254,
0.050800000000000005 0.050800000000000005
@ -1993,17 +1983,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
0.81915,
-0.5735800000000001, -0.5735800000000001,
-0.81915,
0 0
], ],
"yAxis": [ "point": [
0,
0,
1
],
"origin": [
0.11489, 0.11489,
-0.050800000000000005, -0.050800000000000005,
0.050800000000000005 0.050800000000000005
@ -2013,17 +1998,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
1,
0, 0,
0 -1,
-0
], ],
"yAxis": [ "point": [
-0,
0,
1
],
"origin": [
0.19623, 0.19623,
-0.0762, -0.0762,
0.050800000000000005 0.050800000000000005
@ -2033,17 +2013,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
0,
1, 1,
0
],
"yAxis": [
-0,
0, 0,
1 -0
], ],
"origin": [ "point": [
0.2413, 0.2413,
-0.06985000000000001, -0.06985000000000001,
0.050800000000000005 0.050800000000000005
@ -2053,17 +2028,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
0,
1, 1,
0, -0
0
], ],
"yAxis": [ "point": [
0,
-0,
-1
],
"origin": [
0.19823, 0.19823,
-0.0635, -0.0635,
0.050800000000000005 0.050800000000000005
@ -2073,17 +2043,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
0.5735800000000001,
0.81915, 0.81915,
-0.5735800000000001,
0 0
], ],
"yAxis": [ "point": [
0,
0,
-1
],
"origin": [
0.10982, 0.10982,
-0.03175, -0.03175,
0.050800000000000005 0.050800000000000005
@ -2093,17 +2058,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
0.90631,
0.4226200000000001, 0.4226200000000001,
0 -0.90631,
-0
], ],
"yAxis": [ "point": [
-0,
0,
1
],
"origin": [
0.10533, 0.10533,
0.01905, 0.01905,
0.050800000000000005 0.050800000000000005
@ -2113,17 +2073,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
1,
0, 0,
0 -1,
-0
], ],
"yAxis": [ "point": [
-0,
0,
1
],
"origin": [
0.19374, 0.19374,
0.0381, 0.0381,
0.050800000000000005 0.050800000000000005
@ -2133,17 +2088,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
0,
1, 1,
0
],
"yAxis": [
-0,
0, 0,
1 -0
], ],
"origin": [ "point": [
0.2413, 0.2413,
0.04445, 0.04445,
0.050800000000000005 0.050800000000000005
@ -2153,17 +2103,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
0,
1, 1,
0, -0
0
], ],
"yAxis": [ "point": [
0,
-0,
-1
],
"origin": [
0.19234, 0.19234,
0.050800000000000005, 0.050800000000000005,
0.050800000000000005 0.050800000000000005
@ -2173,17 +2118,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
-0.4226200000000001,
0.90631, 0.90631,
0.4226200000000001,
0 0
], ],
"yAxis": [ "point": [
0,
0,
-1
],
"origin": [
0.11614, 0.11614,
0.0381, 0.0381,
0.050800000000000005 0.050800000000000005
@ -2193,17 +2133,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
0,
1, 1,
0, -0
0
], ],
"yAxis": [ "point": [
0,
-0,
-1
],
"origin": [
0.04445, 0.04445,
0.0254, 0.0254,
0.050800000000000005 0.050800000000000005
@ -2213,17 +2148,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
-1,
0, 0,
1,
0 0
], ],
"yAxis": [ "point": [
0,
0,
-1
],
"origin": [
0, 0,
0.0127, 0.0127,
0.050800000000000005 0.050800000000000005
@ -2233,17 +2163,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
1,
0, 0,
0
],
"yAxis": [
0, 0,
1, 1
0
], ],
"origin": [ "point": [
0, 0,
0, 0,
0 0
@ -2253,17 +2178,12 @@
{ {
"type": "plane", "type": "plane",
"plane": { "plane": {
"xAxis": [ "normal": [
1,
0, 0,
-0 0,
1
], ],
"yAxis": [ "point": [
-0,
1,
0
],
"origin": [
0, 0,
0, 0,
0.1016 0.1016
@ -2271,7 +2191,7 @@
} }
} }
], ],
"curves3D": [ "curves": [
{ {
"type": "line", "type": "line",
"line": { "line": {

View File

@ -1,7 +1,7 @@
ISO-10303-21; ISO-10303-21;
HEADER; HEADER;
FILE_DESCRIPTION((('zoo.dev export')), '2;1'); FILE_DESCRIPTION((('kittycad.io export')), '2;1');
FILE_NAME('dump.step', '1970-01-01T00:00:00.0+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown'); FILE_NAME('dump.step', '1970-01-01T00:00:00.0+00:00', ('Author unknown'), ('Organization unknown'), 'kittycad.io beta', 'kittycad.io', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF')); FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC; ENDSEC;
DATA; DATA;
@ -384,23 +384,23 @@ DATA;
#368 = ORIENTED_EDGE('NONE', *, *, #269, .T.); #368 = ORIENTED_EDGE('NONE', *, *, #269, .T.);
#369 = EDGE_LOOP('NONE', (#355, #356, #357, #358, #359, #360, #361, #362, #363, #364, #365, #366, #367, #368)); #369 = EDGE_LOOP('NONE', (#355, #356, #357, #358, #359, #360, #361, #362, #363, #364, #365, #366, #367, #368));
#370 = CARTESIAN_POINT('NONE', (0, -0.0127, 0.0508)); #370 = CARTESIAN_POINT('NONE', (0, -0.0127, 0.0508));
#371 = DIRECTION('NONE', (-1, -0, 0)); #371 = DIRECTION('NONE', (-1, 0, -0));
#372 = AXIS2_PLACEMENT_3D('NONE', #370, #371, $); #372 = AXIS2_PLACEMENT_3D('NONE', #370, #371, $);
#373 = PLANE('NONE', #372); #373 = PLANE('NONE', #372);
#374 = CARTESIAN_POINT('NONE', (0.039306734695977924, -0.025399999999999995, 0.0508)); #374 = CARTESIAN_POINT('NONE', (0.039306734695977924, -0.025399999999999995, 0.0508));
#375 = DIRECTION('NONE', (0, -1, 0)); #375 = DIRECTION('NONE', (0, -1, -0));
#376 = AXIS2_PLACEMENT_3D('NONE', #374, #375, $); #376 = AXIS2_PLACEMENT_3D('NONE', #374, #375, $);
#377 = PLANE('NONE', #376); #377 = PLANE('NONE', #376);
#378 = CARTESIAN_POINT('NONE', (0.11488842876320533, -0.05079999999999996, 0.05079999999999999)); #378 = CARTESIAN_POINT('NONE', (0.11488842876320533, -0.05079999999999996, 0.05079999999999999));
#379 = DIRECTION('NONE', (-0.5735764363510459, -0.8191520442889919, 0)); #379 = DIRECTION('NONE', (-0.5735764363510459, -0.819152044288992, 0));
#380 = AXIS2_PLACEMENT_3D('NONE', #378, #379, $); #380 = AXIS2_PLACEMENT_3D('NONE', #378, #379, $);
#381 = PLANE('NONE', #380); #381 = PLANE('NONE', #380);
#382 = CARTESIAN_POINT('NONE', (0.19623169406722757, -0.07619999999999999, 0.0508)); #382 = CARTESIAN_POINT('NONE', (0.19623169406722757, -0.07619999999999999, 0.0508));
#383 = DIRECTION('NONE', (0, -1, 0)); #383 = DIRECTION('NONE', (0, -1, -0));
#384 = AXIS2_PLACEMENT_3D('NONE', #382, #383, $); #384 = AXIS2_PLACEMENT_3D('NONE', #382, #383, $);
#385 = PLANE('NONE', #384); #385 = PLANE('NONE', #384);
#386 = CARTESIAN_POINT('NONE', (0.2413, -0.06985, 0.0508)); #386 = CARTESIAN_POINT('NONE', (0.2413, -0.06985, 0.0508));
#387 = DIRECTION('NONE', (1, -0, 0)); #387 = DIRECTION('NONE', (1, 0, -0));
#388 = AXIS2_PLACEMENT_3D('NONE', #386, #387, $); #388 = AXIS2_PLACEMENT_3D('NONE', #386, #387, $);
#389 = PLANE('NONE', #388); #389 = PLANE('NONE', #388);
#390 = CARTESIAN_POINT('NONE', (0.19823384137660915, -0.0635, 0.0508)); #390 = CARTESIAN_POINT('NONE', (0.19823384137660915, -0.0635, 0.0508));
@ -408,19 +408,19 @@ DATA;
#392 = AXIS2_PLACEMENT_3D('NONE', #390, #391, $); #392 = AXIS2_PLACEMENT_3D('NONE', #390, #391, $);
#393 = PLANE('NONE', #392); #393 = PLANE('NONE', #392);
#394 = CARTESIAN_POINT('NONE', (0.10982398353915601, -0.03174999999999997, 0.0508)); #394 = CARTESIAN_POINT('NONE', (0.10982398353915601, -0.03174999999999997, 0.0508));
#395 = DIRECTION('NONE', (0.5735764363510459, 0.8191520442889917, -0)); #395 = DIRECTION('NONE', (0.573576436351046, 0.8191520442889918, -0));
#396 = AXIS2_PLACEMENT_3D('NONE', #394, #395, $); #396 = AXIS2_PLACEMENT_3D('NONE', #394, #395, $);
#397 = PLANE('NONE', #396); #397 = PLANE('NONE', #396);
#398 = CARTESIAN_POINT('NONE', (0.105333141160801, 0.019049999999999987, 0.0508)); #398 = CARTESIAN_POINT('NONE', (0.105333141160801, 0.019049999999999987, 0.0508));
#399 = DIRECTION('NONE', (0.4226182617406993, -0.90630778703665, 0)); #399 = DIRECTION('NONE', (0.4226182617406993, -0.90630778703665, -0));
#400 = AXIS2_PLACEMENT_3D('NONE', #398, #399, $); #400 = AXIS2_PLACEMENT_3D('NONE', #398, #399, $);
#401 = PLANE('NONE', #400); #401 = PLANE('NONE', #400);
#402 = CARTESIAN_POINT('NONE', (0.19374299899825406, 0.0381, 0.0508)); #402 = CARTESIAN_POINT('NONE', (0.19374299899825406, 0.0381, 0.0508));
#403 = DIRECTION('NONE', (0, -1, 0)); #403 = DIRECTION('NONE', (0, -1, -0));
#404 = AXIS2_PLACEMENT_3D('NONE', #402, #403, $); #404 = AXIS2_PLACEMENT_3D('NONE', #402, #403, $);
#405 = PLANE('NONE', #404); #405 = PLANE('NONE', #404);
#406 = CARTESIAN_POINT('NONE', (0.2413, 0.044449999999999996, 0.0508)); #406 = CARTESIAN_POINT('NONE', (0.2413, 0.044449999999999996, 0.0508));
#407 = DIRECTION('NONE', (1, -0, 0)); #407 = DIRECTION('NONE', (1, 0, -0));
#408 = AXIS2_PLACEMENT_3D('NONE', #406, #407, $); #408 = AXIS2_PLACEMENT_3D('NONE', #406, #407, $);
#409 = PLANE('NONE', #408); #409 = PLANE('NONE', #408);
#410 = CARTESIAN_POINT('NONE', (0.19233523789047138, 0.0508, 0.0508)); #410 = CARTESIAN_POINT('NONE', (0.19233523789047138, 0.0508, 0.0508));
@ -436,7 +436,7 @@ DATA;
#420 = AXIS2_PLACEMENT_3D('NONE', #418, #419, $); #420 = AXIS2_PLACEMENT_3D('NONE', #418, #419, $);
#421 = PLANE('NONE', #420); #421 = PLANE('NONE', #420);
#422 = CARTESIAN_POINT('NONE', (0, 0.0127, 0.0508)); #422 = CARTESIAN_POINT('NONE', (0, 0.0127, 0.0508));
#423 = DIRECTION('NONE', (-1, -0, 0)); #423 = DIRECTION('NONE', (-1, 0, -0));
#424 = AXIS2_PLACEMENT_3D('NONE', #422, #423, $); #424 = AXIS2_PLACEMENT_3D('NONE', #422, #423, $);
#425 = PLANE('NONE', #424); #425 = PLANE('NONE', #424);
#426 = CARTESIAN_POINT('NONE', (0, 0, -0)); #426 = CARTESIAN_POINT('NONE', (0, 0, -0));
@ -475,7 +475,7 @@ DATA;
#459 = ADVANCED_FACE('NONE', (#458), #421, .T.); #459 = ADVANCED_FACE('NONE', (#458), #421, .T.);
#460 = FACE_OUTER_BOUND('NONE', #339, .T.); #460 = FACE_OUTER_BOUND('NONE', #339, .T.);
#461 = ADVANCED_FACE('NONE', (#460), #425, .T.); #461 = ADVANCED_FACE('NONE', (#460), #425, .T.);
#462 = FACE_OUTER_BOUND('NONE', #354, .F.); #462 = FACE_OUTER_BOUND('NONE', #354, .T.);
#463 = ADVANCED_FACE('NONE', (#462), #429, .F.); #463 = ADVANCED_FACE('NONE', (#462), #429, .F.);
#464 = FACE_OUTER_BOUND('NONE', #369, .T.); #464 = FACE_OUTER_BOUND('NONE', #369, .T.);
#465 = ADVANCED_FACE('NONE', (#464), #433, .T.); #465 = ADVANCED_FACE('NONE', (#464), #433, .T.);

View File

@ -100,15 +100,15 @@ endfacet
facet normal 0.57357645 0 0.819152 facet normal 0.57357645 0 0.819152
outer loop outer loop
vertex 3.4311862 -0 -0.625 vertex 3.4311862 -0 -0.625
vertex 2.5385938 -0 0
vertex 2.5385938 -4 0 vertex 2.5385938 -4 0
vertex 3.4311862 -4 -0.625
endloop endloop
endfacet endfacet
facet normal 0.5735763 0 0.8191522 facet normal 0.57357645 -0 0.819152
outer loop outer loop
vertex 4.323779 -0 -1.25
vertex 3.4311862 -0 -0.625
vertex 3.4311862 -4 -0.625 vertex 3.4311862 -4 -0.625
vertex 3.4311862 -0 -0.625
vertex 2.5385938 -4 0
endloop endloop
endfacet endfacet
facet normal 0.57357645 -0 0.819152 facet normal 0.57357645 -0 0.819152
@ -118,11 +118,11 @@ facet normal 0.57357645 -0 0.819152
vertex 4.323779 -0 -1.25 vertex 4.323779 -0 -1.25
endloop endloop
endfacet endfacet
facet normal 0.57357645 -0 0.819152 facet normal 0.5735763 0 0.8191522
outer loop outer loop
vertex 2.5385938 -4 0
vertex 3.4311862 -0 -0.625 vertex 3.4311862 -0 -0.625
vertex 2.5385938 -0 0 vertex 3.4311862 -4 -0.625
vertex 4.323779 -0 -1.25
endloop endloop
endfacet endfacet
facet normal 0.42261824 0 -0.9063078 facet normal 0.42261824 0 -0.9063078
@ -142,29 +142,29 @@ endfacet
facet normal 0.42261824 0 -0.9063078 facet normal 0.42261824 0 -0.9063078
outer loop outer loop
vertex 3.342784 -0 0.375 vertex 3.342784 -0 0.375
vertex 3.342784 -4 0.375 vertex 4.146974 -0 0.75
vertex 2.5385938 -0 0 vertex 4.146974 -4 0.75
endloop endloop
endfacet endfacet
facet normal 0.42261833 0 -0.90630776 facet normal 0.42261833 0 -0.90630776
outer loop outer loop
vertex 4.146974 -0 0.75
vertex 5.755354 -4 1.5
vertex 4.146974 -4 0.75
endloop
endfacet
facet normal 0.42261824 0 -0.9063078
outer loop
vertex 4.146974 -4 0.75
vertex 3.342784 -0 0.375
vertex 4.146974 -0 0.75
endloop
endfacet
facet normal 0.42261833 0 -0.90630776
outer loop
vertex 5.755354 -4 1.5
vertex 4.146974 -0 0.75 vertex 4.146974 -0 0.75
vertex 5.755354 -0 1.5 vertex 5.755354 -0 1.5
vertex 5.755354 -4 1.5
endloop
endfacet
facet normal 0.42261824 0 -0.9063078
outer loop
vertex 3.342784 -4 0.375
vertex 2.5385938 -0 0
vertex 3.342784 -0 0.375
endloop
endfacet
facet normal 0.42261833 0 -0.90630776
outer loop
vertex 5.755354 -4 1.5
vertex 4.146974 -4 0.75
vertex 4.146974 -0 0.75
endloop endloop
endfacet endfacet
facet normal 0 0 -1 facet normal 0 0 -1
@ -258,13 +258,6 @@ facet normal 0 1 -0
vertex 3.5 -0 1 vertex 3.5 -0 1
endloop endloop
endfacet endfacet
facet normal 0 1 0
outer loop
vertex 4.146974 -0 0.75
vertex 3.342784 -0 0.375
vertex 3.5 -0 1
endloop
endfacet
facet normal 0 1 0 facet normal 0 1 0
outer loop outer loop
vertex 3.4311862 -0 -0.625 vertex 3.4311862 -0 -0.625
@ -272,39 +265,67 @@ facet normal 0 1 0
vertex 3.0950184 -0 -1 vertex 3.0950184 -0 -1
endloop endloop
endfacet endfacet
facet normal 0 1 0
outer loop
vertex 3.342784 -0 0.375
vertex 3.5 -0 1
vertex 4.146974 -0 0.75
endloop
endfacet
facet normal 0 0.99999994 0 facet normal 0 0.99999994 0
outer loop outer loop
vertex 4.146974 -0 0.75 vertex 4.323779 -0 -1.25
vertex 5.644507 -0 2 vertex 5.9513144 -0 -3
vertex 5.755354 -0 1.5 vertex 3.0950184 -0 -1
endloop
endfacet
facet normal -0 1 0
outer loop
vertex 0 -0 1
vertex 3.5 -0 1
vertex 2.5385938 -0 0
endloop endloop
endfacet endfacet
facet normal 0 1 0 facet normal 0 1 0
outer loop outer loop
vertex 0 -0 1 vertex 0 -0 -1
vertex 2.5385938 -0 0 vertex 2.5385938 -0 0
vertex 3.0950184 -0 -1
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 0 -0 -1
vertex 0 -0 0 vertex 0 -0 0
vertex 2.5385938 -0 0
endloop endloop
endfacet endfacet
facet normal -0 1 0 facet normal 0 0.99999994 -0
outer loop outer loop
vertex 5.644507 -0 2 vertex 9.5 -0 -3
vertex 9.5 -0 2 vertex 6.108964 -0 -2.5
vertex 5.755354 -0 1.5 vertex 9.5 -0 -2.5
endloop endloop
endfacet endfacet
facet normal 0 1 0 facet normal 0 1 0
outer loop outer loop
vertex 9.5 -0 2 vertex 9.5 -0 -3
vertex 9.5 -0 1.5 vertex 5.9513144 -0 -3
vertex 6.108964 -0 -2.5
endloop
endfacet
facet normal 0 1 -0
outer loop
vertex 5.9513144 -0 -3
vertex 4.323779 -0 -1.25
vertex 6.108964 -0 -2.5
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 5.644507 -0 2
vertex 5.755354 -0 1.5 vertex 5.755354 -0 1.5
vertex 4.146974 -0 0.75
endloop
endfacet
facet normal 0 0.99999994 -0
outer loop
vertex 3.0950184 -0 -1
vertex 2.5385938 -0 0
vertex 3.4311862 -0 -0.625
endloop endloop
endfacet endfacet
facet normal 0 1 -0 facet normal 0 1 -0
@ -314,53 +335,32 @@ facet normal 0 1 -0
vertex 5.644507 -0 2 vertex 5.644507 -0 2
endloop endloop
endfacet endfacet
facet normal 0 0.99999994 0 facet normal 0 1 -0
outer loop outer loop
vertex 2.5385938 -0 0 vertex 9.5 -0 1.5
vertex 3.4311862 -0 -0.625 vertex 5.755354 -0 1.5
vertex 3.0950184 -0 -1 vertex 9.5 -0 2
endloop endloop
endfacet endfacet
facet normal 0 0.99999994 0 facet normal 0 1 -0
outer loop outer loop
vertex 4.323779 -0 -1.25 vertex 5.755354 -0 1.5
vertex 5.9513144 -0 -3 vertex 5.644507 -0 2
vertex 3.0950184 -0 -1 vertex 9.5 -0 2
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 6.108964 -0 -2.5
vertex 5.9513144 -0 -3
vertex 4.323779 -0 -1.25
endloop
endfacet
facet normal 0 0.99999994 0
outer loop
vertex 9.5 -0 -2.5
vertex 9.5 -0 -3
vertex 6.108964 -0 -2.5
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 6.108964 -0 -2.5
vertex 9.5 -0 -3
vertex 5.9513144 -0 -3
endloop endloop
endfacet endfacet
facet normal 0 1 0 facet normal 0 1 0
outer loop outer loop
vertex 2.5385938 -0 0 vertex 2.5385938 -0 0
vertex 3.0950184 -0 -1
vertex 0 -0 -1
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 0 -0 -1
vertex 0 -0 0 vertex 0 -0 0
vertex 0 -0 1
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 3.5 -0 1
vertex 2.5385938 -0 0 vertex 2.5385938 -0 0
vertex 0 -0 1
endloop endloop
endfacet endfacet
facet normal -0 -1 0 facet normal -0 -1 0

View File

@ -1,5 +1,7 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { secrets } from './secrets' import { secrets } from './secrets'
import { EngineCommand } from '../../src/lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid'
import { getUtils } from './test-utils' import { getUtils } from './test-utils'
import waitOn from 'wait-on' import waitOn from 'wait-on'
import { Themes } from '../../src/lib/theme' import { Themes } from '../../src/lib/theme'
@ -51,38 +53,40 @@ test('Basic sketch', async ({ page }) => {
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.waitForDefaultPlanesVisibilityChange()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible() await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button // click on "Start Sketch" button
await u.clearCommandLogs() await u.clearCommandLogs()
await u.doAndWaitForImageDiff( await Promise.all([
() => page.getByRole('button', { name: 'Start Sketch' }).click(), u.doAndWaitForImageDiff(
200 () => page.getByRole('button', { name: 'Start Sketch' }).click(),
) 200
),
u.waitForDefaultPlanesVisibilityChange(),
])
// select a plane // select a plane
await page.mouse.click(700, 200) await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter')
await u.waitForCmdReceive('set_tool')
await expect(page.locator('.cm-content')).toHaveText( await u.doAndWaitForCmd(
`const part001 = startSketchOn('-XZ')` () => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
) )
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await u.doAndWaitForCmd(
const startAt = '[23.89, -32.23]' () => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10),
await expect(page.locator('.cm-content')) 'mouse_click',
.toHaveText(`const part001 = startSketchOn('-XZ') false
|> startProfileAt(${startAt}, %)`) )
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num = 24.11 const startAt = '[18.26, -24.63]'
const num = '18.43'
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %) |> startProfileAt(${startAt}, %)
@ -93,22 +97,27 @@ test('Basic sketch', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${num}, 0], %) |> line([${num}, 0], %)
|> line([0, ${num + 0.01}], %)`) |> line([0, ${num}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${num}, 0], %) |> line([${num}, 0], %)
|> line([0, ${num + 0.01}], %) |> line([0, ${num}], %)
|> line([-48, 0], %)`) |> line([-36.69, 0], %)`)
// deselect line tool // deselect line tool
await page.getByRole('button', { name: 'Line' }).click() await u.doAndWaitForCmd(
await page.waitForTimeout(100) () => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
)
// click between first two clicks to get center of the line // click between first two clicks to get center of the line
await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10) await u.doAndWaitForCmd(
await page.waitForTimeout(100) () => page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10),
'select_with_point'
)
await u.closeDebugPanel()
// hold down shift // hold down shift
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
@ -124,7 +133,7 @@ test('Basic sketch', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %) |> startProfileAt(${startAt}, %)
|> line({ to: [${num}, 0], tag: 'seg01' }, %) |> line({ to: [${num}, 0], tag: 'seg01' }, %)
|> line([0, ${num + 0.01}], %) |> line([0, ${num}], %)
|> angledLine([180, segLen('seg01', %)], %)`) |> angledLine([180, segLen('seg01', %)], %)`)
}) })
@ -197,7 +206,7 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
test('executes on load', async ({ page, context }) => { test('executes on load', async ({ page, context }) => {
const u = getUtils(page) const u = getUtils(page)
await context.addInitScript(async () => { await context.addInitScript(async (token) => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`const part001 = startSketchOn('-XZ') `const part001 = startSketchOn('-XZ')
@ -262,41 +271,59 @@ test('Can create sketches on all planes and their back sides', async ({
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.waitForDefaultPlanesVisibilityChange()
const camPos: [number, number, number] = [100, 100, 100] const camCmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 15, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
vantage: { x: 30, y: 30, z: 30 },
},
}
const TestSinglePlane = async ({ const TestSinglePlane = async ({
viewCmd, viewCmd,
expectedCode, expectedCode,
clickCoords, clickCoords,
}: { }: {
viewCmd: [number, number, number] viewCmd: EngineCommand
expectedCode: string expectedCode: string
clickCoords: { x: number; y: number } clickCoords: { x: number; y: number }
}) => { }) => {
await u.openDebugPanel() await u.openDebugPanel()
await u.sendCustomCmd(viewCmd)
await u.updateCamPosition(viewCmd)
await u.clearCommandLogs() await u.clearCommandLogs()
// await page.waitForTimeout(200)
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel() await u.closeDebugPanel()
await page.mouse.click(clickCoords.x, clickCoords.y) await page.mouse.click(clickCoords.x, clickCoords.y)
await page.waitForTimeout(300) // wait for animation await u.openDebugPanel()
await expect(page.getByRole('button', { name: 'Line' })).toBeVisible() await expect(page.getByRole('button', { name: 'Line' })).toBeVisible()
// draw a line // draw a line
const startXPx = 600 const startXPx = 600
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Line' }).click()
await u.waitForCmdReceive('set_tool')
await u.clearCommandLogs()
await u.closeDebugPanel() await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await u.openDebugPanel()
await u.waitForCmdReceive('mouse_click')
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await u.openDebugPanel()
await expect(page.locator('.cm-content')).toHaveText(expectedCode) await expect(page.locator('.cm-content')).toHaveText(expectedCode)
await page.getByRole('button', { name: 'Line' }).click() await page.getByRole('button', { name: 'Line' }).click()
await u.openAndClearDebugPanel() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
@ -306,41 +333,51 @@ test('Can create sketches on all planes and their back sides', async ({
const codeTemplate = ( const codeTemplate = (
plane = 'XY', plane = 'XY',
rounded = false, sign = ''
otherThing = '1'
) => `const part001 = startSketchOn('${plane}') ) => `const part001 = startSketchOn('${plane}')
|> startProfileAt([28.9${otherThing}, -39${rounded ? '' : '.01'}], %)` |> startProfileAt([${sign}6.88, -9.29], %)
|> line([${sign}6.95, 0], %)`
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camPos, viewCmd: camCmd,
expectedCode: codeTemplate('XY'), expectedCode: codeTemplate('XY'),
clickCoords: { x: 600, y: 388 }, // red plane clickCoords: { x: 700, y: 350 }, // red plane
// clickCoords: { x: 600, y: 400 }, // red plane // clicks grid helper and that causes problems, should fix so that these coords work too.
}) })
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camPos, viewCmd: camCmd,
expectedCode: codeTemplate('YZ', true), expectedCode: codeTemplate('YZ'),
clickCoords: { x: 700, y: 300 }, // green plane clickCoords: { x: 1000, y: 200 }, // green plane
}) })
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camPos, viewCmd: camCmd,
expectedCode: codeTemplate('XZ'), expectedCode: codeTemplate('XZ', '-'),
clickCoords: { x: 700, y: 80 }, // blue plane clickCoords: { x: 630, y: 130 }, // blue plane
}) })
const camCmdBackSide: [number, number, number] = [-100, -100, -100]
// new camera angle to click the back side of all three planes
const camCmdBackSide: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: -15, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
vantage: { x: -30, y: -30, z: -30 },
},
}
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camCmdBackSide, viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-XY', false, '3'), expectedCode: codeTemplate('-XY', '-'),
clickCoords: { x: 601, y: 118 }, // back of red plane clickCoords: { x: 705, y: 136 }, // back of red plane
}) })
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camCmdBackSide, viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-YZ'), expectedCode: codeTemplate('-YZ', '-'),
clickCoords: { x: 730, y: 219 }, // back of green plane clickCoords: { x: 1000, y: 350 }, // back of green plane
}) })
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camCmdBackSide, viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-XZ', true), expectedCode: codeTemplate('-XZ'),
clickCoords: { x: 680, y: 427 }, // back of blue plane clickCoords: { x: 600, y: 400 }, // back of blue plane
}) })
}) })
@ -350,6 +387,7 @@ test('Auto complete works', async ({ page }) => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.waitForDefaultPlanesVisibilityChange()
// this test might be brittle as we add and remove functions // this test might be brittle as we add and remove functions
// but should also be easy to update. // but should also be easy to update.
@ -367,7 +405,6 @@ test('Auto complete works', async ({ page }) => {
await page.keyboard.type(' |> startProfi') await page.keyboard.type(' |> startProfi')
// expect there be a single auto complete option that we can just hit enter on // expect there be a single auto complete option that we can just hit enter on
await expect(page.locator('.cm-completionLabel')).toBeVisible() await expect(page.locator('.cm-completionLabel')).toBeVisible()
await page.waitForTimeout(100)
await page.keyboard.press('Enter') // accepting the auto complete, not a new line await page.keyboard.press('Enter') // accepting the auto complete, not a new line
await page.keyboard.type('([0,0], %)') await page.keyboard.type('([0,0], %)')
@ -441,36 +478,38 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.waitForDefaultPlanesVisibilityChange()
const xAxisClick = () => const xAxisClick = () => page.mouse.click(700, 250)
page.mouse.click(700, 250).then(() => page.waitForTimeout(100)) const emptySpaceClick = () => page.mouse.click(700, 300)
const emptySpaceClick = () => const topHorzSegmentClick = () => page.mouse.click(700, 285)
page.mouse.click(728, 343).then(() => page.waitForTimeout(100)) const bottomHorzSegmentClick = () => page.mouse.click(750, 393)
const topHorzSegmentClick = () =>
page.mouse.click(709, 289).then(() => page.waitForTimeout(100))
const bottomHorzSegmentClick = () =>
page.mouse.click(767, 396).then(() => page.waitForTimeout(100))
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
// select a plane // select a plane
await page.mouse.click(700, 200) await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter')
await page.waitForTimeout(700) // wait for animation await u.waitForCmdReceive('set_tool')
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
)
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await u.doAndWaitForCmd(
const startAt = '[23.89, -32.23]' () => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10),
await expect(page.locator('.cm-content')) 'mouse_click',
.toHaveText(`const part001 = startSketchOn('-XZ') false
|> startProfileAt(${startAt}, %)`) )
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
const num = 24.11 const startAt = '[18.26, -24.63]'
const num2 = '48' const num = '18.43'
const num2 = '36.69'
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %) |> startProfileAt(${startAt}, %)
@ -481,17 +520,20 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${num}, 0], %) |> line([${num}, 0], %)
|> line([0, ${num + 0.01}], %)`) |> line([0, ${num}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %) |> startProfileAt(${startAt}, %)
|> line([${num}, 0], %) |> line([${num}, 0], %)
|> line([0, ${num + 0.01}], %) |> line([0, ${num}], %)
|> line([-${num2}, 0], %)`) |> line([-${num2}, 0], %)`)
// deselect line tool // deselect line tool
await page.getByRole('button', { name: 'Line' }).click() await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
)
await u.closeDebugPanel() await u.closeDebugPanel()
const selectionSequence = async () => { const selectionSequence = async () => {
@ -513,72 +555,79 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
// now check clicking works including axis // now check clicking works including axis
// click a segment hold shift and click an axis, see that a relevant constraint is enabled // click a segment hold shift and click an axis, see that a relevant constraint is enabled
await topHorzSegmentClick() await u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false)
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
const absYButton = page.getByRole('button', { name: 'ABS Y' }) const absYButton = page.getByRole('button', { name: 'ABS Y' })
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await xAxisClick() await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await absYButton.and(page.locator(':not([disabled])')).waitFor() await absYButton.and(page.locator(':not([disabled])')).waitFor()
await expect(absYButton).not.toBeDisabled() await expect(absYButton).not.toBeDisabled()
// clear selection by clicking on nothing // clear selection by clicking on nothing
await emptySpaceClick() await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
// same selection but click the axis first // same selection but click the axis first
await xAxisClick() await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await topHorzSegmentClick() await u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false)
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await expect(absYButton).not.toBeDisabled() await expect(absYButton).not.toBeDisabled()
// clear selection by clicking on nothing // clear selection by clicking on nothing
await emptySpaceClick() await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
// check the same selection again by putting cursor in code first then selecting axis // check the same selection again by putting cursor in code first then selecting axis
await page.getByText(` |> line([-${num2}, 0], %)`).click() await u.doAndWaitForCmd(
() => page.getByText(` |> line([-${num2}, 0], %)`).click(),
'select_clear',
false
)
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await xAxisClick() await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await expect(absYButton).not.toBeDisabled() await expect(absYButton).not.toBeDisabled()
// clear selection by clicking on nothing // clear selection by clicking on nothing
await emptySpaceClick() await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
// select segment in editor than another segment in scene and check there are two cursors // select segment in editor than another segment in scene and check there are two cursors
await page.getByText(` |> line([-${num2}, 0], %)`).click() await u.doAndWaitForCmd(
await page.waitForTimeout(300) () => page.getByText(` |> line([-${num2}, 0], %)`).click(),
'select_clear',
false
)
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await expect(page.locator('.cm-cursor')).toHaveCount(1) await expect(page.locator('.cm-cursor')).toHaveCount(1)
await bottomHorzSegmentClick() await u.doAndWaitForCmd(bottomHorzSegmentClick, 'select_with_point', false) // another segment, bottom one
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await expect(page.locator('.cm-cursor')).toHaveCount(2) await expect(page.locator('.cm-cursor')).toHaveCount(2)
// clear selection by clicking on nothing // clear selection by clicking on nothing
await emptySpaceClick() await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
} }
await selectionSequence() await selectionSequence()
// hovering in fresh sketch worked, lets try exiting and re-entering // hovering in fresh sketch worked, lets try exiting and re-entering
await u.openAndClearDebugPanel() await u.doAndWaitForCmd(
await page.getByRole('button', { name: 'Exit Sketch' }).click() () => page.getByRole('button', { name: 'Exit Sketch' }).click(),
await page.waitForTimeout(200) 'edit_mode_exit'
)
// wait for execution done // wait for execution done
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// select a line // select a line
await topHorzSegmentClick() await u.doAndWaitForCmd(topHorzSegmentClick, 'select_clear', false)
await page.waitForTimeout(200)
// enter sketch again // enter sketch again
await page.getByRole('button', { name: 'Edit Sketch' }).click() await u.doAndWaitForCmd(
await page.waitForTimeout(700) // wait for animation () => page.getByRole('button', { name: 'Start Sketch' }).click(),
'edit_mode_enter',
false
)
// hover again and check it works // hover again and check it works
await selectionSequence() await selectionSequence()
@ -648,8 +697,6 @@ test('Can extrude from the command bar', async ({ page, context }) => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
let cmdSearchBar = page.getByPlaceholder('Search commands') let cmdSearchBar = page.getByPlaceholder('Search commands')
await page.keyboard.press('Meta+K') await page.keyboard.press('Meta+K')
@ -663,7 +710,10 @@ test('Can extrude from the command bar', async ({ page, context }) => {
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled() await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
// Click to select face and set distance // Click to select face and set distance
await u.openAndClearDebugPanel()
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click() 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 page.getByRole('button', { name: 'Continue' }).click()
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled() await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
@ -685,266 +735,3 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|> extrude(5, %)` |> extrude(5, %)`
) )
}) })
test('Can add multiple sketches', async ({ page }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
)
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('-XZ')`
)
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt = '[23.89, -32.23]'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num = 24.11
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)`)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num + 0.01}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20)
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num + 0.01}], %)
|> line([-48, 0], %)`
await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch)
// exit the sketch
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.updateCamPosition([0, 100, 100])
// start a new sketch
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
await page.mouse.click(673, 384)
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await u.clearAndCloseDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt2 = '[23.61, -31.85]'
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %)`.replace(/\s/g, '')
)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num2 = 23.83
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)`.replace(/\s/g, '')
)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)
|> line([0, ${num2}], %)`.replace(/\s/g, '')
)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
`${finalCodeFirstSketch}
const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)
|> line([0, ${num2}], %)
|> line([-47.44, 0], %)`.replace(/\s/g, '')
)
})
test('ProgramMemory can be serialised', async ({ page, context }) => {
const u = getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const part = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([0, 1], %)
|> line([1, 0], %)
|> line([0, -1], %)
|> close(%)
|> extrude(1, %)
|> patternLinear({
axis: [1, 0, 1],
repetitions: 3,
distance: 6
}, %)`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
const messages: string[] = []
// Listen for all console events and push the message text to an array
page.on('console', (message) => messages.push(message.text()))
await u.waitForAuthSkipAppStart()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
const forbiddenMessages = ['cannot serialize tagged newtype variant']
forbiddenMessages.forEach((forbiddenMessage) => {
messages.forEach((message) => {
expect(message).not.toContain(forbiddenMessage)
})
})
})
test('edit selections', async ({ page, context }) => {
const u = getUtils(page)
const selectionsSnippets = {
extrudeAndEditBlocked: '|> startProfileAt([10.81, 32.99], %)',
extrudeAndEditBlockedInFunction: '|> startProfileAt(pos, %)',
extrudeAndEditAllowed: '|> startProfileAt([15.72, 4.7], %)',
editOnly: '|> startProfileAt([15.79, -14.6], %)',
}
await context.addInitScript(
async ({
extrudeAndEditBlocked,
extrudeAndEditBlockedInFunction,
extrudeAndEditAllowed,
editOnly,
}: any) => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('-XZ')
${extrudeAndEditBlocked}
|> line([25.96, 2.93], %)
|> line([5.25, -5.72], %)
|> line([-2.01, -10.35], %)
|> line([-27.65, -2.78], %)
|> close(%)
|> extrude(5, %)
const part002 = startSketchOn('-XZ')
${extrudeAndEditAllowed}
|> line([10.32, 6.47], %)
|> line([9.71, -6.16], %)
|> line([-3.08, -9.86], %)
|> line([-12.02, -1.54], %)
|> close(%)
const part003 = startSketchOn('-XZ')
${editOnly}
|> line([27.55, -1.65], %)
|> line([4.95, -8], %)
|> line([-20.38, -10.12], %)
|> line([-15.79, 17.08], %)
fn yohey = (pos) => {
const part004 = startSketchOn('-XZ')
${extrudeAndEditBlockedInFunction}
|> line([27.55, -1.65], %)
|> line([4.95, -10.53], %)
|> line([-20.38, -8], %)
|> line([-15.79, 17.08], %)
return ''
}
yohey([15.79, -34.6])
`
)
},
selectionsSnippets
)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await page.getByText(selectionsSnippets.extrudeAndEditBlocked).click()
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).not.toBeVisible()
await page.getByText(selectionsSnippets.extrudeAndEditAllowed).click()
await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).not.toBeDisabled()
await page.getByText(selectionsSnippets.editOnly).click()
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).not.toBeDisabled()
await page
.getByText(selectionsSnippets.extrudeAndEditBlockedInFunction)
.click()
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).not.toBeVisible()
// selecting an editable sketch but clicking "start sktech" should start a new sketch and not edit the existing one
await page.getByText(selectionsSnippets.extrudeAndEditAllowed).click()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.mouse.click(700, 200)
// expect main content to contain `part005` i.e. started a new sketch
await expect(page.locator('.cm-content')).toHaveText(
/part005 = startSketchOn\('-XZ'\)/
)
})

View File

@ -1,5 +1,7 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { secrets } from './secrets' import { secrets } from './secrets'
import { EngineCommand } from '../../src/lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid'
import { getUtils } from './test-utils' import { getUtils } from './test-utils'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import fsp from 'fs/promises' import fsp from 'fs/promises'
@ -38,8 +40,19 @@ test('change camera, show planes', async ({ page, context }) => {
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
const camPos: [number, number, number] = [0, 85, 85] const camCmd: EngineCommand = {
await u.updateCamPosition(camPos) type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
vantage: { x: 0, y: 85, z: 85 },
},
}
await u.sendCustomCmd(camCmd)
await u.waitForCmdReceive('default_camera_look_at')
// rotate // rotate
await u.closeDebugPanel() await u.closeDebugPanel()
@ -49,11 +62,13 @@ test('change camera, show planes', async ({ page, context }) => {
await page.mouse.up({ button: 'right' }) await page.mouse.up({ button: 'right' })
await u.openDebugPanel() await u.openDebugPanel()
await u.waitForCmdReceive('camera_drag_end')
await page.waitForTimeout(500) await page.waitForTimeout(500)
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel() await u.closeDebugPanel()
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
@ -62,8 +77,10 @@ test('change camera, show planes', async ({ page, context }) => {
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.updateCamPosition(camPos) await u.sendCustomCmd(camCmd)
await u.waitForCmdReceive('default_camera_look_at')
await u.clearCommandLogs() await u.clearCommandLogs()
await u.closeDebugPanel() await u.closeDebugPanel()
@ -76,10 +93,12 @@ test('change camera, show planes', async ({ page, context }) => {
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await u.openDebugPanel() await u.openDebugPanel()
await u.waitForCmdReceive('camera_drag_end')
await page.waitForTimeout(300) await page.waitForTimeout(300)
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel() await u.closeDebugPanel()
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
@ -88,8 +107,10 @@ test('change camera, show planes', async ({ page, context }) => {
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.updateCamPosition(camPos) await u.sendCustomCmd(camCmd)
await u.waitForCmdReceive('default_camera_look_at')
await u.clearCommandLogs() await u.clearCommandLogs()
await u.closeDebugPanel() await u.closeDebugPanel()
@ -98,15 +119,17 @@ test('change camera, show planes', async ({ page, context }) => {
await page.keyboard.down('Control') await page.keyboard.down('Control')
await page.mouse.move(700, 400) await page.mouse.move(700, 400)
await page.mouse.down({ button: 'right' }) await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 300) await page.mouse.move(700, 350)
await page.mouse.up({ button: 'right' }) await page.mouse.up({ button: 'right' })
await page.keyboard.up('Control') await page.keyboard.up('Control')
await u.openDebugPanel() await u.openDebugPanel()
await u.waitForCmdReceive('camera_drag_end')
await page.waitForTimeout(300) await page.waitForTimeout(300)
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel() await u.closeDebugPanel()
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
@ -141,11 +164,11 @@ const part001 = startSketchOn('-XZ')
|> xLineTo({ to: totalLen, tag: 'seg03' }, %) |> xLineTo({ to: totalLen, tag: 'seg03' }, %)
|> yLine({ length: -armThick, tag: 'seg01' }, %) |> yLine({ length: -armThick, tag: 'seg01' }, %)
|> angledLineThatIntersects({ |> angledLineThatIntersects({
angle: HALF_TURN, angle: _180,
offset: -armThick, offset: -armThick,
intersectTag: 'seg04' intersectTag: 'seg04'
}, %) }, %)
|> angledLineToY([segAng('seg04', %) + 180, ZERO], %) |> angledLineToY([segAng('seg04', %) + 180, _0], %)
|> angledLineToY({ |> angledLineToY({
angle: -bottomAng, angle: -bottomAng,
to: -totalHeightHalf - armThick, to: -totalHeightHalf - armThick,
@ -154,12 +177,12 @@ const part001 = startSketchOn('-XZ')
|> xLineTo(segEndX('seg03', %) + 0, %) |> xLineTo(segEndX('seg03', %) + 0, %)
|> yLine(-segLen('seg01', %), %) |> yLine(-segLen('seg01', %), %)
|> angledLineThatIntersects({ |> angledLineThatIntersects({
angle: HALF_TURN, angle: _180,
offset: -armThick, offset: -armThick,
intersectTag: 'seg02' intersectTag: 'seg02'
}, %) }, %)
|> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %) |> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %)
|> xLineTo(ZERO, %) |> xLineTo(_0, %)
|> close(%) |> close(%)
|> extrude(4, %)` |> extrude(4, %)`
) )
@ -168,6 +191,7 @@ const part001 = startSketchOn('-XZ')
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.waitForDefaultPlanesVisibilityChange()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.waitForCmdReceive('extrude') await u.waitForCmdReceive('extrude')
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
@ -362,118 +386,3 @@ const part001 = startSketchOn('-XZ')
}) })
} }
}) })
test('extrude on each default plane should be stable', async ({
page,
context,
}) => {
const u = getUtils(page)
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|> startProfileAt([14.06, 8.88], %)
|> line([12.98, -0.15], %)
|> line([5.56, 9.89], %)
|> line([-11.28, 8.96], %)
|> line([-10.81, -7.57], %)
|> close(%)
|> extrude(20, %)
`
await context.addInitScript(async (code) => {
localStorage.setItem('persistCode', code)
}, makeCode('XY'))
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await page.getByText('Code').click()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await page.getByText('Code').click()
const runSnapshotsForOtherPlanes = async (plane = 'XY') => {
// clear code
await u.removeCurrentCode()
// add makeCode('XZ')
await page.locator('.cm-content').fill(makeCode(plane))
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await page.getByText('Code').click()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await page.getByText('Code').click()
}
await runSnapshotsForOtherPlanes('-XY')
await runSnapshotsForOtherPlanes('XZ')
await runSnapshotsForOtherPlanes('-XZ')
await runSnapshotsForOtherPlanes('YZ')
await runSnapshotsForOtherPlanes('-YZ')
})
test('Draft segments should look right', async ({ page }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
)
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`const part001 = startSketchOn('-XZ')`
)
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt = '[23.89, -32.23]'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.move(startXPx + PUR * 20, 500 - PUR * 10)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num = 24.11
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -80,21 +80,10 @@ export function getUtils(page: Page) {
waitForAuthSkipAppStart: () => waitForPageLoad(page), waitForAuthSkipAppStart: () => waitForPageLoad(page),
removeCurrentCode: () => removeCurrentCode(page), removeCurrentCode: () => removeCurrentCode(page),
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd), sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
updateCamPosition: async (xyz: [number, number, number]) => {
const fillInput = async () => {
await page.fill('[data-testid="cam-x-position"]', String(xyz[0]))
await page.fill('[data-testid="cam-y-position"]', String(xyz[1]))
await page.fill('[data-testid="cam-z-position"]', String(xyz[2]))
}
await fillInput()
await page.waitForTimeout(100)
await fillInput()
await page.waitForTimeout(100)
await fillInput()
await page.waitForTimeout(100)
},
clearCommandLogs: () => clearCommandLogs(page), clearCommandLogs: () => clearCommandLogs(page),
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr), expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr),
waitForDefaultPlanesVisibilityChange: () =>
waitForDefaultPlanesToBeVisible(page),
openDebugPanel: () => openDebugPanel(page), openDebugPanel: () => openDebugPanel(page),
closeDebugPanel: () => closeDebugPanel(page), closeDebugPanel: () => closeDebugPanel(page),
openAndClearDebugPanel: async () => { openAndClearDebugPanel: async () => {

View File

@ -86,11 +86,13 @@ describe('ZMA (Tauri, Linux)', () => {
expect(await homeSection.getText()).toContain('project-000') expect(await homeSection.getText()).toContain('project-000')
}) })
it('opens the new file and expects a loading stream', async () => { it('opens the new file and expects an error on Linux', async () => {
const projectLink = await $('[data-testid="project-link"]') const projectLink = await $('[data-testid="project-link"]')
await click(projectLink) await click(projectLink)
const loadingText = await $('[data-testid="loading-stream"]') const error = await $('h3')
expect(await loadingText.getText()).toContain('Loading stream...') expect(await error.getText()).toContain(
"Can't find variable: RTCPeerConnection"
)
await browser.execute('window.location.href = "tauri://localhost/home"') await browser.execute('window.location.href = "tauri://localhost/home"')
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.15.1", "version": "0.14.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.10.2", "@codemirror/autocomplete": "^6.10.2",
@ -10,7 +10,7 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.17", "@headlessui/react": "^1.7.17",
"@headlessui/tailwindcss": "^0.2.0", "@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.53", "@kittycad/lib": "^0.0.46",
"@lezer/javascript": "^1.4.9", "@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1", "@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^1.2.6", "@react-hook/resize-observer": "^1.2.6",
@ -21,7 +21,6 @@
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.1", "@testing-library/user-event": "^14.5.1",
"@ts-stack/markdown": "^1.5.0", "@ts-stack/markdown": "^1.5.0",
"@tweenjs/tween.js": "^23.1.1",
"@types/node": "^16.7.13", "@types/node": "^16.7.13",
"@types/react": "^18.2.41", "@types/react": "^18.2.41",
"@types/react-dom": "^18.0.0", "@types/react-dom": "^18.0.0",
@ -46,7 +45,6 @@
"sketch-helpers": "^0.0.4", "sketch-helpers": "^0.0.4",
"swr": "^2.2.2", "swr": "^2.2.2",
"tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1", "tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1",
"three": "^0.160.0",
"toml": "^3.0.0", "toml": "^3.0.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.2.2", "typescript": "^5.2.2",
@ -84,9 +82,7 @@
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"", "remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings", "wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src", "lint": "eslint --fix src",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json", "bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"
"postinstall": "patch-package && yarn xstate:typegen",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
}, },
"prettier": { "prettier": {
"trailingComma": "es5", "trailingComma": "es5",
@ -117,7 +113,6 @@
"@types/pixelmatch": "^5.2.6", "@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
"@types/react-modal": "^3.16.3", "@types/react-modal": "^3.16.3",
"@types/three": "^0.160.0",
"@types/uuid": "^9.0.4", "@types/uuid": "^9.0.4",
"@types/wait-on": "^5.3.4", "@types/wait-on": "^5.3.4",
"@types/wicg-file-system-access": "^2020.9.6", "@types/wicg-file-system-access": "^2020.9.6",
@ -129,26 +124,21 @@
"@wdio/local-runner": "^8.24.3", "@wdio/local-runner": "^8.24.3",
"@wdio/mocha-framework": "^8.24.3", "@wdio/mocha-framework": "^8.24.3",
"@wdio/spec-reporter": "^8.24.2", "@wdio/spec-reporter": "^8.24.2",
"@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"eslint": "^8.53.0", "eslint": "^8.53.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
"happy-dom": "^10.8.0", "happy-dom": "^10.8.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"patch-package": "^8.0.0",
"pixelmatch": "^5.3.0", "pixelmatch": "^5.3.0",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.8.0", "prettier": "^2.8.0",
"setimmediate": "^1.0.5", "setimmediate": "^1.0.5",
"tailwindcss": "^3.3.6", "tailwindcss": "^3.3.6",
"vite": "^4.5.2", "vite": "^4.5.2",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.2.1", "vite-tsconfig-paths": "^4.2.1",
"vitest-webgl-canvas-mock": "^1.1.0",
"wait-on": "^7.2.0", "wait-on": "^7.2.0",
"yarn": "^1.22.19" "yarn": "^1.22.19"
} }

View File

@ -1,138 +0,0 @@
diff --git a/node_modules/three/examples/jsm/controls/OrbitControls.js b/node_modules/three/examples/jsm/controls/OrbitControls.js
index f29e7fe..0ef636b 100644
--- a/node_modules/three/examples/jsm/controls/OrbitControls.js
+++ b/node_modules/three/examples/jsm/controls/OrbitControls.js
@@ -113,6 +113,25 @@ class OrbitControls extends EventDispatcher {
// public methods
//
+ this.interactionGuards = {
+ pan: {
+ description: 'Right click + Shift + drag or middle click + drag',
+ callback: (e) => e.button === 2 && !e.ctrlKey,
+ },
+ zoom: {
+ description: 'Scroll wheel or Right click + Ctrl + drag',
+ dragCallback: (e) => e.button === 2 && e.ctrlKey,
+ scrollCallback: () => true,
+ },
+ rotate: {
+ description: 'Right click + drag',
+ callback: (e) => e.button === 0,
+ },
+ }
+ this.setMouseGuards = (interactionGuards) => {
+ this.interactionGuards = interactionGuards
+ }
+
this.getPolarAngle = function () {
return spherical.phi;
@@ -1057,92 +1076,21 @@ class OrbitControls extends EventDispatcher {
function onMouseDown( event ) {
- let mouseAction;
-
- switch ( event.button ) {
-
- case 0:
-
- mouseAction = scope.mouseButtons.LEFT;
- break;
-
- case 1:
-
- mouseAction = scope.mouseButtons.MIDDLE;
- break;
-
- case 2:
-
- mouseAction = scope.mouseButtons.RIGHT;
- break;
-
- default:
-
- mouseAction = - 1;
-
- }
-
- switch ( mouseAction ) {
-
- case MOUSE.DOLLY:
-
- if ( scope.enableZoom === false ) return;
-
- handleMouseDownDolly( event );
-
- state = STATE.DOLLY;
-
- break;
-
- case MOUSE.ROTATE:
-
- if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
-
- if ( scope.enablePan === false ) return;
-
- handleMouseDownPan( event );
-
- state = STATE.PAN;
-
- } else {
-
- if ( scope.enableRotate === false ) return;
-
- handleMouseDownRotate( event );
-
- state = STATE.ROTATE;
-
- }
-
- break;
-
- case MOUSE.PAN:
-
- if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
-
- if ( scope.enableRotate === false ) return;
-
- handleMouseDownRotate( event );
-
- state = STATE.ROTATE;
-
- } else {
-
- if ( scope.enablePan === false ) return;
-
- handleMouseDownPan( event );
-
- state = STATE.PAN;
-
- }
-
- break;
-
- default:
-
- state = STATE.NONE;
-
- }
+ if (scope.interactionGuards.pan.callback(event)) {
+ if (scope.enablePan === false) return
+ handleMouseDownPan(event)
+ state = STATE.PAN
+ } else if (scope.interactionGuards.rotate.callback(event)) {
+ if (scope.enableRotate === false) return
+ handleMouseDownRotate(event)
+ state = STATE.ROTATE
+ } else if (scope.interactionGuards.zoom.dragCallback(event)) {
+ if (scope.enableZoom === false) return
+ handleMouseDownDolly(event)
+ state = STATE.DOLLY
+ } else {
+ return
+ }
if ( state !== STATE.NONE ) {

View File

@ -1,4 +1,4 @@
import { defineConfig, devices } from '@playwright/test' import { defineConfig, devices } from '@playwright/test';
/** /**
* Read environment variables from file. * Read environment variables from file.
@ -78,4 +78,5 @@ export default defineConfig({
// url: 'http://127.0.0.1:3000', // url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },
}) });

View File

@ -1,26 +0,0 @@
import requests
import os
webhook_url = os.getenv('DISCORD_WEBHOOK_URL')
release_version = os.getenv('RELEASE_VERSION')
release_body = os.getenv('RELEASE_BODY')
# message to send to Discord
data = {
"content":
f'''
**{release_version}** is now available! Check out the latest features and improvements here: https://zoo.dev/modeling-app/download
{release_body}
''',
"username": "Modeling App Release Updates",
"avatar_url": "https://raw.githubusercontent.com/KittyCAD/modeling-app/main/public/discord-avatar.png"
}
# POST request to the Discord webhook
response = requests.post(webhook_url, json=data)
# Check for success
if response.status_code == 204:
print("Successfully sent the message to Discord.")
else:
print("Failed to send the message to Discord.")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

104
src-tauri/Cargo.lock generated
View File

@ -67,9 +67,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.79" version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]] [[package]]
name = "app" name = "app"
@ -95,7 +95,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -542,7 +542,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -552,7 +552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e" checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -582,7 +582,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -593,7 +593,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -899,7 +899,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -1664,9 +1664,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad" name = "kittycad"
version = "0.2.53" version = "0.2.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a086e1a1bbddb3b38959c0f0ce6de6b3a3b7566e38e0b7d5fb101e91911beed4" checksum = "6aa554d86b6dbbd976a659c912ae25ce817b4378eb12a5684907e263410f0a7b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -2215,7 +2215,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -2368,7 +2368,7 @@ dependencies = [
"regex", "regex",
"regex-syntax 0.7.5", "regex-syntax 0.7.5",
"structmeta", "structmeta",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -2487,7 +2487,7 @@ dependencies = [
"phf_shared 0.11.2", "phf_shared 0.11.2",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -2555,7 +2555,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -2657,9 +2657,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.78" version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -2675,9 +2675,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.35" version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -3235,9 +3235,9 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.196" version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@ -3253,13 +3253,13 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.196" version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -3275,9 +3275,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.113" version = "1.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
dependencies = [ dependencies = [
"itoa 1.0.6", "itoa 1.0.6",
"ryu", "ryu",
@ -3302,7 +3302,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -3352,7 +3352,7 @@ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -3556,7 +3556,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"structmeta-derive", "structmeta-derive",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -3567,7 +3567,7 @@ checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -3605,9 +3605,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.48" version = "2.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3760,9 +3760,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "1.5.4" version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd27c04b9543776a972c86ccf70660b517ecabbeced9fb58d8b961a13ad129af" checksum = "32d563b672acde8d0cc4c1b1f5b855976923f67e8d6fe1eba51df0211e197be2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -3812,9 +3812,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "1.5.1" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9914a4715e0b75d9f387a285c7e26b5bbfeb1249ad9f842675a82481565c532" checksum = "defbfc551bd38ab997e5f8e458f87396d2559d05ce32095076ad6c30f7fc5f9c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@ -3831,9 +3831,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "1.4.2" version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1554c5857f65dbc377cefb6b97c8ac77b1cb2a90d30d3448114d5d6b48a77fc" checksum = "7b3475e55acec0b4a50fb96435f19631fb58cbcd31923e1a213de5c382536bbb"
dependencies = [ dependencies = [
"base64 0.21.2", "base64 0.21.2",
"brotli", "brotli",
@ -3857,9 +3857,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "1.4.3" version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "277abf361a3a6993ec16bcbb179de0d6518009b851090a01adfea12ac89fa875" checksum = "acea6445eececebd72ed7720cfcca46eee3b5bad8eb408be8f7ef2e3f7411500"
dependencies = [ dependencies = [
"heck 0.4.1", "heck 0.4.1",
"proc-macro2", "proc-macro2",
@ -3872,7 +3872,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-fs-extra" name = "tauri-plugin-fs-extra"
version = "0.0.0" version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#01211ff0759d578e0e9ac8c98c31fdf09077eb34" source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#537053d3171a7374a1a86fed422523e7b45a4fb8"
dependencies = [ dependencies = [
"log", "log",
"serde", "serde",
@ -3883,9 +3883,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "0.14.2" version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf2d0652aa2891ff3e9caa2401405257ea29ab8372cce01f186a5825f1bd0e76" checksum = "07f8e9e53e00e9f41212c115749e87d5cd2a9eebccafca77a19722eeecd56d43"
dependencies = [ dependencies = [
"gtk", "gtk",
"http", "http",
@ -3904,9 +3904,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "0.14.3" version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cae61fbc731f690a4899681c9052dde6d05b159b44563ace8186fc1bfb7d158" checksum = "803a01101bc611ba03e13329951a1bde44287a54234189b9024b78619c1bc206"
dependencies = [ dependencies = [
"cocoa", "cocoa",
"gtk", "gtk",
@ -3924,9 +3924,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "1.5.2" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ece74810b1d3d44f29f732a7ae09a63183d63949bbdd59c61f8ed2a1b70150db" checksum = "a52165bb340e6f6a75f1f5eeeab1bb49f861c12abe3a176067d53642b5454986"
dependencies = [ dependencies = [
"brotli", "brotli",
"ctor", "ctor",
@ -4009,7 +4009,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -4051,9 +4051,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.36.0" version = "1.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -4193,7 +4193,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
] ]
[[package]] [[package]]
@ -4443,7 +4443,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -4477,7 +4477,7 @@ checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.33",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]

View File

@ -12,17 +12,17 @@ rust-version = "1.60"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]
tauri-build = { version = "1.5.1", features = [] } tauri-build = { version = "1.5.0", features = [] }
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
kittycad = "0.2.53" kittycad = "0.2.42"
oauth2 = "4.4.2" oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tauri = { version = "1.5.4", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] } tauri = { version = "1.5.3", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] }
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tokio = { version = "1.36.0", features = ["time"] } tokio = { version = "1.34.0", features = ["time"] }
toml = "0.8.2" toml = "0.8.2"
[features] [features]

View File

@ -7,7 +7,6 @@ use std::io::Read;
use anyhow::Result; use anyhow::Result;
use oauth2::TokenResponse; use oauth2::TokenResponse;
use std::process::Command;
use tauri::{InvokeError, Manager}; use tauri::{InvokeError, Manager};
const DEFAULT_HOST: &str = "https://api.kittycad.io"; const DEFAULT_HOST: &str = "https://api.kittycad.io";

View File

@ -7,7 +7,7 @@
}, },
"package": { "package": {
"productName": "zoo-modeling-app", "productName": "zoo-modeling-app",
"version": "0.15.1" "version": "0.14.0"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {

73
src/App.test.tsx Normal file
View File

@ -0,0 +1,73 @@
import { render, screen } from '@testing-library/react'
import { App } from './App'
import { describe, test, vi } from 'vitest'
import {
Route,
RouterProvider,
createMemoryRouter,
createRoutesFromElements,
} from 'react-router-dom'
import { GlobalStateProvider } from './components/GlobalStateProvider'
import CommandBarProvider from 'components/CommandBar/CommandBar'
import ModelingMachineProvider from 'components/ModelingMachineProvider'
import { BROWSER_FILE_NAME } from 'Router'
let listener: ((rect: any) => void) | undefined = undefined
;(global as any).ResizeObserver = class ResizeObserver {
constructor(ls: ((rect: any) => void) | undefined) {
listener = ls
}
observe() {}
unobserve() {}
disconnect() {}
}
describe('App tests', () => {
test('Renders the modeling app screen, including "Variables" pane.', () => {
vi.mock('react-router-dom', async () => {
const actual = (await vi.importActual('react-router-dom')) as Record<
string,
any
>
return {
...actual,
useParams: () => ({ id: BROWSER_FILE_NAME }),
useLoaderData: () => ({ code: null }),
}
})
render(
<TestWrap>
<App />
</TestWrap>
)
const linkElement = screen.getByText(/Variables/i)
expect(linkElement).toBeInTheDocument()
vi.restoreAllMocks()
})
})
function TestWrap({ children }: { children: React.ReactNode }) {
// We have to use a memory router in the testing environment,
// and we have to use the createMemoryRouter function instead of <MemoryRouter /> as of react-router v6.4:
// https://reactrouter.com/en/6.16.0/routers/picking-a-router#using-v64-data-apis
const router = createMemoryRouter(
createRoutesFromElements(
<Route
path="/file/:id"
element={
<CommandBarProvider>
<GlobalStateProvider>
<ModelingMachineProvider>{children}</ModelingMachineProvider>
</GlobalStateProvider>
</CommandBarProvider>
}
/>
),
{
initialEntries: ['/file/new'],
initialIndex: 0,
}
)
return <RouterProvider router={router} />
}

View File

@ -19,24 +19,21 @@ import {
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { getNormalisedCoordinates } from './lib/utils' import { getNormalisedCoordinates } from './lib/utils'
import { useLoaderData, useNavigate } from 'react-router-dom' import { useLoaderData } from 'react-router-dom'
import { type IndexLoaderData } from 'lib/types' import { IndexLoaderData } from './Router'
import { paths } from 'lib/paths'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { CodeMenu } from 'components/CodeMenu' import { CodeMenu } from 'components/CodeMenu'
import { TextEditor } from 'components/TextEditor' import { TextEditor } from 'components/TextEditor'
import { Themes, getSystemTheme } from 'lib/theme' import { Themes, getSystemTheme } from 'lib/theme'
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions' import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
import { engineCommandManager } from './lang/std/engineConnection' import { engineCommandManager } from './lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { isTauri } from 'lib/isTauri'
export function App() { export function App() {
const { project, file } = useLoaderData() as IndexLoaderData const { project, file } = useLoaderData() as IndexLoaderData
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
useHotKeyListener() useHotKeyListener()
const { const {
@ -54,7 +51,8 @@ export function App() {
})) }))
const { settings } = useGlobalStateContext() const { settings } = useGlobalStateContext()
const { showDebugPanel, onboardingStatus, theme } = settings?.context || {} const { showDebugPanel, onboardingStatus, cameraControls, theme } =
settings?.context || {}
const { state, send } = useModelingContext() const { state, send } = useModelingContext()
const editorTheme = theme === Themes.System ? getSystemTheme() : theme const editorTheme = theme === Themes.System ? getSystemTheme() : theme
@ -73,16 +71,6 @@ export function App() {
useHotkeys('shift + e', () => togglePane('kclErrors')) useHotkeys('shift + e', () => togglePane('kclErrors'))
useHotkeys('shift + d', () => togglePane('debug')) useHotkeys('shift + d', () => togglePane('debug'))
useHotkeys('esc', () => send('Cancel')) useHotkeys('esc', () => send('Cancel'))
useHotkeys('backspace', (e) => {
e.preventDefault()
})
useHotkeys(
isTauri() ? 'mod + ,' : 'shift + mod + ,',
() => navigate(filePath + paths.SETTINGS),
{
splitKey: '|',
}
)
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some( const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
(p) => p === onboardingStatus (p) => p === onboardingStatus
@ -96,11 +84,9 @@ export function App() {
const debounceSocketSend = throttle<EngineCommand>((message) => { const debounceSocketSend = throttle<EngineCommand>((message) => {
engineCommandManager.sendSceneCommand(message) engineCommandManager.sendSceneCommand(message)
}, 1000 / 15) }, 16)
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
if (state.matches('Sketch')) { e.nativeEvent.preventDefault()
return
}
const { x, y } = getNormalisedCoordinates({ const { x, y } = getNormalisedCoordinates({
clientX: e.clientX, clientX: e.clientX,
@ -111,11 +97,58 @@ export function App() {
const newCmdId = uuidv4() const newCmdId = uuidv4()
if (buttonDownInStream === undefined) { if (buttonDownInStream === undefined) {
if (state.matches('Sketch.Line Tool')) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd_id: newCmdId,
cmd: {
type: 'mouse_move',
window: { x, y },
},
})
} else {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'highlight_set_entity',
selected_at_window: { x, y },
},
cmd_id: newCmdId,
})
}
} else {
if (state.matches('Sketch.Move Tool')) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd_id: newCmdId,
cmd: {
type: 'handle_mouse_drag_move',
window: { x, y },
},
})
return
}
const interactionGuards = cameraMouseDragGuards[cameraControls]
let interaction: CameraDragInteractionType_type
const eWithButton = { ...e, button: buttonDownInStream }
if (interactionGuards.pan.callback(eWithButton)) {
interaction = 'pan'
} else if (interactionGuards.rotate.callback(eWithButton)) {
interaction = 'rotate'
} else if (interactionGuards.zoom.dragCallback(eWithButton)) {
interaction = 'zoom'
} else {
return
}
debounceSocketSend({ debounceSocketSend({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
type: 'highlight_set_entity', type: 'camera_drag_move',
selected_at_window: { x, y }, interaction,
window: { x, y },
}, },
cmd_id: newCmdId, cmd_id: newCmdId,
}) })
@ -205,7 +238,6 @@ export function App() {
open={openPanes.includes('debug')} open={openPanes.includes('debug')}
/> />
)} )}
{/* <CamToggle /> */}
</div> </div>
) )
} }

View File

@ -14,7 +14,10 @@ import {
import { useEffect } from 'react' import { useEffect } from 'react'
import { ErrorPage } from './components/ErrorPage' import { ErrorPage } from './components/ErrorPage'
import { Settings } from './routes/Settings' import { Settings } from './routes/Settings'
import Onboarding, { onboardingRoutes } from './routes/Onboarding' import Onboarding, {
onboardingRoutes,
onboardingPaths,
} from './routes/Onboarding'
import SignIn from './routes/SignIn' import SignIn from './routes/SignIn'
import { Auth } from './Auth' import { Auth } from './Auth'
import { isTauri } from './lib/isTauri' import { isTauri } from './lib/isTauri'
@ -26,7 +29,7 @@ import {
isProjectDirectory, isProjectDirectory,
PROJECT_ENTRYPOINT, PROJECT_ENTRYPOINT,
} from './lib/tauriFS' } from './lib/tauriFS'
import { metadata } from 'tauri-plugin-fs-extra-api' import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
import DownloadAppBanner from './components/DownloadAppBanner' import DownloadAppBanner from './components/DownloadAppBanner'
import { WasmErrBanner } from './components/WasmErrBanner' import { WasmErrBanner } from './components/WasmErrBanner'
import { GlobalStateProvider } from './components/GlobalStateProvider' import { GlobalStateProvider } from './components/GlobalStateProvider'
@ -39,12 +42,9 @@ 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'
import { KclContextProvider, kclManager } from 'lang/KclSingleton' import { KclContextProvider, kclManager } from 'lang/KclSinglton'
import FileMachineProvider from 'components/FileMachineProvider' import FileMachineProvider from 'components/FileMachineProvider'
import { sep } from '@tauri-apps/api/path' import { sep } from '@tauri-apps/api/path'
import { paths } from 'lib/paths'
import { IndexLoaderData, HomeLoaderData } from 'lib/types'
import { fileSystemManager } from 'lang/std/fileSystemManager'
if (VITE_KC_SENTRY_DSN && !TEST) { if (VITE_KC_SENTRY_DSN && !TEST) {
Sentry.init({ Sentry.init({
@ -78,8 +78,43 @@ if (VITE_KC_SENTRY_DSN && !TEST) {
}) })
} }
const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => {
return Object.fromEntries(
Object.entries(routesObject).map(([constName, path]) => [
constName,
prepend + path,
])
)
}
export const paths = {
INDEX: '/',
HOME: '/home',
FILE: '/file',
SETTINGS: '/settings',
SIGN_IN: '/signin',
ONBOARDING: prependRoutes(onboardingPaths)(
'/onboarding'
) as typeof onboardingPaths,
}
export const BROWSER_FILE_NAME = 'new' export const BROWSER_FILE_NAME = 'new'
export type IndexLoaderData = {
code: string | null
project?: ProjectWithEntryPointMetadata
file?: FileEntry
}
export type ProjectWithEntryPointMetadata = FileEntry & {
entrypointMetadata: Metadata
}
export type HomeLoaderData = {
projects: ProjectWithEntryPointMetadata[]
newDefaultDirectory?: string
}
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0] type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
const addGlobalContextToElements = ( const addGlobalContextToElements = (
@ -111,18 +146,18 @@ const router = createBrowserRouter(
{ {
path: paths.FILE + '/:id', path: paths.FILE + '/:id',
element: ( element: (
<KclContextProvider> <Auth>
<Auth> <FileMachineProvider>
<FileMachineProvider> <KclContextProvider>
<ModelingMachineProvider> <ModelingMachineProvider>
<Outlet /> <Outlet />
<App /> <App />
</ModelingMachineProvider> </ModelingMachineProvider>
<WasmErrBanner /> <WasmErrBanner />
</FileMachineProvider> </KclContextProvider>
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />} </FileMachineProvider>
</Auth> {!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
</KclContextProvider> </Auth>
), ),
id: paths.FILE, id: paths.FILE,
loader: async ({ loader: async ({
@ -175,10 +210,6 @@ const router = createBrowserRouter(
const children = await readDir(projectPath, { recursive: true }) const children = await readDir(projectPath, { recursive: true })
kclManager.setCodeAndExecute(code, false) kclManager.setCodeAndExecute(code, false)
// Set the file system manager to the project path
// So that WASM gets an updated path for operations
fileSystemManager.dir = projectPath
return { return {
code, code,
project: { project: {
@ -218,7 +249,7 @@ const router = createBrowserRouter(
<Home /> <Home />
</Auth> </Auth>
), ),
loader: async (): Promise<HomeLoaderData | Response> => { loader: async () => {
if (!isTauri()) { if (!isTauri()) {
return redirect(paths.FILE + '/' + BROWSER_FILE_NAME) return redirect(paths.FILE + '/' + BROWSER_FILE_NAME)
} }

View File

@ -5,8 +5,6 @@ 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' import usePlatform from 'hooks/usePlatform'
import { isSingleCursorInPipe } from 'lang/queryAst'
import { kclManager } from 'lang/KclSingleton'
export const Toolbar = () => { export const Toolbar = () => {
const platform = usePlatform() const platform = usePlatform()
@ -15,15 +13,14 @@ export const Toolbar = () => {
const toolbarButtonsRef = useRef<HTMLUListElement>(null) const toolbarButtonsRef = useRef<HTMLUListElement>(null)
const bgClassName = const bgClassName =
'group-enabled:group-hover:bg-energy-10 group-pressed:bg-energy-10 dark:group-enabled:group-hover:bg-chalkboard-80 dark:group-pressed:bg-chalkboard-80' 'group-enabled:group-hover:bg-energy-10 group-pressed:bg-energy-10 dark:group-enabled:group-hover:bg-chalkboard-80 dark:group-pressed:bg-chalkboard-80'
const pathId = useMemo(() => { const pathId = useMemo(
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) { () =>
return false isCursorInSketchCommandRange(
} engineCommandManager.artifactMap,
return isCursorInSketchCommandRange( context.selectionRanges
engineCommandManager.artifactMap, ),
context.selectionRanges [engineCommandManager.artifactMap, context.selectionRanges]
) )
}, [engineCommandManager.artifactMap, context.selectionRanges])
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) { function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
const span = toolbarButtonsRef.current const span = toolbarButtonsRef.current
@ -53,9 +50,7 @@ export const Toolbar = () => {
<li className="contents"> <li className="contents">
<ActionButton <ActionButton
Element="button" Element="button"
onClick={() => onClick={() => send({ type: 'Enter sketch' })}
send({ type: 'Enter sketch', data: { forceNewSketch: true } })
}
icon={{ icon={{
icon: 'sketch', icon: 'sketch',
bgClassName, bgClassName,
@ -94,48 +89,44 @@ export const Toolbar = () => {
</li> </li>
)} )}
{state.matches('Sketch') && !state.matches('idle') && ( {state.matches('Sketch') && !state.matches('idle') && (
<> <li className="contents">
<li className="contents" key="line-button"> <ActionButton
<ActionButton Element="button"
Element="button" onClick={() =>
onClick={() => state.matches('Sketch.Line Tool')
state?.matches('Sketch.Line tool') ? send('CancelSketch')
? send('CancelSketch') : send('Equip tool')
: send('Equip Line tool') }
} aria-pressed={state.matches('Sketch.Line Tool')}
aria-pressed={state?.matches('Sketch.Line tool')} className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80" icon={{
icon={{ icon: 'line',
icon: 'line', bgClassName,
bgClassName, }}
}} >
> Line
Line </ActionButton>
</ActionButton> </li>
</li> )}
<li className="contents" key="tangential-arc-button"> {state.matches('Sketch') && (
<ActionButton <li className="contents">
Element="button" <ActionButton
onClick={() => Element="button"
state.matches('Sketch.Tangential arc to') onClick={() =>
? send('CancelSketch') state.matches('Sketch.Move Tool')
: send('Equip tangential arc to') ? send('CancelSketch')
} : send('Equip move tool')
aria-pressed={state.matches('Sketch.Tangential arc to')} }
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80" aria-pressed={state.matches('Sketch.Move Tool')}
icon={{ className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
icon: 'line', icon={{
bgClassName, icon: 'move',
}} bgClassName,
disabled={ }}
!state.can('Equip tangential arc to') && >
!state.matches('Sketch.Tangential arc to') Move
} </ActionButton>
> </li>
Tangential Arc
</ActionButton>
</li>
</>
)} )}
{state.matches('Sketch.SketchIdle') && {state.matches('Sketch.SketchIdle') &&
state.nextEvents state.nextEvents
@ -160,7 +151,7 @@ export const Toolbar = () => {
return 0 return 0
}) })
.map((eventName) => ( .map((eventName) => (
<li className="contents" key={eventName}> <li className="contents">
<ActionButton <ActionButton
Element="button" Element="button"
className="text-sm" className="text-sm"

View File

@ -1,252 +0,0 @@
import { useRef, useEffect, useState } from 'react'
import { useModelingContext } from 'hooks/useModelingContext'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useStore } from 'useStore'
import {
DEBUG_SHOW_BOTH_SCENES,
ReactCameraProperties,
sceneInfra,
} from './sceneInfra'
import { throttle } from 'lib/utils'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const [isCamMoving, setIsCamMoving] = useState(false)
const [isTween, setIsTween] = useState(false)
const { state } = useModelingContext()
useEffect(() => {
sceneInfra.setIsCamMovingCallback((isMoving, isTween) => {
setIsCamMoving(isMoving)
setIsTween(isTween)
})
}, [])
if (DEBUG_SHOW_BOTH_SCENES || !isCamMoving)
return { hideClient: false, hideServer: false }
let hideServer = state.matches('Sketch')
if (isTween) {
hideServer = false
}
return { hideClient: !hideServer, hideServer }
}
export const ClientSideScene = ({
cameraControls,
}: {
cameraControls: ReturnType<
typeof useGlobalStateContext
>['settings']['context']['cameraControls']
}) => {
const canvasRef = useRef<HTMLDivElement>(null)
const { state, send } = useModelingContext()
const { hideClient, hideServer } = useShouldHideScene()
const { setHighlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
highlightRange: s.highlightRange,
}))
// Listen for changes to the camera controls setting
// and update the client-side scene's controls accordingly.
useEffect(() => {
sceneInfra.setInteractionGuards(cameraMouseDragGuards[cameraControls])
}, [cameraControls])
useEffect(() => {
sceneInfra.updateOtherSelectionColors(
state?.context?.selectionRanges?.otherSelections || []
)
}, [state?.context?.selectionRanges?.otherSelections])
useEffect(() => {
if (!canvasRef.current) return
const canvas = canvasRef.current
canvas.appendChild(sceneInfra.renderer.domElement)
sceneInfra.animate()
sceneInfra.setHighlightCallback(setHighlightRange)
canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false)
canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false)
canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false)
sceneInfra.setSend(send)
return () => {
canvas?.removeEventListener('mousemove', sceneInfra.onMouseMove)
canvas?.removeEventListener('mousedown', sceneInfra.onMouseDown)
canvas?.removeEventListener('mouseup', sceneInfra.onMouseUp)
}
}, [])
return (
<div
ref={canvasRef}
className={`absolute inset-0 h-full w-full transition-all duration-300 ${
hideClient ? 'opacity-0' : 'opacity-100'
} ${hideServer ? 'bg-black' : ''} ${
!hideClient && !hideServer && state.matches('Sketch')
? 'bg-black/80'
: ''
}`}
></div>
)
}
const throttled = throttle((a: ReactCameraProperties) => {
if (a.type === 'perspective' && a.fov) {
sceneInfra.dollyZoom(a.fov)
}
}, 1000 / 15)
export const CamDebugSettings = () => {
const [camSettings, setCamSettings] = useState<ReactCameraProperties>({
type: 'perspective',
fov: 12,
position: [0, 0, 0],
quaternion: [0, 0, 0, 1],
})
const [fov, setFov] = useState(12)
useEffect(() => {
sceneInfra.setReactCameraPropertiesCallback(setCamSettings)
}, [sceneInfra])
useEffect(() => {
if (camSettings.type === 'perspective' && camSettings.fov) {
setFov(camSettings.fov)
}
}, [(camSettings as any)?.fov])
return (
<div>
<h3>cam settings</h3>
perspective cam
<input
type="checkbox"
checked={camSettings.type === 'perspective'}
onChange={(e) => {
if (camSettings.type === 'perspective') {
sceneInfra.useOrthographicCamera()
} else {
sceneInfra.usePerspectiveCamera()
}
}}
/>
{camSettings.type === 'perspective' && (
<input
type="range"
min="4"
max="90"
step={0.5}
value={fov}
onChange={(e) => {
setFov(parseFloat(e.target.value))
throttled({
...camSettings,
fov: parseFloat(e.target.value),
})
}}
className="w-full cursor-pointer pointer-events-auto"
/>
)}
{camSettings.type === 'perspective' && (
<div>
<span>fov</span>
<input
type="number"
value={camSettings.fov}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
...camSettings,
fov: parseFloat(e.target.value),
})
}}
/>
</div>
)}
{camSettings.type === 'orthographic' && (
<>
<div>
<span>fov</span>
<input
type="number"
value={camSettings.zoom}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
...camSettings,
zoom: parseFloat(e.target.value),
})
}}
/>
</div>
</>
)}
<div>
Position
<ul className="flex">
<li>
<span className="pl-2 pr-1">x:</span>
<input
type="number"
step={5}
data-testid="cam-x-position"
value={camSettings.position[0]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
...camSettings,
position: [
parseFloat(e.target.value),
camSettings.position[1],
camSettings.position[2],
],
})
}}
/>
</li>
<li>
<span className="pl-2 pr-1">y:</span>
<input
type="number"
step={5}
data-testid="cam-y-position"
value={camSettings.position[1]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
...camSettings,
position: [
camSettings.position[0],
parseFloat(e.target.value),
camSettings.position[2],
],
})
}}
/>
</li>
<li>
<span className="pl-2 pr-1">z:</span>
<input
type="number"
step={5}
data-testid="cam-z-position"
value={camSettings.position[2]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
...camSettings,
position: [
camSettings.position[0],
camSettings.position[1],
parseFloat(e.target.value),
],
})
}}
/>
</li>
</ul>
</div>
</div>
)
}

View File

@ -1,33 +0,0 @@
import {
GridHelper,
LineBasicMaterial,
OrthographicCamera,
PerspectiveCamera,
Group,
Mesh,
} from 'three'
export function createGridHelper({
size,
divisions,
}: {
size: number
divisions: number
}) {
const gridHelperMaterial = new LineBasicMaterial({
color: 0xaaaaaa,
transparent: true,
opacity: 0.5,
depthTest: false,
})
const gridHelper = new GridHelper(size, divisions, 0x0000ff, 0xffffff)
gridHelper.material = gridHelperMaterial
gridHelper.rotation.x = Math.PI / 2
return gridHelper
}
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
0.55 / cam.zoom
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
(group.position.distanceTo(cam.position) * cam.fov) / 4000

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +0,0 @@
import { Quaternion } from 'three'
import { isQuaternionVertical } from './sceneInfra'
describe('isQuaternionVertical', () => {
it('should identify vertical quaternions', () => {
const verticalQuaternions = [
new Quaternion(1, 0, 0, 0).normalize(), // bottom
new Quaternion(-0.7, 0.7, 0, 0).normalize(), // bottom 2
new Quaternion(0, 1, 0, 0).normalize(), // bottom 3
new Quaternion(0, 0, 0, 1).normalize(), // look from top
]
verticalQuaternions.forEach((quaternion) => {
expect(isQuaternionVertical(quaternion)).toBe(true)
})
})
it('should identify non-vertical quaternions', () => {
const nonVerticalQuaternions = [
new Quaternion(0.7, 0, 0, 0.7).normalize(), // front
new Quaternion(0, 0.7, 0.7, 0).normalize(), // back
new Quaternion(-0.5, 0.5, 0.5, -0.5).normalize(), // left side
new Quaternion(0.5, 0.5, 0.5, 0.5).normalize(), // right side
]
nonVerticalQuaternions.forEach((quaternion) => {
expect(isQuaternionVertical(quaternion)).toBe(false)
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -1,358 +0,0 @@
import { Coords2d } from 'lang/std/sketch'
import {
BufferGeometry,
CatmullRomCurve3,
ConeGeometry,
CurvePath,
EllipseCurve,
ExtrudeGeometry,
Group,
LineCurve3,
Mesh,
MeshBasicMaterial,
NormalBufferAttributes,
Shape,
SphereGeometry,
Vector2,
Vector3,
} from 'three'
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
import {
STRAIGHT_SEGMENT,
STRAIGHT_SEGMENT_BODY,
STRAIGHT_SEGMENT_DASH,
TANGENTIAL_ARC_TO_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT_BODY,
TANGENTIAL_ARC_TO__SEGMENT_DASH,
} from './sceneEntities'
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import { ARROWHEAD } from './sceneInfra'
export function straightSegment({
from,
to,
id,
pathToNode,
isDraftSegment,
scale = 1,
}: {
from: Coords2d
to: Coords2d
id: string
pathToNode: PathToNode
isDraftSegment?: boolean
scale?: number
}): Group {
const group = new Group()
const shape = new Shape()
shape.moveTo(0, -0.08 * scale)
shape.lineTo(0, 0.08 * scale) // The width of the line
let geometry
if (isDraftSegment) {
geometry = dashedStraight(from, to, shape, scale)
} else {
const line = new LineCurve3(
new Vector3(from[0], from[1], 0),
new Vector3(to[0], to[1], 0)
)
geometry = new ExtrudeGeometry(shape, {
steps: 2,
bevelEnabled: false,
extrudePath: line,
})
}
const body = new MeshBasicMaterial({ color: 0xffffff })
const mesh = new Mesh(geometry, body)
mesh.userData.type = isDraftSegment
? STRAIGHT_SEGMENT_DASH
: STRAIGHT_SEGMENT_BODY
mesh.name = STRAIGHT_SEGMENT_BODY
group.userData = {
type: STRAIGHT_SEGMENT,
id,
from,
to,
pathToNode,
isSelected: false,
}
const arrowGroup = createArrowhead(scale)
arrowGroup.position.set(to[0], to[1], 0)
const dir = new Vector3()
.subVectors(new Vector3(to[0], to[1], 0), new Vector3(from[0], from[1], 0))
.normalize()
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
group.add(mesh, arrowGroup)
return group
}
function createArrowhead(scale = 1): Group {
const arrowMaterial = new MeshBasicMaterial({ color: 0xffffff })
const arrowheadMesh = new Mesh(new ConeGeometry(0.31, 1.5, 12), arrowMaterial)
arrowheadMesh.position.set(0, -0.6, 0)
const sphereMesh = new Mesh(new SphereGeometry(0.27, 12, 12), arrowMaterial)
const arrowGroup = new Group()
arrowGroup.userData.type = ARROWHEAD
arrowGroup.name = ARROWHEAD
arrowGroup.add(arrowheadMesh, sphereMesh)
arrowGroup.lookAt(new Vector3(0, 1, 0))
arrowGroup.scale.set(scale, scale, scale)
return arrowGroup
}
export function tangentialArcToSegment({
prevSegment,
from,
to,
id,
pathToNode,
isDraftSegment,
scale = 1,
}: {
prevSegment: SketchGroup['value'][number]
from: Coords2d
to: Coords2d
id: string
pathToNode: PathToNode
isDraftSegment?: boolean
scale?: number
}): Group {
const group = new Group()
const previousPoint =
prevSegment?.type === 'TangentialArcTo'
? getTangentPointFromPreviousArc(
prevSegment.center,
prevSegment.ccw,
prevSegment.to
)
: prevSegment.from
const { center, radius, startAngle, endAngle, ccw } = getTangentialArcToInfo({
arcStartPoint: from,
arcEndPoint: to,
tanPreviousPoint: previousPoint,
obtuse: true,
})
const geometry = createArcGeometry({
center,
radius,
startAngle,
endAngle,
ccw,
isDashed: isDraftSegment,
scale,
})
const body = new MeshBasicMaterial({ color: 0xffffff })
const mesh = new Mesh(geometry, body)
mesh.userData.type = isDraftSegment
? TANGENTIAL_ARC_TO__SEGMENT_DASH
: TANGENTIAL_ARC_TO_SEGMENT_BODY
group.userData = {
type: TANGENTIAL_ARC_TO_SEGMENT,
id,
from,
to,
prevSegment,
pathToNode,
isSelected: false,
}
const arrowGroup = createArrowhead(scale)
arrowGroup.position.set(to[0], to[1], 0)
const arrowheadAngle = endAngle + (Math.PI / 2) * (ccw ? 1 : -1)
arrowGroup.quaternion.setFromUnitVectors(
new Vector3(0, 1, 0),
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
)
group.add(mesh, arrowGroup)
return group
}
export function createArcGeometry({
center,
radius,
startAngle,
endAngle,
ccw,
isDashed = false,
scale = 1,
}: {
center: Coords2d
radius: number
startAngle: number
endAngle: number
ccw: boolean
isDashed?: boolean
scale?: number
}): BufferGeometry {
const dashSize = 1.2 * scale
const gapSize = 1.2 * scale
const arcStart = new EllipseCurve(
center[0],
center[1],
radius,
radius,
startAngle,
endAngle,
!ccw,
0
)
const arcEnd = new EllipseCurve(
center[0],
center[1],
radius,
radius,
endAngle,
startAngle,
ccw,
0
)
const shape = new Shape()
shape.moveTo(0, -0.08 * scale)
shape.lineTo(0, 0.08 * scale) // The width of the line
if (!isDashed) {
const points = arcStart.getPoints(50)
const path = new CurvePath<Vector3>()
path.add(new CatmullRomCurve3(points.map((p) => new Vector3(p.x, p.y, 0))))
return new ExtrudeGeometry(shape, {
steps: 100,
bevelEnabled: false,
extrudePath: path,
})
}
const length = arcStart.getLength()
const totalDashes = length / (dashSize + gapSize) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place
const dashesAtEachEnd = Math.min(100, totalDashes / 2) // Assuming we want 50 dashes total, 25 at each end
const dashGeometries = []
// Function to create a dash at a specific t value (0 to 1 along the curve)
const createDashAt = (t: number, curve: EllipseCurve) => {
const startVec = curve.getPoint(t)
const endVec = curve.getPoint(Math.min(0.5, t + dashSize / length))
const midVec = curve.getPoint(Math.min(0.5, t + dashSize / length / 2))
const dashCurve = new CurvePath<Vector3>()
dashCurve.add(
new CatmullRomCurve3([
new Vector3(startVec.x, startVec.y, 0),
new Vector3(midVec.x, midVec.y, 0),
new Vector3(endVec.x, endVec.y, 0),
])
)
return new ExtrudeGeometry(shape, {
steps: 3,
bevelEnabled: false,
extrudePath: dashCurve,
})
}
// Create dashes at the start of the arc
for (let i = 0; i < dashesAtEachEnd; i++) {
const t = i / totalDashes
dashGeometries.push(createDashAt(t, arcStart))
dashGeometries.push(createDashAt(t, arcEnd))
}
// fill in the remaining arc
const remainingArcLength = length - dashesAtEachEnd * 2 * (dashSize + gapSize)
if (remainingArcLength > 0) {
const remainingArcStartT = dashesAtEachEnd / totalDashes
const remainingArcEndT = 1 - remainingArcStartT
const centerVec = new Vector2(center[0], center[1])
const remainingArcStartVec = arcStart.getPoint(remainingArcStartT)
const remainingArcEndVec = arcStart.getPoint(remainingArcEndT)
const remainingArcCurve = new EllipseCurve(
arcStart.aX,
arcStart.aY,
arcStart.xRadius,
arcStart.yRadius,
new Vector2().subVectors(centerVec, remainingArcStartVec).angle() +
Math.PI,
new Vector2().subVectors(centerVec, remainingArcEndVec).angle() + Math.PI,
!ccw
)
const remainingArcPoints = remainingArcCurve.getPoints(50)
const remainingArcPath = new CurvePath<Vector3>()
remainingArcPath.add(
new CatmullRomCurve3(
remainingArcPoints.map((p) => new Vector3(p.x, p.y, 0))
)
)
const remainingArcGeometry = new ExtrudeGeometry(shape, {
steps: 50,
bevelEnabled: false,
extrudePath: remainingArcPath,
})
dashGeometries.push(remainingArcGeometry)
}
const geo = dashGeometries.length
? mergeGeometries(dashGeometries)
: new BufferGeometry()
geo.userData.type = 'dashed'
return geo
}
export function dashedStraight(
from: Coords2d,
to: Coords2d,
shape: Shape,
scale = 1
): BufferGeometry<NormalBufferAttributes> {
const dashSize = 1.2 * scale
const gapSize = 1.2 * scale // todo: gabSize is not respected
const dashLine = new LineCurve3(
new Vector3(from[0], from[1], 0),
new Vector3(to[0], to[1], 0)
)
const length = dashLine.getLength()
const numberOfPoints = (length / (dashSize + gapSize)) * 2
const startOfLine = new Vector3(from[0], from[1], 0)
const endOfLine = new Vector3(to[0], to[1], 0)
const dashGeometries = []
const dashComponent = (xOrY: number, pointIndex: number) =>
((to[xOrY] - from[xOrY]) / numberOfPoints) * pointIndex + from[xOrY]
for (let i = 0; i < numberOfPoints; i += 2) {
const dashStart = new Vector3(dashComponent(0, i), dashComponent(1, i), 0)
let dashEnd = new Vector3(
dashComponent(0, i + 1),
dashComponent(1, i + 1),
0
)
if (startOfLine.distanceTo(dashEnd) > startOfLine.distanceTo(endOfLine))
dashEnd = endOfLine
if (dashEnd) {
const dashCurve = new LineCurve3(dashStart, dashEnd)
const dashGeometry = new ExtrudeGeometry(shape, {
steps: 1,
bevelEnabled: false,
extrudePath: dashCurve,
})
dashGeometries.push(dashGeometry)
}
}
const geo = dashGeometries.length
? mergeGeometries(dashGeometries)
: new BufferGeometry()
geo.userData.type = 'dashed'
return geo
}

View File

@ -1,6 +1,6 @@
import { ActionIcon, ActionIconProps } from './ActionIcon' import { ActionIcon, ActionIconProps } from './ActionIcon'
import React from 'react' import React from 'react'
import { paths } from 'lib/paths' import { paths } from '../Router'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import type { LinkProps } from 'react-router-dom' import type { LinkProps } from 'react-router-dom'

View File

@ -1,6 +1,6 @@
import { Toolbar } from '../Toolbar' import { Toolbar } from '../Toolbar'
import UserSidebarMenu from './UserSidebarMenu' import UserSidebarMenu from './UserSidebarMenu'
import { type IndexLoaderData } from 'lib/types' import { IndexLoaderData } from '../Router'
import ProjectSidebarMenu from './ProjectSidebarMenu' import ProjectSidebarMenu from './ProjectSidebarMenu'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import styles from './AppHeader.module.css' import styles from './AppHeader.module.css'

View File

@ -1,5 +1,5 @@
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSinglton'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useStore } from 'useStore' import { useStore } from 'useStore'

View File

@ -8,7 +8,7 @@ import {
} from '../lang/modifyAst' } from '../lang/modifyAst'
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst' import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
import { kclManager, useKclContext } from 'lang/KclSingleton' import { kclManager, useKclContext } from 'lang/KclSinglton'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { executeAst } from 'useStore' import { executeAst } from 'useStore'
@ -148,6 +148,7 @@ export function useCalc({
executeAst({ executeAst({
ast, ast,
engineCommandManager, engineCommandManager,
defaultPlanes: kclManager.defaultPlanes,
useFakeExecutor: true, useFakeExecutor: true,
programMemoryOverride: JSON.parse( programMemoryOverride: JSON.parse(
JSON.stringify(kclManager.programMemory) JSON.stringify(kclManager.programMemory)

View File

@ -1,75 +0,0 @@
import { useState, useEffect } from 'react'
import { sceneInfra } from '../clientSideScene/sceneInfra'
import { engineCommandManager } from 'lang/std/engineConnection'
import { throttle, isReducedMotion } from 'lib/utils'
const updateDollyZoom = throttle(
(newFov: number) => sceneInfra.dollyZoom(newFov),
1000 / 15
)
export const CamToggle = () => {
const [isPerspective, setIsPerspective] = useState(true)
const [fov, setFov] = useState(40)
const [enableRotate, setEnableRotate] = useState(true)
useEffect(() => {
engineCommandManager.waitForReady.then(async () => {
sceneInfra.dollyZoom(fov)
})
}, [])
const toggleCamera = () => {
if (isPerspective) {
isReducedMotion()
? sceneInfra.useOrthographicCamera()
: sceneInfra.animateToOrthographic()
} else {
isReducedMotion()
? sceneInfra.usePerspectiveCamera()
: sceneInfra.animateToPerspective()
}
setIsPerspective(!isPerspective)
}
const handleFovChange = (newFov: number) => {
setFov(newFov)
updateDollyZoom(newFov)
}
return (
<div className="absolute right-14 bottom-3">
{isPerspective && (
<div className="">
<input
type="range"
min="4"
max="90"
step={0.5}
value={fov}
onChange={(e) => handleFovChange(Number(e.target.value))}
className="w-full cursor-pointer pointer-events-auto"
/>
</div>
)}
<button onClick={toggleCamera} className="">
{isPerspective
? 'Switch to Orthographic Camera'
: 'Switch to Perspective Camera'}
</button>
<button
onClick={() => {
if (enableRotate) {
sceneInfra.controls.enableRotate = false
} else {
sceneInfra.controls.enableRotate = true
}
setEnableRotate(!enableRotate)
}}
className=""
>
{enableRotate ? 'Disable Rotation' : 'Enable Rotation'}
</button>
</div>
)
}

View File

@ -9,7 +9,7 @@ import styles from './CodeMenu.module.css'
import { useConvertToVariable } from 'hooks/useToolbarGuards' import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { editorShortcutMeta } from './TextEditor' import { editorShortcutMeta } from './TextEditor'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSinglton'
export const CodeMenu = ({ children }: PropsWithChildren) => { export const CodeMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } = const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
@ -77,24 +77,6 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
</small> </small>
</a> </a>
</Menu.Item> </Menu.Item>
<Menu.Item>
<a
className={styles.button}
href="https://github.com/KittyCAD/kcl-samples"
target="_blank"
rel="noopener noreferrer"
>
<span>KCL samples</span>
<small>
On GitHub
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1 align-text-top"
width={12}
/>
</small>
</a>
</Menu.Item>
</Menu.Items> </Menu.Items>
</div> </div>
</Menu> </Menu>

View File

@ -1,6 +1,6 @@
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from '../CustomIcon' import { CustomIcon } from '../CustomIcon'
import React, { ReactNode, useState } from 'react' import React, { useState } from 'react'
import { ActionButton } from '../ActionButton' import { ActionButton } from '../ActionButton'
import { Selections, getSelectionTypeDisplayText } from 'lib/selections' import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
@ -99,7 +99,15 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
) : typeof argumentsToSubmit[argName] === 'object' ? ( ) : typeof argumentsToSubmit[argName] === 'object' ? (
JSON.stringify(argumentsToSubmit[argName]) JSON.stringify(argumentsToSubmit[argName])
) : ( ) : (
<em>{argumentsToSubmit[argName] as ReactNode}</em> 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> <em>{argName}</em>

View File

@ -1,6 +1,6 @@
import { useSelector } from '@xstate/react' import { useSelector } from '@xstate/react'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { useKclContext } from 'lang/KclSingleton' import { useKclContext } from 'lang/KclSinglton'
import { CommandArgument } from 'lib/commandTypes' import { CommandArgument } from 'lib/commandTypes'
import { import {
ResolvedSelectionType, ResolvedSelectionType,
@ -27,7 +27,7 @@ function CommandBarSelectionInput({
}) { }) {
const { code } = useKclContext() const { code } = useKclContext()
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const [hasSubmitted, setHasSubmitted] = useState(false) const [hasSubmitted, setHasSubmitted] = useState(false)
const selection = useSelector(arg.actor, selectionSelector) const selection = useSelector(arg.actor, selectionSelector)
const [selectionsByType, setSelectionsByType] = useState< const [selectionsByType, setSelectionsByType] = useState<
@ -59,16 +59,8 @@ function CommandBarSelectionInput({
) )
}, [selection]) }, [selection])
// Fast-forward through this arg if it's marked as skippable
// and we have a valid selection already
useEffect(() => { useEffect(() => {
setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg)) setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
const argValue = commandBarState.context.argumentsToSubmit[arg.name]
if (canSubmitSelection && arg.skip && argValue === undefined) {
handleSubmit({
preventDefault: () => {},
} as React.FormEvent<HTMLFormElement>)
}
}, [selectionsByType, arg]) }, [selectionsByType, arg])
function handleChange() { function handleChange() {

View File

@ -48,7 +48,6 @@ function CommandComboBox({
(event.metaKey && event.key === 'k') || (event.metaKey && event.key === 'k') ||
(event.key === 'Backspace' && !event.currentTarget.value) (event.key === 'Backspace' && !event.currentTarget.value)
) { ) {
event.preventDefault()
commandBarSend({ type: 'Close' }) commandBarSend({ type: 'Close' })
} }
}} }}

View File

@ -4,8 +4,6 @@ export type CustomIconName =
| 'arrowRight' | 'arrowRight'
| 'arrowUp' | 'arrowUp'
| 'checkmark' | 'checkmark'
| 'clipboardPlus'
| 'clipboardCheckmark'
| 'close' | 'close'
| 'equal' | 'equal'
| 'extrude' | 'extrude'
@ -15,11 +13,8 @@ export type CustomIconName =
| 'folderPlus' | 'folderPlus'
| 'gear' | 'gear'
| 'horizontal' | 'horizontal'
| 'horizontalDash'
| 'line' | 'line'
| 'move' | 'move'
| 'network'
| 'networkCrossedOut'
| 'parallel' | 'parallel'
| 'search' | 'search'
| 'sketch' | 'sketch'
@ -112,38 +107,6 @@ export const CustomIcon = ({
/> />
</svg> </svg>
) )
case 'clipboardCheckmark':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.5 3H7L13 3L13.5 3V3.5V4.00001L15.5 4.00002L16 4.00002V4.50002V10.0351C15.6905 9.85609 15.3548 9.71733 15 9.62602V5.00002L13.5 5.00001V6.50001V7.00001L13 7.00001L7 7.00001L6.5 7.00001V6.50001V5.00001L5 5.00001V16H10.8773C11.2024 16.4055 11.6047 16.7463 12.062 17H4.5H4V16.5V4.50001V4.00001L4.5 4.00001L6.5 4.00001V3.5V3ZM15.938 17C15.9588 16.9885 15.9794 16.9768 16 16.9649V17H15.938ZM7.5 4V4.50001V6.00001L12.5 6.00001V4.50001V4L7.5 4ZM13 9H7V8H13V9ZM15.6855 11.5L13.2101 14.8005L12.2071 13.7975L11.5 14.5046L12.9107 15.9153L13.6642 15.8617L16.4855 12.1L15.6855 11.5Z"
fill="currentColor"
/>
</svg>
)
case 'clipboardPlus':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.5 3H7L13 3L13.5 3V3.5V4.00001L15.5 4.00002L16 4.00002V4.50002V10.0351C15.6905 9.85609 15.3548 9.71733 15 9.62602V5.00002L13.5 5.00001V6.50001V7.00001L13 7.00001L7 7.00001L6.5 7.00001V6.50001V5.00001L5 5.00001V16H10.8773C11.2024 16.4055 11.6047 16.7463 12.062 17H4.5H4V16.5V4.50001V4.00001L4.5 4.00001L6.5 4.00001V3.5V3ZM15.938 17C15.9588 16.9885 15.9794 16.9768 16 16.9649V17H15.938ZM7.5 4V4.50001V6.00001L12.5 6.00001V4.50001V4L7.5 4ZM13 9H7V8H13V9ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z"
fill="currentColor"
/>
</svg>
)
case 'close': case 'close':
return ( return (
<svg <svg
@ -286,22 +249,6 @@ export const CustomIcon = ({
/> />
</svg> </svg>
) )
case 'horizontalDash':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14 10.5H6V9.5H14V10.5Z"
fill="currentColor"
/>
</svg>
)
case 'line': case 'line':
return ( return (
<svg <svg
@ -334,38 +281,6 @@ export const CustomIcon = ({
/> />
</svg> </svg>
) )
case 'network':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M18 9.64741C17.1925 8.24871 16.0344 7.08457 14.6399 6.26971C13.2455 5.45486 11.6628 5.01742 10.0478 5.00051C8.4328 4.9836 6.84127 5.38779 5.43006 6.17326C4.01884 6.95873 2.83666 8.09837 2 9.47985L2.76881 9.94546C3.52456 8.69756 4.59243 7.66813 5.86718 6.95862C7.14193 6.2491 8.57955 5.88399 10.0384 5.89927C11.4972 5.91455 12.9269 6.30968 14.1865 7.04574C15.4461 7.7818 16.4922 8.83337 17.2216 10.0968L18 9.64741ZM15.2155 11.0953C14.6772 10.1628 13.9051 9.3867 12.9755 8.84347C12.0459 8.30023 10.9907 8.00861 9.91406 7.99733C8.8374 7.98606 7.77638 8.25552 6.83557 8.77917C5.89476 9.30281 5.10664 10.0626 4.54887 10.9836L5.34391 11.4651C5.81802 10.6822 6.48792 10.0364 7.28761 9.59132C8.0873 9.14622 8.98916 8.91718 9.90432 8.92676C10.8195 8.93635 11.7164 9.18423 12.5065 9.64598C13.2967 10.1077 13.953 10.7674 14.4106 11.56L15.2155 11.0953ZM10 14C10.8284 14 11.5 13.3284 11.5 12.5C11.5 11.6716 10.8284 11 10 11C9.17157 11 8.5 11.6716 8.5 12.5C8.5 13.3284 9.17157 14 10 14Z"
fill="currentColor"
/>
</svg>
)
case 'networkCrossedOut':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.35352 5.39647L14.253 15.296L14.9601 14.5889L5.06062 4.68936L4.35352 5.39647ZM12.5065 9.64599C11.9609 9.32713 11.3643 9.11025 10.746 9.00341L9.74058 7.99796C9.79835 7.99694 9.85618 7.99674 9.91406 7.99735C10.9907 8.00862 12.0459 8.30025 12.9755 8.84348C13.9051 9.38672 14.6772 10.1628 15.2155 11.0953L14.4106 11.56C13.953 10.7674 13.2967 10.1077 12.5065 9.64599ZM6.48788 8.98789L7.16295 9.66297C6.41824 10.1045 5.79317 10.7233 5.34391 11.4651L4.54887 10.9836C5.03646 10.1785 5.70009 9.49656 6.48788 8.98789ZM10.0384 5.89928C9.3134 5.89169 8.59366 5.97804 7.89655 6.15392L7.16867 5.42605C8.09637 5.13507 9.06776 4.99026 10.0478 5.00052C11.6628 5.01744 13.2455 5.45488 14.6399 6.26973C16.0344 7.08458 17.1925 8.24872 18 9.64742L17.2216 10.0968C16.4922 8.83338 15.4461 7.78181 14.1865 7.04575C12.9269 6.3097 11.4972 5.91456 10.0384 5.89928ZM5.00782 7.50783L4.36522 6.86524C3.42033 7.57557 2.61639 8.46208 2 9.47986L2.76881 9.94547C3.34775 8.98952 4.10986 8.16177 5.00782 7.50783ZM10 14C10.4142 14 10.7892 13.8321 11.0607 13.5607L8.93934 11.4394C8.66789 11.7108 8.5 12.0858 8.5 12.5C8.5 13.3284 9.17157 14 10 14Z"
fill="currentColor"
/>
</svg>
)
case 'parallel': case 'parallel':
return ( return (
<svg <svg

View File

@ -1,7 +1,6 @@
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel' import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { AstExplorer } from './AstExplorer' import { AstExplorer } from './AstExplorer'
import { EngineCommands } from './EngineCommands' import { EngineCommands } from './EngineCommands'
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => { export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
return ( return (
@ -16,7 +15,6 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
> >
<section className="p-4 flex flex-col gap-4"> <section className="p-4 flex flex-col gap-4">
<EngineCommands /> <EngineCommands />
<CamDebugSettings />
<div style={{ height: '400px' }} className="overflow-y-auto"> <div style={{ height: '400px' }} className="overflow-y-auto">
<AstExplorer /> <AstExplorer />
</div> </div>

View File

@ -1,43 +0,0 @@
import { MoveDesc } from 'machines/modelingMachine'
export const DragWarningToast = (moveDescs: MoveDesc[]) => {
if (moveDescs.length === 1) {
return (
<div className="flex items-center">
<div>🔒</div>
<div className="dark:bg-slate-950/50 bg-slate-400/50 p-1 px-3 rounded-xl text-sm">
move disabled: line{' '}
<span className="dark:text-energy-20 text-lime-600">
{moveDescs[0].line}
</span>
:{' '}
<pre>
<code className="dark:text-energy-20 text-lime-600">
{moveDescs[0].snippet}
</code>
</pre>{' '}
is fully constrained
</div>
</div>
)
} else if (moveDescs.length > 1) {
return (
<div className="dark:bg-slate-950/50 bg-slate-400/50 p-1 px-3 rounded-xl text-sm">
<div>Move disabled as The following lines are constrained</div>
{moveDescs.map((desc, i) => {
return (
<div key={i}>
line {desc.line}:{' '}
<pre className="inline-block">
<code className="dark:text-energy-20 text-lime-600">
{moveDescs[0].snippet}
</code>
</pre>{' '}
</div>
)
})}
</div>
)
}
return null
}

View File

@ -1,7 +1,6 @@
import { useMachine } from '@xstate/react' import { useMachine } from '@xstate/react'
import { useNavigate, useRouteLoaderData } from 'react-router-dom' import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { type IndexLoaderData } from 'lib/types' import { IndexLoaderData, paths } from '../Router'
import { paths } from 'lib/paths'
import React, { createContext } from 'react' import React, { createContext } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { import {

View File

@ -1,5 +1,4 @@
import { type IndexLoaderData } from 'lib/types' import { IndexLoaderData, paths } from 'Router'
import { paths } from 'lib/paths'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { FileEntry } from '@tauri-apps/api/fs' import { FileEntry } from '@tauri-apps/api/fs'

View File

@ -1,6 +1,6 @@
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 'lib/paths' import { paths } from '../Router'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine' import { 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'

View File

@ -2,7 +2,7 @@ import ReactJson from 'react-json-view'
import { useEffect } from 'react' import { useEffect } from 'react'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel' import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { Themes } from '../lib/theme' import { Themes } from '../lib/theme'
import { useKclContext } from 'lang/KclSingleton' import { useKclContext } from 'lang/KclSinglton'
const ReactJsonTypeHack = ReactJson as any const ReactJsonTypeHack = ReactJson as any

View File

@ -41,9 +41,9 @@ describe('processMemory', () => {
otherVar: 3, otherVar: 3,
theExtrude: [], theExtrude: [],
theSketch: [ theSketch: [
{ type: 'ToPoint', to: [-3.35, 0.17], from: [0, 0], name: '' }, { type: 'toPoint', to: [-3.35, 0.17], from: [0, 0], name: '' },
{ type: 'ToPoint', to: [0.98, 5.16], from: [-3.35, 0.17], name: '' }, { type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17], name: '' },
{ type: 'ToPoint', to: [2.15, 4.32], from: [0.98, 5.16], name: '' }, { type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16], name: '' },
], ],
}) })
}) })

View File

@ -3,7 +3,7 @@ import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { useMemo } from 'react' import { useMemo } from 'react'
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/wasm' import { ProgramMemory, Path, ExtrudeSurface } from '../lang/wasm'
import { Themes } from '../lib/theme' import { Themes } from '../lib/theme'
import { useKclContext } from 'lang/KclSingleton' import { useKclContext } from 'lang/KclSinglton'
interface MemoryPanelProps extends CollapsiblePanelProps { interface MemoryPanelProps extends CollapsiblePanelProps {
theme?: Exclude<Themes, Themes.System> theme?: Exclude<Themes, Themes.System>

View File

@ -13,7 +13,23 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { isCursorInSketchCommandRange } from 'lang/util' import { isCursorInSketchCommandRange } from 'lang/util'
import { engineCommandManager } from 'lang/std/engineConnection' import { engineCommandManager } from 'lang/std/engineConnection'
import { kclManager, useKclContext } from 'lang/KclSingleton' import { v4 as uuidv4 } from 'uuid'
import { addStartSketch } from 'lang/modifyAst'
import { roundOff } from 'lib/utils'
import {
recast,
parse,
Program,
PipeExpression,
CallExpression,
} from 'lang/wasm'
import { getNodeFromPath } from 'lang/queryAst'
import {
addCloseToPipe,
addNewSketchLn,
compareVec2Epsilon,
} from 'lang/std/sketch'
import { kclManager, useKclContext } from 'lang/KclSinglton'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance' import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import { import {
angleBetweenInfo, angleBetweenInfo,
@ -33,11 +49,6 @@ import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance' import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
import useStateMachineCommands from 'hooks/useStateMachineCommands' import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig' import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
import { sceneInfra } from 'clientSideScene/sceneInfra'
import { getSketchQuaternion } from 'clientSideScene/sceneEntities'
import { startSketchOnDefault } from 'lang/modifyAst'
import { Program } from 'lang/wasm'
import { isSingleCursorInPipe } from 'lang/queryAst'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -81,10 +92,181 @@ export const ModelingMachineProvider = ({
const [modelingState, modelingSend, modelingActor] = useMachine( const [modelingState, modelingSend, modelingActor] = useMachine(
modelingMachine, modelingMachine,
{ {
// context: persistedSettings,
actions: { actions: {
'Modify AST': () => {},
'Update code selection cursors': () => {},
'show default planes': () => {
kclManager.showPlanes()
},
'create path': assign({
sketchEnginePathId: () => {
const sketchUuid = uuidv4()
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: sketchUuid,
cmd: {
type: 'start_path',
},
})
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edit_mode_enter',
target: sketchUuid,
},
})
return sketchUuid
},
}),
'AST start new sketch': assign(
({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
if (!axis) {
// Something really weird must have happened for this to happen.
console.error('axis is undefined for starting a new sketch')
return {}
}
if (!segmentId) {
// Something really weird must have happened for this to happen.
console.error('segmentId is undefined for starting a new sketch')
return {}
}
const _addStartSketch = addStartSketch(
kclManager.ast,
axis,
[roundOff(coords[0].x), roundOff(coords[0].y)],
[
roundOff(coords[1].x - coords[0].x),
roundOff(coords[1].y - coords[0].y),
]
)
const _modifiedAst = _addStartSketch.modifiedAst
const _pathToNode = _addStartSketch.pathToNode
const newCode = recast(_modifiedAst)
const astWithUpdatedSource = parse(newCode)
const updatedPipeNode = getNodeFromPath<PipeExpression>(
astWithUpdatedSource,
_pathToNode
).node
const startProfileAtCallExp = updatedPipeNode.body.find(
(exp) =>
exp.type === 'CallExpression' &&
exp.callee.name === 'startProfileAt'
)
if (startProfileAtCallExp)
engineCommandManager.artifactMap[sketchEnginePathId] = {
type: 'result',
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
commandType: 'start_path',
data: null,
raw: {} as any,
}
const lineCallExp = updatedPipeNode.body.find(
(exp) =>
exp.type === 'CallExpression' && exp.callee.name === 'line'
)
if (lineCallExp)
engineCommandManager.artifactMap[segmentId] = {
type: 'result',
range: [lineCallExp.start, lineCallExp.end],
commandType: 'extend_path',
parentId: sketchEnginePathId,
data: null,
raw: {} as any,
}
kclManager.executeAstMock(astWithUpdatedSource, true)
return {
sketchPathToNode: _pathToNode,
}
}
),
'AST add line segment': 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,
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': () => { 'sketch exit execute': () => {
kclManager.executeAst() kclManager.executeAst()
}, },
'set tool': () => {}, // TODO
'Set selection': assign(({ selectionRanges }, event) => { '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 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 const setSelections = event.data
@ -92,6 +274,36 @@ export const ModelingMachineProvider = ({
if (setSelections.selectionType === 'mirrorCodeMirrorSelections') if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
return { selectionRanges: setSelections.selection } return { selectionRanges: setSelections.selection }
else if (setSelections.selectionType === 'otherSelection') { 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 { const {
codeMirrorSelection, codeMirrorSelection,
selectionRangeTypeMap, selectionRangeTypeMap,
@ -172,6 +384,12 @@ export const ModelingMachineProvider = ({
}), }),
}, },
guards: { 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 }) => { 'has valid extrude selection': ({ selectionRanges }) => {
// A user can begin extruding if they either have 1+ faces selected or nothing selected // 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 // TODO: I believe this guard only allows for extruding a single face at a time
@ -183,10 +401,7 @@ export const ModelingMachineProvider = ({
return canExtrudeSelection(selectionRanges) return canExtrudeSelection(selectionRanges)
}, },
'Selection is on face': ({ selectionRanges }, { data }) => { 'Selection is one face': ({ selectionRanges }) => {
if (data?.forceNewSketch) return false
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
return false
return !!isCursorInSketchCommandRange( return !!isCursorInSketchCommandRange(
engineCommandManager.artifactMap, engineCommandManager.artifactMap,
selectionRanges selectionRanges
@ -194,41 +409,6 @@ export const ModelingMachineProvider = ({
}, },
}, },
services: { services: {
'AST-undo-startSketchOn': async ({ sketchPathToNode }) => {
if (!sketchPathToNode) return
const newAst: Program = JSON.parse(JSON.stringify(kclManager.ast))
const varDecIndex = sketchPathToNode[1][0]
// remove body item at varDecIndex
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
await kclManager.executeAstMock(newAst, { updates: 'code' })
sceneInfra.setCallbacks({
onClick: () => {},
onDrag: () => {},
})
},
'animate-to-face': async (_, { data: { plane, normal } }) => {
const { modifiedAst, pathToNode } = startSketchOnDefault(
kclManager.ast,
plane
)
await kclManager.updateAst(modifiedAst, false)
const quaternion = getSketchQuaternion(pathToNode, normal)
await sceneInfra.tweenCameraToQuaternion(quaternion)
return {
sketchPathToNode: pathToNode,
sketchNormalBackUp: normal,
}
},
'animate-to-sketch': async ({
sketchPathToNode,
sketchNormalBackUp,
}) => {
const quaternion = getSketchQuaternion(
sketchPathToNode || [],
sketchNormalBackUp
)
await sceneInfra.tweenCameraToQuaternion(quaternion)
},
'Get horizontal info': async ({ 'Get horizontal info': async ({
selectionRanges, selectionRanges,
}): Promise<SetSelections> => { }): Promise<SetSelections> => {
@ -362,6 +542,17 @@ export const ModelingMachineProvider = ({
} }
) )
useEffect(() => {
engineCommandManager.onPlaneSelected((plane_id: string) => {
if (modelingState.nextEvents.includes('Select default plane')) {
modelingSend({
type: 'Select default plane',
data: { planeId: plane_id },
})
}
})
}, [modelingSend, modelingState.nextEvents])
useEffect(() => { useEffect(() => {
kclManager.registerExecuteCallback(() => { kclManager.registerExecuteCallback(() => {
modelingSend({ type: 'Re-execute' }) modelingSend({ type: 'Re-execute' })
@ -374,7 +565,10 @@ export const ModelingMachineProvider = ({
send: modelingSend, send: modelingSend,
actor: modelingActor, actor: modelingActor,
commandBarConfig: modelingMachineConfig, commandBarConfig: modelingMachineConfig,
onCancel: () => modelingSend({ type: 'Cancel' }), onCancel: () => {
console.log('firing onCancel!!')
modelingSend({ type: 'Cancel' })
},
}) })
return ( return (

View File

@ -3,9 +3,8 @@ import { BrowserRouter } from 'react-router-dom'
import { GlobalStateProvider } from './GlobalStateProvider' import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar/CommandBar' import CommandBarProvider from './CommandBar/CommandBar'
import { import {
NETWORK_HEALTH_TEXT, NETWORK_CONTENT,
NetworkHealthIndicator, NetworkHealthIndicator,
NetworkHealthState,
} from './NetworkHealthIndicator' } from './NetworkHealthIndicator'
function TestWrap({ children }: { children: React.ReactNode }) { function TestWrap({ children }: { children: React.ReactNode }) {
@ -29,8 +28,8 @@ describe('NetworkHealthIndicator tests', () => {
fireEvent.click(screen.getByTestId('network-toggle')) fireEvent.click(screen.getByTestId('network-toggle'))
expect(screen.getByTestId('network')).toHaveTextContent( expect(screen.getByTestId('network-good')).toHaveTextContent(
NETWORK_HEALTH_TEXT[NetworkHealthState.Ok] NETWORK_CONTENT.good
) )
}) })
@ -44,8 +43,8 @@ describe('NetworkHealthIndicator tests', () => {
fireEvent.offline(window) fireEvent.offline(window)
fireEvent.click(screen.getByTestId('network-toggle')) fireEvent.click(screen.getByTestId('network-toggle'))
expect(screen.getByTestId('network')).toHaveTextContent( expect(screen.getByTestId('network-bad')).toHaveTextContent(
NETWORK_HEALTH_TEXT[NetworkHealthState.Disconnected] NETWORK_CONTENT.bad
) )
}) })
}) })

View File

@ -1,188 +1,43 @@
import { 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, ActionIconProps } from './ActionIcon' import { ActionIcon } from './ActionIcon'
import {
ConnectingType,
ConnectingTypeGroup,
DisconnectingType,
engineCommandManager,
EngineConnectionState,
EngineConnectionStateType,
ErrorType,
initialConnectingTypeGroupState,
} from '../lang/std/engineConnection'
import Tooltip from './Tooltip'
export enum NetworkHealthState { export const NETWORK_CONTENT = {
Ok, good: 'Network health is good',
Issue, bad: 'Network issue',
Disconnected,
} }
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = { const NETWORK_MESSAGES = {
[NetworkHealthState.Ok]: 'Connected', offline: 'You are offline',
[NetworkHealthState.Issue]: 'Problem',
[NetworkHealthState.Disconnected]: 'Offline',
}
type IconColorConfig = {
icon: string
bg: string
}
const hasIssueToIcon: Record<
string | number | symbol,
ActionIconProps['icon']
> = {
true: 'close',
undefined: 'horizontalDash',
false: 'checkmark',
}
const hasIssueToIconColors: Record<string | number | symbol, IconColorConfig> =
{
true: {
icon: 'text-destroy-80 dark:text-destroy-10',
bg: 'bg-destroy-10 dark:bg-destroy-80',
},
undefined: {
icon: 'text-chalkboard-70 dark:text-chalkboard-30',
bg: 'bg-chalkboard-30 dark:bg-chalkboard-70',
},
false: {
icon: 'text-chalkboard-110 dark:!text-chalkboard-10',
bg: 'bg-transparent dark:bg-transparent',
},
}
const overallConnectionStateColor: Record<NetworkHealthState, IconColorConfig> =
{
[NetworkHealthState.Ok]: {
icon: 'text-energy-80 dark:text-energy-10',
bg: 'bg-energy-10/30 dark:bg-energy-80/50',
},
[NetworkHealthState.Issue]: {
icon: 'text-destroy-80 dark:text-destroy-10',
bg: 'bg-destroy-10 dark:bg-destroy-80/80',
},
[NetworkHealthState.Disconnected]: {
icon: 'text-destroy-80 dark:text-destroy-10',
bg: 'bg-destroy-10 dark:bg-destroy-80',
},
}
const overallConnectionStateIcon: Record<
NetworkHealthState,
ActionIconProps['icon']
> = {
[NetworkHealthState.Ok]: 'network',
[NetworkHealthState.Issue]: 'networkCrossedOut',
[NetworkHealthState.Disconnected]: 'networkCrossedOut',
} }
export const NetworkHealthIndicator = () => { export const NetworkHealthIndicator = () => {
const [steps, setSteps] = useState(initialConnectingTypeGroupState) const [networkIssues, setNetworkIssues] = useState<string[]>([])
const [internetConnected, setInternetConnected] = useState<boolean>(true) const hasIssues = [...networkIssues.values()].length > 0
const [overallState, setOverallState] = useState<NetworkHealthState>(
NetworkHealthState.Ok
)
const [hasCopied, setHasCopied] = useState<boolean>(false)
const [error, setError] = useState<ErrorType | undefined>(undefined)
const issues: Record<ConnectingTypeGroup, boolean> = {
[ConnectingTypeGroup.WebSocket]: steps[ConnectingTypeGroup.WebSocket].some(
(a: [ConnectingType, boolean | undefined]) => a[1] === false
),
[ConnectingTypeGroup.ICE]: steps[ConnectingTypeGroup.ICE].some(
(a: [ConnectingType, boolean | undefined]) => a[1] === false
),
[ConnectingTypeGroup.WebRTC]: steps[ConnectingTypeGroup.WebRTC].some(
(a: [ConnectingType, boolean | undefined]) => a[1] === false
),
}
const hasIssues: boolean =
issues[ConnectingTypeGroup.WebSocket] ||
issues[ConnectingTypeGroup.ICE] ||
issues[ConnectingTypeGroup.WebRTC]
useEffect(() => { useEffect(() => {
setOverallState( const offlineListener = () =>
!internetConnected setNetworkIssues((issues) => {
? NetworkHealthState.Disconnected return [
: hasIssues ...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline),
? NetworkHealthState.Issue NETWORK_MESSAGES.offline,
: NetworkHealthState.Ok ]
) })
}, [hasIssues, internetConnected]) window.addEventListener('offline', offlineListener)
const onlineListener = () =>
setNetworkIssues((issues) => {
return [...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline)]
})
window.addEventListener('online', onlineListener)
useEffect(() => {
const cb1 = () => {
setSteps(initialConnectingTypeGroupState)
setInternetConnected(true)
}
const cb2 = () => {
setInternetConnected(false)
}
window.addEventListener('online', cb1)
window.addEventListener('offline', cb2)
return () => { return () => {
window.removeEventListener('online', cb1) window.removeEventListener('offline', offlineListener)
window.removeEventListener('offline', cb2) window.removeEventListener('online', onlineListener)
} }
}, []) }, [])
useEffect(() => {
engineCommandManager.onConnectionStateChange(
(engineConnectionState: EngineConnectionState) => {
let hasSetAStep = false
if (
engineConnectionState.type === EngineConnectionStateType.Connecting
) {
const groups = Object.values(steps)
for (let group of groups) {
for (let step of group) {
if (step[0] !== engineConnectionState.value.type) continue
step[1] = true
hasSetAStep = true
}
}
}
if (
engineConnectionState.type === EngineConnectionStateType.Disconnecting
) {
const groups = Object.values(steps)
for (let group of groups) {
for (let step of group) {
if (
engineConnectionState.value.type === DisconnectingType.Error
) {
if (
engineConnectionState.value.value.lastConnectingValue
?.type === step[0]
) {
step[1] = false
hasSetAStep = true
}
}
}
if (engineConnectionState.value.type === DisconnectingType.Error) {
setError(engineConnectionState.value.value)
}
}
}
if (hasSetAStep) {
setSteps(steps)
}
}
)
}, [])
return ( return (
<Popover className="relative"> <Popover className="relative">
<Popover.Button <Popover.Button
@ -190,94 +45,65 @@ export const NetworkHealthIndicator = () => {
'p-0 border-none bg-transparent dark:bg-transparent relative ' + 'p-0 border-none bg-transparent dark:bg-transparent relative ' +
(hasIssues (hasIssues
? 'focus-visible:outline-destroy-80' ? 'focus-visible:outline-destroy-80'
: 'focus-visible:outline-energy-80') : 'focus-visible:outline-succeed-80')
} }
data-testid="network-toggle" data-testid="network-toggle"
> >
<span className="sr-only">Network Health</span> <span className="sr-only">Network Health</span>
<ActionIcon <ActionIcon
icon={overallConnectionStateIcon[overallState]} icon={faWifi}
className="p-1" className="p-1"
iconClassName={overallConnectionStateColor[overallState].icon} iconClassName={
hasIssues
? 'text-destroy-80 dark:text-destroy-30'
: 'text-succeed-80 dark:text-succeed-30'
}
bgClassName={ bgClassName={
'rounded-sm ' + overallConnectionStateColor[overallState].bg 'bg-transparent dark:bg-transparent ' +
(hasIssues
? 'hover:bg-destroy-10/50 hover:dark:bg-destroy-80/50 rounded'
: 'hover:bg-succeed-10/50 hover:dark:bg-succeed-80/50 rounded')
} }
/> />
<Tooltip position="blockEnd" delay={750} className="ui-open:hidden">
Network Health ({NETWORK_HEALTH_TEXT[overallState]})
</Tooltip>
</Popover.Button> </Popover.Button>
<Popover.Panel className="absolute right-0 left-auto top-full mt-1 w-64 flex flex-col gap-1 align-stretch bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm"> <Popover.Panel className="absolute right-0 left-auto top-full mt-1 w-56 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch py-2 bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm">
<div {!hasIssues ? (
className={`flex items-center justify-between p-2 rounded-t-sm ${overallConnectionStateColor[overallState].bg} ${overallConnectionStateColor[overallState].icon}`} <span
> className="flex items-center justify-center gap-1 px-4"
<h2 className="text-sm font-sans font-normal">Network health</h2> data-testid="network-good"
<p
data-testid="network"
className="font-bold text-xs uppercase px-2 py-1 rounded-sm"
> >
{NETWORK_HEALTH_TEXT[overallState]} <ActionIcon
</p> icon="checkmark"
</div> bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded-sm'}
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80"> iconClassName={'text-succeed-80 dark:text-succeed-30'}
{Object.keys(steps).map((name) => ( />
<li {NETWORK_CONTENT.good}
key={name} </span>
className={'flex flex-col px-2 py-4 gap-1 last:mb-0 '} ) : (
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
<span
className="font-bold text-xs uppercase text-destroy-60 dark:text-destroy-50 px-4"
data-testid="network-bad"
> >
<div className="flex items-center text-left gap-1"> {NETWORK_CONTENT.bad}
<p className="flex-1">{name}</p> {networkIssues.length > 1 ? 's' : ''}
{internetConnected ? ( </span>
<ActionIcon {networkIssues.map((issue) => (
size="lg" <li
icon={ key={issue}
hasIssueToIcon[ className="flex items-center gap-1 py-2 my-2 last:mb-0"
issues[name as ConnectingTypeGroup].toString() >
] <ActionIcon
} icon={faExclamation}
iconClassName={ bgClassName={'bg-destroy-10/50 dark:bg-destroy-80/50 rounded'}
hasIssueToIconColors[ iconClassName={'text-destroy-80 dark:text-destroy-30'}
issues[name as ConnectingTypeGroup].toString() className="ml-4"
].icon />
} <p className="flex-1 mr-4">{issue}</p>
bgClassName={ </li>
'rounded-sm ' + ))}
hasIssueToIconColors[ </ul>
issues[name as ConnectingTypeGroup].toString() )}
].bg
}
/>
) : (
<ActionIcon
icon={hasIssueToIcon.true}
bgClassName={hasIssueToIconColors.true.bg}
iconClassName={hasIssueToIconColors.true.icon}
/>
)}
</div>
{issues[name as ConnectingTypeGroup] && (
<button
onClick={async () => {
await navigator.clipboard.writeText(
JSON.stringify(error, null, 2) || ''
)
setHasCopied(true)
setTimeout(() => setHasCopied(false), 5000)
}}
className="flex w-fit gap-2 items-center bg-transparent text-sm p-1 py-0 my-0 -mx-1 text-destroy-80 dark:text-destroy-10 hover:bg-transparent border-transparent dark:border-transparent hover:border-destroy-80 dark:hover:border-destroy-80 dark:hover:bg-destroy-80"
>
{hasCopied ? 'Copied' : 'Copy Error'}
<ActionIcon
size="lg"
icon={hasCopied ? 'clipboardCheckmark' : 'clipboardPlus'}
iconClassName="text-inherit dark:text-inherit"
bgClassName="!bg-transparent"
/>
</button>
)}
</li>
))}
</ul>
</Popover.Panel> </Popover.Panel>
</Popover> </Popover>
) )

View File

@ -1,6 +1,5 @@
import { FormEvent, useEffect, useRef, useState } from 'react' import { FormEvent, useEffect, useRef, useState } from 'react'
import { type ProjectWithEntryPointMetadata } from 'lib/types' import { type ProjectWithEntryPointMetadata, paths } from '../Router'
import { paths } from 'lib/paths'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { import {

View File

@ -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 ProjectSidebarMenu from './ProjectSidebarMenu' import ProjectSidebarMenu from './ProjectSidebarMenu'
import { type ProjectWithEntryPointMetadata } from 'lib/types' import { ProjectWithEntryPointMetadata } from '../Router'
import { GlobalStateProvider } from './GlobalStateProvider' import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar/CommandBar' import CommandBarProvider from './CommandBar/CommandBar'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'

View File

@ -1,8 +1,7 @@
import { Popover, Transition } from '@headlessui/react' import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { faHome } from '@fortawesome/free-solid-svg-icons' import { faHome } from '@fortawesome/free-solid-svg-icons'
import { type IndexLoaderData } from 'lib/types' import { IndexLoaderData, paths } from '../Router'
import { paths } from 'lib/paths'
import { isTauri } from '../lib/isTauri' import { isTauri } from '../lib/isTauri'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ExportButton } from './ExportButton' import { ExportButton } from './ExportButton'

View File

@ -11,13 +11,16 @@ import { getNormalisedCoordinates, throttle } from '../lib/utils'
import Loading from './Loading' import Loading from './Loading'
import { cameraMouseDragGuards } from 'lib/cameraControls' import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { getNodeFromPath } from 'lang/queryAst'
import { VariableDeclarator, recast, CallExpression } from 'lang/wasm'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { useKclContext } from 'lang/KclSingleton' import { kclManager, useKclContext } from 'lang/KclSinglton'
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp' import { changeSketchArguments } from 'lang/std/sketch'
export const Stream = ({ className = '' }: { className?: string }) => { export const Stream = ({ className = '' }) => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>() const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>()
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
@ -36,7 +39,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
})) }))
const { settings } = useGlobalStateContext() const { settings } = useGlobalStateContext()
const cameraControls = settings?.context?.cameraControls const cameraControls = settings?.context?.cameraControls
const { state } = useModelingContext() const { send, state, context } = useModelingContext()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()
useEffect(() => { useEffect(() => {
@ -50,10 +53,8 @@ export const Stream = ({ className = '' }: { className?: string }) => {
videoRef.current.srcObject = mediaStream videoRef.current.srcObject = mediaStream
}, [mediaStream]) }, [mediaStream])
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
if (!videoRef.current) return if (!videoRef.current) return
if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return
const { x, y } = getNormalisedCoordinates({ const { x, y } = getNormalisedCoordinates({
clientX: e.clientX, clientX: e.clientX,
clientY: e.clientY, clientY: e.clientY,
@ -61,6 +62,55 @@ export const Stream = ({ className = '' }: { className?: string }) => {
...streamDimensions, ...streamDimensions,
}) })
const newId = uuidv4()
const interactionGuards = cameraMouseDragGuards[cameraControls]
let interaction: CameraDragInteractionType_type = 'rotate'
if (
interactionGuards.pan.callback(e) ||
interactionGuards.pan.lenientDragStartButton === e.button
) {
interaction = 'pan'
} else if (
interactionGuards.rotate.callback(e) ||
interactionGuards.rotate.lenientDragStartButton === e.button
) {
interaction = 'rotate'
} else if (
interactionGuards.zoom.dragCallback(e) ||
interactionGuards.zoom.lenientDragStartButton === e.button
) {
interaction = 'zoom'
}
if (state.matches('Sketch.Move Tool')) {
if (
state.matches('Sketch.Move Tool.No move') ||
state.matches('Sketch.Move Tool.Move with execute')
) {
return
}
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'handle_mouse_drag_start',
window: { x, y },
},
cmd_id: newId,
})
} else if (!state.matches('Sketch.Line Tool')) {
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_start',
interaction,
window: { x, y },
},
cmd_id: newId,
})
}
setButtonDownInStream(e.button) setButtonDownInStream(e.button)
setClickCoords({ x, y }) setClickCoords({ x, y })
} }
@ -78,15 +128,13 @@ export const Stream = ({ className = '' }: { className?: string }) => {
}) })
}, Math.round(1000 / fps)) }, Math.round(1000 / fps))
const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({ const handleMouseUp: MouseEventHandler<HTMLVideoElement> = ({
clientX, clientX,
clientY, clientY,
ctrlKey, ctrlKey,
}) => { }) => {
if (!videoRef.current) return if (!videoRef.current) return
setButtonDownInStream(undefined) setButtonDownInStream(undefined)
if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return
const { x, y } = getNormalisedCoordinates({ const { x, y } = getNormalisedCoordinates({
clientX, clientX,
clientY, clientY,
@ -107,19 +155,206 @@ export const Stream = ({ className = '' }: { className?: string }) => {
cmd_id: newCmdId, cmd_id: newCmdId,
} }
if (!didDragInStream) { if (!didDragInStream && state.matches('Sketch no face')) {
command.cmd = {
type: 'select_with_point',
selection_type: 'add',
selected_at_window: { x, y },
}
engineCommandManager.sendSceneCommand(command)
} else if (!didDragInStream && state.matches('Sketch.Line Tool')) {
command.cmd = {
type: 'mouse_click',
window: { x, y },
}
engineCommandManager.sendSceneCommand(command).then(async (resp) => {
const entities_modified = resp?.data?.data?.entities_modified
if (!entities_modified) return
if (state.matches('Sketch.Line Tool.No Points')) {
send('Add point')
} else if (state.matches('Sketch.Line Tool.Point Added')) {
const curve = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: entities_modified[0],
},
})
const coords: { x: number; y: number }[] =
curve.data.data.control_points
// We need the normal for the plane we are on.
const plane = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'get_sketch_mode_plane',
},
})
const z_axis = plane.data.data.z_axis
// Get the current axis.
let currentAxis: 'xy' | 'xz' | 'yz' | '-xy' | '-xz' | '-yz' | null =
null
if (context.sketchPlaneId === kclManager.getPlaneId('xy')) {
if (z_axis.z === -1) {
currentAxis = '-xy'
} else {
currentAxis = 'xy'
}
} else if (context.sketchPlaneId === kclManager.getPlaneId('yz')) {
if (z_axis.x === -1) {
currentAxis = '-yz'
} else {
currentAxis = 'yz'
}
} else if (context.sketchPlaneId === kclManager.getPlaneId('xz')) {
if (z_axis.y === -1) {
currentAxis = '-xz'
} else {
currentAxis = 'xz'
}
}
send({
type: 'Add point',
data: {
coords,
axis: currentAxis,
segmentId: entities_modified[0],
},
})
} else if (state.matches('Sketch.Line Tool.Segment Added')) {
const curve = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: entities_modified[0],
},
})
const coords: { x: number; y: number }[] =
curve.data.data.control_points
send({
type: 'Add point',
data: { coords, axis: null, segmentId: entities_modified[0] },
})
}
})
} else if (
!didDragInStream &&
(state.matches('Sketch.SketchIdle') || state.matches('idle'))
) {
command.cmd = {
type: 'select_with_point',
selected_at_window: { x, y },
selection_type: 'add',
}
engineCommandManager.sendSceneCommand(command)
} else if (!didDragInStream && state.matches('Sketch.Move Tool')) {
command.cmd = { 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) { } else if (didDragInStream && state.matches('Sketch.Move Tool')) {
command.cmd = { command.cmd = {
type: 'handle_mouse_drag_end', type: 'handle_mouse_drag_end',
window: { x, y }, window: { x, y },
} }
void engineCommandManager.sendSceneCommand(command) engineCommandManager.sendSceneCommand(command).then(async () => {
if (!context.sketchPathToNode) return
getNodeFromPath<VariableDeclarator>(
kclManager.ast,
context.sketchPathToNode,
'VariableDeclarator'
)
// Get the current plane string for plane we are on.
let currentPlaneString = ''
if (context.sketchPlaneId === kclManager.getPlaneId('xy')) {
currentPlaneString = 'XY'
} else if (context.sketchPlaneId === kclManager.getPlaneId('yz')) {
currentPlaneString = 'YZ'
} else if (context.sketchPlaneId === kclManager.getPlaneId('xz')) {
currentPlaneString = 'XZ'
}
// Do not supporting editing/moving lines on a non-default plane.
// Eventually we can support this but for now we will just throw an
// error.
if (currentPlaneString === '') return
const pathInfo = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'path_get_info',
path_id: context.sketchEnginePathId,
},
})
const segmentsWithMappings = (
pathInfo?.data?.data?.segments as { command_id: string }[]
)
.filter(({ command_id }) => {
return command_id && engineCommandManager.artifactMap[command_id]
})
.map(({ command_id }) => command_id)
const segment2dInfo = await Promise.all(
segmentsWithMappings.map(async (segmentId) => {
const response = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: segmentId,
},
})
const controlPoints: [
{ x: number; y: number },
{ x: number; y: number }
] = response.data.data.control_points
return {
controlPoints,
segmentId,
}
})
)
let modifiedAst = { ...kclManager.ast }
let code = kclManager.code
for (const controlPoint of segment2dInfo) {
const range =
engineCommandManager.artifactMap[controlPoint.segmentId].range
if (!range) continue
const from = controlPoint.controlPoints[0]
const to = controlPoint.controlPoints[1]
const modded = changeSketchArguments(
modifiedAst,
kclManager.programMemory,
range,
[to.x, to.y],
[from.x, from.y]
)
modifiedAst = modded.modifiedAst
// update artifact map ranges now that we have updated the ast.
code = recast(modded.modifiedAst)
const astWithCurrentRanges = kclManager.safeParse(code)
if (!astWithCurrentRanges) return
const updateNode = getNodeFromPath<CallExpression>(
astWithCurrentRanges,
modded.pathToNode
).node
engineCommandManager.artifactMap[controlPoint.segmentId].range = [
updateNode.start,
updateNode.end,
]
}
kclManager.executeAstMock(modifiedAst, true)
})
} else { } else {
engineCommandManager.sendSceneCommand(command) engineCommandManager.sendSceneCommand(command)
} }
@ -129,8 +364,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
} }
const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => { const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => {
if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return
if (!clickCoords) return if (!clickCoords) return
const delta = const delta =
@ -143,19 +376,16 @@ export const Stream = ({ className = '' }: { className?: string }) => {
} }
return ( return (
<div <div id="stream" className={className}>
id="stream"
className={className}
onMouseUp={handleMouseUp}
onMouseDown={handleMouseDown}
onContextMenu={(e) => e.preventDefault()}
onContextMenuCapture={(e) => e.preventDefault()}
>
<video <video
ref={videoRef} ref={videoRef}
muted muted
autoPlay autoPlay
controls={false} controls={false}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onContextMenu={(e) => e.preventDefault()}
onContextMenuCapture={(e) => e.preventDefault()}
onWheel={handleScroll} onWheel={handleScroll}
onPlay={() => setIsLoading(false)} onPlay={() => setIsLoading(false)}
onMouseMoveCapture={handleMouseMove} onMouseMoveCapture={handleMouseMove}
@ -163,7 +393,6 @@ export const Stream = ({ className = '' }: { className?: string }) => {
disablePictureInPicture disablePictureInPicture
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }} style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
/> />
<ClientSideScene cameraControls={settings.context.cameraControls} />
{isLoading && ( {isLoading && (
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"> <div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<Loading> <Loading>

View File

@ -3,20 +3,20 @@ import ReactCodeMirror, {
ViewUpdate, ViewUpdate,
keymap, keymap,
} from '@uiw/react-codemirror' } from '@uiw/react-codemirror'
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec' import { FromServer, IntoServer } from 'editor/lsp/codec'
import Server from '../editor/plugins/lsp/server' import Server from '../editor/lsp/server'
import Client from '../editor/plugins/lsp/client' import Client from '../editor/lsp/client'
import { TEST } from 'env' import { TEST } from 'env'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useConvertToVariable } from 'hooks/useToolbarGuards' import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { useMemo, useRef } from 'react' import { useMemo } from 'react'
import { linter, lintGutter } from '@codemirror/lint' import { linter, lintGutter } from '@codemirror/lint'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { processCodeMirrorRanges } from 'lib/selections' import { processCodeMirrorRanges } from 'lib/selections'
import { LanguageServerClient } from 'editor/plugins/lsp' import { LanguageServerClient } from 'editor/lsp'
import kclLanguage from 'editor/plugins/lsp/kcl/language' import kclLanguage from 'editor/lsp/language'
import { EditorView, lineHighlightField } from 'editor/highlightextension' import { EditorView, lineHighlightField } from 'editor/highlightextension'
import { roundOff } from 'lib/utils' import { roundOff } from 'lib/utils'
import { kclErrToDiagnostic } from 'lang/errors' import { kclErrToDiagnostic } from 'lang/errors'
@ -24,12 +24,7 @@ import { CSSRuleObject } from 'tailwindcss/types/config'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import interact from '@replit/codemirror-interact' import interact from '@replit/codemirror-interact'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
import { kclManager, useKclContext } from 'lang/KclSingleton' import { kclManager, useKclContext } from 'lang/KclSinglton'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { sceneInfra } from 'clientSideScene/sceneInfra'
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
import { isTauri } from 'lib/isTauri'
import type * as LSP from 'vscode-languageserver-protocol'
export const editorShortcutMeta = { export const editorShortcutMeta = {
formatCode: { formatCode: {
@ -42,15 +37,6 @@ export const editorShortcutMeta = {
}, },
} }
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
// We only use workspace folders in Tauri since that is where we use more than
// one file.
if (isTauri()) {
return [{ uri: 'file://', name: 'ProjectRoot' }]
}
return []
}
export const TextEditor = ({ export const TextEditor = ({
theme, theme,
}: { }: {
@ -58,31 +44,25 @@ export const TextEditor = ({
}) => { }) => {
const { const {
editorView, editorView,
isKclLspServerReady, isLSPServerReady,
isCopilotLspServerReady,
setEditorView, setEditorView,
setIsKclLspServerReady, setIsLSPServerReady,
setIsCopilotLspServerReady,
isShiftDown, isShiftDown,
} = useStore((s) => ({ } = useStore((s) => ({
editorView: s.editorView, editorView: s.editorView,
isKclLspServerReady: s.isKclLspServerReady, isLSPServerReady: s.isLSPServerReady,
isCopilotLspServerReady: s.isCopilotLspServerReady,
setEditorView: s.setEditorView, setEditorView: s.setEditorView,
setIsKclLspServerReady: s.setIsKclLspServerReady, setIsLSPServerReady: s.setIsLSPServerReady,
setIsCopilotLspServerReady: s.setIsCopilotLspServerReady,
isShiftDown: s.isShiftDown, isShiftDown: s.isShiftDown,
})) }))
const { code, errors } = useKclContext() const { code, errors } = useKclContext()
const lastEvent = useRef({ event: '', time: Date.now() })
const { const {
context: { selectionRanges, selectionRangeTypeMap }, context: { selectionRanges, selectionRangeTypeMap },
send, send,
state,
} = useModelingContext() } = useModelingContext()
const { settings: { context: { textWrapping } = {} } = {}, auth } = const { settings: { context: { textWrapping } = {} } = {} } =
useGlobalStateContext() useGlobalStateContext()
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { enable: convertEnabled, handleClick: convertCallback } = const { enable: convertEnabled, handleClick: convertCallback } =
@ -91,20 +71,20 @@ export const TextEditor = ({
// So this is a bit weird, we need to initialize the lsp server and client. // So this is a bit weird, we need to initialize the lsp server and client.
// But the server happens async so we break this into two parts. // But the server happens async so we break this into two parts.
// Below is the client and server promise. // Below is the client and server promise.
const { lspClient: kclLspClient } = useMemo(() => { const { lspClient } = useMemo(() => {
const intoServer: IntoServer = new IntoServer() const intoServer: IntoServer = new IntoServer()
const fromServer: FromServer = FromServer.create() const fromServer: FromServer = FromServer.create()
const client = new Client(fromServer, intoServer) const client = new Client(fromServer, intoServer)
if (!TEST) { if (!TEST) {
Server.initialize(intoServer, fromServer).then((lspServer) => { Server.initialize(intoServer, fromServer).then((lspServer) => {
lspServer.start('kcl') lspServer.start()
setIsKclLspServerReady(true) setIsLSPServerReady(true)
}) })
} }
const lspClient = new LanguageServerClient({ client, name: 'kcl' }) const lspClient = new LanguageServerClient({ client })
return { lspClient } return { lspClient }
}, [setIsKclLspServerReady]) }, [setIsLSPServerReady])
// Here we initialize the plugin which will start the client. // Here we initialize the plugin which will start the client.
// When we have multi-file support the name of the file will be a dep of // When we have multi-file support the name of the file will be a dep of
@ -113,57 +93,19 @@ export const TextEditor = ({
// We do not want to restart the server, its just wasteful. // We do not want to restart the server, its just wasteful.
const kclLSP = useMemo(() => { const kclLSP = useMemo(() => {
let plugin = null let plugin = null
if (isKclLspServerReady && !TEST) { if (isLSPServerReady && !TEST) {
// Set up the lsp plugin. // Set up the lsp plugin.
const lsp = kclLanguage({ const lsp = kclLanguage({
// When we have more than one file, we'll need to change this. // When we have more than one file, we'll need to change this.
documentUri: `file:///we-just-have-one-file-for-now.kcl`, documentUri: `file:///we-just-have-one-file-for-now.kcl`,
workspaceFolders: getWorkspaceFolders(), workspaceFolders: null,
client: kclLspClient, client: lspClient,
}) })
plugin = lsp plugin = lsp
} }
return plugin return plugin
}, [kclLspClient, isKclLspServerReady]) }, [lspClient, isLSPServerReady])
const { lspClient: copilotLspClient } = useMemo(() => {
const intoServer: IntoServer = new IntoServer()
const fromServer: FromServer = FromServer.create()
const client = new Client(fromServer, intoServer)
if (!TEST) {
Server.initialize(intoServer, fromServer).then((lspServer) => {
const token = auth?.context?.token
lspServer.start('copilot', token)
setIsCopilotLspServerReady(true)
})
}
const lspClient = new LanguageServerClient({ client, name: 'copilot' })
return { lspClient }
}, [setIsCopilotLspServerReady])
// Here we initialize the plugin which will start the client.
// When we have multi-file support the name of the file will be a dep of
// this use memo, as well as the directory structure, which I think is
// a good setup because it will restart the client but not the server :)
// We do not want to restart the server, its just wasteful.
const copilotLSP = useMemo(() => {
let plugin = null
if (isCopilotLspServerReady && !TEST) {
// Set up the lsp plugin.
const lsp = copilotPlugin({
// When we have more than one file, we'll need to change this.
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
workspaceFolders: getWorkspaceFolders(),
client: copilotLspClient,
allowHTMLContent: true,
})
plugin = lsp
}
return plugin
}, [copilotLspClient, isCopilotLspServerReady])
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => { // const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = (newCode: string) => { const onChange = (newCode: string) => {
@ -173,12 +115,6 @@ export const TextEditor = ({
if (!editorView) { if (!editorView) {
setEditorView(viewUpdate.view) setEditorView(viewUpdate.view)
} }
if (sceneInfra.selected) return // mid drag
const ignoreEvents: ModelingMachineEvent['type'][] = [
'Equip Line tool',
'Equip tangential arc to',
]
if (ignoreEvents.includes(state.event.type)) return
const eventInfo = processCodeMirrorRanges({ const eventInfo = processCodeMirrorRanges({
codeMirrorRanges: viewUpdate.state.selection.ranges, codeMirrorRanges: viewUpdate.state.selection.ranges,
selectionRanges, selectionRanges,
@ -186,20 +122,7 @@ export const TextEditor = ({
isShiftDown, isShiftDown,
}) })
if (!eventInfo) return if (!eventInfo) return
const deterministicEventInfo = {
...eventInfo,
engineEvents: eventInfo.engineEvents.map((e) => ({
...e,
cmd_id: 'static',
})),
}
const stringEvent = JSON.stringify(deterministicEventInfo)
if (
stringEvent === lastEvent.current.event &&
Date.now() - lastEvent.current.time < 500
)
return // don't repeat events
lastEvent.current = { event: stringEvent, time: Date.now() }
send(eventInfo.modelingEvent) send(eventInfo.modelingEvent)
eventInfo.engineEvents.forEach((event) => eventInfo.engineEvents.forEach((event) =>
engineCommandManager.sendSceneCommand(event) engineCommandManager.sendSceneCommand(event)
@ -238,7 +161,6 @@ export const TextEditor = ({
] as Extension[] ] as Extension[]
if (kclLSP) extensions.push(kclLSP) if (kclLSP) extensions.push(kclLSP)
if (copilotLSP) extensions.push(copilotLSP)
// These extensions have proven to mess with vitest // These extensions have proven to mess with vitest
if (!TEST) { if (!TEST) {

View File

@ -11,7 +11,7 @@ import {
getTransformInfos, getTransformInfos,
PathToNodeMap, PathToNodeMap,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSinglton'
export function equalAngleInfo({ export function equalAngleInfo({
selectionRanges, selectionRanges,

View File

@ -11,7 +11,7 @@ import {
getTransformInfos, getTransformInfos,
PathToNodeMap, PathToNodeMap,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSinglton'
export function setEqualLengthInfo({ export function setEqualLengthInfo({
selectionRanges, selectionRanges,

View File

@ -10,7 +10,7 @@ import {
getTransformInfos, getTransformInfos,
transformAstSketchLines, transformAstSketchLines,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSinglton'
export function horzVertInfo( export function horzVertInfo(
selectionRanges: Selections, selectionRanges: Selections,

View File

@ -15,7 +15,7 @@ import {
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal' import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
import { createVariableDeclaration } from '../../lang/modifyAst' import { createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers' import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSinglton'
const getModalInfo = createInfoModal(GetInfoModal) const getModalInfo = createInfoModal(GetInfoModal)

View File

@ -10,7 +10,7 @@ import {
getRemoveConstraintsTransforms, getRemoveConstraintsTransforms,
transformAstSketchLines, transformAstSketchLines,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSinglton'
export function removeConstrainingValuesInfo({ export function removeConstrainingValuesInfo({
selectionRanges, selectionRanges,

View File

@ -19,7 +19,7 @@ import {
createVariableDeclaration, createVariableDeclaration,
} from '../../lang/modifyAst' } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers' import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSinglton'
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal) const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
@ -139,7 +139,7 @@ export function applyConstraintAxisAlign({
constraint, constraint,
}).transforms }).transforms
let finalValue = createIdentifier('ZERO') let finalValue = createIdentifier('_0')
return transformAstSketchLines({ return transformAstSketchLines({
ast: JSON.parse(JSON.stringify(kclManager.ast)), ast: JSON.parse(JSON.stringify(kclManager.ast)),

View File

@ -14,7 +14,7 @@ import {
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal' import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
import { createVariableDeclaration } from '../../lang/modifyAst' import { createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers' import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSinglton'
const getModalInfo = createInfoModal(GetInfoModal) const getModalInfo = createInfoModal(GetInfoModal)

View File

@ -13,7 +13,7 @@ import {
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal' import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst' import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers' import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSinglton'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
const getModalInfo = createInfoModal(GetInfoModal) const getModalInfo = createInfoModal(GetInfoModal)

View File

@ -21,7 +21,7 @@ import {
} from '../../lang/modifyAst' } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers' import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { normaliseAngle } from '../../lib/utils' import { normaliseAngle } from '../../lib/utils'
import { kclManager } from 'lang/KclSingleton' import { kclManager } from 'lang/KclSinglton'
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal) const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
@ -89,16 +89,12 @@ export async function applyConstraintAngleLength({
isReferencingXAxis && angleOrLength === 'setAngle' isReferencingXAxis && angleOrLength === 'setAngle'
let forceVal = valueUsedInTransform || 0 let forceVal = valueUsedInTransform || 0
let calcIdentifier = createIdentifier('ZERO') let calcIdentifier = createIdentifier('_0')
if (isReferencingYAxisAngle) { if (isReferencingYAxisAngle) {
calcIdentifier = createIdentifier( calcIdentifier = createIdentifier(forceVal < 0 ? '_270' : '_90')
forceVal < 0 ? 'THREE_QUARTER_TURN' : 'QUARTER_TURN'
)
forceVal = normaliseAngle(forceVal + (forceVal < 0 ? 90 : -90)) forceVal = normaliseAngle(forceVal + (forceVal < 0 ? 90 : -90))
} else if (isReferencingXAxisAngle) { } else if (isReferencingXAxisAngle) {
calcIdentifier = createIdentifier( calcIdentifier = createIdentifier(Math.abs(forceVal) > 90 ? '_180' : '_0')
Math.abs(forceVal) > 90 ? 'HALF_TURN' : 'ZERO'
)
forceVal = forceVal =
Math.abs(forceVal) > 90 ? normaliseAngle(forceVal - 180) : forceVal Math.abs(forceVal) > 90 ? normaliseAngle(forceVal - 180) : forceVal
} }
@ -116,7 +112,7 @@ export async function applyConstraintAngleLength({
) )
if ( if (
isReferencingYAxisAngle || isReferencingYAxisAngle ||
(isReferencingXAxisAngle && calcIdentifier.name !== 'ZERO') (isReferencingXAxisAngle && calcIdentifier.name !== '_0')
) { ) {
finalValue = createBinaryExpressionWithUnary([calcIdentifier, finalValue]) finalValue = createBinaryExpressionWithUnary([calcIdentifier, finalValue])
} }

View File

@ -4,7 +4,7 @@ import { faBars, faBug, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
import { faGithub } from '@fortawesome/free-brands-svg-icons' import { faGithub } from '@fortawesome/free-brands-svg-icons'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useState } from 'react' import { Fragment, useState } from 'react'
import { paths } from 'lib/paths' import { paths } from '../Router'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'

View File

@ -1,7 +1,7 @@
import { Dialog } from '@headlessui/react' import { Dialog } from '@headlessui/react'
import { useState } from 'react' import { useState } from 'react'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { useKclContext } from 'lang/KclSingleton' import { useKclContext } from 'lang/KclSinglton'
export function WasmErrBanner() { export function WasmErrBanner() {
const [isBannerDismissed, setBannerDismissed] = useState(false) const [isBannerDismissed, setBannerDismissed] = useState(false)

View File

@ -65,7 +65,6 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
afterInitializedHooks: (() => Promise<void>)[] = [] afterInitializedHooks: (() => Promise<void>)[] = []
#fromServer: FromServer #fromServer: FromServer
private serverCapabilities: LSP.ServerCapabilities<any> = {} private serverCapabilities: LSP.ServerCapabilities<any> = {}
private notifyFn: ((message: LSP.NotificationMessage) => void) | null = null
constructor(fromServer: FromServer, intoServer: IntoServer) { constructor(fromServer: FromServer, intoServer: IntoServer) {
super( super(
@ -168,15 +167,9 @@ export default class Client extends jsrpc.JSONRPCServerAndClient {
return this.serverCapabilities return this.serverCapabilities
} }
setNotifyFn(fn: (message: LSP.NotificationMessage) => void): void {
this.notifyFn = fn
}
async processNotifications(): Promise<void> { async processNotifications(): Promise<void> {
for await (const notification of this.#fromServer.notifications) { for await (const notification of this.#fromServer.notifications) {
if (this.notifyFn) { await this.receiveAndSend(notification)
this.notifyFn(notification)
}
} }
} }

View File

@ -1,68 +1,7 @@
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import Client from './client' import Client from './client'
import { SemanticToken, deserializeTokens } from './kcl/semantic_tokens' import { LanguageServerPlugin } from './plugin'
import { LanguageServerPlugin } from 'editor/plugins/lsp/plugin' import { SemanticToken, deserializeTokens } from './semantic_tokens'
export interface CopilotGetCompletionsParams {
doc: {
source: string
tabSize: number
indentSize: number
insertSpaces: boolean
path: string
uri: string
relativePath: string
languageId: string
position: {
line: number
character: number
}
}
}
interface CopilotGetCompletionsResult {
completions: {
text: string
position: {
line: number
character: number
}
uuid: string
range: {
start: {
line: number
character: number
}
end: {
line: number
character: number
}
}
displayText: string
point: {
line: number
character: number
}
region: {
start: {
line: number
character: number
}
end: {
line: number
character: number
}
}
}[]
}
interface CopilotAcceptCompletionParams {
uuid: string
}
interface CopilotRejectCompletionParams {
uuids: string[]
}
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/ // https://microsoft.github.io/language-server-protocol/specifications/specification-current/
@ -78,9 +17,6 @@ interface LSPRequestMap {
LSP.SemanticTokensParams, LSP.SemanticTokensParams,
LSP.SemanticTokens LSP.SemanticTokens
] ]
getCompletions: [CopilotGetCompletionsParams, CopilotGetCompletionsResult]
notifyAccepted: [CopilotAcceptCompletionParams, any]
notifyRejected: [CopilotRejectCompletionParams, any]
} }
// Client to server // Client to server
@ -90,22 +26,26 @@ interface LSPNotifyMap {
'textDocument/didOpen': LSP.DidOpenTextDocumentParams 'textDocument/didOpen': LSP.DidOpenTextDocumentParams
} }
export interface LanguageServerClientOptions { // Server to client
client: Client interface LSPEventMap {
name: string 'textDocument/publishDiagnostics': LSP.PublishDiagnosticsParams
} }
export interface LanguageServerOptions { export type Notification = {
// We assume this is the main project directory, we are currently working in. [key in keyof LSPEventMap]: {
workspaceFolders: LSP.WorkspaceFolder[] jsonrpc: '2.0'
documentUri: string id?: null | undefined
allowHTMLContent: boolean method: key
client: LanguageServerClient params: LSPEventMap[key]
}
}[keyof LSPEventMap]
export interface LanguageServerClientOptions {
client: Client
} }
export class LanguageServerClient { export class LanguageServerClient {
private client: Client private client: Client
private name: string
public ready: boolean public ready: boolean
@ -115,31 +55,23 @@ export class LanguageServerClient {
private isUpdatingSemanticTokens: boolean = false private isUpdatingSemanticTokens: boolean = false
private semanticTokens: SemanticToken[] = [] private semanticTokens: SemanticToken[] = []
private queuedUids: string[] = []
constructor(options: LanguageServerClientOptions) { constructor(options: LanguageServerClientOptions) {
this.plugins = [] this.plugins = []
this.client = options.client this.client = options.client
this.name = options.name
this.ready = false this.ready = false
this.queuedUids = []
this.initializePromise = this.initialize() this.initializePromise = this.initialize()
} }
async initialize() { async initialize() {
// Start the client in the background. // Start the client in the background.
this.client.setNotifyFn(this.processNotifications.bind(this))
this.client.start() this.client.start()
this.ready = true this.ready = true
} }
getName(): string {
return this.name
}
getServerCapabilities(): LSP.ServerCapabilities<any> { getServerCapabilities(): LSP.ServerCapabilities<any> {
return this.client.getServerCapabilities() return this.client.getServerCapabilities()
} }
@ -158,11 +90,6 @@ export class LanguageServerClient {
} }
async updateSemanticTokens(uri: string) { async updateSemanticTokens(uri: string) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.semanticTokensProvider) {
return
}
// Make sure we can only run, if we aren't already running. // Make sure we can only run, if we aren't already running.
if (!this.isUpdatingSemanticTokens) { if (!this.isUpdatingSemanticTokens) {
this.isUpdatingSemanticTokens = true this.isUpdatingSemanticTokens = true
@ -187,18 +114,10 @@ export class LanguageServerClient {
} }
async textDocumentHover(params: LSP.HoverParams) { async textDocumentHover(params: LSP.HoverParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.hoverProvider) {
return
}
return await this.request('textDocument/hover', params) return await this.request('textDocument/hover', params)
} }
async textDocumentCompletion(params: LSP.CompletionParams) { async textDocumentCompletion(params: LSP.CompletionParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.completionProvider) {
return
}
return await this.request('textDocument/completion', params) return await this.request('textDocument/completion', params)
} }
@ -226,35 +145,7 @@ export class LanguageServerClient {
return this.client.notify(method, params) return this.client.notify(method, params)
} }
async getCompletion(params: CopilotGetCompletionsParams) { private processNotification(notification: Notification) {
const response = await this.request('getCompletions', params)
//
this.queuedUids = [...response.completions.map((c) => c.uuid)]
return response
}
async accept(uuid: string) {
const badUids = this.queuedUids.filter((u) => u !== uuid)
this.queuedUids = []
await this.acceptCompletion({ uuid })
await this.rejectCompletions({ uuids: badUids })
}
async reject() {
const badUids = this.queuedUids
this.queuedUids = []
return await this.rejectCompletions({ uuids: badUids })
}
async acceptCompletion(params: CopilotAcceptCompletionParams) {
return await this.request('notifyAccepted', params)
}
async rejectCompletions(params: CopilotRejectCompletionParams) {
return await this.request('notifyRejected', params)
}
private processNotifications(notification: LSP.NotificationMessage) {
for (const plugin of this.plugins) plugin.processNotification(notification) for (const plugin of this.plugins) plugin.processNotification(notification)
} }
} }

View File

@ -5,8 +5,8 @@ import {
defineLanguageFacet, defineLanguageFacet,
LanguageSupport, LanguageSupport,
} from '@codemirror/language' } from '@codemirror/language'
import { LanguageServerClient } from 'editor/plugins/lsp' import { LanguageServerClient } from '.'
import { kclPlugin } from '.' import { kclPlugin } from './plugin'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import { parser as jsParser } from '@lezer/javascript' import { parser as jsParser } from '@lezer/javascript'
import { EditorState } from '@uiw/react-codemirror' import { EditorState } from '@uiw/react-codemirror'
@ -14,7 +14,7 @@ import { EditorState } from '@uiw/react-codemirror'
const data = defineLanguageFacet({}) const data = defineLanguageFacet({})
export interface LanguageOptions { export interface LanguageOptions {
workspaceFolders: LSP.WorkspaceFolder[] workspaceFolders: LSP.WorkspaceFolder[] | null
documentUri: string documentUri: string
client: LanguageServerClient client: LanguageServerClient
} }

View File

@ -9,8 +9,8 @@ import {
NodeType, NodeType,
NodeSet, NodeSet,
} from '@lezer/common' } from '@lezer/common'
import { LanguageServerClient } from 'editor/plugins/lsp' import { LanguageServerClient } from '.'
import { posToOffset } from 'editor/plugins/lsp/util' import { posToOffset } from './plugin'
import { SemanticToken } from './semantic_tokens' import { SemanticToken } from './semantic_tokens'
import { DocInput } from '@codemirror/language' import { DocInput } from '@codemirror/language'
import { tags, styleTags } from '@lezer/highlight' import { tags, styleTags } from '@lezer/highlight'

View File

@ -1,7 +1,13 @@
import { completeFromList } from '@codemirror/autocomplete' import { autocompletion, completeFromList } from '@codemirror/autocomplete'
import { setDiagnostics } from '@codemirror/lint' import { setDiagnostics } from '@codemirror/lint'
import { Facet } from '@codemirror/state' import { Facet } from '@codemirror/state'
import { EditorView, Tooltip } from '@codemirror/view' import {
EditorView,
ViewPlugin,
Tooltip,
hoverTooltip,
tooltips,
} from '@codemirror/view'
import { import {
DiagnosticSeverity, DiagnosticSeverity,
CompletionItemKind, CompletionItemKind,
@ -16,18 +22,10 @@ import type {
} from '@codemirror/autocomplete' } from '@codemirror/autocomplete'
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol' import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
import type { ViewUpdate, PluginValue } from '@codemirror/view' import type { ViewUpdate, PluginValue } from '@codemirror/view'
import type { Text } from '@codemirror/state'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import { LanguageServerClient } from 'editor/plugins/lsp' import { LanguageServerClient, Notification } from '.'
import { Marked } from '@ts-stack/markdown' import { Marked } from '@ts-stack/markdown'
import { posToOffset } from 'editor/plugins/lsp/util'
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
export const documentUri = Facet.define<string, string>({ combine: useLast })
export const languageId = Facet.define<string, string>({ combine: useLast })
export const workspaceFolders = Facet.define<
LSP.WorkspaceFolder[],
LSP.WorkspaceFolder[]
>({ combine: useLast })
const changesDelay = 500 const changesDelay = 500
@ -35,22 +33,31 @@ const CompletionItemKindMap = Object.fromEntries(
Object.entries(CompletionItemKind).map(([key, value]) => [value, key]) Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
) as Record<CompletionItemKind, string> ) as Record<CompletionItemKind, string>
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
const documentUri = Facet.define<string, string>({ combine: useLast })
const languageId = Facet.define<string, string>({ combine: useLast })
const client = Facet.define<LanguageServerClient, LanguageServerClient>({
combine: useLast,
})
export interface LanguageServerOptions {
workspaceFolders: LSP.WorkspaceFolder[] | null
documentUri: string
allowHTMLContent: boolean
client: LanguageServerClient
}
export class LanguageServerPlugin implements PluginValue { export class LanguageServerPlugin implements PluginValue {
public client: LanguageServerClient public client: LanguageServerClient
private documentUri: string private documentUri: string
private languageId: string private languageId: string
private workspaceFolders: LSP.WorkspaceFolder[]
private documentVersion: number private documentVersion: number
constructor( constructor(private view: EditorView, private allowHTMLContent: boolean) {
client: LanguageServerClient, this.client = this.view.state.facet(client)
private view: EditorView,
private allowHTMLContent: boolean
) {
this.client = client
this.documentUri = this.view.state.facet(documentUri) this.documentUri = this.view.state.facet(documentUri)
this.languageId = this.view.state.facet(languageId) this.languageId = this.view.state.facet(languageId)
this.workspaceFolders = this.view.state.facet(workspaceFolders)
this.documentVersion = 0 this.documentVersion = 0
this.client.attachPlugin(this) this.client.attachPlugin(this)
@ -231,28 +238,11 @@ export class LanguageServerPlugin implements PluginValue {
return completeFromList(options)(context) return completeFromList(options)(context)
} }
processNotification(notification: LSP.NotificationMessage) { processNotification(notification: Notification) {
try { try {
switch (notification.method) { switch (notification.method) {
case 'textDocument/publishDiagnostics': case 'textDocument/publishDiagnostics':
this.processDiagnostics( this.processDiagnostics(notification.params)
notification.params as PublishDiagnosticsParams
)
break
case 'window/logMessage':
console.log(
'[lsp] [window/logMessage]',
this.client.getName(),
notification.params
)
break
case 'window/showMessage':
console.log(
'[lsp] [window/showMessage]',
this.client.getName(),
notification.params
)
break
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -294,6 +284,83 @@ export class LanguageServerPlugin implements PluginValue {
} }
} }
export function kclPlugin(options: LanguageServerOptions) {
let plugin: LanguageServerPlugin | null = null
return [
client.of(options.client),
documentUri.of(options.documentUri),
languageId.of('kcl'),
ViewPlugin.define(
(view) =>
(plugin = new LanguageServerPlugin(view, options.allowHTMLContent))
),
hoverTooltip(
(view, pos) =>
plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ??
null
),
tooltips({
position: 'absolute',
}),
autocompletion({
override: [
async (context) => {
if (plugin == null) return null
const { state, pos, explicit } = context
const line = state.doc.lineAt(pos)
let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
let trigChar: string | undefined
if (
!explicit &&
plugin.client
.getServerCapabilities()
.completionProvider?.triggerCharacters?.includes(
line.text[pos - line.from - 1]
)
) {
trigKind = CompletionTriggerKind.TriggerCharacter
trigChar = line.text[pos - line.from - 1]
}
if (
trigKind === CompletionTriggerKind.Invoked &&
!context.matchBefore(/\w+$/)
) {
return null
}
return await plugin.requestCompletion(
context,
offsetToPos(state.doc, pos),
{
triggerKind: trigKind,
triggerCharacter: trigChar,
}
)
},
],
}),
]
}
export function posToOffset(
doc: Text,
pos: { line: number; character: number }
): number | undefined {
if (pos.line >= doc.lines) return
const offset = doc.line(pos.line + 1).from + pos.character
if (offset > doc.length) return
return offset
}
function offsetToPos(doc: Text, offset: number) {
const line = doc.lineAt(offset)
return {
line: line.number - 1,
character: offset - line.from,
}
}
function formatContents( function formatContents(
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[] contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
): string { ): string {

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