@pierremtb

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

View File

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

View File

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

View File

@ -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
View File

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

View File

@ -46,6 +46,7 @@ jobs:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn xstate:typegen
- run: yarn tsc
@ -253,6 +254,7 @@ jobs:
- name: Run e2e tests (linux only)
if: matrix.os == 'ubuntu-latest'
run: |
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
@ -335,17 +337,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*'
@ -353,13 +355,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
View File

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

View File

@ -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

View File

@ -182,7 +182,7 @@ For more information on fuzzing you can check out
First time running plawright locally, you'll need to add the secrets file
```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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -498,7 +498,7 @@
}
],
"asset": {
"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": {

View File

@ -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.);

View File

@ -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

View File

@ -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(
await u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
),
u.waitForDefaultPlanesVisibilityChange(),
])
)
// 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', %)], %)`)
})
@ -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,40 @@ test('Can create sketches on all planes and their back sides', async ({
const codeTemplate = (
plane = 'XY',
sign = ''
rounded = false
) => `const part001 = startSketchOn('${plane}')
|> startProfileAt([${sign}6.88, -9.29], %)
|> line([${sign}6.95, 0], %)`
|> startProfileAt([28.91, -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', true),
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 +349,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 +439,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 +479,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 +511,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 +646,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 +661,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 +683,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)
})
})
})

View File

@ -40,19 +40,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 +51,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 +64,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 +78,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 +90,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 +100,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 +143,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 +156,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 +170,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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View File

