Compare commits
95 Commits
grackle-la
...
v0.15.0
Author | SHA1 | Date | |
---|---|---|---|
70f3ded7e2 | |||
095108252b | |||
20b1c93f12 | |||
3747a1b993 | |||
198feb7d44 | |||
c7a8b8313e | |||
1576dc3256 | |||
341a3b7609 | |||
ecb42b89a6 | |||
f00ee3a44a | |||
900e3b96ad | |||
15fae05659 | |||
2730b6d152 | |||
602e7afef6 | |||
d9bcadb062 | |||
19f669b94c | |||
d9ef471385 | |||
39f8b306a2 | |||
19925d22c1 | |||
e1af4b4219 | |||
c699611f5b | |||
00ede7ec1a | |||
f30601bd2c | |||
cfbc77b62f | |||
808830d29e | |||
e714103655 | |||
fbcb96add5 | |||
7386ccf1bf | |||
6e73578933 | |||
b88d5c8799 | |||
5430c1fa66 | |||
c0d4bb6c9f | |||
25260a88c3 | |||
b6d6f0f4c1 | |||
b1276b2ed8 | |||
5f0f3f40d0 | |||
f1ea9b6ece | |||
b94c5be1af | |||
8378eb1e94 | |||
05f98a8c39 | |||
386571fa60 | |||
b0abdf4f70 | |||
81e70e139f | |||
d6bfc38d62 | |||
ada66de92d | |||
8f133f9662 | |||
b360dbb961 | |||
eca3dc2967 | |||
ae36ab6982 | |||
8cb6cf1b8a | |||
3c235c890a | |||
b6dfd30840 | |||
65d128eecd | |||
77b7c602f2 | |||
fa0e61a2be | |||
1cf35a611e | |||
952d0e4c7c | |||
0f85de9df8 | |||
0e8eed3f82 | |||
5b43a5075f | |||
f5ed4e37b2 | |||
19c8da1a86 | |||
a25f89aaba | |||
aeebe5416f | |||
661788b8b0 | |||
ac24563159 | |||
d17342dfb8 | |||
2e93b58ae6 | |||
6593656b08 | |||
47be749ec7 | |||
a03e7f5c41 | |||
b78e9fa131 | |||
c629233eaa | |||
f640f7a5e0 | |||
64398381a9 | |||
0bc5534056 | |||
9fc1df7c1d | |||
a5879ceeda | |||
379c30824e | |||
a4d3263b88 | |||
c1f661ab52 | |||
7d887a1497 | |||
4ca341e132 | |||
c6249f36d2 | |||
dcbe5d7f75 | |||
390cb2d51d | |||
98f7a564ea | |||
05f9e3c290 | |||
09760fc2e9 | |||
18ffc43e89 | |||
de63e4f19f | |||
b70b271e6b | |||
08b7cdc5f6 | |||
6efe6b54c0 | |||
69f72d62e0 |
@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
ignore-words-list: crate,everytime
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo
|
||||
skip: **/target,node_modules,build,**/Cargo.lock
|
||||
|
@ -1 +1,2 @@
|
||||
src/wasm-lib/*
|
||||
*.typegen.ts
|
||||
|
@ -17,12 +17,12 @@
|
||||
"never"
|
||||
],
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"@typescript-eslint/no-floating-promises": "warn"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
|
||||
"rules": {
|
||||
"@typescript-eslint/no-floating-promises": "warn",
|
||||
"testing-library/prefer-screen-queries": "off"
|
||||
}
|
||||
}
|
||||
|
29
.github/workflows/announce_release.yml
vendored
Normal 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
|
27
.github/workflows/ci.yml
vendored
@ -46,6 +46,7 @@ jobs:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- run: yarn build:wasm
|
||||
- run: yarn xstate:typegen
|
||||
- run: yarn tsc
|
||||
|
||||
|
||||
@ -125,7 +126,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
os: [macos-14, ubuntu-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@ -188,10 +189,10 @@ jobs:
|
||||
- name: Fix format
|
||||
run: yarn fmt
|
||||
|
||||
- name: Install Universal target (MacOS only)
|
||||
if: matrix.os == 'macos-latest'
|
||||
- name: Install x86 target for Universal builds (MacOS only)
|
||||
if: matrix.os == 'macos-14'
|
||||
run: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
|
||||
- name: Prepare certificate and variables (Windows only)
|
||||
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
|
||||
@ -225,7 +226,7 @@ jobs:
|
||||
with:
|
||||
includeRelease: false
|
||||
includeDebug: true
|
||||
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
|
||||
args: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }}
|
||||
|
||||
- name: Build the app (release) and sign
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
@ -241,12 +242,12 @@ jobs:
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
|
||||
with:
|
||||
args: "${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
|
||||
args: "${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: matrix.os != 'ubuntu-latest'
|
||||
env:
|
||||
PREFIX: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
|
||||
PREFIX: ${{ matrix.os == 'macos-14' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
|
||||
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
|
||||
with:
|
||||
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
|
||||
@ -254,7 +255,7 @@ jobs:
|
||||
- name: Run e2e tests (linux only)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
cargo install tauri-driver
|
||||
cargo install tauri-driver@0.1.3
|
||||
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
|
||||
export VITE_KC_API_BASE_URL
|
||||
xvfb-run yarn test:e2e:tauri
|
||||
@ -337,17 +338,17 @@ jobs:
|
||||
cat last_download.json
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: 'google-github-actions/auth@v2.0.0'
|
||||
uses: 'google-github-actions/auth@v2.1.1'
|
||||
with:
|
||||
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
||||
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@v2.0.0
|
||||
uses: google-github-actions/setup-gcloud@v2.1.0
|
||||
with:
|
||||
project_id: kittycadapi
|
||||
|
||||
- name: Upload release files to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.0.0
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.0
|
||||
with:
|
||||
path: artifact
|
||||
glob: '*/Zoo*'
|
||||
@ -355,13 +356,13 @@ jobs:
|
||||
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
|
||||
|
||||
- name: Upload update endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.0.0
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.0
|
||||
with:
|
||||
path: last_update.json
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
|
||||
- name: Upload download endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.0.0
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.0
|
||||
with:
|
||||
path: last_download.json
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
|
4
.gitignore
vendored
@ -50,3 +50,7 @@ e2e/playwright/export-snapshots/*embedded.gltf
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
|
||||
## generated files
|
||||
src/**/*.typegen.ts
|
||||
|
@ -10,4 +10,4 @@ src/wasm-lib/kcl/bindings
|
||||
e2e/playwright/export-snapshots
|
||||
|
||||
# XState generated files
|
||||
src/machines/modelingMachine.typegen.ts
|
||||
src/machines/**.typegen.ts
|
||||
|
@ -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
|
||||
```bash
|
||||
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
|
||||
|
||||
|
16
docs/kcl/KNOWN-ISSUES.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Known Issues
|
||||
|
||||
The following are bugs that are not in modeling-app or kcl itself. These bugs
|
||||
once fixed in engine will just start working here with no language changes.
|
||||
|
||||
- **Sketch on Face**: If your sketch is outside the edges of the face (on which you
|
||||
are sketching) you will get multiple models returned instead of one single
|
||||
model for that sketch and its underlying 3D object.
|
||||
|
||||
- **Patterns**: If you try and pass a pattern to `hole` currently only the first
|
||||
item in the pattern is being subtracted. This is an engine bug that is being
|
||||
worked on.
|
||||
|
||||
- **Import**: Right now you can import a file, even if that file has brep data
|
||||
you cannot edit it. You also cannot move or transform the imported objects at
|
||||
all. In the future, after v1, the engine will account for this.
|
31354
docs/kcl/std.json
5791
docs/kcl/std.md
@ -498,7 +498,7 @@
|
||||
}
|
||||
],
|
||||
"asset": {
|
||||
"generator": "kittycad.io",
|
||||
"generator": "zoo.dev",
|
||||
"version": "2.0"
|
||||
},
|
||||
"buffers": [
|
||||
@ -882,7 +882,7 @@
|
||||
"loops": [
|
||||
[
|
||||
14,
|
||||
-1
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
@ -1953,12 +1953,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
-1,
|
||||
"xAxis": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
0,
|
||||
0,
|
||||
-1
|
||||
],
|
||||
"origin": [
|
||||
0,
|
||||
-0.0127,
|
||||
0.050800000000000005
|
||||
@ -1968,12 +1973,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
"xAxis": [
|
||||
1,
|
||||
0,
|
||||
-1,
|
||||
-0
|
||||
0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
-0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"origin": [
|
||||
0.039310000000000005,
|
||||
-0.0254,
|
||||
0.050800000000000005
|
||||
@ -1983,12 +1993,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
"xAxis": [
|
||||
0.81915,
|
||||
-0.5735800000000001,
|
||||
-0.81915,
|
||||
0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"origin": [
|
||||
0.11489,
|
||||
-0.050800000000000005,
|
||||
0.050800000000000005
|
||||
@ -1998,12 +2013,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
"xAxis": [
|
||||
1,
|
||||
0,
|
||||
-1,
|
||||
-0
|
||||
0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
-0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"origin": [
|
||||
0.19623,
|
||||
-0.0762,
|
||||
0.050800000000000005
|
||||
@ -2013,12 +2033,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
1,
|
||||
"xAxis": [
|
||||
0,
|
||||
-0
|
||||
1,
|
||||
0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
-0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"origin": [
|
||||
0.2413,
|
||||
-0.06985000000000001,
|
||||
0.050800000000000005
|
||||
@ -2028,12 +2053,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
0,
|
||||
"xAxis": [
|
||||
1,
|
||||
-0
|
||||
0,
|
||||
0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
0,
|
||||
-0,
|
||||
-1
|
||||
],
|
||||
"origin": [
|
||||
0.19823,
|
||||
-0.0635,
|
||||
0.050800000000000005
|
||||
@ -2043,12 +2073,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
0.5735800000000001,
|
||||
"xAxis": [
|
||||
0.81915,
|
||||
-0.5735800000000001,
|
||||
0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
0,
|
||||
0,
|
||||
-1
|
||||
],
|
||||
"origin": [
|
||||
0.10982,
|
||||
-0.03175,
|
||||
0.050800000000000005
|
||||
@ -2058,12 +2093,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
"xAxis": [
|
||||
0.90631,
|
||||
0.4226200000000001,
|
||||
-0.90631,
|
||||
-0
|
||||
0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
-0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"origin": [
|
||||
0.10533,
|
||||
0.01905,
|
||||
0.050800000000000005
|
||||
@ -2073,12 +2113,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
"xAxis": [
|
||||
1,
|
||||
0,
|
||||
-1,
|
||||
-0
|
||||
0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
-0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"origin": [
|
||||
0.19374,
|
||||
0.0381,
|
||||
0.050800000000000005
|
||||
@ -2088,12 +2133,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
1,
|
||||
"xAxis": [
|
||||
0,
|
||||
-0
|
||||
1,
|
||||
0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
-0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"origin": [
|
||||
0.2413,
|
||||
0.04445,
|
||||
0.050800000000000005
|
||||
@ -2103,12 +2153,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
0,
|
||||
"xAxis": [
|
||||
1,
|
||||
-0
|
||||
0,
|
||||
0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
0,
|
||||
-0,
|
||||
-1
|
||||
],
|
||||
"origin": [
|
||||
0.19234,
|
||||
0.050800000000000005,
|
||||
0.050800000000000005
|
||||
@ -2118,12 +2173,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
-0.4226200000000001,
|
||||
"xAxis": [
|
||||
0.90631,
|
||||
0.4226200000000001,
|
||||
0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
0,
|
||||
0,
|
||||
-1
|
||||
],
|
||||
"origin": [
|
||||
0.11614,
|
||||
0.0381,
|
||||
0.050800000000000005
|
||||
@ -2133,12 +2193,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
0,
|
||||
"xAxis": [
|
||||
1,
|
||||
-0
|
||||
0,
|
||||
0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
0,
|
||||
-0,
|
||||
-1
|
||||
],
|
||||
"origin": [
|
||||
0.04445,
|
||||
0.0254,
|
||||
0.050800000000000005
|
||||
@ -2148,12 +2213,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
-1,
|
||||
"xAxis": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
0,
|
||||
0,
|
||||
-1
|
||||
],
|
||||
"origin": [
|
||||
0,
|
||||
0.0127,
|
||||
0.050800000000000005
|
||||
@ -2163,12 +2233,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
"xAxis": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"origin": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
@ -2178,12 +2253,17 @@
|
||||
{
|
||||
"type": "plane",
|
||||
"plane": {
|
||||
"normal": [
|
||||
"xAxis": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
-0
|
||||
],
|
||||
"point": [
|
||||
"yAxis": [
|
||||
-0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"origin": [
|
||||
0,
|
||||
0,
|
||||
0.1016
|
||||
@ -2191,7 +2271,7 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"curves": [
|
||||
"curves3D": [
|
||||
{
|
||||
"type": "line",
|
||||
"line": {
|
||||
|
@ -1,7 +1,7 @@
|
||||
ISO-10303-21;
|
||||
HEADER;
|
||||
FILE_DESCRIPTION((('kittycad.io 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_DESCRIPTION((('zoo.dev export')), '2;1');
|
||||
FILE_NAME('dump.step', '1970-01-01T00:00:00.0+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
|
||||
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
@ -384,23 +384,23 @@ DATA;
|
||||
#368 = ORIENTED_EDGE('NONE', *, *, #269, .T.);
|
||||
#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));
|
||||
#371 = DIRECTION('NONE', (-1, 0, -0));
|
||||
#371 = DIRECTION('NONE', (-1, -0, 0));
|
||||
#372 = AXIS2_PLACEMENT_3D('NONE', #370, #371, $);
|
||||
#373 = PLANE('NONE', #372);
|
||||
#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, $);
|
||||
#377 = PLANE('NONE', #376);
|
||||
#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, $);
|
||||
#381 = PLANE('NONE', #380);
|
||||
#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, $);
|
||||
#385 = PLANE('NONE', #384);
|
||||
#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, $);
|
||||
#389 = PLANE('NONE', #388);
|
||||
#390 = CARTESIAN_POINT('NONE', (0.19823384137660915, -0.0635, 0.0508));
|
||||
@ -408,19 +408,19 @@ DATA;
|
||||
#392 = AXIS2_PLACEMENT_3D('NONE', #390, #391, $);
|
||||
#393 = PLANE('NONE', #392);
|
||||
#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, $);
|
||||
#397 = PLANE('NONE', #396);
|
||||
#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, $);
|
||||
#401 = PLANE('NONE', #400);
|
||||
#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, $);
|
||||
#405 = PLANE('NONE', #404);
|
||||
#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, $);
|
||||
#409 = PLANE('NONE', #408);
|
||||
#410 = CARTESIAN_POINT('NONE', (0.19233523789047138, 0.0508, 0.0508));
|
||||
@ -436,7 +436,7 @@ DATA;
|
||||
#420 = AXIS2_PLACEMENT_3D('NONE', #418, #419, $);
|
||||
#421 = PLANE('NONE', #420);
|
||||
#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, $);
|
||||
#425 = PLANE('NONE', #424);
|
||||
#426 = CARTESIAN_POINT('NONE', (0, 0, -0));
|
||||
@ -475,7 +475,7 @@ DATA;
|
||||
#459 = ADVANCED_FACE('NONE', (#458), #421, .T.);
|
||||
#460 = FACE_OUTER_BOUND('NONE', #339, .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.);
|
||||
#464 = FACE_OUTER_BOUND('NONE', #369, .T.);
|
||||
#465 = ADVANCED_FACE('NONE', (#464), #433, .T.);
|
||||
|
@ -100,15 +100,15 @@ endfacet
|
||||
facet normal 0.57357645 0 0.819152
|
||||
outer loop
|
||||
vertex 3.4311862 -0 -0.625
|
||||
vertex 2.5385938 -0 0
|
||||
vertex 2.5385938 -4 0
|
||||
vertex 3.4311862 -4 -0.625
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0.57357645 -0 0.819152
|
||||
facet normal 0.5735763 0 0.8191522
|
||||
outer loop
|
||||
vertex 3.4311862 -4 -0.625
|
||||
vertex 4.323779 -0 -1.25
|
||||
vertex 3.4311862 -0 -0.625
|
||||
vertex 2.5385938 -4 0
|
||||
vertex 3.4311862 -4 -0.625
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0.57357645 -0 0.819152
|
||||
@ -118,11 +118,11 @@ facet normal 0.57357645 -0 0.819152
|
||||
vertex 4.323779 -0 -1.25
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0.5735763 0 0.8191522
|
||||
facet normal 0.57357645 -0 0.819152
|
||||
outer loop
|
||||
vertex 2.5385938 -4 0
|
||||
vertex 3.4311862 -0 -0.625
|
||||
vertex 3.4311862 -4 -0.625
|
||||
vertex 4.323779 -0 -1.25
|
||||
vertex 2.5385938 -0 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0.42261824 0 -0.9063078
|
||||
@ -142,29 +142,29 @@ endfacet
|
||||
facet normal 0.42261824 0 -0.9063078
|
||||
outer loop
|
||||
vertex 3.342784 -0 0.375
|
||||
vertex 4.146974 -0 0.75
|
||||
vertex 4.146974 -4 0.75
|
||||
vertex 3.342784 -4 0.375
|
||||
vertex 2.5385938 -0 0
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0.42261833 0 -0.90630776
|
||||
outer loop
|
||||
vertex 4.146974 -0 0.75
|
||||
vertex 5.755354 -0 1.5
|
||||
vertex 5.755354 -4 1.5
|
||||
vertex 4.146974 -4 0.75
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0.42261824 0 -0.9063078
|
||||
outer loop
|
||||
vertex 3.342784 -4 0.375
|
||||
vertex 2.5385938 -0 0
|
||||
vertex 4.146974 -4 0.75
|
||||
vertex 3.342784 -0 0.375
|
||||
vertex 4.146974 -0 0.75
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0.42261833 0 -0.90630776
|
||||
outer loop
|
||||
vertex 5.755354 -4 1.5
|
||||
vertex 4.146974 -4 0.75
|
||||
vertex 4.146974 -0 0.75
|
||||
vertex 5.755354 -0 1.5
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 0 -1
|
||||
@ -258,6 +258,13 @@ facet normal 0 1 -0
|
||||
vertex 3.5 -0 1
|
||||
endloop
|
||||
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
|
||||
outer loop
|
||||
vertex 3.4311862 -0 -0.625
|
||||
@ -265,11 +272,53 @@ facet normal 0 1 0
|
||||
vertex 3.0950184 -0 -1
|
||||
endloop
|
||||
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
|
||||
outer loop
|
||||
vertex 3.342784 -0 0.375
|
||||
vertex 3.5 -0 1
|
||||
vertex 0 -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 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
|
||||
endfacet
|
||||
facet normal 0 0.99999994 0
|
||||
@ -281,86 +330,37 @@ facet normal 0 0.99999994 0
|
||||
endfacet
|
||||
facet normal 0 1 0
|
||||
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 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 4.323779 -0 -1.25
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 0.99999994 0
|
||||
outer loop
|
||||
vertex 9.5 -0 -2.5
|
||||
vertex 9.5 -0 -3
|
||||
vertex 6.108964 -0 -2.5
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 1 0
|
||||
outer loop
|
||||
vertex 5.644507 -0 2
|
||||
vertex 5.755354 -0 1.5
|
||||
vertex 4.146974 -0 0.75
|
||||
vertex 6.108964 -0 -2.5
|
||||
vertex 9.5 -0 -3
|
||||
vertex 5.9513144 -0 -3
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 0.99999994 -0
|
||||
facet normal 0 1 0
|
||||
outer loop
|
||||
vertex 2.5385938 -0 0
|
||||
vertex 3.0950184 -0 -1
|
||||
vertex 2.5385938 -0 0
|
||||
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
|
||||
vertex 0 -0 -1
|
||||
endloop
|
||||
endfacet
|
||||
facet normal 0 1 0
|
||||
outer loop
|
||||
vertex 2.5385938 -0 0
|
||||
vertex 0 -0 -1
|
||||
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 0 -0 1
|
||||
endloop
|
||||
endfacet
|
||||
facet normal -0 -1 0
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { secrets } from './secrets'
|
||||
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { getUtils } from './test-utils'
|
||||
import waitOn from 'wait-on'
|
||||
import { Themes } from '../../src/lib/theme'
|
||||
@ -53,40 +51,38 @@ test('Basic sketch', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
await u.clearCommandLogs()
|
||||
await Promise.all([
|
||||
u.doAndWaitForImageDiff(
|
||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||
200
|
||||
),
|
||||
u.waitForDefaultPlanesVisibilityChange(),
|
||||
])
|
||||
await u.doAndWaitForImageDiff(
|
||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||
200
|
||||
)
|
||||
|
||||
// select a plane
|
||||
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter')
|
||||
await u.waitForCmdReceive('set_tool')
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByRole('button', { name: 'Line' }).click(),
|
||||
'set_tool'
|
||||
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 u.doAndWaitForCmd(
|
||||
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10),
|
||||
'mouse_click',
|
||||
false
|
||||
)
|
||||
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 startAt = '[18.26, -24.63]'
|
||||
const num = '18.43'
|
||||
const num = 24.11
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
@ -97,27 +93,22 @@ test('Basic sketch', async ({ page }) => {
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)`)
|
||||
|> line([0, ${num + 0.01}], %)`)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)
|
||||
|> line([-36.69, 0], %)`)
|
||||
|> line([0, ${num + 0.01}], %)
|
||||
|> line([-48, 0], %)`)
|
||||
|
||||
// deselect line tool
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByRole('button', { name: 'Line' }).click(),
|
||||
'set_tool'
|
||||
)
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// click between first two clicks to get center of the line
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10),
|
||||
'select_with_point'
|
||||
)
|
||||
await u.closeDebugPanel()
|
||||
await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// hold down shift
|
||||
await page.keyboard.down('Shift')
|
||||
@ -133,7 +124,7 @@ test('Basic sketch', async ({ page }) => {
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line({ to: [${num}, 0], tag: 'seg01' }, %)
|
||||
|> line([0, ${num}], %)
|
||||
|> line([0, ${num + 0.01}], %)
|
||||
|> angledLine([180, segLen('seg01', %)], %)`)
|
||||
})
|
||||
|
||||
@ -206,7 +197,7 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
|
||||
test('executes on load', async ({ page, context }) => {
|
||||
const u = getUtils(page)
|
||||
await context.addInitScript(async (token) => {
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const part001 = startSketchOn('-XZ')
|
||||
@ -271,59 +262,41 @@ test('Can create sketches on all planes and their back sides', async ({
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
const camCmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
center: { x: 15, y: 0, z: 0 },
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
vantage: { x: 30, y: 30, z: 30 },
|
||||
},
|
||||
}
|
||||
const camPos: [number, number, number] = [100, 100, 100]
|
||||
|
||||
const TestSinglePlane = async ({
|
||||
viewCmd,
|
||||
expectedCode,
|
||||
clickCoords,
|
||||
}: {
|
||||
viewCmd: EngineCommand
|
||||
viewCmd: [number, number, number]
|
||||
expectedCode: string
|
||||
clickCoords: { x: number; y: number }
|
||||
}) => {
|
||||
await u.openDebugPanel()
|
||||
await u.sendCustomCmd(viewCmd)
|
||||
|
||||
await u.updateCamPosition(viewCmd)
|
||||
|
||||
await u.clearCommandLogs()
|
||||
// await page.waitForTimeout(200)
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
await u.closeDebugPanel()
|
||||
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()
|
||||
|
||||
// draw a line
|
||||
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 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 page.getByRole('button', { name: 'Line' }).click()
|
||||
await u.clearCommandLogs()
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
|
||||
@ -333,51 +306,41 @@ test('Can create sketches on all planes and their back sides', async ({
|
||||
|
||||
const codeTemplate = (
|
||||
plane = 'XY',
|
||||
sign = ''
|
||||
rounded = false,
|
||||
otherThing = '1'
|
||||
) => `const part001 = startSketchOn('${plane}')
|
||||
|> startProfileAt([${sign}6.88, -9.29], %)
|
||||
|> line([${sign}6.95, 0], %)`
|
||||
|> startProfileAt([28.9${otherThing}, -39${rounded ? '' : '.01'}], %)`
|
||||
await TestSinglePlane({
|
||||
viewCmd: camCmd,
|
||||
viewCmd: camPos,
|
||||
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({
|
||||
viewCmd: camCmd,
|
||||
expectedCode: codeTemplate('YZ'),
|
||||
clickCoords: { x: 1000, y: 200 }, // green plane
|
||||
viewCmd: camPos,
|
||||
expectedCode: codeTemplate('YZ', true),
|
||||
clickCoords: { x: 700, y: 300 }, // green plane
|
||||
})
|
||||
await TestSinglePlane({
|
||||
viewCmd: camCmd,
|
||||
expectedCode: codeTemplate('XZ', '-'),
|
||||
clickCoords: { x: 630, y: 130 }, // blue plane
|
||||
viewCmd: camPos,
|
||||
expectedCode: codeTemplate('XZ'),
|
||||
clickCoords: { x: 700, y: 80 }, // blue plane
|
||||
})
|
||||
|
||||
// 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 },
|
||||
},
|
||||
}
|
||||
const camCmdBackSide: [number, number, number] = [-100, -100, -100]
|
||||
await TestSinglePlane({
|
||||
viewCmd: camCmdBackSide,
|
||||
expectedCode: codeTemplate('-XY', '-'),
|
||||
clickCoords: { x: 705, y: 136 }, // back of red plane
|
||||
expectedCode: codeTemplate('-XY', false, '3'),
|
||||
clickCoords: { x: 601, y: 118 }, // back of red plane
|
||||
})
|
||||
await TestSinglePlane({
|
||||
viewCmd: camCmdBackSide,
|
||||
expectedCode: codeTemplate('-YZ', '-'),
|
||||
clickCoords: { x: 1000, y: 350 }, // back of green plane
|
||||
expectedCode: codeTemplate('-YZ'),
|
||||
clickCoords: { x: 730, y: 219 }, // back of green plane
|
||||
})
|
||||
await TestSinglePlane({
|
||||
viewCmd: camCmdBackSide,
|
||||
expectedCode: codeTemplate('-XZ'),
|
||||
clickCoords: { x: 600, y: 400 }, // back of blue plane
|
||||
expectedCode: codeTemplate('-XZ', true),
|
||||
clickCoords: { x: 680, y: 427 }, // back of blue plane
|
||||
})
|
||||
})
|
||||
|
||||
@ -387,7 +350,6 @@ test('Auto complete works', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
// this test might be brittle as we add and remove functions
|
||||
// but should also be easy to update.
|
||||
@ -478,38 +440,36 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
const xAxisClick = () => page.mouse.click(700, 250)
|
||||
const emptySpaceClick = () => page.mouse.click(700, 300)
|
||||
const topHorzSegmentClick = () => page.mouse.click(700, 285)
|
||||
const bottomHorzSegmentClick = () => page.mouse.click(750, 393)
|
||||
const xAxisClick = () =>
|
||||
page.mouse.click(700, 250).then(() => page.waitForTimeout(100))
|
||||
const emptySpaceClick = () =>
|
||||
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 page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
// select a plane
|
||||
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter')
|
||||
await u.waitForCmdReceive('set_tool')
|
||||
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByRole('button', { name: 'Line' }).click(),
|
||||
'set_tool'
|
||||
)
|
||||
await page.mouse.click(700, 200)
|
||||
await page.waitForTimeout(700) // wait for animation
|
||||
|
||||
const startXPx = 600
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10),
|
||||
'mouse_click',
|
||||
false
|
||||
)
|
||||
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 u.closeDebugPanel()
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
|
||||
const startAt = '[18.26, -24.63]'
|
||||
const num = '18.43'
|
||||
const num2 = '36.69'
|
||||
const num = 24.11
|
||||
const num2 = '48'
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
@ -520,20 +480,17 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)`)
|
||||
|> line([0, ${num + 0.01}], %)`)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${startAt}, %)
|
||||
|> line([${num}, 0], %)
|
||||
|> line([0, ${num}], %)
|
||||
|> line([0, ${num + 0.01}], %)
|
||||
|> line([-${num2}, 0], %)`)
|
||||
|
||||
// deselect line tool
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByRole('button', { name: 'Line' }).click(),
|
||||
'set_tool'
|
||||
)
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
|
||||
await u.closeDebugPanel()
|
||||
const selectionSequence = async () => {
|
||||
@ -555,79 +512,72 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
// now check clicking works including axis
|
||||
|
||||
// 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')
|
||||
const absYButton = page.getByRole('button', { name: 'ABS Y' })
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
|
||||
await xAxisClick()
|
||||
await page.keyboard.up('Shift')
|
||||
await absYButton.and(page.locator(':not([disabled])')).waitFor()
|
||||
await expect(absYButton).not.toBeDisabled()
|
||||
|
||||
// clear selection by clicking on nothing
|
||||
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
|
||||
await emptySpaceClick()
|
||||
|
||||
// same selection but click the axis first
|
||||
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
|
||||
await xAxisClick()
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await page.keyboard.down('Shift')
|
||||
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false)
|
||||
await topHorzSegmentClick()
|
||||
|
||||
await page.keyboard.up('Shift')
|
||||
await expect(absYButton).not.toBeDisabled()
|
||||
|
||||
// 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
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByText(` |> line([-${num2}, 0], %)`).click(),
|
||||
'select_clear',
|
||||
false
|
||||
)
|
||||
await page.getByText(` |> line([-${num2}, 0], %)`).click()
|
||||
await page.keyboard.down('Shift')
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
|
||||
await xAxisClick()
|
||||
await page.keyboard.up('Shift')
|
||||
await expect(absYButton).not.toBeDisabled()
|
||||
|
||||
// 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
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByText(` |> line([-${num2}, 0], %)`).click(),
|
||||
'select_clear',
|
||||
false
|
||||
)
|
||||
await page.getByText(` |> line([-${num2}, 0], %)`).click()
|
||||
await page.waitForTimeout(300)
|
||||
await page.keyboard.down('Shift')
|
||||
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 expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||
|
||||
// clear selection by clicking on nothing
|
||||
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
|
||||
await emptySpaceClick()
|
||||
}
|
||||
|
||||
await selectionSequence()
|
||||
|
||||
// hovering in fresh sketch worked, lets try exiting and re-entering
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByRole('button', { name: 'Exit Sketch' }).click(),
|
||||
'edit_mode_exit'
|
||||
)
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
// wait for execution done
|
||||
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// select a line
|
||||
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_clear', false)
|
||||
await topHorzSegmentClick()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// enter sketch again
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||
'edit_mode_enter',
|
||||
false
|
||||
)
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await page.waitForTimeout(700) // wait for animation
|
||||
|
||||
// hover again and check it works
|
||||
await selectionSequence()
|
||||
@ -697,6 +647,8 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
|
||||
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await page.keyboard.press('Meta+K')
|
||||
@ -710,10 +662,7 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|
||||
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
|
||||
|
||||
// Click to select face and set distance
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
|
||||
await u.waitForCmdReceive('select_add')
|
||||
await u.closeDebugPanel()
|
||||
await page.getByRole('button', { name: 'Continue' }).click()
|
||||
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
|
||||
await page.keyboard.press('Enter')
|
||||
@ -735,3 +684,166 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|
||||
|> 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { secrets } from './secrets'
|
||||
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { getUtils } from './test-utils'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import fsp from 'fs/promises'
|
||||
@ -40,19 +38,8 @@ test('change camera, show planes', async ({ page, context }) => {
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openAndClearDebugPanel()
|
||||
|
||||
const camCmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
center: { x: 0, y: 0, z: 0 },
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
vantage: { x: 0, y: 85, z: 85 },
|
||||
},
|
||||
}
|
||||
|
||||
await u.sendCustomCmd(camCmd)
|
||||
await u.waitForCmdReceive('default_camera_look_at')
|
||||
const camPos: [number, number, number] = [0, 85, 85]
|
||||
await u.updateCamPosition(camPos)
|
||||
|
||||
// rotate
|
||||
await u.closeDebugPanel()
|
||||
@ -62,13 +49,11 @@ test('change camera, show planes', async ({ page, context }) => {
|
||||
await page.mouse.up({ button: 'right' })
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.waitForCmdReceive('camera_drag_end')
|
||||
await page.waitForTimeout(500)
|
||||
await u.clearCommandLogs()
|
||||
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await expect(page).toHaveScreenshot({
|
||||
@ -77,10 +62,8 @@ test('change camera, show planes', async ({ page, context }) => {
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
await u.sendCustomCmd(camCmd)
|
||||
await u.waitForCmdReceive('default_camera_look_at')
|
||||
await u.updateCamPosition(camPos)
|
||||
|
||||
await u.clearCommandLogs()
|
||||
await u.closeDebugPanel()
|
||||
@ -93,12 +76,10 @@ test('change camera, show planes', async ({ page, context }) => {
|
||||
await page.keyboard.up('Shift')
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.waitForCmdReceive('camera_drag_end')
|
||||
await page.waitForTimeout(300)
|
||||
await u.clearCommandLogs()
|
||||
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await expect(page).toHaveScreenshot({
|
||||
@ -107,10 +88,8 @@ test('change camera, show planes', async ({ page, context }) => {
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
|
||||
await u.sendCustomCmd(camCmd)
|
||||
await u.waitForCmdReceive('default_camera_look_at')
|
||||
await u.updateCamPosition(camPos)
|
||||
|
||||
await u.clearCommandLogs()
|
||||
await u.closeDebugPanel()
|
||||
@ -119,17 +98,15 @@ test('change camera, show planes', async ({ page, context }) => {
|
||||
await page.keyboard.down('Control')
|
||||
await page.mouse.move(700, 400)
|
||||
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.keyboard.up('Control')
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.waitForCmdReceive('camera_drag_end')
|
||||
await page.waitForTimeout(300)
|
||||
await u.clearCommandLogs()
|
||||
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await expect(page).toHaveScreenshot({
|
||||
@ -164,11 +141,11 @@ const part001 = startSketchOn('-XZ')
|
||||
|> xLineTo({ to: totalLen, tag: 'seg03' }, %)
|
||||
|> yLine({ length: -armThick, tag: 'seg01' }, %)
|
||||
|> angledLineThatIntersects({
|
||||
angle: _180,
|
||||
angle: HALF_TURN,
|
||||
offset: -armThick,
|
||||
intersectTag: 'seg04'
|
||||
}, %)
|
||||
|> angledLineToY([segAng('seg04', %) + 180, _0], %)
|
||||
|> angledLineToY([segAng('seg04', %) + 180, ZERO], %)
|
||||
|> angledLineToY({
|
||||
angle: -bottomAng,
|
||||
to: -totalHeightHalf - armThick,
|
||||
@ -177,12 +154,12 @@ const part001 = startSketchOn('-XZ')
|
||||
|> xLineTo(segEndX('seg03', %) + 0, %)
|
||||
|> yLine(-segLen('seg01', %), %)
|
||||
|> angledLineThatIntersects({
|
||||
angle: _180,
|
||||
angle: HALF_TURN,
|
||||
offset: -armThick,
|
||||
intersectTag: 'seg02'
|
||||
}, %)
|
||||
|> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %)
|
||||
|> xLineTo(_0, %)
|
||||
|> xLineTo(ZERO, %)
|
||||
|> close(%)
|
||||
|> extrude(4, %)`
|
||||
)
|
||||
@ -191,7 +168,6 @@ const part001 = startSketchOn('-XZ')
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
await u.waitForDefaultPlanesVisibilityChange()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.waitForCmdReceive('extrude')
|
||||
await page.waitForTimeout(1000)
|
||||
@ -386,3 +362,60 @@ const part001 = startSketchOn('-XZ')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('extrude on each default plane should be stable', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const u = getUtils(page)
|
||||
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|
||||
|> startProfileAt([14.06, 8.88], %)
|
||||
|> line([12.98, -0.15], %)
|
||||
|> line([5.56, 9.89], %)
|
||||
|> line([-11.28, 8.96], %)
|
||||
|> line([-10.81, -7.57], %)
|
||||
|> close(%)
|
||||
|> extrude(20, %)
|
||||
`
|
||||
await context.addInitScript(async (code) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
}, makeCode('XY'))
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// wait for execution done
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
|
||||
await page.getByText('Code').click()
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
await page.getByText('Code').click()
|
||||
|
||||
const runSnapshotsForOtherPlanes = async (plane = 'XY') => {
|
||||
// clear code
|
||||
await u.removeCurrentCode()
|
||||
// add makeCode('XZ')
|
||||
await page.locator('.cm-content').fill(makeCode(plane))
|
||||
// wait for execution done
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
|
||||
await page.getByText('Code').click()
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
await page.getByText('Code').click()
|
||||
}
|
||||
await runSnapshotsForOtherPlanes('-XY')
|
||||
|
||||
await runSnapshotsForOtherPlanes('XZ')
|
||||
await runSnapshotsForOtherPlanes('-XZ')
|
||||
|
||||
await runSnapshotsForOtherPlanes('YZ')
|
||||
await runSnapshotsForOtherPlanes('-YZ')
|
||||
})
|
||||
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 95 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 111 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 46 KiB |
@ -80,10 +80,21 @@ export function getUtils(page: Page) {
|
||||
waitForAuthSkipAppStart: () => waitForPageLoad(page),
|
||||
removeCurrentCode: () => removeCurrentCode(page),
|
||||
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),
|
||||
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr),
|
||||
waitForDefaultPlanesVisibilityChange: () =>
|
||||
waitForDefaultPlanesToBeVisible(page),
|
||||
openDebugPanel: () => openDebugPanel(page),
|
||||
closeDebugPanel: () => closeDebugPanel(page),
|
||||
openAndClearDebugPanel: async () => {
|
||||
|
@ -86,13 +86,11 @@ describe('ZMA (Tauri, Linux)', () => {
|
||||
expect(await homeSection.getText()).toContain('project-000')
|
||||
})
|
||||
|
||||
it('opens the new file and expects an error on Linux', async () => {
|
||||
it('opens the new file and expects a loading stream', async () => {
|
||||
const projectLink = await $('[data-testid="project-link"]')
|
||||
await click(projectLink)
|
||||
const error = await $('h3')
|
||||
expect(await error.getText()).toContain(
|
||||
"Can't find variable: RTCPeerConnection"
|
||||
)
|
||||
const loadingText = await $('[data-testid="loading-stream"]')
|
||||
expect(await loadingText.getText()).toContain('Loading stream...')
|
||||
await browser.execute('window.location.href = "tauri://localhost/home"')
|
||||
})
|
||||
|
||||
|
16
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.10.2",
|
||||
@ -10,7 +10,7 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "^0.0.46",
|
||||
"@kittycad/lib": "^0.0.53",
|
||||
"@lezer/javascript": "^1.4.9",
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
@ -21,6 +21,7 @@
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"@ts-stack/markdown": "^1.5.0",
|
||||
"@tweenjs/tween.js": "^23.1.1",
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/react": "^18.2.41",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
@ -45,6 +46,7 @@
|
||||
"sketch-helpers": "^0.0.4",
|
||||
"swr": "^2.2.2",
|
||||
"tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1",
|
||||
"three": "^0.160.0",
|
||||
"toml": "^3.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2",
|
||||
@ -82,7 +84,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\"",
|
||||
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||
"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": {
|
||||
"trailingComma": "es5",
|
||||
@ -113,6 +117,7 @@
|
||||
"@types/pixelmatch": "^5.2.6",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/react-modal": "^3.16.3",
|
||||
"@types/three": "^0.160.0",
|
||||
"@types/uuid": "^9.0.4",
|
||||
"@types/wait-on": "^5.3.4",
|
||||
"@types/wicg-file-system-access": "^2020.9.6",
|
||||
@ -124,21 +129,26 @@
|
||||
"@wdio/local-runner": "^8.24.3",
|
||||
"@wdio/mocha-framework": "^8.24.3",
|
||||
"@wdio/spec-reporter": "^8.24.2",
|
||||
"@xstate/cli": "^0.5.17",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
"happy-dom": "^10.8.0",
|
||||
"husky": "^8.0.3",
|
||||
"patch-package": "^8.0.0",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.8.0",
|
||||
"setimmediate": "^1.0.5",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"vite": "^4.5.2",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-package-version": "^1.1.0",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest-webgl-canvas-mock": "^1.1.0",
|
||||
"wait-on": "^7.2.0",
|
||||
"yarn": "^1.22.19"
|
||||
}
|
||||
|
138
patches/three+0.160.0.patch
Normal 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 ) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
@ -78,5 +78,4 @@ export default defineConfig({
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
|
||||
})
|
26
public/announce_release.py
Normal 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
After Width: | Height: | Size: 11 KiB |
104
src-tauri/Cargo.lock
generated
@ -67,9 +67,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.75"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
|
||||
|
||||
[[package]]
|
||||
name = "app"
|
||||
@ -95,7 +95,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -542,7 +542,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -552,7 +552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -582,7 +582,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -593,7 +593,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -899,7 +899,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1664,9 +1664,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.2.42"
|
||||
version = "0.2.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aa554d86b6dbbd976a659c912ae25ce817b4378eb12a5684907e263410f0a7b"
|
||||
checksum = "a086e1a1bbddb3b38959c0f0ce6de6b3a3b7566e38e0b7d5fb101e91911beed4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -2215,7 +2215,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2368,7 +2368,7 @@ dependencies = [
|
||||
"regex",
|
||||
"regex-syntax 0.7.5",
|
||||
"structmeta",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2487,7 +2487,7 @@ dependencies = [
|
||||
"phf_shared 0.11.2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2555,7 +2555,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2657,9 +2657,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.67"
|
||||
version = "1.0.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
|
||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@ -2675,9 +2675,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.33"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
|
||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@ -3235,9 +3235,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.193"
|
||||
version = "1.0.196"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
|
||||
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@ -3253,13 +3253,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.193"
|
||||
version = "1.0.196"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
|
||||
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3275,9 +3275,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.108"
|
||||
version = "1.0.113"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
|
||||
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
|
||||
dependencies = [
|
||||
"itoa 1.0.6",
|
||||
"ryu",
|
||||
@ -3302,7 +3302,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3352,7 +3352,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3556,7 +3556,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"structmeta-derive",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3567,7 +3567,7 @@ checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3605,9 +3605,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.33"
|
||||
version = "2.0.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668"
|
||||
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3760,9 +3760,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "1.5.3"
|
||||
version = "1.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32d563b672acde8d0cc4c1b1f5b855976923f67e8d6fe1eba51df0211e197be2"
|
||||
checksum = "fd27c04b9543776a972c86ccf70660b517ecabbeced9fb58d8b961a13ad129af"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -3812,9 +3812,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "defbfc551bd38ab997e5f8e458f87396d2559d05ce32095076ad6c30f7fc5f9c"
|
||||
checksum = "e9914a4715e0b75d9f387a285c7e26b5bbfeb1249ad9f842675a82481565c532"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@ -3831,9 +3831,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "1.4.1"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b3475e55acec0b4a50fb96435f19631fb58cbcd31923e1a213de5c382536bbb"
|
||||
checksum = "a1554c5857f65dbc377cefb6b97c8ac77b1cb2a90d30d3448114d5d6b48a77fc"
|
||||
dependencies = [
|
||||
"base64 0.21.2",
|
||||
"brotli",
|
||||
@ -3857,9 +3857,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "1.4.2"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acea6445eececebd72ed7720cfcca46eee3b5bad8eb408be8f7ef2e3f7411500"
|
||||
checksum = "277abf361a3a6993ec16bcbb179de0d6518009b851090a01adfea12ac89fa875"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
@ -3872,7 +3872,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs-extra"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#537053d3171a7374a1a86fed422523e7b45a4fb8"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#01211ff0759d578e0e9ac8c98c31fdf09077eb34"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@ -3883,9 +3883,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "0.14.1"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07f8e9e53e00e9f41212c115749e87d5cd2a9eebccafca77a19722eeecd56d43"
|
||||
checksum = "cf2d0652aa2891ff3e9caa2401405257ea29ab8372cce01f186a5825f1bd0e76"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
@ -3904,9 +3904,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "0.14.2"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "803a01101bc611ba03e13329951a1bde44287a54234189b9024b78619c1bc206"
|
||||
checksum = "6cae61fbc731f690a4899681c9052dde6d05b159b44563ace8186fc1bfb7d158"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"gtk",
|
||||
@ -3924,9 +3924,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "1.5.1"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a52165bb340e6f6a75f1f5eeeab1bb49f861c12abe3a176067d53642b5454986"
|
||||
checksum = "ece74810b1d3d44f29f732a7ae09a63183d63949bbdd59c61f8ed2a1b70150db"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"ctor",
|
||||
@ -4009,7 +4009,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4051,9 +4051,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.34.0"
|
||||
version = "1.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
|
||||
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@ -4193,7 +4193,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4443,7 +4443,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -4477,7 +4477,7 @@ checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.33",
|
||||
"syn 2.0.48",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
@ -12,17 +12,17 @@ rust-version = "1.60"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.5.0", features = [] }
|
||||
tauri-build = { version = "1.5.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
kittycad = "0.2.42"
|
||||
kittycad = "0.2.53"
|
||||
oauth2 = "4.4.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tauri = { version = "1.5.3", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] }
|
||||
tauri = { version = "1.5.4", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] }
|
||||
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tokio = { version = "1.34.0", features = ["time"] }
|
||||
tokio = { version = "1.36.0", features = ["time"] }
|
||||
toml = "0.8.2"
|
||||
|
||||
[features]
|
||||
|
@ -7,6 +7,7 @@ use std::io::Read;
|
||||
|
||||
use anyhow::Result;
|
||||
use oauth2::TokenResponse;
|
||||
use std::process::Command;
|
||||
use tauri::{InvokeError, Manager};
|
||||
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "zoo-modeling-app",
|
||||
"version": "0.14.0"
|
||||
"version": "0.15.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
@ -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} />
|
||||
}
|
84
src/App.tsx
@ -19,21 +19,24 @@ import {
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { getNormalisedCoordinates } from './lib/utils'
|
||||
import { useLoaderData } from 'react-router-dom'
|
||||
import { IndexLoaderData } from './Router'
|
||||
import { useLoaderData, useNavigate } from 'react-router-dom'
|
||||
import { type IndexLoaderData } from 'lib/types'
|
||||
import { paths } from 'lib/paths'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { onboardingPaths } from 'routes/Onboarding'
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { CodeMenu } from 'components/CodeMenu'
|
||||
import { TextEditor } from 'components/TextEditor'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||
import { engineCommandManager } from './lang/std/engineConnection'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
|
||||
export function App() {
|
||||
const { project, file } = useLoaderData() as IndexLoaderData
|
||||
const navigate = useNavigate()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
|
||||
useHotKeyListener()
|
||||
const {
|
||||
@ -51,8 +54,7 @@ export function App() {
|
||||
}))
|
||||
|
||||
const { settings } = useGlobalStateContext()
|
||||
const { showDebugPanel, onboardingStatus, cameraControls, theme } =
|
||||
settings?.context || {}
|
||||
const { showDebugPanel, onboardingStatus, theme } = settings?.context || {}
|
||||
const { state, send } = useModelingContext()
|
||||
|
||||
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||
@ -71,6 +73,16 @@ export function App() {
|
||||
useHotkeys('shift + e', () => togglePane('kclErrors'))
|
||||
useHotkeys('shift + d', () => togglePane('debug'))
|
||||
useHotkeys('esc', () => send('Cancel'))
|
||||
useHotkeys('backspace', (e) => {
|
||||
e.preventDefault()
|
||||
})
|
||||
useHotkeys(
|
||||
isTauri() ? 'mod + ,' : 'shift + mod + ,',
|
||||
() => navigate(filePath + paths.SETTINGS),
|
||||
{
|
||||
splitKey: '|',
|
||||
}
|
||||
)
|
||||
|
||||
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
|
||||
(p) => p === onboardingStatus
|
||||
@ -84,9 +96,11 @@ export function App() {
|
||||
|
||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||
engineCommandManager.sendSceneCommand(message)
|
||||
}, 16)
|
||||
}, 1000 / 15)
|
||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
e.nativeEvent.preventDefault()
|
||||
if (state.matches('Sketch')) {
|
||||
return
|
||||
}
|
||||
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX: e.clientX,
|
||||
@ -97,58 +111,11 @@ export function App() {
|
||||
|
||||
const newCmdId = uuidv4()
|
||||
if (buttonDownInStream === undefined) {
|
||||
if (state.matches('Sketch.Line Tool')) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: newCmdId,
|
||||
cmd: {
|
||||
type: 'mouse_move',
|
||||
window: { x, y },
|
||||
},
|
||||
})
|
||||
} else {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'highlight_set_entity',
|
||||
selected_at_window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (state.matches('Sketch.Move Tool')) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: newCmdId,
|
||||
cmd: {
|
||||
type: 'handle_mouse_drag_move',
|
||||
window: { x, y },
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
||||
let interaction: CameraDragInteractionType_type
|
||||
|
||||
const eWithButton = { ...e, button: buttonDownInStream }
|
||||
|
||||
if (interactionGuards.pan.callback(eWithButton)) {
|
||||
interaction = 'pan'
|
||||
} else if (interactionGuards.rotate.callback(eWithButton)) {
|
||||
interaction = 'rotate'
|
||||
} else if (interactionGuards.zoom.dragCallback(eWithButton)) {
|
||||
interaction = 'zoom'
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_move',
|
||||
interaction,
|
||||
window: { x, y },
|
||||
type: 'highlight_set_entity',
|
||||
selected_at_window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
@ -238,6 +205,7 @@ export function App() {
|
||||
open={openPanes.includes('debug')}
|
||||
/>
|
||||
)}
|
||||
{/* <CamToggle /> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -14,10 +14,7 @@ import {
|
||||
import { useEffect } from 'react'
|
||||
import { ErrorPage } from './components/ErrorPage'
|
||||
import { Settings } from './routes/Settings'
|
||||
import Onboarding, {
|
||||
onboardingRoutes,
|
||||
onboardingPaths,
|
||||
} from './routes/Onboarding'
|
||||
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
|
||||
import SignIn from './routes/SignIn'
|
||||
import { Auth } from './Auth'
|
||||
import { isTauri } from './lib/isTauri'
|
||||
@ -29,7 +26,7 @@ import {
|
||||
isProjectDirectory,
|
||||
PROJECT_ENTRYPOINT,
|
||||
} from './lib/tauriFS'
|
||||
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
|
||||
import { metadata } from 'tauri-plugin-fs-extra-api'
|
||||
import DownloadAppBanner from './components/DownloadAppBanner'
|
||||
import { WasmErrBanner } from './components/WasmErrBanner'
|
||||
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||
@ -42,9 +39,12 @@ import CommandBarProvider from 'components/CommandBar/CommandBar'
|
||||
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||
import { KclContextProvider, kclManager } from 'lang/KclSinglton'
|
||||
import { KclContextProvider, kclManager } from 'lang/KclSingleton'
|
||||
import FileMachineProvider from 'components/FileMachineProvider'
|
||||
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) {
|
||||
Sentry.init({
|
||||
@ -78,43 +78,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 type IndexLoaderData = {
|
||||
code: string | null
|
||||
project?: ProjectWithEntryPointMetadata
|
||||
file?: FileEntry
|
||||
}
|
||||
|
||||
export type ProjectWithEntryPointMetadata = FileEntry & {
|
||||
entrypointMetadata: Metadata
|
||||
}
|
||||
export type HomeLoaderData = {
|
||||
projects: ProjectWithEntryPointMetadata[]
|
||||
newDefaultDirectory?: string
|
||||
}
|
||||
|
||||
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
|
||||
|
||||
const addGlobalContextToElements = (
|
||||
@ -146,18 +111,18 @@ const router = createBrowserRouter(
|
||||
{
|
||||
path: paths.FILE + '/:id',
|
||||
element: (
|
||||
<Auth>
|
||||
<FileMachineProvider>
|
||||
<KclContextProvider>
|
||||
<KclContextProvider>
|
||||
<Auth>
|
||||
<FileMachineProvider>
|
||||
<ModelingMachineProvider>
|
||||
<Outlet />
|
||||
<App />
|
||||
</ModelingMachineProvider>
|
||||
<WasmErrBanner />
|
||||
</KclContextProvider>
|
||||
</FileMachineProvider>
|
||||
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
||||
</Auth>
|
||||
</FileMachineProvider>
|
||||
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
||||
</Auth>
|
||||
</KclContextProvider>
|
||||
),
|
||||
id: paths.FILE,
|
||||
loader: async ({
|
||||
@ -210,6 +175,10 @@ const router = createBrowserRouter(
|
||||
const children = await readDir(projectPath, { recursive: true })
|
||||
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 {
|
||||
code,
|
||||
project: {
|
||||
@ -249,7 +218,7 @@ const router = createBrowserRouter(
|
||||
<Home />
|
||||
</Auth>
|
||||
),
|
||||
loader: async () => {
|
||||
loader: async (): Promise<HomeLoaderData | Response> => {
|
||||
if (!isTauri()) {
|
||||
return redirect(paths.FILE + '/' + BROWSER_FILE_NAME)
|
||||
}
|
||||
|
@ -89,44 +89,48 @@ export const Toolbar = () => {
|
||||
</li>
|
||||
)}
|
||||
{state.matches('Sketch') && !state.matches('idle') && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
state.matches('Sketch.Line Tool')
|
||||
? send('CancelSketch')
|
||||
: send('Equip tool')
|
||||
}
|
||||
aria-pressed={state.matches('Sketch.Line Tool')}
|
||||
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
||||
icon={{
|
||||
icon: 'line',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
Line
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
{state.matches('Sketch') && (
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
state.matches('Sketch.Move Tool')
|
||||
? send('CancelSketch')
|
||||
: send('Equip move tool')
|
||||
}
|
||||
aria-pressed={state.matches('Sketch.Move Tool')}
|
||||
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
||||
icon={{
|
||||
icon: 'move',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
Move
|
||||
</ActionButton>
|
||||
</li>
|
||||
<>
|
||||
<li className="contents" key="line-button">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
state?.matches('Sketch.Line 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"
|
||||
icon={{
|
||||
icon: 'line',
|
||||
bgClassName,
|
||||
}}
|
||||
>
|
||||
Line
|
||||
</ActionButton>
|
||||
</li>
|
||||
<li className="contents" key="tangential-arc-button">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() =>
|
||||
state.matches('Sketch.Tangential arc to')
|
||||
? send('CancelSketch')
|
||||
: send('Equip tangential arc to')
|
||||
}
|
||||
aria-pressed={state.matches('Sketch.Tangential arc to')}
|
||||
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
||||
icon={{
|
||||
icon: 'line',
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={
|
||||
!state.can('Equip tangential arc to') &&
|
||||
!state.matches('Sketch.Tangential arc to')
|
||||
}
|
||||
>
|
||||
Tangential Arc
|
||||
</ActionButton>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
{state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents
|
||||
@ -151,7 +155,7 @@ export const Toolbar = () => {
|
||||
return 0
|
||||
})
|
||||
.map((eventName) => (
|
||||
<li className="contents">
|
||||
<li className="contents" key={eventName}>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
className="text-sm"
|
||||
|
252
src/clientSideScene/ClientSideSceneComp.tsx
Normal file
@ -0,0 +1,252 @@
|
||||
import { useRef, useEffect, useState } from 'react'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { useStore } from 'useStore'
|
||||
import {
|
||||
DEBUG_SHOW_BOTH_SCENES,
|
||||
ReactCameraProperties,
|
||||
sceneInfra,
|
||||
} from './sceneInfra'
|
||||
import { throttle } from 'lib/utils'
|
||||
|
||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||
const [isCamMoving, setIsCamMoving] = useState(false)
|
||||
const [isTween, setIsTween] = useState(false)
|
||||
|
||||
const { state } = useModelingContext()
|
||||
|
||||
useEffect(() => {
|
||||
sceneInfra.setIsCamMovingCallback((isMoving, isTween) => {
|
||||
setIsCamMoving(isMoving)
|
||||
setIsTween(isTween)
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (DEBUG_SHOW_BOTH_SCENES || !isCamMoving)
|
||||
return { hideClient: false, hideServer: false }
|
||||
let hideServer = state.matches('Sketch')
|
||||
if (isTween) {
|
||||
hideServer = false
|
||||
}
|
||||
|
||||
return { hideClient: !hideServer, hideServer }
|
||||
}
|
||||
|
||||
export const ClientSideScene = ({
|
||||
cameraControls,
|
||||
}: {
|
||||
cameraControls: ReturnType<
|
||||
typeof useGlobalStateContext
|
||||
>['settings']['context']['cameraControls']
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLDivElement>(null)
|
||||
const { state, send } = useModelingContext()
|
||||
const { hideClient, hideServer } = useShouldHideScene()
|
||||
const { setHighlightRange } = useStore((s) => ({
|
||||
setHighlightRange: s.setHighlightRange,
|
||||
highlightRange: s.highlightRange,
|
||||
}))
|
||||
|
||||
// Listen for changes to the camera controls setting
|
||||
// and update the client-side scene's controls accordingly.
|
||||
useEffect(() => {
|
||||
sceneInfra.setInteractionGuards(cameraMouseDragGuards[cameraControls])
|
||||
}, [cameraControls])
|
||||
useEffect(() => {
|
||||
sceneInfra.updateOtherSelectionColors(
|
||||
state?.context?.selectionRanges?.otherSelections || []
|
||||
)
|
||||
}, [state?.context?.selectionRanges?.otherSelections])
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) return
|
||||
const canvas = canvasRef.current
|
||||
canvas.appendChild(sceneInfra.renderer.domElement)
|
||||
sceneInfra.animate()
|
||||
sceneInfra.setHighlightCallback(setHighlightRange)
|
||||
canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false)
|
||||
canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false)
|
||||
canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false)
|
||||
sceneInfra.setSend(send)
|
||||
return () => {
|
||||
canvas?.removeEventListener('mousemove', sceneInfra.onMouseMove)
|
||||
canvas?.removeEventListener('mousedown', sceneInfra.onMouseDown)
|
||||
canvas?.removeEventListener('mouseup', sceneInfra.onMouseUp)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={canvasRef}
|
||||
className={`absolute inset-0 h-full w-full transition-all duration-300 ${
|
||||
hideClient ? 'opacity-0' : 'opacity-100'
|
||||
} ${hideServer ? 'bg-black' : ''} ${
|
||||
!hideClient && !hideServer && state.matches('Sketch')
|
||||
? 'bg-black/80'
|
||||
: ''
|
||||
}`}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
|
||||
const throttled = throttle((a: ReactCameraProperties) => {
|
||||
if (a.type === 'perspective' && a.fov) {
|
||||
sceneInfra.dollyZoom(a.fov)
|
||||
}
|
||||
}, 1000 / 15)
|
||||
|
||||
export const CamDebugSettings = () => {
|
||||
const [camSettings, setCamSettings] = useState<ReactCameraProperties>({
|
||||
type: 'perspective',
|
||||
fov: 12,
|
||||
position: [0, 0, 0],
|
||||
quaternion: [0, 0, 0, 1],
|
||||
})
|
||||
const [fov, setFov] = useState(12)
|
||||
|
||||
useEffect(() => {
|
||||
sceneInfra.setReactCameraPropertiesCallback(setCamSettings)
|
||||
}, [sceneInfra])
|
||||
useEffect(() => {
|
||||
if (camSettings.type === 'perspective' && camSettings.fov) {
|
||||
setFov(camSettings.fov)
|
||||
}
|
||||
}, [(camSettings as any)?.fov])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>cam settings</h3>
|
||||
perspective cam
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={camSettings.type === 'perspective'}
|
||||
onChange={(e) => {
|
||||
if (camSettings.type === 'perspective') {
|
||||
sceneInfra.useOrthographicCamera()
|
||||
} else {
|
||||
sceneInfra.usePerspectiveCamera()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{camSettings.type === 'perspective' && (
|
||||
<input
|
||||
type="range"
|
||||
min="4"
|
||||
max="90"
|
||||
step={0.5}
|
||||
value={fov}
|
||||
onChange={(e) => {
|
||||
setFov(parseFloat(e.target.value))
|
||||
|
||||
throttled({
|
||||
...camSettings,
|
||||
fov: parseFloat(e.target.value),
|
||||
})
|
||||
}}
|
||||
className="w-full cursor-pointer pointer-events-auto"
|
||||
/>
|
||||
)}
|
||||
{camSettings.type === 'perspective' && (
|
||||
<div>
|
||||
<span>fov</span>
|
||||
<input
|
||||
type="number"
|
||||
value={camSettings.fov}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.setCam({
|
||||
...camSettings,
|
||||
fov: parseFloat(e.target.value),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{camSettings.type === 'orthographic' && (
|
||||
<>
|
||||
<div>
|
||||
<span>fov</span>
|
||||
<input
|
||||
type="number"
|
||||
value={camSettings.zoom}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.setCam({
|
||||
...camSettings,
|
||||
zoom: parseFloat(e.target.value),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
Position
|
||||
<ul className="flex">
|
||||
<li>
|
||||
<span className="pl-2 pr-1">x:</span>
|
||||
<input
|
||||
type="number"
|
||||
step={5}
|
||||
data-testid="cam-x-position"
|
||||
value={camSettings.position[0]}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.setCam({
|
||||
...camSettings,
|
||||
position: [
|
||||
parseFloat(e.target.value),
|
||||
camSettings.position[1],
|
||||
camSettings.position[2],
|
||||
],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<span className="pl-2 pr-1">y:</span>
|
||||
<input
|
||||
type="number"
|
||||
step={5}
|
||||
data-testid="cam-y-position"
|
||||
value={camSettings.position[1]}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.setCam({
|
||||
...camSettings,
|
||||
position: [
|
||||
camSettings.position[0],
|
||||
parseFloat(e.target.value),
|
||||
camSettings.position[2],
|
||||
],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<span className="pl-2 pr-1">z:</span>
|
||||
<input
|
||||
type="number"
|
||||
step={5}
|
||||
data-testid="cam-z-position"
|
||||
value={camSettings.position[2]}
|
||||
className="text-black w-16"
|
||||
onChange={(e) => {
|
||||
sceneInfra.setCam({
|
||||
...camSettings,
|
||||
position: [
|
||||
camSettings.position[0],
|
||||
camSettings.position[1],
|
||||
parseFloat(e.target.value),
|
||||
],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
33
src/clientSideScene/helpers.ts
Normal 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
|
1026
src/clientSideScene/sceneEntities.ts
Normal file
28
src/clientSideScene/sceneInfra.test.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Quaternion } from 'three'
|
||||
import { isQuaternionVertical } from './sceneInfra'
|
||||
|
||||
describe('isQuaternionVertical', () => {
|
||||
it('should identify vertical quaternions', () => {
|
||||
const verticalQuaternions = [
|
||||
new Quaternion(1, 0, 0, 0).normalize(), // bottom
|
||||
new Quaternion(-0.7, 0.7, 0, 0).normalize(), // bottom 2
|
||||
new Quaternion(0, 1, 0, 0).normalize(), // bottom 3
|
||||
new Quaternion(0, 0, 0, 1).normalize(), // look from top
|
||||
]
|
||||
verticalQuaternions.forEach((quaternion) => {
|
||||
expect(isQuaternionVertical(quaternion)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('should identify non-vertical quaternions', () => {
|
||||
const nonVerticalQuaternions = [
|
||||
new Quaternion(0.7, 0, 0, 0.7).normalize(), // front
|
||||
new Quaternion(0, 0.7, 0.7, 0).normalize(), // back
|
||||
new Quaternion(-0.5, 0.5, 0.5, -0.5).normalize(), // left side
|
||||
new Quaternion(0.5, 0.5, 0.5, 0.5).normalize(), // right side
|
||||
]
|
||||
nonVerticalQuaternions.forEach((quaternion) => {
|
||||
expect(isQuaternionVertical(quaternion)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
1130
src/clientSideScene/sceneInfra.ts
Normal file
358
src/clientSideScene/segments.ts
Normal 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 './sceneEntities'
|
||||
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
||||
import { ARROWHEAD } from './sceneInfra'
|
||||
|
||||
export function straightSegment({
|
||||
from,
|
||||
to,
|
||||
id,
|
||||
pathToNode,
|
||||
isDraftSegment,
|
||||
scale = 1,
|
||||
}: {
|
||||
from: Coords2d
|
||||
to: Coords2d
|
||||
id: string
|
||||
pathToNode: PathToNode
|
||||
isDraftSegment?: boolean
|
||||
scale?: number
|
||||
}): Group {
|
||||
const group = new Group()
|
||||
|
||||
const shape = new Shape()
|
||||
shape.moveTo(0, -0.08 * scale)
|
||||
shape.lineTo(0, 0.08 * scale) // The width of the line
|
||||
|
||||
let geometry
|
||||
if (isDraftSegment) {
|
||||
geometry = dashedStraight(from, to, shape, scale)
|
||||
} else {
|
||||
const line = new LineCurve3(
|
||||
new Vector3(from[0], from[1], 0),
|
||||
new Vector3(to[0], to[1], 0)
|
||||
)
|
||||
|
||||
geometry = new ExtrudeGeometry(shape, {
|
||||
steps: 2,
|
||||
bevelEnabled: false,
|
||||
extrudePath: line,
|
||||
})
|
||||
}
|
||||
|
||||
const body = new MeshBasicMaterial({ color: 0xffffff })
|
||||
const mesh = new Mesh(geometry, body)
|
||||
mesh.userData.type = isDraftSegment
|
||||
? STRAIGHT_SEGMENT_DASH
|
||||
: STRAIGHT_SEGMENT_BODY
|
||||
mesh.name = STRAIGHT_SEGMENT_BODY
|
||||
|
||||
group.userData = {
|
||||
type: STRAIGHT_SEGMENT,
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
pathToNode,
|
||||
isSelected: false,
|
||||
}
|
||||
|
||||
const arrowGroup = createArrowhead(scale)
|
||||
arrowGroup.position.set(to[0], to[1], 0)
|
||||
const dir = new Vector3()
|
||||
.subVectors(new Vector3(to[0], to[1], 0), new Vector3(from[0], from[1], 0))
|
||||
.normalize()
|
||||
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
||||
|
||||
group.add(mesh, arrowGroup)
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
function createArrowhead(scale = 1): Group {
|
||||
const arrowMaterial = new MeshBasicMaterial({ color: 0xffffff })
|
||||
const arrowheadMesh = new Mesh(new ConeGeometry(0.31, 1.5, 12), arrowMaterial)
|
||||
arrowheadMesh.position.set(0, -0.6, 0)
|
||||
const sphereMesh = new Mesh(new SphereGeometry(0.27, 12, 12), arrowMaterial)
|
||||
|
||||
const arrowGroup = new Group()
|
||||
arrowGroup.userData.type = ARROWHEAD
|
||||
arrowGroup.name = ARROWHEAD
|
||||
arrowGroup.add(arrowheadMesh, sphereMesh)
|
||||
arrowGroup.lookAt(new Vector3(0, 1, 0))
|
||||
arrowGroup.scale.set(scale, scale, scale)
|
||||
return arrowGroup
|
||||
}
|
||||
|
||||
export function tangentialArcToSegment({
|
||||
prevSegment,
|
||||
from,
|
||||
to,
|
||||
id,
|
||||
pathToNode,
|
||||
isDraftSegment,
|
||||
scale = 1,
|
||||
}: {
|
||||
prevSegment: SketchGroup['value'][number]
|
||||
from: Coords2d
|
||||
to: Coords2d
|
||||
id: string
|
||||
pathToNode: PathToNode
|
||||
isDraftSegment?: boolean
|
||||
scale?: number
|
||||
}): Group {
|
||||
const group = new Group()
|
||||
|
||||
const previousPoint =
|
||||
prevSegment?.type === 'TangentialArcTo'
|
||||
? getTangentPointFromPreviousArc(
|
||||
prevSegment.center,
|
||||
prevSegment.ccw,
|
||||
prevSegment.to
|
||||
)
|
||||
: prevSegment.from
|
||||
|
||||
const { center, radius, startAngle, endAngle, ccw } = getTangentialArcToInfo({
|
||||
arcStartPoint: from,
|
||||
arcEndPoint: to,
|
||||
tanPreviousPoint: previousPoint,
|
||||
obtuse: true,
|
||||
})
|
||||
|
||||
const geometry = createArcGeometry({
|
||||
center,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
ccw,
|
||||
isDashed: isDraftSegment,
|
||||
scale,
|
||||
})
|
||||
|
||||
const body = new MeshBasicMaterial({ color: 0xffffff })
|
||||
const mesh = new Mesh(geometry, body)
|
||||
mesh.userData.type = isDraftSegment
|
||||
? TANGENTIAL_ARC_TO__SEGMENT_DASH
|
||||
: TANGENTIAL_ARC_TO_SEGMENT_BODY
|
||||
|
||||
group.userData = {
|
||||
type: TANGENTIAL_ARC_TO_SEGMENT,
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
prevSegment,
|
||||
pathToNode,
|
||||
isSelected: false,
|
||||
}
|
||||
|
||||
const arrowGroup = createArrowhead(scale)
|
||||
arrowGroup.position.set(to[0], to[1], 0)
|
||||
const arrowheadAngle = endAngle + (Math.PI / 2) * (ccw ? 1 : -1)
|
||||
arrowGroup.quaternion.setFromUnitVectors(
|
||||
new Vector3(0, 1, 0),
|
||||
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
|
||||
)
|
||||
|
||||
group.add(mesh, arrowGroup)
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
export function createArcGeometry({
|
||||
center,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
ccw,
|
||||
isDashed = false,
|
||||
scale = 1,
|
||||
}: {
|
||||
center: Coords2d
|
||||
radius: number
|
||||
startAngle: number
|
||||
endAngle: number
|
||||
ccw: boolean
|
||||
isDashed?: boolean
|
||||
scale?: number
|
||||
}): BufferGeometry {
|
||||
const dashSize = 1.2 * scale
|
||||
const gapSize = 1.2 * scale
|
||||
const arcStart = new EllipseCurve(
|
||||
center[0],
|
||||
center[1],
|
||||
radius,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
!ccw,
|
||||
0
|
||||
)
|
||||
const arcEnd = new EllipseCurve(
|
||||
center[0],
|
||||
center[1],
|
||||
radius,
|
||||
radius,
|
||||
endAngle,
|
||||
startAngle,
|
||||
ccw,
|
||||
0
|
||||
)
|
||||
const shape = new Shape()
|
||||
shape.moveTo(0, -0.08 * scale)
|
||||
shape.lineTo(0, 0.08 * scale) // The width of the line
|
||||
|
||||
if (!isDashed) {
|
||||
const points = arcStart.getPoints(50)
|
||||
const path = new CurvePath<Vector3>()
|
||||
path.add(new CatmullRomCurve3(points.map((p) => new Vector3(p.x, p.y, 0))))
|
||||
|
||||
return new ExtrudeGeometry(shape, {
|
||||
steps: 100,
|
||||
bevelEnabled: false,
|
||||
extrudePath: path,
|
||||
})
|
||||
}
|
||||
|
||||
const length = arcStart.getLength()
|
||||
const totalDashes = length / (dashSize + gapSize) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place
|
||||
const dashesAtEachEnd = Math.min(100, totalDashes / 2) // Assuming we want 50 dashes total, 25 at each end
|
||||
|
||||
const dashGeometries = []
|
||||
|
||||
// Function to create a dash at a specific t value (0 to 1 along the curve)
|
||||
const createDashAt = (t: number, curve: EllipseCurve) => {
|
||||
const startVec = curve.getPoint(t)
|
||||
const endVec = curve.getPoint(Math.min(0.5, t + dashSize / length))
|
||||
const midVec = curve.getPoint(Math.min(0.5, t + dashSize / length / 2))
|
||||
const dashCurve = new CurvePath<Vector3>()
|
||||
dashCurve.add(
|
||||
new CatmullRomCurve3([
|
||||
new Vector3(startVec.x, startVec.y, 0),
|
||||
new Vector3(midVec.x, midVec.y, 0),
|
||||
new Vector3(endVec.x, endVec.y, 0),
|
||||
])
|
||||
)
|
||||
return new ExtrudeGeometry(shape, {
|
||||
steps: 3,
|
||||
bevelEnabled: false,
|
||||
extrudePath: dashCurve,
|
||||
})
|
||||
}
|
||||
|
||||
// Create dashes at the start of the arc
|
||||
for (let i = 0; i < dashesAtEachEnd; i++) {
|
||||
const t = i / totalDashes
|
||||
dashGeometries.push(createDashAt(t, arcStart))
|
||||
dashGeometries.push(createDashAt(t, arcEnd))
|
||||
}
|
||||
|
||||
// fill in the remaining arc
|
||||
const remainingArcLength = length - dashesAtEachEnd * 2 * (dashSize + gapSize)
|
||||
if (remainingArcLength > 0) {
|
||||
const remainingArcStartT = dashesAtEachEnd / totalDashes
|
||||
const remainingArcEndT = 1 - remainingArcStartT
|
||||
const centerVec = new Vector2(center[0], center[1])
|
||||
const remainingArcStartVec = arcStart.getPoint(remainingArcStartT)
|
||||
const remainingArcEndVec = arcStart.getPoint(remainingArcEndT)
|
||||
const remainingArcCurve = new EllipseCurve(
|
||||
arcStart.aX,
|
||||
arcStart.aY,
|
||||
arcStart.xRadius,
|
||||
arcStart.yRadius,
|
||||
new Vector2().subVectors(centerVec, remainingArcStartVec).angle() +
|
||||
Math.PI,
|
||||
new Vector2().subVectors(centerVec, remainingArcEndVec).angle() + Math.PI,
|
||||
!ccw
|
||||
)
|
||||
const remainingArcPoints = remainingArcCurve.getPoints(50)
|
||||
const remainingArcPath = new CurvePath<Vector3>()
|
||||
remainingArcPath.add(
|
||||
new CatmullRomCurve3(
|
||||
remainingArcPoints.map((p) => new Vector3(p.x, p.y, 0))
|
||||
)
|
||||
)
|
||||
const remainingArcGeometry = new ExtrudeGeometry(shape, {
|
||||
steps: 50,
|
||||
bevelEnabled: false,
|
||||
extrudePath: remainingArcPath,
|
||||
})
|
||||
dashGeometries.push(remainingArcGeometry)
|
||||
}
|
||||
|
||||
const geo = dashGeometries.length
|
||||
? mergeGeometries(dashGeometries)
|
||||
: new BufferGeometry()
|
||||
geo.userData.type = 'dashed'
|
||||
return geo
|
||||
}
|
||||
|
||||
export function dashedStraight(
|
||||
from: Coords2d,
|
||||
to: Coords2d,
|
||||
shape: Shape,
|
||||
scale = 1
|
||||
): BufferGeometry<NormalBufferAttributes> {
|
||||
const dashSize = 1.2 * scale
|
||||
const gapSize = 1.2 * scale // todo: gabSize is not respected
|
||||
const dashLine = new LineCurve3(
|
||||
new Vector3(from[0], from[1], 0),
|
||||
new Vector3(to[0], to[1], 0)
|
||||
)
|
||||
const length = dashLine.getLength()
|
||||
const numberOfPoints = (length / (dashSize + gapSize)) * 2
|
||||
const startOfLine = new Vector3(from[0], from[1], 0)
|
||||
const endOfLine = new Vector3(to[0], to[1], 0)
|
||||
const dashGeometries = []
|
||||
const dashComponent = (xOrY: number, pointIndex: number) =>
|
||||
((to[xOrY] - from[xOrY]) / numberOfPoints) * pointIndex + from[xOrY]
|
||||
for (let i = 0; i < numberOfPoints; i += 2) {
|
||||
const dashStart = new Vector3(dashComponent(0, i), dashComponent(1, i), 0)
|
||||
let dashEnd = new Vector3(
|
||||
dashComponent(0, i + 1),
|
||||
dashComponent(1, i + 1),
|
||||
0
|
||||
)
|
||||
if (startOfLine.distanceTo(dashEnd) > startOfLine.distanceTo(endOfLine))
|
||||
dashEnd = endOfLine
|
||||
|
||||
if (dashEnd) {
|
||||
const dashCurve = new LineCurve3(dashStart, dashEnd)
|
||||
const dashGeometry = new ExtrudeGeometry(shape, {
|
||||
steps: 1,
|
||||
bevelEnabled: false,
|
||||
extrudePath: dashCurve,
|
||||
})
|
||||
dashGeometries.push(dashGeometry)
|
||||
}
|
||||
}
|
||||
const geo = dashGeometries.length
|
||||
? mergeGeometries(dashGeometries)
|
||||
: new BufferGeometry()
|
||||
geo.userData.type = 'dashed'
|
||||
return geo
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
||||
import React from 'react'
|
||||
import { paths } from '../Router'
|
||||
import { paths } from 'lib/paths'
|
||||
import { Link } from 'react-router-dom'
|
||||
import type { LinkProps } from 'react-router-dom'
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Toolbar } from '../Toolbar'
|
||||
import UserSidebarMenu from './UserSidebarMenu'
|
||||
import { IndexLoaderData } from '../Router'
|
||||
import { type IndexLoaderData } from 'lib/types'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import styles from './AppHeader.module.css'
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useStore } from 'useStore'
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
} from '../lang/modifyAst'
|
||||
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
||||
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { executeAst } from 'useStore'
|
||||
|
||||
@ -148,7 +148,6 @@ export function useCalc({
|
||||
executeAst({
|
||||
ast,
|
||||
engineCommandManager,
|
||||
defaultPlanes: kclManager.defaultPlanes,
|
||||
useFakeExecutor: true,
|
||||
programMemoryOverride: JSON.parse(
|
||||
JSON.stringify(kclManager.programMemory)
|
||||
|
75
src/components/CamToggle.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { sceneInfra } from '../clientSideScene/sceneInfra'
|
||||
import { engineCommandManager } from 'lang/std/engineConnection'
|
||||
import { throttle, isReducedMotion } from 'lib/utils'
|
||||
|
||||
const updateDollyZoom = throttle(
|
||||
(newFov: number) => sceneInfra.dollyZoom(newFov),
|
||||
1000 / 15
|
||||
)
|
||||
|
||||
export const CamToggle = () => {
|
||||
const [isPerspective, setIsPerspective] = useState(true)
|
||||
const [fov, setFov] = useState(40)
|
||||
const [enableRotate, setEnableRotate] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
engineCommandManager.waitForReady.then(async () => {
|
||||
sceneInfra.dollyZoom(fov)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const toggleCamera = () => {
|
||||
if (isPerspective) {
|
||||
isReducedMotion()
|
||||
? sceneInfra.useOrthographicCamera()
|
||||
: sceneInfra.animateToOrthographic()
|
||||
} else {
|
||||
isReducedMotion()
|
||||
? sceneInfra.usePerspectiveCamera()
|
||||
: sceneInfra.animateToPerspective()
|
||||
}
|
||||
setIsPerspective(!isPerspective)
|
||||
}
|
||||
|
||||
const handleFovChange = (newFov: number) => {
|
||||
setFov(newFov)
|
||||
updateDollyZoom(newFov)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute right-14 bottom-3">
|
||||
{isPerspective && (
|
||||
<div className="">
|
||||
<input
|
||||
type="range"
|
||||
min="4"
|
||||
max="90"
|
||||
step={0.5}
|
||||
value={fov}
|
||||
onChange={(e) => handleFovChange(Number(e.target.value))}
|
||||
className="w-full cursor-pointer pointer-events-auto"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<button onClick={toggleCamera} className="">
|
||||
{isPerspective
|
||||
? 'Switch to Orthographic Camera'
|
||||
: 'Switch to Perspective Camera'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (enableRotate) {
|
||||
sceneInfra.controls.enableRotate = false
|
||||
} else {
|
||||
sceneInfra.controls.enableRotate = true
|
||||
}
|
||||
setEnableRotate(!enableRotate)
|
||||
}}
|
||||
className=""
|
||||
>
|
||||
{enableRotate ? 'Disable Rotation' : 'Enable Rotation'}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -9,7 +9,7 @@ import styles from './CodeMenu.module.css'
|
||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||
import { editorShortcutMeta } from './TextEditor'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
|
||||
export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||
@ -77,6 +77,24 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||
</small>
|
||||
</a>
|
||||
</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>
|
||||
</div>
|
||||
</Menu>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { CustomIcon } from '../CustomIcon'
|
||||
import React, { useState } from 'react'
|
||||
import React, { ReactNode, useState } from 'react'
|
||||
import { ActionButton } from '../ActionButton'
|
||||
import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
@ -99,15 +99,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
) : typeof argumentsToSubmit[argName] === 'object' ? (
|
||||
JSON.stringify(argumentsToSubmit[argName])
|
||||
) : (
|
||||
argumentsToSubmit[argName]
|
||||
)
|
||||
) : arg.payload ? (
|
||||
arg.inputType === 'selection' ? (
|
||||
getSelectionTypeDisplayText(arg.payload as Selections)
|
||||
) : typeof arg.payload === 'object' ? (
|
||||
JSON.stringify(arg.payload)
|
||||
) : (
|
||||
arg.payload
|
||||
<em>{argumentsToSubmit[argName] as ReactNode}</em>
|
||||
)
|
||||
) : (
|
||||
<em>{argName}</em>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useKclContext } from 'lang/KclSinglton'
|
||||
import { useKclContext } from 'lang/KclSingleton'
|
||||
import { CommandArgument } from 'lib/commandTypes'
|
||||
import {
|
||||
ResolvedSelectionType,
|
||||
@ -27,7 +27,7 @@ function CommandBarSelectionInput({
|
||||
}) {
|
||||
const { code } = useKclContext()
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||
const selection = useSelector(arg.actor, selectionSelector)
|
||||
const [selectionsByType, setSelectionsByType] = useState<
|
||||
@ -59,8 +59,16 @@ function CommandBarSelectionInput({
|
||||
)
|
||||
}, [selection])
|
||||
|
||||
// Fast-forward through this arg if it's marked as skippable
|
||||
// and we have a valid selection already
|
||||
useEffect(() => {
|
||||
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])
|
||||
|
||||
function handleChange() {
|
||||
|
@ -48,6 +48,7 @@ function CommandComboBox({
|
||||
(event.metaKey && event.key === 'k') ||
|
||||
(event.key === 'Backspace' && !event.currentTarget.value)
|
||||
) {
|
||||
event.preventDefault()
|
||||
commandBarSend({ type: 'Close' })
|
||||
}
|
||||
}}
|
||||
|
@ -4,6 +4,8 @@ export type CustomIconName =
|
||||
| 'arrowRight'
|
||||
| 'arrowUp'
|
||||
| 'checkmark'
|
||||
| 'clipboardPlus'
|
||||
| 'clipboardCheckmark'
|
||||
| 'close'
|
||||
| 'equal'
|
||||
| 'extrude'
|
||||
@ -13,8 +15,11 @@ export type CustomIconName =
|
||||
| 'folderPlus'
|
||||
| 'gear'
|
||||
| 'horizontal'
|
||||
| 'horizontalDash'
|
||||
| 'line'
|
||||
| 'move'
|
||||
| 'network'
|
||||
| 'networkCrossedOut'
|
||||
| 'parallel'
|
||||
| 'search'
|
||||
| 'sketch'
|
||||
@ -107,6 +112,38 @@ export const CustomIcon = ({
|
||||
/>
|
||||
</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':
|
||||
return (
|
||||
<svg
|
||||
@ -249,6 +286,22 @@ export const CustomIcon = ({
|
||||
/>
|
||||
</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':
|
||||
return (
|
||||
<svg
|
||||
@ -281,6 +334,38 @@ export const CustomIcon = ({
|
||||
/>
|
||||
</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':
|
||||
return (
|
||||
<svg
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { AstExplorer } from './AstExplorer'
|
||||
import { EngineCommands } from './EngineCommands'
|
||||
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
|
||||
|
||||
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||
return (
|
||||
@ -15,6 +16,7 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
|
||||
>
|
||||
<section className="p-4 flex flex-col gap-4">
|
||||
<EngineCommands />
|
||||
<CamDebugSettings />
|
||||
<div style={{ height: '400px' }} className="overflow-y-auto">
|
||||
<AstExplorer />
|
||||
</div>
|
||||
|
43
src/components/DragWarningToast.tsx
Normal 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
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
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 { toast } from 'react-hot-toast'
|
||||
import {
|
||||
|
@ -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 Tooltip from './Tooltip'
|
||||
import { FileEntry } from '@tauri-apps/api/fs'
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { paths } from '../Router'
|
||||
import { paths } from 'lib/paths'
|
||||
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
||||
import withBaseUrl from '../lib/withBaseURL'
|
||||
import React, { createContext, useEffect, useRef } from 'react'
|
||||
|
@ -2,7 +2,7 @@ import ReactJson from 'react-json-view'
|
||||
import { useEffect } from 'react'
|
||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { Themes } from '../lib/theme'
|
||||
import { useKclContext } from 'lang/KclSinglton'
|
||||
import { useKclContext } from 'lang/KclSingleton'
|
||||
|
||||
const ReactJsonTypeHack = ReactJson as any
|
||||
|
||||
|
@ -41,9 +41,9 @@ describe('processMemory', () => {
|
||||
otherVar: 3,
|
||||
theExtrude: [],
|
||||
theSketch: [
|
||||
{ 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: [2.15, 4.32], from: [0.98, 5.16], 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: [2.15, 4.32], from: [0.98, 5.16], name: '' },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
@ -3,7 +3,7 @@ import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { useMemo } from 'react'
|
||||
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/wasm'
|
||||
import { Themes } from '../lib/theme'
|
||||
import { useKclContext } from 'lang/KclSinglton'
|
||||
import { useKclContext } from 'lang/KclSingleton'
|
||||
|
||||
interface MemoryPanelProps extends CollapsiblePanelProps {
|
||||
theme?: Exclude<Themes, Themes.System>
|
||||
|
@ -13,23 +13,7 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||
import { engineCommandManager } from 'lang/std/engineConnection'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { addStartSketch } from 'lang/modifyAst'
|
||||
import { roundOff } from 'lib/utils'
|
||||
import {
|
||||
recast,
|
||||
parse,
|
||||
Program,
|
||||
PipeExpression,
|
||||
CallExpression,
|
||||
} from 'lang/wasm'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import {
|
||||
addCloseToPipe,
|
||||
addNewSketchLn,
|
||||
compareVec2Epsilon,
|
||||
} from 'lang/std/sketch'
|
||||
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
||||
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
||||
import {
|
||||
angleBetweenInfo,
|
||||
@ -49,6 +33,10 @@ import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
||||
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
|
||||
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
||||
import { getSketchQuaternion } from 'clientSideScene/sceneEntities'
|
||||
import { startSketchOnDefault } from 'lang/modifyAst'
|
||||
import { Program } from 'lang/wasm'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -92,181 +80,10 @@ export const ModelingMachineProvider = ({
|
||||
const [modelingState, modelingSend, modelingActor] = useMachine(
|
||||
modelingMachine,
|
||||
{
|
||||
// context: persistedSettings,
|
||||
actions: {
|
||||
'Modify AST': () => {},
|
||||
'Update code selection cursors': () => {},
|
||||
'show default planes': () => {
|
||||
kclManager.showPlanes()
|
||||
},
|
||||
'create path': assign({
|
||||
sketchEnginePathId: () => {
|
||||
const sketchUuid = uuidv4()
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: sketchUuid,
|
||||
cmd: {
|
||||
type: 'start_path',
|
||||
},
|
||||
})
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'edit_mode_enter',
|
||||
target: sketchUuid,
|
||||
},
|
||||
})
|
||||
return sketchUuid
|
||||
},
|
||||
}),
|
||||
'AST start new sketch': assign(
|
||||
({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
|
||||
if (!axis) {
|
||||
// Something really weird must have happened for this to happen.
|
||||
console.error('axis is undefined for starting a new sketch')
|
||||
return {}
|
||||
}
|
||||
if (!segmentId) {
|
||||
// Something really weird must have happened for this to happen.
|
||||
console.error('segmentId is undefined for starting a new sketch')
|
||||
return {}
|
||||
}
|
||||
|
||||
const _addStartSketch = addStartSketch(
|
||||
kclManager.ast,
|
||||
axis,
|
||||
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
||||
[
|
||||
roundOff(coords[1].x - coords[0].x),
|
||||
roundOff(coords[1].y - coords[0].y),
|
||||
]
|
||||
)
|
||||
const _modifiedAst = _addStartSketch.modifiedAst
|
||||
const _pathToNode = _addStartSketch.pathToNode
|
||||
const newCode = recast(_modifiedAst)
|
||||
const astWithUpdatedSource = parse(newCode)
|
||||
const updatedPipeNode = getNodeFromPath<PipeExpression>(
|
||||
astWithUpdatedSource,
|
||||
_pathToNode
|
||||
).node
|
||||
const startProfileAtCallExp = updatedPipeNode.body.find(
|
||||
(exp) =>
|
||||
exp.type === 'CallExpression' &&
|
||||
exp.callee.name === 'startProfileAt'
|
||||
)
|
||||
if (startProfileAtCallExp)
|
||||
engineCommandManager.artifactMap[sketchEnginePathId] = {
|
||||
type: 'result',
|
||||
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
|
||||
commandType: 'start_path',
|
||||
data: null,
|
||||
raw: {} as any,
|
||||
}
|
||||
const lineCallExp = updatedPipeNode.body.find(
|
||||
(exp) =>
|
||||
exp.type === 'CallExpression' && exp.callee.name === 'line'
|
||||
)
|
||||
if (lineCallExp)
|
||||
engineCommandManager.artifactMap[segmentId] = {
|
||||
type: 'result',
|
||||
range: [lineCallExp.start, lineCallExp.end],
|
||||
commandType: 'extend_path',
|
||||
parentId: sketchEnginePathId,
|
||||
data: null,
|
||||
raw: {} as any,
|
||||
}
|
||||
|
||||
kclManager.executeAstMock(astWithUpdatedSource, true)
|
||||
|
||||
return {
|
||||
sketchPathToNode: _pathToNode,
|
||||
}
|
||||
}
|
||||
),
|
||||
'AST add line segment': async (
|
||||
{ sketchPathToNode, sketchEnginePathId },
|
||||
{ data: { coords, segmentId } }
|
||||
) => {
|
||||
if (!sketchPathToNode) return
|
||||
const lastCoord = coords[coords.length - 1]
|
||||
|
||||
const pathInfo = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'path_get_info',
|
||||
path_id: sketchEnginePathId,
|
||||
},
|
||||
})
|
||||
const firstSegment = pathInfo?.data?.data?.segments.find(
|
||||
(seg: any) => seg.command === 'line_to'
|
||||
)
|
||||
const firstSegCoords = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: firstSegment.command_id,
|
||||
},
|
||||
})
|
||||
const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
|
||||
|
||||
const isClose = compareVec2Epsilon(
|
||||
[startPathCoord.x, startPathCoord.y],
|
||||
[lastCoord.x, lastCoord.y]
|
||||
)
|
||||
|
||||
let _modifiedAst: Program
|
||||
if (!isClose) {
|
||||
const newSketchLn = addNewSketchLn({
|
||||
node: kclManager.ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
to: [lastCoord.x, lastCoord.y],
|
||||
from: [coords[0].x, coords[0].y],
|
||||
fnName: 'line',
|
||||
pathToNode: sketchPathToNode,
|
||||
})
|
||||
const _modifiedAst = newSketchLn.modifiedAst
|
||||
kclManager.executeAstMock(_modifiedAst, true).then(() => {
|
||||
const lineCallExp = getNodeFromPath<CallExpression>(
|
||||
kclManager.ast,
|
||||
newSketchLn.pathToNode
|
||||
).node
|
||||
if (segmentId)
|
||||
engineCommandManager.artifactMap[segmentId] = {
|
||||
type: 'result',
|
||||
range: [lineCallExp.start, lineCallExp.end],
|
||||
commandType: 'extend_path',
|
||||
parentId: sketchEnginePathId,
|
||||
data: null,
|
||||
raw: {} as any,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
_modifiedAst = addCloseToPipe({
|
||||
node: kclManager.ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
pathToNode: sketchPathToNode,
|
||||
})
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'edit_mode_exit' },
|
||||
})
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||
})
|
||||
kclManager.executeAstMock(_modifiedAst, true)
|
||||
// updateAst(_modifiedAst, true)
|
||||
}
|
||||
},
|
||||
'sketch exit execute': () => {
|
||||
kclManager.executeAst()
|
||||
},
|
||||
'set tool': () => {}, // TODO
|
||||
'Set selection': assign(({ selectionRanges }, event) => {
|
||||
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
|
||||
const setSelections = event.data
|
||||
@ -274,36 +91,6 @@ export const ModelingMachineProvider = ({
|
||||
if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
|
||||
return { selectionRanges: setSelections.selection }
|
||||
else if (setSelections.selectionType === 'otherSelection') {
|
||||
// TODO KittyCAD/engine/issues/1620: send axis highlight when it's working (if that's what we settle on)
|
||||
// const axisAddCmd: EngineCommand = {
|
||||
// type: 'modeling_cmd_req',
|
||||
// cmd: {
|
||||
// type: 'highlight_set_entities',
|
||||
// entities: [
|
||||
// setSelections.selection === 'x-axis'
|
||||
// ? X_AXIS_UUID
|
||||
// : Y_AXIS_UUID,
|
||||
// ],
|
||||
// },
|
||||
// cmd_id: uuidv4(),
|
||||
// }
|
||||
|
||||
// if (!isShiftDown) {
|
||||
// engineCommandManager
|
||||
// .sendSceneCommand({
|
||||
// type: 'modeling_cmd_req',
|
||||
// cmd: {
|
||||
// type: 'select_clear',
|
||||
// },
|
||||
// cmd_id: uuidv4(),
|
||||
// })
|
||||
// .then(() => {
|
||||
// engineCommandManager.sendSceneCommand(axisAddCmd)
|
||||
// })
|
||||
// } else {
|
||||
// engineCommandManager.sendSceneCommand(axisAddCmd)
|
||||
// }
|
||||
|
||||
const {
|
||||
codeMirrorSelection,
|
||||
selectionRangeTypeMap,
|
||||
@ -384,12 +171,6 @@ export const ModelingMachineProvider = ({
|
||||
}),
|
||||
},
|
||||
guards: {
|
||||
'Selection contains axis': () => true,
|
||||
'Selection contains edge': () => true,
|
||||
'Selection contains face': () => true,
|
||||
'Selection contains line': () => true,
|
||||
'Selection contains point': () => true,
|
||||
'Selection is not empty': () => true,
|
||||
'has valid extrude selection': ({ selectionRanges }) => {
|
||||
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
||||
// TODO: I believe this guard only allows for extruding a single face at a time
|
||||
@ -409,6 +190,40 @@ export const ModelingMachineProvider = ({
|
||||
},
|
||||
},
|
||||
services: {
|
||||
'AST-undo-startSketchOn': async ({ sketchPathToNode }) => {
|
||||
if (!sketchPathToNode) return
|
||||
const newAst: Program = JSON.parse(JSON.stringify(kclManager.ast))
|
||||
const varDecIndex = sketchPathToNode[1][0]
|
||||
// remove body item at varDecIndex
|
||||
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
|
||||
await kclManager.executeAstMock(newAst, { updates: 'code' })
|
||||
sceneInfra.setCallbacks({
|
||||
onClick: () => {},
|
||||
})
|
||||
},
|
||||
'animate-to-face': async (_, { data: { plane, normal } }) => {
|
||||
const { modifiedAst, pathToNode } = startSketchOnDefault(
|
||||
kclManager.ast,
|
||||
plane
|
||||
)
|
||||
await kclManager.updateAst(modifiedAst, false)
|
||||
const quaternion = getSketchQuaternion(pathToNode, normal)
|
||||
await sceneInfra.tweenCameraToQuaternion(quaternion)
|
||||
return {
|
||||
sketchPathToNode: pathToNode,
|
||||
sketchNormalBackUp: normal,
|
||||
}
|
||||
},
|
||||
'animate-to-sketch': async ({
|
||||
sketchPathToNode,
|
||||
sketchNormalBackUp,
|
||||
}) => {
|
||||
const quaternion = getSketchQuaternion(
|
||||
sketchPathToNode || [],
|
||||
sketchNormalBackUp
|
||||
)
|
||||
await sceneInfra.tweenCameraToQuaternion(quaternion)
|
||||
},
|
||||
'Get horizontal info': async ({
|
||||
selectionRanges,
|
||||
}): Promise<SetSelections> => {
|
||||
@ -542,17 +357,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(() => {
|
||||
kclManager.registerExecuteCallback(() => {
|
||||
modelingSend({ type: 'Re-execute' })
|
||||
@ -565,10 +369,7 @@ export const ModelingMachineProvider = ({
|
||||
send: modelingSend,
|
||||
actor: modelingActor,
|
||||
commandBarConfig: modelingMachineConfig,
|
||||
onCancel: () => {
|
||||
console.log('firing onCancel!!')
|
||||
modelingSend({ type: 'Cancel' })
|
||||
},
|
||||
onCancel: () => modelingSend({ type: 'Cancel' }),
|
||||
})
|
||||
|
||||
return (
|
||||
|
@ -3,8 +3,9 @@ import { BrowserRouter } from 'react-router-dom'
|
||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||
import CommandBarProvider from './CommandBar/CommandBar'
|
||||
import {
|
||||
NETWORK_CONTENT,
|
||||
NETWORK_HEALTH_TEXT,
|
||||
NetworkHealthIndicator,
|
||||
NetworkHealthState,
|
||||
} from './NetworkHealthIndicator'
|
||||
|
||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
@ -28,8 +29,8 @@ describe('NetworkHealthIndicator tests', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||
|
||||
expect(screen.getByTestId('network-good')).toHaveTextContent(
|
||||
NETWORK_CONTENT.good
|
||||
expect(screen.getByTestId('network')).toHaveTextContent(
|
||||
NETWORK_HEALTH_TEXT[NetworkHealthState.Ok]
|
||||
)
|
||||
})
|
||||
|
||||
@ -43,8 +44,8 @@ describe('NetworkHealthIndicator tests', () => {
|
||||
fireEvent.offline(window)
|
||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||
|
||||
expect(screen.getByTestId('network-bad')).toHaveTextContent(
|
||||
NETWORK_CONTENT.bad
|
||||
expect(screen.getByTestId('network')).toHaveTextContent(
|
||||
NETWORK_HEALTH_TEXT[NetworkHealthState.Disconnected]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -1,41 +1,186 @@
|
||||
import { faExclamation, faWifi } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Popover } from '@headlessui/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 = {
|
||||
good: 'Network health is good',
|
||||
bad: 'Network issue',
|
||||
export enum NetworkHealthState {
|
||||
Ok,
|
||||
Issue,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
const NETWORK_MESSAGES = {
|
||||
offline: 'You are offline',
|
||||
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
|
||||
[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 = () => {
|
||||
const [networkIssues, setNetworkIssues] = useState<string[]>([])
|
||||
const hasIssues = [...networkIssues.values()].length > 0
|
||||
const [steps, setSteps] = useState(initialConnectingTypeGroupState)
|
||||
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(() => {
|
||||
const offlineListener = () =>
|
||||
setNetworkIssues((issues) => {
|
||||
return [
|
||||
...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline),
|
||||
NETWORK_MESSAGES.offline,
|
||||
]
|
||||
})
|
||||
window.addEventListener('offline', offlineListener)
|
||||
setOverallState(
|
||||
!internetConnected
|
||||
? NetworkHealthState.Disconnected
|
||||
: hasIssues
|
||||
? NetworkHealthState.Issue
|
||||
: NetworkHealthState.Ok
|
||||
)
|
||||
}, [hasIssues, internetConnected])
|
||||
|
||||
const onlineListener = () =>
|
||||
setNetworkIssues((issues) => {
|
||||
return [...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline)]
|
||||
})
|
||||
window.addEventListener('online', onlineListener)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('offline', offlineListener)
|
||||
window.removeEventListener('online', onlineListener)
|
||||
useEffect(() => {
|
||||
const cb1 = () => {
|
||||
setSteps(initialConnectingTypeGroupState)
|
||||
setInternetConnected(true)
|
||||
}
|
||||
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 (
|
||||
@ -45,65 +190,94 @@ export const NetworkHealthIndicator = () => {
|
||||
'p-0 border-none bg-transparent dark:bg-transparent relative ' +
|
||||
(hasIssues
|
||||
? 'focus-visible:outline-destroy-80'
|
||||
: 'focus-visible:outline-succeed-80')
|
||||
: 'focus-visible:outline-energy-80')
|
||||
}
|
||||
data-testid="network-toggle"
|
||||
>
|
||||
<span className="sr-only">Network Health</span>
|
||||
<ActionIcon
|
||||
icon={faWifi}
|
||||
icon={overallConnectionStateIcon[overallState]}
|
||||
className="p-1"
|
||||
iconClassName={
|
||||
hasIssues
|
||||
? 'text-destroy-80 dark:text-destroy-30'
|
||||
: 'text-succeed-80 dark:text-succeed-30'
|
||||
}
|
||||
iconClassName={overallConnectionStateColor[overallState].icon}
|
||||
bgClassName={
|
||||
'bg-transparent dark:bg-transparent ' +
|
||||
(hasIssues
|
||||
? 'hover:bg-destroy-10/50 hover:dark:bg-destroy-80/50 rounded'
|
||||
: 'hover:bg-succeed-10/50 hover:dark:bg-succeed-80/50 rounded')
|
||||
'rounded-sm ' + overallConnectionStateColor[overallState].bg
|
||||
}
|
||||
/>
|
||||
<Tooltip position="blockEnd" delay={750} className="ui-open:hidden">
|
||||
Network Health ({NETWORK_HEALTH_TEXT[overallState]})
|
||||
</Tooltip>
|
||||
</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">
|
||||
{!hasIssues ? (
|
||||
<span
|
||||
className="flex items-center justify-center gap-1 px-4"
|
||||
data-testid="network-good"
|
||||
<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">
|
||||
<div
|
||||
className={`flex items-center justify-between p-2 rounded-t-sm ${overallConnectionStateColor[overallState].bg} ${overallConnectionStateColor[overallState].icon}`}
|
||||
>
|
||||
<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
|
||||
icon="checkmark"
|
||||
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded-sm'}
|
||||
iconClassName={'text-succeed-80 dark:text-succeed-30'}
|
||||
/>
|
||||
{NETWORK_CONTENT.good}
|
||||
</span>
|
||||
) : (
|
||||
<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_HEALTH_TEXT[overallState]}
|
||||
</p>
|
||||
</div>
|
||||
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
|
||||
{Object.keys(steps).map((name) => (
|
||||
<li
|
||||
key={name}
|
||||
className={'flex flex-col px-2 py-4 gap-1 last:mb-0 '}
|
||||
>
|
||||
{NETWORK_CONTENT.bad}
|
||||
{networkIssues.length > 1 ? 's' : ''}
|
||||
</span>
|
||||
{networkIssues.map((issue) => (
|
||||
<li
|
||||
key={issue}
|
||||
className="flex items-center gap-1 py-2 my-2 last:mb-0"
|
||||
>
|
||||
<ActionIcon
|
||||
icon={faExclamation}
|
||||
bgClassName={'bg-destroy-10/50 dark:bg-destroy-80/50 rounded'}
|
||||
iconClassName={'text-destroy-80 dark:text-destroy-30'}
|
||||
className="ml-4"
|
||||
/>
|
||||
<p className="flex-1 mr-4">{issue}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<div className="flex items-center text-left gap-1">
|
||||
<p className="flex-1">{name}</p>
|
||||
{internetConnected ? (
|
||||
<ActionIcon
|
||||
size="lg"
|
||||
icon={
|
||||
hasIssueToIcon[
|
||||
issues[name as ConnectingTypeGroup].toString()
|
||||
]
|
||||
}
|
||||
iconClassName={
|
||||
hasIssueToIconColors[
|
||||
issues[name as ConnectingTypeGroup].toString()
|
||||
].icon
|
||||
}
|
||||
bgClassName={
|
||||
'rounded-sm ' +
|
||||
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>
|
||||
)
|
||||
|
@ -1,5 +1,6 @@
|
||||
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 { ActionButton } from './ActionButton'
|
||||
import {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||
import CommandBarProvider from './CommandBar/CommandBar'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { ActionButton } from './ActionButton'
|
||||
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 { Link } from 'react-router-dom'
|
||||
import { ExportButton } from './ExportButton'
|
||||
|
@ -11,16 +11,13 @@ import { getNormalisedCoordinates, throttle } from '../lib/utils'
|
||||
import Loading from './Loading'
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { VariableDeclarator, recast, CallExpression } from 'lang/wasm'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
||||
import { changeSketchArguments } from 'lang/std/sketch'
|
||||
import { useKclContext } from 'lang/KclSingleton'
|
||||
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
||||
|
||||
export const Stream = ({ className = '' }) => {
|
||||
export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>()
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
@ -39,7 +36,7 @@ export const Stream = ({ className = '' }) => {
|
||||
}))
|
||||
const { settings } = useGlobalStateContext()
|
||||
const cameraControls = settings?.context?.cameraControls
|
||||
const { send, state, context } = useModelingContext()
|
||||
const { state } = useModelingContext()
|
||||
const { isExecuting } = useKclContext()
|
||||
|
||||
useEffect(() => {
|
||||
@ -53,8 +50,10 @@ export const Stream = ({ className = '' }) => {
|
||||
videoRef.current.srcObject = mediaStream
|
||||
}, [mediaStream])
|
||||
|
||||
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
|
||||
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
if (!videoRef.current) return
|
||||
if (state.matches('Sketch')) return
|
||||
if (state.matches('Sketch no face')) return
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
@ -62,55 +61,6 @@ export const Stream = ({ className = '' }) => {
|
||||
...streamDimensions,
|
||||
})
|
||||
|
||||
const newId = uuidv4()
|
||||
|
||||
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
||||
let interaction: CameraDragInteractionType_type = 'rotate'
|
||||
|
||||
if (
|
||||
interactionGuards.pan.callback(e) ||
|
||||
interactionGuards.pan.lenientDragStartButton === e.button
|
||||
) {
|
||||
interaction = 'pan'
|
||||
} else if (
|
||||
interactionGuards.rotate.callback(e) ||
|
||||
interactionGuards.rotate.lenientDragStartButton === e.button
|
||||
) {
|
||||
interaction = 'rotate'
|
||||
} else if (
|
||||
interactionGuards.zoom.dragCallback(e) ||
|
||||
interactionGuards.zoom.lenientDragStartButton === e.button
|
||||
) {
|
||||
interaction = 'zoom'
|
||||
}
|
||||
|
||||
if (state.matches('Sketch.Move Tool')) {
|
||||
if (
|
||||
state.matches('Sketch.Move Tool.No move') ||
|
||||
state.matches('Sketch.Move Tool.Move with execute')
|
||||
) {
|
||||
return
|
||||
}
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'handle_mouse_drag_start',
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
} else if (!state.matches('Sketch.Line Tool')) {
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_start',
|
||||
interaction,
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
}
|
||||
|
||||
setButtonDownInStream(e.button)
|
||||
setClickCoords({ x, y })
|
||||
}
|
||||
@ -128,13 +78,15 @@ export const Stream = ({ className = '' }) => {
|
||||
})
|
||||
}, Math.round(1000 / fps))
|
||||
|
||||
const handleMouseUp: MouseEventHandler<HTMLVideoElement> = ({
|
||||
const handleMouseUp: MouseEventHandler<HTMLDivElement> = ({
|
||||
clientX,
|
||||
clientY,
|
||||
ctrlKey,
|
||||
}) => {
|
||||
if (!videoRef.current) return
|
||||
setButtonDownInStream(undefined)
|
||||
if (state.matches('Sketch')) return
|
||||
if (state.matches('Sketch no face')) return
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX,
|
||||
clientY,
|
||||
@ -155,206 +107,19 @@ export const Stream = ({ className = '' }) => {
|
||||
cmd_id: newCmdId,
|
||||
}
|
||||
|
||||
if (!didDragInStream && state.matches('Sketch no face')) {
|
||||
command.cmd = {
|
||||
type: 'select_with_point',
|
||||
selection_type: 'add',
|
||||
selected_at_window: { x, y },
|
||||
}
|
||||
engineCommandManager.sendSceneCommand(command)
|
||||
} else if (!didDragInStream && state.matches('Sketch.Line Tool')) {
|
||||
command.cmd = {
|
||||
type: 'mouse_click',
|
||||
window: { x, y },
|
||||
}
|
||||
engineCommandManager.sendSceneCommand(command).then(async (resp) => {
|
||||
const entities_modified = resp?.data?.data?.entities_modified
|
||||
if (!entities_modified) return
|
||||
if (state.matches('Sketch.Line Tool.No Points')) {
|
||||
send('Add point')
|
||||
} else if (state.matches('Sketch.Line Tool.Point Added')) {
|
||||
const curve = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
// We need the normal for the plane we are on.
|
||||
const plane = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'get_sketch_mode_plane',
|
||||
},
|
||||
})
|
||||
const z_axis = plane.data.data.z_axis
|
||||
|
||||
// Get the current axis.
|
||||
let currentAxis: 'xy' | 'xz' | 'yz' | '-xy' | '-xz' | '-yz' | null =
|
||||
null
|
||||
if (context.sketchPlaneId === kclManager.getPlaneId('xy')) {
|
||||
if (z_axis.z === -1) {
|
||||
currentAxis = '-xy'
|
||||
} else {
|
||||
currentAxis = 'xy'
|
||||
}
|
||||
} else if (context.sketchPlaneId === kclManager.getPlaneId('yz')) {
|
||||
if (z_axis.x === -1) {
|
||||
currentAxis = '-yz'
|
||||
} else {
|
||||
currentAxis = 'yz'
|
||||
}
|
||||
} else if (context.sketchPlaneId === kclManager.getPlaneId('xz')) {
|
||||
if (z_axis.y === -1) {
|
||||
currentAxis = '-xz'
|
||||
} else {
|
||||
currentAxis = 'xz'
|
||||
}
|
||||
}
|
||||
|
||||
send({
|
||||
type: 'Add point',
|
||||
data: {
|
||||
coords,
|
||||
axis: currentAxis,
|
||||
segmentId: entities_modified[0],
|
||||
},
|
||||
})
|
||||
} else if (state.matches('Sketch.Line Tool.Segment Added')) {
|
||||
const curve = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
send({
|
||||
type: 'Add point',
|
||||
data: { coords, axis: null, segmentId: entities_modified[0] },
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (
|
||||
!didDragInStream &&
|
||||
(state.matches('Sketch.SketchIdle') || state.matches('idle'))
|
||||
) {
|
||||
command.cmd = {
|
||||
type: 'select_with_point',
|
||||
selected_at_window: { x, y },
|
||||
selection_type: 'add',
|
||||
}
|
||||
|
||||
engineCommandManager.sendSceneCommand(command)
|
||||
} else if (!didDragInStream && state.matches('Sketch.Move Tool')) {
|
||||
if (!didDragInStream) {
|
||||
command.cmd = {
|
||||
type: 'select_with_point',
|
||||
selected_at_window: { x, y },
|
||||
selection_type: 'add',
|
||||
}
|
||||
engineCommandManager.sendSceneCommand(command)
|
||||
} else if (didDragInStream && state.matches('Sketch.Move Tool')) {
|
||||
} else if (didDragInStream) {
|
||||
command.cmd = {
|
||||
type: 'handle_mouse_drag_end',
|
||||
window: { x, y },
|
||||
}
|
||||
engineCommandManager.sendSceneCommand(command).then(async () => {
|
||||
if (!context.sketchPathToNode) return
|
||||
getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
context.sketchPathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
// Get the current plane string for plane we are on.
|
||||
let currentPlaneString = ''
|
||||
if (context.sketchPlaneId === kclManager.getPlaneId('xy')) {
|
||||
currentPlaneString = 'XY'
|
||||
} else if (context.sketchPlaneId === kclManager.getPlaneId('yz')) {
|
||||
currentPlaneString = 'YZ'
|
||||
} else if (context.sketchPlaneId === kclManager.getPlaneId('xz')) {
|
||||
currentPlaneString = 'XZ'
|
||||
}
|
||||
|
||||
// Do not supporting editing/moving lines on a non-default plane.
|
||||
// Eventually we can support this but for now we will just throw an
|
||||
// error.
|
||||
if (currentPlaneString === '') return
|
||||
|
||||
const pathInfo = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'path_get_info',
|
||||
path_id: context.sketchEnginePathId,
|
||||
},
|
||||
})
|
||||
const segmentsWithMappings = (
|
||||
pathInfo?.data?.data?.segments as { command_id: string }[]
|
||||
)
|
||||
.filter(({ command_id }) => {
|
||||
return command_id && engineCommandManager.artifactMap[command_id]
|
||||
})
|
||||
.map(({ command_id }) => command_id)
|
||||
const segment2dInfo = await Promise.all(
|
||||
segmentsWithMappings.map(async (segmentId) => {
|
||||
const response = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: segmentId,
|
||||
},
|
||||
})
|
||||
const controlPoints: [
|
||||
{ x: number; y: number },
|
||||
{ x: number; y: number }
|
||||
] = response.data.data.control_points
|
||||
return {
|
||||
controlPoints,
|
||||
segmentId,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
let modifiedAst = { ...kclManager.ast }
|
||||
let code = kclManager.code
|
||||
for (const controlPoint of segment2dInfo) {
|
||||
const range =
|
||||
engineCommandManager.artifactMap[controlPoint.segmentId].range
|
||||
if (!range) continue
|
||||
const from = controlPoint.controlPoints[0]
|
||||
const to = controlPoint.controlPoints[1]
|
||||
const modded = changeSketchArguments(
|
||||
modifiedAst,
|
||||
kclManager.programMemory,
|
||||
range,
|
||||
[to.x, to.y],
|
||||
[from.x, from.y]
|
||||
)
|
||||
modifiedAst = modded.modifiedAst
|
||||
|
||||
// update artifact map ranges now that we have updated the ast.
|
||||
code = recast(modded.modifiedAst)
|
||||
const astWithCurrentRanges = kclManager.safeParse(code)
|
||||
if (!astWithCurrentRanges) return
|
||||
const updateNode = getNodeFromPath<CallExpression>(
|
||||
astWithCurrentRanges,
|
||||
modded.pathToNode
|
||||
).node
|
||||
engineCommandManager.artifactMap[controlPoint.segmentId].range = [
|
||||
updateNode.start,
|
||||
updateNode.end,
|
||||
]
|
||||
}
|
||||
|
||||
kclManager.executeAstMock(modifiedAst, true)
|
||||
})
|
||||
void engineCommandManager.sendSceneCommand(command)
|
||||
} else {
|
||||
engineCommandManager.sendSceneCommand(command)
|
||||
}
|
||||
@ -364,6 +129,8 @@ export const Stream = ({ className = '' }) => {
|
||||
}
|
||||
|
||||
const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => {
|
||||
if (state.matches('Sketch')) return
|
||||
if (state.matches('Sketch no face')) return
|
||||
if (!clickCoords) return
|
||||
|
||||
const delta =
|
||||
@ -376,16 +143,19 @@ export const Stream = ({ className = '' }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="stream" className={className}>
|
||||
<div
|
||||
id="stream"
|
||||
className={className}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseDown={handleMouseDown}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onContextMenuCapture={(e) => e.preventDefault()}
|
||||
>
|
||||
<video
|
||||
ref={videoRef}
|
||||
muted
|
||||
autoPlay
|
||||
controls={false}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onContextMenuCapture={(e) => e.preventDefault()}
|
||||
onWheel={handleScroll}
|
||||
onPlay={() => setIsLoading(false)}
|
||||
onMouseMoveCapture={handleMouseMove}
|
||||
@ -393,6 +163,7 @@ export const Stream = ({ className = '' }) => {
|
||||
disablePictureInPicture
|
||||
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
||||
/>
|
||||
<ClientSideScene cameraControls={settings.context.cameraControls} />
|
||||
{isLoading && (
|
||||
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Loading>
|
||||
|
@ -11,7 +11,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { linter, lintGutter } from '@codemirror/lint'
|
||||
import { useStore } from 'useStore'
|
||||
import { processCodeMirrorRanges } from 'lib/selections'
|
||||
@ -24,7 +24,10 @@ import { CSSRuleObject } from 'tailwindcss/types/config'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import interact from '@replit/codemirror-interact'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
||||
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||
import { sceneInfra } from 'clientSideScene/sceneInfra'
|
||||
import { copilotBundle } from 'editor/copilot'
|
||||
|
||||
export const editorShortcutMeta = {
|
||||
formatCode: {
|
||||
@ -44,25 +47,31 @@ export const TextEditor = ({
|
||||
}) => {
|
||||
const {
|
||||
editorView,
|
||||
isLSPServerReady,
|
||||
isKclLspServerReady,
|
||||
isCopilotLspServerReady,
|
||||
setEditorView,
|
||||
setIsLSPServerReady,
|
||||
setIsKclLspServerReady,
|
||||
setIsCopilotLspServerReady,
|
||||
isShiftDown,
|
||||
} = useStore((s) => ({
|
||||
editorView: s.editorView,
|
||||
isLSPServerReady: s.isLSPServerReady,
|
||||
isKclLspServerReady: s.isKclLspServerReady,
|
||||
isCopilotLspServerReady: s.isCopilotLspServerReady,
|
||||
setEditorView: s.setEditorView,
|
||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
||||
setIsKclLspServerReady: s.setIsKclLspServerReady,
|
||||
setIsCopilotLspServerReady: s.setIsCopilotLspServerReady,
|
||||
isShiftDown: s.isShiftDown,
|
||||
}))
|
||||
const { code, errors } = useKclContext()
|
||||
const lastEvent = useRef({ event: '', time: Date.now() })
|
||||
|
||||
const {
|
||||
context: { selectionRanges, selectionRangeTypeMap },
|
||||
send,
|
||||
state,
|
||||
} = useModelingContext()
|
||||
|
||||
const { settings: { context: { textWrapping } = {} } = {} } =
|
||||
const { settings: { context: { textWrapping } = {} } = {}, auth } =
|
||||
useGlobalStateContext()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||
@ -71,20 +80,20 @@ export const TextEditor = ({
|
||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
||||
// But the server happens async so we break this into two parts.
|
||||
// Below is the client and server promise.
|
||||
const { lspClient } = useMemo(() => {
|
||||
const { lspClient: kclLspClient } = useMemo(() => {
|
||||
const intoServer: IntoServer = new IntoServer()
|
||||
const fromServer: FromServer = FromServer.create()
|
||||
const client = new Client(fromServer, intoServer)
|
||||
if (!TEST) {
|
||||
Server.initialize(intoServer, fromServer).then((lspServer) => {
|
||||
lspServer.start()
|
||||
setIsLSPServerReady(true)
|
||||
lspServer.start('kcl')
|
||||
setIsKclLspServerReady(true)
|
||||
})
|
||||
}
|
||||
|
||||
const lspClient = new LanguageServerClient({ client })
|
||||
return { lspClient }
|
||||
}, [setIsLSPServerReady])
|
||||
}, [setIsKclLspServerReady])
|
||||
|
||||
// Here we initialize the plugin which will start the client.
|
||||
// When we have multi-file support the name of the file will be a dep of
|
||||
@ -93,19 +102,57 @@ export const TextEditor = ({
|
||||
// We do not want to restart the server, its just wasteful.
|
||||
const kclLSP = useMemo(() => {
|
||||
let plugin = null
|
||||
if (isLSPServerReady && !TEST) {
|
||||
if (isKclLspServerReady && !TEST) {
|
||||
// Set up the lsp plugin.
|
||||
const lsp = kclLanguage({
|
||||
// When we have more than one file, we'll need to change this.
|
||||
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
|
||||
workspaceFolders: null,
|
||||
client: lspClient,
|
||||
client: kclLspClient,
|
||||
})
|
||||
|
||||
plugin = lsp
|
||||
}
|
||||
return plugin
|
||||
}, [lspClient, isLSPServerReady])
|
||||
}, [kclLspClient, isKclLspServerReady])
|
||||
|
||||
const { lspClient: copilotLspClient } = useMemo(() => {
|
||||
const intoServer: IntoServer = new IntoServer()
|
||||
const fromServer: FromServer = FromServer.create()
|
||||
const client = new Client(fromServer, intoServer)
|
||||
if (!TEST) {
|
||||
Server.initialize(intoServer, fromServer).then((lspServer) => {
|
||||
const token = auth?.context?.token
|
||||
lspServer.start('copilot', token)
|
||||
setIsCopilotLspServerReady(true)
|
||||
})
|
||||
}
|
||||
|
||||
const lspClient = new LanguageServerClient({ client })
|
||||
return { lspClient }
|
||||
}, [setIsCopilotLspServerReady])
|
||||
|
||||
// Here we initialize the plugin which will start the client.
|
||||
// When we have multi-file support the name of the file will be a dep of
|
||||
// this use memo, as well as the directory structure, which I think is
|
||||
// a good setup because it will restart the client but not the server :)
|
||||
// We do not want to restart the server, its just wasteful.
|
||||
const copilotLSP = useMemo(() => {
|
||||
let plugin = null
|
||||
if (isCopilotLspServerReady && !TEST) {
|
||||
// Set up the lsp plugin.
|
||||
const lsp = copilotBundle({
|
||||
// When we have more than one file, we'll need to change this.
|
||||
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
|
||||
workspaceFolders: null,
|
||||
client: copilotLspClient,
|
||||
allowHTMLContent: true,
|
||||
})
|
||||
|
||||
plugin = lsp
|
||||
}
|
||||
return plugin
|
||||
}, [copilotLspClient, isCopilotLspServerReady])
|
||||
|
||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||
const onChange = (newCode: string) => {
|
||||
@ -115,6 +162,12 @@ export const TextEditor = ({
|
||||
if (!editorView) {
|
||||
setEditorView(viewUpdate.view)
|
||||
}
|
||||
if (sceneInfra.selected) return // mid drag
|
||||
const ignoreEvents: ModelingMachineEvent['type'][] = [
|
||||
'Equip Line tool',
|
||||
'Equip tangential arc to',
|
||||
]
|
||||
if (ignoreEvents.includes(state.event.type)) return
|
||||
const eventInfo = processCodeMirrorRanges({
|
||||
codeMirrorRanges: viewUpdate.state.selection.ranges,
|
||||
selectionRanges,
|
||||
@ -122,7 +175,20 @@ export const TextEditor = ({
|
||||
isShiftDown,
|
||||
})
|
||||
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)
|
||||
eventInfo.engineEvents.forEach((event) =>
|
||||
engineCommandManager.sendSceneCommand(event)
|
||||
@ -161,6 +227,7 @@ export const TextEditor = ({
|
||||
] as Extension[]
|
||||
|
||||
if (kclLSP) extensions.push(kclLSP)
|
||||
if (copilotLSP) extensions.push(copilotLSP)
|
||||
|
||||
// These extensions have proven to mess with vitest
|
||||
if (!TEST) {
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
getTransformInfos,
|
||||
PathToNodeMap,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
|
||||
export function equalAngleInfo({
|
||||
selectionRanges,
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
getTransformInfos,
|
||||
PathToNodeMap,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
|
||||
export function setEqualLengthInfo({
|
||||
selectionRanges,
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
getTransformInfos,
|
||||
transformAstSketchLines,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
|
||||
export function horzVertInfo(
|
||||
selectionRanges: Selections,
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
|
||||
import { createVariableDeclaration } from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
|
||||
const getModalInfo = createInfoModal(GetInfoModal)
|
||||
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
getRemoveConstraintsTransforms,
|
||||
transformAstSketchLines,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
|
||||
export function removeConstrainingValuesInfo({
|
||||
selectionRanges,
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
createVariableDeclaration,
|
||||
} from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
|
||||
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
||||
|
||||
@ -139,7 +139,7 @@ export function applyConstraintAxisAlign({
|
||||
constraint,
|
||||
}).transforms
|
||||
|
||||
let finalValue = createIdentifier('_0')
|
||||
let finalValue = createIdentifier('ZERO')
|
||||
|
||||
return transformAstSketchLines({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
|
||||
import { createVariableDeclaration } from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
|
||||
const getModalInfo = createInfoModal(GetInfoModal)
|
||||
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
|
||||
import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
import { Selections } from 'lib/selections'
|
||||
|
||||
const getModalInfo = createInfoModal(GetInfoModal)
|
||||
|
@ -21,7 +21,7 @@ import {
|
||||
} from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { normaliseAngle } from '../../lib/utils'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
|
||||
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
||||
|
||||
@ -89,12 +89,16 @@ export async function applyConstraintAngleLength({
|
||||
isReferencingXAxis && angleOrLength === 'setAngle'
|
||||
|
||||
let forceVal = valueUsedInTransform || 0
|
||||
let calcIdentifier = createIdentifier('_0')
|
||||
let calcIdentifier = createIdentifier('ZERO')
|
||||
if (isReferencingYAxisAngle) {
|
||||
calcIdentifier = createIdentifier(forceVal < 0 ? '_270' : '_90')
|
||||
calcIdentifier = createIdentifier(
|
||||
forceVal < 0 ? 'THREE_QUARTER_TURN' : 'QUARTER_TURN'
|
||||
)
|
||||
forceVal = normaliseAngle(forceVal + (forceVal < 0 ? 90 : -90))
|
||||
} else if (isReferencingXAxisAngle) {
|
||||
calcIdentifier = createIdentifier(Math.abs(forceVal) > 90 ? '_180' : '_0')
|
||||
calcIdentifier = createIdentifier(
|
||||
Math.abs(forceVal) > 90 ? 'HALF_TURN' : 'ZERO'
|
||||
)
|
||||
forceVal =
|
||||
Math.abs(forceVal) > 90 ? normaliseAngle(forceVal - 180) : forceVal
|
||||
}
|
||||
@ -112,7 +116,7 @@ export async function applyConstraintAngleLength({
|
||||
)
|
||||
if (
|
||||
isReferencingYAxisAngle ||
|
||||
(isReferencingXAxisAngle && calcIdentifier.name !== '_0')
|
||||
(isReferencingXAxisAngle && calcIdentifier.name !== 'ZERO')
|
||||
) {
|
||||
finalValue = createBinaryExpressionWithUnary([calcIdentifier, finalValue])
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { faBars, faBug, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { paths } from '../Router'
|
||||
import { paths } from 'lib/paths'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Dialog } from '@headlessui/react'
|
||||
import { useState } from 'react'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { useKclContext } from 'lang/KclSinglton'
|
||||
import { useKclContext } from 'lang/KclSingleton'
|
||||
|
||||
export function WasmErrBanner() {
|
||||
const [isBannerDismissed, setBannerDismissed] = useState(false)
|
||||
|
503
src/editor/copilot/index.ts
Normal file
@ -0,0 +1,503 @@
|
||||
/// Thanks to the Cursor folks for their heavy lifting here.
|
||||
import { indentUnit } from '@codemirror/language'
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from '@codemirror/view'
|
||||
import {
|
||||
Annotation,
|
||||
EditorState,
|
||||
Extension,
|
||||
Facet,
|
||||
Prec,
|
||||
StateEffect,
|
||||
StateField,
|
||||
Transaction,
|
||||
} from '@codemirror/state'
|
||||
import { completionStatus } from '@codemirror/autocomplete'
|
||||
import { docPathFacet, offsetToPos, posToOffset } from 'editor/lsp/util'
|
||||
import { LanguageServerPlugin } from 'editor/lsp/plugin'
|
||||
import { LanguageServerOptions } from 'editor/lsp/plugin'
|
||||
import { LanguageServerClient } from 'editor/lsp'
|
||||
|
||||
// Create Facet for the current docPath
|
||||
export const docPath = Facet.define<string, string>({
|
||||
combine(value: readonly string[]) {
|
||||
return value[value.length - 1]
|
||||
},
|
||||
})
|
||||
|
||||
export const relDocPath = Facet.define<string, string>({
|
||||
combine(value: readonly string[]) {
|
||||
return value[value.length - 1]
|
||||
},
|
||||
})
|
||||
|
||||
const ghostMark = Decoration.mark({ class: 'cm-ghostText' })
|
||||
|
||||
interface Suggestion {
|
||||
text: string
|
||||
displayText: string
|
||||
cursorPos: number
|
||||
startPos: number
|
||||
endPos: number
|
||||
endReplacement: number
|
||||
uuid: string
|
||||
}
|
||||
|
||||
// Effects to tell StateEffect what to do with GhostText
|
||||
const addSuggestion = StateEffect.define<Suggestion>()
|
||||
const acceptSuggestion = StateEffect.define<null>()
|
||||
const clearSuggestion = StateEffect.define<null>()
|
||||
const typeFirst = StateEffect.define<number>()
|
||||
|
||||
interface CompletionState {
|
||||
ghostText: GhostText | null
|
||||
}
|
||||
interface GhostText {
|
||||
text: string
|
||||
displayText: string
|
||||
displayPos: number
|
||||
startPos: number
|
||||
endGhostText: number
|
||||
endReplacement: number
|
||||
endPos: number
|
||||
decorations: DecorationSet
|
||||
weirdInsert: boolean
|
||||
uuid: string
|
||||
}
|
||||
|
||||
export const completionDecoration = StateField.define<CompletionState>({
|
||||
create(_state: EditorState) {
|
||||
return { ghostText: null }
|
||||
},
|
||||
update(state: CompletionState, transaction: Transaction) {
|
||||
for (const effect of transaction.effects) {
|
||||
if (effect.is(addSuggestion)) {
|
||||
// When adding a suggestion, we set th ghostText
|
||||
const {
|
||||
text,
|
||||
displayText,
|
||||
endReplacement,
|
||||
cursorPos,
|
||||
startPos,
|
||||
endPos,
|
||||
uuid,
|
||||
} = effect.value
|
||||
const endGhostText = cursorPos + displayText.length
|
||||
const decorations = Decoration.set([
|
||||
ghostMark.range(cursorPos, endGhostText),
|
||||
])
|
||||
return {
|
||||
ghostText: {
|
||||
text,
|
||||
displayText,
|
||||
startPos,
|
||||
endPos,
|
||||
decorations,
|
||||
displayPos: cursorPos,
|
||||
endReplacement,
|
||||
endGhostText,
|
||||
weirdInsert: false,
|
||||
uuid,
|
||||
},
|
||||
}
|
||||
} else if (effect.is(acceptSuggestion)) {
|
||||
if (state.ghostText) {
|
||||
return { ghostText: null }
|
||||
}
|
||||
} else if (effect.is(typeFirst)) {
|
||||
const numChars = effect.value
|
||||
if (state.ghostText && !state.ghostText.weirdInsert) {
|
||||
let {
|
||||
text,
|
||||
displayText,
|
||||
displayPos,
|
||||
startPos,
|
||||
endPos,
|
||||
endGhostText,
|
||||
decorations,
|
||||
endReplacement,
|
||||
uuid,
|
||||
} = state.ghostText
|
||||
|
||||
displayPos += numChars
|
||||
|
||||
displayText = displayText.slice(numChars)
|
||||
|
||||
if (startPos === endGhostText) {
|
||||
return { ghostText: null }
|
||||
} else {
|
||||
decorations = Decoration.set([
|
||||
ghostMark.range(displayPos, endGhostText),
|
||||
])
|
||||
return {
|
||||
ghostText: {
|
||||
text,
|
||||
displayText,
|
||||
startPos,
|
||||
endPos,
|
||||
decorations,
|
||||
endGhostText,
|
||||
endReplacement,
|
||||
uuid,
|
||||
displayPos,
|
||||
weirdInsert: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (effect.is(clearSuggestion)) {
|
||||
return { ghostText: null }
|
||||
}
|
||||
}
|
||||
|
||||
// if (transaction.docChanged && state.ghostText) {
|
||||
// if (transaction.
|
||||
// onsole.log({changes: transaction.changes, transaction})
|
||||
// const newGhostText = state.ghostText.decorations.map(transaction.changes)
|
||||
// return {ghostText: {...state.ghostText, decorations: newGhostText}};
|
||||
// }
|
||||
|
||||
return state
|
||||
},
|
||||
provide: (field) =>
|
||||
EditorView.decorations.from(field, (value) =>
|
||||
value.ghostText ? value.ghostText.decorations : Decoration.none
|
||||
),
|
||||
})
|
||||
|
||||
const copilotEvent = Annotation.define<null>()
|
||||
|
||||
/****************************************************************************
|
||||
************************* COMMANDS ******************************************
|
||||
*****************************************************************************/
|
||||
|
||||
const acceptSuggestionCommand = (
|
||||
copilotClient: LanguageServerClient,
|
||||
view: EditorView
|
||||
) => {
|
||||
// We delete the ghost text and insert the suggestion.
|
||||
// We also set the cursor to the end of the suggestion.
|
||||
const ghostText = view.state.field(completionDecoration)!.ghostText
|
||||
if (!ghostText) {
|
||||
return false
|
||||
}
|
||||
|
||||
const ghostTextStart = ghostText.displayPos
|
||||
const ghostTextEnd = ghostText.endGhostText
|
||||
|
||||
const actualTextStart = ghostText.startPos
|
||||
const actualTextEnd = ghostText.endPos
|
||||
|
||||
const replacementEnd = ghostText.endReplacement
|
||||
|
||||
const suggestion = ghostText.text
|
||||
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: ghostTextStart,
|
||||
to: ghostTextEnd,
|
||||
insert: '',
|
||||
},
|
||||
// selection: {anchor: actualTextEnd},
|
||||
effects: acceptSuggestion.of(null),
|
||||
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
|
||||
})
|
||||
|
||||
const tmpTextEnd = replacementEnd - (ghostTextEnd - ghostTextStart)
|
||||
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: actualTextStart,
|
||||
to: tmpTextEnd,
|
||||
insert: suggestion,
|
||||
},
|
||||
selection: { anchor: actualTextEnd },
|
||||
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(true)],
|
||||
})
|
||||
|
||||
copilotClient.accept(ghostText.uuid)
|
||||
return true
|
||||
}
|
||||
export const rejectSuggestionCommand = (
|
||||
copilotClient: LanguageServerClient,
|
||||
view: EditorView
|
||||
) => {
|
||||
// We delete the suggestion, then carry through with the original keypress
|
||||
const ghostText = view.state.field(completionDecoration)!.ghostText
|
||||
if (!ghostText) {
|
||||
return false
|
||||
}
|
||||
|
||||
const ghostTextStart = ghostText.displayPos
|
||||
const ghostTextEnd = ghostText.endGhostText
|
||||
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: ghostTextStart,
|
||||
to: ghostTextEnd,
|
||||
insert: '',
|
||||
},
|
||||
effects: clearSuggestion.of(null),
|
||||
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
|
||||
})
|
||||
|
||||
copilotClient.reject()
|
||||
return false
|
||||
}
|
||||
|
||||
const sameKeyCommand = (
|
||||
copilotClient: LanguageServerClient,
|
||||
view: EditorView,
|
||||
key: string
|
||||
) => {
|
||||
// When we type a key that is the same as the first letter of the suggestion, we delete the first letter of the suggestion and carry through with the original keypress
|
||||
const ghostText = view.state.field(completionDecoration)!.ghostText
|
||||
if (!ghostText) {
|
||||
return false
|
||||
}
|
||||
const ghostTextStart = ghostText.displayPos
|
||||
const indent = view.state.facet(indentUnit)
|
||||
|
||||
if (key === 'Tab' && ghostText.displayText.startsWith(indent)) {
|
||||
view.dispatch({
|
||||
selection: { anchor: ghostTextStart + indent.length },
|
||||
effects: typeFirst.of(indent.length),
|
||||
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
|
||||
})
|
||||
return true
|
||||
} else if (key === 'Tab') {
|
||||
return acceptSuggestionCommand(copilotClient, view)
|
||||
} else if (ghostText.weirdInsert || key !== ghostText.displayText[0]) {
|
||||
return rejectSuggestionCommand(copilotClient, view)
|
||||
} else if (ghostText.displayText.length === 1) {
|
||||
return acceptSuggestionCommand(copilotClient, view)
|
||||
} else {
|
||||
// Use this to delete the first letter of the suggestion
|
||||
view.dispatch({
|
||||
selection: { anchor: ghostTextStart + 1 },
|
||||
effects: typeFirst.of(1),
|
||||
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)],
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const completionPlugin = (copilotClient: LanguageServerClient) =>
|
||||
EditorView.domEventHandlers({
|
||||
keydown(event, view) {
|
||||
if (
|
||||
event.key !== 'Shift' &&
|
||||
event.key !== 'Control' &&
|
||||
event.key !== 'Alt' &&
|
||||
event.key !== 'Meta'
|
||||
) {
|
||||
return sameKeyCommand(copilotClient, view, event.key)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
mousedown(event, view) {
|
||||
return rejectSuggestionCommand(copilotClient, view)
|
||||
},
|
||||
})
|
||||
|
||||
const viewCompletionPlugin = (copilotClient: LanguageServerClient) =>
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.focusChanged) {
|
||||
rejectSuggestionCommand(copilotClient, update.view)
|
||||
}
|
||||
})
|
||||
// A view plugin that requests completions from the server after a delay
|
||||
const completionRequester = (client: LanguageServerClient) => {
|
||||
let timeout: any = null
|
||||
let lastPos = 0
|
||||
|
||||
const badUpdate = (update: ViewUpdate) => {
|
||||
for (const tr of update.transactions) {
|
||||
if (tr.annotation(copilotEvent) !== undefined) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
const containsGhostText = (update: ViewUpdate) => {
|
||||
return update.state.field(completionDecoration).ghostText != null
|
||||
}
|
||||
const autocompleting = (update: ViewUpdate) => {
|
||||
return completionStatus(update.state) === 'active'
|
||||
}
|
||||
const notFocused = (update: ViewUpdate) => {
|
||||
return !update.view.hasFocus
|
||||
}
|
||||
|
||||
return EditorView.updateListener.of((update: ViewUpdate) => {
|
||||
if (
|
||||
update.docChanged &&
|
||||
!update.transactions.some((tr) =>
|
||||
tr.effects.some((e) => e.is(acceptSuggestion) || e.is(clearSuggestion))
|
||||
)
|
||||
) {
|
||||
// Cancel the previous timeout
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
if (
|
||||
badUpdate(update) ||
|
||||
containsGhostText(update) ||
|
||||
autocompleting(update) ||
|
||||
notFocused(update)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the current position and source
|
||||
const state = update.state
|
||||
const pos = state.selection.main.head
|
||||
const source = state.doc.toString()
|
||||
|
||||
const path = state.facet(docPath)
|
||||
const relativePath = state.facet(relDocPath)
|
||||
const languageId = 'kcl'
|
||||
|
||||
// Set a new timeout to request completion
|
||||
timeout = setTimeout(async () => {
|
||||
// Check if the position has changed
|
||||
if (pos === lastPos) {
|
||||
// Request completion from the server
|
||||
try {
|
||||
const completionResult = await client.getCompletion({
|
||||
doc: {
|
||||
source,
|
||||
tabSize: state.facet(EditorState.tabSize),
|
||||
indentSize: 1,
|
||||
insertSpaces: true,
|
||||
path,
|
||||
uri: `file://${path}`,
|
||||
relativePath,
|
||||
languageId,
|
||||
position: offsetToPos(state.doc, pos),
|
||||
},
|
||||
})
|
||||
|
||||
if (completionResult.completions.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let {
|
||||
text,
|
||||
displayText,
|
||||
range: { start },
|
||||
position,
|
||||
uuid,
|
||||
} = completionResult.completions[0]
|
||||
|
||||
const startPos = posToOffset(state.doc, {
|
||||
line: start.line,
|
||||
character: start.character,
|
||||
})!
|
||||
|
||||
const endGhostPos =
|
||||
posToOffset(state.doc, {
|
||||
line: position.line,
|
||||
character: position.character,
|
||||
})! + displayText.length
|
||||
// EndPos is the position that marks the complete end
|
||||
// of what is to be replaced when we accept a completion
|
||||
// result
|
||||
const endPos = startPos + text.length
|
||||
|
||||
// Check if the position is still the same
|
||||
if (
|
||||
pos === lastPos &&
|
||||
completionStatus(update.view.state) !== 'active' &&
|
||||
update.view.hasFocus
|
||||
) {
|
||||
// Dispatch an effect to add the suggestion
|
||||
// If the completion starts before the end of the line, check the end of the line with the end of the completion
|
||||
const line = update.view.state.doc.lineAt(pos)
|
||||
if (line.to !== pos) {
|
||||
const ending = update.view.state.doc.sliceString(pos, line.to)
|
||||
if (displayText.endsWith(ending)) {
|
||||
displayText = displayText.slice(
|
||||
0,
|
||||
displayText.length - ending.length
|
||||
)
|
||||
} else if (displayText.includes(ending)) {
|
||||
// Remove the ending
|
||||
update.view.dispatch({
|
||||
changes: {
|
||||
from: pos,
|
||||
to: line.to,
|
||||
insert: '',
|
||||
},
|
||||
selection: { anchor: pos },
|
||||
effects: typeFirst.of(ending.length),
|
||||
annotations: [
|
||||
copilotEvent.of(null),
|
||||
Transaction.addToHistory.of(false),
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
update.view.dispatch({
|
||||
changes: {
|
||||
from: pos,
|
||||
to: pos,
|
||||
insert: displayText,
|
||||
},
|
||||
effects: [
|
||||
addSuggestion.of({
|
||||
displayText,
|
||||
endReplacement: endGhostPos,
|
||||
text,
|
||||
cursorPos: pos,
|
||||
startPos,
|
||||
endPos,
|
||||
uuid,
|
||||
}),
|
||||
],
|
||||
annotations: [
|
||||
copilotEvent.of(null),
|
||||
Transaction.addToHistory.of(false),
|
||||
],
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('copilot completion failed', error)
|
||||
// Javascript wait for 500ms for some reason is necessary here.
|
||||
// TODO - FIGURE OUT WHY THIS RESOLVES THE BUG
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
}
|
||||
}
|
||||
}, 150)
|
||||
// Update the last position
|
||||
lastPos = pos
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function copilotServer(options: LanguageServerOptions) {
|
||||
let plugin: LanguageServerPlugin
|
||||
return ViewPlugin.define(
|
||||
(view) =>
|
||||
(plugin = new LanguageServerPlugin(view, options.allowHTMLContent))
|
||||
)
|
||||
}
|
||||
|
||||
export const copilotBundle = (options: LanguageServerOptions): Extension => [
|
||||
docPath.of(options.documentUri.split('/').pop()!),
|
||||
docPathFacet.of(options.documentUri.split('/').pop()!),
|
||||
relDocPath.of(options.documentUri.replace('file://', '')),
|
||||
completionDecoration,
|
||||
Prec.highest(completionPlugin(options.client)),
|
||||
Prec.highest(viewCompletionPlugin(options.client)),
|
||||
completionRequester(options.client),
|
||||
copilotServer(options),
|
||||
]
|
@ -3,6 +3,67 @@ import Client from './client'
|
||||
import { LanguageServerPlugin } from './plugin'
|
||||
import { SemanticToken, deserializeTokens } from './semantic_tokens'
|
||||
|
||||
export interface CopilotGetCompletionsParams {
|
||||
doc: {
|
||||
source: string
|
||||
tabSize: number
|
||||
indentSize: number
|
||||
insertSpaces: boolean
|
||||
path: string
|
||||
uri: string
|
||||
relativePath: string
|
||||
languageId: string
|
||||
position: {
|
||||
line: number
|
||||
character: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface CopilotGetCompletionsResult {
|
||||
completions: {
|
||||
text: string
|
||||
position: {
|
||||
line: number
|
||||
character: number
|
||||
}
|
||||
uuid: string
|
||||
range: {
|
||||
start: {
|
||||
line: number
|
||||
character: number
|
||||
}
|
||||
end: {
|
||||
line: number
|
||||
character: number
|
||||
}
|
||||
}
|
||||
displayText: string
|
||||
point: {
|
||||
line: number
|
||||
character: number
|
||||
}
|
||||
region: {
|
||||
start: {
|
||||
line: number
|
||||
character: number
|
||||
}
|
||||
end: {
|
||||
line: number
|
||||
character: number
|
||||
}
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
||||
interface CopilotAcceptCompletionParams {
|
||||
uuid: string
|
||||
}
|
||||
|
||||
interface CopilotRejectCompletionParams {
|
||||
uuids: string[]
|
||||
}
|
||||
|
||||
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/
|
||||
|
||||
// Client to server then server to client
|
||||
@ -17,6 +78,9 @@ interface LSPRequestMap {
|
||||
LSP.SemanticTokensParams,
|
||||
LSP.SemanticTokens
|
||||
]
|
||||
getCompletions: [CopilotGetCompletionsParams, CopilotGetCompletionsResult]
|
||||
notifyAccepted: [CopilotAcceptCompletionParams, any]
|
||||
notifyRejected: [CopilotRejectCompletionParams, any]
|
||||
}
|
||||
|
||||
// Client to server
|
||||
@ -55,6 +119,7 @@ export class LanguageServerClient {
|
||||
|
||||
private isUpdatingSemanticTokens: boolean = false
|
||||
private semanticTokens: SemanticToken[] = []
|
||||
private queuedUids: string[] = []
|
||||
|
||||
constructor(options: LanguageServerClientOptions) {
|
||||
this.plugins = []
|
||||
@ -62,6 +127,7 @@ export class LanguageServerClient {
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.queuedUids = []
|
||||
this.initializePromise = this.initialize()
|
||||
}
|
||||
|
||||
@ -145,6 +211,33 @@ export class LanguageServerClient {
|
||||
return this.client.notify(method, params)
|
||||
}
|
||||
|
||||
async getCompletion(params: CopilotGetCompletionsParams) {
|
||||
const response = await this.request('getCompletions', params)
|
||||
//
|
||||
this.queuedUids = [...response.completions.map((c) => c.uuid)]
|
||||
return response
|
||||
}
|
||||
|
||||
async accept(uuid: string) {
|
||||
const badUids = this.queuedUids.filter((u) => u !== uuid)
|
||||
this.queuedUids = []
|
||||
await this.acceptCompletion({ uuid })
|
||||
await this.rejectCompletions({ uuids: badUids })
|
||||
}
|
||||
|
||||
async reject() {
|
||||
const badUids = this.queuedUids
|
||||
this.queuedUids = []
|
||||
return await this.rejectCompletions({ uuids: badUids })
|
||||
}
|
||||
|
||||
async acceptCompletion(params: CopilotAcceptCompletionParams) {
|
||||
return await this.request('notifyAccepted', params)
|
||||
}
|
||||
async rejectCompletions(params: CopilotRejectCompletionParams) {
|
||||
return await this.request('notifyRejected', params)
|
||||
}
|
||||
|
||||
private processNotification(notification: Notification) {
|
||||
for (const plugin of this.plugins) plugin.processNotification(notification)
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
NodeSet,
|
||||
} from '@lezer/common'
|
||||
import { LanguageServerClient } from '.'
|
||||
import { posToOffset } from './plugin'
|
||||
import { posToOffset } from 'editor/lsp/util'
|
||||
import { SemanticToken } from './semantic_tokens'
|
||||
import { DocInput } from '@codemirror/language'
|
||||
import { tags, styleTags } from '@lezer/highlight'
|
||||
|
@ -22,10 +22,10 @@ import type {
|
||||
} from '@codemirror/autocomplete'
|
||||
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
|
||||
import type { ViewUpdate, PluginValue } from '@codemirror/view'
|
||||
import type { Text } from '@codemirror/state'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import { LanguageServerClient, Notification } from '.'
|
||||
import { Marked } from '@ts-stack/markdown'
|
||||
import { offsetToPos, posToOffset } from 'editor/lsp/util'
|
||||
|
||||
const changesDelay = 500
|
||||
|
||||
@ -343,24 +343,6 @@ export function kclPlugin(options: LanguageServerOptions) {
|
||||
]
|
||||
}
|
||||
|
||||
export function posToOffset(
|
||||
doc: Text,
|
||||
pos: { line: number; character: number }
|
||||
): number | undefined {
|
||||
if (pos.line >= doc.lines) return
|
||||
const offset = doc.line(pos.line + 1).from + pos.character
|
||||
if (offset > doc.length) return
|
||||
return offset
|
||||
}
|
||||
|
||||
function offsetToPos(doc: Text, offset: number) {
|
||||
const line = doc.lineAt(offset)
|
||||
return {
|
||||
line: line.number - 1,
|
||||
character: offset - line.from,
|
||||
}
|
||||
}
|
||||
|
||||
function formatContents(
|
||||
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
|
||||
): string {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import init, {
|
||||
copilot_lsp_run,
|
||||
InitOutput,
|
||||
lsp_run,
|
||||
kcl_lsp_run,
|
||||
ServerConfig,
|
||||
} from '../../wasm-lib/pkg/wasm_lib'
|
||||
import { FromServer, IntoServer } from './codec'
|
||||
@ -29,8 +30,15 @@ export default class Server {
|
||||
return server
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
async start(type_: 'kcl' | 'copilot', token?: string): Promise<void> {
|
||||
const config = new ServerConfig(this.#intoServer, this.#fromServer)
|
||||
await lsp_run(config)
|
||||
if (type_ === 'copilot') {
|
||||
if (!token) {
|
||||
throw new Error('auth token is required for copilot')
|
||||
}
|
||||
await copilot_lsp_run(config, token)
|
||||
} else if (type_ === 'kcl') {
|
||||
await kcl_lsp_run(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
23
src/editor/lsp/util.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Facet, Text } from '@codemirror/state'
|
||||
|
||||
export function posToOffset(
|
||||
doc: Text,
|
||||
pos: { line: number; character: number }
|
||||
): number | undefined {
|
||||
if (pos.line >= doc.lines) return
|
||||
const offset = doc.line(pos.line + 1).from + pos.character
|
||||
if (offset > doc.length) return
|
||||
return offset
|
||||
}
|
||||
|
||||
export function offsetToPos(doc: Text, offset: number) {
|
||||
const line = doc.lineAt(offset)
|
||||
return {
|
||||
line: line.number - 1,
|
||||
character: offset - line.from,
|
||||
}
|
||||
}
|
||||
|
||||
export const docPathFacet = Facet.define<string, string>({
|
||||
combine: (values) => values[values.length - 1],
|
||||
})
|
@ -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'
|
||||
|
||||
export function useAbsoluteFilePath() {
|
||||
|
@ -15,6 +15,7 @@ export function useEngineConnectionSubscriptions() {
|
||||
if (!engineCommandManager) return
|
||||
|
||||
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',
|
||||
callback: ({ data }) => {
|
||||
if (data?.entity_id) {
|
||||
@ -46,6 +47,6 @@ export function useEngineConnectionSubscriptions() {
|
||||
engineCommandManager,
|
||||
setHighlightRange,
|
||||
highlightRange,
|
||||
context.sketchEnginePathId,
|
||||
context?.sketchEnginePathId,
|
||||
])
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { parse } from '../lang/wasm'
|
||||
import { useStore } from '../useStore'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { deferExecution } from 'lib/utils'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
|
||||
export function useSetupEngineManager(
|
||||
streamRef: React.RefObject<HTMLDivElement>,
|
||||
|
@ -2,7 +2,7 @@ import {
|
||||
SetVarNameModal,
|
||||
createSetVarNameModal,
|
||||
} from 'components/SetVarNameModal'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
import { moveValueIntoNewVariable } from 'lang/modifyAst'
|
||||
import { isNodeSafeToReplace } from 'lang/queryAst'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
@ -219,3 +219,8 @@ code {
|
||||
word-break: normal;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.cm-ghostText,
|
||||
.cm-ghostText * {
|
||||
color: rgb(120, 120, 120, 0.8) !important;
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import { Router } from './Router'
|
||||
import { HotkeysProvider } from 'react-hotkeys-hook'
|
||||
|
||||
// uncomment for xstate inspector
|
||||
// import { DEV } from 'env'
|
||||
// import { inspect } from '@xstate/inspect'
|
||||
// if (DEV)
|
||||
// inspect({
|
||||
// iframe: false,
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
} from './std/engineConnection'
|
||||
import { deferExecution } from 'lib/utils'
|
||||
import {
|
||||
CallExpression,
|
||||
initPromise,
|
||||
parse,
|
||||
PathToNode,
|
||||
@ -17,7 +18,7 @@ import {
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { getNodeFromPath } from './queryAst'
|
||||
import { IndexLoaderData } from 'Router'
|
||||
import { type IndexLoaderData } from 'lib/types'
|
||||
import { Params, useLoaderData } from 'react-router-dom'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { writeTextFile } from '@tauri-apps/api/fs'
|
||||
@ -98,7 +99,7 @@ class KclManager {
|
||||
})
|
||||
})
|
||||
} else {
|
||||
localStorage.setItem(PERSIST_CODE_TOKEN, code)
|
||||
safteLSSetItem(PERSIST_CODE_TOKEN, code)
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,10 +111,6 @@ class KclManager {
|
||||
this._programMemoryCallBack(programMemory)
|
||||
}
|
||||
|
||||
get defaultPlanes() {
|
||||
return this?.engineCommandManager?.defaultPlanes
|
||||
}
|
||||
|
||||
get logs() {
|
||||
return this._logs
|
||||
}
|
||||
@ -157,22 +154,24 @@ class KclManager {
|
||||
this.code = ''
|
||||
return
|
||||
}
|
||||
const storedCode = localStorage.getItem(PERSIST_CODE_TOKEN)
|
||||
const storedCode = safeLSGetItem(PERSIST_CODE_TOKEN) || ''
|
||||
// TODO #819 remove zustand persistence logic in a few months
|
||||
// short term migration, shouldn't make a difference for tauri app users
|
||||
// anyway since that's filesystem based.
|
||||
const zustandStore = JSON.parse(localStorage.getItem('store') || '{}')
|
||||
const zustandStore = JSON.parse(safeLSGetItem('store') || '{}')
|
||||
if (storedCode === null && zustandStore?.state?.code) {
|
||||
this.code = zustandStore.state.code
|
||||
localStorage.setItem(PERSIST_CODE_TOKEN, this._code)
|
||||
safteLSSetItem(PERSIST_CODE_TOKEN, this._code)
|
||||
zustandStore.state.code = ''
|
||||
localStorage.setItem('store', JSON.stringify(zustandStore))
|
||||
safteLSSetItem('store', JSON.stringify(zustandStore))
|
||||
} else if (storedCode === null) {
|
||||
console.log('stored brack thing')
|
||||
this.code = bracket
|
||||
} else {
|
||||
this.code = storedCode
|
||||
}
|
||||
this.ensureWasmInit().then(() => {
|
||||
this.ast = this.safeParse(this.code) || this.ast
|
||||
})
|
||||
}
|
||||
registerCallBacks({
|
||||
setCode,
|
||||
@ -235,7 +234,6 @@ class KclManager {
|
||||
const { logs, errors, programMemory } = await executeAst({
|
||||
ast,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
defaultPlanes: this.defaultPlanes,
|
||||
})
|
||||
this.isExecuting = false
|
||||
this.logs = logs
|
||||
@ -251,13 +249,20 @@ class KclManager {
|
||||
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()
|
||||
const newCode = recast(ast)
|
||||
const newAst = this.safeParse(newCode)
|
||||
if (!newAst) return
|
||||
await this?.engineCommandManager?.waitForReady
|
||||
if (updateCode) {
|
||||
if (updates !== 'none') {
|
||||
this.setCode(recast(ast))
|
||||
}
|
||||
this._ast = { ...newAst }
|
||||
@ -265,22 +270,38 @@ class KclManager {
|
||||
const { logs, errors, programMemory } = await executeAst({
|
||||
ast: newAst,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
defaultPlanes: this.defaultPlanes,
|
||||
useFakeExecutor: true,
|
||||
})
|
||||
this._logs = logs
|
||||
this._kclErrors = errors
|
||||
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) {
|
||||
await this.ensureWasmInit()
|
||||
await this?.engineCommandManager?.waitForReady
|
||||
if (!this?.engineCommandManager?.planesInitialized()) return
|
||||
const result = await executeCode({
|
||||
engineCommandManager,
|
||||
code: code || this._code,
|
||||
lastAst: this._ast,
|
||||
defaultPlanes: this.defaultPlanes,
|
||||
force: false,
|
||||
})
|
||||
if (!result.isChange) return
|
||||
@ -366,25 +387,29 @@ class KclManager {
|
||||
// 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
|
||||
// instead.
|
||||
await this.executeAstMock(astWithUpdatedSource, true)
|
||||
await this.executeAstMock(astWithUpdatedSource, { updates: 'code' })
|
||||
}
|
||||
return returnVal
|
||||
}
|
||||
|
||||
get defaultPlanes() {
|
||||
return this?.engineCommandManager?.defaultPlanes
|
||||
}
|
||||
|
||||
getPlaneId(axis: 'xy' | 'xz' | 'yz'): string {
|
||||
return this.defaultPlanes[axis]
|
||||
}
|
||||
|
||||
showPlanes() {
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, false)
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, false)
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, false)
|
||||
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() {
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, true)
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
|
||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, true)
|
||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
|
||||
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -452,3 +477,13 @@ export function KclContextProvider({
|
||||
</KclContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function safeLSGetItem(key: string) {
|
||||
if (typeof window === 'undefined') return null
|
||||
return localStorage?.getItem(key)
|
||||
}
|
||||
|
||||
function safteLSSetItem(key: string, value: string) {
|
||||
if (typeof window === 'undefined') return
|
||||
localStorage?.setItem(key, value)
|
||||
}
|
@ -1653,7 +1653,7 @@ describe('parsing errors', () => {
|
||||
|
||||
let _theError
|
||||
try {
|
||||
const result = expect(parse(code))
|
||||
let _ = expect(parse(code))
|
||||
} catch (e) {
|
||||
_theError = e
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ show(mySketch001)`
|
||||
expect(shown).toEqual([
|
||||
{
|
||||
type: 'SketchGroup',
|
||||
on: expect.any(Object),
|
||||
start: {
|
||||
to: [0, 0],
|
||||
from: [0, 0],
|
||||
@ -33,7 +34,7 @@ show(mySketch001)`
|
||||
},
|
||||
value: [
|
||||
{
|
||||
type: 'toPoint',
|
||||
type: 'ToPoint',
|
||||
name: '',
|
||||
to: [-1.59, -1.54],
|
||||
from: [0, 0],
|
||||
@ -43,7 +44,7 @@ show(mySketch001)`
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'toPoint',
|
||||
type: 'ToPoint',
|
||||
to: [0.46, -5.82],
|
||||
from: [-1.59, -1.54],
|
||||
name: '',
|
||||
@ -55,8 +56,11 @@ show(mySketch001)`
|
||||
],
|
||||
position: [0, 0, 0],
|
||||
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),
|
||||
planeId: expect.any(String),
|
||||
entityId: expect.any(String),
|
||||
__meta: [{ sourceRange: [46, 71] }],
|
||||
},
|
||||
])
|
||||
@ -85,6 +89,11 @@ show(mySketch001)`
|
||||
height: 2,
|
||||
position: [0, 0, 0],
|
||||
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] }],
|
||||
},
|
||||
])
|
||||
@ -127,6 +136,11 @@ show(theExtrude, sk2)`
|
||||
height: 2,
|
||||
position: [0, 0, 0],
|
||||
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] }],
|
||||
},
|
||||
{
|
||||
@ -136,6 +150,12 @@ show(theExtrude, sk2)`
|
||||
height: 2,
|
||||
position: [0, 0, 0],
|
||||
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] }],
|
||||
},
|
||||
])
|
||||
|