@pierremtb

Merge branch 'main' into pierremtb/issue1349
This commit is contained in:
Pierre Jacquier
2024-02-13 05:08:54 -05:00
197 changed files with 24703 additions and 5584 deletions

View File

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

View File

@ -1 +1,2 @@
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"
} }
} }

29
.github/workflows/announce_release.yml vendored Normal file
View File

@ -0,0 +1,29 @@
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,6 +46,7 @@ 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
@ -253,6 +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
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
@ -335,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.0.0' uses: 'google-github-actions/auth@v2.1.1'
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.0.0 uses: google-github-actions/setup-gcloud@v2.1.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.0.0 uses: google-github-actions/upload-cloud-storage@v2.1.0
with: with:
path: artifact path: artifact
glob: '*/Zoo*' glob: '*/Zoo*'
@ -353,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.0.0 uses: google-github-actions/upload-cloud-storage@v2.1.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.0.0 uses: google-github-actions/upload-cloud-storage@v2.1.0
with: with:
path: last_download.json path: last_download.json
destination: ${{ env.BUCKET_DIR }} destination: ${{ env.BUCKET_DIR }}

4
.gitignore vendored
View File

@ -50,3 +50,7 @@ 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/modelingMachine.typegen.ts src/machines/**.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
echo 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets2.env printf 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets.env
``` ```
then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens

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

View File

@ -1,7 +1,7 @@
ISO-10303-21; ISO-10303-21;
HEADER; HEADER;
FILE_DESCRIPTION((('kittycad.io export')), '2;1'); FILE_DESCRIPTION((('zoo.dev export')), '2;1');
FILE_NAME('dump.step', '1970-01-01T00:00:00.0+00:00', ('Author unknown'), ('Organization unknown'), 'kittycad.io beta', 'kittycad.io', 'Authorization unknown'); FILE_NAME('dump.step', '1970-01-01T00:00:00.0+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', '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.819152044288992, 0)); #379 = DIRECTION('NONE', (-0.5735764363510459, -0.8191520442889919, 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.573576436351046, 0.8191520442889918, -0)); #395 = DIRECTION('NONE', (0.5735764363510459, 0.8191520442889917, -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, .T.); #462 = FACE_OUTER_BOUND('NONE', #354, .F.);
#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.57357645 -0 0.819152 facet normal 0.5735763 0 0.8191522
outer loop outer loop
vertex 3.4311862 -4 -0.625 vertex 4.323779 -0 -1.25
vertex 3.4311862 -0 -0.625 vertex 3.4311862 -0 -0.625
vertex 2.5385938 -4 0 vertex 3.4311862 -4 -0.625
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.5735763 0 0.8191522 facet normal 0.57357645 -0 0.819152
outer loop outer loop
vertex 2.5385938 -4 0
vertex 3.4311862 -0 -0.625 vertex 3.4311862 -0 -0.625
vertex 3.4311862 -4 -0.625 vertex 2.5385938 -0 0
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 4.146974 -0 0.75 vertex 3.342784 -4 0.375
vertex 4.146974 -4 0.75 vertex 2.5385938 -0 0
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 4.146974 -0 0.75
vertex 5.755354 -0 1.5
vertex 5.755354 -4 1.5 vertex 5.755354 -4 1.5
vertex 4.146974 -4 0.75
endloop endloop
endfacet endfacet
facet normal 0.42261824 0 -0.9063078 facet normal 0.42261824 0 -0.9063078
outer loop outer loop
vertex 3.342784 -4 0.375 vertex 4.146974 -4 0.75
vertex 2.5385938 -0 0
vertex 3.342784 -0 0.375 vertex 3.342784 -0 0.375
vertex 4.146974 -0 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 5.755354 -4 1.5 vertex 5.755354 -4 1.5
vertex 4.146974 -4 0.75
vertex 4.146974 -0 0.75 vertex 4.146974 -0 0.75
vertex 5.755354 -0 1.5
endloop endloop
endfacet endfacet
facet normal 0 0 -1 facet normal 0 0 -1
@ -258,6 +258,13 @@ 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
@ -265,11 +272,53 @@ facet normal 0 1 0
vertex 3.0950184 -0 -1 vertex 3.0950184 -0 -1
endloop endloop
endfacet endfacet
facet normal 0 0.99999994 0
outer loop
vertex 4.146974 -0 0.75
vertex 5.644507 -0 2
vertex 5.755354 -0 1.5
endloop
endfacet
facet normal -0 1 0
outer loop
vertex 0 -0 1
vertex 3.5 -0 1
vertex 2.5385938 -0 0
endloop
endfacet
facet normal 0 1 0 facet normal 0 1 0
outer loop outer loop
vertex 3.342784 -0 0.375 vertex 0 -0 1
vertex 3.5 -0 1 vertex 2.5385938 -0 0
vertex 0 -0 0
endloop
endfacet
facet normal -0 1 0
outer loop
vertex 5.644507 -0 2
vertex 9.5 -0 2
vertex 5.755354 -0 1.5
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 9.5 -0 2
vertex 9.5 -0 1.5
vertex 5.755354 -0 1.5
endloop
endfacet
facet normal 0 1 -0
outer loop
vertex 4.146974 -0 0.75 vertex 4.146974 -0 0.75
vertex 3.5 -0 1
vertex 5.644507 -0 2
endloop
endfacet
facet normal 0 0.99999994 0
outer loop
vertex 2.5385938 -0 0
vertex 3.4311862 -0 -0.625
vertex 3.0950184 -0 -1
endloop endloop
endfacet endfacet
facet normal 0 0.99999994 0 facet normal 0 0.99999994 0
@ -281,86 +330,37 @@ facet normal 0 0.99999994 0
endfacet endfacet
facet normal 0 1 0 facet normal 0 1 0
outer loop outer loop
vertex 0 -0 -1
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 2.5385938 -0 0
endloop
endfacet
facet normal 0 0.99999994 -0
outer loop
vertex 9.5 -0 -3
vertex 6.108964 -0 -2.5 vertex 6.108964 -0 -2.5
vertex 9.5 -0 -2.5
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 9.5 -0 -3
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 5.9513144 -0 -3
vertex 4.323779 -0 -1.25 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 vertex 6.108964 -0 -2.5
endloop endloop
endfacet endfacet
facet normal 0 1 0 facet normal 0 1 0
outer loop outer loop
vertex 5.644507 -0 2 vertex 6.108964 -0 -2.5
vertex 5.755354 -0 1.5 vertex 9.5 -0 -3
vertex 4.146974 -0 0.75 vertex 5.9513144 -0 -3
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 3.0950184 -0 -1 vertex 3.0950184 -0 -1
vertex 2.5385938 -0 0 vertex 0 -0 -1
vertex 3.4311862 -0 -0.625
endloop
endfacet
facet normal 0 1 -0
outer loop
vertex 4.146974 -0 0.75
vertex 3.5 -0 1
vertex 5.644507 -0 2
endloop
endfacet
facet normal 0 1 -0
outer loop
vertex 9.5 -0 1.5
vertex 5.755354 -0 1.5
vertex 9.5 -0 2
endloop
endfacet
facet normal 0 1 -0
outer loop
vertex 5.755354 -0 1.5
vertex 5.644507 -0 2
vertex 9.5 -0 2
endloop endloop
endfacet endfacet
facet normal 0 1 0 facet normal 0 1 0
outer loop outer loop
vertex 2.5385938 -0 0 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,7 +1,5 @@
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'
@ -53,40 +51,38 @@ 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 Promise.all([ await u.doAndWaitForImageDiff(
u.doAndWaitForImageDiff( () => page.getByRole('button', { name: 'Start Sketch' }).click(),
() => page.getByRole('button', { name: 'Start Sketch' }).click(), 200
200 )
),
u.waitForDefaultPlanesVisibilityChange(),
])
// select a plane // select a plane
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter') await page.mouse.click(700, 200)
await u.waitForCmdReceive('set_tool')
await u.doAndWaitForCmd( await expect(page.locator('.cm-content')).toHaveText(
() => page.getByRole('button', { name: 'Line' }).click(), `const part001 = startSketchOn('-XZ')`
'set_tool'
) )
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
const startXPx = 600 const startXPx = 600
await u.doAndWaitForCmd( await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10), const startAt = '[23.89, -32.23]'
'mouse_click', await expect(page.locator('.cm-content'))
false .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.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const startAt = '[18.26, -24.63]' const num = 24.11
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}, %)
@ -97,27 +93,22 @@ 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}], %)`) |> line([0, ${num + 0.01}], %)`)
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}], %) |> line([0, ${num + 0.01}], %)
|> line([-36.69, 0], %)`) |> line([-48, 0], %)`)
// deselect line tool // deselect line tool
await u.doAndWaitForCmd( await page.getByRole('button', { name: 'Line' }).click()
() => page.getByRole('button', { name: 'Line' }).click(), await page.waitForTimeout(100)
'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 u.doAndWaitForCmd( await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10)
() => page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10), await page.waitForTimeout(100)
'select_with_point'
)
await u.closeDebugPanel()
// hold down shift // hold down shift
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
@ -133,7 +124,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}], %) |> line([0, ${num + 0.01}], %)
|> angledLine([180, segLen('seg01', %)], %)`) |> angledLine([180, segLen('seg01', %)], %)`)
}) })
@ -271,59 +262,41 @@ 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 camCmd: EngineCommand = { const camPos: [number, number, number] = [100, 100, 100]
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: EngineCommand viewCmd: [number, number, number]
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 u.openDebugPanel() await page.waitForTimeout(300) // wait for animation
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.clearCommandLogs() await u.openAndClearDebugPanel()
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"]')
@ -333,51 +306,40 @@ test('Can create sketches on all planes and their back sides', async ({
const codeTemplate = ( const codeTemplate = (
plane = 'XY', plane = 'XY',
sign = '' rounded = false
) => `const part001 = startSketchOn('${plane}') ) => `const part001 = startSketchOn('${plane}')
|> startProfileAt([${sign}6.88, -9.29], %) |> startProfileAt([28.91, -39${rounded ? '' : '.01'}], %)`
|> line([${sign}6.95, 0], %)`
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camCmd, viewCmd: camPos,
expectedCode: codeTemplate('XY'), expectedCode: codeTemplate('XY'),
clickCoords: { x: 700, y: 350 }, // red plane clickCoords: { x: 600, y: 388 }, // 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: camCmd, viewCmd: camPos,
expectedCode: codeTemplate('YZ'), expectedCode: codeTemplate('YZ', true),
clickCoords: { x: 1000, y: 200 }, // green plane clickCoords: { x: 700, y: 300 }, // green plane
}) })
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camCmd, viewCmd: camPos,
expectedCode: codeTemplate('XZ', '-'), expectedCode: codeTemplate('XZ'),
clickCoords: { x: 630, y: 130 }, // blue plane clickCoords: { x: 700, y: 80 }, // 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', '-'), expectedCode: codeTemplate('-XY', true),
clickCoords: { x: 705, y: 136 }, // back of red plane clickCoords: { x: 601, y: 118 }, // back of red plane
}) })
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camCmdBackSide, viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-YZ', '-'), expectedCode: codeTemplate('-YZ'),
clickCoords: { x: 1000, y: 350 }, // back of green plane clickCoords: { x: 730, y: 219 }, // back of green plane
}) })
await TestSinglePlane({ await TestSinglePlane({
viewCmd: camCmdBackSide, viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-XZ'), expectedCode: codeTemplate('-XZ', true),
clickCoords: { x: 600, y: 400 }, // back of blue plane clickCoords: { x: 680, y: 427 }, // back of blue plane
}) })
}) })
@ -387,7 +349,6 @@ 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.
@ -478,38 +439,36 @@ 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 = () => page.mouse.click(700, 250) const xAxisClick = () =>
const emptySpaceClick = () => page.mouse.click(700, 300) page.mouse.click(700, 250).then(() => page.waitForTimeout(100))
const topHorzSegmentClick = () => page.mouse.click(700, 285) const emptySpaceClick = () =>
const bottomHorzSegmentClick = () => page.mouse.click(750, 393) page.mouse.click(728, 343).then(() => page.waitForTimeout(100))
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 u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter') await page.mouse.click(700, 200)
await u.waitForCmdReceive('set_tool') await page.waitForTimeout(700) // wait for animation
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
)
const startXPx = 600 const startXPx = 600
await u.doAndWaitForCmd( await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10), const startAt = '[23.89, -32.23]'
'mouse_click', await expect(page.locator('.cm-content'))
false .toHaveText(`const part001 = startSketchOn('-XZ')
) |> 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 startAt = '[18.26, -24.63]' const num = 24.11
const num = '18.43' const num2 = '48'
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}, %)
@ -520,20 +479,17 @@ 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}], %)`) |> line([0, ${num + 0.01}], %)`)
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}], %) |> line([0, ${num + 0.01}], %)
|> line([-${num2}, 0], %)`) |> line([-${num2}, 0], %)`)
// deselect line tool // deselect line tool
await u.doAndWaitForCmd( await page.getByRole('button', { name: 'Line' }).click()
() => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
)
await u.closeDebugPanel() await u.closeDebugPanel()
const selectionSequence = async () => { const selectionSequence = async () => {
@ -555,79 +511,72 @@ 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 u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false) await topHorzSegmentClick()
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 u.doAndWaitForCmd(xAxisClick, 'select_with_point', false) await xAxisClick()
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 u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false) await emptySpaceClick()
// same selection but click the axis first // same selection but click the axis first
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false) await xAxisClick()
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false) await topHorzSegmentClick()
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 u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false) await emptySpaceClick()
// 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 u.doAndWaitForCmd( await page.getByText(` |> line([-${num2}, 0], %)`).click()
() => 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 u.doAndWaitForCmd(xAxisClick, 'select_with_point', false) await xAxisClick()
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 u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false) await emptySpaceClick()
// 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 u.doAndWaitForCmd( await page.getByText(` |> line([-${num2}, 0], %)`).click()
() => page.getByText(` |> line([-${num2}, 0], %)`).click(), await page.waitForTimeout(300)
'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 u.doAndWaitForCmd(bottomHorzSegmentClick, 'select_with_point', false) // another segment, bottom one await bottomHorzSegmentClick()
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 u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false) await emptySpaceClick()
} }
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.doAndWaitForCmd( await u.openAndClearDebugPanel()
() => page.getByRole('button', { name: 'Exit Sketch' }).click(), await page.getByRole('button', { name: 'Exit Sketch' }).click()
'edit_mode_exit' await page.waitForTimeout(200)
)
// 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 u.doAndWaitForCmd(topHorzSegmentClick, 'select_clear', false) await topHorzSegmentClick()
await page.waitForTimeout(200)
// enter sketch again // enter sketch again
await u.doAndWaitForCmd( await page.getByRole('button', { name: 'Start Sketch' }).click()
() => page.getByRole('button', { name: 'Start Sketch' }).click(), await page.waitForTimeout(700) // wait for animation
'edit_mode_enter',
false
)
// hover again and check it works // hover again and check it works
await selectionSequence() await selectionSequence()
@ -697,6 +646,8 @@ 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')
@ -710,10 +661,7 @@ 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')
@ -735,3 +683,166 @@ 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 (token) => {
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)
})
})
})

View File

@ -40,19 +40,8 @@ test('change camera, show planes', async ({ page, context }) => {
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
const camCmd: EngineCommand = { const camPos: [number, number, number] = [0, 85, 85]
type: 'modeling_cmd_req', await u.updateCamPosition(camPos)
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()
@ -62,13 +51,11 @@ 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({
@ -77,10 +64,8 @@ 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.sendCustomCmd(camCmd) await u.updateCamPosition(camPos)
await u.waitForCmdReceive('default_camera_look_at')
await u.clearCommandLogs() await u.clearCommandLogs()
await u.closeDebugPanel() await u.closeDebugPanel()
@ -93,12 +78,10 @@ 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({
@ -107,10 +90,8 @@ 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.sendCustomCmd(camCmd) await u.updateCamPosition(camPos)
await u.waitForCmdReceive('default_camera_look_at')
await u.clearCommandLogs() await u.clearCommandLogs()
await u.closeDebugPanel() await u.closeDebugPanel()
@ -119,17 +100,15 @@ 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, 350) await page.mouse.move(700, 300)
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({
@ -164,11 +143,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: _180, angle: HALF_TURN,
offset: -armThick, offset: -armThick,
intersectTag: 'seg04' intersectTag: 'seg04'
}, %) }, %)
|> angledLineToY([segAng('seg04', %) + 180, _0], %) |> angledLineToY([segAng('seg04', %) + 180, ZERO], %)
|> angledLineToY({ |> angledLineToY({
angle: -bottomAng, angle: -bottomAng,
to: -totalHeightHalf - armThick, to: -totalHeightHalf - armThick,
@ -177,12 +156,12 @@ const part001 = startSketchOn('-XZ')
|> xLineTo(segEndX('seg03', %) + 0, %) |> xLineTo(segEndX('seg03', %) + 0, %)
|> yLine(-segLen('seg01', %), %) |> yLine(-segLen('seg01', %), %)
|> angledLineThatIntersects({ |> angledLineThatIntersects({
angle: _180, angle: HALF_TURN,
offset: -armThick, offset: -armThick,
intersectTag: 'seg02' intersectTag: 'seg02'
}, %) }, %)
|> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %) |> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %)
|> xLineTo(_0, %) |> xLineTo(ZERO, %)
|> close(%) |> close(%)
|> extrude(4, %)` |> extrude(4, %)`
) )
@ -191,7 +170,6 @@ 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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View File

@ -80,10 +80,21 @@ 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

@ -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.46", "@kittycad/lib": "^0.0.50",
"@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",
@ -24,6 +24,7 @@
"@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",
@ -47,6 +48,7 @@
"react-router-dom": "^6.14.2", "react-router-dom": "^6.14.2",
"sketch-helpers": "^0.0.4", "sketch-helpers": "^0.0.4",
"swr": "^2.2.2", "swr": "^2.2.2",
"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,7 +86,9 @@
"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",
@ -115,6 +119,7 @@
"@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",
@ -126,21 +131,26 @@
"@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"
} }

138
patches/three+0.160.0.patch Normal file
View File

@ -0,0 +1,138 @@
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,5 +78,4 @@ 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

@ -0,0 +1,26 @@
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.")

BIN
public/discord-avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

1180
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@ tauri-build = { version = "2.0.0-beta", features = [] }
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
kittycad = "0.2.42" kittycad = "0.2.50"
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"
@ -26,12 +26,9 @@ tauri-plugin-fs = { version = "2.0.0-beta.0" }
tauri-plugin-http = { version = "2.0.0-beta.0" } tauri-plugin-http = { version = "2.0.0-beta.0" }
tauri-plugin-os = { version = "2.0.0-beta.0" } tauri-plugin-os = { version = "2.0.0-beta.0" }
tauri-plugin-shell = { version = "2.0.0-beta.0" } tauri-plugin-shell = { version = "2.0.0-beta.0" }
tokio = { version = "1.34.0", features = ["time"] } tokio = { version = "1.36.0", features = ["time"] }
toml = "0.8.2" toml = "0.8.2"
[target.'cfg(target_os = "linux")'.dependencies]
tauri-driver = "0.1.3"
[features] [features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.

View File

@ -1,73 +0,0 @@
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,10 +19,11 @@ 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 } from 'react-router-dom' import { useLoaderData, useNavigate } from 'react-router-dom'
import { IndexLoaderData } from './Router' import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { onboardingPaths } from 'routes/Onboarding' import { onboardingPaths } from 'routes/Onboarding/paths'
import { cameraMouseDragGuards } from 'lib/cameraControls' import { cameraMouseDragGuards } from 'lib/cameraControls'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models' import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { CodeMenu } from 'components/CodeMenu' import { CodeMenu } from 'components/CodeMenu'
@ -31,9 +32,13 @@ 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 {
@ -71,6 +76,13 @@ 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(
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
@ -83,10 +95,12 @@ export function App() {
useEngineConnectionSubscriptions() useEngineConnectionSubscriptions()
const debounceSocketSend = throttle<EngineCommand>((message) => { const debounceSocketSend = throttle<EngineCommand>((message) => {
void engineCommandManager.sendSceneCommand(message) engineCommandManager.sendSceneCommand(message)
}, 16) }, 1000 / 15)
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => { const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
e.nativeEvent.preventDefault() if (state.matches('Sketch')) {
return
}
const { x, y } = getNormalisedCoordinates({ const { x, y } = getNormalisedCoordinates({
clientX: e.clientX, clientX: e.clientX,
@ -97,37 +111,15 @@ export function App() {
const newCmdId = uuidv4() const newCmdId = uuidv4()
if (buttonDownInStream === undefined) { if (buttonDownInStream === undefined) {
if (state.matches('Sketch.Line Tool')) { debounceSocketSend({
debounceSocketSend({ type: 'modeling_cmd_req',
type: 'modeling_cmd_req', cmd: {
cmd_id: newCmdId, type: 'highlight_set_entity',
cmd: { selected_at_window: { x, y },
type: 'mouse_move', },
window: { x, y }, cmd_id: newCmdId,
}, })
})
} else {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'highlight_set_entity',
selected_at_window: { x, y },
},
cmd_id: newCmdId,
})
}
} else { } 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] const interactionGuards = cameraMouseDragGuards[cameraControls]
let interaction: CameraDragInteractionType_type let interaction: CameraDragInteractionType_type
@ -238,6 +230,7 @@ export function App() {
open={openPanes.includes('debug')} open={openPanes.includes('debug')}
/> />
)} )}
{/* <CamToggle /> */}
</div> </div>
) )
} }