@ -80,10 +80,21 @@ export function getUtils(page: Page) {
waitForAuthSkipAppStart: () => waitForPageLoad(page),
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 () => {

View File

@ -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.50",
"@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^1.2.6",
@ -24,6 +24,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",
@ -47,6 +48,7 @@
"react-router-dom": "^6.14.2",
"sketch-helpers": "^0.0.4",
"swr": "^2.2.2",
"three": "^0.160.0",
"toml": "^3.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
@ -84,7 +86,9 @@
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"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",
@ -115,6 +119,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",
@ -126,21 +131,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
View File

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

View File

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

View File

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

BIN
public/discord-avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

1180
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -19,10 +19,11 @@ import {
} from '@fortawesome/free-solid-svg-icons'
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 { onboardingPaths } from 'routes/Onboarding/paths'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { CodeMenu } from 'components/CodeMenu'
@ -31,9 +32,13 @@ 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 {
@ -71,6 +76,13 @@ export function App() {
useHotkeys('shift + e', () => togglePane('kclErrors'))
useHotkeys('shift + d', () => togglePane('debug'))
useHotkeys('esc', () => send('Cancel'))
useHotkeys(
isTauri() ? 'mod + ,' : 'shift + mod + ,',
() => navigate(filePath + paths.SETTINGS),
{
splitKey: '|',
}
)
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
(p) => p === onboardingStatus
@ -83,10 +95,12 @@ export function App() {
useEngineConnectionSubscriptions()
const debounceSocketSend = throttle<EngineCommand>((message) => {
void engineCommandManager.sendSceneCommand(message)
}, 16)
engineCommandManager.sendSceneCommand(message)
}, 1000 / 15)
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
e.nativeEvent.preventDefault()
if (state.matches('Sketch')) {
return
}
const { x, y } = getNormalisedCoordinates({
clientX: e.clientX,
@ -97,16 +111,6 @@ 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: {
@ -115,19 +119,7 @@ export function App() {
},
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
@ -238,6 +230,7 @@ export function App() {
open={openPanes.includes('debug')}
/>
)}
{/* <CamToggle /> */}
</div>
)
}

View File

@ -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'
@ -48,9 +45,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({
@ -84,43 +84,8 @@ if (VITE_KC_SENTRY_DSN && !TEST) {
})
}
const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => {
return Object.fromEntries(
Object.entries(routesObject).map(([constName, path]) => [
constName,
prepend + path,
])
)
}
export const paths = {
INDEX: '/',
HOME: '/home',
FILE: '/file',
SETTINGS: '/settings',
SIGN_IN: '/signin',
ONBOARDING: prependRoutes(onboardingPaths)(
'/onboarding'
) as typeof onboardingPaths,
}
export const BROWSER_FILE_NAME = 'new'
export type IndexLoaderData = {
code: string | null
project?: ProjectWithEntryPointMetadata
file?: FileHandle
}
export type ProjectWithEntryPointMetadata = FileHandle & {
entrypointMetadata: FileInfo
}
export type HomeLoaderData = {
projects: ProjectWithEntryPointMetadata[]
newDefaultDirectory?: string
}
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
const addGlobalContextToElements = (
@ -152,18 +117,18 @@ const router = createBrowserRouter(
{
path: paths.FILE + '/:id',
element: (
<KclContextProvider>
<Auth>
<FileMachineProvider>
<KclContextProvider>
<ModelingMachineProvider>
<Outlet />
<App />
</ModelingMachineProvider>
<WasmErrBanner />
</KclContextProvider>
</FileMachineProvider>
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
</Auth>
</KclContextProvider>
),
id: paths.FILE,
loader: async ({
@ -217,6 +182,10 @@ const router = createBrowserRouter(
const children = await readDir(projectPath)
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: {
@ -256,7 +225,7 @@ const router = createBrowserRouter(
<Home />
</Auth>
),
loader: async () => {
loader: async (): Promise<HomeLoaderData | Response> => {
if (!isTauri()) {
return redirect(paths.FILE + '/' + BROWSER_FILE_NAME)
}

View File

@ -89,15 +89,16 @@ export const Toolbar = () => {
</li>
)}
{state.matches('Sketch') && !state.matches('idle') && (
<li className="contents">
<>
<li className="contents" key="line-button">
<ActionButton
Element="button"
onClick={() =>
state.matches('Sketch.Line Tool')
state?.matches('Sketch.Line tool')
? send('CancelSketch')
: send('Equip tool')
: send('Equip Line tool')
}
aria-pressed={state.matches('Sketch.Line Tool')}
aria-pressed={state?.matches('Sketch.Line tool')}
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
icon={{
icon: 'line',
@ -107,26 +108,29 @@ export const Toolbar = () => {
Line
</ActionButton>
</li>
)}
{state.matches('Sketch') && (
<li className="contents">
<li className="contents" key="tangential-arc-button">
<ActionButton
Element="button"
onClick={() =>
state.matches('Sketch.Move Tool')
state.matches('Sketch.Tangential arc to')
? send('CancelSketch')
: send('Equip move tool')
: send('Equip tangential arc to')
}
aria-pressed={state.matches('Sketch.Move Tool')}
aria-pressed={state.matches('Sketch.Tangential arc to')}
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
icon={{
icon: 'move',
icon: 'line',
bgClassName,
}}
disabled={
!state.can('Equip tangential arc to') &&
!state.matches('Sketch.Tangential arc to')
}
>
Move
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"

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import { ActionIcon, ActionIconProps } from './ActionIcon'
import 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'

View File

@ -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'

View File

@ -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'

View File

@ -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'
@ -138,22 +138,21 @@ export function useCalc({
}, [kclManager.ast, kclManager.programMemory, selectionRange])
useEffect(() => {
const execAstAndSetResult = async () => {
try {
const code = `const __result__ = ${value}`
const ast = parse(code)
const _programMem: any = { root: {}, return: null }
availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
})
const { programMemory } = await executeAst({
executeAst({
ast,
engineCommandManager,
defaultPlanes: kclManager.defaultPlanes,
useFakeExecutor: true,
programMemoryOverride: JSON.parse(
JSON.stringify(kclManager.programMemory)
),
})
}).then(({ programMemory }) => {
const resultDeclaration = ast.body.find(
(a) =>
a.type === 'VariableDeclaration' &&
@ -165,11 +164,11 @@ export function useCalc({
const result = programMemory?.root?.__result__?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init)
}
execAstAndSetResult().catch(() => {
})
} catch (e) {
setCalcResult('NAN')
setValueNode(null)
})
}
}, [value, availableVarInfo])
return {

View File

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

View File

@ -9,7 +9,7 @@ import styles from './CodeMenu.module.css'
import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { 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>

View File

@ -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>

View File

@ -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() {

View File

@ -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

View File

@ -1,6 +1,7 @@
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { AstExplorer } from './AstExplorer'
import { EngineCommands } from './EngineCommands'
import { CamDebugSettings } from 'clientSideScene/setup'
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>

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -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'
@ -101,7 +101,7 @@ export const GlobalStateProvider = ({
goToSignInPage: () => {
navigate(paths.SIGN_IN)
void logout()
logout()
},
goToIndexPage: () => {
if (window.location.pathname.includes(paths.SIGN_IN)) {

View File

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

View File

@ -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: '' },
],
})
})

View File

@ -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>

View File

@ -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,9 @@ import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
import { setupSingleton } from 'clientSideScene/setup'
import { getSketchQuaternion } from 'clientSideScene/clientSideScene'
import { startSketchOnDefault } from 'lang/modifyAst'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -92,181 +79,10 @@ export const ModelingMachineProvider = ({
const [modelingState, modelingSend, modelingActor] = useMachine(
modelingMachine,
{
// context: persistedSettings,
actions: {
'Modify AST': () => {},
'Update code selection cursors': () => {},
'show default planes': () => {
void kclManager.showPlanes()
},
'create path': assign({
sketchEnginePathId: () => {
const sketchUuid = uuidv4()
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: sketchUuid,
cmd: {
type: 'start_path',
},
})
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edit_mode_enter',
target: sketchUuid,
},
})
return sketchUuid
},
}),
'AST start new sketch': assign(
({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
if (!axis) {
// Something really weird must have happened for this to happen.
console.error('axis is undefined for starting a new sketch')
return {}
}
if (!segmentId) {
// Something really weird must have happened for this to happen.
console.error('segmentId is undefined for starting a new sketch')
return {}
}
const _addStartSketch = addStartSketch(
kclManager.ast,
axis,
[roundOff(coords[0].x), roundOff(coords[0].y)],
[
roundOff(coords[1].x - coords[0].x),
roundOff(coords[1].y - coords[0].y),
]
)
const _modifiedAst = _addStartSketch.modifiedAst
const _pathToNode = _addStartSketch.pathToNode
const newCode = recast(_modifiedAst)
const astWithUpdatedSource = parse(newCode)
const updatedPipeNode = getNodeFromPath<PipeExpression>(
astWithUpdatedSource,
_pathToNode
).node
const startProfileAtCallExp = updatedPipeNode.body.find(
(exp) =>
exp.type === 'CallExpression' &&
exp.callee.name === 'startProfileAt'
)
if (startProfileAtCallExp)
engineCommandManager.artifactMap[sketchEnginePathId] = {
type: 'result',
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
commandType: 'start_path',
data: null,
raw: {} as any,
}
const lineCallExp = updatedPipeNode.body.find(
(exp) =>
exp.type === 'CallExpression' && exp.callee.name === 'line'
)
if (lineCallExp)
engineCommandManager.artifactMap[segmentId] = {
type: 'result',
range: [lineCallExp.start, lineCallExp.end],
commandType: 'extend_path',
parentId: sketchEnginePathId,
data: null,
raw: {} as any,
}
void kclManager.executeAstMock(astWithUpdatedSource, true)
return {
sketchPathToNode: _pathToNode,
}
}
),
'AST add line segment': async (
{ sketchPathToNode, sketchEnginePathId },
{ data: { coords, segmentId } }
) => {
if (!sketchPathToNode) return
const lastCoord = coords[coords.length - 1]
const pathInfo = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'path_get_info',
path_id: sketchEnginePathId,
},
})
const firstSegment = pathInfo?.data?.data?.segments.find(
(seg: any) => seg.command === 'line_to'
)
const firstSegCoords = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: firstSegment.command_id,
},
})
const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
const isClose = compareVec2Epsilon(
[startPathCoord.x, startPathCoord.y],
[lastCoord.x, lastCoord.y]
)
let _modifiedAst: Program
if (!isClose) {
const newSketchLn = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
to: [lastCoord.x, lastCoord.y],
from: [coords[0].x, coords[0].y],
fnName: 'line',
pathToNode: sketchPathToNode,
})
const _modifiedAst = newSketchLn.modifiedAst
void kclManager.executeAstMock(_modifiedAst, true).then(() => {
const lineCallExp = getNodeFromPath<CallExpression>(
kclManager.ast,
newSketchLn.pathToNode
).node
if (segmentId)
engineCommandManager.artifactMap[segmentId] = {
type: 'result',
range: [lineCallExp.start, lineCallExp.end],
commandType: 'extend_path',
parentId: sketchEnginePathId,
data: null,
raw: {} as any,
}
})
} else {
_modifiedAst = addCloseToPipe({
node: kclManager.ast,
programMemory: kclManager.programMemory,
pathToNode: sketchPathToNode,
})
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' },
})
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'default_camera_disable_sketch_mode' },
})
void kclManager.executeAstMock(_modifiedAst, true)
// updateAst(_modifiedAst, true)
}
},
'sketch exit execute': () => {
void kclManager.executeAst()
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 +90,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 +170,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 +189,29 @@ export const ModelingMachineProvider = ({
},
},
services: {
'animate-to-face': async (_, { data: { plane, normal } }) => {
const { modifiedAst, pathToNode } = startSketchOnDefault(
kclManager.ast,
plane
)
await kclManager.updateAst(modifiedAst, false)
const quaternion = getSketchQuaternion(pathToNode, normal)
await setupSingleton.tweenCameraToQuaternion(quaternion)
return {
sketchPathToNode: pathToNode,
sketchNormalBackUp: normal,
}
},
'animate-to-sketch': async ({
sketchPathToNode,
sketchNormalBackUp,
}) => {
const quaternion = getSketchQuaternion(
sketchPathToNode || [],
sketchNormalBackUp
)
await setupSingleton.tweenCameraToQuaternion(quaternion)
},
'Get horizontal info': async ({
selectionRanges,
}): Promise<SetSelections> => {
@ -542,17 +345,6 @@ export const ModelingMachineProvider = ({
}
)
useEffect(() => {
engineCommandManager.onPlaneSelected((plane_id: string) => {
if (modelingState.nextEvents.includes('Select default plane')) {
modelingSend({
type: 'Select default plane',
data: { planeId: plane_id },
})
}
})
}, [modelingSend, modelingState.nextEvents])
useEffect(() => {
kclManager.registerExecuteCallback(() => {
modelingSend({ type: 'Re-execute' })
@ -565,10 +357,7 @@ export const ModelingMachineProvider = ({
send: modelingSend,
actor: modelingActor,
commandBarConfig: modelingMachineConfig,
onCancel: () => {
console.log('firing onCancel!!')
modelingSend({ type: 'Cancel' })
},
onCancel: () => modelingSend({ type: 'Cancel' }),
})
return (

View File

@ -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]
)
})
})

View File

@ -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={overallConnectionStateColor[overallState].icon}
bgClassName={
'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-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"
>
{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 '}
>
<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={
hasIssues
? 'text-destroy-80 dark:text-destroy-30'
: 'text-succeed-80 dark:text-succeed-30'
hasIssueToIconColors[
issues[name as ConnectingTypeGroup].toString()
].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 ' +
hasIssueToIconColors[
issues[name as ConnectingTypeGroup].toString()
].bg
}
/>
</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"
>
<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_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"
icon={hasIssueToIcon.true}
bgClassName={hasIssueToIconColors.true.bg}
iconClassName={hasIssueToIconColors.true.icon}
/>
<p className="flex-1 mr-4">{issue}</p>
)}
</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>
)

View File

@ -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 {

View File

@ -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'

View File

@ -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'

View File

@ -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/setup'
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
}
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'handle_mouse_drag_start',
window: { x, y },
},
cmd_id: newId,
})
} else if (!state.matches('Sketch.Line Tool')) {
void engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_start',
interaction,
window: { x, y },
},
cmd_id: newId,
})
}
setButtonDownInStream(e.button)
setClickCoords({ x, y })
}
@ -118,7 +68,7 @@ export const Stream = ({ className = '' }) => {
const fps = 60
const handleScroll: WheelEventHandler<HTMLVideoElement> = throttle((e) => {
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
void engineCommandManager.sendSceneCommand({
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'default_camera_zoom',
@ -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,208 +107,21 @@ 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 },
}
void engineCommandManager.sendSceneCommand(command)
} else if (!didDragInStream && state.matches('Sketch.Line Tool')) {
command.cmd = {
type: 'mouse_click',
window: { x, y },
}
void engineCommandManager.sendSceneCommand(command).then(async (resp) => {
const entities_modified = resp?.data?.data?.entities_modified
if (!entities_modified) return
if (state.matches('Sketch.Line Tool.No Points')) {
send('Add point')
} else if (state.matches('Sketch.Line Tool.Point Added')) {
const curve = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: entities_modified[0],
},
})
const coords: { x: number; y: number }[] =
curve.data.data.control_points
// We need the normal for the plane we are on.
const plane = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'get_sketch_mode_plane',
},
})
const z_axis = plane.data.data.z_axis
// Get the current axis.
let currentAxis: 'xy' | 'xz' | 'yz' | '-xy' | '-xz' | '-yz' | null =
null
if (context.sketchPlaneId === kclManager.getPlaneId('xy')) {
if (z_axis.z === -1) {
currentAxis = '-xy'
} else {
currentAxis = 'xy'
}
} else if (context.sketchPlaneId === kclManager.getPlaneId('yz')) {
if (z_axis.x === -1) {
currentAxis = '-yz'
} else {
currentAxis = 'yz'
}
} else if (context.sketchPlaneId === kclManager.getPlaneId('xz')) {
if (z_axis.y === -1) {
currentAxis = '-xz'
} else {
currentAxis = 'xz'
}
}
send({
type: 'Add point',
data: {
coords,
axis: currentAxis,
segmentId: entities_modified[0],
},
})
} else if (state.matches('Sketch.Line Tool.Segment Added')) {
const curve = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: entities_modified[0],
},
})
const coords: { x: number; y: number }[] =
curve.data.data.control_points
send({
type: 'Add point',
data: { coords, axis: null, segmentId: entities_modified[0] },
})
}
})
} else if (
!didDragInStream &&
(state.matches('Sketch.SketchIdle') || state.matches('idle'))
) {
if (!didDragInStream) {
command.cmd = {
type: 'select_with_point',
selected_at_window: { x, y },
selection_type: 'add',
}
void engineCommandManager.sendSceneCommand(command)
} else if (!didDragInStream && state.matches('Sketch.Move Tool')) {
command.cmd = {
type: 'select_with_point',
selected_at_window: { x, y },
selection_type: 'add',
}
void engineCommandManager.sendSceneCommand(command)
} else if (didDragInStream && state.matches('Sketch.Move Tool')) {
engineCommandManager.sendSceneCommand(command)
} else if (didDragInStream) {
command.cmd = {
type: 'handle_mouse_drag_end',
window: { x, y },
}
void engineCommandManager.sendSceneCommand(command).then(async () => {
if (!context.sketchPathToNode) return
getNodeFromPath<VariableDeclarator>(
kclManager.ast,
context.sketchPathToNode,
'VariableDeclarator'
)
// Get the current plane string for plane we are on.
let currentPlaneString = ''
if (context.sketchPlaneId === kclManager.getPlaneId('xy')) {
currentPlaneString = 'XY'
} else if (context.sketchPlaneId === kclManager.getPlaneId('yz')) {
currentPlaneString = 'YZ'
} else if (context.sketchPlaneId === kclManager.getPlaneId('xz')) {
currentPlaneString = 'XZ'
}
// Do not supporting editing/moving lines on a non-default plane.
// Eventually we can support this but for now we will just throw an
// error.
if (currentPlaneString === '') return
const pathInfo = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'path_get_info',
path_id: context.sketchEnginePathId,
},
})
const segmentsWithMappings = (
pathInfo?.data?.data?.segments as { command_id: string }[]
)
.filter(({ command_id }) => {
return command_id && engineCommandManager.artifactMap[command_id]
})
.map(({ command_id }) => command_id)
const segment2dInfo = await Promise.all(
segmentsWithMappings.map(async (segmentId) => {
const response = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: segmentId,
},
})
const controlPoints: [
{ x: number; y: number },
{ x: number; y: number }
] = response.data.data.control_points
return {
controlPoints,
segmentId,
}
})
)
let modifiedAst = { ...kclManager.ast }
let code = kclManager.code
for (const controlPoint of segment2dInfo) {
const range =
engineCommandManager.artifactMap[controlPoint.segmentId].range
if (!range) continue
const from = controlPoint.controlPoints[0]
const to = controlPoint.controlPoints[1]
const modded = changeSketchArguments(
modifiedAst,
kclManager.programMemory,
range,
[to.x, to.y],
[from.x, from.y]
)
modifiedAst = modded.modifiedAst
// update artifact map ranges now that we have updated the ast.
code = recast(modded.modifiedAst)
const astWithCurrentRanges = kclManager.safeParse(code)
if (!astWithCurrentRanges) return
const updateNode = getNodeFromPath<CallExpression>(
astWithCurrentRanges,
modded.pathToNode
).node
engineCommandManager.artifactMap[controlPoint.segmentId].range = [
updateNode.start,
updateNode.end,
]
}
void kclManager.executeAstMock(modifiedAst, true)
})
} else {
void engineCommandManager.sendSceneCommand(command)
} else {
engineCommandManager.sendSceneCommand(command)
}
setDidDragInStream(false)
@ -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>

