Compare commits
45 Commits
jtran/type
...
v0.24.9
Author | SHA1 | Date | |
---|---|---|---|
a3ff0a45eb | |||
4617ad0fed | |||
5fa51a2f92 | |||
4218777afb | |||
8b1b462e29 | |||
2bc99ba39b | |||
ffe0da6dcd | |||
d27afb8c74 | |||
1c778bf373 | |||
5df8a943a9 | |||
ab729dbcdb | |||
84865eaed0 | |||
543e809739 | |||
61b669cf4e | |||
75f1aaa824 | |||
f4848d7dea | |||
a0167f6ba6 | |||
e5a26c42e6 | |||
9c87b124d9 | |||
21389c089d | |||
29f57be8c1 | |||
cd55f07619 | |||
baf7d3dd9d | |||
54a9a50969 | |||
2830c750fa | |||
d3160cd85a | |||
fd1b4c3a32 | |||
b0a41c31ac | |||
5825ba575c | |||
e5bec2140e | |||
7bf6bc3048 | |||
22f9df73ed | |||
834472e0a6 | |||
bcdf6e314f | |||
55e9845ade | |||
d61cf882c1 | |||
874d19cbfd | |||
9dcc955760 | |||
9b594efe53 | |||
7b9f40c4cb | |||
81b79da90f | |||
2ad5a880fa | |||
b57a9ba54c | |||
b32f5c1d4e | |||
b6d4cc7a4e |
@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall
|
||||
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas
|
||||
|
48
.github/workflows/ci.yml
vendored
@ -20,6 +20,11 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
check-format:
|
||||
runs-on: 'ubuntu-latest'
|
||||
@ -84,8 +89,43 @@ jobs:
|
||||
- run: yarn build:wasm
|
||||
|
||||
- run: yarn simpleserver:ci
|
||||
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
|
||||
|
||||
- name: Install Chromium Browser
|
||||
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
|
||||
run: yarn playwright install chromium --with-deps
|
||||
|
||||
- name: run unit tests
|
||||
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
|
||||
run: yarn test:nowatch
|
||||
env:
|
||||
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
|
||||
- name: check for changes
|
||||
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
|
||||
id: git-check
|
||||
run: |
|
||||
git add src/lang/std/artifactMapGraphs
|
||||
if git status src/lang/std/artifactMapGraphs | grep -q "Changes to be committed"
|
||||
then echo "modified=true" >> $GITHUB_OUTPUT
|
||||
else echo "modified=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Commit changes, if any
|
||||
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' && steps.git-check.outputs.modified == 'true' }}
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
|
||||
git fetch origin
|
||||
echo ${{ github.head_ref }}
|
||||
git checkout ${{ github.head_ref }}
|
||||
# TODO when webkit works on ubuntu remove the os part of the commit message
|
||||
git commit -am "Look at this (photo)Graph *in the voice of Nickelback*" || true
|
||||
git push
|
||||
git push origin ${{ github.head_ref }}
|
||||
|
||||
|
||||
|
||||
- run: yarn test:nowatch
|
||||
|
||||
|
||||
prepare-json-files:
|
||||
@ -495,7 +535,7 @@ jobs:
|
||||
project_id: kittycadapi
|
||||
|
||||
- name: Upload release files to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.0
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.1
|
||||
with:
|
||||
path: artifact
|
||||
glob: '*/Zoo*'
|
||||
@ -503,13 +543,13 @@ jobs:
|
||||
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
|
||||
|
||||
- name: Upload update endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.0
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.1
|
||||
with:
|
||||
path: last_update.json
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
|
||||
- name: Upload download endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.0
|
||||
uses: google-github-actions/upload-cloud-storage@v2.1.1
|
||||
with:
|
||||
path: last_download.json
|
||||
destination: ${{ env.BUCKET_DIR }}
|
||||
|
49
.github/workflows/generate-machine-api-types.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
name: generate machine-api types
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'openapi/machine-api.json'
|
||||
- '.github/workflows/generate-machine-api-types.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
generate:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
- run: yarn install
|
||||
- run: yarn generate:machine-api
|
||||
- run: yarn fmt
|
||||
- name: check for changes
|
||||
id: git-check
|
||||
run: |
|
||||
git add .
|
||||
if git status | grep -q "Changes to be committed"
|
||||
then echo "modified=true" >> $GITHUB_OUTPUT
|
||||
else echo "modified=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Commit changes, if any
|
||||
if: steps.git-check.outputs.modified == 'true'
|
||||
run: |
|
||||
git add .
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
|
||||
git fetch origin
|
||||
echo ${{ github.head_ref }}
|
||||
git checkout ${{ github.head_ref }}
|
||||
git commit -am "New machine-api types" || true
|
||||
git push
|
||||
git push origin ${{ github.head_ref }}
|
||||
|
67
.github/workflows/playwright.yml
vendored
@ -13,7 +13,7 @@ permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
@ -34,8 +34,13 @@ jobs:
|
||||
- 'src/wasm-lib/**'
|
||||
|
||||
playwright-ubuntu:
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shardIndex: [1, 2, 3, 4]
|
||||
shardTotal: [4]
|
||||
needs: check-rust-changes
|
||||
steps:
|
||||
- name: Tune GitHub-hosted runner network
|
||||
@ -106,13 +111,19 @@ jobs:
|
||||
- name: build web
|
||||
run: yarn build:local
|
||||
- name: Run ubuntu/chrome snapshots
|
||||
continue-on-error: true
|
||||
run: |
|
||||
yarn playwright test --project="Google Chrome" --update-snapshots e2e/playwright/snapshot-tests.spec.ts
|
||||
yarn playwright test --project="Google Chrome" --retries="3" --update-snapshots --grep=@snapshot --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
env:
|
||||
CI: true
|
||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-ubuntu-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
- name: Clean up test-results
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
@ -143,7 +154,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: steps.git-check.outputs.modified == 'true'
|
||||
with:
|
||||
name: playwright-report-ubuntu-${{ github.sha }}
|
||||
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
# if have previous run results, use them
|
||||
@ -151,7 +162,7 @@ jobs:
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: test-results-ubuntu-${{ github.sha }}
|
||||
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
- name: Run ubuntu/chrome flow (with retries)
|
||||
id: retry
|
||||
@ -160,14 +171,14 @@ jobs:
|
||||
if [[ ! -f "test-results/.last-run.json" ]]; then
|
||||
# if no last run artifact, than run plawright normally
|
||||
echo "run playwright normally"
|
||||
yarn playwright test --project="Google Chrome" e2e/playwright/flow-tests.spec.ts || true
|
||||
yarn playwright test --project="Google Chrome" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert=@snapshot || true
|
||||
# # send to axiom
|
||||
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
|
||||
retry=1
|
||||
max_retrys=4
|
||||
|
||||
|
||||
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
|
||||
while [[ $retry -le $max_retrys ]]; do
|
||||
if [[ -f "test-results/.last-run.json" ]]; then
|
||||
@ -175,7 +186,7 @@ jobs:
|
||||
if [[ $failed_tests -gt 0 ]]; then
|
||||
echo "retried=true" >>$GITHUB_OUTPUT
|
||||
echo "run playwright with last failed tests and retry $retry"
|
||||
yarn playwright test --project="Google Chrome" --last-failed e2e/playwright/flow-tests.spec.ts || true
|
||||
yarn playwright test --project="Google Chrome" --last-failed --grep-invert=@snapshot || true
|
||||
# send to axiom
|
||||
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
|
||||
retry=$((retry + 1))
|
||||
@ -188,9 +199,9 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
echo "retried=false" >>$GITHUB_OUTPUT
|
||||
|
||||
|
||||
if [[ -f "test-results/.last-run.json" ]]; then
|
||||
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
|
||||
if [[ $failed_tests -gt 0 ]]; then
|
||||
@ -210,21 +221,26 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-results-ubuntu-${{ github.sha }}
|
||||
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-ubuntu-${{ github.sha }}
|
||||
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
playwright-macos:
|
||||
timeout-minutes: 60
|
||||
runs-on: macos-14-large
|
||||
timeout-minutes: 30
|
||||
runs-on: macos-14
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shardIndex: [1, 2, 3, 4]
|
||||
shardTotal: [4]
|
||||
needs: check-rust-changes
|
||||
steps:
|
||||
- name: Tune GitHub-hosted runner network
|
||||
@ -300,7 +316,7 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: test-results-macos-${{ github.sha }}
|
||||
name: test-results-macos-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
- name: Run macos/safari flow (with retries)
|
||||
id: retry
|
||||
@ -309,14 +325,14 @@ jobs:
|
||||
if [[ ! -f "test-results/.last-run.json" ]]; then
|
||||
# if no last run artifact, than run plawright normally
|
||||
echo "run playwright normally"
|
||||
yarn playwright test --project="webkit" e2e/playwright/flow-tests.spec.ts || true
|
||||
yarn playwright test --project="webkit" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert=@snapshot || true
|
||||
# # send to axiom
|
||||
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
|
||||
retry=1
|
||||
max_retrys=4
|
||||
|
||||
|
||||
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
|
||||
while [[ $retry -le $max_retrys ]]; do
|
||||
if [[ -f "test-results/.last-run.json" ]]; then
|
||||
@ -324,7 +340,7 @@ jobs:
|
||||
if [[ $failed_tests -gt 0 ]]; then
|
||||
echo "retried=true" >>$GITHUB_OUTPUT
|
||||
echo "run playwright with last failed tests and retry $retry"
|
||||
yarn playwright test --project="webkit" --last-failed e2e/playwright/flow-tests.spec.ts || true
|
||||
yarn playwright test --project="webkit" --last-failed --grep-invert=@snapshot || true
|
||||
# send to axiom
|
||||
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
|
||||
retry=$((retry + 1))
|
||||
@ -337,9 +353,9 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
echo "retried=false" >>$GITHUB_OUTPUT
|
||||
|
||||
|
||||
if [[ -f "test-results/.last-run.json" ]]; then
|
||||
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
|
||||
if [[ $failed_tests -gt 0 ]]; then
|
||||
@ -354,15 +370,14 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: test-results-macos-${{ github.sha }}
|
||||
name: test-results-macos-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: playwright-report-macos-${{ github.sha }}
|
||||
name: playwright-report-macos-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
|
2
.gitignore
vendored
@ -39,6 +39,7 @@ src/wasm-lib/grackle/test_json_output
|
||||
e2e/playwright/playwright-secrets.env
|
||||
e2e/playwright/temp1.png
|
||||
e2e/playwright/temp2.png
|
||||
e2e/playwright/temp3.png
|
||||
# exports from snapshot-tests.spec.ts "exports of each format should work"
|
||||
e2e/playwright/export-snapshots/*
|
||||
!e2e/playwright/export-snapshots/*.png
|
||||
@ -48,6 +49,7 @@ e2e/playwright/export-snapshots/*
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/src/lang/std/artifactMapCache
|
||||
|
||||
|
||||
## generated files
|
||||
|
37
docs/kcl/assertEqual.md
Normal file
@ -21,6 +21,7 @@ layout: manual
|
||||
* [`arc`](kcl/arc)
|
||||
* [`asin`](kcl/asin)
|
||||
* [`assert`](kcl/assert)
|
||||
* [`assertEqual`](kcl/assertEqual)
|
||||
* [`assertGreaterThan`](kcl/assertGreaterThan)
|
||||
* [`assertGreaterThanOrEq`](kcl/assertGreaterThanOrEq)
|
||||
* [`assertLessThan`](kcl/assertLessThan)
|
||||
|
@ -54791,6 +54791,62 @@
|
||||
"const myVar = true\nassert(myVar, \"should always be true\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "assertEqual",
|
||||
"summary": "Check that a numerical value equals another at runtime,",
|
||||
"description": "otherwise raise an error.",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "left",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "right",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "epsilon",
|
||||
"type": "number",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"type": "string",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"returnValue": {
|
||||
"name": "",
|
||||
"type": "()",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"unpublished": false,
|
||||
"deprecated": false,
|
||||
"examples": [
|
||||
"let n = 1.0285\nlet m = 1.0286\nassertEqual(n, m, 0.01, \"n is within the given tolerance for m\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "assertGreaterThan",
|
||||
"summary": "Check that a numerical value is greater than another at runtime,",
|
||||
@ -223435,7 +223491,7 @@
|
||||
},
|
||||
{
|
||||
"name": "xLine",
|
||||
"summary": "Draw a line on the x-axis.",
|
||||
"summary": "Draw a line parallel to the X-axis, with the given length.",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
@ -230069,8 +230125,8 @@
|
||||
},
|
||||
{
|
||||
"name": "xLineTo",
|
||||
"summary": "Draw a line to a point on the x-axis.",
|
||||
"description": "",
|
||||
"summary": "Draw a line parallel to the X axis, that ends at the given X.",
|
||||
"description": "E.g. if the previous line ended at (1, 1), then xLineTo(4) draws a line from (1, 1) to (4, 1)",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
@ -236703,7 +236759,7 @@
|
||||
},
|
||||
{
|
||||
"name": "yLine",
|
||||
"summary": "Draw a line on the y-axis.",
|
||||
"summary": "Draw a line parallel to the Y-axis, with the given length.",
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"args": [
|
||||
@ -243337,8 +243393,8 @@
|
||||
},
|
||||
{
|
||||
"name": "yLineTo",
|
||||
"summary": "Draw a line to a point on the y-axis.",
|
||||
"description": "",
|
||||
"summary": "Draw a line parallel to the Y axis, that ends at the given Y.",
|
||||
"description": "E.g. if the previous line ended at (1, 1), then yLineTo(4) draws a line from (1, 1) to (1, 4)",
|
||||
"tags": [],
|
||||
"args": [
|
||||
{
|
||||
|
@ -1,10 +1,10 @@
|
||||
---
|
||||
title: "xLine"
|
||||
excerpt: "Draw a line on the x-axis."
|
||||
excerpt: "Draw a line parallel to the X-axis, with the given length."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Draw a line on the x-axis.
|
||||
Draw a line parallel to the X-axis, with the given length.
|
||||
|
||||
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
---
|
||||
title: "xLineTo"
|
||||
excerpt: "Draw a line to a point on the x-axis."
|
||||
excerpt: "Draw a line parallel to the X axis, that ends at the given X."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Draw a line to a point on the x-axis.
|
||||
|
||||
Draw a line parallel to the X axis, that ends at the given X.
|
||||
|
||||
E.g. if the previous line ended at (1, 1), then xLineTo(4) draws a line from (1, 1) to (4, 1)
|
||||
|
||||
```js
|
||||
xLineTo(to: number, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup
|
||||
|
@ -1,10 +1,10 @@
|
||||
---
|
||||
title: "yLine"
|
||||
excerpt: "Draw a line on the y-axis."
|
||||
excerpt: "Draw a line parallel to the Y-axis, with the given length."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Draw a line on the y-axis.
|
||||
Draw a line parallel to the Y-axis, with the given length.
|
||||
|
||||
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
---
|
||||
title: "yLineTo"
|
||||
excerpt: "Draw a line to a point on the y-axis."
|
||||
excerpt: "Draw a line parallel to the Y axis, that ends at the given Y."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Draw a line to a point on the y-axis.
|
||||
|
||||
Draw a line parallel to the Y axis, that ends at the given Y.
|
||||
|
||||
E.g. if the previous line ended at (1, 1), then yLineTo(4) draws a line from (1, 1) to (1, 4)
|
||||
|
||||
```js
|
||||
yLineTo(to: number, sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
TEST_SETTINGS_ONBOARDING_EXPORT,
|
||||
TEST_SETTINGS_ONBOARDING_START,
|
||||
TEST_CODE_GIZMO,
|
||||
TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW,
|
||||
TEST_SETTINGS_ONBOARDING_USER_MENU,
|
||||
TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING,
|
||||
} from './storageStates'
|
||||
@ -26,7 +27,7 @@ import * as TOML from '@iarna/toml'
|
||||
import { LineInputsType } from 'lang/std/sketchcombos'
|
||||
import { Coords2d } from 'lang/std/sketch'
|
||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||
import { EngineCommand } from 'lang/std/artifactMap'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
|
||||
@ -466,7 +467,7 @@ test.describe('Testing Camera Movement', () => {
|
||||
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.waitForTimeout(200)
|
||||
// hover over horizontal line
|
||||
await u.canvasLocator.hover({ position: { x: 800, y } })
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
@ -1019,7 +1020,7 @@ test.describe('Editor tests', () => {
|
||||
|> line([0, -10], %, $revolveAxis)
|
||||
|> close(%)
|
||||
|> extrude(10, %)
|
||||
|
||||
|
||||
const sketch001 = startSketchOn(box, revolveAxis)
|
||||
|> startProfileAt([5, 10], %)
|
||||
|> line([0, -10], %)
|
||||
@ -2534,18 +2535,29 @@ test.describe('Onboarding tests', () => {
|
||||
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
|
||||
|
||||
// Test that the text in this step is correct
|
||||
const avatarLocator = await page
|
||||
.getByTestId('user-sidebar-toggle')
|
||||
.locator('img')
|
||||
const onboardingOverlayLocator = await page
|
||||
const sidebar = page.getByTestId('user-sidebar-toggle')
|
||||
const avatar = sidebar.locator('img')
|
||||
const onboardingOverlayLocator = page
|
||||
.getByTestId('onboarding-content')
|
||||
.locator('div')
|
||||
.nth(1)
|
||||
|
||||
// Expect the avatar to be visible and for the text to reference it
|
||||
await expect(avatarLocator).not.toBeVisible()
|
||||
await expect(avatar).not.toBeVisible()
|
||||
await expect(onboardingOverlayLocator).toBeVisible()
|
||||
await expect(onboardingOverlayLocator).toContainText('the menu button')
|
||||
|
||||
// Test we mention what else is in this menu for https://github.com/KittyCAD/modeling-app/issues/2939
|
||||
// which doesn't deserver its own full test spun up
|
||||
const userMenuFeatures = [
|
||||
'manage your account',
|
||||
'report a bug',
|
||||
'request a feature',
|
||||
'sign out',
|
||||
]
|
||||
for (const feature of userMenuFeatures) {
|
||||
await expect(onboardingOverlayLocator).toContainText(feature)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -2623,10 +2635,9 @@ test.describe('Testing selections', () => {
|
||||
await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
|
||||
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
// bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience
|
||||
// bg-yellow-300/70 is more brittle than hover-highlight, but is closer to the user experience
|
||||
// and will be an easy fix if it breaks because we change the colour
|
||||
await expect(page.locator('.bg-yellow-200').first()).toBeVisible()
|
||||
|
||||
await expect(page.locator('.bg-yellow-300\\/70')).toBeVisible()
|
||||
// check mousing off, than mousing onto another line
|
||||
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
@ -3078,7 +3089,7 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
|
||||
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible()
|
||||
|
||||
await page.mouse.move(flatExtrusionFace[0], flatExtrusionFace[1])
|
||||
await expect(page.getByTestId('hover-highlight')).toHaveCount(5) // multiple lines
|
||||
await expect(page.getByTestId('hover-highlight')).toHaveCount(6) // multiple lines
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible()
|
||||
@ -3894,6 +3905,39 @@ const extrude001 = extrude(distance001, sketch001)`.replace(
|
||||
|
||||
test.describe('Regression tests', () => {
|
||||
// bugs we found that don't fit neatly into other categories
|
||||
test('bad model has inline error #3251', async ({ page }) => {
|
||||
// because the model has `line([0,0]..` it is valid code, but the model is invalid
|
||||
// regression test for https://github.com/KittyCAD/modeling-app/issues/3251
|
||||
// Since the bad model also found as issue with the artifact graph, which in tern blocked the editor diognostics
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const sketch2 = startSketchOn("XY")
|
||||
const sketch001 = startSketchAt([-0, -0])
|
||||
|> line([0, 0], %)
|
||||
|> line([-4.84, -5.29], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// error in guter
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
// this is a cryptic error message, fact that all the lines are co-linear from the `line([0,0])` is the issue why
|
||||
// the close doesn't work
|
||||
// when https://github.com/KittyCAD/modeling-app/issues/3268 is closed
|
||||
// this test will need updating
|
||||
const crypticErrorText = `ApiError`
|
||||
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
|
||||
})
|
||||
test('executes on load', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
@ -4903,6 +4947,116 @@ const sketch002 = startSketchOn(extrude001, 'END')
|
||||
`.replace(/\s/g, '')
|
||||
)
|
||||
})
|
||||
test('empty-scene default-planes act as expected', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
test.skip(
|
||||
browserName === 'webkit',
|
||||
'Skip on Safari until `window.tearDown` is working there'
|
||||
)
|
||||
/**
|
||||
* Tests the following things
|
||||
* 1) The the planes are there on load because the scene is empty
|
||||
* 2) The planes don't changes color when hovered initially
|
||||
* 3) Putting something in the scene makes the planes hidden
|
||||
* 4) Removing everything from the scene shows the plans again
|
||||
* 3) Once "start sketch" is click, the planes do respond to hovers
|
||||
* 4) Selecting a plan works as expected, i.e. sketch mode
|
||||
* 5) Reloading the scene with something already in the scene means the planes are hidden
|
||||
*/
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const XYPlanePoint = { x: 774, y: 116 } as const
|
||||
const unHoveredColor: [number, number, number] = [47, 47, 93]
|
||||
expect(
|
||||
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
|
||||
).toBeLessThan(8)
|
||||
|
||||
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// color should not change for having been hovered
|
||||
expect(
|
||||
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
|
||||
).toBeLessThan(8)
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
|
||||
await u.codeLocator.fill(`const sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, -10], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, 20], %)
|
||||
|> xLine(-20, %)
|
||||
`)
|
||||
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
|
||||
const noPlanesColor: [number, number, number] = [30, 30, 30]
|
||||
expect(
|
||||
await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor)
|
||||
).toBeLessThan(3)
|
||||
|
||||
await u.clearCommandLogs()
|
||||
await u.removeCurrentCode()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(XYPlanePoint, unHoveredColor), {
|
||||
timeout: 5_000,
|
||||
})
|
||||
.toBeLessThan(8)
|
||||
|
||||
// click start Sketch
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y, { steps: 5 })
|
||||
const hoveredColor: [number, number, number] = [93, 93, 127]
|
||||
// now that we're expecting the user to select a plan, it does respond to hover
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(XYPlanePoint, hoveredColor))
|
||||
.toBeLessThan(8)
|
||||
|
||||
await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y)
|
||||
await page.waitForTimeout(600)
|
||||
|
||||
await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y)
|
||||
await page.waitForTimeout(200)
|
||||
await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50)
|
||||
await expect(u.codeLocator)
|
||||
.toHaveText(`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([11.8, 9.09], %)
|
||||
|> line([3.39, -3.39], %)
|
||||
`)
|
||||
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([11.8, 9.09], %)
|
||||
|> line([3.39, -3.39], %)
|
||||
`
|
||||
)
|
||||
})
|
||||
await page.reload()
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// expect there to be no planes on load since there's something in the scene
|
||||
expect(
|
||||
await u.getGreatestPixDiff(XYPlanePoint, noPlanesColor)
|
||||
).toBeLessThan(3)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Testing constraints', () => {
|
||||
@ -8098,33 +8252,208 @@ const sketch002 = extrude(${[5, 5]} + 7, sketch002)`
|
||||
await expect(page.locator('.cm-content')).toHaveText(result2.regExp)
|
||||
})
|
||||
|
||||
test('Typing KCL errors induces a badge on the error logs pane button', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
test.describe('Code pane and errors', () => {
|
||||
test('Typing KCL errors induces a badge on the code pane button', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
// Load the app with the working starter code
|
||||
await page.addInitScript((code) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
}, bracket)
|
||||
// Load the app with the working starter code
|
||||
await page.addInitScript((code) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
}, bracket)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// wait for execution done
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
// wait for execution done
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// Ensure no badge is present
|
||||
const errorLogsButton = page.getByRole('button', { name: 'KCL Errors pane' })
|
||||
await expect(errorLogsButton).not.toContainText('notification')
|
||||
// Ensure no badge is present
|
||||
const codePaneButtonHolder = page.locator('#code-button-holder')
|
||||
await expect(codePaneButtonHolder).not.toContainText('notification')
|
||||
|
||||
// Delete a character to break the KCL
|
||||
await u.openKclCodePanel()
|
||||
await page.getByText('extrude(').click()
|
||||
await page.keyboard.press('Backspace')
|
||||
// Delete a character to break the KCL
|
||||
await u.openKclCodePanel()
|
||||
await page.getByText('extrude(').click()
|
||||
await page.keyboard.press('Backspace')
|
||||
|
||||
// Ensure that a badge appears on the button
|
||||
await expect(errorLogsButton).toContainText('notification')
|
||||
// Ensure that a badge appears on the button
|
||||
await expect(codePaneButtonHolder).toContainText('notification')
|
||||
})
|
||||
|
||||
test('Opening and closing the code pane will consistently show error diagnostics', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
// Load the app with the working starter code
|
||||
await page.addInitScript((code) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
}, bracket)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 900 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// wait for execution done
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// Ensure we have no errors in the gutter.
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
// Ensure no badge is present
|
||||
const codePaneButton = page.getByRole('button', { name: 'KCL Code pane' })
|
||||
const codePaneButtonHolder = page.locator('#code-button-holder')
|
||||
await expect(codePaneButtonHolder).not.toContainText('notification')
|
||||
|
||||
// Delete a character to break the KCL
|
||||
await u.openKclCodePanel()
|
||||
await page.getByText('extrude(').click()
|
||||
await page.keyboard.press('Backspace')
|
||||
|
||||
// Ensure that a badge appears on the button
|
||||
await expect(codePaneButtonHolder).toContainText('notification')
|
||||
|
||||
// Ensure we have an error diagnostic.
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
await expect(page.getByText('Unexpected token').first()).toBeVisible()
|
||||
|
||||
// Close the code pane
|
||||
codePaneButton.click()
|
||||
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Ensure that a badge appears on the button
|
||||
await expect(codePaneButtonHolder).toContainText('notification')
|
||||
// Ensure we have no errors in the gutter.
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
// Open the code pane
|
||||
u.openKclCodePanel()
|
||||
|
||||
// Ensure that a badge appears on the button
|
||||
await expect(codePaneButtonHolder).toContainText('notification')
|
||||
|
||||
// Ensure we have an error diagnostic.
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
await expect(page.getByText('Unexpected token').first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('When error is not in view you can click the badge to scroll to it', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
// Load the app with the working starter code
|
||||
await page.addInitScript((code) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
}, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Ensure badge is present
|
||||
const codePaneButtonHolder = page.locator('#code-button-holder')
|
||||
await expect(codePaneButtonHolder).toContainText('notification')
|
||||
|
||||
// Ensure we have no errors in the gutter, since error out of view.
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
// Click the badge.
|
||||
const badge = page.locator('#code-badge')
|
||||
await expect(badge).toBeVisible()
|
||||
await badge.click()
|
||||
|
||||
// Ensure we have an error diagnostic.
|
||||
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
|
||||
|
||||
// Hover over the error to see the error message
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
await expect(
|
||||
page
|
||||
.getByText(
|
||||
'sketch profile must lie entirely on one side of the revolution axis'
|
||||
)
|
||||
.first()
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('When error is not in view WITH LINTS you can click the badge to scroll to it', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
// Load the app with the working starter code
|
||||
await page.addInitScript((code) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
}, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Ensure badge is present
|
||||
const codePaneButtonHolder = page.locator('#code-button-holder')
|
||||
await expect(codePaneButtonHolder).toContainText('notification')
|
||||
|
||||
// Ensure we have no errors in the gutter, since error out of view.
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
|
||||
// click in the editor to focus it
|
||||
await page.locator('.cm-content').click()
|
||||
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// go to the start of the editor and enter more text which will trigger
|
||||
// a lint error.
|
||||
// GO to the start of the editor.
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('Home')
|
||||
await page.keyboard.type('const foo_bar = 1')
|
||||
await page.waitForTimeout(500)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// ensure we have a lint error
|
||||
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
|
||||
|
||||
// Click the badge.
|
||||
const badge = page.locator('#code-badge')
|
||||
await expect(badge).toBeVisible()
|
||||
await badge.click()
|
||||
|
||||
// Ensure we have an error diagnostic.
|
||||
await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible()
|
||||
|
||||
// Hover over the error to see the error message
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
await expect(
|
||||
page
|
||||
.getByText(
|
||||
'sketch profile must lie entirely on one side of the revolution axis'
|
||||
)
|
||||
.first()
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
@ -86,3 +86,285 @@ export const TEST_CODE_GIZMO = `const part001 = startSketchOn('XZ')
|
||||
|> close(%)
|
||||
|> extrude(5 + 7, %)
|
||||
`
|
||||
|
||||
export const TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW = `const width = 50.8
|
||||
const height = 30
|
||||
const thickness = 2
|
||||
const keychainHoleSize = 3
|
||||
|
||||
const keychain = startSketchOn("XY")
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> lineTo([width, 0], %)
|
||||
|> lineTo([width, height], %)
|
||||
|> lineTo([0, height], %)
|
||||
|> close(%)
|
||||
|> extrude(thickness, %)
|
||||
|
||||
// generated from /home/paultag/Downloads/zma-logomark.svg
|
||||
fn svg = (surface, origin, depth) => {
|
||||
let a0 = surface |> startProfileAt([origin[0] + 45.430427, origin[1] + -14.627736], %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0, 0.764157 ],
|
||||
control2: [ 0, 1.528314 ],
|
||||
to: [ 0, 2.292469 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ -3.03202, 0 ],
|
||||
control2: [ -6.064039, 0 ],
|
||||
to: [ -9.09606, 0 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0, -1.077657 ],
|
||||
control2: [ 0, -2.155312 ],
|
||||
to: [ 0, -3.232969 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 2.741805, 0 ],
|
||||
control2: [ 5.483613, 0 ],
|
||||
to: [ 8.225417, 0 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ -2.740682, -2.961815 ],
|
||||
control2: [ -5.490342, -5.925794 ],
|
||||
to: [ -8.225417, -8.886255 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0, -0.723995 ],
|
||||
control2: [ 0, -1.447988 ],
|
||||
to: [ 0, -2.171981 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0.712124, 0.05061 ],
|
||||
control2: [ 1.511636, -0.09877 ],
|
||||
to: [ 2.172096, 0.07005 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0.68573, 0.740811 ],
|
||||
control2: [ 1.371459, 1.481622 ],
|
||||
to: [ 2.057187, 2.222436 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0, -0.76416 ],
|
||||
control2: [ 0, -1.52832 ],
|
||||
to: [ 0, -2.29248 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 3.032013, 0 ],
|
||||
control2: [ 6.064026, 0 ],
|
||||
to: [ 9.096038, 0 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0, 1.077657 ],
|
||||
control2: [ 0, 2.155314 ],
|
||||
to: [ 0, 3.232973 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ -2.741312, 0 ],
|
||||
control2: [ -5.482623, 0 ],
|
||||
to: [ -8.223936, 0 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 2.741313, 2.961108 ],
|
||||
control2: [ 5.482624, 5.922216 ],
|
||||
to: [ 8.223936, 8.883325 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0, 0.724968 ],
|
||||
control2: [ 0, 1.449938 ],
|
||||
to: [ 0, 2.174907 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ -0.712656, -0.05145 ],
|
||||
control2: [ -1.512554, 0.09643 ],
|
||||
to: [ -2.173592, -0.07298 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ -0.685222, -0.739834 ],
|
||||
control2: [ -1.370445, -1.479669 ],
|
||||
to: [ -2.055669, -2.219505 ]
|
||||
}, %)
|
||||
|> close(%)
|
||||
|> extrude(depth, %)
|
||||
|
||||
let a1 = surface |> startProfileAt([origin[0] + 57.920488, origin[1] + -15.244943], %)
|
||||
|> bezierCurve({
|
||||
control1: [ -2.78904, 0.106635 ],
|
||||
control2: [ -5.052548, -2.969529 ],
|
||||
to: [ -4.055141, -5.598369 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0.841523, -0.918736 ],
|
||||
control2: [ 0.439412, -1.541892 ],
|
||||
to: [ -0.368488, -2.214378 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ -0.418245, -0.448461 ],
|
||||
control2: [ -0.836489, -0.896922 ],
|
||||
to: [ -1.254732, -1.345384 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ -2.76806, 2.995359 ],
|
||||
control2: [ -2.32667, 8.18409 ],
|
||||
to: [ 0.897655, 10.678932 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 2.562822, 2.186098 ],
|
||||
control2: [ 6.605111, 2.28043 ],
|
||||
to: [ 9.271202, 0.226476 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ -0.743744, -0.797465 ],
|
||||
control2: [ -1.487487, -1.594932 ],
|
||||
to: [ -2.231232, -2.392397 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ -0.672938, 0.421422 ],
|
||||
control2: [ -1.465362, 0.646946 ],
|
||||
to: [ -2.259264, 0.64512 ]
|
||||
}, %)
|
||||
|> close(%)
|
||||
|> extrude(depth, %)
|
||||
|
||||
let a2 = surface |> startProfileAt([origin[0] + 62.19406300000001, origin[1] + -19.500698999999997], %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0.302938, 1.281141 ],
|
||||
control2: [ -1.53575, 2.434288 ],
|
||||
to: [ -0.10908, 3.279477 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0.504637, 0.54145 ],
|
||||
control2: [ 1.009273, 1.082899 ],
|
||||
to: [ 1.513909, 1.624348 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 2.767778, -2.995425 ],
|
||||
control2: [ 2.327135, -8.184384 ],
|
||||
to: [ -0.897661, -10.679047 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ -2.562947, -2.186022 ],
|
||||
control2: [ -6.604089, -2.279606 ],
|
||||
to: [ -9.271196, -0.227813 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0.744231, 0.797952 ],
|
||||
control2: [ 1.488461, 1.595904 ],
|
||||
to: [ 2.232692, 2.393856 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 2.302377, -1.564629 ],
|
||||
control2: [ 5.793126, -0.15358 ],
|
||||
to: [ 6.396577, 2.547372 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0.08981, 0.346302 ],
|
||||
control2: [ 0.134865, 0.704078 ],
|
||||
to: [ 0.13476, 1.061807 ]
|
||||
}, %)
|
||||
|> close(%)
|
||||
|> extrude(depth, %)
|
||||
|
||||
let a3 = surface |> startProfileAt([origin[0] + 74.124866, origin[1] + -15.244943], %)
|
||||
|> bezierCurve({
|
||||
control1: [ -2.78904, 0.106635 ],
|
||||
control2: [ -5.052549, -2.969529 ],
|
||||
to: [ -4.055142, -5.598369 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0.841527, -0.918738 ],
|
||||
control2: [ 0.43941, -1.541892 ],
|
||||
to: [ -0.368497, -2.214367 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ -0.418254, -0.448466 ],
|
||||
control2: [ -0.836507, -0.896931 ],
|
||||
to: [ -1.254761, -1.345395 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ -2.768019, 2.995371 ],
|
||||
control2: [ -2.326624, 8.184088 ],
|
||||
to: [ 0.897678, 10.678932 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 2.56289, 2.186191 ],
|
||||
control2: [ 6.60516, 2.280307 ],
|
||||
to: [ 9.271371, 0.226476 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ -0.743808, -0.797465 ],
|
||||
control2: [ -1.487616, -1.594932 ],
|
||||
to: [ -2.231424, -2.392397 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ -0.672916, 0.421433 ],
|
||||
control2: [ -1.465344, 0.646926 ],
|
||||
to: [ -2.259225, 0.64512 ]
|
||||
}, %)
|
||||
|> close(%)
|
||||
|> extrude(depth, %)
|
||||
|
||||
let a4 = surface |> startProfileAt([origin[0] + 77.57333899999998, origin[1] + -16.989262999999998], %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0.743298, 0.797463 ],
|
||||
control2: [ 1.486592, 1.594926 ],
|
||||
to: [ 2.229888, 2.392389 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 2.767827, -2.995393 ],
|
||||
control2: [ 2.327103, -8.184396 ],
|
||||
to: [ -0.897672, -10.679047 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ -2.562939, -2.186037 ],
|
||||
control2: [ -6.604077, -2.279589 ],
|
||||
to: [ -9.271185, -0.227813 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0.744243, 0.797952 ],
|
||||
control2: [ 1.488486, 1.595904 ],
|
||||
to: [ 2.232729, 2.393856 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 2.302394, -1.564623 ],
|
||||
control2: [ 5.793201, -0.153598 ],
|
||||
to: [ 6.396692, 2.547372 ]
|
||||
}, %)
|
||||
|> bezierCurve({
|
||||
control1: [ 0.32074, 1.215468 ],
|
||||
control2: [ 0.06159, 2.564765 ],
|
||||
to: [ -0.690452, 3.573243 ]
|
||||
}, %)
|
||||
|> close(%)
|
||||
|> extrude(depth, %)
|
||||
|
||||
const box = startSketchOn('XY')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([0, 10], %)
|
||||
|> line([10, 0], %)
|
||||
|> line([0, -10], %, $revolveAxis)
|
||||
|> close(%)
|
||||
|> extrude(10, %)
|
||||
|
||||
const sketch001 = startSketchOn(box, revolveAxis)
|
||||
|> startProfileAt([5, 10], %)
|
||||
|> line([0, -10], %)
|
||||
|> line([2, 0], %)
|
||||
|> line([0, -10], %)
|
||||
|> close(%)
|
||||
|> revolve({
|
||||
axis: revolveAxis,
|
||||
angle: 90
|
||||
}, %)
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
svg(startSketchOn(keychain, 'end'), [-33, 32], -thickness)
|
||||
|
||||
startSketchOn(keychain, 'end')
|
||||
|> circle([
|
||||
width / 2,
|
||||
height - (keychainHoleSize + 1.5)
|
||||
], keychainHoleSize, %)
|
||||
|> extrude(-thickness, %)`
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { expect, Page, Download } from '@playwright/test'
|
||||
import { EngineCommand } from 'lang/std/artifactMap'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import os from 'os'
|
||||
import fsp from 'fs/promises'
|
||||
import pixelMatch from 'pixelmatch'
|
||||
@ -15,6 +15,23 @@ export const TEST_COLORS = {
|
||||
BLUE: [0, 0, 255] as TestColor,
|
||||
} as const
|
||||
|
||||
async function waitForPageLoadWithRetry(page: Page) {
|
||||
await expect(async () => {
|
||||
await page.goto('/')
|
||||
const errorMessage = 'App failed to load - 🔃 Retrying ...'
|
||||
await expect(page.getByTestId('loading'), errorMessage).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' }),
|
||||
errorMessage
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
}).toPass({ timeout: 70_000, intervals: [1_000] })
|
||||
}
|
||||
|
||||
async function waitForPageLoad(page: Page) {
|
||||
// wait for all spinners to be gone
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
@ -218,9 +235,12 @@ async function waitForAuthAndLsp(page: Page) {
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
await page.goto('/')
|
||||
await waitForPageLoad(page)
|
||||
if (process.env.CI) {
|
||||
await waitForPageLoadWithRetry(page)
|
||||
} else {
|
||||
await page.goto('/')
|
||||
await waitForPageLoad(page)
|
||||
}
|
||||
|
||||
return waitForLspPromise
|
||||
}
|
||||
@ -234,6 +254,7 @@ export async function getUtils(page: Page) {
|
||||
return {
|
||||
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
|
||||
waitForPageLoad: () => waitForPageLoad(page),
|
||||
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),
|
||||
removeCurrentCode: () => removeCurrentCode(page),
|
||||
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
|
||||
updateCamPosition: async (xyz: [number, number, number]) => {
|
||||
|
12
flake.lock
generated
@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1718470082,
|
||||
"narHash": "sha256-u2F0MMYE+Efc+ocruTbtU/wWHuYHWcJafp5zJ++n/YE=",
|
||||
"lastModified": 1721933792,
|
||||
"narHash": "sha256-zYVwABlQnxpbaHMfX6Wt9jhyQstFYwN2XjleOJV3VVg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3027ba73dfef68eb555fc2fa97aed4e999e74f97",
|
||||
"rev": "2122a9b35b35719ad9a395fe783eabb092df01b1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -43,11 +43,11 @@
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1718681902,
|
||||
"narHash": "sha256-E/T7Ge6ayEQe7FVKMJqDBoHyLhRhjc6u9CmU8MyYfy0=",
|
||||
"lastModified": 1721960387,
|
||||
"narHash": "sha256-o21ax+745ETGXrcgc/yUuLw1SI77ymp3xEpJt+w/kks=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "16c8ad83297c278eebe740dea5491c1708960dd1",
|
||||
"rev": "9cbf831c5b20a53354fc12758abd05966f9f1699",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -57,6 +57,7 @@
|
||||
pkg-config
|
||||
|
||||
nodejs_22
|
||||
yarn
|
||||
]) ++ pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [
|
||||
libiconv
|
||||
darwin.apple_sdk.frameworks.Security
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.24.6",
|
||||
"version": "0.24.9",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.17.0",
|
||||
@ -87,7 +87,8 @@
|
||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
|
||||
"postinstall": "yarn xstate:typegen",
|
||||
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
|
||||
"make:dev": "make dev"
|
||||
"make:dev": "make dev",
|
||||
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
@ -116,6 +117,7 @@
|
||||
"@tauri-apps/cli": "==2.0.0-beta.13",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^15.0.2",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/node": "^18.19.31",
|
||||
"@types/pixelmatch": "^5.2.6",
|
||||
@ -138,6 +140,7 @@
|
||||
"@wdio/spec-reporter": "^8.36.0",
|
||||
"@xstate/cli": "^0.5.17",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"d3-force": "^3.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
|
@ -18,11 +18,12 @@ export default defineConfig({
|
||||
/* Do not retry */
|
||||
retries: process.env.CI ? 0 : 0,
|
||||
/* Different amount of parallelism on CI and local. */
|
||||
workers: process.env.CI ? 4 : 4,
|
||||
workers: process.env.CI ? 1 : 4,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [
|
||||
[process.env.CI ? 'dot' : 'list'],
|
||||
['json', { outputFile: './test-results/report.json' }],
|
||||
['html'],
|
||||
],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
|
68
src-tauri/Cargo.lock
generated
@ -172,7 +172,9 @@ dependencies = [
|
||||
"kcl-lib",
|
||||
"kittycad",
|
||||
"log",
|
||||
"mdns-sd",
|
||||
"oauth2",
|
||||
"reqwest 0.12.4",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
@ -286,7 +288,7 @@ dependencies = [
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"polling 3.7.0",
|
||||
"rustix",
|
||||
"slab",
|
||||
"tracing",
|
||||
@ -1570,6 +1572,17 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@ -2405,6 +2418,16 @@ dependencies = [
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "if-addrs"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cabb0019d51a643781ff15c9c8a3e5dedc365c47211270f4e8f82812fedd8f0a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.2"
|
||||
@ -2626,6 +2649,7 @@ dependencies = [
|
||||
"tower-lsp",
|
||||
"ts-rs",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"validator",
|
||||
"wasm-bindgen",
|
||||
@ -2895,6 +2919,19 @@ version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
|
||||
[[package]]
|
||||
name = "mdns-sd"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "807457e493076539ff8f202806f9dc2eaa9f13f69701da7ed38eec7a9afd1616"
|
||||
dependencies = [
|
||||
"flume",
|
||||
"if-addrs",
|
||||
"log",
|
||||
"polling 2.8.0",
|
||||
"socket2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.2"
|
||||
@ -3634,6 +3671,22 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"libc",
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.7.0"
|
||||
@ -4585,9 +4638,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.121"
|
||||
version = "1.0.122"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609"
|
||||
checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"itoa 1.0.11",
|
||||
@ -4870,6 +4923,9 @@ name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
@ -6258,6 +6314,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "urlpattern"
|
||||
version = "0.2.0"
|
||||
|
@ -18,7 +18,9 @@ anyhow = "1"
|
||||
kcl-lib = { version = "0.2", path = "../src/wasm-lib/kcl" }
|
||||
kittycad = "0.3.7"
|
||||
log = "0.4.21"
|
||||
mdns-sd = "0.11.1"
|
||||
oauth2 = "4.4.2"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
serde_json = "1.0"
|
||||
tauri = { version = "2.0.0-beta.23", features = [ "devtools", "unstable"] }
|
||||
tauri-plugin-cli = { version = "2.0.0-beta.7" }
|
||||
|
@ -370,6 +370,70 @@ fn show_in_folder(app: tauri::AppHandle, path: &str) -> Result<(), InvokeError>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const SERVICE_NAME: &str = "_machine-api._tcp.local.";
|
||||
|
||||
async fn find_machine_api() -> Result<Option<String>> {
|
||||
println!("Looking for machine API...");
|
||||
// Timeout if no response is received after 5 seconds.
|
||||
let timeout_duration = std::time::Duration::from_secs(5);
|
||||
|
||||
let mdns = mdns_sd::ServiceDaemon::new()?;
|
||||
|
||||
// Browse for a service type.
|
||||
let receiver = mdns.browse(SERVICE_NAME)?;
|
||||
let resp = tokio::time::timeout(
|
||||
timeout_duration,
|
||||
tokio::spawn(async move {
|
||||
while let Ok(event) = receiver.recv() {
|
||||
if let mdns_sd::ServiceEvent::ServiceResolved(info) = event {
|
||||
if let Some(addr) = info.get_addresses().iter().next() {
|
||||
return Some(format!("{}:{}", addr, info.get_port()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Shut down.
|
||||
mdns.shutdown()?;
|
||||
|
||||
let Ok(Ok(Some(addr))) = resp else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(addr))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_machine_api_ip() -> Result<Option<String>, InvokeError> {
|
||||
let machine_api = find_machine_api().await.map_err(InvokeError::from_anyhow)?;
|
||||
|
||||
Ok(machine_api)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn list_machines() -> Result<String, InvokeError> {
|
||||
let machine_api = find_machine_api().await.map_err(InvokeError::from_anyhow)?;
|
||||
|
||||
let Some(machine_api) = machine_api else {
|
||||
// Empty array.
|
||||
return Ok("[]".to_string());
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(format!("http://{}/machines", machine_api))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
|
||||
let text = response.text().await.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn open_url_sync(app: &tauri::AppHandle, url: &url::Url) {
|
||||
log::debug!("Opening URL: {:?}", url);
|
||||
@ -417,6 +481,8 @@ fn main() -> Result<()> {
|
||||
read_project_settings_file,
|
||||
write_project_settings_file,
|
||||
rename_project_directory,
|
||||
get_machine_api_ip,
|
||||
list_machines
|
||||
])
|
||||
.plugin(tauri_plugin_cli::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
|
@ -80,5 +80,5 @@
|
||||
}
|
||||
},
|
||||
"productName": "Zoo Modeling App",
|
||||
"version": "0.24.6"
|
||||
"version": "0.24.9"
|
||||
}
|
||||
|
22
src/App.tsx
@ -2,7 +2,7 @@ import { MouseEventHandler, useEffect, useMemo, useRef } from 'react'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||
import { Stream } from './components/Stream'
|
||||
import { EngineCommand } from 'lang/std/artifactMap'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import { throttle } from './lib/utils'
|
||||
import { AppHeader } from './components/AppHeader'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
@ -95,16 +95,16 @@ export function App() {
|
||||
})
|
||||
|
||||
const newCmdId = uuidv4()
|
||||
if (context.store?.buttonDownInStream === undefined) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'highlight_set_entity',
|
||||
selected_at_window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
if (state.matches('idle.showPlanes')) return
|
||||
if (context.store?.buttonDownInStream !== undefined) return
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'highlight_set_entity',
|
||||
selected_at_window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
198
src/Toolbar.tsx
@ -40,10 +40,10 @@ export function Toolbar({
|
||||
return false
|
||||
}
|
||||
return isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
engineCommandManager.artifactGraph,
|
||||
context.selectionRanges
|
||||
)
|
||||
}, [engineCommandManager.artifactMap, context.selectionRanges])
|
||||
}, [engineCommandManager.artifactGraph, context.selectionRanges])
|
||||
|
||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||
const { overallState } = useNetworkContext()
|
||||
@ -190,49 +190,59 @@ export function Toolbar({
|
||||
maybeIconConfig[0].onClick(configCallbackProps)
|
||||
}
|
||||
>
|
||||
<ToolbarItemContents
|
||||
itemConfig={maybeIconConfig[0]}
|
||||
configCallbackProps={configCallbackProps}
|
||||
/>
|
||||
<span
|
||||
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
|
||||
>
|
||||
{maybeIconConfig[0].title}
|
||||
</span>
|
||||
</ActionButton>
|
||||
<ToolbarItemTooltip
|
||||
itemConfig={maybeIconConfig[0]}
|
||||
configCallbackProps={configCallbackProps}
|
||||
/>
|
||||
</ActionButtonDropdown>
|
||||
)
|
||||
}
|
||||
const itemConfig = maybeIconConfig
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
Element="button"
|
||||
key={itemConfig.id}
|
||||
id={itemConfig.id}
|
||||
data-testid={itemConfig.id}
|
||||
iconStart={{
|
||||
icon: itemConfig.icon,
|
||||
className: iconClassName,
|
||||
bgClassName: bgClassName,
|
||||
}}
|
||||
className={
|
||||
'pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
||||
buttonBorderClassName +
|
||||
' ' +
|
||||
buttonBgClassName +
|
||||
(!itemConfig.showTitle ? ' !px-0' : '')
|
||||
}
|
||||
name={itemConfig.title}
|
||||
aria-description={itemConfig.description}
|
||||
aria-pressed={itemConfig.isActive}
|
||||
disabled={
|
||||
disableAllButtons ||
|
||||
itemConfig.status !== 'available' ||
|
||||
itemConfig.disabled
|
||||
}
|
||||
onClick={() => itemConfig.onClick(configCallbackProps)}
|
||||
>
|
||||
<ToolbarItemContents
|
||||
<div className="relative" key={itemConfig.id}>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
key={itemConfig.id}
|
||||
id={itemConfig.id}
|
||||
data-testid={itemConfig.id}
|
||||
iconStart={{
|
||||
icon: itemConfig.icon,
|
||||
className: iconClassName,
|
||||
bgClassName: bgClassName,
|
||||
}}
|
||||
className={
|
||||
'pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
||||
buttonBorderClassName +
|
||||
' ' +
|
||||
buttonBgClassName +
|
||||
(!itemConfig.showTitle ? ' !px-0' : '')
|
||||
}
|
||||
name={itemConfig.title}
|
||||
aria-description={itemConfig.description}
|
||||
aria-pressed={itemConfig.isActive}
|
||||
disabled={
|
||||
disableAllButtons ||
|
||||
itemConfig.status !== 'available' ||
|
||||
itemConfig.disabled
|
||||
}
|
||||
onClick={() => itemConfig.onClick(configCallbackProps)}
|
||||
>
|
||||
<span className={!itemConfig.showTitle ? 'sr-only' : ''}>
|
||||
{itemConfig.title}
|
||||
</span>
|
||||
</ActionButton>
|
||||
<ToolbarItemTooltip
|
||||
itemConfig={itemConfig}
|
||||
configCallbackProps={configCallbackProps}
|
||||
/>
|
||||
</ActionButton>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
@ -250,7 +260,7 @@ export function Toolbar({
|
||||
* It contains a tooltip with the title, description, and links
|
||||
* and a hotkey listener
|
||||
*/
|
||||
const ToolbarItemContents = memo(function ToolbarItemContents({
|
||||
const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
itemConfig,
|
||||
configCallbackProps,
|
||||
}: {
|
||||
@ -272,73 +282,69 @@ const ToolbarItemContents = memo(function ToolbarItemContents({
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={!itemConfig.showTitle ? 'sr-only' : ''}>
|
||||
{itemConfig.title}
|
||||
</span>
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
wrapperClassName="!p-4 !pointer-events-auto"
|
||||
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
|
||||
>
|
||||
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
|
||||
<span
|
||||
className={`text-sm flex-1 ${
|
||||
itemConfig.status !== 'available'
|
||||
? 'text-chalkboard-70 dark:text-chalkboard-40'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{itemConfig.title}
|
||||
</span>
|
||||
{itemConfig.status === 'available' && itemConfig.hotkey ? (
|
||||
<kbd className="flex-none hotkey">{itemConfig.hotkey}</kbd>
|
||||
) : itemConfig.status === 'kcl-only' ? (
|
||||
<Tooltip
|
||||
inert={false}
|
||||
position="bottom"
|
||||
wrapperClassName="!p-4 !pointer-events-auto"
|
||||
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
|
||||
>
|
||||
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
|
||||
<span
|
||||
className={`text-sm flex-1 ${
|
||||
itemConfig.status !== 'available'
|
||||
? 'text-chalkboard-70 dark:text-chalkboard-40'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{itemConfig.title}
|
||||
</span>
|
||||
{itemConfig.status === 'available' && itemConfig.hotkey ? (
|
||||
<kbd className="flex-none hotkey">{itemConfig.hotkey}</kbd>
|
||||
) : itemConfig.status === 'kcl-only' ? (
|
||||
<>
|
||||
<span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
|
||||
KCL code only
|
||||
</span>
|
||||
<CustomIcon
|
||||
name="code"
|
||||
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
itemConfig.status === 'unavailable' && (
|
||||
<>
|
||||
<span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
|
||||
KCL code only
|
||||
In development
|
||||
</span>
|
||||
<CustomIcon
|
||||
name="code"
|
||||
name="lockClosed"
|
||||
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
itemConfig.status === 'unavailable' && (
|
||||
<>
|
||||
<span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
|
||||
In development
|
||||
</span>
|
||||
<CustomIcon
|
||||
name="lockClosed"
|
||||
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<p className="px-2 text-ch font-sans">{itemConfig.description}</p>
|
||||
{itemConfig.links.length > 0 && (
|
||||
<>
|
||||
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />
|
||||
<ul className="p-0 px-1 m-0 flex flex-col">
|
||||
{itemConfig.links.map((link) => (
|
||||
<li key={link.label} className="contents">
|
||||
<a
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit"
|
||||
>
|
||||
<span className="flex-1">Open {link.label}</span>
|
||||
<CustomIcon name="link" className="w-4 h-4" />
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</Tooltip>
|
||||
</>
|
||||
</div>
|
||||
<p className="px-2 text-ch font-sans">{itemConfig.description}</p>
|
||||
{itemConfig.links.length > 0 && (
|
||||
<>
|
||||
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />
|
||||
<ul className="p-0 px-1 m-0 flex flex-col">
|
||||
{itemConfig.links.map((link) => (
|
||||
<li key={link.label} className="contents">
|
||||
<a
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit"
|
||||
>
|
||||
<span className="flex-1">Open {link.label}</span>
|
||||
<CustomIcon name="link" className="w-4 h-4" />
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
|
@ -21,7 +21,7 @@ import {
|
||||
EngineCommandManager,
|
||||
UnreliableSubscription,
|
||||
} from 'lang/std/engineConnection'
|
||||
import { EngineCommand } from 'lang/std/artifactMap'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { deg2Rad } from 'lib/utils2d'
|
||||
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
||||
|
@ -5,7 +5,7 @@ import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
||||
import { ReactCameraProperties } from './CameraControls'
|
||||
import { isArray, throttle } from 'lib/utils'
|
||||
import { throttle } from 'lib/utils'
|
||||
import {
|
||||
sceneInfra,
|
||||
kclManager,
|
||||
@ -20,17 +20,13 @@ import {
|
||||
getParentGroup,
|
||||
} from './sceneEntities'
|
||||
import { SegmentOverlay, SketchDetails } from 'machines/modelingMachine'
|
||||
import {
|
||||
expectNodeOnPath,
|
||||
findUsesOfTagInPipe,
|
||||
getLastNodeFromPath,
|
||||
getNodeFromPath,
|
||||
} from 'lang/queryAst'
|
||||
import { findUsesOfTagInPipe, getNodeFromPath } from 'lang/queryAst'
|
||||
import {
|
||||
CallExpression,
|
||||
PathToNode,
|
||||
Program,
|
||||
SourceRange,
|
||||
Value,
|
||||
parse,
|
||||
recast,
|
||||
} from 'lang/wasm'
|
||||
@ -106,6 +102,7 @@ export const ClientSideScene = ({
|
||||
canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false)
|
||||
canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false)
|
||||
sceneInfra.setSend(send)
|
||||
engineCommandManager.modelingSend = send
|
||||
return () => {
|
||||
canvas?.removeEventListener('mousemove', sceneInfra.onMouseMove)
|
||||
canvas?.removeEventListener('mousedown', sceneInfra.onMouseDown)
|
||||
@ -190,12 +187,13 @@ const Overlay = ({
|
||||
let xAlignment = overlay.angle < 0 ? '0%' : '-100%'
|
||||
let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%'
|
||||
|
||||
const callExpression = expectNodeOnPath<CallExpression>(
|
||||
const _node1 = getNodeFromPath<CallExpression>(
|
||||
kclManager.ast,
|
||||
overlay.pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(callExpression)) return
|
||||
if (err(_node1)) return
|
||||
const callExpression = _node1.node
|
||||
|
||||
const constraints = getConstraintInfo(
|
||||
callExpression,
|
||||
@ -552,13 +550,13 @@ const ConstraintSymbol = ({
|
||||
varNameMap[_type as LineInputsType]?.implicitConstraintDesc
|
||||
|
||||
const _node = useMemo(
|
||||
() => getLastNodeFromPath(kclManager.ast, pathToNode),
|
||||
() => getNodeFromPath<Value>(kclManager.ast, pathToNode),
|
||||
[kclManager.ast, pathToNode]
|
||||
)
|
||||
if (err(_node)) return
|
||||
const node = _node.node
|
||||
|
||||
const range: SourceRange = !isArray(node) ? [node.start, node.end] : [0, 0]
|
||||
const range: SourceRange = node ? [node.start, node.end] : [0, 0]
|
||||
|
||||
if (_type === 'intersectionTag') return null
|
||||
|
||||
@ -577,10 +575,10 @@ const ConstraintSymbol = ({
|
||||
: 'bg-primary/30 dark:bg-primary text-primary dark:text-chalkboard-10 dark:border-transparent group-hover:bg-primary/40 group-hover:border-primary/50 group-hover:brightness-125'
|
||||
} h-[26px] w-[26px] rounded-sm relative m-0 p-0`}
|
||||
onMouseEnter={() => {
|
||||
editorManager.setHighlightRange(range)
|
||||
editorManager.setHighlightRange([range])
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
editorManager.setHighlightRange([0, 0])
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
}}
|
||||
// disabled={isConstrained || !convertToVarEnabled}
|
||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||
|
@ -22,9 +22,6 @@ import {
|
||||
import {
|
||||
ARROWHEAD,
|
||||
AXIS_GROUP,
|
||||
DEFAULT_PLANES,
|
||||
DefaultPlane,
|
||||
defaultPlaneColor,
|
||||
getSceneScale,
|
||||
INTERSECTION_PLANE_LAYER,
|
||||
OnClickCallbackArgs,
|
||||
@ -61,12 +58,7 @@ import {
|
||||
codeManager,
|
||||
editorManager,
|
||||
} from 'lib/singletons'
|
||||
import {
|
||||
isNodeType,
|
||||
expectNodeOnPath,
|
||||
getNodeFromPath,
|
||||
getNodePathFromSourceRange,
|
||||
} from 'lang/queryAst'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { executeAst } from 'lang/langHelpers'
|
||||
import {
|
||||
createArcGeometry,
|
||||
@ -82,13 +74,7 @@ import {
|
||||
changeSketchArguments,
|
||||
updateStartProfileAtArgs,
|
||||
} from 'lang/std/sketch'
|
||||
import {
|
||||
isArray,
|
||||
isOverlap,
|
||||
normaliseAngle,
|
||||
roundOff,
|
||||
throttle,
|
||||
} from 'lib/utils'
|
||||
import { isOverlap, normaliseAngle, roundOff, throttle } from 'lib/utils'
|
||||
import {
|
||||
addStartProfileAt,
|
||||
createArrayExpression,
|
||||
@ -98,11 +84,7 @@ import {
|
||||
createPipeSubstitution,
|
||||
findUniqueName,
|
||||
} from 'lang/modifyAst'
|
||||
import {
|
||||
Selections,
|
||||
getEventForSegmentSelection,
|
||||
sendSelectEventToEngine,
|
||||
} from 'lib/selections'
|
||||
import { Selections, getEventForSegmentSelection } from 'lib/selections'
|
||||
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
||||
import { createGridHelper, orthoScale, perspScale } from './helpers'
|
||||
import { Models } from '@kittycad/lib'
|
||||
@ -213,6 +195,7 @@ export class SceneEntities {
|
||||
|
||||
createIntersectionPlane() {
|
||||
if (sceneInfra.scene.getObjectByName(RAYCASTABLE_PLANE)) {
|
||||
// this.removeIntersectionPlane()
|
||||
console.warn('createIntersectionPlane called when it already exists')
|
||||
return
|
||||
}
|
||||
@ -474,18 +457,13 @@ export class SceneEntities {
|
||||
)
|
||||
|
||||
let seg: Group
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
const _node1 = getNodeFromPath<CallExpression>(
|
||||
maybeModdedAst,
|
||||
segPathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(callExp)) return
|
||||
const callExpName = isNodeType<CallExpression>(
|
||||
callExp.node,
|
||||
'CallExpression'
|
||||
)
|
||||
? callExp.node.callee.name
|
||||
: ''
|
||||
if (err(_node1)) return
|
||||
const callExpName = _node1.node?.callee?.name
|
||||
|
||||
if (segment.type === 'TangentialArcTo') {
|
||||
seg = tangentialArcToSegment({
|
||||
@ -606,12 +584,8 @@ export class SceneEntities {
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node1)) return Promise.reject(_node1)
|
||||
const variableDeclarationName = isNodeType<VariableDeclaration>(
|
||||
_node1.node,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
? _node1.node.declarations[0]?.id?.name || ''
|
||||
: ''
|
||||
const variableDeclarationName =
|
||||
_node1.node?.declarations?.[0]?.id?.name || ''
|
||||
|
||||
const sg = kclManager.programMemory.get(
|
||||
variableDeclarationName
|
||||
@ -748,14 +722,15 @@ export class SceneEntities {
|
||||
) => {
|
||||
let _ast = structuredClone(kclManager.ast)
|
||||
|
||||
const varDec = expectNodeOnPath<VariableDeclaration>(
|
||||
const _node1 = getNodeFromPath<VariableDeclaration>(
|
||||
_ast,
|
||||
sketchPathToNode || [],
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(varDec)) return Promise.reject(varDec)
|
||||
const variableDeclarationName = varDec.declarations?.[0]?.id?.name || ''
|
||||
const startSketchOn = varDec.declarations
|
||||
if (trap(_node1)) return Promise.reject(_node1)
|
||||
const variableDeclarationName =
|
||||
_node1.node?.declarations?.[0]?.id?.name || ''
|
||||
const startSketchOn = _node1.node?.declarations
|
||||
const startSketchOnInit = startSketchOn?.[0]?.init
|
||||
|
||||
const tags: [string, string, string] = [
|
||||
@ -794,17 +769,12 @@ export class SceneEntities {
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node)) return Promise.reject(_node)
|
||||
const sketchInit = isNodeType<VariableDeclaration>(
|
||||
_node.node,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
? _node.node.declarations[0]?.init
|
||||
: null
|
||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
||||
|
||||
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
|
||||
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
|
||||
|
||||
if (sketchInit?.type === 'PipeExpression') {
|
||||
if (sketchInit.type === 'PipeExpression') {
|
||||
updateRectangleSketch(sketchInit, x, y, tags[0])
|
||||
}
|
||||
|
||||
@ -847,14 +817,9 @@ export class SceneEntities {
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node)) return Promise.reject(_node)
|
||||
const sketchInit = isNodeType<VariableDeclaration>(
|
||||
_node.node,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
? _node.node.declarations[0]?.init
|
||||
: null
|
||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
||||
|
||||
if (sketchInit?.type === 'PipeExpression') {
|
||||
if (sketchInit.type === 'PipeExpression') {
|
||||
updateRectangleSketch(sketchInit, x, y, tags[0])
|
||||
|
||||
let _recastAst = parse(recast(_ast))
|
||||
@ -1090,7 +1055,7 @@ export class SceneEntities {
|
||||
if (trap(_node)) return
|
||||
const node = _node.node
|
||||
|
||||
if (isArray(node) || node.type !== 'CallExpression') return
|
||||
if (node.type !== 'CallExpression') return
|
||||
|
||||
let modded:
|
||||
| {
|
||||
@ -1531,146 +1496,6 @@ export class SceneEntities {
|
||||
this._tearDownSketch(0, resolve, reject, { removeAxis })
|
||||
})
|
||||
}
|
||||
setupDefaultPlaneHover() {
|
||||
sceneInfra.setCallbacks({
|
||||
onMouseEnter: ({ selected }) => {
|
||||
if (!(selected instanceof Mesh && selected.parent)) return
|
||||
if (selected.parent.userData.type !== DEFAULT_PLANES) return
|
||||
const type: DefaultPlane = selected.userData.type
|
||||
selected.material.color = defaultPlaneColor(type, 0.5, 1)
|
||||
},
|
||||
onMouseLeave: ({ selected }) => {
|
||||
if (!(selected instanceof Mesh && selected.parent)) return
|
||||
if (selected.parent.userData.type !== DEFAULT_PLANES) return
|
||||
const type: DefaultPlane = selected.userData.type
|
||||
selected.material.color = defaultPlaneColor(type)
|
||||
},
|
||||
onClick: async (args) => {
|
||||
const { entity_id } = await sendSelectEventToEngine(
|
||||
args?.mouseEvent,
|
||||
document.getElementById('video-stream') as HTMLVideoElement,
|
||||
sceneInfra._streamDimensions
|
||||
)
|
||||
|
||||
let _entity_id = entity_id
|
||||
if (!_entity_id) return
|
||||
if (
|
||||
engineCommandManager.defaultPlanes?.xy === _entity_id ||
|
||||
engineCommandManager.defaultPlanes?.xz === _entity_id ||
|
||||
engineCommandManager.defaultPlanes?.yz === _entity_id ||
|
||||
engineCommandManager.defaultPlanes?.negXy === _entity_id ||
|
||||
engineCommandManager.defaultPlanes?.negXz === _entity_id ||
|
||||
engineCommandManager.defaultPlanes?.negYz === _entity_id
|
||||
) {
|
||||
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
|
||||
[engineCommandManager.defaultPlanes.xy]: 'XY',
|
||||
[engineCommandManager.defaultPlanes.xz]: 'XZ',
|
||||
[engineCommandManager.defaultPlanes.yz]: 'YZ',
|
||||
[engineCommandManager.defaultPlanes.negXy]: '-XY',
|
||||
[engineCommandManager.defaultPlanes.negXz]: '-XZ',
|
||||
[engineCommandManager.defaultPlanes.negYz]: '-YZ',
|
||||
}
|
||||
// TODO can we get this information from rust land when it creates the default planes?
|
||||
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
|
||||
let zAxis: [number, number, number] = [0, 0, 1]
|
||||
let yAxis: [number, number, number] = [0, 1, 0]
|
||||
|
||||
// get unit vector from camera position to target
|
||||
const camVector = sceneInfra.camControls.camera.position
|
||||
.clone()
|
||||
.sub(sceneInfra.camControls.target)
|
||||
|
||||
if (engineCommandManager.defaultPlanes?.xy === _entity_id) {
|
||||
zAxis = [0, 0, 1]
|
||||
yAxis = [0, 1, 0]
|
||||
if (camVector.z < 0) {
|
||||
zAxis = [0, 0, -1]
|
||||
_entity_id = engineCommandManager.defaultPlanes?.negXy || ''
|
||||
}
|
||||
} else if (engineCommandManager.defaultPlanes?.yz === _entity_id) {
|
||||
zAxis = [1, 0, 0]
|
||||
yAxis = [0, 0, 1]
|
||||
if (camVector.x < 0) {
|
||||
zAxis = [-1, 0, 0]
|
||||
_entity_id = engineCommandManager.defaultPlanes?.negYz || ''
|
||||
}
|
||||
} else if (engineCommandManager.defaultPlanes?.xz === _entity_id) {
|
||||
zAxis = [0, 1, 0]
|
||||
yAxis = [0, 0, 1]
|
||||
_entity_id = engineCommandManager.defaultPlanes?.negXz || ''
|
||||
if (camVector.y < 0) {
|
||||
zAxis = [0, -1, 0]
|
||||
_entity_id = engineCommandManager.defaultPlanes?.xz || ''
|
||||
}
|
||||
}
|
||||
|
||||
sceneInfra.modelingSend({
|
||||
type: 'Select default plane',
|
||||
data: {
|
||||
type: 'defaultPlane',
|
||||
planeId: _entity_id,
|
||||
plane: defaultPlaneStrMap[_entity_id],
|
||||
zAxis,
|
||||
yAxis,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
const artifact = this.engineCommandManager.artifactMap[_entity_id]
|
||||
// If we clicked on an extrude wall, we climb up the parent Id
|
||||
// to get the sketch profile's face ID. If we clicked on an endcap,
|
||||
// we already have it.
|
||||
const pathId =
|
||||
artifact?.type === 'extrudeWall' || artifact?.type === 'extrudeCap'
|
||||
? artifact.pathId
|
||||
: ''
|
||||
|
||||
// tsc cannot infer that target can have extrusions
|
||||
// from the commandType (why?) so we need to cast it
|
||||
const path = this.engineCommandManager.artifactMap?.[pathId || '']
|
||||
const extrusionId =
|
||||
path?.type === 'startPath' ? path.extrusionIds[0] : ''
|
||||
|
||||
// TODO: We get the first extrusion command ID,
|
||||
// which is fine while backend systems only support one extrusion.
|
||||
// but we need to more robustly handle resolving to the correct extrusion
|
||||
// if there are multiple.
|
||||
const extrusions = this.engineCommandManager.artifactMap?.[extrusionId]
|
||||
|
||||
if (artifact?.type !== 'extrudeCap' && artifact?.type !== 'extrudeWall')
|
||||
return
|
||||
|
||||
const faceInfo = await getFaceDetails(_entity_id)
|
||||
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis) return
|
||||
const { z_axis, y_axis, origin } = faceInfo
|
||||
const sketchPathToNode = getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
artifact.range
|
||||
)
|
||||
|
||||
const extrudePathToNode = extrusions?.range
|
||||
? getNodePathFromSourceRange(kclManager.ast, extrusions.range)
|
||||
: []
|
||||
|
||||
sceneInfra.modelingSend({
|
||||
type: 'Select default plane',
|
||||
data: {
|
||||
type: 'extrudeFace',
|
||||
zAxis: [z_axis.x, z_axis.y, z_axis.z],
|
||||
yAxis: [y_axis.x, y_axis.y, y_axis.z],
|
||||
position: [origin.x, origin.y, origin.z].map(
|
||||
(num) => num / sceneInfra._baseUnitMultiplier
|
||||
) as [number, number, number],
|
||||
sketchPathToNode,
|
||||
extrudePathToNode,
|
||||
cap: artifact.type === 'extrudeCap' ? artifact.cap : 'none',
|
||||
faceId: _entity_id,
|
||||
},
|
||||
})
|
||||
return
|
||||
},
|
||||
})
|
||||
}
|
||||
mouseEnterLeaveCallbacks() {
|
||||
return {
|
||||
onMouseEnter: ({ selected, dragSelected }: OnMouseEnterLeaveArgs) => {
|
||||
@ -1695,9 +1520,7 @@ export class SceneEntities {
|
||||
)
|
||||
if (trap(_node, { suppress: true })) return
|
||||
const node = _node.node
|
||||
editorManager.setHighlightRange(
|
||||
!isArray(node) ? [node.start, node.end] : [0, 0]
|
||||
)
|
||||
editorManager.setHighlightRange([[node.start, node.end]])
|
||||
const yellow = 0xffff00
|
||||
colorSegment(selected, yellow)
|
||||
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||
@ -1733,10 +1556,10 @@ export class SceneEntities {
|
||||
}
|
||||
return
|
||||
}
|
||||
editorManager.setHighlightRange([0, 0])
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
},
|
||||
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
|
||||
editorManager.setHighlightRange([0, 0])
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
const parent = getParentGroup(selected, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
@ -1827,14 +1650,7 @@ function prepareTruncatedMemoryAndAst(
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(_node)) return _node
|
||||
if (isArray(_node.node))
|
||||
return new Error('Expected node to be an object, but found Array')
|
||||
const variableDeclarationName = isNodeType<VariableDeclaration>(
|
||||
_node.node,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
? _node.node.declarations[0]?.id?.name || ''
|
||||
: ''
|
||||
const variableDeclarationName = _node.node?.declarations?.[0]?.id?.name || ''
|
||||
const lastSeg = (
|
||||
programMemory.get(variableDeclarationName) as SketchGroup
|
||||
).value.slice(-1)[0]
|
||||
@ -1957,12 +1773,7 @@ export function sketchGroupFromPathToNode({
|
||||
)
|
||||
if (err(_varDec)) return _varDec
|
||||
const varDec = _varDec.node
|
||||
if (isArray(varDec))
|
||||
return new Error('Expected node to be an object, but found Array')
|
||||
const varName = isNodeType<VariableDeclarator>(varDec, 'VariableDeclarator')
|
||||
? varDec.id.name
|
||||
: ''
|
||||
const result = programMemory.get(varName)
|
||||
const result = programMemory.get(varDec?.id?.name || '')
|
||||
if (result?.type === 'ExtrudeGroup') {
|
||||
return result.sketchGroup
|
||||
}
|
||||
|
@ -11,10 +11,8 @@ import {
|
||||
Raycaster,
|
||||
Vector2,
|
||||
Group,
|
||||
PlaneGeometry,
|
||||
MeshBasicMaterial,
|
||||
Mesh,
|
||||
DoubleSide,
|
||||
Intersection,
|
||||
Object3D,
|
||||
Object3DEventMap,
|
||||
@ -48,7 +46,6 @@ export const DEBUG_SHOW_INTERSECTION_PLANE: false = false
|
||||
export const DEBUG_SHOW_BOTH_SCENES: false = false
|
||||
|
||||
export const RAYCASTABLE_PLANE = 'raycastable-plane'
|
||||
export const DEFAULT_PLANES = 'default-planes'
|
||||
|
||||
export const X_AXIS = 'xAxis'
|
||||
export const Y_AXIS = 'yAxis'
|
||||
@ -325,16 +322,9 @@ export class SceneInfra {
|
||||
this.camControls.camera,
|
||||
this.camControls.target
|
||||
)
|
||||
const planesGroup = this.scene.getObjectByName(DEFAULT_PLANES)
|
||||
const axisGroup = this.scene
|
||||
.getObjectByName(AXIS_GROUP)
|
||||
?.getObjectByName('gridHelper')
|
||||
planesGroup &&
|
||||
planesGroup.scale.set(
|
||||
scale / this._baseUnitMultiplier,
|
||||
scale / this._baseUnitMultiplier,
|
||||
scale / this._baseUnitMultiplier
|
||||
)
|
||||
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
||||
}
|
||||
|
||||
@ -632,59 +622,6 @@ export class SceneInfra {
|
||||
this.onClickCallback({ mouseEvent, intersects })
|
||||
}
|
||||
}
|
||||
showDefaultPlanes() {
|
||||
const addPlane = (
|
||||
rotation: { x: number; y: number; z: number }, //
|
||||
type: DefaultPlane
|
||||
): Mesh => {
|
||||
const planeGeometry = new PlaneGeometry(100, 100)
|
||||
const planeMaterial = new MeshBasicMaterial({
|
||||
color: defaultPlaneColor(type),
|
||||
transparent: true,
|
||||
opacity: 0.0,
|
||||
side: DoubleSide,
|
||||
depthTest: false, // needed to avoid transparency issues
|
||||
})
|
||||
const plane = new Mesh(planeGeometry, planeMaterial)
|
||||
plane.rotation.x = rotation.x
|
||||
plane.rotation.y = rotation.y
|
||||
plane.rotation.z = rotation.z
|
||||
plane.userData.type = type
|
||||
plane.name = type
|
||||
return plane
|
||||
}
|
||||
const planes = [
|
||||
addPlane({ x: 0, y: Math.PI / 2, z: 0 }, YZ_PLANE),
|
||||
addPlane({ x: 0, y: 0, z: 0 }, XY_PLANE),
|
||||
addPlane({ x: -Math.PI / 2, y: 0, z: 0 }, XZ_PLANE),
|
||||
]
|
||||
const planesGroup = new Group()
|
||||
planesGroup.userData.type = DEFAULT_PLANES
|
||||
planesGroup.name = DEFAULT_PLANES
|
||||
planesGroup.add(...planes)
|
||||
planesGroup.traverse((child) => {
|
||||
if (child instanceof Mesh) {
|
||||
child.layers.enable(SKETCH_LAYER)
|
||||
}
|
||||
})
|
||||
planesGroup.layers.enable(SKETCH_LAYER)
|
||||
const sceneScale = getSceneScale(
|
||||
this.camControls.camera,
|
||||
this.camControls.target
|
||||
)
|
||||
planesGroup.scale.set(
|
||||
sceneScale / this._baseUnitMultiplier,
|
||||
sceneScale / this._baseUnitMultiplier,
|
||||
sceneScale / this._baseUnitMultiplier
|
||||
)
|
||||
this.scene.add(planesGroup)
|
||||
}
|
||||
removeDefaultPlanes() {
|
||||
const planesGroup = this.scene.children.find(
|
||||
({ userData }) => userData.type === DEFAULT_PLANES
|
||||
)
|
||||
if (planesGroup) this.scene.remove(planesGroup)
|
||||
}
|
||||
updateOtherSelectionColors = (otherSelections: Axis[]) => {
|
||||
const axisGroup = this.scene.children.find(
|
||||
({ userData }) => userData?.type === AXIS_GROUP
|
||||
@ -742,28 +679,3 @@ function baseUnitTomm(baseUnit: BaseUnit) {
|
||||
return 914.4
|
||||
}
|
||||
}
|
||||
|
||||
export type DefaultPlane =
|
||||
| 'xy-default-plane'
|
||||
| 'xz-default-plane'
|
||||
| 'yz-default-plane'
|
||||
|
||||
export const XY_PLANE: DefaultPlane = 'xy-default-plane'
|
||||
export const XZ_PLANE: DefaultPlane = 'xz-default-plane'
|
||||
export const YZ_PLANE: DefaultPlane = 'yz-default-plane'
|
||||
|
||||
export function defaultPlaneColor(
|
||||
plane: DefaultPlane,
|
||||
lowCh = 0.1,
|
||||
highCh = 0.7
|
||||
): Color {
|
||||
switch (plane) {
|
||||
case XY_PLANE:
|
||||
return new Color(highCh, lowCh, lowCh)
|
||||
case XZ_PLANE:
|
||||
return new Color(lowCh, lowCh, highCh)
|
||||
case YZ_PLANE:
|
||||
return new Color(lowCh, highCh, lowCh)
|
||||
}
|
||||
return new Color(lowCh, lowCh, lowCh)
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ export function AstExplorer() {
|
||||
<div
|
||||
className="h-full relative"
|
||||
onMouseLeave={(e) => {
|
||||
editorManager.setHighlightRange([0, 0])
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
}}
|
||||
>
|
||||
<pre className="text-xs">
|
||||
@ -113,12 +113,12 @@ function DisplayObj({
|
||||
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
|
||||
}`}
|
||||
onMouseEnter={(e) => {
|
||||
editorManager.setHighlightRange([obj?.start || 0, obj.end])
|
||||
editorManager.setHighlightRange([[obj?.start || 0, obj.end]])
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onMouseMove={(e) => {
|
||||
e.stopPropagation()
|
||||
editorManager.setHighlightRange([obj?.start || 0, obj.end])
|
||||
editorManager.setHighlightRange([[obj?.start || 0, obj.end]])
|
||||
}}
|
||||
onClick={(e) => {
|
||||
send({
|
||||
|
@ -124,7 +124,11 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
4
|
||||
)
|
||||
) : typeof argValue === 'object' ? (
|
||||
JSON.stringify(argValue)
|
||||
arg.valueSummary ? (
|
||||
arg.valueSummary(argValue)
|
||||
) : (
|
||||
JSON.stringify(argValue)
|
||||
)
|
||||
) : (
|
||||
<em>{argValue}</em>
|
||||
)
|
||||
|
@ -541,6 +541,16 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
printer3d: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M16 5H4V7.5H7V7V6H8H9H10V7V7.5H16V5ZM17 7.5V8.5V15V16V17H16V16H15H14H6H5H4V17H3V16V15V8.5V7.5V5V4H4H16H17V5V7.5ZM4 8.5V15H5V13.5V13H5.5H14.5H15V13.5V15H16V8.5H10V9H9V10L8.5 10.5L8 10V9H7V8.5H4ZM14 14V15H6V14H14ZM8 7H9V8H8V7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
polygon: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
|
@ -5,6 +5,8 @@ import { CustomIcon } from './CustomIcon'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { createAndOpenNewProject } from 'lib/tauriFS'
|
||||
import { paths } from 'lib/paths'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import { useLspContext } from './LspProvider'
|
||||
|
||||
const HelpMenuDivider = () => (
|
||||
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
|
||||
@ -12,16 +14,18 @@ const HelpMenuDivider = () => (
|
||||
|
||||
export function HelpMenu(props: React.PropsWithChildren) {
|
||||
const location = useLocation()
|
||||
const { onProjectOpen } = useLspContext()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const isInProject = location.pathname.includes(paths.FILE)
|
||||
const navigate = useNavigate()
|
||||
const { settings } = useSettingsAuthContext()
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<Popover.Button className="border-none p-0 m-0 rounded-full grid place-content-center">
|
||||
<Popover.Button className="grid p-0 m-0 border-none rounded-full place-content-center">
|
||||
<CustomIcon
|
||||
name="questionMark"
|
||||
className="w-7 h-7 rounded-full bg-chalkboard-110 dark:bg-chalkboard-80 text-chalkboard-10"
|
||||
className="rounded-full w-7 h-7 bg-chalkboard-110 dark:bg-chalkboard-80 text-chalkboard-10"
|
||||
/>
|
||||
<span className="sr-only">Help and resources</span>
|
||||
<Tooltip position="top-right" wrapperClassName="ui-open:hidden">
|
||||
@ -30,7 +34,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
||||
</Popover.Button>
|
||||
<Popover.Panel
|
||||
as="ul"
|
||||
className="absolute right-0 left-auto bottom-full mb-1 w-64 py-2 flex flex-col gap-1 align-stretch text-chalkboard-10 dark:text-inherit bg-chalkboard-110 dark:bg-chalkboard-100 rounded shadow-lg border border-solid border-chalkboard-110 dark:border-chalkboard-80 text-sm m-0 p-0"
|
||||
className="absolute right-0 left-auto flex flex-col w-64 gap-1 p-0 py-2 m-0 mb-1 text-sm border border-solid rounded shadow-lg bottom-full align-stretch text-chalkboard-10 dark:text-inherit bg-chalkboard-110 dark:bg-chalkboard-100 border-chalkboard-110 dark:border-chalkboard-80"
|
||||
>
|
||||
<HelpMenuItem
|
||||
as="a"
|
||||
@ -84,7 +88,12 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
||||
</HelpMenuItem>
|
||||
<HelpMenuItem
|
||||
as="button"
|
||||
onClick={() => navigate('settings?tab=keybindings')}
|
||||
onClick={() => {
|
||||
const targetPath = location.pathname.includes(paths.FILE)
|
||||
? filePath + paths.SETTINGS
|
||||
: paths.HOME + paths.SETTINGS
|
||||
navigate(targetPath + '?tab=keybindings')
|
||||
}}
|
||||
>
|
||||
Keyboard shortcuts
|
||||
</HelpMenuItem>
|
||||
@ -99,9 +108,9 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
||||
},
|
||||
})
|
||||
if (isInProject) {
|
||||
navigate('onboarding')
|
||||
navigate(filePath + paths.ONBOARDING.INDEX)
|
||||
} else {
|
||||
createAndOpenNewProject(navigate)
|
||||
createAndOpenNewProject({ onProjectOpen, navigate })
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -128,7 +137,7 @@ function HelpMenuItem({
|
||||
}: HelpMenuItemProps) {
|
||||
const baseClassName = 'block px-2 py-1 hover:bg-chalkboard-80'
|
||||
return (
|
||||
<li className="m-0 p-0">
|
||||
<li className="p-0 m-0">
|
||||
{as === 'a' ? (
|
||||
<a
|
||||
{...(props as React.ComponentProps<'a'>)}
|
||||
|
@ -10,6 +10,7 @@ import { coreDump } from 'lang/wasm'
|
||||
import toast from 'react-hot-toast'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import openWindow from 'lib/openWindow'
|
||||
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
|
||||
|
||||
export function LowerRightControls({
|
||||
children,
|
||||
@ -100,6 +101,7 @@ export function LowerRightControls({
|
||||
Settings
|
||||
</Tooltip>
|
||||
</Link>
|
||||
<NetworkMachineIndicator className={linkOverrideClassName} />
|
||||
<NetworkHealthIndicator />
|
||||
<HelpMenu />
|
||||
</menu>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
import React, { createContext, useEffect, useRef } from 'react'
|
||||
import React, { createContext, useEffect, useMemo, useRef } from 'react'
|
||||
import {
|
||||
AnyStateMachine,
|
||||
ContextFrom,
|
||||
@ -8,7 +8,12 @@ import {
|
||||
StateFrom,
|
||||
assign,
|
||||
} from 'xstate'
|
||||
import { SetSelections, modelingMachine } from 'machines/modelingMachine'
|
||||
import {
|
||||
SetSelections,
|
||||
getPersistedContext,
|
||||
modelingMachine,
|
||||
modelingMachineDefaultContext,
|
||||
} from 'machines/modelingMachine'
|
||||
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import {
|
||||
@ -23,6 +28,7 @@ import {
|
||||
editorManager,
|
||||
sceneEntitiesManager,
|
||||
} from 'lib/singletons'
|
||||
import { machineManager } from 'lib/machineManager'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
||||
import {
|
||||
@ -72,6 +78,7 @@ import { err, trap } from 'lib/trap'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { modelingMachineEvent } from 'editor/manager'
|
||||
import { hasValidFilletSelection } from 'lang/modifyAst/addFillet'
|
||||
import { ExportIntent } from 'lang/std/engineConnection'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -99,6 +106,7 @@ export const ModelingMachineProvider = ({
|
||||
} = useSettingsAuthContext()
|
||||
const token = auth?.context?.token
|
||||
const streamRef = useRef<HTMLDivElement>(null)
|
||||
const persistedContext = useMemo(() => getPersistedContext(), [])
|
||||
|
||||
let [searchParams] = useSearchParams()
|
||||
const pool = searchParams.get('pool')
|
||||
@ -121,6 +129,13 @@ export const ModelingMachineProvider = ({
|
||||
const [modelingState, modelingSend, modelingActor] = useMachine(
|
||||
modelingMachine,
|
||||
{
|
||||
context: {
|
||||
...modelingMachineDefaultContext,
|
||||
store: {
|
||||
...modelingMachineDefaultContext.store,
|
||||
...persistedContext,
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
'disable copilot': () => {
|
||||
editorManager.setCopilotEnabled(false)
|
||||
@ -338,8 +353,57 @@ export const ModelingMachineProvider = ({
|
||||
|
||||
return {}
|
||||
}),
|
||||
Make: async (_, event) => {
|
||||
if (event.type !== 'Make' || TEST) return
|
||||
// Check if we already have an export intent.
|
||||
if (engineCommandManager.exportIntent) {
|
||||
toast.error('Already exporting')
|
||||
return
|
||||
}
|
||||
// Set the export intent.
|
||||
engineCommandManager.exportIntent = ExportIntent.Make
|
||||
|
||||
console.log('making', event.data)
|
||||
// Set the current machine.
|
||||
machineManager.currentMachine = event.data.machine
|
||||
|
||||
const format: Models['OutputFormat_type'] = {
|
||||
type: 'stl',
|
||||
coords: {
|
||||
forward: {
|
||||
axis: 'y',
|
||||
direction: 'negative',
|
||||
},
|
||||
up: {
|
||||
axis: 'z',
|
||||
direction: 'positive',
|
||||
},
|
||||
},
|
||||
storage: 'ascii',
|
||||
units: defaultUnit.current,
|
||||
selection: { type: 'default_scene' },
|
||||
}
|
||||
|
||||
toast.promise(
|
||||
exportFromEngine({
|
||||
format: format,
|
||||
}),
|
||||
{
|
||||
loading: 'Starting print...',
|
||||
success: 'Started print successfully',
|
||||
error: 'Error while starting print',
|
||||
}
|
||||
)
|
||||
},
|
||||
'Engine export': async (_, event) => {
|
||||
if (event.type !== 'Export' || TEST) return
|
||||
if (engineCommandManager.exportIntent) {
|
||||
toast.error('Already exporting')
|
||||
return
|
||||
}
|
||||
// Set the export intent.
|
||||
engineCommandManager.exportIntent = ExportIntent.Save
|
||||
|
||||
console.log('exporting', event.data)
|
||||
const format = {
|
||||
...event.data,
|
||||
@ -433,7 +497,7 @@ export const ModelingMachineProvider = ({
|
||||
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
|
||||
return false
|
||||
return !!isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
engineCommandManager.artifactGraph,
|
||||
selectionRanges
|
||||
)
|
||||
},
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
historyKeymap,
|
||||
history,
|
||||
} from '@codemirror/commands'
|
||||
import { lintGutter, lintKeymap } from '@codemirror/lint'
|
||||
import { diagnosticCount, lintGutter, lintKeymap } from '@codemirror/lint'
|
||||
import {
|
||||
foldGutter,
|
||||
foldKeymap,
|
||||
@ -193,6 +193,13 @@ export const KclEditorPane = () => {
|
||||
if (_editorView === null) return
|
||||
|
||||
editorManager.setEditorView(_editorView)
|
||||
|
||||
// On first load of this component, ensure we show the current errors
|
||||
// in the editor.
|
||||
// Make sure we don't add them twice.
|
||||
if (diagnosticCount(_editorView.state) === 0) {
|
||||
kclManager.setDiagnosticsForCurrentErrors()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -27,27 +27,3 @@ export const LogsPane = () => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const KclErrorsPane = () => {
|
||||
const theme = useResolvedTheme()
|
||||
const { errors } = useKclContext()
|
||||
return (
|
||||
<div className="overflow-hidden">
|
||||
<div className="absolute inset-0 p-2 flex flex-col overflow-auto">
|
||||
<ReactJsonTypeHack
|
||||
src={errors}
|
||||
collapsed={1}
|
||||
collapseStringsAfterLength={60}
|
||||
enableClipboard={false}
|
||||
displayArrayKey={false}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={true}
|
||||
indentWidth={2}
|
||||
quotesOnKeys={false}
|
||||
name={false}
|
||||
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -3,29 +3,33 @@ import {
|
||||
faBugSlash,
|
||||
faCode,
|
||||
faCodeCommit,
|
||||
faExclamationCircle,
|
||||
faSquareRootVariable,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEditorMenu'
|
||||
import { CustomIconName } from 'components/CustomIcon'
|
||||
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
|
||||
import { ReactNode } from 'react'
|
||||
import { MouseEventHandler, ReactNode } from 'react'
|
||||
import { MemoryPane, MemoryPaneMenu } from './MemoryPane'
|
||||
import { KclErrorsPane, LogsPane } from './LoggingPanes'
|
||||
import { LogsPane } from './LoggingPanes'
|
||||
import { DebugPane } from './DebugPane'
|
||||
import { FileTreeInner, FileTreeMenu } from 'components/FileTree'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { editorManager } from 'lib/singletons'
|
||||
|
||||
export type SidebarType =
|
||||
| 'code'
|
||||
| 'debug'
|
||||
| 'export'
|
||||
| 'files'
|
||||
| 'kclErrors'
|
||||
| 'logs'
|
||||
| 'lspMessages'
|
||||
| 'variables'
|
||||
|
||||
export interface BadgeInfo {
|
||||
value: (props: PaneCallbackProps) => boolean | number
|
||||
onClick?: MouseEventHandler<any>
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface can be extended as more context is needed for the panes
|
||||
* to determine if they should show their badges or not.
|
||||
@ -42,7 +46,7 @@ export type SidebarPane = {
|
||||
Content: ReactNode | React.FC
|
||||
Menu?: ReactNode | React.FC
|
||||
hideOnPlatform?: 'desktop' | 'web'
|
||||
showBadge?: (props: PaneCallbackProps) => boolean | number
|
||||
showBadge?: BadgeInfo
|
||||
}
|
||||
|
||||
export const sidebarPanes: SidebarPane[] = [
|
||||
@ -53,6 +57,15 @@ export const sidebarPanes: SidebarPane[] = [
|
||||
Content: KclEditorPane,
|
||||
keybinding: 'Shift + C',
|
||||
Menu: KclEditorMenu,
|
||||
showBadge: {
|
||||
value: ({ kclContext }) => {
|
||||
return kclContext.errors.length
|
||||
},
|
||||
onClick: (e) => {
|
||||
e.preventDefault()
|
||||
editorManager.scrollToFirstErrorDiagnosticIfExists()
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
@ -78,14 +91,6 @@ export const sidebarPanes: SidebarPane[] = [
|
||||
Content: LogsPane,
|
||||
keybinding: 'Shift + L',
|
||||
},
|
||||
{
|
||||
id: 'kclErrors',
|
||||
title: 'KCL Errors',
|
||||
icon: faExclamationCircle,
|
||||
Content: KclErrorsPane,
|
||||
keybinding: 'Shift + E',
|
||||
showBadge: ({ kclContext }) => kclContext.errors.length,
|
||||
},
|
||||
{
|
||||
id: 'debug',
|
||||
title: 'Debug',
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { Resizable } from 're-resizable'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { MouseEventHandler, useCallback, useMemo } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { SidebarType, sidebarPanes } from './ModelingPanes'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
@ -13,11 +13,17 @@ import { CustomIconName } from 'components/CustomIcon'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { machineManager } from 'lib/machineManager'
|
||||
|
||||
interface ModelingSidebarProps {
|
||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||
}
|
||||
|
||||
interface BadgeInfoComputed {
|
||||
value: number | boolean
|
||||
onClick?: MouseEventHandler<any>
|
||||
}
|
||||
|
||||
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const kclContext = useKclContext()
|
||||
@ -45,7 +51,30 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
data: { name: 'Export', groupId: 'modeling' },
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'make',
|
||||
title: 'Make part',
|
||||
icon: 'printer3d',
|
||||
iconClassName: '!p-0',
|
||||
keybinding: 'Ctrl + Shift + M',
|
||||
action: async () => {
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Make', groupId: 'modeling' },
|
||||
})
|
||||
},
|
||||
hide: () => machineManager.machineCount() === 0,
|
||||
hideOnPlatform: 'web',
|
||||
},
|
||||
]
|
||||
const filteredActions: SidebarAction[] = sidebarActions.filter(
|
||||
(action) =>
|
||||
(!action.hide || (action.hide instanceof Function && !action.hide())) &&
|
||||
(!action.hideOnPlatform ||
|
||||
(isTauri()
|
||||
? action.hideOnPlatform === 'web'
|
||||
: action.hideOnPlatform === 'desktop'))
|
||||
)
|
||||
|
||||
// // Filter out the debug panel if it's not supposed to be shown
|
||||
// // TODO: abstract out for allowing user to configure which panes to show
|
||||
@ -64,13 +93,16 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
[sidebarPanes, showDebugPanel.current]
|
||||
)
|
||||
|
||||
const paneBadgeMap: Record<SidebarType, number | boolean> = useMemo(() => {
|
||||
const paneBadgeMap: Record<SidebarType, BadgeInfoComputed> = useMemo(() => {
|
||||
return filteredPanes.reduce((acc, pane) => {
|
||||
if (pane.showBadge) {
|
||||
acc[pane.id] = pane.showBadge({ kclContext })
|
||||
acc[pane.id] = {
|
||||
value: pane.showBadge.value({ kclContext }),
|
||||
onClick: pane.showBadge.onClick,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<SidebarType, number | boolean>)
|
||||
}, {} as Record<SidebarType, BadgeInfoComputed>)
|
||||
}, [kclContext.errors])
|
||||
|
||||
const togglePane = useCallback(
|
||||
@ -135,23 +167,30 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<hr className="w-full border-chalkboard-20 dark:border-chalkboard-80" />
|
||||
<ul id="sidebar-actions" className="w-fit p-2 flex flex-col gap-2">
|
||||
{sidebarActions.map((action) => (
|
||||
<ModelingPaneButton
|
||||
key={action.id}
|
||||
paneConfig={{
|
||||
id: action.id,
|
||||
title: action.title,
|
||||
icon: action.icon,
|
||||
keybinding: action.keybinding,
|
||||
iconClassName: action.iconClassName,
|
||||
iconSize: 'md',
|
||||
}}
|
||||
onClick={action.action}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
{filteredActions.length > 0 && (
|
||||
<>
|
||||
<hr className="w-full border-chalkboard-20 dark:border-chalkboard-80" />
|
||||
<ul
|
||||
id="sidebar-actions"
|
||||
className="w-fit p-2 flex flex-col gap-2"
|
||||
>
|
||||
{filteredActions.map((action) => (
|
||||
<ModelingPaneButton
|
||||
key={action.id}
|
||||
paneConfig={{
|
||||
id: action.id,
|
||||
title: action.title,
|
||||
icon: action.icon,
|
||||
keybinding: action.keybinding,
|
||||
iconClassName: action.iconClassName,
|
||||
iconSize: 'md',
|
||||
}}
|
||||
onClick={action.action}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
<ul
|
||||
id="pane-section"
|
||||
@ -198,7 +237,7 @@ interface ModelingPaneButtonProps
|
||||
}
|
||||
onClick: () => void
|
||||
paneIsOpen?: boolean
|
||||
showBadge?: boolean | number
|
||||
showBadge?: BadgeInfoComputed
|
||||
}
|
||||
|
||||
function ModelingPaneButton({
|
||||
@ -213,59 +252,68 @@ function ModelingPaneButton({
|
||||
})
|
||||
|
||||
return (
|
||||
<button
|
||||
className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
|
||||
onClick={onClick}
|
||||
name={paneConfig.title}
|
||||
data-testid={paneConfig.id + '-pane-button'}
|
||||
{...props}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={paneConfig.icon}
|
||||
className={'p-1 ' + paneConfig.iconClassName || ''}
|
||||
size={paneConfig.iconSize || 'sm'}
|
||||
iconClassName={
|
||||
paneIsOpen
|
||||
? ' !text-chalkboard-10'
|
||||
: '!text-chalkboard-80 dark:!text-chalkboard-30'
|
||||
}
|
||||
bgClassName={
|
||||
'rounded-sm ' + (paneIsOpen ? '!bg-primary' : '!bg-transparent')
|
||||
}
|
||||
/>
|
||||
<span className="sr-only">
|
||||
{paneConfig.title}
|
||||
{paneIsOpen !== undefined ? ` pane` : ''}
|
||||
</span>
|
||||
{!!showBadge && (
|
||||
<p
|
||||
className={
|
||||
'absolute m-0 p-0 -top-1 -right-1 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80'
|
||||
<div id={paneConfig.id + '-button-holder'}>
|
||||
<button
|
||||
className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
|
||||
onClick={onClick}
|
||||
name={paneConfig.title}
|
||||
data-testid={paneConfig.id + '-pane-button'}
|
||||
{...props}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={paneConfig.icon}
|
||||
className={'p-1 ' + paneConfig.iconClassName || ''}
|
||||
size={paneConfig.iconSize || 'sm'}
|
||||
iconClassName={
|
||||
paneIsOpen
|
||||
? ' !text-chalkboard-10'
|
||||
: '!text-chalkboard-80 dark:!text-chalkboard-30'
|
||||
}
|
||||
bgClassName={
|
||||
'rounded-sm ' + (paneIsOpen ? '!bg-primary' : '!bg-transparent')
|
||||
}
|
||||
/>
|
||||
<span className="sr-only">
|
||||
{paneConfig.title}
|
||||
{paneIsOpen !== undefined ? ` pane` : ''}
|
||||
</span>
|
||||
<Tooltip
|
||||
position="right"
|
||||
contentClassName="max-w-none flex items-center gap-4"
|
||||
hoverOnly
|
||||
>
|
||||
<span className="flex-1">
|
||||
{paneConfig.title}
|
||||
{paneIsOpen !== undefined ? ` pane` : ''}
|
||||
</span>
|
||||
<kbd className="hotkey text-xs capitalize">
|
||||
{paneConfig.keybinding}
|
||||
</kbd>
|
||||
</Tooltip>
|
||||
</button>
|
||||
{!!showBadge?.value && (
|
||||
<p
|
||||
id={`${paneConfig.id}-badge`}
|
||||
className={
|
||||
'absolute m-0 p-0 top-1 right-0 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer'
|
||||
}
|
||||
onClick={showBadge.onClick}
|
||||
title={`Click to view ${showBadge.value} notification${
|
||||
Number(showBadge.value) > 1 ? 's' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="sr-only"> has </span>
|
||||
{typeof showBadge === 'number' ? (
|
||||
<span>{showBadge}</span>
|
||||
{typeof showBadge.value === 'number' ? (
|
||||
<span>{showBadge.value}</span>
|
||||
) : (
|
||||
<span className="sr-only">a</span>
|
||||
)}
|
||||
<span className="sr-only">
|
||||
notification{Number(showBadge) > 1 ? 's' : ''}
|
||||
notification{Number(showBadge.value) > 1 ? 's' : ''}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
<Tooltip
|
||||
position="right"
|
||||
contentClassName="max-w-none flex items-center gap-4"
|
||||
hoverOnly
|
||||
>
|
||||
<span className="flex-1">
|
||||
{paneConfig.title}
|
||||
{paneIsOpen !== undefined ? ` pane` : ''}
|
||||
</span>
|
||||
<kbd className="hotkey text-xs capitalize">{paneConfig.keybinding}</kbd>
|
||||
</Tooltip>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -277,4 +325,5 @@ export type SidebarAction = {
|
||||
keybinding: string
|
||||
action: () => void
|
||||
hideOnPlatform?: 'desktop' | 'web'
|
||||
hide?: boolean | (() => boolean)
|
||||
}
|
||||
|
@ -103,8 +103,8 @@ export const NetworkHealthIndicator = () => {
|
||||
'rounded-sm ' + overallConnectionStateColor[overallState].bg
|
||||
}
|
||||
/>
|
||||
<Tooltip position="top-right">
|
||||
Network Health ({NETWORK_HEALTH_TEXT[overallState]})
|
||||
<Tooltip position="top-right" wrapperClassName="ui-open:hidden">
|
||||
Network health ({NETWORK_HEALTH_TEXT[overallState]})
|
||||
</Tooltip>
|
||||
</Popover.Button>
|
||||
<Popover.Panel
|
||||
|
62
src/components/NetworkMachineIndicator.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import Tooltip from './Tooltip'
|
||||
import { machineManager } from 'lib/machineManager'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
|
||||
export const NetworkMachineIndicator = ({
|
||||
className,
|
||||
}: {
|
||||
className?: string
|
||||
}) => {
|
||||
const machineCount = Object.keys(machineManager.machines).length
|
||||
return isTauri() ? (
|
||||
<Popover className="relative">
|
||||
<Popover.Button
|
||||
className={
|
||||
'flex items-center p-0 border-none bg-transparent dark:bg-transparent relative ' +
|
||||
(className || '')
|
||||
}
|
||||
data-testid="network-machine-toggle"
|
||||
>
|
||||
<CustomIcon name="printer3d" className="w-5 h-5" />
|
||||
{machineCount > 0 && (
|
||||
<p aria-hidden className="flex items-center justify-center text-xs">
|
||||
{machineCount}
|
||||
</p>
|
||||
)}
|
||||
<Tooltip position="top-right" wrapperClassName="ui-open:hidden">
|
||||
Network machines ({machineCount})
|
||||
</Tooltip>
|
||||
</Popover.Button>
|
||||
<Popover.Panel
|
||||
className="absolute right-0 left-auto bottom-full mb-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"
|
||||
data-testid="network-popover"
|
||||
>
|
||||
<div className="flex items-center justify-between p-2 rounded-t-sm bg-chalkboard-20 dark:bg-chalkboard-80">
|
||||
<h2 className="text-sm font-sans font-normal">Network machines</h2>
|
||||
<p
|
||||
data-testid="network"
|
||||
className="font-bold text-xs uppercase px-2 py-1 rounded-sm"
|
||||
>
|
||||
{machineCount}
|
||||
</p>
|
||||
</div>
|
||||
{machineCount > 0 && (
|
||||
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
|
||||
{Object.entries(machineManager.machines).map(
|
||||
([hostname, machine]) => (
|
||||
<li key={hostname} className={'px-2 py-4 gap-1 last:mb-0 '}>
|
||||
<p className="">{machine.model || machine.manufacturer}</p>
|
||||
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
|
||||
Hostname {hostname}
|
||||
</p>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
) : null
|
||||
}
|
@ -12,6 +12,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { useLspContext } from './LspProvider'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { machineManager } from 'lib/machineManager'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import Tooltip from './Tooltip'
|
||||
@ -90,12 +91,14 @@ function ProjectMenuPopover({
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const { onProjectClose } = useLspContext()
|
||||
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
|
||||
const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
|
||||
const findCommand = (obj: { name: string; groupId: string }) =>
|
||||
Boolean(
|
||||
commandBarState.context.commands.find(
|
||||
(c) => c.name === obj.name && c.groupId === obj.groupId
|
||||
)
|
||||
)
|
||||
const machineCount = machineManager.machineCount()
|
||||
|
||||
// We filter this memoized list so that no orphan "break" elements are rendered.
|
||||
const projectMenuItems = useMemo<(ActionButtonProps | 'break')[]>(
|
||||
@ -144,6 +147,32 @@ function ProjectMenuPopover({
|
||||
}),
|
||||
},
|
||||
'break',
|
||||
{
|
||||
id: 'make',
|
||||
Element: 'button',
|
||||
className: !isTauri() ? 'hidden' : '',
|
||||
children: (
|
||||
<>
|
||||
<span>Make current part</span>
|
||||
{!findCommand(makeCommandInfo) && (
|
||||
<Tooltip
|
||||
position="right"
|
||||
wrapperClassName="!max-w-none min-w-fit"
|
||||
>
|
||||
Awaiting engine connection
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
disabled: !findCommand(makeCommandInfo) || machineCount === 0,
|
||||
onClick: () => {
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
data: makeCommandInfo,
|
||||
})
|
||||
},
|
||||
},
|
||||
'break',
|
||||
{
|
||||
id: 'go-home',
|
||||
Element: 'button',
|
||||
|
@ -19,7 +19,8 @@ import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/tauriFS'
|
||||
import { paths } from 'lib/paths'
|
||||
import { useDotDotSlash } from 'hooks/useDotDotSlash'
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
import { ForwardedRef, forwardRef } from 'react'
|
||||
import { ForwardedRef, forwardRef, useEffect } from 'react'
|
||||
import { useLspContext } from 'components/LspProvider'
|
||||
|
||||
interface AllSettingsFieldsProps {
|
||||
searchParamTab: SettingsLevel
|
||||
@ -33,9 +34,10 @@ export const AllSettingsFields = forwardRef(
|
||||
) => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { onProjectOpen } = useLspContext()
|
||||
const dotDotSlash = useDotDotSlash()
|
||||
const {
|
||||
settings: { send, context },
|
||||
settings: { send, context, state },
|
||||
} = useSettingsAuthContext()
|
||||
|
||||
const projectPath =
|
||||
@ -48,19 +50,37 @@ export const AllSettingsFields = forwardRef(
|
||||
)
|
||||
: undefined
|
||||
|
||||
function restartOnboarding() {
|
||||
async function restartOnboarding() {
|
||||
send({
|
||||
type: `set.app.onboardingStatus`,
|
||||
data: { level: 'user', value: '' },
|
||||
})
|
||||
|
||||
if (isFileSettings) {
|
||||
navigate(dotDotSlash(1) + paths.ONBOARDING.INDEX)
|
||||
} else {
|
||||
createAndOpenNewProject(navigate)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A "listener" for the XState to return to "idle" state
|
||||
* when the user resets the onboarding, using the callback above
|
||||
*/
|
||||
useEffect(() => {
|
||||
async function navigateToOnboardingStart() {
|
||||
if (
|
||||
state.context.app.onboardingStatus.user === '' &&
|
||||
state.matches('idle')
|
||||
) {
|
||||
if (isFileSettings) {
|
||||
// If we're in a project, first navigate to the onboarding start here
|
||||
// so we can trigger the warning screen if necessary
|
||||
navigate(dotDotSlash(1) + paths.ONBOARDING.INDEX)
|
||||
} else {
|
||||
// If we're in the global settings, create a new project and navigate
|
||||
// to the onboarding start in that project
|
||||
await createAndOpenNewProject({ onProjectOpen, navigate })
|
||||
}
|
||||
}
|
||||
}
|
||||
navigateToOnboardingStart()
|
||||
}, [isFileSettings, navigate, state])
|
||||
|
||||
return (
|
||||
<div className="relative overflow-y-auto">
|
||||
<div ref={scrollRef} className="flex flex-col gap-4 px-2">
|
||||
|
@ -225,7 +225,7 @@ export const Stream = () => {
|
||||
},
|
||||
})
|
||||
if (state.matches('Sketch')) return
|
||||
if (state.matches('Sketch no face')) return
|
||||
if (state.matches('idle.showPlanes')) return
|
||||
|
||||
if (!context.store?.didDragInStream && btnName(e).left) {
|
||||
sendSelectEventToEngine(
|
||||
|
@ -1,12 +1,9 @@
|
||||
import { toolTips } from 'lang/langHelpers'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { CallExpression, Program, VariableDeclarator } from '../../lang/wasm'
|
||||
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getLastNodeFromPath,
|
||||
DynamicNode,
|
||||
isNodeType,
|
||||
expectNodeOnPath,
|
||||
getNodeFromPath,
|
||||
} from '../../lang/queryAst'
|
||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||
import {
|
||||
@ -17,7 +14,6 @@ import {
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { err } from 'lib/trap'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
export function equalAngleInfo({
|
||||
selectionRanges,
|
||||
@ -33,33 +29,26 @@ export function equalAngleInfo({
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const _nodes = paths.map((pathToNode) => {
|
||||
const tmp = getLastNodeFromPath(kclManager.ast, pathToNode)
|
||||
const tmp = getNodeFromPath<Value>(kclManager.ast, pathToNode)
|
||||
if (err(tmp)) return tmp
|
||||
if (isArray(tmp.node)) {
|
||||
return new Error('Expected value node, but found array')
|
||||
}
|
||||
return tmp.node
|
||||
})
|
||||
const nodes: DynamicNode[] = []
|
||||
for (const node of _nodes) {
|
||||
if (err(node)) return node
|
||||
nodes.push(node)
|
||||
}
|
||||
const _err1 = _nodes.find(err)
|
||||
if (err(_err1)) return _err1
|
||||
const nodes = _nodes as Value[]
|
||||
|
||||
const _varDecs = paths.map((pathToNode) => {
|
||||
const tmp = expectNodeOnPath<VariableDeclarator>(
|
||||
const tmp = getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(tmp)) return tmp
|
||||
return tmp
|
||||
return tmp.node
|
||||
})
|
||||
const varDecs: VariableDeclarator[] = []
|
||||
for (const varDec of _varDecs) {
|
||||
if (err(varDec)) return varDec
|
||||
varDecs.push(varDec)
|
||||
}
|
||||
const _err2 = _varDecs.find(err)
|
||||
if (err(_err2)) return _err2
|
||||
const varDecs = _varDecs as VariableDeclarator[]
|
||||
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
@ -68,7 +57,7 @@ export function equalAngleInfo({
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
isNodeType<CallExpression>(node, 'CallExpression') &&
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
|
@ -1,12 +1,9 @@
|
||||
import { toolTips } from 'lang/langHelpers'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { CallExpression, Program, VariableDeclarator } from '../../lang/wasm'
|
||||
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getLastNodeFromPath,
|
||||
DynamicNode,
|
||||
isNodeType,
|
||||
expectNodeOnPath,
|
||||
getNodeFromPath,
|
||||
} from '../../lang/queryAst'
|
||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||
import {
|
||||
@ -17,7 +14,6 @@ import {
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { err } from 'lib/trap'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
export function setEqualLengthInfo({
|
||||
selectionRanges,
|
||||
@ -33,33 +29,26 @@ export function setEqualLengthInfo({
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const _nodes = paths.map((pathToNode) => {
|
||||
const tmp = getLastNodeFromPath(kclManager.ast, pathToNode)
|
||||
const tmp = getNodeFromPath<Value>(kclManager.ast, pathToNode)
|
||||
if (err(tmp)) return tmp
|
||||
if (isArray(tmp.node)) {
|
||||
return new Error('Expected value node, but found array')
|
||||
}
|
||||
return tmp.node
|
||||
})
|
||||
const nodes: DynamicNode[] = []
|
||||
for (const node of _nodes) {
|
||||
if (err(node)) return node
|
||||
nodes.push(node)
|
||||
}
|
||||
const _err1 = _nodes.find(err)
|
||||
if (err(_err1)) return _err1
|
||||
const nodes = _nodes as Value[]
|
||||
|
||||
const _varDecs = paths.map((pathToNode) => {
|
||||
const varDec = expectNodeOnPath<VariableDeclarator>(
|
||||
const tmp = getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(varDec)) return varDec
|
||||
return varDec
|
||||
if (err(tmp)) return tmp
|
||||
return tmp.node
|
||||
})
|
||||
const varDecs: VariableDeclarator[] = []
|
||||
for (const varDec of _varDecs) {
|
||||
if (err(varDec)) return varDec
|
||||
varDecs.push(varDec)
|
||||
}
|
||||
const _err2 = _varDecs.find(err)
|
||||
if (err(_err2)) return _err2
|
||||
const varDecs = _varDecs as VariableDeclarator[]
|
||||
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
@ -68,7 +57,7 @@ export function setEqualLengthInfo({
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
isNodeType<CallExpression>(node, 'CallExpression') &&
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { toolTips } from 'lang/langHelpers'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { CallExpression, Program, ProgramMemory } from '../../lang/wasm'
|
||||
import { Program, ProgramMemory, Value } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getLastNodeFromPath,
|
||||
DynamicNode,
|
||||
isNodeType,
|
||||
getNodeFromPath,
|
||||
} from '../../lang/queryAst'
|
||||
import {
|
||||
PathToNodeMap,
|
||||
@ -15,7 +13,6 @@ import {
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { err } from 'lib/trap'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
export function horzVertInfo(
|
||||
selectionRanges: Selections,
|
||||
@ -30,22 +27,17 @@ export function horzVertInfo(
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const _nodes = paths.map((pathToNode) => {
|
||||
const tmp = getLastNodeFromPath(kclManager.ast, pathToNode)
|
||||
const tmp = getNodeFromPath<Value>(kclManager.ast, pathToNode)
|
||||
if (err(tmp)) return tmp
|
||||
if (isArray(tmp.node)) {
|
||||
return new Error('Expected value node, but found array')
|
||||
}
|
||||
return tmp.node
|
||||
})
|
||||
const nodes: DynamicNode[] = []
|
||||
for (const node of _nodes) {
|
||||
if (err(node)) return node
|
||||
nodes.push(node)
|
||||
}
|
||||
const _err1 = _nodes.find(err)
|
||||
if (err(_err1)) return _err1
|
||||
const nodes = _nodes as Value[]
|
||||
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
isNodeType<CallExpression>(node, 'CallExpression') &&
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
|
@ -1,18 +1,10 @@
|
||||
import { toolTips } from 'lang/langHelpers'
|
||||
import { Selections } from 'lib/selections'
|
||||
import {
|
||||
BinaryPart,
|
||||
CallExpression,
|
||||
Program,
|
||||
VariableDeclarator,
|
||||
} from '../../lang/wasm'
|
||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getNodeFromPath,
|
||||
isLinesParallelAndConstrained,
|
||||
getLastNodeFromPath,
|
||||
expectNodeOnPath,
|
||||
DynamicNode,
|
||||
isNodeType,
|
||||
} from '../../lang/queryAst'
|
||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||
import {
|
||||
@ -26,7 +18,6 @@ import { createVariableDeclaration } from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { err } from 'lib/trap'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
const getModalInfo = createInfoModal(GetInfoModal)
|
||||
|
||||
@ -81,33 +72,26 @@ export function intersectInfo({
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const _nodes = paths.map((pathToNode) => {
|
||||
const tmp = getLastNodeFromPath(kclManager.ast, pathToNode)
|
||||
const tmp = getNodeFromPath<Value>(kclManager.ast, pathToNode)
|
||||
if (err(tmp)) return tmp
|
||||
if (isArray(tmp.node)) {
|
||||
return new Error('Expected value node, but found array')
|
||||
}
|
||||
return tmp.node
|
||||
})
|
||||
const nodes: DynamicNode[] = []
|
||||
for (const node of _nodes) {
|
||||
if (err(node)) return node
|
||||
nodes.push(node)
|
||||
}
|
||||
const _err1 = _nodes.find(err)
|
||||
if (err(_err1)) return _err1
|
||||
const nodes = _nodes as Value[]
|
||||
|
||||
const _varDecs = paths.map((pathToNode) => {
|
||||
const varDec = expectNodeOnPath<VariableDeclarator>(
|
||||
const tmp = getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(varDec)) return varDec
|
||||
return varDec
|
||||
if (err(tmp)) return tmp
|
||||
return tmp.node
|
||||
})
|
||||
const varDecs: VariableDeclarator[] = []
|
||||
for (const varDec of _varDecs) {
|
||||
if (err(varDec)) return varDec
|
||||
varDecs.push(varDec)
|
||||
}
|
||||
const _err2 = _varDecs.find(err)
|
||||
if (err(_err2)) return _err2
|
||||
const varDecs = _varDecs as VariableDeclarator[]
|
||||
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
@ -116,7 +100,7 @@ export function intersectInfo({
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
isNodeType<CallExpression>(node, 'CallExpression') &&
|
||||
node?.type === 'CallExpression' &&
|
||||
[
|
||||
...toolTips,
|
||||
'startSketchAt', // TODO probably a better place for this to live
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { toolTips } from 'lang/langHelpers'
|
||||
import { Selection, Selections } from 'lib/selections'
|
||||
import { CallExpression, PathToNode, Program } from '../../lang/wasm'
|
||||
import { PathToNode, Program, Value } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getLastNodeFromPath,
|
||||
DynamicNode,
|
||||
isNodeType,
|
||||
getNodeFromPath,
|
||||
} from '../../lang/queryAst'
|
||||
import {
|
||||
PathToNodeMap,
|
||||
@ -15,7 +13,6 @@ import {
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { err } from 'lib/trap'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
export function removeConstrainingValuesInfo({
|
||||
selectionRanges,
|
||||
@ -36,18 +33,13 @@ export function removeConstrainingValuesInfo({
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const _nodes = paths.map((pathToNode) => {
|
||||
const tmp = getLastNodeFromPath(kclManager.ast, pathToNode)
|
||||
const tmp = getNodeFromPath<Value>(kclManager.ast, pathToNode)
|
||||
if (err(tmp)) return tmp
|
||||
if (isArray(tmp.node)) {
|
||||
return new Error('Expected value node, but found array')
|
||||
}
|
||||
return tmp.node
|
||||
})
|
||||
const nodes: DynamicNode[] = []
|
||||
for (const node of _nodes) {
|
||||
if (err(node)) return node
|
||||
nodes.push(node)
|
||||
}
|
||||
const _err1 = _nodes.find(err)
|
||||
if (err(_err1)) return _err1
|
||||
const nodes = _nodes as Value[]
|
||||
|
||||
const updatedSelectionRanges = pathToNodes
|
||||
? {
|
||||
@ -62,7 +54,7 @@ export function removeConstrainingValuesInfo({
|
||||
: selectionRanges
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
isNodeType<CallExpression>(node, 'CallExpression') &&
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { toolTips } from 'lang/langHelpers'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { BinaryPart, CallExpression, Program, Value } from '../../lang/wasm'
|
||||
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getNodeFromPath,
|
||||
@ -49,22 +49,22 @@ export function absDistanceInfo({
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const _nodes = paths.map((pathToNode) => {
|
||||
const tmp = getNodeFromPath<CallExpression>(
|
||||
const tmp = getNodeFromPath<Value>(
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(tmp)) return tmp
|
||||
return tmp.stopAtNode
|
||||
return tmp.node
|
||||
})
|
||||
const nodes: (CallExpression | null)[] = []
|
||||
for (const node of _nodes) {
|
||||
if (err(node)) return node
|
||||
nodes.push(node)
|
||||
}
|
||||
const _err1 = _nodes.find(err)
|
||||
if (err(_err1)) return _err1
|
||||
const nodes = _nodes as Value[]
|
||||
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) => node && toolTips.includes(node.callee.name as any)
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const transforms = getTransformInfos(selectionRanges, kclManager.ast, disType)
|
||||
|
@ -1,17 +1,9 @@
|
||||
import { toolTips } from 'lang/langHelpers'
|
||||
import { Selections } from 'lib/selections'
|
||||
import {
|
||||
BinaryPart,
|
||||
CallExpression,
|
||||
Program,
|
||||
VariableDeclarator,
|
||||
} from '../../lang/wasm'
|
||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getLastNodeFromPath,
|
||||
DynamicNode,
|
||||
expectNodeOnPath,
|
||||
isNodeType,
|
||||
getNodeFromPath,
|
||||
} from '../../lang/queryAst'
|
||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||
import {
|
||||
@ -25,7 +17,6 @@ import { createVariableDeclaration } from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { err } from 'lib/trap'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
const getModalInfo = createInfoModal(GetInfoModal)
|
||||
|
||||
@ -44,33 +35,26 @@ export function angleBetweenInfo({
|
||||
)
|
||||
|
||||
const _nodes = paths.map((pathToNode) => {
|
||||
const tmp = getLastNodeFromPath(kclManager.ast, pathToNode)
|
||||
const tmp = getNodeFromPath<Value>(kclManager.ast, pathToNode)
|
||||
if (err(tmp)) return tmp
|
||||
if (isArray(tmp.node)) {
|
||||
return new Error('Expected value node, but found array')
|
||||
}
|
||||
return tmp.node
|
||||
})
|
||||
const nodes: DynamicNode[] = []
|
||||
for (const node of _nodes) {
|
||||
if (err(node)) return node
|
||||
nodes.push(node)
|
||||
}
|
||||
const _err1 = _nodes.find(err)
|
||||
if (err(_err1)) return _err1
|
||||
const nodes = _nodes as Value[]
|
||||
|
||||
const _varDecs = paths.map((pathToNode) => {
|
||||
const varDec = expectNodeOnPath<VariableDeclarator>(
|
||||
const tmp = getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(varDec)) return varDec
|
||||
return varDec
|
||||
if (err(tmp)) return tmp
|
||||
return tmp.node
|
||||
})
|
||||
const varDecs: VariableDeclarator[] = []
|
||||
for (const varDec of _varDecs) {
|
||||
if (err(varDec)) return varDec
|
||||
varDecs.push(varDec)
|
||||
}
|
||||
const _err2 = _varDecs.find(err)
|
||||
if (err(_err2)) return _err2
|
||||
const varDecs = _varDecs as VariableDeclarator[]
|
||||
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
@ -79,7 +63,7 @@ export function angleBetweenInfo({
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
isNodeType<CallExpression>(node, 'CallExpression') &&
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
|
@ -1,15 +1,8 @@
|
||||
import { toolTips } from 'lang/langHelpers'
|
||||
import {
|
||||
BinaryPart,
|
||||
CallExpression,
|
||||
Program,
|
||||
VariableDeclarator,
|
||||
} from '../../lang/wasm'
|
||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getLastNodeFromPath,
|
||||
expectNodeOnPath,
|
||||
isNodeType,
|
||||
getNodeFromPath,
|
||||
} from '../../lang/queryAst'
|
||||
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
|
||||
import {
|
||||
@ -24,7 +17,6 @@ import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { cleanErrs, err } from 'lib/trap'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
const getModalInfo = createInfoModal(GetInfoModal)
|
||||
|
||||
@ -44,31 +36,27 @@ export function horzVertDistanceInfo({
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const _nodes = paths.map((pathToNode) => {
|
||||
const tmp = getLastNodeFromPath(kclManager.ast, pathToNode)
|
||||
const tmp = getNodeFromPath<Value>(kclManager.ast, pathToNode)
|
||||
if (err(tmp)) return tmp
|
||||
if (isArray(tmp.node)) {
|
||||
return new Error('Expected value node, but found array')
|
||||
}
|
||||
return tmp.node
|
||||
})
|
||||
const [hasErr, nodes, nodesWErrs] = cleanErrs(_nodes)
|
||||
const [hasErr, , nodesWErrs] = cleanErrs(_nodes)
|
||||
|
||||
if (hasErr) return nodesWErrs[0]
|
||||
const nodes = _nodes as Value[]
|
||||
|
||||
const _varDecs = paths.map((pathToNode) => {
|
||||
const varDec = expectNodeOnPath<VariableDeclarator>(
|
||||
const tmp = getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(varDec)) return varDec
|
||||
return varDec
|
||||
if (err(tmp)) return tmp
|
||||
return tmp.node
|
||||
})
|
||||
const varDecs: VariableDeclarator[] = []
|
||||
for (const varDec of _varDecs) {
|
||||
if (err(varDec)) return varDec
|
||||
varDecs.push(varDec)
|
||||
}
|
||||
const _err2 = _varDecs.find(err)
|
||||
if (err(_err2)) return _err2
|
||||
const varDecs = _varDecs as VariableDeclarator[]
|
||||
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
@ -77,7 +65,7 @@ export function horzVertDistanceInfo({
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
isNodeType<CallExpression>(node, 'CallExpression') &&
|
||||
node?.type === 'CallExpression' &&
|
||||
[
|
||||
...toolTips,
|
||||
'startSketchAt', // TODO probably a better place for this to live
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { toolTips } from 'lang/langHelpers'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { BinaryPart, CallExpression, Program } from '../../lang/wasm'
|
||||
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getNodeFromPath,
|
||||
@ -43,24 +43,18 @@ export function angleLengthInfo({
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
|
||||
const _nodes = paths.map((pathToNode) => {
|
||||
const tmp = getNodeFromPath<CallExpression>(
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(tmp)) return tmp
|
||||
return tmp.stopAtNode
|
||||
})
|
||||
const nodes: (CallExpression | null)[] = []
|
||||
for (const node of _nodes) {
|
||||
if (err(node)) return node
|
||||
nodes.push(node)
|
||||
}
|
||||
const nodes = paths.map((pathToNode) =>
|
||||
getNodeFromPath<Value>(kclManager.ast, pathToNode, 'CallExpression')
|
||||
)
|
||||
const _err1 = nodes.find(err)
|
||||
if (err(_err1)) return _err1
|
||||
|
||||
const isAllTooltips = nodes.every((node) => {
|
||||
if (err(node)) return false
|
||||
return node && toolTips.includes(node.callee.name as any)
|
||||
const isAllTooltips = nodes.every((meta) => {
|
||||
if (err(meta)) return false
|
||||
return (
|
||||
meta.node?.type === 'CallExpression' &&
|
||||
toolTips.includes(meta.node.callee.name as any)
|
||||
)
|
||||
})
|
||||
|
||||
const transforms = getTransformInfos(
|
||||
|
@ -57,7 +57,8 @@
|
||||
transition-delay: var(--_delay);
|
||||
}
|
||||
|
||||
:is(:focus-visible) > .tooltipWrapper.withFocus {
|
||||
:is(:focus-visible) > .tooltipWrapper.withFocus,
|
||||
:focus-within > .tooltipWrapper.withFocus {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ export default function Tooltip({
|
||||
return (
|
||||
<div
|
||||
// @ts-ignore while awaiting merge of this PR for support of "inert" https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60822
|
||||
inert={inert}
|
||||
{...{ inert: inert ? '' : undefined }}
|
||||
role="tooltip"
|
||||
className={`p-3 ${
|
||||
position !== 'left' && position !== 'right' ? 'px-0' : ''
|
||||
|
@ -3,7 +3,7 @@ import { EditorView, Decoration } from '@codemirror/view'
|
||||
|
||||
export { EditorView }
|
||||
|
||||
export const addLineHighlight = StateEffect.define<[number, number]>()
|
||||
export const addLineHighlight = StateEffect.define<Array<[number, number]>>()
|
||||
|
||||
const addLineHighlightAnnotation = Annotation.define<null>()
|
||||
export const addLineHighlightEvent = addLineHighlightAnnotation.of(null)
|
||||
@ -24,10 +24,18 @@ export const lineHighlightField = StateField.define({
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(addLineHighlight)) {
|
||||
lines = Decoration.none
|
||||
const [from, to] = e.value || [0, 0]
|
||||
if (from && to && !(from === to && from === 0)) {
|
||||
lines = lines.update({ add: [matchDeco.range(from, to)] })
|
||||
deco.push(matchDeco.range(from, to))
|
||||
for (let index = 0; index < e.value.length; index++) {
|
||||
const highlightRange = e.value[index]
|
||||
const [from, to] = highlightRange || [0, 0]
|
||||
if (from && to && !(from === to && from === 0)) {
|
||||
if (index === 0) {
|
||||
lines = lines.update({ add: [matchDeco.range(from, to)] })
|
||||
deco.push(matchDeco.range(from, to))
|
||||
} else {
|
||||
lines = lines.update({ add: [matchDeco2.range(from, to)] })
|
||||
deco.push(matchDeco2.range(from, to))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -37,6 +45,10 @@ export const lineHighlightField = StateField.define({
|
||||
})
|
||||
|
||||
const matchDeco = Decoration.mark({
|
||||
class: 'bg-yellow-200',
|
||||
class: 'bg-yellow-300/70',
|
||||
attributes: { 'data-testid': 'hover-highlight' },
|
||||
})
|
||||
const matchDeco2 = Decoration.mark({
|
||||
class: 'bg-yellow-200/40',
|
||||
attributes: { 'data-testid': 'hover-highlight' },
|
||||
})
|
||||
|
@ -6,7 +6,11 @@ import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
|
||||
import { undo, redo } from '@codemirror/commands'
|
||||
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
|
||||
import { addLineHighlight, addLineHighlightEvent } from './highlightextension'
|
||||
import { Diagnostic, setDiagnosticsEffect } from '@codemirror/lint'
|
||||
import {
|
||||
Diagnostic,
|
||||
forEachDiagnostic,
|
||||
setDiagnosticsEffect,
|
||||
} from '@codemirror/lint'
|
||||
|
||||
const updateOutsideEditorAnnotation = Annotation.define<boolean>()
|
||||
export const updateOutsideEditorEvent = updateOutsideEditorAnnotation.of(true)
|
||||
@ -42,7 +46,7 @@ export default class EditorManager {
|
||||
private _convertToVariableEnabled: boolean = false
|
||||
private _convertToVariableCallback: () => void = () => {}
|
||||
|
||||
private _highlightRange: [number, number] = [0, 0]
|
||||
private _highlightRange: Array<[number, number]> = [[0, 0]]
|
||||
|
||||
setCopilotEnabled(enabled: boolean) {
|
||||
this._copilotEnabled = enabled
|
||||
@ -88,19 +92,21 @@ export default class EditorManager {
|
||||
return this._commandBarSend(eventInfo)
|
||||
}
|
||||
|
||||
get highlightRange(): [number, number] {
|
||||
get highlightRange(): Array<[number, number]> {
|
||||
return this._highlightRange
|
||||
}
|
||||
|
||||
setHighlightRange(selection: Selection['range']): void {
|
||||
this._highlightRange = selection
|
||||
const safeEnd = Math.min(
|
||||
selection[1],
|
||||
this._editorView?.state.doc.length || selection[1]
|
||||
)
|
||||
setHighlightRange(selections: Array<Selection['range']>): void {
|
||||
this._highlightRange = selections
|
||||
|
||||
const selectionsWithSafeEnds = selections.map((s): [number, number] => {
|
||||
const safeEnd = Math.min(s[1], this._editorView?.state.doc.length || s[1])
|
||||
return [s[0], safeEnd]
|
||||
})
|
||||
|
||||
if (this._editorView) {
|
||||
this._editorView.dispatch({
|
||||
effects: addLineHighlight.of([selection[0], safeEnd]),
|
||||
effects: addLineHighlight.of(selectionsWithSafeEnds),
|
||||
annotations: [
|
||||
updateOutsideEditorEvent,
|
||||
addLineHighlightEvent,
|
||||
@ -135,6 +141,38 @@ export default class EditorManager {
|
||||
})
|
||||
}
|
||||
|
||||
scrollToFirstErrorDiagnosticIfExists() {
|
||||
if (!this._editorView) return
|
||||
|
||||
let firstDiagnosticPos: [number, number] | null = null
|
||||
forEachDiagnostic(
|
||||
this._editorView.state,
|
||||
(d: Diagnostic, from: number, to: number) => {
|
||||
if (!firstDiagnosticPos && d.severity === 'error') {
|
||||
firstDiagnosticPos = [from, to]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (!firstDiagnosticPos) return
|
||||
|
||||
this._editorView.focus()
|
||||
this._editorView.dispatch({
|
||||
selection: EditorSelection.create([
|
||||
EditorSelection.cursor(firstDiagnosticPos[0]),
|
||||
]),
|
||||
effects: [
|
||||
EditorView.scrollIntoView(
|
||||
EditorSelection.range(firstDiagnosticPos[0], firstDiagnosticPos[1])
|
||||
),
|
||||
],
|
||||
annotations: [
|
||||
updateOutsideEditorEvent,
|
||||
Transaction.addToHistory.of(false),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this._editorView) {
|
||||
undo(this._editorView)
|
||||
|
@ -12,3 +12,4 @@ export const VITE_KC_DEV_TOKEN = import.meta.env.VITE_KC_DEV_TOKEN as
|
||||
| undefined
|
||||
export const TEST = import.meta.env.TEST
|
||||
export const DEV = import.meta.env.DEV
|
||||
export const CI = import.meta.env.CI
|
||||
|
@ -1,10 +1,24 @@
|
||||
import { useEffect } from 'react'
|
||||
import { editorManager, engineCommandManager } from 'lib/singletons'
|
||||
import {
|
||||
editorManager,
|
||||
engineCommandManager,
|
||||
kclManager,
|
||||
sceneInfra,
|
||||
} from 'lib/singletons'
|
||||
import { useModelingContext } from './useModelingContext'
|
||||
import { getEventForSelectWithPoint } from 'lib/selections'
|
||||
import {
|
||||
getCapCodeRef,
|
||||
getExtrusionFromSuspectedExtrudeSurface,
|
||||
getSolid2dCodeRef,
|
||||
getWallCodeRef,
|
||||
} from 'lang/std/artifactGraph'
|
||||
import { err } from 'lib/trap'
|
||||
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
|
||||
export function useEngineConnectionSubscriptions() {
|
||||
const { send, context } = useModelingContext()
|
||||
const { send, context, state } = useModelingContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!engineCommandManager) return
|
||||
@ -14,24 +28,58 @@ export function useEngineConnectionSubscriptions() {
|
||||
event: 'highlight_set_entity',
|
||||
callback: ({ data }) => {
|
||||
if (data?.entity_id) {
|
||||
const sourceRange = engineCommandManager.artifactMap?.[data.entity_id]
|
||||
?.range || [0, 0]
|
||||
editorManager.setHighlightRange(sourceRange)
|
||||
const artifact = engineCommandManager.artifactGraph.get(
|
||||
data.entity_id
|
||||
)
|
||||
if (artifact?.type === 'solid2D') {
|
||||
const codeRef = getSolid2dCodeRef(
|
||||
artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(codeRef)) return
|
||||
editorManager.setHighlightRange([codeRef.range])
|
||||
} else if (artifact?.type === 'cap') {
|
||||
const codeRef = getCapCodeRef(
|
||||
artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(codeRef)) return
|
||||
editorManager.setHighlightRange([codeRef.range])
|
||||
} else if (artifact?.type === 'wall') {
|
||||
const extrusion = getExtrusionFromSuspectedExtrudeSurface(
|
||||
data.entity_id,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
const codeRef = getWallCodeRef(
|
||||
artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(codeRef)) return
|
||||
editorManager.setHighlightRange(
|
||||
err(extrusion)
|
||||
? [codeRef.range]
|
||||
: [codeRef.range, extrusion.codeRef.range]
|
||||
)
|
||||
} else if (artifact?.type === 'segment') {
|
||||
editorManager.setHighlightRange([
|
||||
artifact?.codeRef?.range || [0, 0],
|
||||
])
|
||||
} else {
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
}
|
||||
} else if (
|
||||
!editorManager.highlightRange ||
|
||||
(editorManager.highlightRange[0] !== 0 &&
|
||||
editorManager.highlightRange[1] !== 0)
|
||||
(editorManager.highlightRange[0][0] !== 0 &&
|
||||
editorManager.highlightRange[0][1] !== 0)
|
||||
) {
|
||||
editorManager.setHighlightRange([0, 0])
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
}
|
||||
},
|
||||
})
|
||||
const unSubClick = engineCommandManager.subscribeTo({
|
||||
event: 'select_with_point',
|
||||
callback: async (engineEvent) => {
|
||||
const event = await getEventForSelectWithPoint(engineEvent, {
|
||||
sketchEnginePathId: context.sketchEnginePathId,
|
||||
})
|
||||
const event = await getEventForSelectWithPoint(engineEvent)
|
||||
event && send(event)
|
||||
},
|
||||
})
|
||||
@ -40,4 +88,126 @@ export function useEngineConnectionSubscriptions() {
|
||||
unSubClick()
|
||||
}
|
||||
}, [engineCommandManager, context?.sketchEnginePathId])
|
||||
|
||||
useEffect(() => {
|
||||
const unSub = engineCommandManager.subscribeTo({
|
||||
event: 'select_with_point',
|
||||
callback: state.matches('Sketch no face')
|
||||
? async ({ data }) => {
|
||||
let planeOrFaceId = data.entity_id
|
||||
if (!planeOrFaceId) return
|
||||
if (
|
||||
engineCommandManager.defaultPlanes?.xy === planeOrFaceId ||
|
||||
engineCommandManager.defaultPlanes?.xz === planeOrFaceId ||
|
||||
engineCommandManager.defaultPlanes?.yz === planeOrFaceId ||
|
||||
engineCommandManager.defaultPlanes?.negXy === planeOrFaceId ||
|
||||
engineCommandManager.defaultPlanes?.negXz === planeOrFaceId ||
|
||||
engineCommandManager.defaultPlanes?.negYz === planeOrFaceId
|
||||
) {
|
||||
let planeId = planeOrFaceId
|
||||
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
|
||||
[engineCommandManager.defaultPlanes.xy]: 'XY',
|
||||
[engineCommandManager.defaultPlanes.xz]: 'XZ',
|
||||
[engineCommandManager.defaultPlanes.yz]: 'YZ',
|
||||
[engineCommandManager.defaultPlanes.negXy]: '-XY',
|
||||
[engineCommandManager.defaultPlanes.negXz]: '-XZ',
|
||||
[engineCommandManager.defaultPlanes.negYz]: '-YZ',
|
||||
}
|
||||
// TODO can we get this information from rust land when it creates the default planes?
|
||||
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
|
||||
let zAxis: [number, number, number] = [0, 0, 1]
|
||||
let yAxis: [number, number, number] = [0, 1, 0]
|
||||
|
||||
// get unit vector from camera position to target
|
||||
const camVector = sceneInfra.camControls.camera.position
|
||||
.clone()
|
||||
.sub(sceneInfra.camControls.target)
|
||||
|
||||
if (engineCommandManager.defaultPlanes?.xy === planeId) {
|
||||
zAxis = [0, 0, 1]
|
||||
yAxis = [0, 1, 0]
|
||||
if (camVector.z < 0) {
|
||||
zAxis = [0, 0, -1]
|
||||
planeId = engineCommandManager.defaultPlanes?.negXy || ''
|
||||
}
|
||||
} else if (engineCommandManager.defaultPlanes?.yz === planeId) {
|
||||
zAxis = [1, 0, 0]
|
||||
yAxis = [0, 0, 1]
|
||||
if (camVector.x < 0) {
|
||||
zAxis = [-1, 0, 0]
|
||||
planeId = engineCommandManager.defaultPlanes?.negYz || ''
|
||||
}
|
||||
} else if (engineCommandManager.defaultPlanes?.xz === planeId) {
|
||||
zAxis = [0, 1, 0]
|
||||
yAxis = [0, 0, 1]
|
||||
planeId = engineCommandManager.defaultPlanes?.negXz || ''
|
||||
if (camVector.y < 0) {
|
||||
zAxis = [0, -1, 0]
|
||||
planeId = engineCommandManager.defaultPlanes?.xz || ''
|
||||
}
|
||||
}
|
||||
|
||||
sceneInfra.modelingSend({
|
||||
type: 'Select default plane',
|
||||
data: {
|
||||
type: 'defaultPlane',
|
||||
planeId: planeId,
|
||||
plane: defaultPlaneStrMap[planeId],
|
||||
zAxis,
|
||||
yAxis,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
const faceId = planeOrFaceId
|
||||
const artifact = engineCommandManager.artifactGraph.get(faceId)
|
||||
const extrusion = getExtrusionFromSuspectedExtrudeSurface(
|
||||
faceId,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
|
||||
if (artifact?.type !== 'cap' && artifact?.type !== 'wall') return
|
||||
|
||||
const codeRef =
|
||||
artifact.type === 'cap'
|
||||
? getCapCodeRef(artifact, engineCommandManager.artifactGraph)
|
||||
: getWallCodeRef(artifact, engineCommandManager.artifactGraph)
|
||||
|
||||
const faceInfo = await getFaceDetails(faceId)
|
||||
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
||||
return
|
||||
const { z_axis, y_axis, origin } = faceInfo
|
||||
const sketchPathToNode = getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
err(codeRef) ? [0, 0] : codeRef.range
|
||||
)
|
||||
|
||||
const extrudePathToNode = !err(extrusion)
|
||||
? getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
extrusion.codeRef.range
|
||||
)
|
||||
: []
|
||||
|
||||
sceneInfra.modelingSend({
|
||||
type: 'Select default plane',
|
||||
data: {
|
||||
type: 'extrudeFace',
|
||||
zAxis: [z_axis.x, z_axis.y, z_axis.z],
|
||||
yAxis: [y_axis.x, y_axis.y, y_axis.z],
|
||||
position: [origin.x, origin.y, origin.z].map(
|
||||
(num) => num / sceneInfra._baseUnitMultiplier
|
||||
) as [number, number, number],
|
||||
sketchPathToNode,
|
||||
extrudePathToNode,
|
||||
cap: artifact.type === 'cap' ? artifact.subType : 'none',
|
||||
faceId: faceId,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
: () => {},
|
||||
})
|
||||
return unSub
|
||||
}, [state])
|
||||
}
|
||||
|
@ -46,7 +46,6 @@ export function useSetupEngineManager(
|
||||
streamRef?.current?.offsetHeight ?? 0
|
||||
)
|
||||
engineCommandManager.start({
|
||||
restart,
|
||||
setMediaStream: (mediaStream) => setMediaStream(mediaStream),
|
||||
setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }),
|
||||
width: quadWidth,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { executeAst, lintAst } from 'lang/langHelpers'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { KCLError, kclErrorsToDiagnostics } from './errors'
|
||||
import { isArray, uuidv4 } from 'lib/utils'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { EngineCommandManager } from './std/engineConnection'
|
||||
import { err } from 'lib/trap'
|
||||
|
||||
@ -15,17 +15,12 @@ import {
|
||||
recast,
|
||||
SourceRange,
|
||||
} from 'lang/wasm'
|
||||
import {
|
||||
expectNodeOnPath,
|
||||
getLastNodeFromPath,
|
||||
getNodeFromPath,
|
||||
} from './queryAst'
|
||||
import { getNodeFromPath } from './queryAst'
|
||||
import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
|
||||
import { Diagnostic } from '@codemirror/lint'
|
||||
|
||||
export class KclManager {
|
||||
private _ast: Program = {
|
||||
type: 'Program',
|
||||
body: [],
|
||||
start: 0,
|
||||
end: 0,
|
||||
@ -96,12 +91,16 @@ export class KclManager {
|
||||
set kclErrors(kclErrors) {
|
||||
if (kclErrors === this._kclErrors && this.lints.length === 0) return
|
||||
this._kclErrors = kclErrors
|
||||
let diagnostics = kclErrorsToDiagnostics(kclErrors)
|
||||
this.setDiagnosticsForCurrentErrors()
|
||||
this._kclErrorsCallBack(kclErrors)
|
||||
}
|
||||
|
||||
setDiagnosticsForCurrentErrors() {
|
||||
let diagnostics = kclErrorsToDiagnostics(this.kclErrors)
|
||||
if (this.lints.length > 0) {
|
||||
diagnostics = diagnostics.concat(this.lints)
|
||||
}
|
||||
editorManager.setDiagnostics(diagnostics)
|
||||
this._kclErrorsCallBack(kclErrors)
|
||||
}
|
||||
|
||||
addKclErrors(kclErrors: KCLError[]) {
|
||||
@ -161,7 +160,6 @@ export class KclManager {
|
||||
|
||||
clearAst() {
|
||||
this._ast = {
|
||||
type: 'Program',
|
||||
body: [],
|
||||
start: 0,
|
||||
end: 0,
|
||||
@ -316,24 +314,30 @@ export class KclManager {
|
||||
this._kclErrors = errors
|
||||
this._programMemory = programMemory
|
||||
if (updates !== 'artifactRanges') return
|
||||
Object.entries(this.engineCommandManager.artifactMap).forEach(
|
||||
|
||||
// TODO the below seems like a work around, I wish there's a comment explaining exactly what
|
||||
// problem this solves, but either way we should strive to remove it.
|
||||
Array.from(this.engineCommandManager.artifactGraph).forEach(
|
||||
([commandId, artifact]) => {
|
||||
if (!artifact.pathToNode) return
|
||||
const _node = getNodeFromPath<CallExpression>(
|
||||
if (!('codeRef' in artifact)) return
|
||||
const _node1 = getNodeFromPath<CallExpression>(
|
||||
this.ast,
|
||||
artifact.pathToNode,
|
||||
artifact.codeRef.pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(_node)) return
|
||||
const { node } = _node
|
||||
if (isArray(node) || node.type !== 'CallExpression') return
|
||||
const [oldStart, oldEnd] = artifact.range
|
||||
if (err(_node1)) return
|
||||
const { node } = _node1
|
||||
if (node.type !== 'CallExpression') return
|
||||
const [oldStart, oldEnd] = artifact.codeRef.range
|
||||
if (oldStart === 0 && oldEnd === 0) return
|
||||
if (oldStart === node.start && oldEnd === node.end) return
|
||||
this.engineCommandManager.artifactMap[commandId].range = [
|
||||
node.start,
|
||||
node.end,
|
||||
]
|
||||
this.engineCommandManager.artifactGraph.set(commandId, {
|
||||
...artifact,
|
||||
codeRef: {
|
||||
...artifact.codeRef,
|
||||
range: [node.start, node.end],
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -398,15 +402,12 @@ export class KclManager {
|
||||
let returnVal: Selections | undefined = undefined
|
||||
|
||||
if (optionalParams?.focusPath) {
|
||||
const _node1 = getLastNodeFromPath(
|
||||
const _node1 = getNodeFromPath<any>(
|
||||
astWithUpdatedSource,
|
||||
optionalParams?.focusPath
|
||||
)
|
||||
if (err(_node1)) return Promise.reject(_node1)
|
||||
const { node } = _node1
|
||||
if (isArray(node)) {
|
||||
return Promise.reject(new Error('Expected node to not be an array'))
|
||||
}
|
||||
|
||||
const { start, end } = node
|
||||
if (!start || !end)
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { isArray } from 'lib/utils'
|
||||
import { getNodePathFromSourceRange, getLastNodeFromPath } from './queryAst'
|
||||
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
|
||||
import { Identifier, parse, initPromise, Parameter } from './wasm'
|
||||
import { err } from 'lib/trap'
|
||||
|
||||
@ -26,13 +25,10 @@ const sk3 = startSketchAt([0, 0])
|
||||
const ast = parse(code)
|
||||
if (err(ast)) throw ast
|
||||
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const _node = getLastNodeFromPath(ast, nodePath)
|
||||
const _node = getNodeFromPath<any>(ast, nodePath)
|
||||
if (err(_node)) throw _node
|
||||
const { node } = _node
|
||||
|
||||
if (isArray(node)) {
|
||||
throw new Error('Expected call expression node, but found array')
|
||||
}
|
||||
expect([node.start, node.end]).toEqual(sourceRange)
|
||||
expect(node.type).toBe('CallExpression')
|
||||
})
|
||||
@ -57,7 +53,7 @@ const b1 = cube([0,0], 10)`
|
||||
const ast = parse(code)
|
||||
if (err(ast)) throw ast
|
||||
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const _node = getLastNodeFromPath(ast, nodePath)
|
||||
const _node = getNodeFromPath<Parameter>(ast, nodePath)
|
||||
if (err(_node)) throw _node
|
||||
const node = _node.node
|
||||
|
||||
@ -70,12 +66,8 @@ const b1 = cube([0,0], 10)`
|
||||
['params', 'FunctionExpression'],
|
||||
[0, 'index'],
|
||||
])
|
||||
if (isArray(node)) {
|
||||
throw new Error('Expected parameter node, but found array')
|
||||
}
|
||||
expect(node.type).toBe('Parameter')
|
||||
const param = node as any as Parameter
|
||||
expect(param.identifier.name).toBe('pos')
|
||||
expect(node.identifier.name).toBe('pos')
|
||||
})
|
||||
it('gets path right for deep within function definition body', () => {
|
||||
const code = `fn cube = (pos, scale) => {
|
||||
@ -98,7 +90,7 @@ const b1 = cube([0,0], 10)`
|
||||
const ast = parse(code)
|
||||
if (err(ast)) throw ast
|
||||
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const _node = getLastNodeFromPath(ast, nodePath)
|
||||
const _node = getNodeFromPath<Identifier>(ast, nodePath)
|
||||
if (err(_node)) throw _node
|
||||
const node = _node.node
|
||||
expect(nodePath).toEqual([
|
||||
@ -120,11 +112,7 @@ const b1 = cube([0,0], 10)`
|
||||
['elements', 'ArrayExpression'],
|
||||
[0, 'index'],
|
||||
])
|
||||
if (isArray(node)) {
|
||||
throw new Error('Expected identifier node, but found array')
|
||||
}
|
||||
expect(node.type).toBe('Identifier')
|
||||
const ident = node as any as Identifier
|
||||
expect(ident.name).toBe('scale')
|
||||
expect(node.name).toBe('scale')
|
||||
})
|
||||
})
|
||||
|
@ -119,7 +119,6 @@ describe('Testing addSketchTo', () => {
|
||||
it('should add a sketch to a program', () => {
|
||||
const result = addSketchTo(
|
||||
{
|
||||
type: 'Program',
|
||||
body: [],
|
||||
start: 0,
|
||||
end: 0,
|
||||
|
@ -27,8 +27,6 @@ import {
|
||||
getNodePathFromSourceRange,
|
||||
isNodeSafeToReplace,
|
||||
traverse,
|
||||
getLastNodeFromPath,
|
||||
expectNodeOnPath,
|
||||
} from './queryAst'
|
||||
import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch'
|
||||
import {
|
||||
@ -38,7 +36,7 @@ import {
|
||||
transformAstSketchLines,
|
||||
} from './std/sketchcombos'
|
||||
import { DefaultPlaneStr } from 'clientSideScene/sceneEntities'
|
||||
import { isArray, isOverlap, roundOff } from 'lib/utils'
|
||||
import { isOverlap, roundOff } from 'lib/utils'
|
||||
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
|
||||
import { ConstrainInfo } from './std/stdTypes'
|
||||
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
|
||||
@ -81,12 +79,16 @@ export function addStartProfileAt(
|
||||
pathToNode: PathToNode,
|
||||
at: [number, number]
|
||||
): { modifiedAst: Program; pathToNode: PathToNode } | Error {
|
||||
const variableDeclaration = expectNodeOnPath<VariableDeclaration>(
|
||||
const _node1 = getNodeFromPath<VariableDeclaration>(
|
||||
node,
|
||||
pathToNode,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(variableDeclaration)) return variableDeclaration
|
||||
if (err(_node1)) return _node1
|
||||
const variableDeclaration = _node1.node
|
||||
if (variableDeclaration.type !== 'VariableDeclaration') {
|
||||
return new Error('variableDeclaration.init.type !== PipeExpression')
|
||||
}
|
||||
const _node = { ...node }
|
||||
const init = variableDeclaration.declarations[0].init
|
||||
const startProfileAt = createCallExpressionStdLib('startProfileAt', [
|
||||
@ -261,7 +263,7 @@ export function extrudeSketch(
|
||||
}
|
||||
| Error {
|
||||
const _node = { ...node }
|
||||
const _node1 = getLastNodeFromPath(_node, pathToNode)
|
||||
const _node1 = getNodeFromPath(_node, pathToNode)
|
||||
if (err(_node1)) return _node1
|
||||
const { node: sketchExpression } = _node1
|
||||
|
||||
@ -272,9 +274,9 @@ export function extrudeSketch(
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(_node2)) return _node2
|
||||
const { stopAtNode: pipeExpression } = _node2
|
||||
const { node: pipeExpression } = _node2
|
||||
|
||||
const isInPipeExpression = !!pipeExpression
|
||||
const isInPipeExpression = pipeExpression.type === 'PipeExpression'
|
||||
|
||||
const _node3 = getNodeFromPath<VariableDeclarator>(
|
||||
_node,
|
||||
@ -282,11 +284,7 @@ export function extrudeSketch(
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(_node3)) return _node3
|
||||
const { stopAtNode: variableDeclarator, shallowPath: pathToDecleration } =
|
||||
_node3
|
||||
if (!variableDeclarator) {
|
||||
return new Error('VariableDeclarator not found')
|
||||
}
|
||||
const { node: variableDeclarator, shallowPath: pathToDecleration } = _node3
|
||||
|
||||
const extrudeCall = createCallExpressionStdLib('extrude', [
|
||||
distance,
|
||||
@ -358,34 +356,34 @@ export function sketchOnExtrudedFace(
|
||||
node,
|
||||
KCL_DEFAULT_CONSTANT_PREFIXES.SKETCH
|
||||
)
|
||||
const oldSketchNode = expectNodeOnPath<VariableDeclarator>(
|
||||
const _node1 = getNodeFromPath<VariableDeclarator>(
|
||||
_node,
|
||||
sketchPathToNode,
|
||||
'VariableDeclarator',
|
||||
{
|
||||
firstFound: true,
|
||||
message: 'Old sketch node not found',
|
||||
}
|
||||
true
|
||||
)
|
||||
if (err(oldSketchNode)) return oldSketchNode
|
||||
if (err(_node1)) return _node1
|
||||
const { node: oldSketchNode } = _node1
|
||||
|
||||
const oldSketchName = oldSketchNode.id.name
|
||||
const expression = expectNodeOnPath<CallExpression>(
|
||||
const _node2 = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
sketchPathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(expression)) return expression
|
||||
if (err(_node2)) return _node2
|
||||
const { node: expression } = _node2
|
||||
|
||||
const extrudeVarDec = expectNodeOnPath<VariableDeclarator>(
|
||||
const _node3 = getNodeFromPath<VariableDeclarator>(
|
||||
_node,
|
||||
extrudePathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(extrudeVarDec)) return extrudeVarDec
|
||||
const extrudeName = extrudeVarDec.id.name
|
||||
if (err(_node3)) return _node3
|
||||
const { node: extrudeVarDec } = _node3
|
||||
const extrudeName = extrudeVarDec.id?.name
|
||||
|
||||
let _tag: Identifier | Literal | null = null
|
||||
let _tag = null
|
||||
if (cap === 'none') {
|
||||
const __tag = addTagForSketchOnFace(
|
||||
{
|
||||
@ -680,12 +678,9 @@ export function giveSketchFnCallTag(
|
||||
}
|
||||
| Error {
|
||||
const path = getNodePathFromSourceRange(ast, range)
|
||||
const primaryCallExp = expectNodeOnPath<CallExpression>(
|
||||
ast,
|
||||
path,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(primaryCallExp)) return primaryCallExp
|
||||
const _node1 = getNodeFromPath<CallExpression>(ast, path, 'CallExpression')
|
||||
if (err(_node1)) return _node1
|
||||
const { node: primaryCallExp } = _node1
|
||||
|
||||
// Tag is always 3rd expression now, using arg index feels brittle
|
||||
// but we can come up with a better way to identify tag later.
|
||||
@ -789,35 +784,27 @@ export function deleteSegmentFromPipeExpression(
|
||||
): Program | Error {
|
||||
let _modifiedAst = structuredClone(modifiedAst)
|
||||
|
||||
for (const range of dependentRanges) {
|
||||
dependentRanges.forEach((range) => {
|
||||
const path = getNodePathFromSourceRange(_modifiedAst, range)
|
||||
|
||||
const _callExp = getNodeFromPath<CallExpression>(
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
_modifiedAst,
|
||||
path,
|
||||
'CallExpression',
|
||||
true
|
||||
)
|
||||
if (err(_callExp)) return _callExp
|
||||
const callExp = _callExp.stopAtNode
|
||||
if (!callExp) {
|
||||
return new Error('Call Expression not found')
|
||||
}
|
||||
if (err(callExp)) return callExp
|
||||
|
||||
const constraintInfo = getConstraintInfo(callExp, code, path).find(
|
||||
const constraintInfo = getConstraintInfo(callExp.node, code, path).find(
|
||||
({ sourceRange }) => isOverlap(sourceRange, range)
|
||||
)
|
||||
if (!constraintInfo) {
|
||||
return new Error('Constraint Info not found')
|
||||
}
|
||||
if (!constraintInfo) return
|
||||
|
||||
const input = makeRemoveSingleConstraintInput(
|
||||
constraintInfo.argPosition,
|
||||
_callExp.shallowPath
|
||||
callExp.shallowPath
|
||||
)
|
||||
if (!input) {
|
||||
return new Error('Input not found')
|
||||
}
|
||||
if (!input) return
|
||||
const transform = removeSingleConstraintInfo(
|
||||
{
|
||||
...input,
|
||||
@ -825,13 +812,11 @@ export function deleteSegmentFromPipeExpression(
|
||||
_modifiedAst,
|
||||
programMemory
|
||||
)
|
||||
if (!transform) {
|
||||
return new Error('Transform not found')
|
||||
}
|
||||
if (!transform) return
|
||||
_modifiedAst = transform.modifiedAst
|
||||
}
|
||||
})
|
||||
|
||||
const pipeExpression = expectNodeOnPath<PipeExpression>(
|
||||
const pipeExpression = getNodeFromPath<PipeExpression>(
|
||||
_modifiedAst,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
@ -842,7 +827,7 @@ export function deleteSegmentFromPipeExpression(
|
||||
([_, desc]) => desc === 'PipeExpression'
|
||||
)
|
||||
const segmentIndexInPipe = pathToNode[pipeInPathIndex + 1]
|
||||
pipeExpression.body.splice(segmentIndexInPipe[0] as number, 1)
|
||||
pipeExpression.node.body.splice(segmentIndexInPipe[0] as number, 1)
|
||||
|
||||
// Move up to the next segment.
|
||||
segmentIndexInPipe[0] = Math.max((segmentIndexInPipe[0] as number) - 1, 0)
|
||||
@ -917,23 +902,19 @@ export async function deleteFromSelection(
|
||||
const astClone = structuredClone(ast)
|
||||
const range = selection.range
|
||||
const path = getNodePathFromSourceRange(ast, range)
|
||||
const _varDec = getNodeFromPath<VariableDeclarator>(
|
||||
const varDec = getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
path,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(_varDec)) return _varDec
|
||||
const { stopAtNode: varDec } = _varDec
|
||||
if (!varDec) {
|
||||
return new Error('VariableDeclarator not found')
|
||||
}
|
||||
if (err(varDec)) return varDec
|
||||
if (
|
||||
(selection.type === 'extrude-wall' ||
|
||||
selection.type === 'end-cap' ||
|
||||
selection.type === 'start-cap') &&
|
||||
varDec.init.type === 'PipeExpression'
|
||||
varDec.node.init.type === 'PipeExpression'
|
||||
) {
|
||||
const varDecName = varDec.id.name
|
||||
const varDecName = varDec.node.id.name
|
||||
let pathToNode: PathToNode | null = null
|
||||
let extrudeNameToDelete = ''
|
||||
traverse(astClone, {
|
||||
@ -995,16 +976,13 @@ export async function deleteFromSelection(
|
||||
lastKey: number
|
||||
}[] = []
|
||||
for (const { path, sketchName } of pathsDependingOnExtrude) {
|
||||
const _parent = getLastNodeFromPath(astClone, path.slice(0, -1))
|
||||
if (err(_parent)) {
|
||||
const parent = getNodeFromPath<PipeExpression['body']>(
|
||||
astClone,
|
||||
path.slice(0, -1)
|
||||
)
|
||||
if (err(parent)) {
|
||||
return
|
||||
}
|
||||
const { node: parent } = _parent
|
||||
if (!isArray(parent)) {
|
||||
console.error(`Parent is not an array: ${parent}`)
|
||||
return
|
||||
}
|
||||
const pipeBodyItems = parent as PipeExpression['body']
|
||||
const sketchToPreserve = programMemory.get(sketchName) as SketchGroup
|
||||
console.log('sketchName', sketchName)
|
||||
// Can't kick off multiple requests at once as getFaceDetails
|
||||
@ -1022,7 +1000,7 @@ export async function deleteFromSelection(
|
||||
}
|
||||
const lastKey = Number(path.slice(-1)[0][0])
|
||||
modificationDetails.push({
|
||||
parent: pipeBodyItems,
|
||||
parent: parent.node,
|
||||
faceDetails,
|
||||
lastKey,
|
||||
})
|
||||
@ -1070,14 +1048,14 @@ export async function deleteFromSelection(
|
||||
}
|
||||
// await prom
|
||||
return astClone
|
||||
} else if (varDec.init.type === 'PipeExpression') {
|
||||
const pipeBody = varDec.init.body
|
||||
} else if (varDec.node.init.type === 'PipeExpression') {
|
||||
const pipeBody = varDec.node.init.body
|
||||
if (
|
||||
pipeBody[0].type === 'CallExpression' &&
|
||||
pipeBody[0].callee.name === 'startSketchOn'
|
||||
) {
|
||||
// remove varDec
|
||||
const varDecIndex = _varDec.shallowPath[1][0] as number
|
||||
const varDecIndex = varDec.shallowPath[1][0] as number
|
||||
astClone.body.splice(varDecIndex, 1)
|
||||
return astClone
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
hasValidFilletSelection,
|
||||
isTagUsedInFillet,
|
||||
} from './addFillet'
|
||||
import { expectNodeOnPath, getNodePathFromSourceRange } from '../queryAst'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
||||
import { createLiteral } from 'lang/modifyAst'
|
||||
import { err } from 'lib/trap'
|
||||
import { Selections } from 'lib/selections'
|
||||
@ -270,13 +270,13 @@ const extrude001 = extrude(-5, sketch001)
|
||||
]
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
if (err(pathToNode)) return
|
||||
const callExp = expectNodeOnPath<CallExpression>(
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(callExp)) return
|
||||
const edges = isTagUsedInFillet({ ast, callExp })
|
||||
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
|
||||
expect(edges).toEqual(['getOppositeEdge', 'baseEdge'])
|
||||
})
|
||||
it('should correctly identify getPreviousAdjacentEdge edges', () => {
|
||||
@ -289,13 +289,13 @@ const extrude001 = extrude(-5, sketch001)
|
||||
]
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
if (err(pathToNode)) return
|
||||
const callExp = expectNodeOnPath<CallExpression>(
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(callExp)) return
|
||||
const edges = isTagUsedInFillet({ ast, callExp })
|
||||
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
|
||||
expect(edges).toEqual(['getPreviousAdjacentEdge'])
|
||||
})
|
||||
it('should correctly identify no edges', () => {
|
||||
@ -308,13 +308,13 @@ const extrude001 = extrude(-5, sketch001)
|
||||
]
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
if (err(pathToNode)) return
|
||||
const callExp = expectNodeOnPath<CallExpression>(
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(callExp)) return
|
||||
const edges = isTagUsedInFillet({ ast, callExp })
|
||||
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
|
||||
expect(edges).toEqual([])
|
||||
})
|
||||
})
|
||||
|
@ -18,7 +18,6 @@ import {
|
||||
createPipeExpression,
|
||||
} from '../modifyAst'
|
||||
import {
|
||||
expectNodeOnPath,
|
||||
getNodeFromPath,
|
||||
getNodePathFromSourceRange,
|
||||
hasSketchPipeBeenExtruded,
|
||||
@ -47,12 +46,15 @@ export function addFillet(
|
||||
*/
|
||||
|
||||
// Find the specific sketch segment to tag with the new tag
|
||||
const sketchSegmentNode = expectNodeOnPath<CallExpression>(
|
||||
const sketchSegmentChunk = getNodeFromPath(
|
||||
_node,
|
||||
pathToSegmentNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(sketchSegmentNode)) return sketchSegmentNode
|
||||
if (err(sketchSegmentChunk)) return sketchSegmentChunk
|
||||
const { node: sketchSegmentNode } = sketchSegmentChunk as {
|
||||
node: CallExpression
|
||||
}
|
||||
|
||||
// Check whether selection is a valid segment from sketchLineHelpersMap
|
||||
if (!(sketchSegmentNode.callee.name in sketchLineHelperMap)) {
|
||||
@ -91,12 +93,13 @@ export function addFillet(
|
||||
])
|
||||
|
||||
// Locate the extrude call
|
||||
const extrudeVarDecl = expectNodeOnPath<VariableDeclaration>(
|
||||
const extrudeChunk = getNodeFromPath<VariableDeclaration>(
|
||||
_node,
|
||||
pathToExtrudeNode,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(extrudeVarDecl)) return extrudeVarDecl
|
||||
if (err(extrudeChunk)) return extrudeChunk
|
||||
const { node: extrudeVarDecl } = extrudeChunk
|
||||
|
||||
const extrudeDeclarator = extrudeVarDecl.declarations[0]
|
||||
const extrudeInit = extrudeDeclarator.init
|
||||
@ -278,12 +281,12 @@ export const hasValidFilletSelection = ({
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(segmentNode)) return false
|
||||
if (segmentNode.stopAtNode) {
|
||||
const segmentName = segmentNode.stopAtNode.callee.name
|
||||
if (segmentNode.node.type === 'CallExpression') {
|
||||
const segmentName = segmentNode.node.callee.name
|
||||
if (segmentName in sketchLineHelperMap) {
|
||||
const edges = isTagUsedInFillet({
|
||||
ast,
|
||||
callExp: segmentNode.stopAtNode,
|
||||
callExp: segmentNode.node,
|
||||
})
|
||||
// edge has already been filleted
|
||||
if (
|
||||
|
@ -19,47 +19,13 @@ import {
|
||||
} from './wasm'
|
||||
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
|
||||
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
|
||||
import { getAngle, isArray } from '../lib/utils'
|
||||
import { getAngle } from '../lib/utils'
|
||||
import { getFirstArg } from './std/sketch'
|
||||
import {
|
||||
getConstraintLevelFromSourceRange,
|
||||
getConstraintType,
|
||||
} from './std/sketchcombos'
|
||||
import { err } from 'lib/trap'
|
||||
import { BodyItem } from 'wasm-lib/kcl/bindings/BodyItem'
|
||||
|
||||
export interface DynamicNode {
|
||||
type: SyntaxType
|
||||
// Source range of the node.
|
||||
start: number
|
||||
end: number
|
||||
[index: string]: unknown
|
||||
}
|
||||
|
||||
function isAstNode(
|
||||
node: any
|
||||
): node is { type: SyntaxType; start: number; end: number } {
|
||||
// TODO: Should we check for start and end also?
|
||||
return node && typeof node === 'object' && 'type' in node
|
||||
}
|
||||
|
||||
/**
|
||||
* Given T and its corresponding SyntaxType, narrow the node to type T if
|
||||
* node.type matches.
|
||||
*/
|
||||
export function isNodeType<T extends DynamicNode>(
|
||||
node: unknown,
|
||||
syntaxType: SyntaxType | SyntaxType[]
|
||||
): node is T {
|
||||
return (
|
||||
!!node &&
|
||||
typeof node === 'object' &&
|
||||
'type' in node &&
|
||||
(isArray(syntaxType)
|
||||
? syntaxType.includes(node.type as SyntaxType)
|
||||
: node.type === syntaxType)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
|
||||
@ -67,64 +33,51 @@ export function isNodeType<T extends DynamicNode>(
|
||||
* and return the node at the end of this path.
|
||||
* By default it will return the node of the deepest "stopAt" type encountered, or the node at the end of the path if no "stopAt" type is provided.
|
||||
* If the "returnEarly" flag is set to true, the function will return as soon as a node of the specified type is found.
|
||||
*
|
||||
* If stopAt is provided, it must match T's type property.
|
||||
*/
|
||||
export function getNodeFromPath<T extends DynamicNode>(
|
||||
export function getNodeFromPath<T>(
|
||||
node: Program,
|
||||
path: PathToNode,
|
||||
stopAt?: SyntaxType | SyntaxType[],
|
||||
returnEarly = false
|
||||
):
|
||||
| {
|
||||
node: DynamicNode | unknown[]
|
||||
stopAtNode: T | null
|
||||
node: T
|
||||
shallowPath: PathToNode
|
||||
deepPath: PathToNode
|
||||
}
|
||||
| Error {
|
||||
let currentNode: DynamicNode | unknown[] = node
|
||||
let stopAtNode: T | null = null
|
||||
let currentNode = node as any
|
||||
let stopAtNode = null
|
||||
let successfulPaths: PathToNode = []
|
||||
let pathsExplored: PathToNode = []
|
||||
for (const pathItem of path) {
|
||||
const pathIndex = pathItem[0]
|
||||
let nextNode: unknown
|
||||
if (currentNode && typeof pathIndex === 'number' && isArray(currentNode)) {
|
||||
nextNode = currentNode[pathIndex]
|
||||
}
|
||||
if (
|
||||
currentNode &&
|
||||
typeof pathIndex === 'string' &&
|
||||
typeof currentNode === 'object' &&
|
||||
!isArray(currentNode)
|
||||
) {
|
||||
nextNode = currentNode[pathIndex]
|
||||
}
|
||||
if (!isArray(nextNode) && !isAstNode(nextNode)) {
|
||||
if (isAstNode(stopAtNode)) {
|
||||
if (typeof currentNode[pathItem[0]] !== 'object') {
|
||||
if (stopAtNode) {
|
||||
return {
|
||||
node: stopAtNode,
|
||||
stopAtNode,
|
||||
shallowPath: pathsExplored,
|
||||
deepPath: successfulPaths,
|
||||
}
|
||||
}
|
||||
return new Error('not an object')
|
||||
}
|
||||
currentNode = nextNode
|
||||
currentNode = currentNode?.[pathItem[0]]
|
||||
successfulPaths.push(pathItem)
|
||||
if (!stopAtNode) {
|
||||
pathsExplored.push(pathItem)
|
||||
}
|
||||
if (stopAt && isNodeType<T>(currentNode, stopAt)) {
|
||||
if (
|
||||
typeof stopAt !== 'undefined' &&
|
||||
(Array.isArray(stopAt)
|
||||
? stopAt.includes(currentNode.type)
|
||||
: currentNode.type === stopAt)
|
||||
) {
|
||||
// it will match the deepest node of the type
|
||||
// instead of returning at the first match
|
||||
stopAtNode = currentNode
|
||||
if (returnEarly) {
|
||||
return {
|
||||
node: stopAtNode,
|
||||
stopAtNode,
|
||||
shallowPath: pathsExplored,
|
||||
deepPath: successfulPaths,
|
||||
}
|
||||
@ -133,104 +86,32 @@ export function getNodeFromPath<T extends DynamicNode>(
|
||||
}
|
||||
return {
|
||||
node: stopAtNode || currentNode,
|
||||
stopAtNode,
|
||||
shallowPath: pathsExplored,
|
||||
deepPath: successfulPaths,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the terminal node in the given path. Like getNodeFromPath, but no
|
||||
* stopAt parameter.
|
||||
*/
|
||||
export function getLastNodeFromPath(
|
||||
node: Program,
|
||||
path: PathToNode
|
||||
):
|
||||
| {
|
||||
node: DynamicNode | unknown[]
|
||||
}
|
||||
| Error {
|
||||
const _result = getNodeFromPath<DynamicNode>(node, path)
|
||||
if (err(_result)) return _result
|
||||
return {
|
||||
node: _result.node,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the terminal node in the given path, and asserts that it's the given
|
||||
* type.
|
||||
*/
|
||||
export function expectLastNodeFromPath<T extends DynamicNode>(
|
||||
node: Program,
|
||||
path: PathToNode,
|
||||
syntaxType: SyntaxType | SyntaxType[]
|
||||
): T | Error {
|
||||
const result = getLastNodeFromPath(node, path)
|
||||
if (err(result)) return result
|
||||
if (!isNodeType<T>(result.node, syntaxType)) {
|
||||
return new Error(
|
||||
`Expected node of type ${syntaxType}: found ${JSON.stringify(result)}`
|
||||
)
|
||||
}
|
||||
|
||||
return result.node
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the node in the path with the given type. Like getNodeFromPath, but
|
||||
* if no stopAt node is found, an error is returned instead of null.
|
||||
*
|
||||
* @param {boolean} [options.firstFound=false] if true, return the first node of
|
||||
* the type found. The default returns the last node found.
|
||||
* @param {boolean} [options.message] the error message to return if the node is
|
||||
* not found.
|
||||
*/
|
||||
export function expectNodeOnPath<T extends DynamicNode>(
|
||||
node: Program,
|
||||
path: PathToNode,
|
||||
syntaxType: SyntaxType,
|
||||
options: { firstFound?: boolean; message?: string } = {
|
||||
firstFound: false,
|
||||
}
|
||||
): T | Error {
|
||||
const result = getNodeFromPath<T>(node, path, syntaxType, options.firstFound)
|
||||
if (err(result)) return result
|
||||
if (!result.stopAtNode) {
|
||||
return new Error(
|
||||
options.message ?? `Node of type ${syntaxType} not found in path`
|
||||
)
|
||||
}
|
||||
return result.stopAtNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Functions the same as getNodeFromPath, but returns a curried function that can be called with the stopAt and returnEarly arguments.
|
||||
*/
|
||||
export function getNodeFromPathCurry(
|
||||
node: Program,
|
||||
path: PathToNode
|
||||
): <T extends DynamicNode>(
|
||||
stopAt: SyntaxType | SyntaxType[],
|
||||
): <T>(
|
||||
stopAt?: SyntaxType | SyntaxType[],
|
||||
returnEarly?: boolean
|
||||
) =>
|
||||
| {
|
||||
node: DynamicNode | unknown[]
|
||||
stopAtNode: T | null
|
||||
node: T
|
||||
path: PathToNode
|
||||
}
|
||||
| Error {
|
||||
return <T extends DynamicNode>(
|
||||
stopAt: SyntaxType | SyntaxType[],
|
||||
returnEarly = false
|
||||
) => {
|
||||
return <T>(stopAt?: SyntaxType | SyntaxType[], returnEarly = false) => {
|
||||
const _node1 = getNodeFromPath<T>(node, path, stopAt, returnEarly)
|
||||
if (err(_node1)) return _node1
|
||||
const { node: _node, stopAtNode, shallowPath } = _node1
|
||||
const { node: _node, shallowPath } = _node1
|
||||
return {
|
||||
node: _node,
|
||||
stopAtNode,
|
||||
path: shallowPath,
|
||||
}
|
||||
}
|
||||
@ -578,7 +459,7 @@ export function findAllPreviousVariablesPath(
|
||||
|
||||
const { index: insertIndex, path: bodyPath } = splitPathAtLastIndex(pathToDec)
|
||||
|
||||
const _node2 = getLastNodeFromPath(ast, bodyPath)
|
||||
const _node2 = getNodeFromPath<Program['body']>(ast, bodyPath)
|
||||
if (err(_node2)) {
|
||||
console.error(_node2)
|
||||
return {
|
||||
@ -587,30 +468,19 @@ export function findAllPreviousVariablesPath(
|
||||
insertIndex: 0,
|
||||
}
|
||||
}
|
||||
if (!isArray(_node2.node)) {
|
||||
console.error(
|
||||
`Expected node of type array, but found: ${JSON.stringify(_node2)}`
|
||||
)
|
||||
return {
|
||||
variables: [],
|
||||
bodyPath: [],
|
||||
insertIndex: 0,
|
||||
}
|
||||
}
|
||||
const { node: bodyItems } = _node2
|
||||
|
||||
const variables: PrevVariable<any>[] = []
|
||||
for (const unknownItem of bodyItems) {
|
||||
const item = unknownItem as BodyItem
|
||||
if (item.type !== 'VariableDeclaration' || item.end > startRange) continue
|
||||
bodyItems?.forEach?.((item) => {
|
||||
if (item.type !== 'VariableDeclaration' || item.end > startRange) return
|
||||
const varName = item.declarations[0].id.name
|
||||
const varValue = programMemory?.get(varName)
|
||||
if (!varValue || typeof varValue?.value !== type) continue
|
||||
if (!varValue || typeof varValue?.value !== type) return
|
||||
variables.push({
|
||||
key: varName,
|
||||
value: varValue.value,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
insertIndex,
|
||||
@ -684,7 +554,7 @@ export function isNodeSafeToReplacePath(
|
||||
}
|
||||
pathToReplaced[1][0] = index + 1
|
||||
const startPath = finPath.slice(0, -1)
|
||||
const _nodeToReplace = getLastNodeFromPath(_ast, startPath)
|
||||
const _nodeToReplace = getNodeFromPath(_ast, startPath)
|
||||
if (err(_nodeToReplace)) return _nodeToReplace
|
||||
const nodeToReplace = _nodeToReplace.node as any
|
||||
nodeToReplace[last[0]] = identifier
|
||||
@ -786,8 +656,7 @@ export function isLinesParallelAndConstrained(
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(_secondaryNode)) return _secondaryNode
|
||||
const secondaryNode = _secondaryNode.stopAtNode
|
||||
if (!secondaryNode) return new Error('no secondary node found')
|
||||
const secondaryNode = _secondaryNode.node
|
||||
const _varDec = getNodeFromPath(ast, primaryPath, 'VariableDeclaration')
|
||||
if (err(_varDec)) return _varDec
|
||||
const varDec = _varDec.node
|
||||
@ -813,7 +682,7 @@ export function isLinesParallelAndConstrained(
|
||||
Math.abs(primaryAngle - secondaryAngle) < EPSILON ||
|
||||
Math.abs(primaryAngle - secondaryAngleAlt) < EPSILON
|
||||
|
||||
// is secondary line fully constrain, or has constrain type of 'angle'
|
||||
// is secordary line fully constrain, or has constrain type of 'angle'
|
||||
const secondaryFirstArg = getFirstArg(secondaryNode)
|
||||
if (err(secondaryFirstArg)) return secondaryFirstArg
|
||||
|
||||
@ -878,8 +747,8 @@ export function doesPipeHaveCallExp({
|
||||
console.error(pipeExpressionMeta)
|
||||
return false
|
||||
}
|
||||
const pipeExpression = pipeExpressionMeta.stopAtNode
|
||||
if (!pipeExpression) return false
|
||||
const pipeExpression = pipeExpressionMeta.node
|
||||
if (pipeExpression.type !== 'PipeExpression') return false
|
||||
return pipeExpression.body.some(
|
||||
(expression) =>
|
||||
expression.type === 'CallExpression' &&
|
||||
@ -906,8 +775,8 @@ export function hasExtrudeSketchGroup({
|
||||
console.error(varDecMeta)
|
||||
return false
|
||||
}
|
||||
const varDec = varDecMeta.stopAtNode
|
||||
if (!varDec) return false
|
||||
const varDec = varDecMeta.node
|
||||
if (varDec.type !== 'VariableDeclaration') return false
|
||||
const varName = varDec.declarations[0].id.name
|
||||
const varValue = programMemory?.get(varName)
|
||||
return varValue?.type === 'ExtrudeGroup' || varValue?.type === 'SketchGroup'
|
||||
@ -922,6 +791,7 @@ export function isSingleCursorInPipe(
|
||||
const pathToNode = getNodePathFromSourceRange(ast, selection.range)
|
||||
const nodeTypes = pathToNode.map(([, type]) => type)
|
||||
if (nodeTypes.includes('FunctionExpression')) return false
|
||||
if (!nodeTypes.includes('VariableDeclaration')) return false
|
||||
if (nodeTypes.includes('PipeExpression')) return true
|
||||
return false
|
||||
}
|
||||
@ -945,8 +815,8 @@ export function findUsesOfTagInPipe(
|
||||
console.error(nodeMeta)
|
||||
return []
|
||||
}
|
||||
const node = nodeMeta.stopAtNode
|
||||
if (!node) return []
|
||||
const node = nodeMeta.node
|
||||
if (node.type !== 'CallExpression') return []
|
||||
const tagIndex = node.callee.name === 'close' ? 1 : 2
|
||||
const thirdParam = node.arguments[tagIndex]
|
||||
if (
|
||||
@ -967,14 +837,9 @@ export function findUsesOfTagInPipe(
|
||||
console.error(varDec)
|
||||
return []
|
||||
}
|
||||
const varDecNode = varDec.stopAtNode
|
||||
if (!varDecNode) {
|
||||
console.error('varDecNode not found')
|
||||
return []
|
||||
}
|
||||
const dependentRanges: SourceRange[] = []
|
||||
|
||||
traverse(varDecNode, {
|
||||
traverse(varDec.node, {
|
||||
enter: (node) => {
|
||||
if (
|
||||
node.type !== 'CallExpression' ||
|
||||
@ -996,16 +861,16 @@ export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) {
|
||||
const path = getNodePathFromSourceRange(ast, selection.range)
|
||||
const _node = getNodeFromPath<PipeExpression>(ast, path, 'PipeExpression')
|
||||
if (err(_node)) return false
|
||||
const { stopAtNode: pipeExpression } = _node
|
||||
if (!pipeExpression) return false
|
||||
const { node: pipeExpression } = _node
|
||||
if (pipeExpression.type !== 'PipeExpression') return false
|
||||
const _varDec = getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
path,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(_varDec)) return false
|
||||
const varDec = _varDec.stopAtNode
|
||||
if (!varDec) return false
|
||||
const varDec = _varDec.node
|
||||
if (varDec.type !== 'VariableDeclarator') return false
|
||||
let extruded = false
|
||||
traverse(ast as any, {
|
||||
enter(node) {
|
||||
|
388
src/lang/std/__snapshots__/artifactGraph.test.ts.snap
Normal file
@ -0,0 +1,388 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`testing createArtifactGraph > code with an extrusion, fillet and sketch of face: > snapshot of the artifactGraph 1`] = `
|
||||
Map {
|
||||
"UUID-0" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
43,
|
||||
70,
|
||||
],
|
||||
},
|
||||
"pathIds": [
|
||||
"UUID",
|
||||
],
|
||||
"type": "plane",
|
||||
},
|
||||
"UUID-1" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
43,
|
||||
70,
|
||||
],
|
||||
},
|
||||
"extrusionId": "UUID",
|
||||
"planeId": "UUID",
|
||||
"segIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"solid2dId": "UUID",
|
||||
"type": "path",
|
||||
},
|
||||
"UUID-2" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
76,
|
||||
92,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-3" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
98,
|
||||
125,
|
||||
],
|
||||
},
|
||||
"edgeCutId": "UUID",
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-4" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
131,
|
||||
156,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-5" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
162,
|
||||
209,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-6" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
215,
|
||||
223,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-7" => {
|
||||
"pathId": "UUID",
|
||||
"type": "solid2D",
|
||||
},
|
||||
"UUID-8" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
243,
|
||||
266,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"type": "extrusion",
|
||||
},
|
||||
"UUID-9" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-10" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [
|
||||
"UUID",
|
||||
],
|
||||
"segId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-11" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-12" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-13" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"subType": "start",
|
||||
"type": "cap",
|
||||
},
|
||||
"UUID-14" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"subType": "end",
|
||||
"type": "cap",
|
||||
},
|
||||
"UUID-15" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
272,
|
||||
311,
|
||||
],
|
||||
},
|
||||
"consumedEdgeId": "UUID",
|
||||
"edgeIds": [],
|
||||
"subType": "fillet",
|
||||
"type": "edgeCut",
|
||||
},
|
||||
"UUID-16" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
368,
|
||||
395,
|
||||
],
|
||||
},
|
||||
"extrusionId": "UUID",
|
||||
"planeId": "UUID",
|
||||
"segIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"solid2dId": "UUID",
|
||||
"type": "path",
|
||||
},
|
||||
"UUID-17" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
401,
|
||||
416,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-18" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
422,
|
||||
438,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-19" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
444,
|
||||
491,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-20" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
497,
|
||||
505,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-21" => {
|
||||
"pathId": "UUID",
|
||||
"type": "solid2D",
|
||||
},
|
||||
"UUID-22" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
525,
|
||||
546,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"type": "extrusion",
|
||||
},
|
||||
"UUID-23" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-24" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-25" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-26" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"subType": "start",
|
||||
"type": "cap",
|
||||
},
|
||||
"UUID-27" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"subType": "end",
|
||||
"type": "cap",
|
||||
},
|
||||
}
|
||||
`;
|
48
src/lang/std/artifactGraph-README.md
Normal file
@ -0,0 +1,48 @@
|
||||
## Artifact Graph
|
||||
|
||||
#### What it does
|
||||
|
||||
The artifact graph's primary role is to map geometry artifacts in the 3d-scene/engine, to the code/AST such that when the engine sends the FE an id of some piece of geometry (say because the user clicked on something) then we know both what it is, and how it relates to the user's code.
|
||||
|
||||
Relating it to a user's code is important because this is how we drive our AST-mods, say a user clicks a segment and wants to constrain it horizontally, because of the artifact graph we know that their selection was in fact a specific `line(...)` callExpression, and now we're able to transform this to `xLine(...)` in order to constrain it.
|
||||
|
||||
#### How to reason about the graph
|
||||
|
||||
Here is what roughly what the artifact graph looks like
|
||||
|
||||

|
||||
|
||||
The best way to read this is starting with the plane at the bottom and going upwards, as this is roughly the command order (which the graph is based on).
|
||||
Here's an explanation:
|
||||
|
||||
- plane is created (kcl:`startSketchOn`, command: `enable_sketch_mode`)
|
||||
- path is created, needs to refer to the plane that the sketch is on (kcl:`startProfileAt`, command: `start_path`)
|
||||
- each segment that is created (kcl: `line`, command: `extend_path`) must refer back to the path.
|
||||
- Once we're read to extrude (kcl: `extrude`, command: `extrude`) it much refer to the path.
|
||||
- The extrude created a bunch of faces, edges etc, each of these relates back to the extrude command and the segment call expression, but there's no direct bit of kcl to refer to.
|
||||
|
||||
The above is probably enough to give more examples of how the graph is used.
|
||||
|
||||
- When a user hovers over a segment, the engine sends us the id of the segment, we can look it up directly in the graph, and we store pointers to the code in the graph, This allows use to highlight the `line(...)` call expression in the code.
|
||||
- Same as above but the user hovers over a extrude wall-face, the engine sends us this id, we look it up in the graph, but there's no pointer to the code in this node. We can then traverse to both the segment and the extrude nodes to get source ranges for `line(...)` and `extrude(...)` and highlight them both.
|
||||
|
||||
Other things to point out is that a new path can be created directly on a wall-face, i.e. this is sketch on face, and more than one path can point to the same plane, that is multiple profiles on the same plane.
|
||||
|
||||
#### Generated Graphs
|
||||
|
||||
The image above is hand drawn for grokablitiy, but it's useful to look at a real graph, take this bit of geometry
|
||||
|
||||

|
||||
|
||||
In `src/lang/std/artifactGraph.test.ts` we generate the graph for it
|
||||
|
||||

|
||||
|
||||
It's definitely harder to read, if you start at roughly the bottom center of the page and find the node `plane-0` and visually traverse from there you can see it has the same structure, plane is connected to a path, which is connected to multiple segments and an extrusion etc.
|
||||
|
||||
Generating the graph here serves a couple of purposes
|
||||
|
||||
1. Allows us to sanity check the graph, in development or as a debug tool.
|
||||
2. Is a form of test and regression check. The code that creates the node and edges would error if we tried to create an edge to a node that didn't exist, this gives us some confidence that the graph is correct. Also because we want want to be able to traverse the graph in both directions, checking each edge has an arrowhead going both directions is a good check. Lastly this images are generated and committed as part of CI, if something changes in the graph, we'll notice.
|
||||
|
||||
We'll need to add more sample code to `src/lang/std/artifactGraph.test.ts` to generate more graphs, to test more kcl API as the app continues development.
|
746
src/lang/std/artifactGraph.test.ts
Normal file
@ -0,0 +1,746 @@
|
||||
import { makeDefaultPlanes, parse, initPromise, Program } from 'lang/wasm'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import {
|
||||
OrderedCommand,
|
||||
ResponseMap,
|
||||
createArtifactGraph,
|
||||
filterArtifacts,
|
||||
expandPlane,
|
||||
expandPath,
|
||||
expandExtrusion,
|
||||
ArtifactGraph,
|
||||
expandSegment,
|
||||
getArtifactsToUpdate,
|
||||
} from './artifactGraph'
|
||||
import { err } from 'lib/trap'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { CI, VITE_KC_DEV_TOKEN } from 'env'
|
||||
import fsp from 'fs/promises'
|
||||
import fs from 'fs'
|
||||
import { chromium } from 'playwright'
|
||||
import * as d3 from 'd3-force'
|
||||
import path from 'path'
|
||||
import pixelmatch from 'pixelmatch'
|
||||
import { PNG } from 'pngjs'
|
||||
|
||||
/*
|
||||
Note this is an integration test, these tests connect to our real dev server and make websocket commands.
|
||||
It's needed for testing the artifactGraph, as it is tied to the websocket commands.
|
||||
*/
|
||||
|
||||
const pathStart = 'src/lang/std/artifactMapCache'
|
||||
const fullPath = `${pathStart}/artifactMapCache.json`
|
||||
|
||||
const exampleCode1 = `const sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-5, -5], %)
|
||||
|> line([0, 10], %)
|
||||
|> line([10.55, 0], %, $seg01)
|
||||
|> line([0, -10], %, $seg02)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(-10, sketch001)
|
||||
|> fillet({ radius: 5, tags: [seg01] }, %)
|
||||
const sketch002 = startSketchOn(extrude001, seg02)
|
||||
|> startProfileAt([-2, -6], %)
|
||||
|> line([2, 3], %)
|
||||
|> line([2, -3], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude002 = extrude(5, sketch002)
|
||||
`
|
||||
|
||||
const sketchOnFaceOnFaceEtc = `const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([4, 8], %)
|
||||
|> line([5, -8], %, $seg01)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(6, sketch001)
|
||||
const sketch002 = startSketchOn(extrude001, seg01)
|
||||
|> startProfileAt([-0.5, 0.5], %)
|
||||
|> line([2, 5], %)
|
||||
|> line([2, -5], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude002 = extrude(5, sketch002)
|
||||
const sketch003 = startSketchOn(extrude002, 'END')
|
||||
|> startProfileAt([1, 1.5], %)
|
||||
|> line([0.5, 2], %, $seg02)
|
||||
|> line([1, -2], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude003 = extrude(4, sketch003)
|
||||
const sketch004 = startSketchOn(extrude003, seg02)
|
||||
|> startProfileAt([-3, 14], %)
|
||||
|> line([0.5, 1], %)
|
||||
|> line([0.5, -2], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude004 = extrude(3, sketch004)
|
||||
`
|
||||
|
||||
// add more code snippets here and use `getCommands` to get the orderedCommands and responseMap for more tests
|
||||
const codeToWriteCacheFor = {
|
||||
exampleCode1,
|
||||
sketchOnFaceOnFaceEtc,
|
||||
} as const
|
||||
|
||||
type CodeKey = keyof typeof codeToWriteCacheFor
|
||||
|
||||
type CacheShape = {
|
||||
[key in CodeKey]: {
|
||||
orderedCommands: OrderedCommand[]
|
||||
responseMap: ResponseMap
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await initPromise
|
||||
|
||||
let parsed
|
||||
try {
|
||||
const file = await fsp.readFile(fullPath, 'utf-8')
|
||||
parsed = JSON.parse(file)
|
||||
} catch (e) {
|
||||
parsed = false
|
||||
}
|
||||
|
||||
if (!CI && parsed) {
|
||||
// caching the results of the websocket commands makes testing this locally much faster
|
||||
// real calls to the engine are needed to test the artifact map
|
||||
// bust the cache with: `rm -rf src/lang/std/artifactGraphCache`
|
||||
return
|
||||
}
|
||||
|
||||
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
|
||||
engineCommandManager.start({
|
||||
disableWebRTC: true,
|
||||
token: VITE_KC_DEV_TOKEN,
|
||||
// there does seem to be a minimum resolution, not sure what it is but 256 works ok.
|
||||
width: 256,
|
||||
height: 256,
|
||||
executeCode: () => {},
|
||||
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
||||
setMediaStream: () => {},
|
||||
setIsStreamReady: () => {},
|
||||
modifyGrid: async () => {},
|
||||
})
|
||||
await engineCommandManager.waitForReady
|
||||
|
||||
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
|
||||
CodeKey,
|
||||
string
|
||||
][]
|
||||
const cacheToWriteToFileTemp: Partial<CacheShape> = {}
|
||||
for (const [codeKey, code] of cacheEntries) {
|
||||
const ast = parse(code)
|
||||
if (err(ast)) {
|
||||
console.error(ast)
|
||||
throw ast
|
||||
}
|
||||
await kclManager.executeAst(ast)
|
||||
|
||||
cacheToWriteToFileTemp[codeKey] = {
|
||||
orderedCommands: engineCommandManager.orderedCommands,
|
||||
responseMap: engineCommandManager.responseMap,
|
||||
}
|
||||
}
|
||||
const cache = JSON.stringify(cacheToWriteToFileTemp)
|
||||
|
||||
await fsp.mkdir(pathStart, { recursive: true })
|
||||
await fsp.writeFile(fullPath, cache)
|
||||
}, 20_000)
|
||||
|
||||
afterAll(() => {
|
||||
engineCommandManager.tearDown()
|
||||
})
|
||||
|
||||
describe('testing createArtifactGraph', () => {
|
||||
describe('code with an extrusion, fillet and sketch of face:', () => {
|
||||
let ast: Program
|
||||
let theMap: ReturnType<typeof createArtifactGraph>
|
||||
it('setup', () => {
|
||||
// putting this logic in here because describe blocks runs before beforeAll has finished
|
||||
const {
|
||||
orderedCommands,
|
||||
responseMap,
|
||||
ast: _ast,
|
||||
} = getCommands('exampleCode1')
|
||||
ast = _ast
|
||||
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
|
||||
})
|
||||
|
||||
it('there should be two planes for the extrusion and the sketch on face', () => {
|
||||
const planes = [...filterArtifacts({ types: ['plane'] }, theMap)].map(
|
||||
(plane) => expandPlane(plane[1], theMap)
|
||||
)
|
||||
expect(planes).toHaveLength(1)
|
||||
planes.forEach((path) => {
|
||||
expect(path.type).toBe('plane')
|
||||
})
|
||||
})
|
||||
it('there should be two paths for the extrusion and the sketch on face', () => {
|
||||
const paths = [...filterArtifacts({ types: ['path'] }, theMap)].map(
|
||||
(path) => expandPath(path[1], theMap)
|
||||
)
|
||||
expect(paths).toHaveLength(2)
|
||||
paths.forEach((path) => {
|
||||
if (err(path)) throw path
|
||||
expect(path.type).toBe('path')
|
||||
})
|
||||
})
|
||||
|
||||
it('there should be two extrusions, for the original and the sketchOnFace, the first extrusion should have 6 sides of the cube', () => {
|
||||
const extrusions = [
|
||||
...filterArtifacts({ types: ['extrusion'] }, theMap),
|
||||
].map((extrusion) => expandExtrusion(extrusion[1], theMap))
|
||||
expect(extrusions).toHaveLength(2)
|
||||
extrusions.forEach((extrusion, index) => {
|
||||
if (err(extrusion)) throw extrusion
|
||||
expect(extrusion.type).toBe('extrusion')
|
||||
const firstExtrusionIsACubeIE6Sides = 6
|
||||
const secondExtrusionIsATriangularPrismIE5Sides = 5
|
||||
expect(extrusion.surfaces.length).toBe(
|
||||
!index
|
||||
? firstExtrusionIsACubeIE6Sides
|
||||
: secondExtrusionIsATriangularPrismIE5Sides
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('there should be 5 + 4 segments, 4 (+close) from the first extrusion and 3 (+close) from the second', () => {
|
||||
const segments = [...filterArtifacts({ types: ['segment'] }, theMap)].map(
|
||||
(segment) => expandSegment(segment[1], theMap)
|
||||
)
|
||||
expect(segments).toHaveLength(9)
|
||||
})
|
||||
|
||||
it('snapshot of the artifactGraph', () => {
|
||||
const stableMap = new Map(
|
||||
[...theMap].map(([, artifact], index): [string, any] => {
|
||||
const stableValue: any = {}
|
||||
Object.entries(artifact).forEach(([propName, value]) => {
|
||||
if (
|
||||
propName === 'type' ||
|
||||
propName === 'codeRef' ||
|
||||
propName === 'subType'
|
||||
) {
|
||||
stableValue[propName] = value
|
||||
return
|
||||
}
|
||||
if (Array.isArray(value))
|
||||
stableValue[propName] = value.map(() => 'UUID')
|
||||
if (typeof value === 'string' && value)
|
||||
stableValue[propName] = 'UUID'
|
||||
})
|
||||
return [`UUID-${index}`, stableValue]
|
||||
})
|
||||
)
|
||||
expect(stableMap).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('screenshot graph', async () => {
|
||||
// Ostensibly this takes a screen shot of the graph of the artifactGraph
|
||||
// but it's it also tests that all of the id links are correct because if one
|
||||
// of the edges refers to a non-existent node, the graph will throw.
|
||||
// further more we can check that each edge is bi-directional, if it's not
|
||||
// by checking the arrow heads going both ways, on the graph.
|
||||
await GraphTheGraph(theMap, 1400, 1400, 'exampleCode1.png')
|
||||
}, 20000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('capture graph of sketchOnFaceOnFace...', () => {
|
||||
describe('code with an extrusion, fillet and sketch of face:', () => {
|
||||
let ast: Program
|
||||
let theMap: ReturnType<typeof createArtifactGraph>
|
||||
it('setup', async () => {
|
||||
// putting this logic in here because describe blocks runs before beforeAll has finished
|
||||
const {
|
||||
orderedCommands,
|
||||
responseMap,
|
||||
ast: _ast,
|
||||
} = getCommands('sketchOnFaceOnFaceEtc')
|
||||
ast = _ast
|
||||
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
|
||||
|
||||
// Ostensibly this takes a screen shot of the graph of the artifactGraph
|
||||
// but it's it also tests that all of the id links are correct because if one
|
||||
// of the edges refers to a non-existent node, the graph will throw.
|
||||
// further more we can check that each edge is bi-directional, if it's not
|
||||
// by checking the arrow heads going both ways, on the graph.
|
||||
await GraphTheGraph(theMap, 2500, 2500, 'sketchOnFaceOnFaceEtc.png')
|
||||
}, 20000)
|
||||
})
|
||||
})
|
||||
|
||||
function getCommands(codeKey: CodeKey): CacheShape[CodeKey] & { ast: Program } {
|
||||
const ast = parse(codeKey)
|
||||
if (err(ast)) {
|
||||
console.error(ast)
|
||||
throw ast
|
||||
}
|
||||
const file = fs.readFileSync(fullPath, 'utf-8')
|
||||
const parsed: CacheShape = JSON.parse(file)
|
||||
// these either already exist from the last run, or were created in
|
||||
const orderedCommands = parsed[codeKey].orderedCommands
|
||||
const responseMap = parsed[codeKey].responseMap
|
||||
return {
|
||||
orderedCommands,
|
||||
responseMap,
|
||||
ast,
|
||||
}
|
||||
}
|
||||
|
||||
async function GraphTheGraph(
|
||||
theMap: ArtifactGraph,
|
||||
sizeX: number,
|
||||
sizeY: number,
|
||||
imageName: string
|
||||
) {
|
||||
const nodes: Array<{ id: string; label: string }> = []
|
||||
const edges: Array<{ source: string; target: string; label: string }> = []
|
||||
let index = 0
|
||||
for (const [commandId, artifact] of theMap) {
|
||||
nodes.push({
|
||||
id: commandId,
|
||||
label: `${artifact.type}-${index++}`,
|
||||
})
|
||||
Object.entries(artifact).forEach(([propName, value]) => {
|
||||
if (
|
||||
propName === 'type' ||
|
||||
propName === 'codeRef' ||
|
||||
propName === 'subType'
|
||||
)
|
||||
return
|
||||
if (Array.isArray(value))
|
||||
value.forEach((v) => {
|
||||
v && edges.push({ source: commandId, target: v, label: propName })
|
||||
})
|
||||
if (typeof value === 'string' && value)
|
||||
edges.push({ source: commandId, target: value, label: propName })
|
||||
})
|
||||
}
|
||||
|
||||
// Create a force simulation to calculate node positions
|
||||
const simulation = d3
|
||||
.forceSimulation(nodes as any)
|
||||
.force(
|
||||
'link',
|
||||
d3
|
||||
.forceLink(edges)
|
||||
.id((d: any) => d.id)
|
||||
.distance(100)
|
||||
)
|
||||
.force('charge', d3.forceManyBody().strength(-300))
|
||||
.force('center', d3.forceCenter(300, 200))
|
||||
.stop()
|
||||
|
||||
// Run the simulation
|
||||
for (let i = 0; i < 300; ++i) simulation.tick()
|
||||
|
||||
// Create traces for Plotly
|
||||
const nodeTrace = {
|
||||
x: nodes.map((node: any) => node.x),
|
||||
y: nodes.map((node: any) => node.y),
|
||||
text: nodes.map((node) => node.label), // Use the custom label
|
||||
mode: 'markers+text',
|
||||
type: 'scatter',
|
||||
marker: { size: 20, color: 'gray' }, // Nodes in gray
|
||||
textfont: { size: 14, color: 'black' }, // Labels in black
|
||||
textposition: 'top center', // Position text on top
|
||||
}
|
||||
|
||||
const edgeTrace = {
|
||||
x: [],
|
||||
y: [],
|
||||
mode: 'lines',
|
||||
type: 'scatter',
|
||||
line: { width: 2, color: 'lightgray' }, // Edges in light gray
|
||||
}
|
||||
|
||||
const annotations: any[] = []
|
||||
|
||||
edges.forEach((edge) => {
|
||||
const sourceNode = nodes.find(
|
||||
(node: any) => node.id === (edge as any).source.id
|
||||
)
|
||||
const targetNode = nodes.find(
|
||||
(node: any) => node.id === (edge as any).target.id
|
||||
)
|
||||
|
||||
// Check if nodes are found
|
||||
if (!sourceNode || !targetNode) {
|
||||
throw new Error(
|
||||
// @ts-ignore
|
||||
`Node not found: ${!sourceNode ? edge.source.id : edge.target.id}`
|
||||
)
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
edgeTrace.x.push(sourceNode.x, targetNode.x, null)
|
||||
// @ts-ignore
|
||||
edgeTrace.y.push(sourceNode.y, targetNode.y, null)
|
||||
|
||||
// Calculate offset for arrowhead
|
||||
const offsetFactor = 0.9 // Adjust this factor to control the offset distance
|
||||
// @ts-ignore
|
||||
const offsetX = (targetNode.x - sourceNode.x) * offsetFactor
|
||||
// @ts-ignore
|
||||
const offsetY = (targetNode.y - sourceNode.y) * offsetFactor
|
||||
|
||||
// Add arrowhead annotation with offset
|
||||
annotations.push({
|
||||
// @ts-ignore
|
||||
ax: sourceNode.x,
|
||||
// @ts-ignore
|
||||
ay: sourceNode.y,
|
||||
// @ts-ignore
|
||||
x: targetNode.x - offsetX,
|
||||
// @ts-ignore
|
||||
y: targetNode.y - offsetY,
|
||||
xref: 'x',
|
||||
yref: 'y',
|
||||
axref: 'x',
|
||||
ayref: 'y',
|
||||
showarrow: true,
|
||||
arrowhead: 2,
|
||||
arrowsize: 1,
|
||||
arrowwidth: 2,
|
||||
arrowcolor: 'darkgray', // Arrowheads in dark gray
|
||||
})
|
||||
|
||||
// Add edge label annotation closer to the edge tail (25% of the length)
|
||||
// @ts-ignore
|
||||
const labelX = sourceNode.x * 0.75 + targetNode.x * 0.25
|
||||
// @ts-ignore
|
||||
const labelY = sourceNode.y * 0.75 + targetNode.y * 0.25
|
||||
annotations.push({
|
||||
x: labelX,
|
||||
y: labelY,
|
||||
xref: 'x',
|
||||
yref: 'y',
|
||||
text: edge.label,
|
||||
showarrow: false,
|
||||
font: { size: 12, color: 'black' }, // Edge labels in black
|
||||
align: 'center',
|
||||
})
|
||||
})
|
||||
|
||||
const data = [edgeTrace, nodeTrace]
|
||||
|
||||
const layout = {
|
||||
// title: 'Force-Directed Graph with Nodes and Edges',
|
||||
xaxis: { showgrid: false, zeroline: false, showticklabels: false },
|
||||
yaxis: { showgrid: false, zeroline: false, showticklabels: false },
|
||||
showlegend: false,
|
||||
annotations: annotations,
|
||||
}
|
||||
|
||||
// Export to PNG using Playwright
|
||||
const browser = await chromium.launch()
|
||||
const page = await browser.newPage()
|
||||
await page.setContent(`
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="plotly-graph" style="width:${sizeX}px;height:${sizeY}px;"></div>
|
||||
<script>
|
||||
Plotly.newPlot('plotly-graph', ${JSON.stringify(
|
||||
data
|
||||
)}, ${JSON.stringify(layout)})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
await page.waitForSelector('#plotly-graph')
|
||||
const element = await page.$('#plotly-graph')
|
||||
|
||||
// wait an extra bit for things to settle
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
// @ts-ignore
|
||||
await element.screenshot({
|
||||
path: `./e2e/playwright/temp3.png`,
|
||||
})
|
||||
|
||||
await browser.close()
|
||||
|
||||
const img1Path = path.resolve(`./src/lang/std/artifactMapGraphs/${imageName}`)
|
||||
const img2Path = path.resolve('./e2e/playwright/temp3.png')
|
||||
|
||||
const img1 = PNG.sync.read(fs.readFileSync(img1Path))
|
||||
const img2 = PNG.sync.read(fs.readFileSync(img2Path))
|
||||
|
||||
const { width, height } = img1
|
||||
const diff = new PNG({ width, height })
|
||||
|
||||
const numDiffPixels = pixelmatch(
|
||||
img1.data,
|
||||
img2.data,
|
||||
diff.data,
|
||||
width,
|
||||
height,
|
||||
{ threshold: 0.1 }
|
||||
)
|
||||
|
||||
if (numDiffPixels > 10) {
|
||||
console.warn('numDiffPixels', numDiffPixels)
|
||||
// write file out to final place
|
||||
fs.writeFileSync(
|
||||
`src/lang/std/artifactMapGraphs/${imageName}`,
|
||||
PNG.sync.write(img2)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
describe('testing getArtifactsToUpdate', () => {
|
||||
it('should return an array of artifacts to update', () => {
|
||||
const { orderedCommands, responseMap, ast } = getCommands('exampleCode1')
|
||||
const map = createArtifactGraph({ orderedCommands, responseMap, ast })
|
||||
const getArtifact = (id: string) => map.get(id)
|
||||
const currentPlaneId = 'UUID-1'
|
||||
const getUpdateObjects = (type: Models['ModelingCmd_type']['type']) => {
|
||||
const artifactsToUpdate = getArtifactsToUpdate({
|
||||
orderedCommand: orderedCommands.find(
|
||||
(a) =>
|
||||
a.command.type === 'modeling_cmd_req' && a.command.cmd.type === type
|
||||
)!,
|
||||
responseMap,
|
||||
getArtifact,
|
||||
currentPlaneId,
|
||||
ast,
|
||||
})
|
||||
return artifactsToUpdate.map(({ artifact }) => artifact)
|
||||
}
|
||||
expect(getUpdateObjects('start_path')).toEqual([
|
||||
{
|
||||
type: 'path',
|
||||
segIds: [],
|
||||
planeId: 'UUID-1',
|
||||
extrusionId: '',
|
||||
codeRef: {
|
||||
pathToNode: [['body', '']],
|
||||
range: [43, 70],
|
||||
},
|
||||
},
|
||||
])
|
||||
expect(getUpdateObjects('extrude')).toEqual([
|
||||
{
|
||||
type: 'extrusion',
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: [],
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [243, 266],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'path',
|
||||
segIds: expect.any(Array),
|
||||
planeId: expect.any(String),
|
||||
extrusionId: expect.any(String),
|
||||
codeRef: {
|
||||
range: [43, 70],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
solid2dId: expect.any(String),
|
||||
},
|
||||
])
|
||||
expect(getUpdateObjects('extend_path')).toEqual([
|
||||
{
|
||||
type: 'segment',
|
||||
pathId: expect.any(String),
|
||||
surfaceId: '',
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [76, 92],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'path',
|
||||
segIds: expect.any(Array),
|
||||
planeId: expect.any(String),
|
||||
extrusionId: expect.any(String),
|
||||
codeRef: {
|
||||
range: [43, 70],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
solid2dId: expect.any(String),
|
||||
},
|
||||
])
|
||||
expect(getUpdateObjects('solid3d_fillet_edge')).toEqual([
|
||||
{
|
||||
type: 'edgeCut',
|
||||
subType: 'fillet',
|
||||
consumedEdgeId: expect.any(String),
|
||||
edgeIds: [],
|
||||
surfaceId: '',
|
||||
codeRef: {
|
||||
range: [272, 311],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'segment',
|
||||
pathId: expect.any(String),
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [98, 125],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
edgeCutId: expect.any(String),
|
||||
},
|
||||
])
|
||||
expect(getUpdateObjects('solid3d_get_extrusion_face_info')).toEqual([
|
||||
{
|
||||
type: 'wall',
|
||||
segId: expect.any(String),
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'segment',
|
||||
pathId: expect.any(String),
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [162, 209],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'extrusion',
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [243, 266],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'wall',
|
||||
segId: expect.any(String),
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'segment',
|
||||
pathId: expect.any(String),
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [131, 156],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'extrusion',
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [243, 266],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'wall',
|
||||
segId: expect.any(String),
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'segment',
|
||||
pathId: expect.any(String),
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [98, 125],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
edgeCutId: expect.any(String),
|
||||
},
|
||||
{
|
||||
type: 'extrusion',
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [243, 266],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'wall',
|
||||
segId: expect.any(String),
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'segment',
|
||||
pathId: expect.any(String),
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [76, 92],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'extrusion',
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [243, 266],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'cap',
|
||||
subType: 'start',
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'extrusion',
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [243, 266],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'cap',
|
||||
subType: 'end',
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'extrusion',
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [243, 266],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
683
src/lang/std/artifactGraph.ts
Normal file
@ -0,0 +1,683 @@
|
||||
import { PathToNode, Program, SourceRange } from 'lang/wasm'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { err } from 'lib/trap'
|
||||
|
||||
interface CommonCommandProperties {
|
||||
range: SourceRange
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
|
||||
export interface PlaneArtifact {
|
||||
type: 'plane'
|
||||
pathIds: Array<string>
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
export interface PlaneArtifactRich {
|
||||
type: 'plane'
|
||||
paths: Array<PathArtifact>
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
|
||||
export interface PathArtifact {
|
||||
type: 'path'
|
||||
planeId: string
|
||||
segIds: Array<string>
|
||||
extrusionId: string
|
||||
solid2dId?: string
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
|
||||
interface solid2D {
|
||||
type: 'solid2D'
|
||||
pathId: string
|
||||
}
|
||||
export interface PathArtifactRich {
|
||||
type: 'path'
|
||||
plane: PlaneArtifact | WallArtifact
|
||||
segments: Array<SegmentArtifact>
|
||||
extrusion: ExtrusionArtifact
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
|
||||
interface SegmentArtifact {
|
||||
type: 'segment'
|
||||
pathId: string
|
||||
surfaceId: string
|
||||
edgeIds: Array<string>
|
||||
edgeCutId?: string
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
interface SegmentArtifactRich {
|
||||
type: 'segment'
|
||||
path: PathArtifact
|
||||
surf: WallArtifact
|
||||
edges: Array<ExtrudeEdge>
|
||||
edgeCut?: EdgeCut
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
|
||||
interface ExtrusionArtifact {
|
||||
type: 'extrusion'
|
||||
pathId: string
|
||||
surfaceIds: Array<string>
|
||||
edgeIds: Array<string>
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
interface ExtrusionArtifactRich {
|
||||
type: 'extrusion'
|
||||
path: PathArtifact
|
||||
surfaces: Array<WallArtifact | CapArtifact>
|
||||
edges: Array<ExtrudeEdge>
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
|
||||
interface WallArtifact {
|
||||
type: 'wall'
|
||||
segId: string
|
||||
edgeCutEdgeIds: Array<string>
|
||||
extrusionId: string
|
||||
pathIds: Array<string>
|
||||
}
|
||||
interface CapArtifact {
|
||||
type: 'cap'
|
||||
subType: 'start' | 'end'
|
||||
edgeCutEdgeIds: Array<string>
|
||||
extrusionId: string
|
||||
pathIds: Array<string>
|
||||
}
|
||||
|
||||
interface ExtrudeEdge {
|
||||
type: 'extrudeEdge'
|
||||
segId: string
|
||||
extrusionId: string
|
||||
edgeId: string
|
||||
}
|
||||
|
||||
/** A edgeCut is a more generic term for both fillet or chamfer */
|
||||
interface EdgeCut {
|
||||
type: 'edgeCut'
|
||||
subType: 'fillet' | 'chamfer'
|
||||
consumedEdgeId: string
|
||||
edgeIds: Array<string>
|
||||
surfaceId: string
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
|
||||
interface EdgeCutEdge {
|
||||
type: 'edgeCutEdge'
|
||||
edgeCutId: string
|
||||
surfaceId: string
|
||||
}
|
||||
|
||||
export type Artifact =
|
||||
| PlaneArtifact
|
||||
| PathArtifact
|
||||
| SegmentArtifact
|
||||
| ExtrusionArtifact
|
||||
| WallArtifact
|
||||
| CapArtifact
|
||||
| ExtrudeEdge
|
||||
| EdgeCut
|
||||
| EdgeCutEdge
|
||||
| solid2D
|
||||
|
||||
export type ArtifactGraph = Map<string, Artifact>
|
||||
|
||||
export type EngineCommand = Models['WebSocketRequest_type']
|
||||
|
||||
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
|
||||
|
||||
export interface ResponseMap {
|
||||
[commandId: string]: OkWebSocketResponseData
|
||||
}
|
||||
export interface OrderedCommand {
|
||||
command: EngineCommand
|
||||
range: SourceRange
|
||||
}
|
||||
|
||||
/** Creates a graph of artifacts from a list of ordered commands and their responses
|
||||
* muting the Map should happen entirely this function, other functions called within
|
||||
* should return data on how to update the map, and not do so directly.
|
||||
*/
|
||||
export function createArtifactGraph({
|
||||
orderedCommands,
|
||||
responseMap,
|
||||
ast,
|
||||
}: {
|
||||
orderedCommands: Array<OrderedCommand>
|
||||
responseMap: ResponseMap
|
||||
ast: Program
|
||||
}) {
|
||||
const myMap = new Map<string, Artifact>()
|
||||
|
||||
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
|
||||
let currentPlaneId = ''
|
||||
|
||||
orderedCommands.forEach((orderedCommand) => {
|
||||
if (orderedCommand.command?.type === 'modeling_cmd_req') {
|
||||
if (orderedCommand.command.cmd.type === 'enable_sketch_mode') {
|
||||
currentPlaneId = orderedCommand.command.cmd.entity_id
|
||||
}
|
||||
if (orderedCommand.command.cmd.type === 'sketch_mode_disable') {
|
||||
currentPlaneId = ''
|
||||
}
|
||||
}
|
||||
const artifactsToUpdate = getArtifactsToUpdate({
|
||||
orderedCommand,
|
||||
responseMap,
|
||||
getArtifact: (id: string) => myMap.get(id),
|
||||
currentPlaneId,
|
||||
ast,
|
||||
})
|
||||
artifactsToUpdate.forEach(({ id, artifact }) => {
|
||||
const mergedArtifact = mergeArtifacts(myMap.get(id), artifact)
|
||||
myMap.set(id, mergedArtifact)
|
||||
})
|
||||
})
|
||||
return myMap
|
||||
}
|
||||
|
||||
/** Merges two artifacts, since our artifacts only contain strings and arrays of string for values we coerce that
|
||||
* but maybe types can be improved here.
|
||||
*/
|
||||
function mergeArtifacts(
|
||||
oldArtifact: Artifact | undefined,
|
||||
newArtifact: Artifact
|
||||
): Artifact {
|
||||
// only has string and array of strings
|
||||
interface GenericArtifact {
|
||||
[key: string]: string | Array<string>
|
||||
}
|
||||
if (!oldArtifact) return newArtifact
|
||||
// merging artifacts of different types should never happen, but if it does, just return the new artifact
|
||||
if (oldArtifact.type !== newArtifact.type) return newArtifact
|
||||
const _oldArtifact = oldArtifact as any as GenericArtifact
|
||||
const mergedArtifact = { ...oldArtifact, ...newArtifact } as GenericArtifact
|
||||
Object.entries(newArtifact as any as GenericArtifact).forEach(
|
||||
([propName, value]) => {
|
||||
const otherValue = _oldArtifact[propName]
|
||||
if (Array.isArray(value) && Array.isArray(otherValue)) {
|
||||
mergedArtifact[propName] = [...new Set([...otherValue, ...value])]
|
||||
}
|
||||
}
|
||||
)
|
||||
return mergedArtifact as any as Artifact
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a single command and it's response in order to populate the artifact map
|
||||
* It does not mutate the map directly, but returns an array of artifacts to update
|
||||
*
|
||||
* @param currentPlaneId is only needed for `start_path` commands because this command does not have a pathId
|
||||
* instead it relies on the id used with the `enable_sketch_mode` command, so this much be kept track of
|
||||
* outside of this function. It would be good to update the `start_path` command to include the planeId so we
|
||||
* can remove this.
|
||||
*/
|
||||
export function getArtifactsToUpdate({
|
||||
orderedCommand: { command, range },
|
||||
getArtifact,
|
||||
responseMap,
|
||||
currentPlaneId,
|
||||
ast,
|
||||
}: {
|
||||
orderedCommand: OrderedCommand
|
||||
responseMap: ResponseMap
|
||||
/** Passing in a getter because we don't wan this function to update the map directly */
|
||||
getArtifact: (id: string) => Artifact | undefined
|
||||
currentPlaneId: string
|
||||
ast: Program
|
||||
}): Array<{
|
||||
id: string
|
||||
artifact: Artifact
|
||||
}> {
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
|
||||
// expect all to be `modeling_cmd_req` as batch commands have
|
||||
// already been expanded before being added to orderedCommands
|
||||
if (command.type !== 'modeling_cmd_req') return []
|
||||
const id = command.cmd_id
|
||||
const response = responseMap[id]
|
||||
const cmd = command.cmd
|
||||
const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
|
||||
if (!response) return returnArr
|
||||
if (cmd.type === 'enable_sketch_mode') {
|
||||
const plane = getArtifact(currentPlaneId)
|
||||
const pathIds = plane?.type === 'plane' ? plane?.pathIds : []
|
||||
const codeRef =
|
||||
plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode }
|
||||
const existingPlane = getArtifact(currentPlaneId)
|
||||
if (existingPlane?.type === 'wall') {
|
||||
return [
|
||||
{
|
||||
id: currentPlaneId,
|
||||
artifact: {
|
||||
type: 'wall',
|
||||
segId: existingPlane.segId,
|
||||
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
|
||||
extrusionId: existingPlane.extrusionId,
|
||||
pathIds: existingPlane.pathIds,
|
||||
},
|
||||
},
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
{ id: currentPlaneId, artifact: { type: 'plane', pathIds, codeRef } },
|
||||
]
|
||||
}
|
||||
} else if (cmd.type === 'start_path') {
|
||||
returnArr.push({
|
||||
id,
|
||||
artifact: {
|
||||
type: 'path',
|
||||
segIds: [],
|
||||
planeId: currentPlaneId,
|
||||
extrusionId: '',
|
||||
codeRef: { range, pathToNode },
|
||||
},
|
||||
})
|
||||
const plane = getArtifact(currentPlaneId)
|
||||
const codeRef =
|
||||
plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode }
|
||||
if (plane?.type === 'plane') {
|
||||
returnArr.push({
|
||||
id: currentPlaneId,
|
||||
artifact: { type: 'plane', pathIds: [id], codeRef },
|
||||
})
|
||||
}
|
||||
if (plane?.type === 'wall') {
|
||||
returnArr.push({
|
||||
id: currentPlaneId,
|
||||
artifact: {
|
||||
type: 'wall',
|
||||
segId: plane.segId,
|
||||
edgeCutEdgeIds: plane.edgeCutEdgeIds,
|
||||
extrusionId: plane.extrusionId,
|
||||
pathIds: [id],
|
||||
},
|
||||
})
|
||||
}
|
||||
return returnArr
|
||||
} else if (cmd.type === 'extend_path' || cmd.type === 'close_path') {
|
||||
const pathId = cmd.type === 'extend_path' ? cmd.path : cmd.path_id
|
||||
returnArr.push({
|
||||
id,
|
||||
artifact: {
|
||||
type: 'segment',
|
||||
pathId,
|
||||
surfaceId: '',
|
||||
edgeIds: [],
|
||||
codeRef: { range, pathToNode },
|
||||
},
|
||||
})
|
||||
const path = getArtifact(pathId)
|
||||
if (path?.type === 'path')
|
||||
returnArr.push({
|
||||
id: pathId,
|
||||
artifact: { ...path, segIds: [id] },
|
||||
})
|
||||
if (
|
||||
response?.type === 'modeling' &&
|
||||
response.data.modeling_response.type === 'close_path'
|
||||
) {
|
||||
returnArr.push({
|
||||
id: response.data.modeling_response.data.face_id,
|
||||
artifact: { type: 'solid2D', pathId },
|
||||
})
|
||||
const path = getArtifact(pathId)
|
||||
if (path?.type === 'path')
|
||||
returnArr.push({
|
||||
id: pathId,
|
||||
artifact: {
|
||||
...path,
|
||||
solid2dId: response.data.modeling_response.data.face_id,
|
||||
},
|
||||
})
|
||||
}
|
||||
return returnArr
|
||||
} else if (cmd.type === 'extrude') {
|
||||
returnArr.push({
|
||||
id,
|
||||
artifact: {
|
||||
type: 'extrusion',
|
||||
pathId: cmd.target,
|
||||
surfaceIds: [],
|
||||
edgeIds: [],
|
||||
codeRef: { range, pathToNode },
|
||||
},
|
||||
})
|
||||
const path = getArtifact(cmd.target)
|
||||
if (path?.type === 'path')
|
||||
returnArr.push({
|
||||
id: cmd.target,
|
||||
artifact: { ...path, extrusionId: id },
|
||||
})
|
||||
return returnArr
|
||||
} else if (
|
||||
cmd.type === 'solid3d_get_extrusion_face_info' &&
|
||||
response?.type === 'modeling' &&
|
||||
response.data.modeling_response.type === 'solid3d_get_extrusion_face_info'
|
||||
) {
|
||||
let lastPath: PathArtifact
|
||||
response.data.modeling_response.data.faces.forEach(
|
||||
({ curve_id, cap, face_id }) => {
|
||||
if (cap === 'none' && curve_id && face_id) {
|
||||
const seg = getArtifact(curve_id)
|
||||
if (seg?.type !== 'segment') return
|
||||
const path = getArtifact(seg.pathId)
|
||||
if (path?.type === 'path' && seg?.type === 'segment') {
|
||||
lastPath = path
|
||||
returnArr.push({
|
||||
id: face_id,
|
||||
artifact: {
|
||||
type: 'wall',
|
||||
segId: curve_id,
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: path.extrusionId,
|
||||
pathIds: [],
|
||||
},
|
||||
})
|
||||
returnArr.push({
|
||||
id: curve_id,
|
||||
artifact: { ...seg, surfaceId: face_id },
|
||||
})
|
||||
const extrusion = getArtifact(path.extrusionId)
|
||||
if (extrusion?.type === 'extrusion') {
|
||||
returnArr.push({
|
||||
id: path.extrusionId,
|
||||
artifact: {
|
||||
...extrusion,
|
||||
surfaceIds: [face_id],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
response.data.modeling_response.data.faces.forEach(({ cap, face_id }) => {
|
||||
if ((cap === 'top' || cap === 'bottom') && face_id) {
|
||||
const path = lastPath
|
||||
if (path?.type === 'path') {
|
||||
returnArr.push({
|
||||
id: face_id,
|
||||
artifact: {
|
||||
type: 'cap',
|
||||
subType: cap === 'bottom' ? 'start' : 'end',
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: path.extrusionId,
|
||||
pathIds: [],
|
||||
},
|
||||
})
|
||||
const extrusion = getArtifact(path.extrusionId)
|
||||
if (extrusion?.type !== 'extrusion') return
|
||||
returnArr.push({
|
||||
id: path.extrusionId,
|
||||
artifact: {
|
||||
...extrusion,
|
||||
surfaceIds: [face_id],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
return returnArr
|
||||
} else if (cmd.type === 'solid3d_fillet_edge') {
|
||||
returnArr.push({
|
||||
id,
|
||||
artifact: {
|
||||
type: 'edgeCut',
|
||||
subType: cmd.cut_type,
|
||||
consumedEdgeId: cmd.edge_id,
|
||||
edgeIds: [],
|
||||
surfaceId: '',
|
||||
codeRef: { range, pathToNode },
|
||||
},
|
||||
})
|
||||
const consumedEdge = getArtifact(cmd.edge_id)
|
||||
if (consumedEdge?.type === 'segment') {
|
||||
returnArr.push({
|
||||
id: cmd.edge_id,
|
||||
artifact: { ...consumedEdge, edgeCutId: id },
|
||||
})
|
||||
}
|
||||
return returnArr
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/** filter map items of a specific type */
|
||||
export function filterArtifacts<T extends Artifact['type'][]>(
|
||||
{
|
||||
types,
|
||||
predicate,
|
||||
}: {
|
||||
types: T
|
||||
predicate?: (value: Extract<Artifact, { type: T[number] }>) => boolean
|
||||
},
|
||||
map: ArtifactGraph
|
||||
) {
|
||||
return new Map(
|
||||
Array.from(map).filter(
|
||||
([_, value]) =>
|
||||
types.includes(value.type) &&
|
||||
(!predicate ||
|
||||
predicate(value as Extract<Artifact, { type: T[number] }>))
|
||||
)
|
||||
) as Map<string, Extract<Artifact, { type: T[number] }>>
|
||||
}
|
||||
|
||||
export function getArtifactsOfTypes<T extends Artifact['type'][]>(
|
||||
{
|
||||
keys,
|
||||
types,
|
||||
predicate,
|
||||
}: {
|
||||
keys: string[]
|
||||
types: T
|
||||
predicate?: (value: Extract<Artifact, { type: T[number] }>) => boolean
|
||||
},
|
||||
map: ArtifactGraph
|
||||
): Map<string, Extract<Artifact, { type: T[number] }>> {
|
||||
return new Map(
|
||||
[...map].filter(
|
||||
([key, value]) =>
|
||||
keys.includes(key) &&
|
||||
types.includes(value.type) &&
|
||||
(!predicate ||
|
||||
predicate(value as Extract<Artifact, { type: T[number] }>))
|
||||
)
|
||||
) as Map<string, Extract<Artifact, { type: T[number] }>>
|
||||
}
|
||||
|
||||
export function getArtifactOfTypes<T extends Artifact['type'][]>(
|
||||
{
|
||||
key,
|
||||
types,
|
||||
}: {
|
||||
key: string
|
||||
types: T
|
||||
},
|
||||
map: ArtifactGraph
|
||||
): Extract<Artifact, { type: T[number] }> | Error {
|
||||
const artifact = map.get(key)
|
||||
if (!artifact) return new Error(`No artifact found with key ${key}`)
|
||||
if (!types.includes(artifact?.type))
|
||||
return new Error(`Expected ${types} but got ${artifact?.type}`)
|
||||
return artifact as Extract<Artifact, { type: T[number] }>
|
||||
}
|
||||
|
||||
export function expandPlane(
|
||||
plane: PlaneArtifact,
|
||||
artifactGraph: ArtifactGraph
|
||||
): PlaneArtifactRich {
|
||||
const paths = getArtifactsOfTypes(
|
||||
{ keys: plane.pathIds, types: ['path'] },
|
||||
artifactGraph
|
||||
)
|
||||
return {
|
||||
type: 'plane',
|
||||
paths: Array.from(paths.values()),
|
||||
codeRef: plane.codeRef,
|
||||
}
|
||||
}
|
||||
|
||||
export function expandPath(
|
||||
path: PathArtifact,
|
||||
artifactGraph: ArtifactGraph
|
||||
): PathArtifactRich | Error {
|
||||
const segs = getArtifactsOfTypes(
|
||||
{ keys: path.segIds, types: ['segment'] },
|
||||
artifactGraph
|
||||
)
|
||||
const extrusion = getArtifactOfTypes(
|
||||
{
|
||||
key: path.extrusionId,
|
||||
types: ['extrusion'],
|
||||
},
|
||||
artifactGraph
|
||||
)
|
||||
const plane = getArtifactOfTypes(
|
||||
{ key: path.planeId, types: ['plane', 'wall'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(extrusion)) return extrusion
|
||||
if (err(plane)) return plane
|
||||
return {
|
||||
type: 'path',
|
||||
segments: Array.from(segs.values()),
|
||||
extrusion,
|
||||
plane,
|
||||
codeRef: path.codeRef,
|
||||
}
|
||||
}
|
||||
|
||||
export function expandExtrusion(
|
||||
extrusion: ExtrusionArtifact,
|
||||
artifactGraph: ArtifactGraph
|
||||
): ExtrusionArtifactRich | Error {
|
||||
const surfs = getArtifactsOfTypes(
|
||||
{ keys: extrusion.surfaceIds, types: ['wall', 'cap'] },
|
||||
artifactGraph
|
||||
)
|
||||
const edges = getArtifactsOfTypes(
|
||||
{ keys: extrusion.edgeIds, types: ['extrudeEdge'] },
|
||||
artifactGraph
|
||||
)
|
||||
const path = getArtifactOfTypes(
|
||||
{ key: extrusion.pathId, types: ['path'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(path)) return path
|
||||
return {
|
||||
type: 'extrusion',
|
||||
surfaces: Array.from(surfs.values()),
|
||||
edges: Array.from(edges.values()),
|
||||
path,
|
||||
codeRef: extrusion.codeRef,
|
||||
}
|
||||
}
|
||||
|
||||
export function expandSegment(
|
||||
segment: SegmentArtifact,
|
||||
artifactGraph: ArtifactGraph
|
||||
): SegmentArtifactRich | Error {
|
||||
const path = getArtifactOfTypes(
|
||||
{ key: segment.pathId, types: ['path'] },
|
||||
artifactGraph
|
||||
)
|
||||
const surf = getArtifactOfTypes(
|
||||
{ key: segment.surfaceId, types: ['wall'] },
|
||||
artifactGraph
|
||||
)
|
||||
const edges = getArtifactsOfTypes(
|
||||
{ keys: segment.edgeIds, types: ['extrudeEdge'] },
|
||||
artifactGraph
|
||||
)
|
||||
const edgeCut = segment.edgeCutId
|
||||
? getArtifactOfTypes(
|
||||
{ key: segment.edgeCutId, types: ['edgeCut'] },
|
||||
artifactGraph
|
||||
)
|
||||
: undefined
|
||||
if (err(path)) return path
|
||||
if (err(surf)) return surf
|
||||
if (err(edgeCut)) return edgeCut
|
||||
|
||||
return {
|
||||
type: 'segment',
|
||||
path,
|
||||
surf,
|
||||
edges: Array.from(edges.values()),
|
||||
edgeCut: edgeCut,
|
||||
codeRef: segment.codeRef,
|
||||
}
|
||||
}
|
||||
|
||||
export function getCapCodeRef(
|
||||
cap: CapArtifact,
|
||||
artifactGraph: ArtifactGraph
|
||||
): CommonCommandProperties | Error {
|
||||
const extrusion = getArtifactOfTypes(
|
||||
{ key: cap.extrusionId, types: ['extrusion'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(extrusion)) return extrusion
|
||||
const path = getArtifactOfTypes(
|
||||
{ key: extrusion.pathId, types: ['path'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(path)) return path
|
||||
return path.codeRef
|
||||
}
|
||||
|
||||
export function getSolid2dCodeRef(
|
||||
solid2D: solid2D,
|
||||
artifactGraph: ArtifactGraph
|
||||
): CommonCommandProperties | Error {
|
||||
const path = getArtifactOfTypes(
|
||||
{ key: solid2D.pathId, types: ['path'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(path)) return path
|
||||
return path.codeRef
|
||||
}
|
||||
|
||||
export function getWallCodeRef(
|
||||
wall: WallArtifact,
|
||||
artifactGraph: ArtifactGraph
|
||||
): CommonCommandProperties | Error {
|
||||
const seg = getArtifactOfTypes(
|
||||
{ key: wall.segId, types: ['segment'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(seg)) return seg
|
||||
return seg.codeRef
|
||||
}
|
||||
|
||||
export function getExtrusionFromSuspectedExtrudeSurface(
|
||||
id: string,
|
||||
artifactGraph: ArtifactGraph
|
||||
): ExtrusionArtifact | Error {
|
||||
const artifact = getArtifactOfTypes(
|
||||
{ key: id, types: ['wall', 'cap'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(artifact)) return artifact
|
||||
return getArtifactOfTypes(
|
||||
{ key: artifact.extrusionId, types: ['extrusion'] },
|
||||
artifactGraph
|
||||
)
|
||||
}
|
||||
|
||||
export function getExtrusionFromSuspectedPath(
|
||||
id: string,
|
||||
artifactGraph: ArtifactGraph
|
||||
): ExtrusionArtifact | Error {
|
||||
const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph)
|
||||
if (err(path)) return path
|
||||
return getArtifactOfTypes(
|
||||
{ key: path.extrusionId, types: ['extrusion'] },
|
||||
artifactGraph
|
||||
)
|
||||
}
|
@ -1,272 +0,0 @@
|
||||
import { PathToNode, Program, SourceRange } from 'lang/wasm'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
|
||||
interface CommonCommandProperties {
|
||||
range: SourceRange
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
|
||||
interface ExtrudeArtifact extends CommonCommandProperties {
|
||||
type: 'extrude'
|
||||
pathId: string
|
||||
}
|
||||
|
||||
export interface StartPathArtifact extends CommonCommandProperties {
|
||||
type: 'startPath'
|
||||
extrusionIds: string[]
|
||||
}
|
||||
|
||||
export interface SegmentArtifact extends CommonCommandProperties {
|
||||
type: 'segment'
|
||||
subType: 'segment' | 'closeSegment'
|
||||
pathId: string
|
||||
}
|
||||
|
||||
interface ExtrudeCapArtifact extends CommonCommandProperties {
|
||||
type: 'extrudeCap'
|
||||
cap: 'start' | 'end'
|
||||
pathId: string
|
||||
}
|
||||
interface ExtrudeWallArtifact extends CommonCommandProperties {
|
||||
type: 'extrudeWall'
|
||||
pathId: string
|
||||
}
|
||||
|
||||
interface PatternInstance extends CommonCommandProperties {
|
||||
type: 'patternInstance'
|
||||
}
|
||||
|
||||
export type ArtifactMapCommand =
|
||||
| ExtrudeArtifact
|
||||
| StartPathArtifact
|
||||
| ExtrudeCapArtifact
|
||||
| ExtrudeWallArtifact
|
||||
| SegmentArtifact
|
||||
| PatternInstance
|
||||
|
||||
export type EngineCommand = Models['WebSocketRequest_type']
|
||||
|
||||
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
|
||||
|
||||
/**
|
||||
* The ArtifactMap is a client-side representation of the artifacts that
|
||||
* have been sent to the server-side engine. It is used to keep track of
|
||||
* the state of each command, and to resolve the promise that was returned.
|
||||
* It is also used to keep track of what entities are in the engine scene,
|
||||
* so that we can associate IDs returned from the engine with the
|
||||
* lines of KCL code that generated them.
|
||||
*/
|
||||
export interface ArtifactMap {
|
||||
[commandId: string]: ArtifactMapCommand
|
||||
}
|
||||
|
||||
export interface ResponseMap {
|
||||
[commandId: string]: OkWebSocketResponseData
|
||||
}
|
||||
export interface OrderedCommand {
|
||||
command: EngineCommand
|
||||
range: SourceRange
|
||||
}
|
||||
|
||||
export function createArtifactMap({
|
||||
orderedCommands,
|
||||
responseMap,
|
||||
ast,
|
||||
}: {
|
||||
orderedCommands: Array<OrderedCommand>
|
||||
responseMap: ResponseMap
|
||||
ast: Program
|
||||
}): ArtifactMap {
|
||||
const artifactMap: ArtifactMap = {}
|
||||
orderedCommands.forEach(({ command, range }) => {
|
||||
// expect all to be `modeling_cmd_req` as batch commands have
|
||||
// already been expanded before being added to orderedCommands
|
||||
if (command.type !== 'modeling_cmd_req') return
|
||||
const id = command.cmd_id
|
||||
const response = responseMap[id]
|
||||
const artifacts = handleIndividualResponse({
|
||||
id,
|
||||
pendingMsg: {
|
||||
command,
|
||||
range,
|
||||
},
|
||||
response,
|
||||
ast,
|
||||
prevArtifactMap: artifactMap,
|
||||
})
|
||||
artifacts.forEach(({ commandId, artifact }) => {
|
||||
artifactMap[commandId] = artifact
|
||||
})
|
||||
})
|
||||
return artifactMap
|
||||
}
|
||||
|
||||
function handleIndividualResponse({
|
||||
id,
|
||||
pendingMsg,
|
||||
response,
|
||||
ast,
|
||||
prevArtifactMap,
|
||||
}: {
|
||||
id: string
|
||||
pendingMsg: {
|
||||
command: EngineCommand
|
||||
range: SourceRange
|
||||
}
|
||||
response: OkWebSocketResponseData
|
||||
ast: Program
|
||||
prevArtifactMap: ArtifactMap
|
||||
}): Array<{
|
||||
commandId: string
|
||||
artifact: ArtifactMapCommand
|
||||
}> {
|
||||
const command = pendingMsg
|
||||
if (command?.command?.type !== 'modeling_cmd_req') return []
|
||||
if (response?.type !== 'modeling') return []
|
||||
const command2 = command.command.cmd
|
||||
|
||||
const range = command.range
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
const modelingResponse = response.data.modeling_response
|
||||
|
||||
const artifacts: Array<{
|
||||
commandId: string
|
||||
artifact: ArtifactMapCommand
|
||||
}> = []
|
||||
|
||||
if (command) {
|
||||
if (
|
||||
command2.type !== 'extrude' &&
|
||||
command2.type !== 'extend_path' &&
|
||||
command2.type !== 'solid3d_get_extrusion_face_info' &&
|
||||
command2.type !== 'start_path' &&
|
||||
command2.type !== 'close_path'
|
||||
) {
|
||||
}
|
||||
if (command2.type === 'extrude') {
|
||||
artifacts.push({
|
||||
commandId: id,
|
||||
artifact: {
|
||||
type: 'extrude',
|
||||
range,
|
||||
pathToNode,
|
||||
pathId: command2.target,
|
||||
},
|
||||
})
|
||||
|
||||
const targetArtifact = { ...prevArtifactMap[command2.target] }
|
||||
if (targetArtifact?.type === 'startPath') {
|
||||
artifacts.push({
|
||||
commandId: command2.target,
|
||||
artifact: {
|
||||
...targetArtifact,
|
||||
type: 'startPath',
|
||||
range: targetArtifact.range,
|
||||
pathToNode: targetArtifact.pathToNode,
|
||||
extrusionIds: targetArtifact?.extrusionIds
|
||||
? [...targetArtifact?.extrusionIds, id]
|
||||
: [id],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
if (command2.type === 'extend_path') {
|
||||
artifacts.push({
|
||||
commandId: id,
|
||||
artifact: {
|
||||
type: 'segment',
|
||||
subType: 'segment',
|
||||
range,
|
||||
pathToNode,
|
||||
pathId: command2.path,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (command2.type === 'close_path')
|
||||
artifacts.push({
|
||||
commandId: id,
|
||||
artifact: {
|
||||
type: 'segment',
|
||||
subType: 'closeSegment',
|
||||
range,
|
||||
pathToNode,
|
||||
pathId: command2.path_id,
|
||||
},
|
||||
})
|
||||
if (command2.type === 'start_path') {
|
||||
artifacts.push({
|
||||
commandId: id,
|
||||
artifact: {
|
||||
type: 'startPath',
|
||||
range,
|
||||
pathToNode,
|
||||
extrusionIds: [],
|
||||
},
|
||||
})
|
||||
}
|
||||
if (
|
||||
(command2.type === 'entity_linear_pattern' &&
|
||||
modelingResponse.type === 'entity_linear_pattern') ||
|
||||
(command2.type === 'entity_circular_pattern' &&
|
||||
modelingResponse.type === 'entity_circular_pattern')
|
||||
) {
|
||||
// TODO this is not working perfectly, maybe it's like a selection filter issue
|
||||
// but when clicking on a instance it does put the cursor somewhat relevant but
|
||||
// edges and what not do not highlight the correct segment.
|
||||
const entities = modelingResponse.data.entity_ids
|
||||
entities?.forEach((entity: string) => {
|
||||
artifacts.push({
|
||||
commandId: entity,
|
||||
artifact: {
|
||||
range: range,
|
||||
pathToNode,
|
||||
type: 'patternInstance',
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
if (
|
||||
command2.type === 'solid3d_get_extrusion_face_info' &&
|
||||
modelingResponse.type === 'solid3d_get_extrusion_face_info'
|
||||
) {
|
||||
const edgeArtifact = prevArtifactMap[command2.edge_id]
|
||||
const parent =
|
||||
edgeArtifact?.type === 'segment'
|
||||
? prevArtifactMap[edgeArtifact.pathId]
|
||||
: null
|
||||
modelingResponse.data.faces.forEach((face) => {
|
||||
if (
|
||||
face.cap !== 'none' &&
|
||||
face.face_id &&
|
||||
parent?.type === 'startPath'
|
||||
) {
|
||||
artifacts.push({
|
||||
commandId: face.face_id,
|
||||
artifact: {
|
||||
type: 'extrudeCap',
|
||||
cap: face.cap === 'bottom' ? 'start' : 'end',
|
||||
range: parent.range,
|
||||
pathToNode: parent.pathToNode,
|
||||
pathId:
|
||||
edgeArtifact?.type === 'segment' ? edgeArtifact.pathId : '',
|
||||
},
|
||||
})
|
||||
}
|
||||
const curveArtifact = prevArtifactMap[face?.curve_id || '']
|
||||
if (curveArtifact?.type === 'segment' && face?.face_id) {
|
||||
artifacts.push({
|
||||
commandId: face.face_id,
|
||||
artifact: {
|
||||
type: 'extrudeWall',
|
||||
range: curveArtifact.range,
|
||||
pathToNode: curveArtifact.pathToNode,
|
||||
pathId: curveArtifact.pathId,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return artifacts
|
||||
}
|
BIN
src/lang/std/artifactMapGraphs/demoGeometry.png
Normal file
After Width: | Height: | Size: 222 KiB |
BIN
src/lang/std/artifactMapGraphs/exampleCode1.png
Normal file
After Width: | Height: | Size: 182 KiB |
BIN
src/lang/std/artifactMapGraphs/grokable-graph.png
Normal file
After Width: | Height: | Size: 110 KiB |
BIN
src/lang/std/artifactMapGraphs/sketchOnFaceOnFaceEtc.png
Normal file
After Width: | Height: | Size: 374 KiB |
@ -2,16 +2,19 @@ import { Program, SourceRange } from 'lang/wasm'
|
||||
import { VITE_KC_API_WS_MODELING_URL } from 'env'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { exportSave } from 'lib/exportSave'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { deferExecution, isOverlap, uuidv4 } from 'lib/utils'
|
||||
import { Themes, getThemeColorForEngine, getOppositeTheme } from 'lib/theme'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
import {
|
||||
ArtifactMap,
|
||||
ArtifactGraph,
|
||||
EngineCommand,
|
||||
OrderedCommand,
|
||||
ResponseMap,
|
||||
createArtifactMap,
|
||||
} from 'lang/std/artifactMap'
|
||||
createArtifactGraph,
|
||||
} from 'lang/std/artifactGraph'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { exportMake } from 'lib/exportMake'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
// TODO(paultag): This ought to be tweakable.
|
||||
const pingIntervalMs = 10000
|
||||
@ -29,11 +32,10 @@ interface NewTrackArgs {
|
||||
mediaStream: MediaStream
|
||||
}
|
||||
|
||||
/** This looks funny, I know. This is needed because node and the browser
|
||||
* disagree as to the type. In a browser it's a number, but in node it's a
|
||||
* "Timeout".
|
||||
*/
|
||||
type IsomorphicTimeout = ReturnType<typeof setTimeout>
|
||||
export enum ExportIntent {
|
||||
Save = 'save',
|
||||
Make = 'make',
|
||||
}
|
||||
|
||||
type ClientMetrics = Models['ClientMetrics_type']
|
||||
|
||||
@ -285,8 +287,6 @@ class EngineConnection extends EventTarget {
|
||||
)
|
||||
}
|
||||
|
||||
private failedConnTimeout: IsomorphicTimeout | null
|
||||
|
||||
readonly url: string
|
||||
private readonly token?: string
|
||||
|
||||
@ -311,7 +311,6 @@ class EngineConnection extends EventTarget {
|
||||
this.engineCommandManager = engineCommandManager
|
||||
this.url = url
|
||||
this.token = token
|
||||
this.failedConnTimeout = null
|
||||
|
||||
this.pingPongSpan = { ping: undefined, pong: undefined }
|
||||
|
||||
@ -450,9 +449,11 @@ class EngineConnection extends EventTarget {
|
||||
}
|
||||
|
||||
const createPeerConnection = () => {
|
||||
this.pc = new RTCPeerConnection({
|
||||
bundlePolicy: 'max-bundle',
|
||||
})
|
||||
if (!this.engineCommandManager.disableWebRTC) {
|
||||
this.pc = new RTCPeerConnection({
|
||||
bundlePolicy: 'max-bundle',
|
||||
})
|
||||
}
|
||||
|
||||
// Other parts of the application expect pc to be initialized when firing.
|
||||
this.dispatchEvent(
|
||||
@ -464,7 +465,7 @@ class EngineConnection extends EventTarget {
|
||||
// Data channels MUST BE specified before SDP offers because requesting
|
||||
// them affects what our needs are!
|
||||
const DATACHANNEL_NAME_UMC = 'unreliable_modeling_cmds'
|
||||
this.pc.createDataChannel(DATACHANNEL_NAME_UMC)
|
||||
this.pc?.createDataChannel?.(DATACHANNEL_NAME_UMC)
|
||||
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Connecting,
|
||||
@ -497,7 +498,7 @@ class EngineConnection extends EventTarget {
|
||||
},
|
||||
})
|
||||
}
|
||||
this.pc.addEventListener('icecandidate', this.onIceCandidate)
|
||||
this.pc?.addEventListener?.('icecandidate', this.onIceCandidate)
|
||||
|
||||
this.onIceCandidateError = (_event: Event) => {
|
||||
const event = _event as RTCPeerConnectionIceErrorEvent
|
||||
@ -505,7 +506,7 @@ class EngineConnection extends EventTarget {
|
||||
`ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}`
|
||||
)
|
||||
}
|
||||
this.pc.addEventListener('icecandidateerror', this.onIceCandidateError)
|
||||
this.pc?.addEventListener?.('icecandidateerror', this.onIceCandidateError)
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event
|
||||
// Event type: generic Event type...
|
||||
@ -539,7 +540,7 @@ class EngineConnection extends EventTarget {
|
||||
break
|
||||
}
|
||||
}
|
||||
this.pc.addEventListener(
|
||||
this.pc?.addEventListener?.(
|
||||
'connectionstatechange',
|
||||
this.onConnectionStateChange
|
||||
)
|
||||
@ -629,7 +630,7 @@ class EngineConnection extends EventTarget {
|
||||
|
||||
this.mediaStream = mediaStream
|
||||
}
|
||||
this.pc.addEventListener('track', this.onTrack)
|
||||
this.pc?.addEventListener?.('track', this.onTrack)
|
||||
|
||||
this.onDataChannel = (event) => {
|
||||
this.unreliableDataChannel = event.channel
|
||||
@ -720,7 +721,7 @@ class EngineConnection extends EventTarget {
|
||||
this.onDataChannelMessage
|
||||
)
|
||||
}
|
||||
this.pc.addEventListener('datachannel', this.onDataChannel)
|
||||
this.pc?.addEventListener?.('datachannel', this.onDataChannel)
|
||||
}
|
||||
|
||||
const createWebSocketConnection = () => {
|
||||
@ -755,6 +756,11 @@ class EngineConnection extends EventTarget {
|
||||
// Send an initial ping
|
||||
this.send({ type: 'ping' })
|
||||
this.pingPongSpan.ping = new Date()
|
||||
if (this.engineCommandManager.disableWebRTC) {
|
||||
this.engineCommandManager
|
||||
.initPlanes()
|
||||
.then(() => this.engineCommandManager.resolveReady())
|
||||
}
|
||||
}
|
||||
this.websocket.addEventListener('open', this.onWebSocketOpen)
|
||||
|
||||
@ -802,11 +808,20 @@ class EngineConnection extends EventTarget {
|
||||
.join('\n')
|
||||
if (message.request_id) {
|
||||
const artifactThatFailed =
|
||||
this.engineCommandManager.artifactMap[message.request_id]
|
||||
this.engineCommandManager.artifactGraph.get(message.request_id)
|
||||
console.error(
|
||||
`Error in response to request ${message.request_id}:\n${errorsString}
|
||||
failed cmd type was ${artifactThatFailed?.type}`
|
||||
)
|
||||
// Check if this was a pending export command.
|
||||
if (
|
||||
this.engineCommandManager.pendingExport?.commandId ===
|
||||
message.request_id
|
||||
) {
|
||||
// Reject the promise with the error.
|
||||
this.engineCommandManager.pendingExport.reject(errorsString)
|
||||
this.engineCommandManager.pendingExport = undefined
|
||||
}
|
||||
} else {
|
||||
console.error(`Error from server:\n${errorsString}`)
|
||||
}
|
||||
@ -1088,8 +1103,10 @@ export enum EngineCommandManagerEvents {
|
||||
* of those commands. It also sets up and tears down the connection to the Engine
|
||||
* through the {@link EngineConnection} class.
|
||||
*
|
||||
* It also maintains an {@link artifactMap} that keeps track of the state of each
|
||||
* command, and the artifacts that have been generated by those commands.
|
||||
* As commands are send their state is tracked in {@link pendingCommands} and clear as soon as we receive a response.
|
||||
*
|
||||
* Also all commands that are sent are kept track of in {@link orderedCommands} and their responses are kept in {@link responseMap}
|
||||
* Both of these data structures are used to process the {@link artifactGraph}.
|
||||
*/
|
||||
|
||||
interface PendingMessage {
|
||||
@ -1102,17 +1119,10 @@ interface PendingMessage {
|
||||
}
|
||||
export class EngineCommandManager extends EventTarget {
|
||||
/**
|
||||
* The artifactMap is a client-side representation of the commands that have been sent
|
||||
* to the server-side geometry engine, and the state of their resulting artifacts.
|
||||
*
|
||||
* It is used to keep track of the state of each command, which can fail, succeed, or be
|
||||
* pending.
|
||||
*
|
||||
* It is also used to keep track of our client's understanding of what is in the engine scene
|
||||
* so that we can map to and from KCL code. Each artifact maintains a source range to the part
|
||||
* of the KCL code that generated it.
|
||||
* The artifactGraph is a client-side representation of the commands that have been sent
|
||||
* see: src/lang/std/artifactGraph-README.md for a full explanation.
|
||||
*/
|
||||
artifactMap: ArtifactMap = {}
|
||||
artifactGraph: ArtifactGraph = new Map()
|
||||
/**
|
||||
* The pendingCommands object is a map of the commands that have been sent to the engine that are still waiting on a reply
|
||||
*/
|
||||
@ -1121,21 +1131,14 @@ export class EngineCommandManager extends EventTarget {
|
||||
} = {}
|
||||
/**
|
||||
* The orderedCommands array of all the the commands sent to the engine, un-folded from batches, and made into one long
|
||||
* list of the individual commands, this is used to process all the commands into the artifactMap
|
||||
* list of the individual commands, this is used to process all the commands into the artifactGraph
|
||||
*/
|
||||
orderedCommands: Array<OrderedCommand> = []
|
||||
/**
|
||||
* A map of the responses to the @this.orderedCommands, when processing the commands into the artifactMap, this response map allow
|
||||
* A map of the responses to the {@link orderedCommands}, when processing the commands into the artifactGraph, this response map allow
|
||||
* us to look up the response by command id
|
||||
*/
|
||||
responseMap: ResponseMap = {}
|
||||
/**
|
||||
* The client-side representation of the scene command artifacts that have been sent to the server;
|
||||
* that is, the *non-modeling* commands and corresponding artifacts.
|
||||
*
|
||||
* For modeling commands, see {@link artifactMap}.
|
||||
*/
|
||||
sceneCommandArtifacts: ArtifactMap = {}
|
||||
/**
|
||||
* A counter that is incremented with each command sent over the *unreliable* channel to the engine.
|
||||
* This is compared to the latest received {@link inSequence} number to determine if we should ignore
|
||||
@ -1155,9 +1158,16 @@ export class EngineCommandManager extends EventTarget {
|
||||
pendingExport?: {
|
||||
resolve: (a: null) => void
|
||||
reject: (reason: any) => void
|
||||
commandId: string
|
||||
}
|
||||
/**
|
||||
* Export intent traxcks the intent of the export. If it is null there is no
|
||||
* export in progress. Otherwise it is an enum value of the intent.
|
||||
* Another export cannot be started if one is already in progress.
|
||||
*/
|
||||
private _exportIntent: ExportIntent | null = null
|
||||
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
||||
private resolveReady = () => {}
|
||||
resolveReady = () => {}
|
||||
/** Folks should realize that wait for ready does not get called _everytime_
|
||||
* the connection resets and restarts, it only gets called the first time.
|
||||
*
|
||||
@ -1204,9 +1214,20 @@ export class EngineCommandManager extends EventTarget {
|
||||
private onEngineConnectionNewTrack = ({
|
||||
detail,
|
||||
}: CustomEvent<NewTrackArgs>) => {}
|
||||
disableWebRTC = false
|
||||
modelingSend: ReturnType<typeof useModelingContext>['send'] =
|
||||
(() => {}) as any
|
||||
|
||||
set exportIntent(intent: ExportIntent | null) {
|
||||
this._exportIntent = intent
|
||||
}
|
||||
|
||||
get exportIntent() {
|
||||
return this._exportIntent
|
||||
}
|
||||
|
||||
start({
|
||||
restart,
|
||||
disableWebRTC = false,
|
||||
setMediaStream,
|
||||
setIsStreamReady,
|
||||
width,
|
||||
@ -1222,7 +1243,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
showScaleGrid: false,
|
||||
},
|
||||
}: {
|
||||
restart?: boolean
|
||||
disableWebRTC?: boolean
|
||||
setMediaStream: (stream: MediaStream) => void
|
||||
setIsStreamReady: (isStreamReady: boolean) => void
|
||||
width: number
|
||||
@ -1239,6 +1260,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
}
|
||||
}) {
|
||||
this.makeDefaultPlanes = makeDefaultPlanes
|
||||
this.disableWebRTC = disableWebRTC
|
||||
this.modifyGrid = modifyGrid
|
||||
if (width === 0 || height === 0) {
|
||||
return
|
||||
@ -1381,9 +1403,36 @@ export class EngineCommandManager extends EventTarget {
|
||||
// because in all other cases we send JSON strings. But in the case of
|
||||
// export we send a binary blob.
|
||||
// Pass this to our export function.
|
||||
exportSave(event.data).then(() => {
|
||||
this.pendingExport?.resolve(null)
|
||||
}, this.pendingExport?.reject)
|
||||
if (this.exportIntent === null) {
|
||||
toast.error(
|
||||
'Export intent was not set, but export data was received'
|
||||
)
|
||||
console.error(
|
||||
'Export intent was not set, but export data was received'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
switch (this.exportIntent) {
|
||||
case ExportIntent.Save: {
|
||||
exportSave(event.data).then(() => {
|
||||
this.pendingExport?.resolve(null)
|
||||
}, this.pendingExport?.reject)
|
||||
break
|
||||
}
|
||||
case ExportIntent.Make: {
|
||||
exportMake(event.data).then((result) => {
|
||||
if (result) {
|
||||
this.pendingExport?.resolve(null)
|
||||
} else {
|
||||
this.pendingExport?.reject('Failed to make export')
|
||||
}
|
||||
}, this.pendingExport?.reject)
|
||||
break
|
||||
}
|
||||
}
|
||||
// Set the export intent back to null.
|
||||
this.exportIntent = null
|
||||
return
|
||||
}
|
||||
|
||||
@ -1549,7 +1598,6 @@ export class EngineCommandManager extends EventTarget {
|
||||
}
|
||||
}
|
||||
async startNewSession() {
|
||||
this.artifactMap = {}
|
||||
this.orderedCommands = []
|
||||
this.responseMap = {}
|
||||
await this.initPlanes()
|
||||
@ -1688,7 +1736,13 @@ export class EngineCommandManager extends EventTarget {
|
||||
return Promise.resolve(null)
|
||||
} else if (cmd.type === 'export') {
|
||||
const promise = new Promise<null>((resolve, reject) => {
|
||||
this.pendingExport = { resolve, reject }
|
||||
this.pendingExport = {
|
||||
resolve,
|
||||
reject: () => {
|
||||
this.exportIntent = null
|
||||
},
|
||||
commandId: command.cmd_id,
|
||||
}
|
||||
})
|
||||
this.engineConnection?.send(command)
|
||||
return promise
|
||||
@ -1718,15 +1772,11 @@ export class EngineCommandManager extends EventTarget {
|
||||
if (this.engineConnection === undefined) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
if (!this.engineConnection?.isReady() && !this.disableWebRTC)
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (id === undefined) {
|
||||
return Promise.reject(new Error('id is undefined'))
|
||||
}
|
||||
if (rangeStr === undefined) {
|
||||
if (id === undefined) return Promise.reject(new Error('id is undefined'))
|
||||
if (rangeStr === undefined)
|
||||
return Promise.reject(new Error('rangeStr is undefined'))
|
||||
}
|
||||
if (commandStr === undefined) {
|
||||
return Promise.reject(new Error('commandStr is undefined'))
|
||||
}
|
||||
@ -1784,32 +1834,35 @@ export class EngineCommandManager extends EventTarget {
|
||||
this.engineConnection?.send(message.command)
|
||||
return promise
|
||||
}
|
||||
|
||||
deferredArtifactPopulated = deferExecution((a?: null) => {
|
||||
this.modelingSend({ type: 'Artifact graph populated' })
|
||||
}, 200)
|
||||
deferredArtifactEmptied = deferExecution((a?: null) => {
|
||||
this.modelingSend({ type: 'Artifact graph emptied' })
|
||||
}, 200)
|
||||
|
||||
/**
|
||||
* When an execution takes place we want to wait until we've got replies for all of the commands
|
||||
* When this is done when we build the artifact map synchronously.
|
||||
*/
|
||||
async waitForAllCommands() {
|
||||
await Promise.all(Object.values(this.pendingCommands).map((a) => a.promise))
|
||||
this.artifactMap = createArtifactMap({
|
||||
this.artifactGraph = createArtifactGraph({
|
||||
orderedCommands: this.orderedCommands,
|
||||
responseMap: this.responseMap,
|
||||
ast: this.getAst(),
|
||||
})
|
||||
if (this.artifactGraph.size) {
|
||||
this.deferredArtifactEmptied(null)
|
||||
} else {
|
||||
this.deferredArtifactPopulated(null)
|
||||
}
|
||||
}
|
||||
private async initPlanes() {
|
||||
async initPlanes() {
|
||||
if (this.planesInitialized()) return
|
||||
const planes = await this.makeDefaultPlanes()
|
||||
this.defaultPlanes = planes
|
||||
|
||||
this.subscribeTo({
|
||||
event: 'select_with_point',
|
||||
callback: ({ data }) => {
|
||||
if (!data?.entity_id) return
|
||||
if (!planes) return
|
||||
if (![planes.xy, planes.yz, planes.xz].includes(data.entity_id)) return
|
||||
this.onPlaneSelectCallback(data.entity_id)
|
||||
},
|
||||
})
|
||||
}
|
||||
planesInitialized(): boolean {
|
||||
return (
|
||||
@ -1820,11 +1873,6 @@ export class EngineCommandManager extends EventTarget {
|
||||
)
|
||||
}
|
||||
|
||||
onPlaneSelectCallback = (id: string) => {}
|
||||
onPlaneSelected(callback: (id: string) => void) {
|
||||
this.onPlaneSelectCallback = callback
|
||||
}
|
||||
|
||||
async setPlaneHidden(id: string, hidden: boolean) {
|
||||
return await this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
@ -1851,15 +1899,10 @@ export class EngineCommandManager extends EventTarget {
|
||||
range: SourceRange,
|
||||
commandTypeToTarget: string
|
||||
): string | undefined {
|
||||
const values = Object.entries(this.artifactMap)
|
||||
for (const [id, data] of values) {
|
||||
// // Our range selection seems to just select the cursor position, so either
|
||||
// // of these can be right...
|
||||
if (
|
||||
(data.range[0] === range[0] || data.range[1] === range[1]) &&
|
||||
data.type === commandTypeToTarget
|
||||
)
|
||||
return id
|
||||
for (const [artifactId, artifact] of this.artifactGraph) {
|
||||
if ('codeRef' in artifact && isOverlap(range, artifact.codeRef.range)) {
|
||||
if (commandTypeToTarget === artifact.type) return artifactId
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
@ -14,11 +14,7 @@ import {
|
||||
SourceRange,
|
||||
CallExpression,
|
||||
} from '../wasm'
|
||||
import {
|
||||
expectNodeOnPath,
|
||||
getNodeFromPath,
|
||||
getNodePathFromSourceRange,
|
||||
} from '../queryAst'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||
import { err } from 'lib/trap'
|
||||
|
||||
@ -604,13 +600,13 @@ describe('testing getConstraintInfo', () => {
|
||||
]
|
||||
if (err(ast)) return ast
|
||||
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const callExp = expectNodeOnPath<CallExpression>(
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(callExp)) return callExp
|
||||
const result = getConstraintInfo(callExp, code, pathToNode)
|
||||
const result = getConstraintInfo(callExp.node, code, pathToNode)
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
})
|
||||
@ -758,13 +754,13 @@ describe('testing getConstraintInfo', () => {
|
||||
]
|
||||
if (err(ast)) return ast
|
||||
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const callExp = expectNodeOnPath<CallExpression>(
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(callExp)) return callExp
|
||||
const result = getConstraintInfo(callExp, code, pathToNode)
|
||||
const result = getConstraintInfo(callExp.node, code, pathToNode)
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
})
|
||||
@ -1114,14 +1110,14 @@ describe('testing getConstraintInfo', () => {
|
||||
]
|
||||
if (err(ast)) return ast
|
||||
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
|
||||
const callExp = expectNodeOnPath<CallExpression>(
|
||||
const callExp = getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(callExp)) return callExp
|
||||
|
||||
const result = getConstraintInfo(callExp, code, pathToNode)
|
||||
const result = getConstraintInfo(callExp.node, code, pathToNode)
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
@ -14,10 +14,8 @@ import {
|
||||
Identifier,
|
||||
} from 'lang/wasm'
|
||||
import {
|
||||
isNodeType,
|
||||
expectLastNodeFromPath,
|
||||
expectNodeOnPath,
|
||||
getNodeFromPath,
|
||||
getNodeFromPathCurry,
|
||||
getNodePathFromSourceRange,
|
||||
} from 'lang/queryAst'
|
||||
import {
|
||||
@ -52,7 +50,7 @@ import {
|
||||
mutateObjExpProp,
|
||||
findUniqueName,
|
||||
} from 'lang/modifyAst'
|
||||
import { roundOff, getLength, getAngle, isArray } from 'lib/utils'
|
||||
import { roundOff, getLength, getAngle } from 'lib/utils'
|
||||
import { err } from 'lib/trap'
|
||||
import { perpendicularDistance } from 'sketch-helpers'
|
||||
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
|
||||
@ -332,12 +330,13 @@ export const lineTo: SketchLineHelper = {
|
||||
referencedSegment,
|
||||
}) => {
|
||||
const _node = { ...node }
|
||||
const pipe = expectNodeOnPath<PipeExpression>(
|
||||
const nodeMeta = getNodeFromPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(pipe)) return pipe
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: pipe } = nodeMeta
|
||||
|
||||
const newVals: [Value, Value] = [
|
||||
createLiteral(roundOff(to[0], 2)),
|
||||
@ -374,7 +373,7 @@ export const lineTo: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to }) => {
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath(_node, pathToNode)
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: callExpression } = nodeMeta
|
||||
|
||||
@ -383,13 +382,8 @@ export const lineTo: SketchLineHelper = {
|
||||
createLiteral(to[1]),
|
||||
])
|
||||
|
||||
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
|
||||
const firstArg = callExpression.arguments[0]
|
||||
if (firstArg) {
|
||||
mutateArrExp(firstArg, toArrExp) ||
|
||||
mutateObjExpProp(firstArg, toArrExp, 'to')
|
||||
}
|
||||
}
|
||||
mutateArrExp(callExpression.arguments?.[0], toArrExp) ||
|
||||
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
@ -420,7 +414,7 @@ export const line: SketchLineHelper = {
|
||||
spliceBetween,
|
||||
}) => {
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<PipeExpression>(
|
||||
const nodeMeta = getNodeFromPath<PipeExpression | CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
@ -433,16 +427,12 @@ export const line: SketchLineHelper = {
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(nodeMeta2)) return nodeMeta2
|
||||
const { stopAtNode: varDec } = nodeMeta2
|
||||
const { node: varDec } = nodeMeta2
|
||||
|
||||
const newXVal = createLiteral(roundOff(to[0] - from[0], 2))
|
||||
const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
|
||||
|
||||
if (
|
||||
spliceBetween &&
|
||||
!createCallback &&
|
||||
isNodeType<PipeExpression>(pipe, 'PipeExpression')
|
||||
) {
|
||||
if (spliceBetween && !createCallback && pipe.type === 'PipeExpression') {
|
||||
const callExp = createCallExpression('line', [
|
||||
createArrayExpression([newXVal, newYVal]),
|
||||
createPipeSubstitution(),
|
||||
@ -465,11 +455,7 @@ export const line: SketchLineHelper = {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
replaceExisting &&
|
||||
createCallback &&
|
||||
isNodeType<PipeExpression>(pipe, 'PipeExpression')
|
||||
) {
|
||||
if (replaceExisting && createCallback && pipe.type !== 'CallExpression') {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[newXVal, newYVal],
|
||||
@ -491,7 +477,7 @@ export const line: SketchLineHelper = {
|
||||
createArrayExpression([newXVal, newYVal]),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
if (isNodeType<PipeExpression>(pipe, 'PipeExpression')) {
|
||||
if (pipe.type === 'PipeExpression') {
|
||||
pipe.body = [...pipe.body, callExp]
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -502,7 +488,6 @@ export const line: SketchLineHelper = {
|
||||
],
|
||||
}
|
||||
} else {
|
||||
if (!varDec) return new Error('Variable declaration not found')
|
||||
varDec.init = createPipeExpression([varDec.init, callExp])
|
||||
}
|
||||
return {
|
||||
@ -512,11 +497,7 @@ export const line: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from }) => {
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: callExpression } = nodeMeta
|
||||
|
||||
@ -525,15 +506,10 @@ export const line: SketchLineHelper = {
|
||||
createLiteral(roundOff(to[1] - from[1], 2)),
|
||||
])
|
||||
|
||||
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
|
||||
const firstArg = callExpression.arguments[0]
|
||||
if (firstArg) {
|
||||
if (firstArg.type === 'ObjectExpression') {
|
||||
mutateObjExpProp(firstArg, toArrExp, 'to')
|
||||
} else {
|
||||
mutateArrExp(firstArg, toArrExp)
|
||||
}
|
||||
}
|
||||
if (callExpression.arguments?.[0].type === 'ObjectExpression') {
|
||||
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
|
||||
} else {
|
||||
mutateArrExp(callExpression.arguments?.[0], toArrExp)
|
||||
}
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -555,12 +531,10 @@ export const line: SketchLineHelper = {
|
||||
export const xLineTo: SketchLineHelper = {
|
||||
add: ({ node, pathToNode, to, replaceExisting, createCallback }) => {
|
||||
const _node = { ...node }
|
||||
const pipe = expectLastNodeFromPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(pipe)) return pipe
|
||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||
const _node1 = getNode<PipeExpression>('PipeExpression')
|
||||
if (err(_node1)) return _node1
|
||||
const { node: pipe } = _node1
|
||||
|
||||
const newVal = createLiteral(roundOff(to[0], 2))
|
||||
|
||||
@ -589,24 +563,14 @@ export const xLineTo: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to }) => {
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: callExpression } = nodeMeta
|
||||
|
||||
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
|
||||
const firstArg = callExpression.arguments[0]
|
||||
if (firstArg) {
|
||||
const newX = createLiteral(roundOff(to[0], 2))
|
||||
if (isLiteralArrayOrStatic(firstArg)) {
|
||||
callExpression.arguments[0] = newX
|
||||
} else {
|
||||
mutateObjExpProp(firstArg, newX, 'to')
|
||||
}
|
||||
}
|
||||
const newX = createLiteral(roundOff(to[0], 2))
|
||||
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||
callExpression.arguments[0] = newX
|
||||
} else {
|
||||
mutateObjExpProp(callExpression.arguments?.[0], newX, 'to')
|
||||
}
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -628,12 +592,10 @@ export const xLineTo: SketchLineHelper = {
|
||||
export const yLineTo: SketchLineHelper = {
|
||||
add: ({ node, pathToNode, to, replaceExisting, createCallback }) => {
|
||||
const _node = { ...node }
|
||||
const pipe = expectLastNodeFromPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(pipe)) return pipe
|
||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||
const _node1 = getNode<PipeExpression>('PipeExpression')
|
||||
if (err(_node1)) return _node1
|
||||
const { node: pipe } = _node1
|
||||
|
||||
const newVal = createLiteral(roundOff(to[1], 2))
|
||||
|
||||
@ -662,24 +624,14 @@ export const yLineTo: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from }) => {
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: callExpression } = nodeMeta
|
||||
|
||||
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
|
||||
const firstArg = callExpression.arguments[0]
|
||||
if (firstArg) {
|
||||
const newY = createLiteral(roundOff(to[1], 2))
|
||||
if (isLiteralArrayOrStatic(firstArg)) {
|
||||
callExpression.arguments[0] = newY
|
||||
} else {
|
||||
mutateObjExpProp(firstArg, newY, 'to')
|
||||
}
|
||||
}
|
||||
const newY = createLiteral(roundOff(to[1], 2))
|
||||
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||
callExpression.arguments[0] = newY
|
||||
} else {
|
||||
mutateObjExpProp(callExpression.arguments?.[0], newY, 'to')
|
||||
}
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -701,12 +653,10 @@ export const yLineTo: SketchLineHelper = {
|
||||
export const xLine: SketchLineHelper = {
|
||||
add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => {
|
||||
const _node = { ...node }
|
||||
const pipe = expectNodeOnPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(pipe)) return pipe
|
||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||
const _node1 = getNode<PipeExpression>('PipeExpression')
|
||||
if (err(_node1)) return _node1
|
||||
const { node: pipe } = _node1
|
||||
|
||||
const newVal = createLiteral(roundOff(to[0] - from[0], 2))
|
||||
const firstArg = newVal
|
||||
@ -734,24 +684,14 @@ export const xLine: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from }) => {
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: callExpression } = nodeMeta
|
||||
|
||||
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
|
||||
const firstArg = callExpression.arguments[0]
|
||||
if (firstArg) {
|
||||
const newX = createLiteral(roundOff(to[0] - from[0], 2))
|
||||
if (isLiteralArrayOrStatic(firstArg)) {
|
||||
callExpression.arguments[0] = newX
|
||||
} else {
|
||||
mutateObjExpProp(firstArg, newX, 'length')
|
||||
}
|
||||
}
|
||||
const newX = createLiteral(roundOff(to[0] - from[0], 2))
|
||||
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||
callExpression.arguments[0] = newX
|
||||
} else {
|
||||
mutateObjExpProp(callExpression.arguments?.[0], newX, 'length')
|
||||
}
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -773,12 +713,10 @@ export const xLine: SketchLineHelper = {
|
||||
export const yLine: SketchLineHelper = {
|
||||
add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => {
|
||||
const _node = { ...node }
|
||||
const pipe = expectNodeOnPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(pipe)) return pipe
|
||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||
const _node1 = getNode<PipeExpression>('PipeExpression')
|
||||
if (err(_node1)) return _node1
|
||||
const { node: pipe } = _node1
|
||||
const newVal = createLiteral(roundOff(to[1] - from[1], 2))
|
||||
if (replaceExisting && createCallback) {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
@ -803,24 +741,14 @@ export const yLine: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from }) => {
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: callExpression } = nodeMeta
|
||||
|
||||
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
|
||||
const firstArg = callExpression.arguments[0]
|
||||
if (firstArg) {
|
||||
const newY = createLiteral(roundOff(to[1] - from[1], 2))
|
||||
if (isLiteralArrayOrStatic(firstArg)) {
|
||||
callExpression.arguments[0] = newY
|
||||
} else {
|
||||
mutateObjExpProp(firstArg, newY, 'length')
|
||||
}
|
||||
}
|
||||
const newY = createLiteral(roundOff(to[1] - from[1], 2))
|
||||
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
|
||||
callExpression.arguments[0] = newY
|
||||
} else {
|
||||
mutateObjExpProp(callExpression.arguments?.[0], newY, 'length')
|
||||
}
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -849,11 +777,8 @@ export const tangentialArcTo: SketchLineHelper = {
|
||||
referencedSegment,
|
||||
}) => {
|
||||
const _node = { ...node }
|
||||
const _node1 = getNodeFromPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||
const _node1 = getNode<PipeExpression | CallExpression>('PipeExpression')
|
||||
if (err(_node1)) return _node1
|
||||
const { node: pipe } = _node1
|
||||
const _node2 = getNodeFromPath<VariableDeclarator>(
|
||||
@ -862,16 +787,12 @@ export const tangentialArcTo: SketchLineHelper = {
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(_node2)) return _node2
|
||||
const { stopAtNode: varDec } = _node2
|
||||
const { node: varDec } = _node2
|
||||
|
||||
const toX = createLiteral(roundOff(to[0], 2))
|
||||
const toY = createLiteral(roundOff(to[1], 2))
|
||||
|
||||
if (
|
||||
replaceExisting &&
|
||||
createCallback &&
|
||||
isNodeType<PipeExpression>(pipe, 'PipeExpression')
|
||||
) {
|
||||
if (replaceExisting && createCallback && pipe.type !== 'CallExpression') {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[toX, toY],
|
||||
@ -892,7 +813,7 @@ export const tangentialArcTo: SketchLineHelper = {
|
||||
createArrayExpression([toX, toY]),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
if (isNodeType<PipeExpression>(pipe, 'PipeExpression')) {
|
||||
if (pipe.type === 'PipeExpression') {
|
||||
pipe.body = [...pipe.body, newLine]
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -903,7 +824,6 @@ export const tangentialArcTo: SketchLineHelper = {
|
||||
],
|
||||
}
|
||||
} else {
|
||||
if (!varDec) return new Error('Variable declaration not found')
|
||||
varDec.init = createPipeExpression([varDec.init, newLine])
|
||||
}
|
||||
return {
|
||||
@ -913,23 +833,15 @@ export const tangentialArcTo: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from }) => {
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: callExpression } = nodeMeta
|
||||
const x = createLiteral(roundOff(to[0], 2))
|
||||
const y = createLiteral(roundOff(to[1], 2))
|
||||
|
||||
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
|
||||
const firstArg = callExpression.arguments[0]
|
||||
if (firstArg) {
|
||||
if (!mutateArrExp(firstArg, createArrayExpression([x, y]))) {
|
||||
mutateObjExpProp(firstArg, createArrayExpression([x, y]), 'to')
|
||||
}
|
||||
}
|
||||
const firstArg = callExpression.arguments?.[0]
|
||||
if (!mutateArrExp(firstArg, createArrayExpression([x, y]))) {
|
||||
mutateObjExpProp(firstArg, createArrayExpression([x, y]), 'to')
|
||||
}
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -997,12 +909,10 @@ export const angledLine: SketchLineHelper = {
|
||||
referencedSegment,
|
||||
}) => {
|
||||
const _node = { ...node }
|
||||
const pipe = expectNodeOnPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(pipe)) return pipe
|
||||
const getNode = getNodeFromPathCurry(_node, pathToNode)
|
||||
const _node1 = getNode<PipeExpression>('PipeExpression')
|
||||
if (err(_node1)) return _node1
|
||||
const { node: pipe } = _node1
|
||||
|
||||
const newAngleVal = createLiteral(roundOff(getAngle(from, to), 0))
|
||||
const newLengthVal = createLiteral(roundOff(getLength(from, to), 2))
|
||||
@ -1037,11 +947,7 @@ export const angledLine: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from }) => {
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: callExpression } = nodeMeta
|
||||
const angle = roundOff(getAngle(from, to), 0)
|
||||
@ -1050,16 +956,10 @@ export const angledLine: SketchLineHelper = {
|
||||
const angleLit = createLiteral(angle)
|
||||
const lengthLit = createLiteral(lineLength)
|
||||
|
||||
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
|
||||
const firstArg = callExpression.arguments[0]
|
||||
if (firstArg) {
|
||||
if (
|
||||
!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))
|
||||
) {
|
||||
mutateObjExpProp(firstArg, angleLit, 'angle')
|
||||
mutateObjExpProp(firstArg, lengthLit, 'length')
|
||||
}
|
||||
}
|
||||
const firstArg = callExpression.arguments?.[0]
|
||||
if (!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))) {
|
||||
mutateObjExpProp(firstArg, angleLit, 'angle')
|
||||
mutateObjExpProp(firstArg, lengthLit, 'length')
|
||||
}
|
||||
|
||||
return {
|
||||
@ -1093,18 +993,20 @@ export const angledLineOfXLength: SketchLineHelper = {
|
||||
replaceExisting,
|
||||
}) => {
|
||||
const _node = { ...node }
|
||||
const pipe = expectNodeOnPath<PipeExpression>(
|
||||
const nodeMeta = getNodeFromPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(pipe)) return pipe
|
||||
const varDec = expectNodeOnPath<VariableDeclarator>(
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: pipe } = nodeMeta
|
||||
const nodeMeta2 = getNodeFromPath<VariableDeclarator>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(varDec)) return varDec
|
||||
if (err(nodeMeta2)) return nodeMeta2
|
||||
const { node: varDec } = nodeMeta2
|
||||
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.get(variableName)
|
||||
@ -1138,33 +1040,23 @@ export const angledLineOfXLength: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from }) => {
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: callExpression } = nodeMeta
|
||||
const angle = roundOff(getAngle(from, to), 0)
|
||||
const xLength = roundOff(Math.abs(to[0] - from[0]), 2)
|
||||
|
||||
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
|
||||
const firstArg = callExpression.arguments[0]
|
||||
if (firstArg) {
|
||||
const adjustedXLength = isAngleLiteral(firstArg)
|
||||
? Math.abs(xLength)
|
||||
: xLength // todo make work for variable angle > 180
|
||||
const firstArg = callExpression.arguments?.[0]
|
||||
const adjustedXLength = isAngleLiteral(firstArg)
|
||||
? Math.abs(xLength)
|
||||
: xLength // todo make work for variable angle > 180
|
||||
|
||||
const angleLit = createLiteral(angle)
|
||||
const lengthLit = createLiteral(adjustedXLength)
|
||||
const angleLit = createLiteral(angle)
|
||||
const lengthLit = createLiteral(adjustedXLength)
|
||||
|
||||
if (
|
||||
!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))
|
||||
) {
|
||||
mutateObjExpProp(firstArg, angleLit, 'angle')
|
||||
mutateObjExpProp(firstArg, lengthLit, 'length')
|
||||
}
|
||||
}
|
||||
if (!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))) {
|
||||
mutateObjExpProp(firstArg, angleLit, 'angle')
|
||||
mutateObjExpProp(firstArg, lengthLit, 'length')
|
||||
}
|
||||
|
||||
return {
|
||||
@ -1198,18 +1090,20 @@ export const angledLineOfYLength: SketchLineHelper = {
|
||||
replaceExisting,
|
||||
}) => {
|
||||
const _node = { ...node }
|
||||
const pipe = expectNodeOnPath<PipeExpression>(
|
||||
const nodeMeta = getNodeFromPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(pipe)) return pipe
|
||||
const varDec = expectNodeOnPath<VariableDeclarator>(
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: pipe } = nodeMeta
|
||||
const nodeMeta2 = getNodeFromPath<VariableDeclarator>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(varDec)) return varDec
|
||||
if (err(nodeMeta2)) return nodeMeta2
|
||||
const { node: varDec } = nodeMeta2
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.get(variableName)
|
||||
if (!sketch || sketch.type !== 'SketchGroup') {
|
||||
@ -1243,33 +1137,23 @@ export const angledLineOfYLength: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from }) => {
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: callExpression } = nodeMeta
|
||||
const angle = roundOff(getAngle(from, to), 0)
|
||||
const yLength = roundOff(to[1] - from[1], 2)
|
||||
|
||||
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
|
||||
const firstArg = callExpression.arguments[0]
|
||||
if (firstArg) {
|
||||
const adjustedYLength = isAngleLiteral(firstArg)
|
||||
? Math.abs(yLength)
|
||||
: yLength // todo make work for variable angle > 180
|
||||
const firstArg = callExpression.arguments?.[0]
|
||||
const adjustedYLength = isAngleLiteral(firstArg)
|
||||
? Math.abs(yLength)
|
||||
: yLength // todo make work for variable angle > 180
|
||||
|
||||
const angleLit = createLiteral(angle)
|
||||
const lengthLit = createLiteral(adjustedYLength)
|
||||
const angleLit = createLiteral(angle)
|
||||
const lengthLit = createLiteral(adjustedYLength)
|
||||
|
||||
if (
|
||||
!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))
|
||||
) {
|
||||
mutateObjExpProp(firstArg, angleLit, 'angle')
|
||||
mutateObjExpProp(firstArg, lengthLit, 'length')
|
||||
}
|
||||
}
|
||||
if (!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))) {
|
||||
mutateObjExpProp(firstArg, angleLit, 'angle')
|
||||
mutateObjExpProp(firstArg, lengthLit, 'length')
|
||||
}
|
||||
|
||||
return {
|
||||
@ -1303,13 +1187,14 @@ export const angledLineToX: SketchLineHelper = {
|
||||
referencedSegment,
|
||||
}) => {
|
||||
const _node = { ...node }
|
||||
const pipe = expectNodeOnPath<PipeExpression>(
|
||||
const nodeMeta = getNodeFromPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(pipe)) return pipe
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
|
||||
const { node: pipe } = nodeMeta
|
||||
const angle = createLiteral(roundOff(getAngle(from, to), 0))
|
||||
const xArg = createLiteral(roundOff(to[0], 2))
|
||||
if (replaceExisting && createCallback) {
|
||||
@ -1342,32 +1227,22 @@ export const angledLineToX: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from }) => {
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: callExpression } = nodeMeta
|
||||
|
||||
const { node: callExpression } = nodeMeta
|
||||
const angle = roundOff(getAngle(from, to), 0)
|
||||
const xLength = roundOff(to[0], 2)
|
||||
|
||||
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
|
||||
const firstArg = callExpression.arguments[0]
|
||||
if (firstArg) {
|
||||
const adjustedXLength = xLength
|
||||
const firstArg = callExpression.arguments?.[0]
|
||||
const adjustedXLength = xLength
|
||||
|
||||
const angleLit = createLiteral(angle)
|
||||
const lengthLit = createLiteral(adjustedXLength)
|
||||
const angleLit = createLiteral(angle)
|
||||
const lengthLit = createLiteral(adjustedXLength)
|
||||
|
||||
if (
|
||||
!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))
|
||||
) {
|
||||
mutateObjExpProp(firstArg, angleLit, 'angle')
|
||||
mutateObjExpProp(firstArg, lengthLit, 'to')
|
||||
}
|
||||
}
|
||||
if (!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))) {
|
||||
mutateObjExpProp(firstArg, angleLit, 'angle')
|
||||
mutateObjExpProp(firstArg, lengthLit, 'to')
|
||||
}
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -1400,12 +1275,14 @@ export const angledLineToY: SketchLineHelper = {
|
||||
referencedSegment,
|
||||
}) => {
|
||||
const _node = { ...node }
|
||||
const pipe = expectNodeOnPath<PipeExpression>(
|
||||
const nodeMeta = getNodeFromPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(pipe)) return pipe
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
|
||||
const { node: pipe } = nodeMeta
|
||||
|
||||
const angle = createLiteral(roundOff(getAngle(from, to), 0))
|
||||
const yArg = createLiteral(roundOff(to[1], 2))
|
||||
@ -1440,32 +1317,22 @@ export const angledLineToY: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from }) => {
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: callExpression } = nodeMeta
|
||||
|
||||
const { node: callExpression } = nodeMeta
|
||||
const angle = roundOff(getAngle(from, to), 0)
|
||||
const xLength = roundOff(to[1], 2)
|
||||
|
||||
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
|
||||
const firstArg = callExpression.arguments[0]
|
||||
if (firstArg) {
|
||||
const adjustedXLength = xLength
|
||||
const firstArg = callExpression.arguments?.[0]
|
||||
const adjustedXLength = xLength
|
||||
|
||||
const angleLit = createLiteral(angle)
|
||||
const lengthLit = createLiteral(adjustedXLength)
|
||||
const angleLit = createLiteral(angle)
|
||||
const lengthLit = createLiteral(adjustedXLength)
|
||||
|
||||
if (
|
||||
!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))
|
||||
) {
|
||||
mutateObjExpProp(firstArg, angleLit, 'angle')
|
||||
mutateObjExpProp(firstArg, lengthLit, 'to')
|
||||
}
|
||||
}
|
||||
if (!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))) {
|
||||
mutateObjExpProp(firstArg, angleLit, 'angle')
|
||||
mutateObjExpProp(firstArg, lengthLit, 'to')
|
||||
}
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -1505,7 +1372,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
|
||||
const { stopAtNode: pipe } = nodeMeta
|
||||
const { node: pipe } = nodeMeta
|
||||
|
||||
const angle = createLiteral(roundOff(getAngle(from, to), 0))
|
||||
if (!referencedSegment) {
|
||||
@ -1542,7 +1409,6 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
]
|
||||
)
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
if (!pipe) return new Error('Pipe expression not found')
|
||||
pipe.body[callIndex] = callExp
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
@ -1554,13 +1420,10 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, to, from, previousProgramMemory }) => {
|
||||
const _node = { ...node }
|
||||
const callExpression = expectLastNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(callExpression)) return callExpression
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
|
||||
const { node: callExpression } = nodeMeta
|
||||
const angle = roundOff(getAngle(from, to), 0)
|
||||
|
||||
const firstArg = callExpression.arguments?.[0]
|
||||
@ -1571,13 +1434,14 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
: createLiteral('')
|
||||
const intersectTagName =
|
||||
intersectTag.type === 'Identifier' ? intersectTag.name : ''
|
||||
const varDec = expectNodeOnPath<VariableDeclaration>(
|
||||
const nodeMeta2 = getNodeFromPath<VariableDeclaration>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(varDec)) return varDec
|
||||
if (err(nodeMeta2)) return nodeMeta2
|
||||
|
||||
const { node: varDec } = nodeMeta2
|
||||
const varName = varDec.declarations[0].id.name
|
||||
const sketchGroup = previousProgramMemory.get(varName) as SketchGroup
|
||||
const intersectPath = sketchGroup.value.find(
|
||||
@ -1689,16 +1553,11 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
|
||||
to,
|
||||
}) => {
|
||||
const _node = { ...node }
|
||||
const callExpression = expectLastNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(callExpression)) {
|
||||
console.error(callExpression)
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) {
|
||||
console.error(nodeMeta)
|
||||
return {
|
||||
modifiedAst: {
|
||||
type: 'Program',
|
||||
start: 0,
|
||||
end: 0,
|
||||
body: [],
|
||||
@ -1713,6 +1572,7 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
|
||||
}
|
||||
}
|
||||
|
||||
const { node: callExpression } = nodeMeta
|
||||
const toArrExp = createArrayExpression([
|
||||
createLiteral(roundOff(to[0])),
|
||||
createLiteral(roundOff(to[1])),
|
||||
@ -1755,14 +1615,8 @@ export function changeSketchArguments(
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
|
||||
const { node: callExpression, shallowPath } = nodeMeta
|
||||
if (isArray(callExpression)) {
|
||||
return new Error('Expected call expression but found array')
|
||||
}
|
||||
if (!isNodeType<CallExpression>(callExpression, 'CallExpression')) {
|
||||
return new Error('Call expression not found')
|
||||
}
|
||||
|
||||
if (callExpression.callee.name in sketchLineHelperMap) {
|
||||
if (callExpression?.callee?.name in sketchLineHelperMap) {
|
||||
const { updateArgs } = sketchLineHelperMap[callExpression.callee.name]
|
||||
if (!updateArgs) {
|
||||
return new Error('not a sketch line helper')
|
||||
@ -1777,7 +1631,7 @@ export function changeSketchArguments(
|
||||
})
|
||||
}
|
||||
|
||||
return new Error(`not a sketch line helper: ${callExpression.callee.name}`)
|
||||
return new Error(`not a sketch line helper: ${callExpression?.callee?.name}`)
|
||||
}
|
||||
|
||||
export function getConstraintInfo(
|
||||
@ -1883,13 +1737,10 @@ export function addCallExpressionsToPipe({
|
||||
)
|
||||
if (err(pipeExpression)) return pipeExpression
|
||||
|
||||
if (!pipeExpression.stopAtNode) {
|
||||
if (pipeExpression.node.type !== 'PipeExpression') {
|
||||
return new Error('not a pipe expression')
|
||||
}
|
||||
pipeExpression.stopAtNode.body = [
|
||||
...pipeExpression.stopAtNode.body,
|
||||
...expressions,
|
||||
]
|
||||
pipeExpression.node.body = [...pipeExpression.node.body, ...expressions]
|
||||
return _node
|
||||
}
|
||||
|
||||
@ -1912,13 +1763,10 @@ export function addCloseToPipe({
|
||||
)
|
||||
if (err(pipeExpression)) return pipeExpression
|
||||
|
||||
if (!pipeExpression.stopAtNode) {
|
||||
if (pipeExpression.node.type !== 'PipeExpression') {
|
||||
return new Error('not a pipe expression')
|
||||
}
|
||||
pipeExpression.stopAtNode.body = [
|
||||
...pipeExpression.stopAtNode.body,
|
||||
closeExpression,
|
||||
]
|
||||
pipeExpression.node.body = [...pipeExpression.node.body, closeExpression]
|
||||
return _node
|
||||
}
|
||||
|
||||
@ -2006,16 +1854,18 @@ type addTagFn = (a: AddTagInfo) => { modifiedAst: Program; tag: string } | Error
|
||||
function addTag(tagIndex = 2): addTagFn {
|
||||
return ({ node, pathToNode }) => {
|
||||
const _node = { ...node }
|
||||
const primaryCallExp = expectNodeOnPath<CallExpression>(
|
||||
const callExpr = getNodeFromPath<CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(primaryCallExp)) return primaryCallExp
|
||||
if (err(callExpr)) return callExpr
|
||||
|
||||
const { node: primaryCallExp } = callExpr
|
||||
|
||||
// Tag is always 3rd expression now, using arg index feels brittle
|
||||
// but we can come up with a better way to identify tag later.
|
||||
const thirdArg = primaryCallExp.arguments[tagIndex]
|
||||
const thirdArg = primaryCallExp.arguments?.[tagIndex]
|
||||
const tagDeclarator =
|
||||
thirdArg ||
|
||||
(createTagDeclarator(findUniqueName(_node, 'seg', 2)) as TagDeclarator)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getLastNodeFromPath } from 'lang/queryAst'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { ToolTip, toolTips } from 'lang/langHelpers'
|
||||
import {
|
||||
Program,
|
||||
@ -8,9 +8,9 @@ import {
|
||||
SourceRange,
|
||||
Path,
|
||||
PathToNode,
|
||||
Value,
|
||||
} from '../wasm'
|
||||
import { err } from 'lib/trap'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
export function getSketchSegmentFromPathToNode(
|
||||
sketchGroup: SketchGroup,
|
||||
@ -25,14 +25,11 @@ export function getSketchSegmentFromPathToNode(
|
||||
// TODO: once pathTodNode is stored on program memory as part of execution,
|
||||
// we can check if the pathToNode matches the pathToNode of the sketchGroup.
|
||||
// For now we fall back to the sourceRange
|
||||
const nodeMeta = getLastNodeFromPath(ast, pathToNode)
|
||||
const nodeMeta = getNodeFromPath<Value>(ast, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node } = nodeMeta
|
||||
if (isArray(node)) {
|
||||
return new Error('Value node expected, but found array')
|
||||
}
|
||||
|
||||
if (typeof node.start !== 'number' || !node.end)
|
||||
const node = nodeMeta.node
|
||||
if (!node || typeof node.start !== 'number' || !node.end)
|
||||
return new Error('no node found')
|
||||
const sourceRange: SourceRange = [node.start, node.end]
|
||||
return getSketchSegmentFromSourceRange(sketchGroup, sourceRange)
|
||||
|