View File

@ -14,10 +14,7 @@ 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, { import Onboarding, { onboardingRoutes } from './routes/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'
@ -48,9 +45,12 @@ 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/KclSinglton' import { KclContextProvider, kclManager } from 'lang/KclSingleton'
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({
@ -84,43 +84,8 @@ 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?: FileHandle
}
export type ProjectWithEntryPointMetadata = FileHandle & {
entrypointMetadata: FileInfo
}
export type HomeLoaderData = {
projects: ProjectWithEntryPointMetadata[]
newDefaultDirectory?: string
}
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0] type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
const addGlobalContextToElements = ( const addGlobalContextToElements = (
@ -152,18 +117,18 @@ const router = createBrowserRouter(
{ {
path: paths.FILE + '/:id', path: paths.FILE + '/:id',
element: ( element: (
<Auth> <KclContextProvider>
<FileMachineProvider> <Auth>
<KclContextProvider> <FileMachineProvider>
<ModelingMachineProvider> <ModelingMachineProvider>
<Outlet /> <Outlet />
<App /> <App />
</ModelingMachineProvider> </ModelingMachineProvider>
<WasmErrBanner /> <WasmErrBanner />
</KclContextProvider> </FileMachineProvider>
</FileMachineProvider> {!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />} </Auth>
</Auth> </KclContextProvider>
), ),
id: paths.FILE, id: paths.FILE,
loader: async ({ loader: async ({
@ -217,6 +182,10 @@ const router = createBrowserRouter(
const children = await readDir(projectPath) const children = await readDir(projectPath)
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: {
@ -256,7 +225,7 @@ const router = createBrowserRouter(
<Home /> <Home />
</Auth> </Auth>
), ),
loader: async () => { loader: async (): Promise<HomeLoaderData | Response> => {
if (!isTauri()) { if (!isTauri()) {
return redirect(paths.FILE + '/' + BROWSER_FILE_NAME) return redirect(paths.FILE + '/' + BROWSER_FILE_NAME)
} }

View File

@ -89,44 +89,48 @@ export const Toolbar = () => {
</li> </li>
)} )}
{state.matches('Sketch') && !state.matches('idle') && ( {state.matches('Sketch') && !state.matches('idle') && (
<li className="contents"> <>
<ActionButton <li className="contents" key="line-button">
Element="button" <ActionButton
onClick={() => Element="button"
state.matches('Sketch.Line Tool') onClick={() =>
? send('CancelSketch') state?.matches('Sketch.Line tool')
: send('Equip tool') ? send('CancelSketch')
} : send('Equip Line tool')
aria-pressed={state.matches('Sketch.Line Tool')} }
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80" aria-pressed={state?.matches('Sketch.Line tool')}
icon={{ className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
icon: 'line', icon={{
bgClassName, icon: 'line',
}} bgClassName,
> }}
Line >
</ActionButton> Line
</li> </ActionButton>
)} </li>
{state.matches('Sketch') && ( <li className="contents" key="tangential-arc-button">
<li className="contents"> <ActionButton
<ActionButton Element="button"
Element="button" onClick={() =>
onClick={() => state.matches('Sketch.Tangential arc to')
state.matches('Sketch.Move Tool') ? send('CancelSketch')
? send('CancelSketch') : send('Equip tangential arc to')
: send('Equip move tool') }
} aria-pressed={state.matches('Sketch.Tangential arc to')}
aria-pressed={state.matches('Sketch.Move 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: 'move', bgClassName,
bgClassName, }}
}} disabled={
> !state.can('Equip tangential arc to') &&
Move !state.matches('Sketch.Tangential arc to')
</ActionButton> }
</li> >
Tangential Arc
</ActionButton>
</li>
</>
)} )}
{state.matches('Sketch.SketchIdle') && {state.matches('Sketch.SketchIdle') &&
state.nextEvents state.nextEvents
@ -151,7 +155,7 @@ export const Toolbar = () => {
return 0 return 0
}) })
.map((eventName) => ( .map((eventName) => (
<li className="contents"> <li className="contents" key={eventName}>
<ActionButton <ActionButton
Element="button" Element="button"
className="text-sm" className="text-sm"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
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

View File

@ -0,0 +1,358 @@
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 './clientSideScene'
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import { ARROWHEAD } from './setup'
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

@ -0,0 +1,28 @@
import { Quaternion } from 'three'
import { isQuaternionVertical } from './setup'
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,6 +1,6 @@
import { ActionIcon, ActionIconProps } from './ActionIcon' import { ActionIcon, ActionIconProps } from './ActionIcon'
import React from 'react' import React from 'react'
import { paths } from '../Router' import { paths } from 'lib/paths'
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 { IndexLoaderData } from '../Router' import { type IndexLoaderData } from 'lib/types'
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/KclSinglton' import { kclManager } from 'lang/KclSingleton'
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/KclSinglton' import { kclManager, useKclContext } from 'lang/KclSingleton'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { executeAst } from 'useStore' import { executeAst } from 'useStore'
@ -138,38 +138,37 @@ export function useCalc({
}, [kclManager.ast, kclManager.programMemory, selectionRange]) }, [kclManager.ast, kclManager.programMemory, selectionRange])
useEffect(() => { useEffect(() => {
const execAstAndSetResult = async () => { try {
const code = `const __result__ = ${value}` const code = `const __result__ = ${value}`
const ast = parse(code) const ast = parse(code)
const _programMem: any = { root: {}, return: null } const _programMem: any = { root: {}, return: null }
availableVarInfo.variables.forEach(({ key, value }) => { availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] } _programMem.root[key] = { type: 'userVal', value, __meta: [] }
}) })
const { programMemory } = await 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)
), ),
}).then(({ programMemory }) => {
const resultDeclaration = ast.body.find(
(a) =>
a.type === 'VariableDeclaration' &&
a.declarations?.[0]?.id?.name === '__result__'
)
const init =
resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.root?.__result__?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init)
}) })
const resultDeclaration = ast.body.find( } catch (e) {
(a) =>
a.type === 'VariableDeclaration' &&
a.declarations?.[0]?.id?.name === '__result__'
)
const init =
resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.root?.__result__?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init)
}
execAstAndSetResult().catch(() => {
setCalcResult('NAN') setCalcResult('NAN')
setValueNode(null) setValueNode(null)
}) }
}, [value, availableVarInfo]) }, [value, availableVarInfo])
return { return {

View File

@ -0,0 +1,75 @@
import { useState, useEffect } from 'react'
import { setupSingleton } from '../clientSideScene/setup'
import { engineCommandManager } from 'lang/std/engineConnection'
import { throttle, isReducedMotion } from 'lib/utils'
const updateDollyZoom = throttle(
(newFov: number) => setupSingleton.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 () => {
setupSingleton.dollyZoom(fov)
})
}, [])
const toggleCamera = () => {
if (isPerspective) {
isReducedMotion()
? setupSingleton.useOrthographicCamera()
: setupSingleton.animateToOrthographic()
} else {
isReducedMotion()
? setupSingleton.usePerspectiveCamera()
: setupSingleton.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) {
setupSingleton.controls.enableRotate = false
} else {
setupSingleton.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/KclSinglton' import { kclManager } from 'lang/KclSingleton'
export const CodeMenu = ({ children }: PropsWithChildren) => { export const CodeMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } = const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
@ -77,6 +77,24 @@ 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, { useState } from 'react' import React, { ReactNode, 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,15 +99,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
) : typeof argumentsToSubmit[argName] === 'object' ? ( ) : typeof argumentsToSubmit[argName] === 'object' ? (
JSON.stringify(argumentsToSubmit[argName]) JSON.stringify(argumentsToSubmit[argName])
) : ( ) : (
argumentsToSubmit[argName] <em>{argumentsToSubmit[argName] as ReactNode}</em>
)
) : 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/KclSinglton' import { useKclContext } from 'lang/KclSingleton'
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 { commandBarSend } = useCommandsContext() const { commandBarState, 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,8 +59,16 @@ 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

@ -4,6 +4,8 @@ export type CustomIconName =
| 'arrowRight' | 'arrowRight'
| 'arrowUp' | 'arrowUp'
| 'checkmark' | 'checkmark'
| 'clipboardPlus'
| 'clipboardCheckmark'
| 'close' | 'close'
| 'equal' | 'equal'
| 'extrude' | 'extrude'
@ -13,8 +15,11 @@ export type CustomIconName =
| 'folderPlus' | 'folderPlus'
| 'gear' | 'gear'
| 'horizontal' | 'horizontal'
| 'horizontalDash'
| 'line' | 'line'
| 'move' | 'move'
| 'network'
| 'networkCrossedOut'
| 'parallel' | 'parallel'
| 'search' | 'search'
| 'sketch' | 'sketch'
@ -107,6 +112,38 @@ 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
fill-rule="evenodd"
clip-rule="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
fill-rule="evenodd"
clip-rule="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
@ -249,6 +286,22 @@ 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
fill-rule="evenodd"
clip-rule="evenodd"
d="M14 10.5H6V9.5H14V10.5Z"
fill="currentColor"
/>
</svg>
)
case 'line': case 'line':
return ( return (
<svg <svg
@ -281,6 +334,38 @@ 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
fill-rule="evenodd"
clip-rule="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
fill-rule="evenodd"
clip-rule="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,6 +1,7 @@
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/setup'
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => { export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
return ( return (
@ -15,6 +16,7 @@ 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

@ -0,0 +1,43 @@
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

@ -93,7 +93,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
if (values.type === 'ply' || values.type === 'stl') { if (values.type === 'ply' || values.type === 'stl') {
values.selection = { type: 'default_scene' } values.selection = { type: 'default_scene' }
} }
void engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
type: 'export', type: 'export',

View File

@ -1,6 +1,7 @@
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 { IndexLoaderData, paths } from '../Router' import { type IndexLoaderData } from 'lib/types'
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,4 +1,5 @@
import { IndexLoaderData, paths } from 'Router' import { type IndexLoaderData } from 'lib/types'
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/plugin-fs' import { FileEntry } from '@tauri-apps/plugin-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 '../Router' import { paths } from 'lib/paths'
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'
@ -101,7 +101,7 @@ export const GlobalStateProvider = ({
goToSignInPage: () => { goToSignInPage: () => {
navigate(paths.SIGN_IN) navigate(paths.SIGN_IN)
void logout() logout()
}, },
goToIndexPage: () => { goToIndexPage: () => {
if (window.location.pathname.includes(paths.SIGN_IN)) { if (window.location.pathname.includes(paths.SIGN_IN)) {

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/KclSinglton' import { useKclContext } from 'lang/KclSingleton'
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/KclSinglton' import { useKclContext } from 'lang/KclSingleton'
interface MemoryPanelProps extends CollapsiblePanelProps { interface MemoryPanelProps extends CollapsiblePanelProps {
theme?: Exclude<Themes, Themes.System> theme?: Exclude<Themes, Themes.System>

View File

@ -13,23 +13,7 @@ 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 { v4 as uuidv4 } from 'uuid' import { kclManager, useKclContext } from 'lang/KclSingleton'
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,
@ -49,6 +33,9 @@ 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 { setupSingleton } from 'clientSideScene/setup'
import { getSketchQuaternion } from 'clientSideScene/clientSideScene'
import { startSketchOnDefault } from 'lang/modifyAst'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -92,181 +79,10 @@ 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': () => {
void kclManager.showPlanes()
},
'create path': assign({
sketchEnginePathId: () => {
const sketchUuid = uuidv4()
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: sketchUuid,
cmd: {
type: 'start_path',
},
})
void 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,
}
void 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
void 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,
})
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' },
})
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'default_camera_disable_sketch_mode' },
})
void kclManager.executeAstMock(_modifiedAst, true)
// updateAst(_modifiedAst, true)
}
},
'sketch exit execute': () => { 'sketch exit execute': () => {
void 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
@ -274,36 +90,6 @@ 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,
@ -384,12 +170,6 @@ 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
@ -409,6 +189,29 @@ export const ModelingMachineProvider = ({
}, },
}, },
services: { services: {
'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 setupSingleton.tweenCameraToQuaternion(quaternion)
return {
sketchPathToNode: pathToNode,
sketchNormalBackUp: normal,
}
},
'animate-to-sketch': async ({
sketchPathToNode,
sketchNormalBackUp,
}) => {
const quaternion = getSketchQuaternion(
sketchPathToNode || [],
sketchNormalBackUp
)
await setupSingleton.tweenCameraToQuaternion(quaternion)
},
'Get horizontal info': async ({ 'Get horizontal info': async ({
selectionRanges, selectionRanges,
}): Promise<SetSelections> => { }): Promise<SetSelections> => {
@ -542,17 +345,6 @@ 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' })
@ -565,10 +357,7 @@ export const ModelingMachineProvider = ({
send: modelingSend, send: modelingSend,
actor: modelingActor, actor: modelingActor,
commandBarConfig: modelingMachineConfig, commandBarConfig: modelingMachineConfig,
onCancel: () => { onCancel: () => modelingSend({ type: 'Cancel' }),
console.log('firing onCancel!!')
modelingSend({ type: 'Cancel' })
},
}) })
return ( return (

View File

@ -3,8 +3,9 @@ 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_CONTENT, NETWORK_HEALTH_TEXT,
NetworkHealthIndicator, NetworkHealthIndicator,
NetworkHealthState,
} from './NetworkHealthIndicator' } from './NetworkHealthIndicator'
function TestWrap({ children }: { children: React.ReactNode }) { function TestWrap({ children }: { children: React.ReactNode }) {
@ -28,8 +29,8 @@ describe('NetworkHealthIndicator tests', () => {
fireEvent.click(screen.getByTestId('network-toggle')) fireEvent.click(screen.getByTestId('network-toggle'))
expect(screen.getByTestId('network-good')).toHaveTextContent( expect(screen.getByTestId('network')).toHaveTextContent(
NETWORK_CONTENT.good NETWORK_HEALTH_TEXT[NetworkHealthState.Ok]
) )
}) })
@ -43,8 +44,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-bad')).toHaveTextContent( expect(screen.getByTestId('network')).toHaveTextContent(
NETWORK_CONTENT.bad NETWORK_HEALTH_TEXT[NetworkHealthState.Disconnected]
) )
}) })
}) })