View File

@ -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,9 @@ 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 { setupSingleton } from 'clientSideScene/setup'
export const editorShortcutMeta = {
formatCode: {
@ -56,10 +58,12 @@ export const TextEditor = ({
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 } = {} } = {} } =
@ -76,12 +80,10 @@ export const TextEditor = ({
const fromServer: FromServer = FromServer.create()
const client = new Client(fromServer, intoServer)
if (!TEST) {
Server.initialize(intoServer, fromServer)
.then((lspServer) => {
void lspServer.start()
Server.initialize(intoServer, fromServer).then((lspServer) => {
lspServer.start()
setIsLSPServerReady(true)
})
.catch((e) => console.log(e))
}
const lspClient = new LanguageServerClient({ client })
@ -117,6 +119,12 @@ export const TextEditor = ({
if (!editorView) {
setEditorView(viewUpdate.view)
}
if (setupSingleton.selected) return // mid drag
const ignoreEvents: ModelingMachineEvent['type'][] = [
'Equip Line tool',
'Equip tangential arc to',
]
if (ignoreEvents.includes(state.event.type)) return
const eventInfo = processCodeMirrorRanges({
codeMirrorRanges: viewUpdate.state.selection.ranges,
selectionRanges,
@ -124,7 +132,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)
@ -153,7 +174,7 @@ export const TextEditor = ({
key: editorShortcutMeta.convertToVariable.codeMirror,
run: () => {
if (convertEnabled) {
void convertCallback()
convertCallback()
return true
}
return false

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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)

View File

@ -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,

View File

@ -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)),

View File

@ -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)

View File

@ -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)

View File

@ -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])
}

View File

@ -4,7 +4,7 @@ import { faBars, faBug, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
import { faGithub } from '@fortawesome/free-brands-svg-icons'
import { 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'

View File

@ -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)

View File

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

View File

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

View File

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

View File

@ -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,
])
}

View File

@ -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>,

View File

@ -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'
@ -39,7 +39,7 @@ export function useConvertToVariable() {
variableName
)
void kclManager.updateAst(_modifiedAst, true)
kclManager.updateAst(_modifiedAst, true)
} catch (e) {
console.log('error', e)
}