View File

@ -1,41 +1,186 @@
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 } from './ActionIcon' import { ActionIcon, ActionIconProps } from './ActionIcon'
import {
ConnectingType,
ConnectingTypeGroup,
DisconnectingType,
engineCommandManager,
EngineConnectionState,
EngineConnectionStateType,
ErrorType,
initialConnectingTypeGroupState,
} from '../lang/std/engineConnection'
import Tooltip from './Tooltip'
export const NETWORK_CONTENT = { export enum NetworkHealthState {
good: 'Network health is good', Ok,
bad: 'Network issue', Issue,
Disconnected,
} }
const NETWORK_MESSAGES = { export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
offline: 'You are offline', [NetworkHealthState.Ok]: 'Connected',
[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 [networkIssues, setNetworkIssues] = useState<string[]>([]) const [steps, setSteps] = useState(initialConnectingTypeGroupState)
const hasIssues = [...networkIssues.values()].length > 0 const [internetConnected, setInternetConnected] = useState<boolean>(true)
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(() => {
const offlineListener = () => setOverallState(
setNetworkIssues((issues) => { !internetConnected
return [ ? NetworkHealthState.Disconnected
...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline), : hasIssues
NETWORK_MESSAGES.offline, ? NetworkHealthState.Issue
] : NetworkHealthState.Ok
}) )
window.addEventListener('offline', offlineListener) }, [hasIssues, internetConnected])
const onlineListener = () => useEffect(() => {
setNetworkIssues((issues) => { const cb1 = () => {
return [...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline)] setSteps(initialConnectingTypeGroupState)
}) setInternetConnected(true)
window.addEventListener('online', onlineListener)
return () => {
window.removeEventListener('offline', offlineListener)
window.removeEventListener('online', onlineListener)
} }
const cb2 = () => {
setInternetConnected(false)
}
window.addEventListener('online', cb1)
window.addEventListener('offline', cb2)
return () => {
window.removeEventListener('online', cb1)
window.removeEventListener('offline', cb2)
}
}, [])
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 (
@ -45,65 +190,94 @@ 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-succeed-80') : 'focus-visible:outline-energy-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={faWifi} icon={overallConnectionStateIcon[overallState]}
className="p-1" className="p-1"
iconClassName={ iconClassName={overallConnectionStateColor[overallState].icon}
hasIssues
? 'text-destroy-80 dark:text-destroy-30'
: 'text-succeed-80 dark:text-succeed-30'
}
bgClassName={ bgClassName={
'bg-transparent dark:bg-transparent ' + 'rounded-sm ' + overallConnectionStateColor[overallState].bg
(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-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"> <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">
{!hasIssues ? ( <div
<span className={`flex items-center justify-between p-2 rounded-t-sm ${overallConnectionStateColor[overallState].bg} ${overallConnectionStateColor[overallState].icon}`}
className="flex items-center justify-center gap-1 px-4" >
data-testid="network-good" <h2 className="text-sm font-sans font-normal">Network health</h2>
<p
data-testid="network"
className="font-bold text-xs uppercase px-2 py-1 rounded-sm"
> >
<ActionIcon {NETWORK_HEALTH_TEXT[overallState]}
icon="checkmark" </p>
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded-sm'} </div>
iconClassName={'text-succeed-80 dark:text-succeed-30'} <ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
/> {Object.keys(steps).map((name) => (
{NETWORK_CONTENT.good} <li
</span> key={name}
) : ( 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"
> >
{NETWORK_CONTENT.bad} <div className="flex items-center text-left gap-1">
{networkIssues.length > 1 ? 's' : ''} <p className="flex-1">{name}</p>
</span> {internetConnected ? (
{networkIssues.map((issue) => ( <ActionIcon
<li size="lg"
key={issue} icon={
className="flex items-center gap-1 py-2 my-2 last:mb-0" hasIssueToIcon[
> issues[name as ConnectingTypeGroup].toString()
<ActionIcon ]
icon={faExclamation} }
bgClassName={'bg-destroy-10/50 dark:bg-destroy-80/50 rounded'} iconClassName={
iconClassName={'text-destroy-80 dark:text-destroy-30'} hasIssueToIconColors[
className="ml-4" issues[name as ConnectingTypeGroup].toString()
/> ].icon
<p className="flex-1 mr-4">{issue}</p> }
</li> bgClassName={
))} 'rounded-sm ' +
</ul> hasIssueToIconColors[
)} 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,5 +1,6 @@
import { FormEvent, useEffect, useRef, useState } from 'react' import { FormEvent, useEffect, useRef, useState } from 'react'
import { type ProjectWithEntryPointMetadata, paths } from '../Router' import { type ProjectWithEntryPointMetadata } from 'lib/types'
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 { ProjectWithEntryPointMetadata } from '../Router' import { type ProjectWithEntryPointMetadata } from 'lib/types'
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,7 +1,8 @@
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 { IndexLoaderData, paths } from '../Router' import { type IndexLoaderData } from 'lib/types'
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,16 +11,13 @@ 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 { kclManager, useKclContext } from 'lang/KclSinglton' import { useKclContext } from 'lang/KclSingleton'
import { changeSketchArguments } from 'lang/std/sketch' import { ClientSideScene } from 'clientSideScene/setup'
export const Stream = ({ className = '' }) => { export const Stream = ({ className = '' }: { className?: string }) => {
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)
@ -39,7 +36,7 @@ export const Stream = ({ className = '' }) => {
})) }))
const { settings } = useGlobalStateContext() const { settings } = useGlobalStateContext()
const cameraControls = settings?.context?.cameraControls const cameraControls = settings?.context?.cameraControls
const { send, state, context } = useModelingContext() const { state } = useModelingContext()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()
useEffect(() => { useEffect(() => {
@ -53,8 +50,10 @@ export const Stream = ({ className = '' }) => {
videoRef.current.srcObject = mediaStream videoRef.current.srcObject = mediaStream
}, [mediaStream]) }, [mediaStream])
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => { const handleMouseDown: MouseEventHandler<HTMLDivElement> = (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,
@ -62,55 +61,6 @@ export const Stream = ({ className = '' }) => {
...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
}
void 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')) {
void 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 })
} }
@ -118,7 +68,7 @@ export const Stream = ({ className = '' }) => {
const fps = 60 const fps = 60
const handleScroll: WheelEventHandler<HTMLVideoElement> = throttle((e) => { const handleScroll: WheelEventHandler<HTMLVideoElement> = throttle((e) => {
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
void engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
type: 'default_camera_zoom', type: 'default_camera_zoom',
@ -128,13 +78,15 @@ export const Stream = ({ className = '' }) => {
}) })
}, Math.round(1000 / fps)) }, Math.round(1000 / fps))
const handleMouseUp: MouseEventHandler<HTMLVideoElement> = ({ const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({
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,
@ -155,208 +107,21 @@ export const Stream = ({ className = '' }) => {
cmd_id: newCmdId, cmd_id: newCmdId,
} }
if (!didDragInStream && state.matches('Sketch no face')) { if (!didDragInStream) {
command.cmd = {
type: 'select_with_point',
selection_type: 'add',
selected_at_window: { x, y },
}
void engineCommandManager.sendSceneCommand(command)
} else if (!didDragInStream && state.matches('Sketch.Line Tool')) {
command.cmd = {
type: 'mouse_click',
window: { x, y },
}
void 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 = { 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)
void engineCommandManager.sendSceneCommand(command) } else if (didDragInStream) {
} else if (!didDragInStream && state.matches('Sketch.Move Tool')) {
command.cmd = {
type: 'select_with_point',
selected_at_window: { x, y },
selection_type: 'add',
}
void engineCommandManager.sendSceneCommand(command)
} 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).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,
]
}
void kclManager.executeAstMock(modifiedAst, true)
})
} else {
void engineCommandManager.sendSceneCommand(command) void engineCommandManager.sendSceneCommand(command)
} else {
engineCommandManager.sendSceneCommand(command)
} }
setDidDragInStream(false) setDidDragInStream(false)
@ -364,6 +129,8 @@ export const Stream = ({ className = '' }) => {
} }
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 =
@ -376,16 +143,19 @@ export const Stream = ({ className = '' }) => {
} }
return ( return (
<div id="stream" className={className}> <div
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}
@ -393,6 +163,7 @@ export const Stream = ({ className = '' }) => {
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

@ -11,7 +11,7 @@ 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 } from 'react' import { useMemo, useRef } 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'
@ -24,7 +24,9 @@ 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/KclSinglton' import { kclManager, useKclContext } from 'lang/KclSingleton'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { setupSingleton } from 'clientSideScene/setup'
export const editorShortcutMeta = { export const editorShortcutMeta = {
formatCode: { formatCode: {
@ -56,10 +58,12 @@ export const TextEditor = ({
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 } = {} } = {} } = const { settings: { context: { textWrapping } = {} } = {} } =
@ -76,12 +80,10 @@ export const TextEditor = ({
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) Server.initialize(intoServer, fromServer).then((lspServer) => {
.then((lspServer) => { lspServer.start()
void lspServer.start() setIsLSPServerReady(true)
setIsLSPServerReady(true) })
})
.catch((e) => console.log(e))
} }
const lspClient = new LanguageServerClient({ client }) const lspClient = new LanguageServerClient({ client })
@ -117,6 +119,12 @@ export const TextEditor = ({
if (!editorView) { if (!editorView) {
setEditorView(viewUpdate.view) setEditorView(viewUpdate.view)
} }
if (setupSingleton.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,
@ -124,7 +132,20 @@ 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)
@ -153,7 +174,7 @@ export const TextEditor = ({
key: editorShortcutMeta.convertToVariable.codeMirror, key: editorShortcutMeta.convertToVariable.codeMirror,
run: () => { run: () => {
if (convertEnabled) { if (convertEnabled) {
void convertCallback() convertCallback()
return true return true
} }
return false return false

View File

@ -11,7 +11,7 @@ import {
getTransformInfos, getTransformInfos,
PathToNodeMap, PathToNodeMap,
} from '../../lang/std/sketchcombos' } from '../../lang/std/sketchcombos'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
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/KclSinglton' import { kclManager } from 'lang/KclSingleton'
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/KclSinglton' import { kclManager } from 'lang/KclSingleton'
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/KclSinglton' import { kclManager } from 'lang/KclSingleton'
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/KclSinglton' import { kclManager } from 'lang/KclSingleton'
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/KclSinglton' import { kclManager } from 'lang/KclSingleton'
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal) const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
@ -139,7 +139,7 @@ export function applyConstraintAxisAlign({
constraint, constraint,
}).transforms }).transforms
let finalValue = createIdentifier('_0') let finalValue = createIdentifier('ZERO')
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/KclSinglton' import { kclManager } from 'lang/KclSingleton'
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/KclSinglton' import { kclManager } from 'lang/KclSingleton'
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/KclSinglton' import { kclManager } from 'lang/KclSingleton'
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal) const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
@ -89,12 +89,16 @@ export async function applyConstraintAngleLength({
isReferencingXAxis && angleOrLength === 'setAngle' isReferencingXAxis && angleOrLength === 'setAngle'
let forceVal = valueUsedInTransform || 0 let forceVal = valueUsedInTransform || 0
let calcIdentifier = createIdentifier('_0') let calcIdentifier = createIdentifier('ZERO')
if (isReferencingYAxisAngle) { if (isReferencingYAxisAngle) {
calcIdentifier = createIdentifier(forceVal < 0 ? '_270' : '_90') calcIdentifier = createIdentifier(
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(Math.abs(forceVal) > 90 ? '_180' : '_0') calcIdentifier = createIdentifier(
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
} }
@ -112,7 +116,7 @@ export async function applyConstraintAngleLength({
) )
if ( if (
isReferencingYAxisAngle || isReferencingYAxisAngle ||
(isReferencingXAxisAngle && calcIdentifier.name !== '_0') (isReferencingXAxisAngle && calcIdentifier.name !== 'ZERO')
) { ) {
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 '../Router' import { paths } from 'lib/paths'
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/KclSinglton' import { useKclContext } from 'lang/KclSingleton'
export function WasmErrBanner() { export function WasmErrBanner() {
const [isBannerDismissed, setBannerDismissed] = useState(false) const [isBannerDismissed, setBannerDismissed] = useState(false)

View File

@ -67,7 +67,7 @@ export class LanguageServerClient {
async initialize() { async initialize() {
// Start the client in the background. // Start the client in the background.
await this.client.start() this.client.start()
this.ready = true this.ready = true
} }
@ -81,12 +81,12 @@ export class LanguageServerClient {
textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) { textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) {
this.notify('textDocument/didOpen', params) this.notify('textDocument/didOpen', params)
void this.updateSemanticTokens(params.textDocument.uri) this.updateSemanticTokens(params.textDocument.uri)
} }
textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) { textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) {
this.notify('textDocument/didChange', params) this.notify('textDocument/didChange', params)
void this.updateSemanticTokens(params.textDocument.uri) this.updateSemanticTokens(params.textDocument.uri)
} }
async updateSemanticTokens(uri: string) { async updateSemanticTokens(uri: string) {

View File

@ -62,7 +62,7 @@ export class LanguageServerPlugin implements PluginValue {
this.client.attachPlugin(this) this.client.attachPlugin(this)
void this.initialize({ this.initialize({
documentText: this.view.state.doc.toString(), documentText: this.view.state.doc.toString(),
}) })
} }
@ -70,7 +70,7 @@ export class LanguageServerPlugin implements PluginValue {
update({ docChanged }: ViewUpdate) { update({ docChanged }: ViewUpdate) {
if (!docChanged) return if (!docChanged) return
void this.sendChange({ this.sendChange({
documentText: this.view.state.doc.toString(), documentText: this.view.state.doc.toString(),
}) })
} }
@ -127,7 +127,7 @@ export class LanguageServerPlugin implements PluginValue {
} }
requestDiagnostics(view: EditorView) { requestDiagnostics(view: EditorView) {
void this.sendChange({ documentText: view.state.doc.toString() }) this.sendChange({ documentText: view.state.doc.toString() })
} }
async requestHoverTooltip( async requestHoverTooltip(
@ -140,7 +140,7 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
await this.sendChange({ documentText: view.state.doc.toString() }) this.sendChange({ documentText: view.state.doc.toString() })
const result = await this.client.textDocumentHover({ const result = await this.client.textDocumentHover({
textDocument: { uri: this.documentUri }, textDocument: { uri: this.documentUri },
position: { line, character }, position: { line, character },
@ -178,7 +178,7 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
await this.sendChange({ this.sendChange({
documentText: context.state.doc.toString(), documentText: context.state.doc.toString(),
}) })

View File

@ -1,4 +1,6 @@
import { BROWSER_FILE_NAME, IndexLoaderData, paths } from 'Router' import { BROWSER_FILE_NAME } from 'Router'
import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths'
import { useRouteLoaderData } from 'react-router-dom' import { useRouteLoaderData } from 'react-router-dom'
export function useAbsoluteFilePath() { export function useAbsoluteFilePath() {

View File

@ -15,6 +15,7 @@ export function useEngineConnectionSubscriptions() {
if (!engineCommandManager) return if (!engineCommandManager) return
const unSubHover = engineCommandManager.subscribeToUnreliable({ const unSubHover = engineCommandManager.subscribeToUnreliable({
// Note this is our hover logic, "highlight_set_entity" is the event that is fired when we hover over an entity
event: 'highlight_set_entity', event: 'highlight_set_entity',
callback: ({ data }) => { callback: ({ data }) => {
if (data?.entity_id) { if (data?.entity_id) {
@ -46,6 +47,6 @@ export function useEngineConnectionSubscriptions() {
engineCommandManager, engineCommandManager,
setHighlightRange, setHighlightRange,
highlightRange, highlightRange,
context.sketchEnginePathId, context?.sketchEnginePathId,
]) ])
} }

View File

@ -3,7 +3,7 @@ import { parse } from '../lang/wasm'
import { useStore } from '../useStore' import { useStore } from '../useStore'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
import { deferExecution } from 'lib/utils' import { deferExecution } from 'lib/utils'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
export function useSetupEngineManager( export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>, streamRef: React.RefObject<HTMLDivElement>,

View File

@ -2,7 +2,7 @@ import {
SetVarNameModal, SetVarNameModal,
createSetVarNameModal, createSetVarNameModal,
} from 'components/SetVarNameModal' } from 'components/SetVarNameModal'
import { kclManager } from 'lang/KclSinglton' import { kclManager } from 'lang/KclSingleton'
import { moveValueIntoNewVariable } from 'lang/modifyAst' import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst' import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -39,7 +39,7 @@ export function useConvertToVariable() {
variableName variableName
) )
void kclManager.updateAst(_modifiedAst, true) kclManager.updateAst(_modifiedAst, true)
} catch (e) { } catch (e) {
console.log('error', e) console.log('error', e)
} }

View File

@ -6,6 +6,8 @@ import { Router } from './Router'
import { HotkeysProvider } from 'react-hotkeys-hook' import { HotkeysProvider } from 'react-hotkeys-hook'
// uncomment for xstate inspector // uncomment for xstate inspector
// import { DEV } from 'env'
// import { inspect } from '@xstate/inspect'
// if (DEV) // if (DEV)
// inspect({ // inspect({
// iframe: false, // iframe: false,

View File

@ -7,6 +7,7 @@ import {
} from './std/engineConnection' } from './std/engineConnection'
import { deferExecution } from 'lib/utils' import { deferExecution } from 'lib/utils'
import { import {
CallExpression,
initPromise, initPromise,
parse, parse,
PathToNode, PathToNode,
@ -17,7 +18,7 @@ import {
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useState } from 'react'
import { getNodeFromPath } from './queryAst' import { getNodeFromPath } from './queryAst'
import { IndexLoaderData } from 'Router' import { type IndexLoaderData } from 'lib/types'
import { Params, useLoaderData } from 'react-router-dom' import { Params, useLoaderData } from 'react-router-dom'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { writeTextFile } from '@tauri-apps/plugin-fs' import { writeTextFile } from '@tauri-apps/plugin-fs'
@ -59,7 +60,7 @@ class KclManager {
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
void this.executeAst(ast) this.executeAst(ast)
}, 600) }, 600)
private _isExecutingCallback: (arg: boolean) => void = () => {} private _isExecutingCallback: (arg: boolean) => void = () => {}
@ -98,7 +99,7 @@ class KclManager {
}) })
}) })
} else { } else {
localStorage.setItem(PERSIST_CODE_TOKEN, code) localStorage?.setItem(PERSIST_CODE_TOKEN, code)
} }
} }
@ -110,10 +111,6 @@ class KclManager {
this._programMemoryCallBack(programMemory) this._programMemoryCallBack(programMemory)
} }
get defaultPlanes() {
return this?.engineCommandManager?.defaultPlanes
}
get logs() { get logs() {
return this._logs return this._logs
} }
@ -168,11 +165,13 @@ class KclManager {
zustandStore.state.code = '' zustandStore.state.code = ''
localStorage.setItem('store', JSON.stringify(zustandStore)) localStorage.setItem('store', JSON.stringify(zustandStore))
} else if (storedCode === null) { } else if (storedCode === null) {
console.log('stored brack thing')
this.code = bracket this.code = bracket
} else { } else {
this.code = storedCode this.code = storedCode
} }
this.ensureWasmInit().then(() => {
this.ast = this.safeParse(this.code) || this.ast
})
} }
registerCallBacks({ registerCallBacks({
setCode, setCode,
@ -235,7 +234,6 @@ class KclManager {
const { logs, errors, programMemory } = await executeAst({ const { logs, errors, programMemory } = await executeAst({
ast, ast,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
defaultPlanes: this.defaultPlanes,
}) })
this.isExecuting = false this.isExecuting = false
this.logs = logs this.logs = logs
@ -251,13 +249,20 @@ class KclManager {
data: null, data: null,
}) })
} }
async executeAstMock(ast: Program = this._ast, updateCode = false) { async executeAstMock(
ast: Program = this._ast,
{
updates,
}: {
updates: 'none' | 'code' | 'codeAndArtifactRanges'
} = { updates: 'none' }
) {
await this.ensureWasmInit() await this.ensureWasmInit()
const newCode = recast(ast) const newCode = recast(ast)
const newAst = this.safeParse(newCode) const newAst = this.safeParse(newCode)
if (!newAst) return if (!newAst) return
await this?.engineCommandManager?.waitForReady await this?.engineCommandManager?.waitForReady
if (updateCode) { if (updates !== 'none') {
this.setCode(recast(ast)) this.setCode(recast(ast))
} }
this._ast = { ...newAst } this._ast = { ...newAst }
@ -265,22 +270,38 @@ class KclManager {
const { logs, errors, programMemory } = await executeAst({ const { logs, errors, programMemory } = await executeAst({
ast: newAst, ast: newAst,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
defaultPlanes: this.defaultPlanes,
useFakeExecutor: true, useFakeExecutor: true,
}) })
this._logs = logs this._logs = logs
this._kclErrors = errors this._kclErrors = errors
this._programMemory = programMemory this._programMemory = programMemory
if (updates !== 'codeAndArtifactRanges') return
Object.entries(engineCommandManager.artifactMap).forEach(
([commandId, artifact]) => {
if (!artifact.pathToNode) return
const node = getNodeFromPath<CallExpression>(
kclManager.ast,
artifact.pathToNode,
'CallExpression'
).node
if (node.type !== 'CallExpression') return
const [oldStart, oldEnd] = artifact.range
if (oldStart === 0 && oldEnd === 0) return
if (oldStart === node.start && oldEnd === node.end) return
engineCommandManager.artifactMap[commandId].range = [
node.start,
node.end,
]
}
)
} }
async executeCode(code?: string) { async executeCode(code?: string) {
await this.ensureWasmInit() await this.ensureWasmInit()
await this?.engineCommandManager?.waitForReady await this?.engineCommandManager?.waitForReady
if (!this?.engineCommandManager?.planesInitialized()) return
const result = await executeCode({ const result = await executeCode({
engineCommandManager, engineCommandManager,
code: code || this._code, code: code || this._code,
lastAst: this._ast, lastAst: this._ast,
defaultPlanes: this.defaultPlanes,
force: false, force: false,
}) })
if (!result.isChange) return if (!result.isChange) return
@ -366,26 +387,10 @@ class KclManager {
// When we don't re-execute, we still want to update the program // When we don't re-execute, we still want to update the program
// memory with the new ast. So we will hit the mock executor // memory with the new ast. So we will hit the mock executor
// instead. // instead.
await this.executeAstMock(astWithUpdatedSource, true) await this.executeAstMock(astWithUpdatedSource, { updates: 'code' })
} }
return returnVal return returnVal
} }
getPlaneId(axis: 'xy' | 'xz' | 'yz'): string {
return this.defaultPlanes[axis]
}
showPlanes() {
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, false)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, false)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, false)
}
hidePlanes() {
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, true)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
}
} }
export const kclManager = new KclManager(engineCommandManager) export const kclManager = new KclManager(engineCommandManager)