View File

@ -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,

View File

@ -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/plugin-fs'
@ -59,7 +60,7 @@ class KclManager {
} catch (e) {
console.error(e)
}
void this.executeAst(ast)
this.executeAst(ast)
}, 600)
private _isExecutingCallback: (arg: boolean) => void = () => {}
@ -98,7 +99,7 @@ class KclManager {
})
})
} else {
localStorage.setItem(PERSIST_CODE_TOKEN, code)
localStorage?.setItem(PERSIST_CODE_TOKEN, code)
}
}
@ -110,10 +111,6 @@ class KclManager {
this._programMemoryCallBack(programMemory)
}
get defaultPlanes() {
return this?.engineCommandManager?.defaultPlanes
}
get logs() {
return this._logs
}
@ -168,11 +165,13 @@ class KclManager {
zustandStore.state.code = ''
localStorage.setItem('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,26 +387,10 @@ 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
}
getPlaneId(axis: 'xy' | 'xz' | 'yz'): string {
return this.defaultPlanes[axis]
}
showPlanes() {
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, false)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, false)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, false)
}
hidePlanes() {
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, true)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
void this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
}
}
export const kclManager = new KclManager(engineCommandManager)

View File

@ -33,7 +33,7 @@ show(mySketch001)`
},
value: [
{
type: 'toPoint',
type: 'ToPoint',
name: '',
to: [-1.59, -1.54],
from: [0, 0],
@ -43,7 +43,7 @@ show(mySketch001)`
},
},
{
type: 'toPoint',
type: 'ToPoint',
to: [0.46, -5.82],
from: [-1.59, -1.54],
name: '',
@ -55,8 +55,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 +88,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 +135,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 +149,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] }],
},
])