View File

@ -33,7 +33,7 @@ show(mySketch001)`
}, },
value: [ value: [
{ {
type: 'toPoint', type: 'ToPoint',
name: '', name: '',
to: [-1.59, -1.54], to: [-1.59, -1.54],
from: [0, 0], from: [0, 0],
@ -43,7 +43,7 @@ show(mySketch001)`
}, },
}, },
{ {
type: 'toPoint', type: 'ToPoint',
to: [0.46, -5.82], to: [0.46, -5.82],
from: [-1.59, -1.54], from: [-1.59, -1.54],
name: '', name: '',
@ -55,8 +55,11 @@ show(mySketch001)`
], ],
position: [0, 0, 0], position: [0, 0, 0],
rotation: [0, 0, 0, 1], rotation: [0, 0, 0, 1],
xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 },
id: expect.any(String), id: expect.any(String),
planeId: expect.any(String), entityId: expect.any(String),
__meta: [{ sourceRange: [46, 71] }], __meta: [{ sourceRange: [46, 71] }],
}, },
]) ])
@ -85,6 +88,11 @@ show(mySketch001)`
height: 2, height: 2,
position: [0, 0, 0], position: [0, 0, 0],
rotation: [0, 0, 0, 1], rotation: [0, 0, 0, 1],
endCapId: null,
startCapId: null,
xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 },
__meta: [{ sourceRange: [46, 71] }], __meta: [{ sourceRange: [46, 71] }],
}, },
]) ])
@ -127,6 +135,11 @@ show(theExtrude, sk2)`
height: 2, height: 2,
position: [0, 0, 0], position: [0, 0, 0],
rotation: [0, 0, 0, 1], rotation: [0, 0, 0, 1],
endCapId: null,
startCapId: null,
xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 },
__meta: [{ sourceRange: [38, 63] }], __meta: [{ sourceRange: [38, 63] }],
}, },
{ {
@ -136,6 +149,12 @@ show(theExtrude, sk2)`
height: 2, height: 2,
position: [0, 0, 0], position: [0, 0, 0],
rotation: [0, 0, 0, 1], rotation: [0, 0, 0, 1],
endCapId: null,
startCapId: null,
xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 },
__meta: [{ sourceRange: [356, 381] }], __meta: [{ sourceRange: [356, 381] }],
}, },
]) ])

View File

@ -54,7 +54,7 @@ show(mySketch)
const minusGeo = root.mySketch.value const minusGeo = root.mySketch.value
expect(minusGeo).toEqual([ expect(minusGeo).toEqual([
{ {
type: 'toPoint', type: 'ToPoint',
to: [0, 2], to: [0, 2],
from: [0, 0], from: [0, 0],
__geoMeta: { __geoMeta: {
@ -64,7 +64,7 @@ show(mySketch)
name: 'myPath', name: 'myPath',
}, },
{ {
type: 'toPoint', type: 'ToPoint',
to: [2, 3], to: [2, 3],
from: [0, 2], from: [0, 2],
name: '', name: '',
@ -74,7 +74,7 @@ show(mySketch)
}, },
}, },
{ {
type: 'toPoint', type: 'ToPoint',
to: [5, -1], to: [5, -1],
from: [2, 3], from: [2, 3],
__geoMeta: { __geoMeta: {
@ -154,7 +154,7 @@ show(mySketch)
}, },
value: [ value: [
{ {
type: 'toPoint', type: 'ToPoint',
to: [1, 1], to: [1, 1],
from: [0, 0], from: [0, 0],
name: '', name: '',
@ -164,7 +164,7 @@ show(mySketch)
}, },
}, },
{ {
type: 'toPoint', type: 'ToPoint',
to: [0, 1], to: [0, 1],
from: [1, 1], from: [1, 1],
__geoMeta: { __geoMeta: {
@ -174,7 +174,7 @@ show(mySketch)
name: 'myPath', name: 'myPath',
}, },
{ {
type: 'toPoint', type: 'ToPoint',
to: [1, 1], to: [1, 1],
from: [0, 1], from: [0, 1],
name: '', name: '',
@ -186,8 +186,11 @@ show(mySketch)
], ],
position: [0, 0, 0], position: [0, 0, 0],
rotation: [0, 0, 0, 1], rotation: [0, 0, 0, 1],
xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 },
id: expect.any(String), id: expect.any(String),
planeId: expect.any(String), entityId: expect.any(String),
__meta: [{ sourceRange: [39, 63] }], __meta: [{ sourceRange: [39, 63] }],
}) })
}) })

View File