View File

@ -54,7 +54,7 @@ show(mySketch)
const minusGeo = root.mySketch.value
expect(minusGeo).toEqual([
{
type: 'toPoint',
type: 'ToPoint',
to: [0, 2],
from: [0, 0],
__geoMeta: {
@ -64,7 +64,7 @@ show(mySketch)
name: 'myPath',
},
{
type: 'toPoint',
type: 'ToPoint',
to: [2, 3],
from: [0, 2],
name: '',
@ -74,7 +74,7 @@ show(mySketch)
},
},
{
type: 'toPoint',
type: 'ToPoint',
to: [5, -1],
from: [2, 3],
__geoMeta: {
@ -154,7 +154,7 @@ show(mySketch)
},
value: [
{
type: 'toPoint',
type: 'ToPoint',
to: [1, 1],
from: [0, 0],
name: '',
@ -164,7 +164,7 @@ show(mySketch)
},
},
{
type: 'toPoint',
type: 'ToPoint',
to: [0, 1],
from: [1, 1],
__geoMeta: {
@ -174,7 +174,7 @@ show(mySketch)
name: 'myPath',
},
{
type: 'toPoint',
type: 'ToPoint',
to: [1, 1],
from: [0, 1],
name: '',
@ -186,8 +186,11 @@ show(mySketch)
],
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: [39, 63] }],
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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