@ -30,41 +30,28 @@ import {
createFirstArg, createFirstArg,
} from './std/sketch' } from './std/sketch'
import { isLiteralArrayOrStatic } from './std/sketchcombos' import { isLiteralArrayOrStatic } from './std/sketchcombos'
import { DefaultPlaneStr } from 'clientSideScene/clientSideScene'
import { roundOff } from 'lib/utils'
export function addStartSketch( export function startSketchOnDefault(
node: Program, node: Program,
axis: 'xy' | 'xz' | 'yz' | '-xy' | '-xz' | '-yz', axis: DefaultPlaneStr,
start: [number, number], name = ''
end: [number, number]
): { modifiedAst: Program; id: string; pathToNode: PathToNode } { ): { modifiedAst: Program; id: string; pathToNode: PathToNode } {
const _node = { ...node } const _node = { ...node }
const _name = findUniqueName(node, 'part') const _name = name || findUniqueName(node, 'part')
const startSketchOn = createCallExpressionStdLib('startSketchOn', [ const startSketchOn = createCallExpressionStdLib('startSketchOn', [
createLiteral(axis.toUpperCase()), createLiteral(axis),
])
const startProfileAt = createCallExpressionStdLib('startProfileAt', [
createArrayExpression([createLiteral(start[0]), createLiteral(start[1])]),
createPipeSubstitution(),
])
const initialLineTo = createCallExpression('line', [
createArrayExpression([createLiteral(end[0]), createLiteral(end[1])]),
createPipeSubstitution(),
]) ])
const pipeBody = [startSketchOn, startProfileAt, initialLineTo] const variableDeclaration = createVariableDeclaration(_name, startSketchOn)
const variableDeclaration = createVariableDeclaration(
_name,
createPipeExpression(pipeBody)
)
const newIndex = node.body.length
_node.body = [...node.body, variableDeclaration] _node.body = [...node.body, variableDeclaration]
const sketchIndex = _node.body.length - 1
let pathToNode: PathToNode = [ let pathToNode: PathToNode = [
['body', ''], ['body', ''],
[newIndex.toString(10), 'index'], [sketchIndex, 'index'],
['declarations', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
['0', 'index'], ['0', 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
@ -77,6 +64,43 @@ export function addStartSketch(
} }
} }
export function addStartProfileAt(
node: Program,
pathToNode: PathToNode,
at: [number, number]
): { modifiedAst: Program; pathToNode: PathToNode } {
console.log('addStartProfileAt called')
const variableDeclaration = getNodeFromPath<VariableDeclaration>(
node,
pathToNode,
'VariableDeclaration'
).node
if (variableDeclaration.type !== 'VariableDeclaration') {
throw new Error('variableDeclaration.init.type !== PipeExpression')
}
const _node = { ...node }
const init = variableDeclaration.declarations[0].init
const startProfileAt = createCallExpressionStdLib('startProfileAt', [
createArrayExpression([
createLiteral(roundOff(at[0])),
createLiteral(roundOff(at[1])),
]),
createPipeSubstitution(),
])
if (init.type === 'PipeExpression') {
init.body.splice(1, 0, startProfileAt)
} else {
variableDeclaration.declarations[0].init = createPipeExpression([
init,
startProfileAt,
])
}
return {
modifiedAst: _node,
pathToNode,
}
}
export function addSketchTo( export function addSketchTo(
node: Program, node: Program,
axis: 'xy' | 'xz' | 'yz', axis: 'xy' | 'xz' | 'yz',

View File

@ -65,13 +65,13 @@ export function getNodeFromPath<T>(
} }
} }
} catch (e) { } catch (e) {
console.error( // console.error(
`Could not find path ${pathItem} in node ${JSON.stringify( // `Could not find path ${pathItem} in node ${JSON.stringify(
currentNode, // currentNode,
null, // null,
2 // 2
)}, successful path was ${successfulPaths}` // )}, successful path was ${successfulPaths}`
) // )
} }
} }
return { return {
@ -266,6 +266,7 @@ function moreNodePathFromSourceRange(
} }
} }
} }
if (_node.type === 'PipeSubstitution' && isInRange) return path
console.error('not implemented: ' + node.type) console.error('not implemented: ' + node.type)
return path return path
} }
@ -489,7 +490,7 @@ export function isLinesParallelAndConstrained(
const constraintLevel = getConstraintLevelFromSourceRange( const constraintLevel = getConstraintLevelFromSourceRange(
secondaryLine.range, secondaryLine.range,
ast ast
) ).level
const isConstrained = const isConstrained =
constraintType === 'angle' || constraintLevel === 'full' constraintType === 'angle' || constraintLevel === 'full'

View File

@ -1,16 +1,18 @@
import { SourceRange } from 'lang/wasm' import { PathToNode, Program, SourceRange } from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env' import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave' import { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import * as Sentry from '@sentry/react' import * as Sentry from '@sentry/react'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { getNodePathFromSourceRange } from 'lang/queryAst'
import { setupSingleton } from 'clientSideScene/setup'
let lastMessage = '' let lastMessage = ''
interface CommandInfo { interface CommandInfo {
commandType: CommandTypes commandType: CommandTypes
range: SourceRange range: SourceRange
pathToNode: PathToNode
parentId?: string parentId?: string
} }
@ -56,26 +58,36 @@ type Value<T, U> = U extends undefined
type State<T, U> = Value<T, U> type State<T, U> = Value<T, U>
enum EngineConnectionStateType { export enum EngineConnectionStateType {
Fresh = 'fresh', Fresh = 'fresh',
Connecting = 'connecting', Connecting = 'connecting',
ConnectionEstablished = 'connection-established', ConnectionEstablished = 'connection-established',
Disconnecting = 'disconnecting',
Disconnected = 'disconnected', Disconnected = 'disconnected',
} }
enum DisconnectedType { export enum DisconnectingType {
Error = 'error', Error = 'error',
Timeout = 'timeout', Timeout = 'timeout',
Quit = 'quit', Quit = 'quit',
} }
type DisconnectedValue = export interface ErrorType {
| State<DisconnectedType.Error, Error | undefined> // We may not necessary have an error to assign.
| State<DisconnectedType.Timeout, void> error?: Error
| State<DisconnectedType.Quit, void>
// We assign this in the state setter because we may have not failed at
// a Connecting state, which we check for there.
lastConnectingValue?: ConnectingValue
}
export type DisconnectingValue =
| State<DisconnectingType.Error, ErrorType>
| State<DisconnectingType.Timeout, void>
| State<DisconnectingType.Quit, void>
// These are ordered by the expected sequence. // These are ordered by the expected sequence.
enum ConnectingType { export enum ConnectingType {
WebSocketConnecting = 'websocket-connecting', WebSocketConnecting = 'websocket-connecting',
WebSocketEstablished = 'websocket-established', WebSocketEstablished = 'websocket-established',
PeerConnectionCreated = 'peer-connection-created', PeerConnectionCreated = 'peer-connection-created',
@ -92,7 +104,39 @@ enum ConnectingType {
DataChannelEstablished = 'data-channel-established', DataChannelEstablished = 'data-channel-established',
} }
type ConnectingValue = export enum ConnectingTypeGroup {
WebSocket = 'WebSocket',
ICE = 'ICE',
WebRTC = 'WebRTC',
}
export const initialConnectingTypeGroupState: Record<
ConnectingTypeGroup,
[ConnectingType, boolean | undefined][]
> = {
[ConnectingTypeGroup.WebSocket]: [
[ConnectingType.WebSocketConnecting, undefined],
[ConnectingType.WebSocketEstablished, undefined],
],
[ConnectingTypeGroup.ICE]: [
[ConnectingType.PeerConnectionCreated, undefined],
[ConnectingType.ICEServersSet, undefined],
[ConnectingType.SetLocalDescription, undefined],
[ConnectingType.OfferedSdp, undefined],
[ConnectingType.ReceivedSdp, undefined],
[ConnectingType.SetRemoteDescription, undefined],
[ConnectingType.WebRTCConnecting, undefined],
[ConnectingType.ICECandidateReceived, undefined],
],
[ConnectingTypeGroup.WebRTC]: [
[ConnectingType.TrackReceived, undefined],
[ConnectingType.DataChannelRequested, undefined],
[ConnectingType.DataChannelConnecting, undefined],
[ConnectingType.DataChannelEstablished, undefined],
],
}
export type ConnectingValue =
| State<ConnectingType.WebSocketConnecting, void> | State<ConnectingType.WebSocketConnecting, void>
| State<ConnectingType.WebSocketEstablished, void> | State<ConnectingType.WebSocketEstablished, void>
| State<ConnectingType.PeerConnectionCreated, void> | State<ConnectingType.PeerConnectionCreated, void>
@ -108,11 +152,12 @@ type ConnectingValue =
| State<ConnectingType.DataChannelConnecting, string> | State<ConnectingType.DataChannelConnecting, string>
| State<ConnectingType.DataChannelEstablished, void> | State<ConnectingType.DataChannelEstablished, void>
type EngineConnectionState = export type EngineConnectionState =
| State<EngineConnectionStateType.Fresh, void> | State<EngineConnectionStateType.Fresh, void>
| State<EngineConnectionStateType.Connecting, ConnectingValue> | State<EngineConnectionStateType.Connecting, ConnectingValue>
| State<EngineConnectionStateType.ConnectionEstablished, void> | State<EngineConnectionStateType.ConnectionEstablished, void>
| State<EngineConnectionStateType.Disconnected, DisconnectedValue> | State<EngineConnectionStateType.Disconnecting, DisconnectingValue>
| State<EngineConnectionStateType.Disconnected, void>
// EngineConnection encapsulates the connection(s) to the Engine // EngineConnection encapsulates the connection(s) to the Engine
// for the EngineCommandManager; namely, the underlying WebSocket // for the EngineCommandManager; namely, the underlying WebSocket
@ -133,22 +178,38 @@ class EngineConnection {
set state(next: EngineConnectionState) { set state(next: EngineConnectionState) {
console.log(`${JSON.stringify(this.state)}${JSON.stringify(next)}`) console.log(`${JSON.stringify(this.state)}${JSON.stringify(next)}`)
if (next.type === EngineConnectionStateType.Disconnected) {
if (next.type === EngineConnectionStateType.Disconnecting) {
console.trace() console.trace()
const sub = next.value const sub = next.value
if (sub.type === DisconnectedType.Error) { if (sub.type === DisconnectingType.Error) {
// Record the last step we failed at.
// (Check the current state that we're about to override that
// it was a Connecting state.)
console.log(sub)
if (this._state.type === EngineConnectionStateType.Connecting) {
if (!sub.value) sub.value = {}
sub.value.lastConnectingValue = this._state.value
}
console.error(sub.value) console.error(sub.value)
} }
} }
this._state = next this._state = next
this.onConnectionStateChange(this._state)
} }
private failedConnTimeout: Timeout | null private failedConnTimeout: Timeout | null
readonly url: string readonly url: string
private readonly token?: string private readonly token?: string
private onWebsocketOpen: (engineConnection: EngineConnection) => void
private onDataChannelOpen: (engineConnection: EngineConnection) => void // For now, this is only used by the NetworkHealthIndicator.
// We can eventually use it for more, but one step at a time.
private onConnectionStateChange: (state: EngineConnectionState) => void
// These are used for the EngineCommandManager and were created
// before onConnectionStateChange existed.
private onEngineConnectionOpen: (engineConnection: EngineConnection) => void private onEngineConnectionOpen: (engineConnection: EngineConnection) => void
private onConnectionStarted: (engineConnection: EngineConnection) => void private onConnectionStarted: (engineConnection: EngineConnection) => void
private onClose: (engineConnection: EngineConnection) => void private onClose: (engineConnection: EngineConnection) => void
@ -160,17 +221,15 @@ class EngineConnection {
constructor({ constructor({
url, url,
token, token,
onWebsocketOpen = () => {}, onConnectionStateChange = () => {},
onNewTrack = () => {}, onNewTrack = () => {},
onEngineConnectionOpen = () => {}, onEngineConnectionOpen = () => {},
onConnectionStarted = () => {}, onConnectionStarted = () => {},
onClose = () => {}, onClose = () => {},
onDataChannelOpen = () => {},
}: { }: {
url: string url: string
token?: string token?: string
onWebsocketOpen?: (engineConnection: EngineConnection) => void onConnectionStateChange?: (state: EngineConnectionState) => void
onDataChannelOpen?: (engineConnection: EngineConnection) => void
onEngineConnectionOpen?: (engineConnection: EngineConnection) => void onEngineConnectionOpen?: (engineConnection: EngineConnection) => void
onConnectionStarted?: (engineConnection: EngineConnection) => void onConnectionStarted?: (engineConnection: EngineConnection) => void
onClose?: (engineConnection: EngineConnection) => void onClose?: (engineConnection: EngineConnection) => void
@ -179,8 +238,7 @@ class EngineConnection {
this.url = url this.url = url
this.token = token this.token = token
this.failedConnTimeout = null this.failedConnTimeout = null
this.onWebsocketOpen = onWebsocketOpen this.onConnectionStateChange = onConnectionStateChange
this.onDataChannelOpen = onDataChannelOpen
this.onEngineConnectionOpen = onEngineConnectionOpen this.onEngineConnectionOpen = onEngineConnectionOpen
this.onConnectionStarted = onConnectionStarted this.onConnectionStarted = onConnectionStarted
@ -196,6 +254,7 @@ class EngineConnection {
case EngineConnectionStateType.ConnectionEstablished: case EngineConnectionStateType.ConnectionEstablished:
this.send({ type: 'ping' }) this.send({ type: 'ping' })
break break
case EngineConnectionStateType.Disconnecting:
case EngineConnectionStateType.Disconnected: case EngineConnectionStateType.Disconnected:
clearInterval(pingInterval) clearInterval(pingInterval)
break break
@ -207,17 +266,11 @@ class EngineConnection {
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
let connectRetryInterval = setInterval(() => { let connectRetryInterval = setInterval(() => {
if (this.state.type !== EngineConnectionStateType.Disconnected) return if (this.state.type !== EngineConnectionStateType.Disconnected) return
switch (this.state.value.type) {
case DisconnectedType.Error: // Only try reconnecting when completely disconnected.
clearInterval(connectRetryInterval) clearInterval(connectRetryInterval)
break console.log('Trying to reconnect')
case DisconnectedType.Timeout: this.connect()
console.log('Trying to reconnect')
this.connect()
break
default:
break
}
}, connectionTimeoutMs) }, connectionTimeoutMs)
} }
@ -232,8 +285,8 @@ class EngineConnection {
tearDown() { tearDown() {
this.disconnectAll() this.disconnectAll()
this.state = { this.state = {
type: EngineConnectionStateType.Disconnected, type: EngineConnectionStateType.Disconnecting,
value: { type: DisconnectedType.Quit }, value: { type: DisconnectingType.Quit },
} }
} }
@ -350,12 +403,14 @@ class EngineConnection {
case 'failed': case 'failed':
this.disconnectAll() this.disconnectAll()
this.state = { this.state = {
type: EngineConnectionStateType.Disconnected, type: EngineConnectionStateType.Disconnecting,
value: { value: {
type: DisconnectedType.Error, type: DisconnectingType.Error,
value: new Error( value: {
'failed to negotiate ice connection; restarting' error: new Error(
), 'failed to negotiate ice connection; restarting'
),
},
}, },
} }
break break
@ -471,8 +526,6 @@ class EngineConnection {
dataChannelSpan.resolve?.() dataChannelSpan.resolve?.()
} }
this.onDataChannelOpen(this)
// Everything is now connected. // Everything is now connected.
this.state = { type: EngineConnectionStateType.ConnectionEstablished } this.state = { type: EngineConnectionStateType.ConnectionEstablished }
@ -480,27 +533,20 @@ class EngineConnection {
}) })
this.unreliableDataChannel.addEventListener('close', (event) => { this.unreliableDataChannel.addEventListener('close', (event) => {
console.log(event)
console.log('unreliable data channel closed')
this.disconnectAll() this.disconnectAll()
this.unreliableDataChannel = undefined this.finalizeIfAllConnectionsClosed()
if (this.areAllConnectionsClosed()) {
this.state = {
type: EngineConnectionStateType.Disconnected,
value: { type: DisconnectedType.Quit },
}
}
}) })
this.unreliableDataChannel.addEventListener('error', (event) => { this.unreliableDataChannel.addEventListener('error', (event) => {
this.disconnectAll() this.disconnectAll()
this.state = { this.state = {
type: EngineConnectionStateType.Disconnected, type: EngineConnectionStateType.Disconnecting,
value: { value: {
type: DisconnectedType.Error, type: DisconnectingType.Error,
value: new Error(event.toString()), value: {
error: new Error(event.toString()),
},
}, },
} }
}) })
@ -525,8 +571,6 @@ class EngineConnection {
}, },
} }
this.onWebsocketOpen(this)
// This is required for when KCMA is running stand-alone / within Tauri. // This is required for when KCMA is running stand-alone / within Tauri.
// Otherwise when run in a browser, the token is sent implicitly via // Otherwise when run in a browser, the token is sent implicitly via
// the Cookie header. // the Cookie header.
@ -558,24 +602,19 @@ class EngineConnection {
this.websocket.addEventListener('close', (event) => { this.websocket.addEventListener('close', (event) => {
this.disconnectAll() this.disconnectAll()
this.websocket = undefined this.finalizeIfAllConnectionsClosed()
if (this.areAllConnectionsClosed()) {
this.state = {
type: EngineConnectionStateType.Disconnected,
value: { type: DisconnectedType.Quit },
}
}
}) })
this.websocket.addEventListener('error', (event) => { this.websocket.addEventListener('error', (event) => {
this.disconnectAll() this.disconnectAll()
this.state = { this.state = {
type: EngineConnectionStateType.Disconnected, type: EngineConnectionStateType.Disconnecting,
value: { value: {
type: DisconnectedType.Error, type: DisconnectingType.Error,
value: new Error(event.toString()), value: {
error: new Error(event.toString()),
},
}, },
} }
}) })
@ -601,9 +640,14 @@ class EngineConnection {
}) })
.join('\n') .join('\n')
if (message.request_id) { if (message.request_id) {
const artifactThatFailed =
engineCommandManager.artifactMap[message.request_id] ||
engineCommandManager.lastArtifactMap[message.request_id]
console.error( console.error(
`Error in response to request ${message.request_id}:\n${errorsString}` `Error in response to request ${message.request_id}:\n${errorsString}
failed cmd type was ${artifactThatFailed?.commandType}`
) )
console.log(artifactThatFailed)
} else { } else {
console.error(`Error from server:\n${errorsString}`) console.error(`Error from server:\n${errorsString}`)
} }
@ -618,8 +662,6 @@ class EngineConnection {
return return
} }
console.log('received', resp)
switch (resp.type) { switch (resp.type) {
case 'ice_server_info': case 'ice_server_info':
let ice_servers = resp.data?.ice_servers let ice_servers = resp.data?.ice_servers
@ -701,10 +743,12 @@ class EngineConnection {
// The local description is invalid, so there's no point continuing. // The local description is invalid, so there's no point continuing.
this.disconnectAll() this.disconnectAll()
this.state = { this.state = {
type: EngineConnectionStateType.Disconnected, type: EngineConnectionStateType.Disconnecting,
value: { value: {
type: DisconnectedType.Error, type: DisconnectingType.Error,
value: error, value: {
error,
},
}, },
} }
}) })
@ -782,13 +826,14 @@ class EngineConnection {
return return
} }
this.failedConnTimeout = null this.failedConnTimeout = null
this.disconnectAll()
this.state = { this.state = {
type: EngineConnectionStateType.Disconnected, type: EngineConnectionStateType.Disconnecting,
value: { value: {
type: DisconnectedType.Timeout, type: DisconnectingType.Timeout,
}, },
} }
this.disconnectAll()
this.finalizeIfAllConnectionsClosed()
}, connectionTimeoutMs) }, connectionTimeoutMs)
this.onConnectionStarted(this) this.onConnectionStarted(this)
@ -811,11 +856,18 @@ class EngineConnection {
this.websocket?.close() this.websocket?.close()
this.unreliableDataChannel?.close() this.unreliableDataChannel?.close()
this.pc?.close() this.pc?.close()
this.webrtcStatsCollector = undefined this.webrtcStatsCollector = undefined
} }
areAllConnectionsClosed() { finalizeIfAllConnectionsClosed() {
console.log(this.websocket, this.pc, this.unreliableDataChannel) console.log(this.websocket, this.pc, this.unreliableDataChannel)
return !this.websocket && !this.pc && !this.unreliableDataChannel const allClosed =
this.websocket?.readyState === 3 &&
this.pc?.connectionState === 'closed' &&
this.unreliableDataChannel?.readyState === 'closed'
if (allClosed) {
this.state = { type: EngineConnectionStateType.Disconnected }
}
} }
} }
@ -863,10 +915,11 @@ export type CommandLog =
export class EngineCommandManager { export class EngineCommandManager {
artifactMap: ArtifactMap = {} artifactMap: ArtifactMap = {}
lastArtifactMap: ArtifactMap = {} lastArtifactMap: ArtifactMap = {}
sceneCommandArtifacts: ArtifactMap = {}
private getAst: () => Program = () => ({ start: 0, end: 0, body: [] } as any)
outSequence = 1 outSequence = 1
inSequence = 1 inSequence = 1
engineConnection?: EngineConnection engineConnection?: EngineConnection
defaultPlanes: DefaultPlanes = { xy: '', yz: '', xz: '' }
_commandLogs: CommandLog[] = [] _commandLogs: CommandLog[] = []
_commandLogCallBack: (command: CommandLog[]) => void = () => {} _commandLogCallBack: (command: CommandLog[]) => void = () => {}
// Folks should realize that wait for ready does not get called _everytime_ // Folks should realize that wait for ready does not get called _everytime_
@ -888,8 +941,16 @@ export class EngineCommandManager {
} }
} = {} as any } = {} as any
callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] =
[]
constructor() { constructor() {
this.engineConnection = undefined this.engineConnection = undefined
;(async () => {
// circular dependency needs one to be lazy loaded
const { kclManager } = await import('lang/KclSingleton')
this.getAst = () => kclManager.ast
})()
} }
start({ start({
@ -924,6 +985,11 @@ export class EngineCommandManager {
this.engineConnection = new EngineConnection({ this.engineConnection = new EngineConnection({
url, url,
token, token,
onConnectionStateChange: (state: EngineConnectionState) => {
for (let cb of this.callbacksEngineStateConnection) {
cb(state)
}
},
onEngineConnectionOpen: () => { onEngineConnectionOpen: () => {
this.resolveReady() this.resolveReady()
setIsStreamReady(true) setIsStreamReady(true)
@ -946,16 +1012,9 @@ export class EngineCommandManager {
gizmo_mode: true, gizmo_mode: true,
}, },
}) })
setupSingleton.onStreamStart()
// Initialize the planes. executeCode(undefined, true)
void this.initPlanes().then(() => {
// We execute the code here to make sure if the stream was to
// restart in a session, we want to make sure to execute the code.
// We force it to re-execute the code because we want to make sure
// the code is executed everytime the stream is restarted.
// We pass undefined for the code so it reads from the current state.
executeCode(undefined, true)
})
}, },
onClose: () => { onClose: () => {
setIsStreamReady(false) setIsStreamReady(false)
@ -1060,6 +1119,7 @@ export class EngineCommandManager {
} }
const modelingResponse = message.data.modeling_response const modelingResponse = message.data.modeling_response
const command = this.artifactMap[id] const command = this.artifactMap[id]
const sceneCommand = this.sceneCommandArtifacts[id]
this.addCommandLog({ this.addCommandLog({
type: 'receive-reliable', type: 'receive-reliable',
data: message, data: message,
@ -1072,13 +1132,21 @@ export class EngineCommandManager {
if (command && command.type === 'pending') { if (command && command.type === 'pending') {
const resolve = command.resolve const resolve = command.resolve
this.artifactMap[id] = { const artifact = {
type: 'result', type: 'result',
range: command.range, range: command.range,
pathToNode: command.pathToNode,
commandType: command.commandType, commandType: command.commandType,
parentId: command.parentId ? command.parentId : undefined, parentId: command.parentId ? command.parentId : undefined,
data: modelingResponse, data: modelingResponse,
raw: message, raw: message,
} as const
this.artifactMap[id] = artifact
if (command.commandType === 'entity_linear_pattern') {
const entities = (modelingResponse as any)?.data?.entity_ids
entities?.forEach((entity: string) => {
this.artifactMap[entity] = artifact
})
} }
resolve({ resolve({
id, id,
@ -1087,11 +1155,39 @@ export class EngineCommandManager {
data: modelingResponse, data: modelingResponse,
raw: message, raw: message,
}) })
} else { } else if (sceneCommand && sceneCommand.type === 'pending') {
const resolve = sceneCommand.resolve
const artifact = {
type: 'result',
range: sceneCommand.range,
pathToNode: sceneCommand.pathToNode,
commandType: sceneCommand.commandType,
parentId: sceneCommand.parentId ? sceneCommand.parentId : undefined,
data: modelingResponse,
raw: message,
} as const
this.sceneCommandArtifacts[id] = artifact
resolve({
id,
commandType: sceneCommand.commandType,
range: sceneCommand.range,
data: modelingResponse,
})
} else if (command) {
this.artifactMap[id] = { this.artifactMap[id] = {
type: 'result', type: 'result',
commandType: command?.commandType, commandType: command?.commandType,
range: command?.range, range: command?.range,
pathToNode: command?.pathToNode,
data: modelingResponse,
raw: message,
}
} else {
this.sceneCommandArtifacts[id] = {
type: 'result',
commandType: sceneCommand?.commandType,
range: sceneCommand?.range,
pathToNode: sceneCommand?.pathToNode,
data: modelingResponse, data: modelingResponse,
raw: message, raw: message,
} }
@ -1109,6 +1205,7 @@ export class EngineCommandManager {
this.artifactMap[id] = { this.artifactMap[id] = {
type: 'failed', type: 'failed',
range: command.range, range: command.range,
pathToNode: command.pathToNode,
commandType: command.commandType, commandType: command.commandType,
parentId: command.parentId ? command.parentId : undefined, parentId: command.parentId ? command.parentId : undefined,
errors, errors,
@ -1123,6 +1220,7 @@ export class EngineCommandManager {
this.artifactMap[id] = { this.artifactMap[id] = {
type: 'failed', type: 'failed',
range: command.range, range: command.range,
pathToNode: command.pathToNode,
commandType: command.commandType, commandType: command.commandType,
parentId: command.parentId ? command.parentId : undefined, parentId: command.parentId ? command.parentId : undefined,
errors, errors,
@ -1175,6 +1273,9 @@ export class EngineCommandManager {
) { ) {
delete this.unreliableSubscriptions[event][id] delete this.unreliableSubscriptions[event][id]
} }
onConnectionStateChange(callback: (state: EngineConnectionState) => void) {
this.callbacksEngineStateConnection.push(callback)
}
endSession() { endSession() {
// TODO: instead of sending a single command with `object_ids: Object.keys(this.artifactMap)` // TODO: instead of sending a single command with `object_ids: Object.keys(this.artifactMap)`
// we need to loop over them each individually because if the engine doesn't recognise a single // we need to loop over them each individually because if the engine doesn't recognise a single
@ -1187,12 +1288,14 @@ export class EngineCommandManager {
// this fact is very opaque in the api and docs (as to what should can be deleted). // this fact is very opaque in the api and docs (as to what should can be deleted).
// Using an array is the list is likely to grow. // Using an array is the list is likely to grow.
'start_path', 'start_path',
'entity_linear_pattern',
] ]
if (!artifactTypesToDelete.includes(artifact.commandType)) return if (artifactTypesToDelete.includes(artifact.commandType)) {
artifactsToDelete[id] = artifact artifactsToDelete[id] = artifact
}
}) })
Object.keys(artifactsToDelete).forEach((id) => { Object.keys(artifactsToDelete).forEach((id) => {
const deletCmd: EngineCommand = { const deleteCmd: EngineCommand = {
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
cmd: { cmd: {
@ -1200,7 +1303,7 @@ export class EngineCommandManager {
object_ids: [id], object_ids: [id],
}, },
} }
this.engineConnection?.send(deletCmd) this.engineConnection?.send(deleteCmd)
}) })
} }
addCommandLog(message: CommandLog) { addCommandLog(message: CommandLog) {
@ -1218,7 +1321,10 @@ export class EngineCommandManager {
registerCommandLogCallback(callback: (command: CommandLog[]) => void) { registerCommandLogCallback(callback: (command: CommandLog[]) => void) {
this._commandLogCallBack = callback this._commandLogCallBack = callback
} }
sendSceneCommand(command: EngineCommand): Promise<any> { sendSceneCommand(
command: EngineCommand,
forceWebsocket = false
): Promise<any> {
if (this.engineConnection === undefined) { if (this.engineConnection === undefined) {
return Promise.resolve() return Promise.resolve()
} }
@ -1232,7 +1338,9 @@ export class EngineCommandManager {
command.type === 'modeling_cmd_req' && command.type === 'modeling_cmd_req' &&
(command.cmd.type === 'highlight_set_entity' || (command.cmd.type === 'highlight_set_entity' ||
command.cmd.type === 'mouse_move' || command.cmd.type === 'mouse_move' ||
command.cmd.type === 'camera_drag_move') command.cmd.type === 'camera_drag_move' ||
command.cmd.type === 'default_camera_look_at' ||
command.cmd.type === ('default_camera_perspective_settings' as any))
) )
) { ) {
// highlight_set_entity, mouse_move and camera_drag_move are sent over the unreliable channel and are too noisy // highlight_set_entity, mouse_move and camera_drag_move are sent over the unreliable channel and are too noisy
@ -1249,14 +1357,23 @@ export class EngineCommandManager {
console.log('sending command', command.cmd.type) console.log('sending command', command.cmd.type)
lastMessage = command.cmd.type lastMessage = command.cmd.type
} }
if (command.type === 'modeling_cmd_batch_req') {
this.engineConnection?.send(command)
// TODO - handlePendingCommands does not handle batch commands
// return this.handlePendingCommand(command.requests[0].cmd_id, command.cmd)
return Promise.resolve()
}
if (command.type !== 'modeling_cmd_req') return Promise.resolve() if (command.type !== 'modeling_cmd_req') return Promise.resolve()
const cmd = command.cmd const cmd = command.cmd
if ( if (
(cmd.type === 'camera_drag_move' || (cmd.type === 'camera_drag_move' ||
cmd.type === 'handle_mouse_drag_move') && cmd.type === 'handle_mouse_drag_move' ||
this.engineConnection?.unreliableDataChannel cmd.type === 'default_camera_look_at' ||
cmd.type === ('default_camera_perspective_settings' as any)) &&
this.engineConnection?.unreliableDataChannel &&
!forceWebsocket
) { ) {
cmd.sequence = this.outSequence ;(cmd as any).sequence = this.outSequence
this.outSequence++ this.outSequence++
this.engineConnection?.unreliableSend(command) this.engineConnection?.unreliableSend(command)
return Promise.resolve() return Promise.resolve()
@ -1277,18 +1394,26 @@ export class EngineCommandManager {
this.engineConnection?.unreliableSend(command) this.engineConnection?.unreliableSend(command)
return Promise.resolve() return Promise.resolve()
} }
if (
command.cmd.type === 'default_camera_look_at' ||
command.cmd.type === ('default_camera_perspective_settings' as any)
) {
;(cmd as any).sequence = this.outSequence++
}
// since it's not mouse drag or highlighting send over TCP and keep track of the command // since it's not mouse drag or highlighting send over TCP and keep track of the command
this.engineConnection?.send(command) this.engineConnection?.send(command)
return this.handlePendingCommand(command.cmd_id, command.cmd) return this.handlePendingSceneCommand(command.cmd_id, command.cmd)
} }
sendModelingCommand({ sendModelingCommand({
id, id,
range, range,
command, command,
ast,
}: { }: {
id: string id: string
range: SourceRange range: SourceRange
command: EngineCommand | string command: EngineCommand | string
ast: Program
}): Promise<any> { }): Promise<any> {
if (this.engineConnection === undefined) { if (this.engineConnection === undefined) {
return Promise.resolve() return Promise.resolve()
@ -1310,17 +1435,18 @@ export class EngineCommandManager {
} }
this.engineConnection?.send(command) this.engineConnection?.send(command)
if (typeof command !== 'string' && command.type === 'modeling_cmd_req') { if (typeof command !== 'string' && command.type === 'modeling_cmd_req') {
return this.handlePendingCommand(id, command?.cmd, range) return this.handlePendingCommand(id, command?.cmd, ast, range)
} else if (typeof command === 'string') { } else if (typeof command === 'string') {
const parseCommand: EngineCommand = JSON.parse(command) const parseCommand: EngineCommand = JSON.parse(command)
if (parseCommand.type === 'modeling_cmd_req') if (parseCommand.type === 'modeling_cmd_req')
return this.handlePendingCommand(id, parseCommand?.cmd, range) return this.handlePendingCommand(id, parseCommand?.cmd, ast, range)
} }
throw Error('shouldnt reach here') throw Error('shouldnt reach here')
} }
handlePendingCommand( handlePendingSceneCommand(
id: string, id: string,
command: Models['ModelingCmd_type'], command: Models['ModelingCmd_type'],
ast?: Program,
range?: SourceRange range?: SourceRange
) { ) {
let resolve: (val: any) => void = () => {} let resolve: (val: any) => void = () => {}
@ -1333,8 +1459,42 @@ export class EngineCommandManager {
} }
// TODO handle other commands that have a parent // TODO handle other commands that have a parent
} }
const pathToNode = ast
? getNodePathFromSourceRange(ast, range || [0, 0])
: []
this.sceneCommandArtifacts[id] = {
range: range || [0, 0],
pathToNode,
type: 'pending',
commandType: command.type,
parentId: getParentId(),
promise,
resolve,
}
return promise
}
handlePendingCommand(
id: string,
command: Models['ModelingCmd_type'],
ast?: Program,
range?: SourceRange
) {
let resolve: (val: any) => void = () => {}
const promise = new Promise((_resolve, reject) => {
resolve = _resolve
})
const getParentId = (): string | undefined => {
if (command.type === 'extend_path') {
return command.path
}
// TODO handle other commands that have a parent
}
const pathToNode = ast
? getNodePathFromSourceRange(ast, range || [0, 0])
: []
this.artifactMap[id] = { this.artifactMap[id] = {
range: range || [0, 0], range: range || [0, 0],
pathToNode,
type: 'pending', type: 'pending',
commandType: command.type, commandType: command.type,
parentId: getParentId(), parentId: getParentId(),
@ -1363,9 +1523,12 @@ export class EngineCommandManager {
const range: SourceRange = JSON.parse(rangeStr) const range: SourceRange = JSON.parse(rangeStr)
// We only care about the modeling command response. // We only care about the modeling command response.
return this.sendModelingCommand({ id, range, command: commandStr }).then( return this.sendModelingCommand({
({ raw }) => JSON.stringify(raw) id,
) range,
command: commandStr,
ast: this.getAst(),
}).then(({ raw }) => JSON.stringify(raw))
} }
commandResult(id: string): Promise<any> { commandResult(id: string): Promise<any> {
const command = this.artifactMap[id] const command = this.artifactMap[id]
@ -1392,102 +1555,6 @@ export class EngineCommandManager {
artifactMap: this.artifactMap, artifactMap: this.artifactMap,
} }
} }
private async initPlanes() {
const [xy, yz, xz] = [
await this.createPlane({
x_axis: { x: 1, y: 0, z: 0 },
y_axis: { x: 0, y: 1, z: 0 },
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
}),
await this.createPlane({
x_axis: { x: 0, y: 1, z: 0 },
y_axis: { x: 0, y: 0, z: 1 },
color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
}),
await this.createPlane({
x_axis: { x: 1, y: 0, z: 0 },
y_axis: { x: 0, y: 0, z: 1 },
color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
}),
]
this.defaultPlanes = { xy, yz, xz }
this.subscribeTo({
event: 'select_with_point',
callback: ({ data }) => {
if (!data?.entity_id) return
if (
![
this.defaultPlanes.xy,
this.defaultPlanes.yz,
this.defaultPlanes.xz,
].includes(data.entity_id)
)
return
this.onPlaneSelectCallback(data.entity_id)
},
})
}
planesInitialized(): boolean {
return (
this.defaultPlanes.xy !== '' &&
this.defaultPlanes.yz !== '' &&
this.defaultPlanes.xz !== ''
)
}
onPlaneSelectCallback = (id: string) => {}
onPlaneSelected(callback: (id: string) => void) {
this.onPlaneSelectCallback = callback
}
async setPlaneHidden(id: string, hidden: boolean): Promise<string> {
return await this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'object_visible',
object_id: id,
hidden: hidden,
},
})
}
private async createPlane({
x_axis,
y_axis,
color,
}: {
x_axis: Models['Point3d_type']
y_axis: Models['Point3d_type']
color: Models['Color_type']
}): Promise<string> {
const planeId = uuidv4()
await this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'make_plane',
size: 100,
origin: { x: 0, y: 0, z: 0 },
x_axis,
y_axis,
clobber: false,
hide: true,
},
cmd_id: planeId,
})
await this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'plane_set_color',
plane_id: planeId,
color,
},
cmd_id: uuidv4(),
})
await this.setPlaneHidden(planeId, true)
return planeId
}
} }
export const engineCommandManager = new EngineCommandManager() export const engineCommandManager = new EngineCommandManager()

View File

@ -0,0 +1,61 @@
import {
readFile,
exists as tauriExists,
} from '@tauri-apps/plugin-fs'
import { isTauri } from 'lib/isTauri'
import { join } from '@tauri-apps/api/path'
/// FileSystemManager is a class that provides a way to read files from the local file system.
/// It assumes that you are in a project since it is solely used by the std lib
/// when executing code.
class FileSystemManager {
private _dir: string | null = null
get dir() {
if (this._dir === null) {
throw new Error('current project dir is not set')
}
return this._dir
}
set dir(dir: string) {
this._dir = dir
}
readFile(path: string): Promise<Uint8Array | void> {
// Using local file system only works from Tauri.
if (!isTauri()) {
throw new Error(
'This function can only be called from a Tauri application'
)
}
return join(this.dir, path)
.catch((error) => {
throw new Error(`Error reading file: ${error}`)
})
.then((file) => {
return readFile(file)
})
}
exists(path: string): Promise<boolean | void> {
// Using local file system only works from Tauri.
if (!isTauri()) {
throw new Error(
'This function can only be called from a Tauri application'
)
}
return join(this.dir, path)
.catch((error) => {
throw new Error(`Error checking file exists: ${error}`)
})
.then((file) => {
return tauriExists(file)
})
}
}
export const fileSystemManager = new FileSystemManager()

View File

@ -45,7 +45,7 @@ export function getCoordsFromPaths(skGroup: SketchGroup, index = 0): Coords2d {
} else if (!currentPath) { } else if (!currentPath) {
return [0, 0] return [0, 0]
} }
if (currentPath.type === 'topoint') { if (currentPath.type === 'ToPoint') {
return [currentPath.to[0], currentPath.to[1]] return [currentPath.to[0], currentPath.to[1]]
} }
return [0, 0] return [0, 0]
@ -445,6 +445,85 @@ export const yLine: SketchLineHelper = {
addTag: addTagWithTo('length'), addTag: addTagWithTo('length'),
} }
export const tangentialArcTo: SketchLineHelper = {
add: ({
node,
pathToNode,
to,
createCallback,
replaceExisting,
referencedSegment,
}) => {
const _node = { ...node }
const getNode = getNodeFromPathCurry(_node, pathToNode)
const { node: pipe } = getNode<PipeExpression | CallExpression>(
'PipeExpression'
)
const { node: varDec } = getNodeFromPath<VariableDeclarator>(
_node,
pathToNode,
'VariableDeclarator'
)
const toX = createLiteral(roundOff(to[0], 2))
const toY = createLiteral(roundOff(to[1], 2))
if (replaceExisting && createCallback && pipe.type !== 'CallExpression') {
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
const { callExp, valueUsedInTransform } = createCallback(
[toX, toY],
referencedSegment
)
pipe.body[callIndex] = callExp
return {
modifiedAst: _node,
pathToNode,
valueUsedInTransform,
}
}
const newLine = createCallExpression('tangentialArcTo', [
createArrayExpression([toX, toY]),
createPipeSubstitution(),
])
if (pipe.type === 'PipeExpression') {
pipe.body = [...pipe.body, newLine]
return {
modifiedAst: _node,
pathToNode: [
...pathToNode,
['body', 'PipeExpression'],
[pipe.body.length - 1, 'CallExpression'],
],
}
} else {
varDec.init = createPipeExpression([varDec.init, newLine])
}
return {
modifiedAst: _node,
pathToNode,
}
},
updateArgs: ({ node, pathToNode, to, from }) => {
const _node = { ...node }
const { node: callExpression } = getNodeFromPath<CallExpression>(
_node,
pathToNode
)
const x = createLiteral(roundOff(to[0], 2))
const y = createLiteral(roundOff(to[1], 2))
const firstArg = callExpression.arguments?.[0]
if (!mutateArrExp(firstArg, createArrayExpression([x, y]))) {
mutateObjExpProp(firstArg, createArrayExpression([x, y]), 'to')
}
return {
modifiedAst: _node,
pathToNode,
}
},
// TODO copy-paste from angledLine
addTag: addTagWithTo('angleLength'),
}
export const angledLine: SketchLineHelper = { export const angledLine: SketchLineHelper = {
add: ({ add: ({
node, node,
@ -900,6 +979,7 @@ export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
angledLineToX, angledLineToX,
angledLineToY, angledLineToY,
angledLineThatIntersects, angledLineThatIntersects,
tangentialArcTo,
} as const } as const
export function changeSketchArguments( export function changeSketchArguments(
@ -942,14 +1022,28 @@ interface CreateLineFnCallArgs {
export function compareVec2Epsilon( export function compareVec2Epsilon(
vec1: [number, number], vec1: [number, number],
vec2: [number, number] vec2: [number, number],
compareEpsilon = 0.015625 // or 2^-6
) { ) {
const compareEpsilon = 0.015625 // or 2^-6
const xDifference = Math.abs(vec1[0] - vec2[0]) const xDifference = Math.abs(vec1[0] - vec2[0])
const yDifference = Math.abs(vec1[1] - vec2[1]) const yDifference = Math.abs(vec1[1] - vec2[1])
return xDifference < compareEpsilon && yDifference < compareEpsilon return xDifference < compareEpsilon && yDifference < compareEpsilon
} }
// this version uses this distance of the two points instead of comparing x and y separately
export function compareVec2Epsilon2(
vec1: [number, number],
vec2: [number, number],
compareEpsilon = 0.015625 // or 2^-6
) {
const xDifference = Math.abs(vec1[0] - vec2[0])
const yDifference = Math.abs(vec1[1] - vec2[1])
const distance = Math.sqrt(
xDifference * xDifference + yDifference * yDifference
)
return distance < compareEpsilon
}
export function addNewSketchLn({ export function addNewSketchLn({
node: _node, node: _node,
programMemory: previousProgramMemory, programMemory: previousProgramMemory,
@ -1288,5 +1382,9 @@ export function getFirstArg(callExp: CallExpression): {
if (['angledLineThatIntersects'].includes(name)) { if (['angledLineThatIntersects'].includes(name)) {
return getAngledLineThatIntersects(callExp) return getAngledLineThatIntersects(callExp)
} }
if (['tangentialArcTo'].includes(name)) {
// TODO probably needs it's own implementation
return getFirstArgValuesForXYFns(callExp)
}
throw new Error('unexpected call expression') throw new Error('unexpected call expression')
} }

View File

@ -388,7 +388,7 @@ show(part001)`
[index, index] [index, index]
).segment ).segment
expect(segment).toEqual({ expect(segment).toEqual({
type: 'toPoint', type: 'ToPoint',
to: [5.62, 1.79], to: [5.62, 1.79],
from: [3.48, 0.44], from: [3.48, 0.44],
name: '', name: '',
@ -405,7 +405,7 @@ show(part001)`
to: [0, 0.04], to: [0, 0.04],
from: [0, 0.04], from: [0, 0.04],
name: '', name: '',
type: 'base', type: 'Base',
}) })
}) })
}) })

View File

@ -22,7 +22,7 @@ export function getSketchSegmentFromSourceRange(
startSourceRange[1] >= rangeEnd && startSourceRange[1] >= rangeEnd &&
sketchGroup.start sketchGroup.start
) )
return { segment: { ...sketchGroup.start, type: 'base' }, index: -1 } return { segment: { ...sketchGroup.start, type: 'Base' }, index: -1 }
const lineIndex = sketchGroup.value.findIndex( const lineIndex = sketchGroup.value.findIndex(
({ __geoMeta: { sourceRange } }: Path) => ({ __geoMeta: { sourceRange } }: Path) =>

View File

@ -506,7 +506,7 @@ show(part001)`
const ast = parse(code) const ast = parse(code)
const constraintLevels: ReturnType< const constraintLevels: ReturnType<
typeof getConstraintLevelFromSourceRange typeof getConstraintLevelFromSourceRange
>[] = ['full', 'partial', 'free'] >['level'][] = ['full', 'partial', 'free']
constraintLevels.forEach((constraintLevel) => { constraintLevels.forEach((constraintLevel) => {
const recursivelySeachCommentsAndCheckConstraintLevel = ( const recursivelySeachCommentsAndCheckConstraintLevel = (
str: string, str: string,
@ -520,7 +520,7 @@ show(part001)`
const expectedConstraintLevel = getConstraintLevelFromSourceRange( const expectedConstraintLevel = getConstraintLevelFromSourceRange(
[offsetIndex, offsetIndex], [offsetIndex, offsetIndex],
ast ast
) ).level
expect(expectedConstraintLevel).toBe(constraintLevel) expect(expectedConstraintLevel).toBe(constraintLevel)
return recursivelySeachCommentsAndCheckConstraintLevel( return recursivelySeachCommentsAndCheckConstraintLevel(
str, str,

View File

@ -405,8 +405,14 @@ const setAngledIntersectLineForLines: TransformInfo['createNode'] =
2 2
) )
const angle = args[0].type === 'Literal' ? Number(args[0].value) : 0 const angle = args[0].type === 'Literal' ? Number(args[0].value) : 0
const varNamMap: { [key: number]: string } = {
0: 'ZERO',
90: 'QUARTER_TURN',
180: 'HALF_TURN',
270: 'THREE_QUARTER_TURN',
}
const angleVal = [0, 90, 180, 270].includes(angle) const angleVal = [0, 90, 180, 270].includes(angle)
? createIdentifier(`_${angle}`) ? createIdentifier(varNamMap[angle])
: createLiteral(angle) : createLiteral(angle)
return intersectCallWrapper({ return intersectCallWrapper({
fnName: 'angledLineThatIntersects', fnName: 'angledLineThatIntersects',
@ -455,7 +461,7 @@ const setAngleBetweenCreateNode =
firstHalfValue = createBinaryExpression([ firstHalfValue = createBinaryExpression([
firstHalfValue, firstHalfValue,
'+', '+',
createIdentifier('_180'), createIdentifier('HALF_TURN'),
]) ])
valueUsedInTransform = normaliseAngle(valueUsedInTransform - 180) valueUsedInTransform = normaliseAngle(valueUsedInTransform - 180)
} }
@ -1503,20 +1509,21 @@ function getArgLiteralVal(arg: Value): number {
export function getConstraintLevelFromSourceRange( export function getConstraintLevelFromSourceRange(
cursorRange: Selection['range'], cursorRange: Selection['range'],
ast: Program ast: Program
): 'free' | 'partial' | 'full' { ): { range: [number, number]; level: 'free' | 'partial' | 'full' } {
const { node: sketchFnExp } = getNodeFromPath<CallExpression>( const { node: sketchFnExp } = getNodeFromPath<CallExpression>(
ast, ast,
getNodePathFromSourceRange(ast, cursorRange), getNodePathFromSourceRange(ast, cursorRange),
'CallExpression' 'CallExpression'
) )
const name = sketchFnExp?.callee?.name as ToolTip const name = sketchFnExp?.callee?.name as ToolTip
if (!toolTips.includes(name)) return 'free' const range: [number, number] = [sketchFnExp.start, sketchFnExp.end]
if (!toolTips.includes(name)) return { level: 'free', range: range }
const firstArg = getFirstArg(sketchFnExp) const firstArg = getFirstArg(sketchFnExp)
// check if the function is fully constrained // check if the function is fully constrained
if (isNotLiteralArrayOrStatic(firstArg.val)) { if (isNotLiteralArrayOrStatic(firstArg.val)) {
return 'full' return { level: 'full', range: range }
} }
// check if the function has no constraints // check if the function has no constraints
@ -1525,10 +1532,10 @@ export function getConstraintLevelFromSourceRange(
const isOneValFree = const isOneValFree =
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val) !Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isTwoValFree) return 'free' if (isTwoValFree) return { level: 'free', range: range }
if (isOneValFree) return 'partial' if (isOneValFree) return { level: 'partial', range: range }
return 'partial' return { level: 'partial', range: range }
} }
export function isLiteralArrayOrStatic( export function isLiteralArrayOrStatic(

View File

@ -26,15 +26,6 @@ export function pathMapToSelections(
return newSelections return newSelections
} }
export function isReducedMotion(): boolean {
return (
typeof window !== 'undefined' &&
window.matchMedia &&
// TODO/Note I (Kurt) think '(prefers-reduced-motion: reduce)' and '(prefers-reduced-motion)' are equivalent, but not 100% sure
window.matchMedia('(prefers-reduced-motion)').matches
)
}
export function isCursorInSketchCommandRange( export function isCursorInSketchCommandRange(
artifactMap: ArtifactMap, artifactMap: ArtifactMap,
selectionRanges: Selections selectionRanges: Selections

View File

@ -4,6 +4,9 @@ import init, {
execute_wasm, execute_wasm,
lexer_wasm, lexer_wasm,
modify_ast_for_sketch_wasm, modify_ast_for_sketch_wasm,
is_points_ccw,
get_tangential_arc_to_info,
program_memory_init,
} from '../wasm-lib/pkg/wasm_lib' } from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors' import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
@ -12,7 +15,8 @@ import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem' import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
import type { Program } from '../wasm-lib/kcl/bindings/Program' import type { Program } from '../wasm-lib/kcl/bindings/Program'
import type { Token } from '../wasm-lib/kcl/bindings/Token' import type { Token } from '../wasm-lib/kcl/bindings/Token'
import { DefaultPlanes } from '../wasm-lib/kcl/bindings/DefaultPlanes' import { Coords2d } from './std/sketch'
import { fileSystemManager } from 'lang/std/fileSystemManager'
export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Value } from '../wasm-lib/kcl/bindings/Value' export type { Value } from '../wasm-lib/kcl/bindings/Value'
@ -118,15 +122,13 @@ export interface ProgramMemory {
export const executor = async ( export const executor = async (
node: Program, node: Program,
programMemory: ProgramMemory = { root: {}, return: null }, programMemory: ProgramMemory = { root: {}, return: null },
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager
planes: DefaultPlanes
): Promise<ProgramMemory> => { ): Promise<ProgramMemory> => {
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
const _programMemory = await _executor( const _programMemory = await _executor(
node, node,
programMemory, programMemory,
engineCommandManager, engineCommandManager
planes
) )
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()
@ -137,18 +139,18 @@ export const executor = async (
export const _executor = async ( export const _executor = async (
node: Program, node: Program,
programMemory: ProgramMemory = { root: {}, return: null }, programMemory: ProgramMemory = { root: {}, return: null },
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager
planes: DefaultPlanes
): Promise<ProgramMemory> => { ): Promise<ProgramMemory> => {
try { try {
const memory: ProgramMemory = await execute_wasm( const memory: ProgramMemory = await execute_wasm(
JSON.stringify(node), JSON.stringify(node),
JSON.stringify(programMemory), JSON.stringify(programMemory),
engineCommandManager, engineCommandManager,
JSON.stringify(planes) fileSystemManager
) )
return memory return memory
} catch (e: any) { } catch (e: any) {
console.log(e)
const parsed: RustKclError = JSON.parse(e.toString()) const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError( const kclError = new KCLError(
parsed.kind, parsed.kind,
@ -212,3 +214,62 @@ export const modifyAstForSketch = async (
throw kclError throw kclError
} }
} }
export function isPointsCCW(points: Coords2d[]): number {
return is_points_ccw(new Float64Array(points.flat()))
}
export function getTangentialArcToInfo({
arcStartPoint,
arcEndPoint,
tanPreviousPoint,
obtuse = true,
}: {
arcStartPoint: Coords2d
arcEndPoint: Coords2d
tanPreviousPoint: Coords2d
obtuse?: boolean
}): {
center: Coords2d
arcMidPoint: Coords2d
radius: number
startAngle: number
endAngle: number
ccw: boolean
} {
const result = get_tangential_arc_to_info(
arcStartPoint[0],
arcStartPoint[1],
arcEndPoint[0],
arcEndPoint[1],
tanPreviousPoint[0],
tanPreviousPoint[1],
obtuse
)
return {
center: [result.center_x, result.center_y],
arcMidPoint: [result.arc_mid_point_x, result.arc_mid_point_y],
radius: result.radius,
startAngle: result.start_angle,
endAngle: result.end_angle,
ccw: result.ccw > 0,
}
}
export function programMemoryInit(): ProgramMemory {
try {
const memory: ProgramMemory = program_memory_init()
return memory
} catch (e: any) {
console.log(e)
const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError(
parsed.kind,
parsed.msg,
rangeTypeFix(parsed.sourceRanges)
)
console.log(kclError)
throw kclError
}
}

View File

@ -33,7 +33,7 @@ interface MouseGuardZoomHandler {
lenientDragStartButton?: number lenientDragStartButton?: number
} }
interface MouseGuard { export interface MouseGuard {
pan: MouseGuardHandler pan: MouseGuardHandler
zoom: MouseGuardZoomHandler zoom: MouseGuardZoomHandler
rotate: MouseGuardHandler rotate: MouseGuardHandler

View File

@ -36,10 +36,12 @@ export const modelingMachineConfig: CommandSetConfig<
selectionTypes: ['face'], selectionTypes: ['face'],
multiple: false, // TODO: multiple selection multiple: false, // TODO: multiple selection
required: true, required: true,
skip: true,
}, },
// result: { // result: {
// inputType: 'options', // inputType: 'options',
// payload: 'add', // defaultValue: 'add',
// skip: true,
// required: true, // required: true,
// options: EXTRUSION_RESULTS.map((r) => ({ // options: EXTRUSION_RESULTS.map((r) => ({
// name: r, // name: r,

View File

@ -83,7 +83,6 @@ export type CommandArgumentConfig<
required: boolean required: boolean
skip?: true skip?: true
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType) defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
payload?: OutputType
} & ( } & (
| { | {
inputType: Extract<CommandInputType, 'options'> inputType: Extract<CommandInputType, 'options'>
@ -106,8 +105,8 @@ export type CommandArgument<
| { | {
description?: string description?: string
required: boolean required: boolean
payload?: OutputType // Payload sets the initialized value and more importantly its type skip?: true
defaultValue?: OutputType // Default value is used as the starting value for the input on this argument defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
} & ( } & (
| { | {
inputType: Extract<CommandInputType, 'options'> inputType: Extract<CommandInputType, 'options'>

View File

@ -115,7 +115,7 @@ function buildCommandArgument<
const baseCommandArgument = { const baseCommandArgument = {
description: arg.description, description: arg.description,
required: arg.required, required: arg.required,
payload: arg.payload, skip: arg.skip,
defaultValue: defaultValue:
arg.defaultValue instanceof Function arg.defaultValue instanceof Function
? arg.defaultValue(state.context) ? arg.defaultValue(state.context)

View File

@ -1,5 +1,8 @@
export function isTauri(): boolean { export function isTauri(): boolean {
// return '__TAURI__' in window // TODO: fix it this broke in v2
// TODO: replace with working check
return true return true
if (typeof window !== 'undefined') {
return '__TAURI__' in window
}
return false
} }

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