Compare commits
108 Commits
franknoiro
...
stream-pau
Author | SHA1 | Date | |
---|---|---|---|
c6b80eec68 | |||
1b68f5dc19 | |||
a0aa4802d1 | |||
746f76ec63 | |||
8e624935c2 | |||
0a5f22c80a | |||
142db64796 | |||
59b0cdc3ac | |||
4763257dc3 | |||
b48ba7f081 | |||
64537a59b8 | |||
c24efaf2e4 | |||
2fb16ed074 | |||
82e647db3b | |||
fa2d0a69bf | |||
e372b2680e | |||
c1c1f817c9 | |||
00c0c993f2 | |||
b505c0be07 | |||
6c2d06c2c6 | |||
a4b7dd5182 | |||
0c2ca726d0 | |||
fcfecf702b | |||
347a6ef15a | |||
eed4386f76 | |||
14afcba599 | |||
faee6cbc64 | |||
0673e98fad | |||
b4eea5f842 | |||
2c9eb7f7c0 | |||
e259b2e3e8 | |||
91049204c5 | |||
0128c67aae | |||
ecc42b1e9c | |||
31811d0269 | |||
def5959836 | |||
4c7fab405b | |||
1e12e8d36b | |||
ad775891a3 | |||
efe207f4d2 | |||
01f0162991 | |||
2bbf7fad67 | |||
98549945a4 | |||
315fdc3060 | |||
c7e77e2597 | |||
a48679c014 | |||
c5e74866a9 | |||
731cb6c532 | |||
8a36a4c205 | |||
f29f2557de | |||
5f0ffb56c4 | |||
70078176b0 | |||
098fa2b5c9 | |||
2755156b84 | |||
a8b3ec660d | |||
3747c6ff0e | |||
bc1bc817ba | |||
b415e88746 | |||
9173e368a2 | |||
25928813e3 | |||
aec9cac7c7 | |||
0e82fbf7b0 | |||
f5975bbd61 | |||
765e587f6b | |||
6ccd5e22b8 | |||
c8bf82ba04 | |||
daad2039ec | |||
b567f6dfad | |||
41e85c77ac | |||
9f615b9d3e | |||
e8b5618b34 | |||
799b2d77b4 | |||
7b569f9b4f | |||
0f0c396a0c | |||
83214a88a3 | |||
dbab7876de | |||
6706695502 | |||
fa1f8d8d02 | |||
b4e59b5c56 | |||
20495383ac | |||
7f5fb83761 | |||
2ac874971e | |||
230e3132e9 | |||
bf9bb4fb22 | |||
31e7634669 | |||
84c71aa046 | |||
721b3e8cbd | |||
89309b6ccd | |||
15b163bba8 | |||
60d047ef6a | |||
f105044a47 | |||
9388e09c47 | |||
d71f2af9bd | |||
2bb372de12 | |||
0a3a8afbbd | |||
351df2f306 | |||
05a2eada9a | |||
788270d4fc | |||
6845f0c4bc | |||
563096fba4 | |||
35133c4f45 | |||
b78c6508c2 | |||
08b776134f | |||
cca544189c | |||
69754c82a2 | |||
afbee552ee | |||
b11772b27c | |||
6dc87aa4fe |
@ -4,9 +4,9 @@ set -euo pipefail
|
|||||||
if [[ ! -f "test-results/.last-run.json" ]]; then
|
if [[ ! -f "test-results/.last-run.json" ]]; then
|
||||||
# if no last run artifact, than run plawright normally
|
# if no last run artifact, than run plawright normally
|
||||||
echo "run playwright normally"
|
echo "run playwright normally"
|
||||||
if [[ "$3" == "ubuntu-latest" ]]; then
|
if [[ "$3" == ubuntu-latest* ]]; then
|
||||||
yarn test:playwright:browser:chrome:ubuntu -- --shard=$1/$2 || true
|
yarn test:playwright:browser:chrome:ubuntu -- --shard=$1/$2 || true
|
||||||
elif [[ "$3" == "windows-latest" ]]; then
|
elif [[ "$3" == windows-latest* ]]; then
|
||||||
yarn test:playwright:browser:chrome:windows -- --shard=$1/$2 || true
|
yarn test:playwright:browser:chrome:windows -- --shard=$1/$2 || true
|
||||||
else
|
else
|
||||||
echo "Do not run playwright. Unable to detect os runtime."
|
echo "Do not run playwright. Unable to detect os runtime."
|
||||||
@ -26,9 +26,9 @@ while [[ $retry -le $max_retrys ]]; do
|
|||||||
if [[ $failed_tests -gt 0 ]]; then
|
if [[ $failed_tests -gt 0 ]]; then
|
||||||
echo "retried=true" >>$GITHUB_OUTPUT
|
echo "retried=true" >>$GITHUB_OUTPUT
|
||||||
echo "run playwright with last failed tests and retry $retry"
|
echo "run playwright with last failed tests and retry $retry"
|
||||||
if [[ "$3" == "ubuntu-latest" ]]; then
|
if [[ "$3" == ubuntu-latest* ]]; then
|
||||||
yarn test:playwright:browser:chrome:ubuntu -- --last-failed || true
|
yarn test:playwright:browser:chrome:ubuntu -- --last-failed || true
|
||||||
elif [[ "$3" == "windows-latest" ]]; then
|
elif [[ "$3" == windows-latest* ]]; then
|
||||||
yarn test:playwright:browser:chrome:windows -- --last-failed || true
|
yarn test:playwright:browser:chrome:windows -- --last-failed || true
|
||||||
else
|
else
|
||||||
echo "Do not run playwright. Unable to detect os runtime."
|
echo "Do not run playwright. Unable to detect os runtime."
|
||||||
|
12
.github/ci-cd-scripts/playwright-electron.sh
vendored
@ -4,11 +4,11 @@ set -euo pipefail
|
|||||||
if [[ ! -f "test-results/.last-run.json" ]]; then
|
if [[ ! -f "test-results/.last-run.json" ]]; then
|
||||||
# if no last run artifact, than run plawright normally
|
# if no last run artifact, than run plawright normally
|
||||||
echo "run playwright normally"
|
echo "run playwright normally"
|
||||||
if [[ "$1" == "ubuntu-latest" ]]; then
|
if [[ "$1" == ubuntu-latest* ]]; then
|
||||||
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu || true
|
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu || true
|
||||||
elif [[ "$1" == "windows-latest" ]]; then
|
elif [[ "$1" == windows-latest* ]]; then
|
||||||
yarn test:playwright:electron:windows || true
|
yarn test:playwright:electron:windows || true
|
||||||
elif [[ "$1" == "macos-14" ]]; then
|
elif [[ "$1" == macos-14* ]]; then
|
||||||
yarn test:playwright:electron:macos || true
|
yarn test:playwright:electron:macos || true
|
||||||
else
|
else
|
||||||
echo "Do not run playwright. Unable to detect os runtime."
|
echo "Do not run playwright. Unable to detect os runtime."
|
||||||
@ -28,11 +28,11 @@ while [[ $retry -le $max_retrys ]]; do
|
|||||||
if [[ $failed_tests -gt 0 ]]; then
|
if [[ $failed_tests -gt 0 ]]; then
|
||||||
echo "retried=true" >>$GITHUB_OUTPUT
|
echo "retried=true" >>$GITHUB_OUTPUT
|
||||||
echo "run playwright with last failed tests and retry $retry"
|
echo "run playwright with last failed tests and retry $retry"
|
||||||
if [[ "$1" == "ubuntu-latest" ]]; then
|
if [[ "$1" == ubuntu-latest* ]]; then
|
||||||
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu -- --last-failed || true
|
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:playwright:electron:ubuntu -- --last-failed || true
|
||||||
elif [[ "$1" == "windows-latest" ]]; then
|
elif [[ "$1" == windows-latest* ]]; then
|
||||||
yarn test:playwright:electron:windows -- --last-failed || true
|
yarn test:playwright:electron:windows -- --last-failed || true
|
||||||
elif [[ "$1" == "macos-14" ]]; then
|
elif [[ "$1" == macos-14* ]]; then
|
||||||
yarn test:playwright:electron:macos -- --last-failed || true
|
yarn test:playwright:electron:macos -- --last-failed || true
|
||||||
else
|
else
|
||||||
echo "Do not run playwright. Unable to detect os runtime."
|
echo "Do not run playwright. Unable to detect os runtime."
|
||||||
|
6
.github/dependabot.yml
vendored
@ -8,21 +8,21 @@ updates:
|
|||||||
- package-ecosystem: 'npm' # See documentation for possible values
|
- package-ecosystem: 'npm' # See documentation for possible values
|
||||||
directory: '/' # Location of package manifests
|
directory: '/' # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'daily'
|
interval: 'weekly'
|
||||||
reviewers:
|
reviewers:
|
||||||
- franknoirot
|
- franknoirot
|
||||||
- irev-dev
|
- irev-dev
|
||||||
- package-ecosystem: 'github-actions' # See documentation for possible values
|
- package-ecosystem: 'github-actions' # See documentation for possible values
|
||||||
directory: '/' # Location of package manifests
|
directory: '/' # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'daily'
|
interval: 'weekly'
|
||||||
reviewers:
|
reviewers:
|
||||||
- adamchalmers
|
- adamchalmers
|
||||||
- jessfraz
|
- jessfraz
|
||||||
- package-ecosystem: 'cargo' # See documentation for possible values
|
- package-ecosystem: 'cargo' # See documentation for possible values
|
||||||
directory: '/src/wasm-lib/' # Location of package manifests
|
directory: '/src/wasm-lib/' # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'daily'
|
interval: 'weekly'
|
||||||
reviewers:
|
reviewers:
|
||||||
- adamchalmers
|
- adamchalmers
|
||||||
- jessfraz
|
- jessfraz
|
||||||
|
13
.github/workflows/build-test-publish-apps.yml
vendored
@ -85,7 +85,7 @@ jobs:
|
|||||||
- name: Prepare electron-builder.yml file for updater test
|
- name: Prepare electron-builder.yml file for updater test
|
||||||
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
||||||
run: |
|
run: |
|
||||||
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test-release-notes"' electron-builder.yml
|
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test"' electron-builder.yml
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
||||||
@ -181,6 +181,7 @@ jobs:
|
|||||||
- name: Build the app (release)
|
- name: Build the app (release)
|
||||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||||
env:
|
env:
|
||||||
|
PUBLISH_FOR_PULL_REQUEST: true
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
@ -360,17 +361,17 @@ jobs:
|
|||||||
run: "ls -R out"
|
run: "ls -R out"
|
||||||
|
|
||||||
- name: Authenticate to Google Cloud
|
- name: Authenticate to Google Cloud
|
||||||
uses: 'google-github-actions/auth@v2.1.6'
|
uses: 'google-github-actions/auth@v2.1.7'
|
||||||
with:
|
with:
|
||||||
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
||||||
|
|
||||||
- name: Set up Google Cloud SDK
|
- name: Set up Google Cloud SDK
|
||||||
uses: google-github-actions/setup-gcloud@v2.1.0
|
uses: google-github-actions/setup-gcloud@v2.1.2
|
||||||
with:
|
with:
|
||||||
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
|
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
|
||||||
|
|
||||||
- name: Upload release files to public bucket
|
- name: Upload release files to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
uses: google-github-actions/upload-cloud-storage@v2.2.1
|
||||||
with:
|
with:
|
||||||
path: out
|
path: out
|
||||||
glob: 'Zoo*'
|
glob: 'Zoo*'
|
||||||
@ -378,7 +379,7 @@ jobs:
|
|||||||
destination: ${{ env.BUCKET_DIR }}
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
|
||||||
- name: Upload update endpoint to public bucket
|
- name: Upload update endpoint to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
uses: google-github-actions/upload-cloud-storage@v2.2.1
|
||||||
with:
|
with:
|
||||||
path: out
|
path: out
|
||||||
glob: 'latest*'
|
glob: 'latest*'
|
||||||
@ -386,7 +387,7 @@ jobs:
|
|||||||
destination: ${{ env.BUCKET_DIR }}
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
|
||||||
- name: Upload download endpoint to public bucket
|
- name: Upload download endpoint to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v2.2.0
|
uses: google-github-actions/upload-cloud-storage@v2.2.1
|
||||||
with:
|
with:
|
||||||
path: last_download.json
|
path: last_download.json
|
||||||
destination: ${{ env.BUCKET_DIR }}
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
4
.github/workflows/cargo-test.yml
vendored
@ -5,6 +5,8 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'src/wasm-lib/**.rs'
|
- 'src/wasm-lib/**.rs'
|
||||||
- 'src/wasm-lib/**.hbs'
|
- 'src/wasm-lib/**.hbs'
|
||||||
|
- 'src/wasm-lib/**.gen'
|
||||||
|
- 'src/wasm-lib/**.snap'
|
||||||
- '**/Cargo.toml'
|
- '**/Cargo.toml'
|
||||||
- '**/Cargo.lock'
|
- '**/Cargo.lock'
|
||||||
- '**/rust-toolchain.toml'
|
- '**/rust-toolchain.toml'
|
||||||
@ -15,6 +17,8 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'src/wasm-lib/**.rs'
|
- 'src/wasm-lib/**.rs'
|
||||||
- 'src/wasm-lib/**.hbs'
|
- 'src/wasm-lib/**.hbs'
|
||||||
|
- 'src/wasm-lib/**.gen'
|
||||||
|
- 'src/wasm-lib/**.snap'
|
||||||
- '**/Cargo.toml'
|
- '**/Cargo.toml'
|
||||||
- '**/Cargo.lock'
|
- '**/Cargo.lock'
|
||||||
- '**/rust-toolchain.toml'
|
- '**/rust-toolchain.toml'
|
||||||
|
6
.github/workflows/e2e-tests.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest]
|
os: [ubuntu-latest-8-cores, windows-latest-8-cores]
|
||||||
shardIndex: [1, 2, 3, 4]
|
shardIndex: [1, 2, 3, 4]
|
||||||
shardTotal: [4]
|
shardTotal: [4]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
@ -227,7 +227,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macos-14]
|
os: [ubuntu-latest-8-cores, windows-latest-8-cores, macos-14-large]
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
needs: check-rust-changes
|
needs: check-rust-changes
|
||||||
@ -287,7 +287,7 @@ jobs:
|
|||||||
brew install gnu-sed
|
brew install gnu-sed
|
||||||
echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH
|
echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH
|
||||||
- name: Install vector
|
- name: Install vector
|
||||||
if: ${{ !startsWith(matrix.os, 'windows') }}
|
if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
|
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
|
||||||
|
25
.github/workflows/static-analysis.yml
vendored
@ -81,6 +81,31 @@ jobs:
|
|||||||
- name: Run codespell
|
- name: Run codespell
|
||||||
run: codespell --config .codespellrc # Edit this file to tweak the typo list and other configuration.
|
run: codespell --config .codespellrc # Edit this file to tweak the typo list and other configuration.
|
||||||
|
|
||||||
|
yarn-unit-test-kcl-samples:
|
||||||
|
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 build:wasm
|
||||||
|
|
||||||
|
- run: yarn simpleserver:bg
|
||||||
|
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 for kcl samples
|
||||||
|
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
|
||||||
|
run: yarn test:unit:kcl-samples
|
||||||
|
env:
|
||||||
|
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
|
|
||||||
yarn-unit-test:
|
yarn-unit-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
2
Makefile
@ -19,7 +19,7 @@ $(XSTATE_TYPEGENS): $(TS_SRC)
|
|||||||
yarn xstate typegen 'src/**/*.ts?(x)'
|
yarn xstate typegen 'src/**/*.ts?(x)'
|
||||||
|
|
||||||
public/wasm_lib_bg.wasm: $(WASM_LIB_FILES)
|
public/wasm_lib_bg.wasm: $(WASM_LIB_FILES)
|
||||||
yarn build:wasm-dev
|
yarn build:wasm
|
||||||
|
|
||||||
node_modules: package.json yarn.lock
|
node_modules: package.json yarn.lock
|
||||||
yarn install
|
yarn install
|
||||||
|
@ -110,7 +110,7 @@ Which commands from setup are one off vs need to be run every time?
|
|||||||
The following will need to be run when checking out a new commit and guarantees the build is not stale:
|
The following will need to be run when checking out a new commit and guarantees the build is not stale:
|
||||||
```bash
|
```bash
|
||||||
yarn install
|
yarn install
|
||||||
yarn build:wasm-dev # or yarn build:wasm for slower but more production-like build
|
yarn build:wasm
|
||||||
yarn start # or yarn build:local && yarn serve for slower but more production-like build
|
yarn start # or yarn build:local && yarn serve for slower but more production-like build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "angleToMatchLengthX"
|
title: "angleToMatchLengthX"
|
||||||
excerpt: "Compute the angle (in degrees) in o"
|
excerpt: "Returns the angle to match the given length for x."
|
||||||
layout: manual
|
layout: manual
|
||||||
---
|
---
|
||||||
|
|
||||||
Compute the angle (in degrees) in o
|
Returns the angle to match the given length for x.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,9 +84,13 @@ layout: manual
|
|||||||
* [`rem`](kcl/rem)
|
* [`rem`](kcl/rem)
|
||||||
* [`revolve`](kcl/revolve)
|
* [`revolve`](kcl/revolve)
|
||||||
* [`segAng`](kcl/segAng)
|
* [`segAng`](kcl/segAng)
|
||||||
|
* [`segEnd`](kcl/segEnd)
|
||||||
* [`segEndX`](kcl/segEndX)
|
* [`segEndX`](kcl/segEndX)
|
||||||
* [`segEndY`](kcl/segEndY)
|
* [`segEndY`](kcl/segEndY)
|
||||||
* [`segLen`](kcl/segLen)
|
* [`segLen`](kcl/segLen)
|
||||||
|
* [`segStart`](kcl/segStart)
|
||||||
|
* [`segStartX`](kcl/segStartX)
|
||||||
|
* [`segStartY`](kcl/segStartY)
|
||||||
* [`shell`](kcl/shell)
|
* [`shell`](kcl/shell)
|
||||||
* [`sin`](kcl/sin)
|
* [`sin`](kcl/sin)
|
||||||
* [`sqrt`](kcl/sqrt)
|
* [`sqrt`](kcl/sqrt)
|
||||||
|
59
docs/kcl/modules.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
title: "KCL Modules"
|
||||||
|
excerpt: "Documentation of modules for the KCL language for the Zoo Modeling App."
|
||||||
|
layout: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
`KCL` allows splitting code up into multiple files. Each file is somewhat
|
||||||
|
isolated from other files as a separate module.
|
||||||
|
|
||||||
|
When you define a function, you can use `export` before it to make it available
|
||||||
|
to other modules.
|
||||||
|
|
||||||
|
```
|
||||||
|
// util.kcl
|
||||||
|
export fn increment = (x) => {
|
||||||
|
return x + 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Other files in the project can now import functions that have been exported.
|
||||||
|
This makes them available to use in another file.
|
||||||
|
|
||||||
|
```
|
||||||
|
// main.kcl
|
||||||
|
import increment from "util.kcl"
|
||||||
|
|
||||||
|
answer = increment(41)
|
||||||
|
```
|
||||||
|
|
||||||
|
Imported files _must_ be in the same project so that units are uniform across
|
||||||
|
modules. This means that it must be in the same directory.
|
||||||
|
|
||||||
|
Import statements must be at the top-level of a file. It is not allowed to have
|
||||||
|
an `import` statement inside a function or in the body of an if-else.
|
||||||
|
|
||||||
|
Multiple functions can be exported in a file.
|
||||||
|
|
||||||
|
```
|
||||||
|
// util.kcl
|
||||||
|
export fn increment = (x) => {
|
||||||
|
return x + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn decrement = (x) => {
|
||||||
|
return x - 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When importing, you can import multiple functions at once.
|
||||||
|
|
||||||
|
```
|
||||||
|
import increment, decrement from "util.kcl"
|
||||||
|
```
|
||||||
|
|
||||||
|
Imported symbols can be renamed for convenience or to avoid name collisions.
|
||||||
|
|
||||||
|
```
|
||||||
|
import increment as inc, decrement as dec from "util.kcl"
|
||||||
|
```
|
53
docs/kcl/segEnd.md
Normal file
56
docs/kcl/segStart.md
Normal file
43
docs/kcl/segStartX.md
Normal file
44
docs/kcl/segStartY.md
Normal file
8981
docs/kcl/std.json
16
docs/kcl/types/KclNone.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "KclNone"
|
||||||
|
excerpt: "KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application)."
|
||||||
|
layout: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application).
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -23,8 +23,110 @@ Any KCL value.
|
|||||||
|
|
||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `type` |enum: `UserVal`| | No |
|
| `type` |enum: `Uuid`| | No |
|
||||||
| `value` |``| | No |
|
| `value` |`string`| | No |
|
||||||
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: `Bool`| | No |
|
||||||
|
| `value` |`boolean`| | No |
|
||||||
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: `Number`| | No |
|
||||||
|
| `value` |`number`| | No |
|
||||||
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: `Int`| | No |
|
||||||
|
| `value` |`integer`| | No |
|
||||||
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: `String`| | No |
|
||||||
|
| `value` |`string`| | No |
|
||||||
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: `Array`| | No |
|
||||||
|
| `value` |`[` [`KclValue`](/docs/kcl/types/KclValue) `]`| | No |
|
||||||
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: `Object`| | No |
|
||||||
|
| `value` |`object`| | No |
|
||||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
@ -111,6 +213,38 @@ A face.
|
|||||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: [`Sketch`](/docs/kcl/types/Sketch)| | No |
|
||||||
|
| `value` |[`Sketch`](/docs/kcl/types/Sketch)| Any KCL value. | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: `Sketches`| | No |
|
||||||
|
| `value` |`[` [`Sketch`](/docs/kcl/types/Sketch) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
----
|
----
|
||||||
An solid is a collection of extrude surfaces.
|
An solid is a collection of extrude surfaces.
|
||||||
|
|
||||||
@ -190,6 +324,23 @@ Data for an imported geometry.
|
|||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
|
**Type:** `object`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Type | Description | Required |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| `type` |enum: [`KclNone`](/docs/kcl/types/KclNone)| | No |
|
||||||
|
| `value` |[`KclNone`](/docs/kcl/types/KclNone)| Any KCL value. | No |
|
||||||
|
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,15 +67,15 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
|||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)`)
|
|> xLine(${commonPoints.num1}, %)`)
|
||||||
}
|
}
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> xLine(${commonPoints.num1}, %)
|
||||||
|> line([0, ${commonPoints.num1 + 0.01}], %)`)
|
|> yLine(${commonPoints.num1 + 0.01}, %)`)
|
||||||
} else {
|
} else {
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
}
|
}
|
||||||
@ -84,9 +84,9 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
|||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> xLine(${commonPoints.num1}, %)
|
||||||
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
|> yLine(${commonPoints.num1 + 0.01}, %)
|
||||||
|> lineTo([0, ${commonPoints.num3}], %)`)
|
|> xLine(${commonPoints.num2 * -1}, %)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// deselect line tool
|
// deselect line tool
|
||||||
@ -142,9 +142,9 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
|||||||
await u.openKclCodePanel()
|
await u.openKclCodePanel()
|
||||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %, $seg01)
|
|> xLine(${commonPoints.num1}, %, $seg01)
|
||||||
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
|> yLine(${commonPoints.num1 + 0.01}, %)
|
||||||
|> angledLine([180, segLen(seg01)], %)`)
|
|> xLine(-segLen(seg01), %)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe('Basic sketch', () => {
|
test.describe('Basic sketch', () => {
|
||||||
|
@ -632,16 +632,18 @@ test.describe('Editor tests', () => {
|
|||||||
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
// this test might be brittle as we add and remove functions
|
|
||||||
// but should also be easy to update.
|
|
||||||
// tests clicking on an option, selection the first option
|
// tests clicking on an option, selection the first option
|
||||||
// and arrowing down to an option
|
// and arrowing down to an option
|
||||||
|
|
||||||
await u.codeLocator.click()
|
await u.codeLocator.click()
|
||||||
await page.keyboard.type('sketch001 = start')
|
await page.keyboard.type('sketch001 = start')
|
||||||
|
|
||||||
// expect there to be six auto complete options
|
// expect there to be some auto complete options
|
||||||
await expect(page.locator('.cm-completionLabel')).toHaveCount(8)
|
// exact number depends on the KCL stdlib, so let's just check it's > 0 for now.
|
||||||
|
await expect(async () => {
|
||||||
|
const children = await page.locator('.cm-completionLabel').count()
|
||||||
|
expect(children).toBeGreaterThan(0)
|
||||||
|
}).toPass()
|
||||||
// this makes sure we can accept a completion with click
|
// this makes sure we can accept a completion with click
|
||||||
await page.getByText('startSketchOn').click()
|
await page.getByText('startSketchOn').click()
|
||||||
await page.keyboard.type("'XZ'")
|
await page.keyboard.type("'XZ'")
|
||||||
@ -692,6 +694,9 @@ test.describe('Editor tests', () => {
|
|||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([3.14, 12], %)
|
|> startProfileAt([3.14, 12], %)
|
||||||
|> xLine(5, %) // lin`)
|
|> xLine(5, %) // lin`)
|
||||||
|
|
||||||
|
// expect there to be no KCL errors
|
||||||
|
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('with tab to accept the completion', async ({ page }) => {
|
test('with tab to accept the completion', async ({ page }) => {
|
||||||
@ -985,7 +990,7 @@ test.describe('Editor tests', () => {
|
|||||||
|> extrude(5, %)`)
|
|> extrude(5, %)`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test(
|
test.fixme(
|
||||||
`Can use the import stdlib function on a local OBJ file`,
|
`Can use the import stdlib function on a local OBJ file`,
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ browserName }, testInfo) => {
|
async ({ browserName }, testInfo) => {
|
||||||
|
@ -26,10 +26,6 @@ test.describe('integrations tests', () => {
|
|||||||
'Creating a new file or switching file while in sketchMode should exit sketchMode',
|
'Creating a new file or switching file while in sketchMode should exit sketchMode',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ tronApp, homePage, scene, editor, toolbar }) => {
|
async ({ tronApp, homePage, scene, editor, toolbar }) => {
|
||||||
test.skip(
|
|
||||||
process.platform === 'win32',
|
|
||||||
'windows times out will waiting for the execution indicator?'
|
|
||||||
)
|
|
||||||
await tronApp.initialise({
|
await tronApp.initialise({
|
||||||
fixtures: { homePage, scene, editor, toolbar },
|
fixtures: { homePage, scene, editor, toolbar },
|
||||||
folderSetupFn: async (dir) => {
|
folderSetupFn: async (dir) => {
|
||||||
@ -55,7 +51,6 @@ test.describe('integrations tests', () => {
|
|||||||
sortBy: 'last-modified-desc',
|
sortBy: 'last-modified-desc',
|
||||||
})
|
})
|
||||||
await homePage.openProject('test-sample')
|
await homePage.openProject('test-sample')
|
||||||
// windows times out here, hence the skip above
|
|
||||||
await scene.waitForExecutionDone()
|
await scene.waitForExecutionDone()
|
||||||
})
|
})
|
||||||
await test.step('enter sketch mode', async () => {
|
await test.step('enter sketch mode', async () => {
|
||||||
@ -71,10 +66,13 @@ test.describe('integrations tests', () => {
|
|||||||
await toolbar.editSketch()
|
await toolbar.editSketch()
|
||||||
await expect(toolbar.exitSketchBtn).toBeVisible()
|
await expect(toolbar.exitSketchBtn).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const fileName = 'Untitled.kcl'
|
||||||
await test.step('check sketch mode is exited when creating new file', async () => {
|
await test.step('check sketch mode is exited when creating new file', async () => {
|
||||||
await toolbar.fileTreeBtn.click()
|
await toolbar.fileTreeBtn.click()
|
||||||
await toolbar.expectFileTreeState(['main.kcl'])
|
await toolbar.expectFileTreeState(['main.kcl'])
|
||||||
await toolbar.createFile({ wait: true })
|
|
||||||
|
await toolbar.createFile({ fileName, waitForToastToDisappear: true })
|
||||||
|
|
||||||
// check we're out of sketch mode
|
// check we're out of sketch mode
|
||||||
await expect(toolbar.exitSketchBtn).not.toBeVisible()
|
await expect(toolbar.exitSketchBtn).not.toBeVisible()
|
||||||
@ -93,10 +91,10 @@ test.describe('integrations tests', () => {
|
|||||||
})
|
})
|
||||||
await toolbar.editSketch()
|
await toolbar.editSketch()
|
||||||
await expect(toolbar.exitSketchBtn).toBeVisible()
|
await expect(toolbar.exitSketchBtn).toBeVisible()
|
||||||
await toolbar.expectFileTreeState(['main.kcl', 'Untitled.kcl'])
|
await toolbar.expectFileTreeState(['main.kcl', fileName])
|
||||||
})
|
})
|
||||||
await test.step('check sketch mode is exited when opening a different file', async () => {
|
await test.step('check sketch mode is exited when opening a different file', async () => {
|
||||||
await toolbar.openFile('untitled.kcl', { wait: false })
|
await toolbar.openFile(fileName, { wait: false })
|
||||||
|
|
||||||
// check we're out of sketch mode
|
// check we're out of sketch mode
|
||||||
await expect(toolbar.exitSketchBtn).not.toBeVisible()
|
await expect(toolbar.exitSketchBtn).not.toBeVisible()
|
||||||
@ -109,7 +107,7 @@ test.describe('when using the file tree to', () => {
|
|||||||
const fromFile = 'main.kcl'
|
const fromFile = 'main.kcl'
|
||||||
const toFile = 'hello.kcl'
|
const toFile = 'hello.kcl'
|
||||||
|
|
||||||
test(
|
test.fixme(
|
||||||
`rename ${fromFile} to ${toFile}, and doesn't crash on reload and settings load`,
|
`rename ${fromFile} to ${toFile}, and doesn't crash on reload and settings load`,
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ browser: _, tronApp }, testInfo) => {
|
async ({ browser: _, tronApp }, testInfo) => {
|
||||||
@ -157,7 +155,7 @@ test.describe('when using the file tree to', () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
test.fixme(
|
||||||
`create many new untitled files they increment their names`,
|
`create many new untitled files they increment their names`,
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ browser: _, tronApp }, testInfo) => {
|
async ({ browser: _, tronApp }, testInfo) => {
|
||||||
@ -298,7 +296,7 @@ test.describe('when using the file tree to', () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
test.fixme(
|
||||||
'loading small file, then large, then back to small',
|
'loading small file, then large, then back to small',
|
||||||
{
|
{
|
||||||
tag: '@electron',
|
tag: '@electron',
|
||||||
|
@ -195,7 +195,7 @@ export class SceneFixture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
waitForExecutionDone = async () => {
|
waitForExecutionDone = async () => {
|
||||||
await expect(this.exeIndicator).toBeVisible()
|
await expect(this.exeIndicator).toBeVisible({ timeout: 30000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
expectPixelColor = async (
|
expectPixelColor = async (
|
||||||
|
@ -16,6 +16,7 @@ export class ToolbarFixture {
|
|||||||
fileCreateToast!: Locator
|
fileCreateToast!: Locator
|
||||||
filePane!: Locator
|
filePane!: Locator
|
||||||
exeIndicator!: Locator
|
exeIndicator!: Locator
|
||||||
|
treeInputField!: Locator
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
this.page = page
|
this.page = page
|
||||||
@ -31,6 +32,7 @@ export class ToolbarFixture {
|
|||||||
this.editSketchBtn = page.getByText('Edit Sketch')
|
this.editSketchBtn = page.getByText('Edit Sketch')
|
||||||
this.fileTreeBtn = page.locator('[id="files-button-holder"]')
|
this.fileTreeBtn = page.locator('[id="files-button-holder"]')
|
||||||
this.createFileBtn = page.getByTestId('create-file-button')
|
this.createFileBtn = page.getByTestId('create-file-button')
|
||||||
|
this.treeInputField = page.getByTestId('tree-input-field')
|
||||||
|
|
||||||
this.filePane = page.locator('#files-pane')
|
this.filePane = page.locator('#files-pane')
|
||||||
this.fileCreateToast = page.getByText('Successfully created')
|
this.fileCreateToast = page.getByText('Successfully created')
|
||||||
@ -59,10 +61,15 @@ export class ToolbarFixture {
|
|||||||
expectFileTreeState = async (expected: string[]) => {
|
expectFileTreeState = async (expected: string[]) => {
|
||||||
await expect.poll(this._serialiseFileTree).toEqual(expected)
|
await expect.poll(this._serialiseFileTree).toEqual(expected)
|
||||||
}
|
}
|
||||||
createFile = async ({ wait }: { wait: boolean } = { wait: false }) => {
|
createFile = async (args: {
|
||||||
|
fileName: string
|
||||||
|
waitForToastToDisappear: boolean
|
||||||
|
}) => {
|
||||||
await this.createFileBtn.click()
|
await this.createFileBtn.click()
|
||||||
|
await this.treeInputField.fill(args.fileName)
|
||||||
|
await this.treeInputField.press('Enter')
|
||||||
await expect(this.fileCreateToast).toBeVisible()
|
await expect(this.fileCreateToast).toBeVisible()
|
||||||
if (wait) {
|
if (args.waitForToastToDisappear) {
|
||||||
await this.fileCreateToast.waitFor({ state: 'detached' })
|
await this.fileCreateToast.waitFor({ state: 'detached' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ export const isErrorWhitelisted = (exception: Error) => {
|
|||||||
{
|
{
|
||||||
name: '"{"kind"',
|
name: '"{"kind"',
|
||||||
message:
|
message:
|
||||||
'"engine","sourceRanges":[[0,0]],"msg":"Failed to get string from response from engine: `JsValue(undefined)`"}"',
|
'"engine","sourceRanges":[[0,0,0]],"msg":"Failed to get string from response from engine: `JsValue(undefined)`"}"',
|
||||||
stack: '',
|
stack: '',
|
||||||
foundInSpec: 'e2e/playwright/testing-settings.spec.ts',
|
foundInSpec: 'e2e/playwright/testing-settings.spec.ts',
|
||||||
project: 'Google Chrome',
|
project: 'Google Chrome',
|
||||||
@ -156,8 +156,8 @@ export const isErrorWhitelisted = (exception: Error) => {
|
|||||||
{
|
{
|
||||||
name: 'Unhandled Promise Rejection',
|
name: 'Unhandled Promise Rejection',
|
||||||
message:
|
message:
|
||||||
'{"kind":"engine","sourceRanges":[[0,0]],"msg":"Failed to get string from response from engine: `JsValue(undefined)`"}',
|
'{"kind":"engine","sourceRanges":[[0,0,0]],"msg":"Failed to get string from response from engine: `JsValue(undefined)`"}',
|
||||||
stack: `Unhandled Promise Rejection: {"kind":"engine","sourceRanges":[[0,0]],"msg":"Failed to get string from response from engine: \`JsValue(undefined)\`"}
|
stack: `Unhandled Promise Rejection: {"kind":"engine","sourceRanges":[[0,0,0]],"msg":"Failed to get string from response from engine: \`JsValue(undefined)\`"}
|
||||||
at unknown (http://localhost:3000/src/lang/std/engineConnection.ts:1245:26)`,
|
at unknown (http://localhost:3000/src/lang/std/engineConnection.ts:1245:26)`,
|
||||||
foundInSpec:
|
foundInSpec:
|
||||||
'e2e/playwright/onboarding-tests.spec.ts Click through each onboarding step',
|
'e2e/playwright/onboarding-tests.spec.ts Click through each onboarding step',
|
||||||
@ -253,7 +253,7 @@ export const isErrorWhitelisted = (exception: Error) => {
|
|||||||
{
|
{
|
||||||
name: '{"kind"',
|
name: '{"kind"',
|
||||||
stack: ``,
|
stack: ``,
|
||||||
message: `engine","sourceRanges":[[0,0]],"msg":"Failed to wait for promise from engine: JsValue(\\"Force interrupt, executionIsStale, new AST requested\\")"}`,
|
message: `engine","sourceRanges":[[0,0,0]],"msg":"Failed to wait for promise from engine: JsValue(\\"Force interrupt, executionIsStale, new AST requested\\")"}`,
|
||||||
project: 'Google Chrome',
|
project: 'Google Chrome',
|
||||||
foundInSpec: 'e2e/playwright/testing-settings.spec.ts',
|
foundInSpec: 'e2e/playwright/testing-settings.spec.ts',
|
||||||
},
|
},
|
||||||
|
@ -452,7 +452,7 @@ sketch002 = startSketchOn(extrude001, seg03)
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test(`Verify axis and origin snapping`, async ({
|
test(`Verify axis, origin, and horizontal snapping`, async ({
|
||||||
app,
|
app,
|
||||||
editor,
|
editor,
|
||||||
toolbar,
|
toolbar,
|
||||||
@ -505,7 +505,7 @@ test(`Verify axis and origin snapping`, async ({
|
|||||||
const expectedCodeSnippets = {
|
const expectedCodeSnippets = {
|
||||||
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
|
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
|
||||||
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`,
|
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`,
|
||||||
segmentOnXAxis: `lineTo([${xAxisSloppy.kcl[0]}, ${xAxisSloppy.kcl[1]}], %)`,
|
segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`,
|
||||||
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`,
|
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`,
|
||||||
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
|
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
|
||||||
}
|
}
|
||||||
|
@ -854,7 +854,7 @@ test(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
test.fixme(
|
||||||
'Deleting projects, can delete individual project, can still create projects after deleting all',
|
'Deleting projects, can delete individual project, can still create projects after deleting all',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ browserName }, testInfo) => {
|
async ({ browserName }, testInfo) => {
|
||||||
@ -1490,7 +1490,6 @@ test(
|
|||||||
'function_sketch.kcl',
|
'function_sketch.kcl',
|
||||||
'function_sketch_with_position.kcl',
|
'function_sketch_with_position.kcl',
|
||||||
'global-tags.kcl',
|
'global-tags.kcl',
|
||||||
'helix_ccw.kcl',
|
|
||||||
'helix_defaults.kcl',
|
'helix_defaults.kcl',
|
||||||
'helix_defaults_negative_extrude.kcl',
|
'helix_defaults_negative_extrude.kcl',
|
||||||
'helix_with_length.kcl',
|
'helix_with_length.kcl',
|
||||||
@ -1670,7 +1669,8 @@ test(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
// Flaky
|
||||||
|
test.fixme(
|
||||||
'Original project name persist after onboarding',
|
'Original project name persist after onboarding',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ browserName }, testInfo) => {
|
async ({ browserName }, testInfo) => {
|
||||||
|
@ -115,7 +115,7 @@ test.describe('Sketch tests', () => {
|
|||||||
'persistCode',
|
'persistCode',
|
||||||
`sketch001 = startSketchOn('XZ')
|
`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([4.61, -14.01], %)
|
|> startProfileAt([4.61, -14.01], %)
|
||||||
|> line([12.73, -0.09], %)
|
|> xLine(12.73, %)
|
||||||
|> tangentialArcTo([24.95, -5.38], %)`
|
|> tangentialArcTo([24.95, -5.38], %)`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -156,7 +156,7 @@ test.describe('Sketch tests', () => {
|
|||||||
await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
|
await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
|
||||||
.toBe(`sketch001 = startSketchOn('XZ')
|
.toBe(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([12.34, -12.34], %)
|
|> startProfileAt([12.34, -12.34], %)
|
||||||
|> line([-12.34, 12.34], %)
|
|> yLine(12.34, %)
|
||||||
|
|
||||||
`)
|
`)
|
||||||
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
||||||
@ -202,35 +202,19 @@ test.describe('Sketch tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
const viewport = { width: 1200, height: 500 }
|
||||||
|
await page.setViewportSize(viewport)
|
||||||
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled()
|
).not.toBeDisabled()
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
const center = {
|
||||||
await u.openAndClearDebugPanel()
|
x: viewport.width / 2,
|
||||||
await u.sendCustomCmd({
|
y: viewport.height / 2,
|
||||||
type: 'modeling_cmd_req',
|
}
|
||||||
cmd_id: uuidv4(),
|
const modelAreaSize = await u.getModelViewAreaSize()
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_look_at',
|
|
||||||
vantage: { x: 0, y: -1250, z: 580 },
|
|
||||||
center: { x: 0, y: 0, z: 0 },
|
|
||||||
up: { x: 0, y: 0, z: 1 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await u.sendCustomCmd({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_get_settings',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
// If we have the code pane open, we should see the code.
|
// If we have the code pane open, we should see the code.
|
||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
@ -244,7 +228,7 @@ test.describe('Sketch tests', () => {
|
|||||||
await expect(u.codeLocator).not.toBeVisible()
|
await expect(u.codeLocator).not.toBeVisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
const startPX = [665, 458]
|
const startPX = [center.x + 65, 458]
|
||||||
|
|
||||||
const dragPX = 30
|
const dragPX = 30
|
||||||
let prevContent = ''
|
let prevContent = ''
|
||||||
@ -255,7 +239,7 @@ test.describe('Sketch tests', () => {
|
|||||||
// Wait for the render.
|
// Wait for the render.
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
// Select the sketch
|
// Select the sketch
|
||||||
await page.mouse.click(700, 370)
|
await page.mouse.click(center.x + 100, 370)
|
||||||
}
|
}
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Edit Sketch' })
|
page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
@ -266,24 +250,47 @@ test.describe('Sketch tests', () => {
|
|||||||
prevContent = await page.locator('.cm-content').innerText()
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
vantage: { x: 0, y: -1250, z: 580 - modelAreaSize.w },
|
||||||
|
center: { x: 0, y: 0, z: 0 },
|
||||||
|
up: { x: 0, y: 0, z: 1 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_get_settings',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
const step5 = { steps: 5 }
|
const step5 = { steps: 5 }
|
||||||
|
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
||||||
|
|
||||||
// drag startProfieAt handle
|
test.step('drag startProfileAt handle', async () => {
|
||||||
await page.mouse.move(startPX[0], startPX[1])
|
await page.mouse.move(startPX[0], startPX[1])
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
||||||
await page.mouse.up()
|
await page.mouse.up()
|
||||||
|
|
||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
prevContent = await page.locator('.cm-content').innerText()
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// drag line handle
|
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
test.step('drag line handle', async () => {
|
||||||
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
|
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
|
||||||
await page.mouse.move(lineEnd.x - 5, lineEnd.y)
|
await page.mouse.move(lineEnd.x - 5, lineEnd.y)
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
@ -294,17 +301,23 @@ test.describe('Sketch tests', () => {
|
|||||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
prevContent = await page.locator('.cm-content').innerText()
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// drag tangentialArcTo handle
|
test.step('drag tangentialArcTo handle', async () => {
|
||||||
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
|
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
|
||||||
await page.mouse.move(tangentEnd.x, tangentEnd.y - 5)
|
await page.mouse.move(tangentEnd.x, tangentEnd.y - 5)
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.move(tangentEnd.x + dragPX, tangentEnd.y - dragPX, step5)
|
await page.mouse.move(
|
||||||
|
tangentEnd.x + dragPX,
|
||||||
|
tangentEnd.y - dragPX,
|
||||||
|
step5
|
||||||
|
)
|
||||||
await page.mouse.up()
|
await page.mouse.up()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Open the code pane
|
// Open the code pane
|
||||||
await u.openKclCodePanel()
|
await u.openKclCodePanel()
|
||||||
@ -580,7 +593,7 @@ test.describe('Sketch tests', () => {
|
|||||||
})
|
})
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
const startPX = [665, 458]
|
const center = await u.getCenterOfModelViewArea()
|
||||||
|
|
||||||
const dragPX = 30
|
const dragPX = 30
|
||||||
|
|
||||||
@ -596,7 +609,7 @@ test.describe('Sketch tests', () => {
|
|||||||
|
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
||||||
|
|
||||||
// drag startProfieAt handle
|
// drag startProfileAt handle
|
||||||
await page.mouse.move(startPX[0], startPX[1])
|
await page.mouse.move(startPX[0], startPX[1])
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
||||||
@ -638,6 +651,7 @@ test.describe('Sketch tests', () => {
|
|||||||
})
|
})
|
||||||
test('Can add multiple sketches', async ({ page }) => {
|
test('Can add multiple sketches', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
|
|
||||||
const viewportSize = { width: 1200, height: 500 }
|
const viewportSize = { width: 1200, height: 500 }
|
||||||
await page.setViewportSize(viewportSize)
|
await page.setViewportSize(viewportSize)
|
||||||
|
|
||||||
@ -645,7 +659,7 @@ test.describe('Sketch tests', () => {
|
|||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
|
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
|
||||||
const { toSU, click00r } = getMovementUtils({ center, page })
|
const { toSU, toU, click00r } = getMovementUtils({ center, page })
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
@ -661,29 +675,32 @@ test.describe('Sketch tests', () => {
|
|||||||
200
|
200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const center = await u.getCenterOfModelViewArea()
|
||||||
|
|
||||||
let codeStr = "sketch001 = startSketchOn('XY')"
|
let codeStr = "sketch001 = startSketchOn('XY')"
|
||||||
|
|
||||||
await page.mouse.click(center.x, viewportSize.height * 0.55)
|
await page.mouse.click(center.x - 50, viewportSize.height * 0.55)
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||||
|
|
||||||
await click00r(0, 0)
|
const { click00r } = await getMovementUtils({ center, page })
|
||||||
codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)`
|
|
||||||
|
let coord = await click00r(0, 0)
|
||||||
|
codeStr += ` |> startProfileAt(${coord.kcl}, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(50, 0)
|
await click00r(50, 0)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
codeStr += ` |> lineTo(${toSU([50, 0])}, %)`
|
codeStr += ` |> xLine(${toU(50, 0)[0]}, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(0, 50)
|
await click00r(0, 50)
|
||||||
codeStr += ` |> line(${toSU([0, 50])}, %)`
|
codeStr += ` |> yLine(${toU(0, 50)[1]}, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
let clickCoords = await click00r(-50, 0)
|
await click00r(-50, 0)
|
||||||
expect(clickCoords).not.toBeUndefined()
|
codeStr += ` |> xLine(${toU(-50, 0)[0]}, %)`
|
||||||
codeStr += ` |> lineTo(${toSU(clickCoords!)}, %)`
|
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
// exit the sketch, reset relative clicker
|
// exit the sketch, reset relative clicker
|
||||||
@ -699,28 +716,29 @@ test.describe('Sketch tests', () => {
|
|||||||
|
|
||||||
// when exiting the sketch above the camera is still looking down at XY,
|
// when exiting the sketch above the camera is still looking down at XY,
|
||||||
// so selecting the plane again is a bit easier.
|
// so selecting the plane again is a bit easier.
|
||||||
await page.mouse.click(center.x + 200, center.y + 100)
|
await page.mouse.move(center.x - 100, center.y + 50, { steps: 5 })
|
||||||
|
await page.mouse.click(center.x - 100, center.y + 50)
|
||||||
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
|
||||||
codeStr += "sketch002 = startSketchOn('XY')"
|
codeStr += "sketch002 = startSketchOn('XY')"
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await click00r(30, 0)
|
coord = await click00r(30, 0)
|
||||||
codeStr += ` |> startProfileAt([2.03, 0], %)`
|
codeStr += ` |> startProfileAt(${coord.kcl}, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
// TODO: I couldn't use `toSU` here because of some rounding error causing
|
// TODO: I couldn't use `toSU` here because of some rounding error causing
|
||||||
// it to be off by 0.01
|
// it to be off by 0.01
|
||||||
await click00r(30, 0)
|
await click00r(30, 0)
|
||||||
codeStr += ` |> lineTo([4.07, 0], %)`
|
codeStr += ` |> xLine(2.04, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(0, 30)
|
await click00r(0, 30)
|
||||||
codeStr += ` |> line([0, -2.03], %)`
|
codeStr += ` |> yLine(-2.03, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(-30, 0)
|
await click00r(-30, 0)
|
||||||
codeStr += ` |> line([-2.04, 0], %)`
|
codeStr += ` |> xLine(-2.04, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(undefined, undefined)
|
await click00r(undefined, undefined)
|
||||||
@ -744,8 +762,8 @@ test.describe('Sketch tests', () => {
|
|||||||
|
|
||||||
const code = `sketch001 = startSketchOn('-XZ')
|
const code = `sketch001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %)
|
|> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %)
|
||||||
|> line([${roundOff(scale * 139.19)}, 0], %)
|
|> xLine(${roundOff(scale * 139.19)}, %)
|
||||||
|> line([0, -${roundOff(scale * 139.2)}], %)
|
|> yLine(-${roundOff(scale * 139.2)}, %)
|
||||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|> close(%)`
|
|> close(%)`
|
||||||
|
|
||||||
@ -764,20 +782,21 @@ test.describe('Sketch tests', () => {
|
|||||||
await u.updateCamPosition(camPos)
|
await u.updateCamPosition(camPos)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
const center = await u.getCenterOfModelViewArea()
|
||||||
await page.mouse.move(0, 0)
|
await page.mouse.move(0, 0)
|
||||||
|
|
||||||
// select a plane
|
// select a plane
|
||||||
await page.mouse.move(700, 200, { steps: 10 })
|
await page.mouse.move(center.x + 100, 200, { steps: 10 })
|
||||||
await page.mouse.click(700, 200, { delay: 200 })
|
await page.mouse.click(center.x + 100, 200, { delay: 200 })
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`sketch001 = startSketchOn('-XZ')`
|
`sketch001 = startSketchOn('-XZ')`
|
||||||
)
|
)
|
||||||
|
|
||||||
let prevContent = await page.locator('.cm-content').innerText()
|
let prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
const pointA = [700, 200]
|
const pointA = [center.x + 100, 200]
|
||||||
const pointB = [900, 200]
|
const pointB = [center.x + 300, 200]
|
||||||
const pointC = [900, 400]
|
const pointC = [center.x + 300, 400]
|
||||||
|
|
||||||
// draw three lines
|
// draw three lines
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
@ -914,7 +933,9 @@ extrude001 = extrude(5, sketch001)
|
|||||||
|
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
|
||||||
await page.mouse.click(622, 355)
|
const center = await u.getCenterOfModelViewArea()
|
||||||
|
|
||||||
|
await page.mouse.click(center.x + 22, 355)
|
||||||
|
|
||||||
await page.waitForTimeout(800)
|
await page.waitForTimeout(800)
|
||||||
await page.getByText(`END')`).click()
|
await page.getByText(`END')`).click()
|
||||||
|
@ -462,7 +462,7 @@ test(
|
|||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
code += `
|
code += `
|
||||||
|> line([7.25, 0], %)`
|
|> xLine(7.25, %)`
|
||||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||||
|
|
||||||
await page
|
await page
|
||||||
@ -647,7 +647,7 @@ test.describe(
|
|||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
code += `
|
code += `
|
||||||
|> line([7.25, 0], %)`
|
|> xLine(7.25, %)`
|
||||||
await expect(u.codeLocator).toHaveText(code)
|
await expect(u.codeLocator).toHaveText(code)
|
||||||
|
|
||||||
await page
|
await page
|
||||||
@ -752,7 +752,7 @@ test.describe(
|
|||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
code += `
|
code += `
|
||||||
|> line([184.3, 0], %)`
|
|> xLine(184.3, %)`
|
||||||
await expect(u.codeLocator).toHaveText(code)
|
await expect(u.codeLocator).toHaveText(code)
|
||||||
|
|
||||||
await page
|
await page
|
||||||
@ -1031,7 +1031,7 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('theme persists', async ({ page, context }) => {
|
test.fixme('theme persists', async ({ page, context }) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await context.addInitScript(async () => {
|
await context.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
@ -141,7 +141,7 @@ test.describe('Test network and connection issues', () => {
|
|||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)`)
|
|> xLine(${commonPoints.num1}, %)`)
|
||||||
|
|
||||||
// Expect the network to be up
|
// Expect the network to be up
|
||||||
await expect(networkToggle).toContainText('Connected')
|
await expect(networkToggle).toContainText('Connected')
|
||||||
@ -207,7 +207,7 @@ test.describe('Test network and connection issues', () => {
|
|||||||
await expect.poll(u.normalisedEditorCode)
|
await expect.poll(u.normalisedEditorCode)
|
||||||
.toBe(`sketch001 = startSketchOn('XZ')
|
.toBe(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([12.34, -12.34], %)
|
|> startProfileAt([12.34, -12.34], %)
|
||||||
|> line([12.34, 0], %)
|
|> xLine(12.34, %)
|
||||||
|> line([-12.34, 12.34], %)
|
|> line([-12.34, 12.34], %)
|
||||||
|
|
||||||
`)
|
`)
|
||||||
@ -217,9 +217,9 @@ test.describe('Test network and connection issues', () => {
|
|||||||
await expect.poll(u.normalisedEditorCode)
|
await expect.poll(u.normalisedEditorCode)
|
||||||
.toBe(`sketch001 = startSketchOn('XZ')
|
.toBe(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([12.34, -12.34], %)
|
|> startProfileAt([12.34, -12.34], %)
|
||||||
|> line([12.34, 0], %)
|
|> xLine(12.34, %)
|
||||||
|> line([-12.34, 12.34], %)
|
|> line([-12.34, 12.34], %)
|
||||||
|> lineTo([0, -12.34], %)
|
|> xLine(-12.34, %)
|
||||||
|
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
@ -8,6 +8,21 @@ import {
|
|||||||
Locator,
|
Locator,
|
||||||
test,
|
test,
|
||||||
} from '@playwright/test'
|
} from '@playwright/test'
|
||||||
|
import {
|
||||||
|
OrthographicCamera,
|
||||||
|
Mesh,
|
||||||
|
Scene,
|
||||||
|
Raycaster,
|
||||||
|
PlaneGeometry,
|
||||||
|
MeshBasicMaterial,
|
||||||
|
DoubleSide,
|
||||||
|
Vector2,
|
||||||
|
Vector3,
|
||||||
|
} from 'three'
|
||||||
|
import {
|
||||||
|
RAYCASTABLE_PLANE,
|
||||||
|
INTERSECTION_PLANE_LAYER,
|
||||||
|
} from 'clientSideScene/constants'
|
||||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||||
import fsp from 'fs/promises'
|
import fsp from 'fs/promises'
|
||||||
import fsSync from 'fs'
|
import fsSync from 'fs'
|
||||||
@ -257,55 +272,141 @@ export const circleMove = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMovementUtils = (opts: any) => {
|
export function rollingRound(n: number, digitsAfterDecimal: number) {
|
||||||
// The way we truncate is kinda odd apparently, so we need this function
|
const s = String(n).split('.')
|
||||||
// "[k]itty[c]ad round"
|
|
||||||
const kcRound = (n: number) => Math.trunc(n * 100) / 100
|
|
||||||
|
|
||||||
// To translate between screen and engine ("[U]nit") coordinates
|
// There are no decimals, just return the number.
|
||||||
// NOTE: these pretty much can't be perfect because of screen scaling.
|
if (s.length === 1) return n
|
||||||
// Handle on a case-by-case.
|
|
||||||
const toU = (x: number, y: number) => [
|
|
||||||
kcRound(x * 0.0678),
|
|
||||||
kcRound(-y * 0.0678), // Y is inverted in our coordinate system
|
|
||||||
]
|
|
||||||
|
|
||||||
// Turn the array into a string with specific formatting
|
// Find the closest 9. We don't care about anything beyond that.
|
||||||
const fromUToString = (xy: number[]) => `[${xy[0]}, ${xy[1]}]`
|
const nineIndex = s[1].indexOf('9')
|
||||||
|
|
||||||
// Combine because used often
|
const fractStr = nineIndex > 0 ? s[1].slice(0, nineIndex + 1) : s[1]
|
||||||
const toSU = (xy: number[]) => fromUToString(toU(xy[0], xy[1]))
|
|
||||||
|
let fract = Number(fractStr) / 10 ** fractStr.length
|
||||||
|
|
||||||
|
for (let i = fractStr.length - 1; i >= 0; i -= 1) {
|
||||||
|
if (i === digitsAfterDecimal) break
|
||||||
|
fract = Math.round(fract * 10 ** i) / 10 ** i
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Number(s[0]) + fract).toFixed(digitsAfterDecimal)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMovementUtils = async (opts: any) => {
|
||||||
|
const sceneInfra = await opts.page.evaluate(() => window.sceneInfra)
|
||||||
|
|
||||||
|
// Various data for raycasting into the scene to get our XY.
|
||||||
|
const hundredM = 100_0000
|
||||||
|
const planeGeometry = new PlaneGeometry(hundredM, hundredM)
|
||||||
|
const planeMaterial = new MeshBasicMaterial({
|
||||||
|
color: 0xff0000,
|
||||||
|
side: DoubleSide,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.5,
|
||||||
|
})
|
||||||
|
const scene = new Scene()
|
||||||
|
const intersectionPlane = new Mesh(planeGeometry, planeMaterial)
|
||||||
|
intersectionPlane.userData = { type: RAYCASTABLE_PLANE }
|
||||||
|
intersectionPlane.name = RAYCASTABLE_PLANE
|
||||||
|
intersectionPlane.layers.set(INTERSECTION_PLANE_LAYER)
|
||||||
|
scene.add(intersectionPlane)
|
||||||
|
const planeRaycaster = new Raycaster()
|
||||||
|
planeRaycaster.far = Infinity
|
||||||
|
planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
|
const kcRound = (n: number) => Math.round(n * 100) / 100
|
||||||
|
|
||||||
// Make it easier to click around from center ("click [from] zero zero")
|
// Make it easier to click around from center ("click [from] zero zero")
|
||||||
const click00 = (x: number, y: number) =>
|
const click00 = (x: number, y: number) =>
|
||||||
opts.page.mouse.click(opts.center.x + x, opts.center.y + y, { delay: 100 })
|
opts.page.mouse.click(x, y, { delay: 100 })
|
||||||
|
|
||||||
// Relative clicker, must keep state
|
// Relative clicker, must keep state
|
||||||
let last = { x: 0, y: 0 }
|
let last = { x: 0, y: 0 }
|
||||||
|
let lastScreenSpace = { x: 0, y: 0 }
|
||||||
|
|
||||||
const click00r = async (x?: number, y?: number) => {
|
const click00r = async (x?: number, y?: number) => {
|
||||||
// reset relative coordinates when anything is undefined
|
// reset relative coordinates when anything is undefined
|
||||||
if (x === undefined || y === undefined) {
|
if (x === undefined || y === undefined) {
|
||||||
last.x = 0
|
last = { x: 0, y: 0 }
|
||||||
last.y = 0
|
lastScreenSpace = { x: 0, y: 0 }
|
||||||
return
|
return {
|
||||||
|
nextXY: [0, 0],
|
||||||
|
kcl: `[0, 0]`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await circleMove(
|
const absX = opts.center.x + x
|
||||||
opts.page,
|
const absY = opts.center.y + y
|
||||||
opts.center.x + last.x + x,
|
|
||||||
opts.center.y + last.y + y,
|
const nextX = last.x + x
|
||||||
10,
|
const nextY = last.y + y
|
||||||
10
|
|
||||||
|
const targetX = opts.center.x + nextX
|
||||||
|
const targetY = opts.center.y + -nextY
|
||||||
|
|
||||||
|
// Use the current camera specification
|
||||||
|
const camera = await opts.page.evaluate(() => {
|
||||||
|
window.sceneInfra.camControls.onCameraChange(true)
|
||||||
|
return window.sceneInfra.camControls.camera
|
||||||
|
})
|
||||||
|
|
||||||
|
const windowWH = await opts.page.evaluate(() => ({
|
||||||
|
w: window.innerWidth,
|
||||||
|
h: window.innerHeight,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// I didn't write this math, it's copied from sceneInfra.ts, and I understand
|
||||||
|
// it's just normalizing the point, but why *-2 ± 1 I have no idea.
|
||||||
|
const mouseVector = new Vector2(
|
||||||
|
(targetX / windowWH.w) * 2 - 1,
|
||||||
|
-(targetY / windowWH.h) * 2 + 1
|
||||||
)
|
)
|
||||||
await click00(last.x + x, last.y + y)
|
planeRaycaster.setFromCamera(mouseVector, camera)
|
||||||
|
const intersections = planeRaycaster.intersectObjects(scene.children, true)
|
||||||
|
|
||||||
|
const planePosition = intersections[0].object.position
|
||||||
|
const inversePlaneQuaternion = intersections[0].object.quaternion
|
||||||
|
.clone()
|
||||||
|
.invert()
|
||||||
|
let transformedPoint = intersections[0].point.clone()
|
||||||
|
if (transformedPoint) {
|
||||||
|
transformedPoint.applyQuaternion(inversePlaneQuaternion)
|
||||||
|
}
|
||||||
|
const twoD = new Vector2(
|
||||||
|
// I think the intersection plane doesn't get scale when nearly everything else does, maybe that should change
|
||||||
|
transformedPoint.x / sceneInfra._baseUnitMultiplier,
|
||||||
|
transformedPoint.y / sceneInfra._baseUnitMultiplier
|
||||||
|
) // z should be 0
|
||||||
|
const planePositionCorrected = new Vector3(
|
||||||
|
...planePosition
|
||||||
|
).applyQuaternion(inversePlaneQuaternion)
|
||||||
|
twoD.sub(new Vector2(...planePositionCorrected))
|
||||||
|
|
||||||
|
await circleMove(opts.page, targetX, targetY, 10, 10)
|
||||||
|
await click00(targetX, targetY)
|
||||||
|
|
||||||
last.x += x
|
last.x += x
|
||||||
last.y += y
|
last.y += y
|
||||||
|
|
||||||
// Returns the new absolute coordinate if you need it.
|
const relativeScreenSpace = {
|
||||||
return [last.x, last.y]
|
x: twoD.x - lastScreenSpace.x,
|
||||||
|
y: -(twoD.y - lastScreenSpace.y),
|
||||||
}
|
}
|
||||||
|
|
||||||
return { toSU, click00r }
|
lastScreenSpace.x = kcRound(twoD.x)
|
||||||
|
lastScreenSpace.y = kcRound(twoD.y)
|
||||||
|
|
||||||
|
// Returns the new absolute coordinate and the screen space coordinate if you need it.
|
||||||
|
return {
|
||||||
|
nextXY: [last.x, last.y],
|
||||||
|
kcl: `[${kcRound(relativeScreenSpace.x)}, ${-kcRound(
|
||||||
|
relativeScreenSpace.y
|
||||||
|
)}]`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { toSU, toU, click00r }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForAuthAndLsp(page: Page) {
|
async function waitForAuthAndLsp(page: Page) {
|
||||||
@ -356,6 +457,30 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
|||||||
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
|
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
|
||||||
|
|
||||||
const util = {
|
const util = {
|
||||||
|
async getModelViewAreaSize() {
|
||||||
|
const windowInnerWidth = await page.evaluate(() => window.innerWidth)
|
||||||
|
const windowInnerHeight = await page.evaluate(() => window.innerHeight)
|
||||||
|
|
||||||
|
const sidebar = page.getByTestId('modeling-sidebar')
|
||||||
|
const bb = await sidebar.boundingBox()
|
||||||
|
return {
|
||||||
|
w: windowInnerWidth - (bb?.width ?? 0),
|
||||||
|
h: windowInnerHeight - (bb?.height ?? 0),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getCenterOfModelViewArea() {
|
||||||
|
const windowInnerWidth = await page.evaluate(() => window.innerWidth)
|
||||||
|
const windowInnerHeight = await page.evaluate(() => window.innerHeight)
|
||||||
|
|
||||||
|
const sidebar = page.getByTestId('modeling-sidebar')
|
||||||
|
const bb = await sidebar.boundingBox()
|
||||||
|
const goRightPx = (bb?.width ?? 0) / 2
|
||||||
|
const borderWidthsCombined = 2
|
||||||
|
return {
|
||||||
|
x: Math.round(windowInnerWidth / 2 + goRightPx) - borderWidthsCombined,
|
||||||
|
y: Math.round(windowInnerHeight / 2),
|
||||||
|
}
|
||||||
|
},
|
||||||
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
|
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
|
||||||
waitForPageLoad: () => waitForPageLoad(page),
|
waitForPageLoad: () => waitForPageLoad(page),
|
||||||
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),
|
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),
|
||||||
|
@ -43,10 +43,12 @@ test.describe('Testing constraints', () => {
|
|||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
await page.waitForTimeout(500) // wait for animation
|
await page.waitForTimeout(500) // wait for animation
|
||||||
|
|
||||||
const startXPx = 500
|
const center = await u.getCenterOfModelViewArea()
|
||||||
|
|
||||||
|
const startXPx = center.x - 100
|
||||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await page.mouse.click(834, 244)
|
await page.mouse.click(center.x + 234, 244)
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
|
|
||||||
await page
|
await page
|
||||||
|
@ -32,10 +32,17 @@ test.describe('Testing selections', () => {
|
|||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
const xAxisClick = () =>
|
const yAxisClick = () =>
|
||||||
page.mouse.click(700, 253).then(() => page.waitForTimeout(100))
|
test.step('Click on Y axis', async () => {
|
||||||
|
await page.mouse.move(600, 200, { steps: 5 })
|
||||||
|
await page.mouse.click(600, 200)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
})
|
||||||
const xAxisClickAfterExitingSketch = () =>
|
const xAxisClickAfterExitingSketch = () =>
|
||||||
page.mouse.click(639, 278).then(() => page.waitForTimeout(100))
|
test.step(`Click on X axis after exiting sketch, which shifts it at the moment`, async () => {
|
||||||
|
await page.mouse.click(639, 278)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
})
|
||||||
const emptySpaceHover = () =>
|
const emptySpaceHover = () =>
|
||||||
test.step('Hover over empty space', async () => {
|
test.step('Hover over empty space', async () => {
|
||||||
await page.mouse.move(700, 143, { steps: 5 })
|
await page.mouse.move(700, 143, { steps: 5 })
|
||||||
@ -80,23 +87,23 @@ test.describe('Testing selections', () => {
|
|||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)`)
|
|> xLine(${commonPoints.num1}, %)`)
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> xLine(${commonPoints.num1}, %)
|
||||||
|> line([0, ${commonPoints.num1 + 0.01}], %)`)
|
|> yLine(${commonPoints.num1 + 0.01}, %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> xLine(${commonPoints.num1}, %)
|
||||||
|> line([0, ${commonPoints.num1 + 0.01}], %)
|
|> yLine(${commonPoints.num1 + 0.01}, %)
|
||||||
|> lineTo([0, ${commonPoints.num3}], %)`)
|
|> xLine(${commonPoints.num2 * -1}, %)`)
|
||||||
|
|
||||||
// deselect line tool
|
// deselect line tool
|
||||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||||
@ -121,29 +128,31 @@ test.describe('Testing selections', () => {
|
|||||||
// now check clicking works including axis
|
// now check clicking works including axis
|
||||||
|
|
||||||
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
|
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
|
||||||
await topHorzSegmentClick()
|
|
||||||
await page.keyboard.down('Shift')
|
|
||||||
const constrainButton = page.getByRole('button', {
|
const constrainButton = page.getByRole('button', {
|
||||||
name: 'Length: open menu',
|
name: 'Length: open menu',
|
||||||
})
|
})
|
||||||
const absYButton = page.getByRole('button', { name: 'Absolute Y' })
|
const absXButton = page.getByRole('button', { name: 'Absolute X' })
|
||||||
|
|
||||||
|
await test.step(`Select a segment and an axis, see that a relevant constraint is enabled`, async () => {
|
||||||
|
await topHorzSegmentClick()
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
await constrainButton.click()
|
await constrainButton.click()
|
||||||
await expect(absYButton).toBeDisabled()
|
await expect(absXButton).toBeDisabled()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await xAxisClick()
|
await yAxisClick()
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
await constrainButton.click()
|
await constrainButton.click()
|
||||||
await absYButton.and(page.locator(':not([disabled])')).waitFor()
|
await absXButton.and(page.locator(':not([disabled])')).waitFor()
|
||||||
await expect(absYButton).not.toBeDisabled()
|
await expect(absXButton).not.toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
// clear selection by clicking on nothing
|
|
||||||
await emptySpaceClick()
|
await emptySpaceClick()
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
// same selection but click the axis first
|
|
||||||
await xAxisClick()
|
await test.step(`Same selection but click the axis first`, async () => {
|
||||||
|
await yAxisClick()
|
||||||
await constrainButton.click()
|
await constrainButton.click()
|
||||||
await expect(absYButton).toBeDisabled()
|
await expect(absXButton).toBeDisabled()
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await topHorzSegmentClick()
|
await topHorzSegmentClick()
|
||||||
@ -151,23 +160,26 @@ test.describe('Testing selections', () => {
|
|||||||
|
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
await constrainButton.click()
|
await constrainButton.click()
|
||||||
await expect(absYButton).not.toBeDisabled()
|
await expect(absXButton).not.toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
// clear selection by clicking on nothing
|
// clear selection by clicking on nothing
|
||||||
await emptySpaceClick()
|
await emptySpaceClick()
|
||||||
|
|
||||||
// check the same selection again by putting cursor in code first then selecting axis
|
// check the same selection again by putting cursor in code first then selecting axis
|
||||||
|
await test.step(`Same selection but code selection then axis`, async () => {
|
||||||
await page
|
await page
|
||||||
.getByText(` |> lineTo([0, ${commonPoints.num3}], %)`)
|
.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`)
|
||||||
.click()
|
.click()
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await constrainButton.click()
|
await constrainButton.click()
|
||||||
await expect(absYButton).toBeDisabled()
|
await expect(absXButton).toBeDisabled()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await xAxisClick()
|
await yAxisClick()
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
await constrainButton.click()
|
await constrainButton.click()
|
||||||
await expect(absYButton).not.toBeDisabled()
|
await expect(absXButton).not.toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
// clear selection by clicking on nothing
|
// clear selection by clicking on nothing
|
||||||
await emptySpaceClick()
|
await emptySpaceClick()
|
||||||
@ -182,9 +194,7 @@ test.describe('Testing selections', () => {
|
|||||||
process.platform === 'linux' ? 'Control' : 'Meta'
|
process.platform === 'linux' ? 'Control' : 'Meta'
|
||||||
)
|
)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page
|
await page.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`).click()
|
||||||
.getByText(` |> lineTo([0, ${commonPoints.num3}], %)`)
|
|
||||||
.click()
|
|
||||||
|
|
||||||
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
@ -928,6 +938,7 @@ sketch002 = startSketchOn(extrude001, $seg01)
|
|||||||
// test fillet button with the body in the scene
|
// test fillet button with the body in the scene
|
||||||
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
|
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
|
||||||
extrude001 = extrude(10, sketch001)`
|
extrude001 = extrude(10, sketch001)`
|
||||||
|
await u.codeLocator.clear()
|
||||||
await u.codeLocator.fill(codeToAdd)
|
await u.codeLocator.fill(codeToAdd)
|
||||||
await selectSegment()
|
await selectSegment()
|
||||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
import { test, expect } from '@playwright/test'
|
||||||
import * as fsp from 'fs/promises'
|
import * as fsp from 'fs/promises'
|
||||||
import * as fs from 'fs'
|
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import {
|
import {
|
||||||
getUtils,
|
getUtils,
|
||||||
@ -259,7 +258,7 @@ test.describe('Testing settings', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test(
|
test.fixme(
|
||||||
`Project settings override user settings on desktop`,
|
`Project settings override user settings on desktop`,
|
||||||
{ tag: ['@electron', '@skipWin'] },
|
{ tag: ['@electron', '@skipWin'] },
|
||||||
async ({ browser: _ }, testInfo) => {
|
async ({ browser: _ }, testInfo) => {
|
||||||
@ -305,21 +304,6 @@ test.describe('Testing settings', () => {
|
|||||||
const projectLink = page.getByText('bracket')
|
const projectLink = page.getByText('bracket')
|
||||||
const logoLink = page.getByTestId('app-logo')
|
const logoLink = page.getByTestId('app-logo')
|
||||||
|
|
||||||
async function confirmThemeWasWritten(filePath: string, value: string) {
|
|
||||||
return expect
|
|
||||||
.poll(
|
|
||||||
async () => {
|
|
||||||
const fileExists = await fs.existsSync(filePath)
|
|
||||||
return fileExists ? fsp.readFile(filePath, 'utf-8') : ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Setting should now be written to the file',
|
|
||||||
timeout: 5_000,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.toContain(`themeColor = "${value}"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
await test.step('Set user theme color on home', async () => {
|
await test.step('Set user theme color on home', async () => {
|
||||||
await expect(settingsOpenButton).toBeVisible()
|
await expect(settingsOpenButton).toBeVisible()
|
||||||
await settingsOpenButton.click()
|
await settingsOpenButton.click()
|
||||||
@ -327,8 +311,13 @@ test.describe('Testing settings', () => {
|
|||||||
await expect(userSettingsTab).toBeChecked()
|
await expect(userSettingsTab).toBeChecked()
|
||||||
await themeColorSetting.fill(userThemeColor)
|
await themeColorSetting.fill(userThemeColor)
|
||||||
await expect(logoLink).toHaveCSS('--primary-hue', userThemeColor)
|
await expect(logoLink).toHaveCSS('--primary-hue', userThemeColor)
|
||||||
await confirmThemeWasWritten(tempUserSettingsFilePath, userThemeColor)
|
|
||||||
await settingsCloseButton.click()
|
await settingsCloseButton.click()
|
||||||
|
await expect
|
||||||
|
.poll(async () => fsp.readFile(tempUserSettingsFilePath, 'utf-8'), {
|
||||||
|
message: 'Setting should now be written to the file',
|
||||||
|
timeout: 5_000,
|
||||||
|
})
|
||||||
|
.toContain(`themeColor = "${userThemeColor}"`)
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step('Set project theme color', async () => {
|
await test.step('Set project theme color', async () => {
|
||||||
@ -339,25 +328,29 @@ test.describe('Testing settings', () => {
|
|||||||
await expect(projectSettingsTab).toBeChecked()
|
await expect(projectSettingsTab).toBeChecked()
|
||||||
await themeColorSetting.fill(projectThemeColor)
|
await themeColorSetting.fill(projectThemeColor)
|
||||||
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
|
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
|
||||||
// Make sure that the project settings file has been written to before continuing
|
|
||||||
await confirmThemeWasWritten(
|
|
||||||
tempProjectSettingsFilePath,
|
|
||||||
projectThemeColor
|
|
||||||
)
|
|
||||||
await settingsCloseButton.click()
|
await settingsCloseButton.click()
|
||||||
|
// Make sure that the project settings file has been written to before continuing
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => fsp.readFile(tempProjectSettingsFilePath, 'utf-8'),
|
||||||
|
{
|
||||||
|
message: 'Setting should now be written to the file',
|
||||||
|
timeout: 5_000,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.toContain(`themeColor = "${projectThemeColor}"`)
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step('Refresh the application and see project setting applied', async () => {
|
await test.step('Refresh the application and see project setting applied', async () => {
|
||||||
// Make sure we're done navigating before we reload
|
// Make sure we're done navigating before we reload
|
||||||
await expect(settingsCloseButton).not.toBeVisible()
|
await expect(settingsCloseButton).not.toBeVisible()
|
||||||
await page.reload({ waitUntil: 'domcontentloaded' })
|
|
||||||
|
|
||||||
|
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||||
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
|
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Navigate back to the home view and see user setting applied`, async () => {
|
await test.step(`Navigate back to the home view and see user setting applied`, async () => {
|
||||||
await logoLink.click()
|
await logoLink.click()
|
||||||
await page.screenshot({ path: 'out.png' })
|
|
||||||
await expect(logoLink).toHaveCSS('--primary-hue', userThemeColor)
|
await expect(logoLink).toHaveCSS('--primary-hue', userThemeColor)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -421,7 +414,7 @@ test.describe('Testing settings', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// It was much easier to test the logo color than the background stream color.
|
// It was much easier to test the logo color than the background stream color.
|
||||||
test(
|
test.fixme(
|
||||||
'user settings reload on external change, on project and modeling view',
|
'user settings reload on external change, on project and modeling view',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ browserName }, testInfo) => {
|
async ({ browserName }, testInfo) => {
|
||||||
@ -750,18 +743,19 @@ extrude001 = extrude(5, sketch001)
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
// Selectors and constants
|
// Selectors and constants
|
||||||
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
|
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
const lineToolButton = page.getByTestId('line')
|
const lineToolButton = page.getByTestId('line')
|
||||||
const segmentOverlays = page.getByTestId('segment-overlay')
|
const segmentOverlays = page.getByTestId('segment-overlay')
|
||||||
const sketchOriginLocation = { x: 600, y: 250 }
|
const sketchOriginLocation = await u.getCenterOfModelViewArea()
|
||||||
const darkThemeSegmentColor: [number, number, number] = [215, 215, 215]
|
const darkThemeSegmentColor: [number, number, number] = [215, 215, 215]
|
||||||
const lightThemeSegmentColor: [number, number, number] = [90, 90, 90]
|
const lightThemeSegmentColor: [number, number, number] = [90, 90, 90]
|
||||||
|
|
||||||
await test.step(`Get into sketch mode`, async () => {
|
await test.step(`Get into sketch mode`, async () => {
|
||||||
await u.waitForAuthSkipAppStart()
|
await page.mouse.click(sketchOriginLocation.x, sketchOriginLocation.y)
|
||||||
await page.mouse.click(700, 200)
|
|
||||||
await expect(editSketchButton).toBeVisible()
|
await expect(editSketchButton).toBeVisible()
|
||||||
await editSketchButton.click()
|
await editSketchButton.click()
|
||||||
|
|
||||||
@ -772,12 +766,18 @@ extrude001 = extrude(5, sketch001)
|
|||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0)
|
||||||
|
|
||||||
|
// Our lines are translucent (surprise!), so we need to get on portion
|
||||||
|
// of the line that is only on the background, and not on top of something
|
||||||
|
// like the axis lines.
|
||||||
|
line1.x -= 1
|
||||||
|
line1.y -= 1
|
||||||
|
|
||||||
await test.step(`Check the sketch line color before`, async () => {
|
await test.step(`Check the sketch line color before`, async () => {
|
||||||
await expect
|
await expect
|
||||||
.poll(() =>
|
.poll(() => u.getGreatestPixDiff(line1, darkThemeSegmentColor))
|
||||||
u.getGreatestPixDiff(sketchOriginLocation, darkThemeSegmentColor)
|
.toBeLessThanOrEqual(34)
|
||||||
)
|
|
||||||
.toBeLessThan(15)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Change theme to light using command palette`, async () => {
|
await test.step(`Change theme to light using command palette`, async () => {
|
||||||
@ -792,10 +792,8 @@ extrude001 = extrude(5, sketch001)
|
|||||||
|
|
||||||
await test.step(`Check the sketch line color after`, async () => {
|
await test.step(`Check the sketch line color after`, async () => {
|
||||||
await expect
|
await expect
|
||||||
.poll(() =>
|
.poll(() => u.getGreatestPixDiff(line1, lightThemeSegmentColor))
|
||||||
u.getGreatestPixDiff(sketchOriginLocation, lightThemeSegmentColor)
|
.toBeLessThanOrEqual(34)
|
||||||
)
|
|
||||||
.toBeLessThan(15)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -256,13 +256,17 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
|
|||||||
).not.toBeVisible()
|
).not.toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
|
test.fixme(
|
||||||
|
'Basic default modeling and sketch hotkeys work',
|
||||||
|
async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
|
|
||||||
// This test can run long if it takes a little too long to load
|
// This test can run long if it takes a little too long to load
|
||||||
// the engine.
|
// the engine.
|
||||||
test.setTimeout(90000)
|
test.setTimeout(90000)
|
||||||
// This test has a weird bug on ubuntu
|
// This test has a weird bug on ubuntu
|
||||||
|
// Funny, it's flaking on Windows too :). I think there is just something
|
||||||
|
// actually wrong.
|
||||||
test.skip(
|
test.skip(
|
||||||
process.platform === 'linux',
|
process.platform === 'linux',
|
||||||
'weird playwright bug on ubuntu https://github.com/KittyCAD/modeling-app/issues/2444'
|
'weird playwright bug on ubuntu https://github.com/KittyCAD/modeling-app/issues/2444'
|
||||||
@ -430,7 +434,8 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
|
|||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
test('Delete key does not navigate back', async ({ page }) => {
|
test('Delete key does not navigate back', async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
@ -498,14 +503,16 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
|
|
||||||
let previousCodeContent = await page.locator('.cm-content').innerText()
|
let previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
await u.openAndClearDebugPanel()
|
const center = await u.getCenterOfModelViewArea()
|
||||||
|
|
||||||
|
// This basically waits for sketch mode to be ready.
|
||||||
await u.doAndWaitForCmd(
|
await u.doAndWaitForCmd(
|
||||||
() => page.mouse.click(625, 165),
|
async () => page.mouse.click(center.x, 180),
|
||||||
'default_camera_get_settings',
|
'default_camera_get_settings',
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
await page.waitForTimeout(150)
|
|
||||||
await u.closeDebugPanel()
|
await page.waitForTimeout(300)
|
||||||
|
|
||||||
const firstClickPosition = [612, 238]
|
const firstClickPosition = [612, 238]
|
||||||
const secondClickPosition = [661, 242]
|
const secondClickPosition = [661, 242]
|
||||||
|
1
interface.d.ts
vendored
@ -78,6 +78,7 @@ export interface IElectronAPI {
|
|||||||
) => Electron.IpcRenderer
|
) => Electron.IpcRenderer
|
||||||
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
|
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
|
||||||
appRestart: () => void
|
appRestart: () => void
|
||||||
|
getArgvParsed: () => any
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
25
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zoo-modeling-app",
|
"name": "zoo-modeling-app",
|
||||||
"version": "0.26.2",
|
"version": "0.26.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"productName": "Zoo Modeling App",
|
"productName": "Zoo Modeling App",
|
||||||
"author": {
|
"author": {
|
||||||
@ -14,7 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.17.0",
|
"@codemirror/autocomplete": "^6.17.0",
|
||||||
"@codemirror/commands": "^6.6.0",
|
"@codemirror/commands": "^6.6.0",
|
||||||
"@codemirror/language": "^6.10.2",
|
"@codemirror/language": "^6.10.3",
|
||||||
"@codemirror/lint": "^6.8.1",
|
"@codemirror/lint": "^6.8.1",
|
||||||
"@codemirror/search": "^6.5.6",
|
"@codemirror/search": "^6.5.6",
|
||||||
"@codemirror/state": "^6.4.1",
|
"@codemirror/state": "^6.4.1",
|
||||||
@ -40,7 +40,7 @@
|
|||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"electron-updater": "^6.3.0",
|
"electron-updater": "^6.3.9",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"html2canvas-pro": "^1.5.8",
|
"html2canvas-pro": "^1.5.8",
|
||||||
"isomorphic-fetch": "^3.0.0",
|
"isomorphic-fetch": "^3.0.0",
|
||||||
@ -60,12 +60,13 @@
|
|||||||
"sketch-helpers": "^0.0.4",
|
"sketch-helpers": "^0.0.4",
|
||||||
"three": "^0.166.1",
|
"three": "^0.166.1",
|
||||||
"ua-parser-js": "^1.0.37",
|
"ua-parser-js": "^1.0.37",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^11.0.2",
|
||||||
"vscode-jsonrpc": "^8.2.1",
|
"vscode-jsonrpc": "^8.2.1",
|
||||||
"vscode-languageserver-protocol": "^3.17.5",
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
"vscode-uri": "^3.0.8",
|
"vscode-uri": "^3.0.8",
|
||||||
"web-vitals": "^3.5.2",
|
"web-vitals": "^3.5.2",
|
||||||
"xstate": "^5.17.4"
|
"xstate": "^5.17.4",
|
||||||
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
@ -105,7 +106,8 @@
|
|||||||
"tronb:package": "electron-builder --config electron-builder.yml",
|
"tronb:package": "electron-builder --config electron-builder.yml",
|
||||||
"test-setup": "yarn install && yarn build:wasm",
|
"test-setup": "yarn install && yarn build:wasm",
|
||||||
"test": "vitest --mode development",
|
"test": "vitest --mode development",
|
||||||
"test:unit": "vitest run --mode development",
|
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts",
|
||||||
|
"test:unit:kcl-samples": "vitest run --mode development ./src/lang/kclSamples.test.ts",
|
||||||
"test:playwright:browser:chrome": "playwright test --project='Google Chrome' --config=playwright.ci.config.ts --grep-invert='@snapshot|@electron'",
|
"test:playwright:browser:chrome": "playwright test --project='Google Chrome' --config=playwright.ci.config.ts --grep-invert='@snapshot|@electron'",
|
||||||
"test:playwright:browser:chrome:windows": "playwright test --project=\"Google Chrome\" --config=playwright.ci.config.ts --grep-invert=\"@snapshot|@electron|@skipWin\"",
|
"test:playwright:browser:chrome:windows": "playwright test --project=\"Google Chrome\" --config=playwright.ci.config.ts --grep-invert=\"@snapshot|@electron|@skipWin\"",
|
||||||
"test:playwright:browser:chrome:ubuntu": "playwright test --project='Google Chrome' --config=playwright.ci.config.ts --grep-invert='@snapshot|@electron|@skipLinux'",
|
"test:playwright:browser:chrome:ubuntu": "playwright test --project='Google Chrome' --config=playwright.ci.config.ts --grep-invert='@snapshot|@electron|@skipLinux'",
|
||||||
@ -117,7 +119,8 @@
|
|||||||
"test:playwright:electron:windows:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep=@electron --grep-invert=@skipWin",
|
"test:playwright:electron:windows:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep=@electron --grep-invert=@skipWin",
|
||||||
"test:playwright:electron:macos:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep=@electron --grep-invert=@skipMacos",
|
"test:playwright:electron:macos:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep=@electron --grep-invert=@skipMacos",
|
||||||
"test:playwright:electron:ubuntu:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep=@electron --grep-invert=@skipLinux",
|
"test:playwright:electron:ubuntu:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep=@electron --grep-invert=@skipLinux",
|
||||||
"test:unit:local": "yarn simpleserver:bg && yarn test:unit; kill-port 3000"
|
"test:unit:local": "yarn simpleserver:bg && yarn test:unit; kill-port 3000",
|
||||||
|
"test:unit:kcl-samples:local": "yarn simpleserver:bg && yarn test:unit:kcl-samples; kill-port 3000"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
@ -144,8 +147,8 @@
|
|||||||
"@electron-forge/maker-deb": "^7.4.0",
|
"@electron-forge/maker-deb": "^7.4.0",
|
||||||
"@electron-forge/maker-rpm": "^7.4.0",
|
"@electron-forge/maker-rpm": "^7.4.0",
|
||||||
"@electron-forge/maker-squirrel": "^7.4.0",
|
"@electron-forge/maker-squirrel": "^7.4.0",
|
||||||
"@electron-forge/maker-wix": "^7.4.0",
|
"@electron-forge/maker-wix": "^7.5.0",
|
||||||
"@electron-forge/maker-zip": "^7.4.0",
|
"@electron-forge/maker-zip": "^7.5.0",
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
|
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
|
||||||
"@electron-forge/plugin-fuses": "^7.4.0",
|
"@electron-forge/plugin-fuses": "^7.4.0",
|
||||||
"@electron-forge/plugin-vite": "^7.4.0",
|
"@electron-forge/plugin-vite": "^7.4.0",
|
||||||
@ -171,7 +174,7 @@
|
|||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"@types/wicg-file-system-access": "^2023.10.5",
|
"@types/wicg-file-system-access": "^2023.10.5",
|
||||||
"@types/ws": "^8.5.10",
|
"@types/ws": "^8.5.13",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
"@vitejs/plugin-react": "^4.3.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
@ -187,7 +190,7 @@
|
|||||||
"eslint-plugin-css-modules": "^2.12.0",
|
"eslint-plugin-css-modules": "^2.12.0",
|
||||||
"eslint-plugin-import": "^2.30.0",
|
"eslint-plugin-import": "^2.30.0",
|
||||||
"eslint-plugin-suggest-no-throw": "^1.0.0",
|
"eslint-plugin-suggest-no-throw": "^1.0.0",
|
||||||
"happy-dom": "^14.3.10",
|
"happy-dom": "^15.10.2",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"husky": "^9.1.5",
|
"husky": "^9.1.5",
|
||||||
"kill-port": "^2.0.1",
|
"kill-port": "^2.0.1",
|
||||||
|
38
src/App.tsx
@ -1,15 +1,14 @@
|
|||||||
import { useEffect, useMemo, useRef } from 'react'
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||||
import { Stream } from './components/Stream'
|
|
||||||
import { AppHeader } from './components/AppHeader'
|
import { AppHeader } from './components/AppHeader'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { useLoaderData, useNavigate } from 'react-router-dom'
|
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { type IndexLoaderData } from 'lib/types'
|
import { type IndexLoaderData } from 'lib/types'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||||
import { codeManager, engineCommandManager } from 'lib/singletons'
|
import { codeManager, engineCommandManager, sceneInfra } from 'lib/singletons'
|
||||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { useLspContext } from 'components/LspProvider'
|
import { useLspContext } from 'components/LspProvider'
|
||||||
@ -22,6 +21,12 @@ import Gizmo from 'components/Gizmo'
|
|||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import { UnitsMenu } from 'components/UnitsMenu'
|
import { UnitsMenu } from 'components/UnitsMenu'
|
||||||
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
||||||
|
import EngineStreamContext from 'hooks/useEngineStreamContext'
|
||||||
|
import { EngineStream } from 'components/EngineStream'
|
||||||
|
import { maybeWriteToDisk } from 'lib/telemetry'
|
||||||
|
maybeWriteToDisk()
|
||||||
|
.then(() => {})
|
||||||
|
.catch(() => {})
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { project, file } = useLoaderData() as IndexLoaderData
|
const { project, file } = useLoaderData() as IndexLoaderData
|
||||||
@ -33,6 +38,13 @@ export function App() {
|
|||||||
// the coredump.
|
// the coredump.
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Stream related refs and data
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
const modelingSidebarRef = useRef<HTMLUListElement>(null)
|
||||||
|
let [searchParams] = useSearchParams()
|
||||||
|
const pool = searchParams.get('pool')
|
||||||
|
|
||||||
const projectName = project?.name || null
|
const projectName = project?.name || null
|
||||||
const projectPath = project?.path || null
|
const projectPath = project?.path || null
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -53,6 +65,10 @@ export function App() {
|
|||||||
app: { onboardingStatus },
|
app: { onboardingStatus },
|
||||||
} = settings.context
|
} = settings.context
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sceneInfra.camControls.modelingSidebarRef = modelingSidebarRef
|
||||||
|
}, [modelingSidebarRef.current])
|
||||||
|
|
||||||
useHotkeys('backspace', (e) => {
|
useHotkeys('backspace', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
})
|
})
|
||||||
@ -80,14 +96,26 @@ export function App() {
|
|||||||
enableMenu={true}
|
enableMenu={true}
|
||||||
/>
|
/>
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<ModelingSidebar paneOpacity={paneOpacity} />
|
<ModelingSidebar paneOpacity={paneOpacity} ref={modelingSidebarRef} />
|
||||||
<Stream />
|
<EngineStreamContext.Provider
|
||||||
|
options={{
|
||||||
|
input: {
|
||||||
|
videoRef,
|
||||||
|
canvasRef,
|
||||||
|
mediaStream: null,
|
||||||
|
authToken: auth?.context?.token ?? null,
|
||||||
|
pool,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EngineStream />
|
||||||
{/* <CamToggle /> */}
|
{/* <CamToggle /> */}
|
||||||
<LowerRightControls coreDumpManager={coreDumpManager}>
|
<LowerRightControls coreDumpManager={coreDumpManager}>
|
||||||
<UnitsMenu />
|
<UnitsMenu />
|
||||||
<Gizmo />
|
<Gizmo />
|
||||||
<CameraProjectionToggle />
|
<CameraProjectionToggle />
|
||||||
</LowerRightControls>
|
</LowerRightControls>
|
||||||
|
</EngineStreamContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import { ErrorPage } from './components/ErrorPage'
|
import { ErrorPage } from './components/ErrorPage'
|
||||||
import { Settings } from './routes/Settings'
|
import { Settings } from './routes/Settings'
|
||||||
|
import { Telemetry } from './routes/Telemetry'
|
||||||
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
|
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
|
||||||
import SignIn from './routes/SignIn'
|
import SignIn from './routes/SignIn'
|
||||||
import { Auth } from './Auth'
|
import { Auth } from './Auth'
|
||||||
@ -28,6 +29,7 @@ import {
|
|||||||
homeLoader,
|
homeLoader,
|
||||||
onboardingRedirectLoader,
|
onboardingRedirectLoader,
|
||||||
settingsLoader,
|
settingsLoader,
|
||||||
|
telemetryLoader,
|
||||||
} from 'lib/routeLoaders'
|
} from 'lib/routeLoaders'
|
||||||
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
|
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
|
||||||
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
||||||
@ -43,6 +45,7 @@ import { coreDump } from 'lang/wasm'
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { AppStateProvider } from 'AppState'
|
import { AppStateProvider } from 'AppState'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { RouteProvider } from 'components/RouteProvider'
|
||||||
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
||||||
|
|
||||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||||
@ -56,6 +59,7 @@ const router = createRouter([
|
|||||||
* inefficient re-renders, use the react profiler to see. */
|
* inefficient re-renders, use the react profiler to see. */
|
||||||
element: (
|
element: (
|
||||||
<CommandBarProvider>
|
<CommandBarProvider>
|
||||||
|
<RouteProvider>
|
||||||
<SettingsAuthProvider>
|
<SettingsAuthProvider>
|
||||||
<LspProvider>
|
<LspProvider>
|
||||||
<ProjectsContextProvider>
|
<ProjectsContextProvider>
|
||||||
@ -69,6 +73,7 @@ const router = createRouter([
|
|||||||
</ProjectsContextProvider>
|
</ProjectsContextProvider>
|
||||||
</LspProvider>
|
</LspProvider>
|
||||||
</SettingsAuthProvider>
|
</SettingsAuthProvider>
|
||||||
|
</RouteProvider>
|
||||||
</CommandBarProvider>
|
</CommandBarProvider>
|
||||||
),
|
),
|
||||||
errorElement: <ErrorPage />,
|
errorElement: <ErrorPage />,
|
||||||
@ -124,6 +129,16 @@ const router = createRouter([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: PATHS.FILE + 'TELEMETRY',
|
||||||
|
loader: telemetryLoader,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: makeUrlPathRelative(PATHS.TELEMETRY),
|
||||||
|
element: <Telemetry />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -149,6 +164,11 @@ const router = createRouter([
|
|||||||
loader: settingsLoader,
|
loader: settingsLoader,
|
||||||
element: <Settings />,
|
element: <Settings />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: makeUrlPathRelative(PATHS.TELEMETRY),
|
||||||
|
loader: telemetryLoader,
|
||||||
|
element: <Telemetry />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
} from 'lib/toolbar'
|
} from 'lib/toolbar'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
|
import { EngineConnectionStateType } from 'lang/std/engineConnection'
|
||||||
|
|
||||||
export function Toolbar({
|
export function Toolbar({
|
||||||
className = '',
|
className = '',
|
||||||
@ -48,7 +49,7 @@ export function Toolbar({
|
|||||||
}, [engineCommandManager.artifactGraph, context.selectionRanges])
|
}, [engineCommandManager.artifactGraph, context.selectionRanges])
|
||||||
|
|
||||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||||
const { overallState } = useNetworkContext()
|
const { overallState, immediateState } = useNetworkContext()
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { isStreamReady } = useAppState()
|
const { isStreamReady } = useAppState()
|
||||||
|
|
||||||
@ -56,6 +57,7 @@ export function Toolbar({
|
|||||||
(overallState !== NetworkHealthState.Ok &&
|
(overallState !== NetworkHealthState.Ok &&
|
||||||
overallState !== NetworkHealthState.Weak) ||
|
overallState !== NetworkHealthState.Weak) ||
|
||||||
isExecuting ||
|
isExecuting ||
|
||||||
|
immediateState.type !== EngineConnectionStateType.ConnectionEstablished ||
|
||||||
!isStreamReady
|
!isStreamReady
|
||||||
|
|
||||||
const currentMode =
|
const currentMode =
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Models } from '@kittycad/lib'
|
||||||
|
import { MutableRefObject } from 'react'
|
||||||
import { cameraMouseDragGuards, MouseGuard } from 'lib/cameraControls'
|
import { cameraMouseDragGuards, MouseGuard } from 'lib/cameraControls'
|
||||||
import {
|
import {
|
||||||
Euler,
|
Euler,
|
||||||
@ -87,6 +89,9 @@ class CameraRateLimiter {
|
|||||||
|
|
||||||
export class CameraControls {
|
export class CameraControls {
|
||||||
engineCommandManager: EngineCommandManager
|
engineCommandManager: EngineCommandManager
|
||||||
|
modelingSidebarRef: MutableRefObject<HTMLUListElement | null> = {
|
||||||
|
current: null,
|
||||||
|
}
|
||||||
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
|
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
|
||||||
camera: PerspectiveCamera | OrthographicCamera
|
camera: PerspectiveCamera | OrthographicCamera
|
||||||
target: Vector3
|
target: Vector3
|
||||||
@ -95,6 +100,13 @@ export class CameraControls {
|
|||||||
wasDragging: boolean
|
wasDragging: boolean
|
||||||
mouseDownPosition: Vector2
|
mouseDownPosition: Vector2
|
||||||
mouseNewPosition: Vector2
|
mouseNewPosition: Vector2
|
||||||
|
cameraDragStartXY = new Vector2()
|
||||||
|
old:
|
||||||
|
| {
|
||||||
|
camera: PerspectiveCamera | OrthographicCamera
|
||||||
|
target: Vector3
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
rotationSpeed = 0.3
|
rotationSpeed = 0.3
|
||||||
enableRotate = true
|
enableRotate = true
|
||||||
enablePan = true
|
enablePan = true
|
||||||
@ -461,6 +473,7 @@ export class CameraControls {
|
|||||||
if (this.syncDirection === 'engineToClient') {
|
if (this.syncDirection === 'engineToClient') {
|
||||||
const interaction = this.getInteractionType(event)
|
const interaction = this.getInteractionType(event)
|
||||||
if (interaction === 'none') return
|
if (interaction === 'none') return
|
||||||
|
|
||||||
void this.engineCommandManager.sendSceneCommand({
|
void this.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -909,18 +922,123 @@ export class CameraControls {
|
|||||||
up: { x: 0, y: 0, z: 1 },
|
up: { x: 0, y: 0, z: 1 },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await this.engineCommandManager.sendSceneCommand({
|
|
||||||
|
await this.centerModelRelativeToPanes({
|
||||||
|
zoomToFit: true,
|
||||||
|
resetLastPaneWidth: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.cameraDragStartXY = new Vector2()
|
||||||
|
this.cameraDragStartXY.x = 0
|
||||||
|
this.cameraDragStartXY.y = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreCameraPosition(): Promise<void> {
|
||||||
|
if (!this.old) return
|
||||||
|
|
||||||
|
this.camera = this.old.camera.clone()
|
||||||
|
this.target = this.old.target.clone()
|
||||||
|
|
||||||
|
void this.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'zoom_to_fit',
|
type: 'default_camera_look_at',
|
||||||
object_ids: [], // leave empty to zoom to all objects
|
...convertThreeCamValuesToEngineCam({
|
||||||
padding: 0.2, // padding around the objects
|
isPerspective: true,
|
||||||
animated: false, // don't animate the zoom for now
|
position: this.camera.position,
|
||||||
|
quaternion: this.camera.quaternion,
|
||||||
|
zoom: this.camera.zoom,
|
||||||
|
target: this.target,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private lastFramePaneWidth: number = 0
|
||||||
|
|
||||||
|
async centerModelRelativeToPanes(args?: {
|
||||||
|
zoomObjectId?: string
|
||||||
|
zoomToFit?: boolean
|
||||||
|
resetLastPaneWidth?: boolean
|
||||||
|
}): Promise<void> {
|
||||||
|
const panes = this.modelingSidebarRef?.current
|
||||||
|
if (!panes) return
|
||||||
|
|
||||||
|
const panesWidth = panes.offsetWidth + panes.offsetLeft
|
||||||
|
|
||||||
|
if (args?.resetLastPaneWidth) {
|
||||||
|
this.lastFramePaneWidth = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const goPx =
|
||||||
|
(panesWidth - this.lastFramePaneWidth) / 2 / window.devicePixelRatio
|
||||||
|
this.lastFramePaneWidth = panesWidth
|
||||||
|
|
||||||
|
// Originally I had tried to use the default_camera_look_at endpoint and
|
||||||
|
// some quaternion math to move the camera right, but it ended up being
|
||||||
|
// overly complicated, and I think the threejs scene also doesn't have the
|
||||||
|
// camera coordinates after a zoom-to-fit... So this is much easier, and
|
||||||
|
// maps better to screen coordinates.
|
||||||
|
|
||||||
|
const requests: Models['ModelingCmdReq_type'][] = [
|
||||||
|
{
|
||||||
|
cmd: {
|
||||||
|
type: 'camera_drag_start',
|
||||||
|
interaction: 'pan',
|
||||||
|
window: { x: goPx < 0 ? -goPx : 0, y: 0 },
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: {
|
||||||
|
type: 'camera_drag_move',
|
||||||
|
interaction: 'pan',
|
||||||
|
window: {
|
||||||
|
x: goPx < 0 ? 0 : goPx,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (args?.zoomToFit) {
|
||||||
|
requests.unshift({
|
||||||
|
cmd: {
|
||||||
|
type: 'zoom_to_fit',
|
||||||
|
object_ids: args?.zoomObjectId ? [args?.zoomObjectId] : [], // leave empty to zoom to all objects
|
||||||
|
padding: 0.2, // padding around the objects
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.engineCommandManager
|
||||||
|
.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_batch_req',
|
||||||
|
batch_id: uuidv4(),
|
||||||
|
responses: true,
|
||||||
|
requests,
|
||||||
|
})
|
||||||
|
// engineCommandManager can't subscribe to batch responses so we'll send
|
||||||
|
// this one off by its lonesome after.
|
||||||
|
.then(() =>
|
||||||
|
this.engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'camera_drag_end',
|
||||||
|
interaction: 'pan',
|
||||||
|
window: {
|
||||||
|
x: goPx < 0 ? 0 : goPx,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async tweenCameraToQuaternion(
|
async tweenCameraToQuaternion(
|
||||||
targetQuaternion: Quaternion,
|
targetQuaternion: Quaternion,
|
||||||
targetPosition = new Vector3(),
|
targetPosition = new Vector3(),
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
import { useRef, useEffect, useState, useMemo, Fragment } from 'react'
|
import {
|
||||||
|
CSSProperties,
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
Fragment,
|
||||||
|
} from 'react'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
|
||||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
@ -202,12 +209,20 @@ const Overlay = ({
|
|||||||
let xAlignment = overlay.angle < 0 ? '0%' : '-100%'
|
let xAlignment = overlay.angle < 0 ? '0%' : '-100%'
|
||||||
let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%'
|
let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%'
|
||||||
|
|
||||||
|
// It's possible for the pathToNode to request a newer AST node
|
||||||
|
// than what's available in the AST at the moment of query.
|
||||||
|
// It eventually settles on being updated.
|
||||||
const _node1 = getNodeFromPath<Node<CallExpression>>(
|
const _node1 = getNodeFromPath<Node<CallExpression>>(
|
||||||
kclManager.ast,
|
kclManager.ast,
|
||||||
overlay.pathToNode,
|
overlay.pathToNode,
|
||||||
'CallExpression'
|
'CallExpression'
|
||||||
)
|
)
|
||||||
if (err(_node1)) return
|
|
||||||
|
// For that reason, to prevent console noise, we do not use err here.
|
||||||
|
if (_node1 instanceof Error) {
|
||||||
|
console.warn('ast older than pathToNode, not fatal, eventually settles', '')
|
||||||
|
return
|
||||||
|
}
|
||||||
const callExpression = _node1.node
|
const callExpression = _node1.node
|
||||||
|
|
||||||
const constraints = getConstraintInfo(
|
const constraints = getConstraintInfo(
|
||||||
@ -234,6 +249,13 @@ const Overlay = ({
|
|||||||
state.matches({ Sketch: 'Rectangle tool' })
|
state.matches({ Sketch: 'Rectangle tool' })
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Line labels will cover the constraints overlay if this is not used.
|
||||||
|
// For each line label, ThreeJS increments each CSS2DObject z-index as they
|
||||||
|
// are added. I have looked into overriding renderOrder and depthTest and
|
||||||
|
// while renderOrder is set, ThreeJS still sets z-index on these 2D objects.
|
||||||
|
// It is easier to set this to a large number, such as a billion.
|
||||||
|
const zIndex = 1000000000
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`absolute w-0 h-0`}>
|
<div className={`absolute w-0 h-0`}>
|
||||||
<div
|
<div
|
||||||
@ -244,6 +266,7 @@ const Overlay = ({
|
|||||||
data-overlay-angle={overlay.angle}
|
data-overlay-angle={overlay.angle}
|
||||||
className="pointer-events-auto absolute w-0 h-0"
|
className="pointer-events-auto absolute w-0 h-0"
|
||||||
style={{
|
style={{
|
||||||
|
zIndex,
|
||||||
transform: `translate3d(${overlay.windowCoords[0]}px, ${overlay.windowCoords[1]}px, 0)`,
|
transform: `translate3d(${overlay.windowCoords[0]}px, ${overlay.windowCoords[1]}px, 0)`,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
@ -252,6 +275,7 @@ const Overlay = ({
|
|||||||
data-overlay-toolbar-index={overlayIndex}
|
data-overlay-toolbar-index={overlayIndex}
|
||||||
className={`px-0 pointer-events-auto absolute flex gap-1`}
|
className={`px-0 pointer-events-auto absolute flex gap-1`}
|
||||||
style={{
|
style={{
|
||||||
|
zIndex,
|
||||||
transform: `translate3d(calc(${
|
transform: `translate3d(calc(${
|
||||||
overlay.windowCoords[0] + xOffset
|
overlay.windowCoords[0] + xOffset
|
||||||
}px + ${xAlignment}), calc(${
|
}px + ${xAlignment}), calc(${
|
||||||
@ -293,6 +317,7 @@ const Overlay = ({
|
|||||||
*/}
|
*/}
|
||||||
{callExpression?.callee?.name !== 'circle' && (
|
{callExpression?.callee?.name !== 'circle' && (
|
||||||
<SegmentMenu
|
<SegmentMenu
|
||||||
|
style={{ zIndex }}
|
||||||
verticalPosition={
|
verticalPosition={
|
||||||
overlay.windowCoords[1] > window.innerHeight / 2
|
overlay.windowCoords[1] > window.innerHeight / 2
|
||||||
? 'top'
|
? 'top'
|
||||||
@ -434,15 +459,17 @@ const SegmentMenu = ({
|
|||||||
verticalPosition,
|
verticalPosition,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
stdLibFnName,
|
stdLibFnName,
|
||||||
|
style,
|
||||||
}: {
|
}: {
|
||||||
verticalPosition: 'top' | 'bottom'
|
verticalPosition: 'top' | 'bottom'
|
||||||
pathToNode: PathToNode
|
pathToNode: PathToNode
|
||||||
stdLibFnName: string
|
stdLibFnName: string
|
||||||
|
style?: CSSProperties
|
||||||
}) => {
|
}) => {
|
||||||
const { send } = useModelingContext()
|
const { send } = useModelingContext()
|
||||||
const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode)
|
const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode)
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover style={style} className="relative">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
@ -637,10 +664,16 @@ const ConstraintSymbol = ({
|
|||||||
kclManager.ast,
|
kclManager.ast,
|
||||||
kclManager.programMemory
|
kclManager.programMemory
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!transform) return
|
if (!transform) return
|
||||||
const { modifiedAst } = transform
|
const { modifiedAst } = transform
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
kclManager.updateAst(modifiedAst, true)
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
|
||||||
|
// Code editor will be updated in the modelingMachine.
|
||||||
|
const newCode = recast(modifiedAst)
|
||||||
|
if (err(newCode)) return
|
||||||
|
await codeManager.updateCodeEditor(newCode)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('error', e)
|
console.log('error', e)
|
||||||
}
|
}
|
||||||
|
22
src/clientSideScene/constants.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// 63.5 is definitely a bit of a magic number, play with it until it looked right
|
||||||
|
// if it were 64, that would feel like it's something in the engine where a random
|
||||||
|
// power of 2 is used, but it's the 0.5 seems to make things look much more correct
|
||||||
|
export const ZOOM_MAGIC_NUMBER = 63.5
|
||||||
|
|
||||||
|
export const INTERSECTION_PLANE_LAYER = 1
|
||||||
|
export const SKETCH_LAYER = 2
|
||||||
|
|
||||||
|
export const RAYCASTABLE_PLANE = 'raycastable-plane'
|
||||||
|
|
||||||
|
// redundant types so that it can be changed temporarily but CI will catch the wrong type
|
||||||
|
export const DEBUG_SHOW_INTERSECTION_PLANE: false = false
|
||||||
|
export const DEBUG_SHOW_BOTH_SCENES: false = false
|
||||||
|
|
||||||
|
export const X_AXIS = 'xAxis'
|
||||||
|
export const Y_AXIS = 'yAxis'
|
||||||
|
export const AXIS_GROUP = 'axisGroup'
|
||||||
|
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
|
||||||
|
export const ARROWHEAD = 'arrowhead'
|
||||||
|
export const SEGMENT_LENGTH_LABEL = 'segment-length-label'
|
||||||
|
export const SEGMENT_LENGTH_LABEL_TEXT = 'segment-length-label-text'
|
||||||
|
export const SEGMENT_LENGTH_LABEL_OFFSET_PX = 30
|
@ -2,10 +2,7 @@ import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
|||||||
import {
|
import {
|
||||||
GridHelper,
|
GridHelper,
|
||||||
LineBasicMaterial,
|
LineBasicMaterial,
|
||||||
OrthographicCamera,
|
|
||||||
PerspectiveCamera,
|
PerspectiveCamera,
|
||||||
Group,
|
|
||||||
Mesh,
|
|
||||||
Quaternion,
|
Quaternion,
|
||||||
Vector3,
|
Vector3,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
@ -28,15 +25,9 @@ export function createGridHelper({
|
|||||||
gridHelper.rotation.x = Math.PI / 2
|
gridHelper.rotation.x = Math.PI / 2
|
||||||
return gridHelper
|
return gridHelper
|
||||||
}
|
}
|
||||||
const fudgeFactor = 72.66985970437086
|
|
||||||
|
|
||||||
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
|
// Re-export scale.ts
|
||||||
(0.55 * fudgeFactor) / cam.zoom / window.innerHeight
|
export * from './scale'
|
||||||
|
|
||||||
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
|
|
||||||
(group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) /
|
|
||||||
4000 /
|
|
||||||
window.innerHeight
|
|
||||||
|
|
||||||
export function isQuaternionVertical(q: Quaternion) {
|
export function isQuaternionVertical(q: Quaternion) {
|
||||||
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
||||||
|
17
src/clientSideScene/scale.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { OrthographicCamera, PerspectiveCamera, Group, Mesh } from 'three'
|
||||||
|
|
||||||
|
export const fudgeFactor = 72.66985970437086
|
||||||
|
|
||||||
|
export const orthoScale = (
|
||||||
|
cam: OrthographicCamera | PerspectiveCamera,
|
||||||
|
innerHeight?: number
|
||||||
|
) => (0.55 * fudgeFactor) / cam.zoom / (innerHeight ?? window.innerHeight)
|
||||||
|
|
||||||
|
export const perspScale = (
|
||||||
|
cam: PerspectiveCamera,
|
||||||
|
group: Group | Mesh,
|
||||||
|
innerHeight?: number
|
||||||
|
) =>
|
||||||
|
(group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) /
|
||||||
|
4000 /
|
||||||
|
(innerHeight ?? window.innerHeight)
|
@ -17,6 +17,7 @@ import {
|
|||||||
Vector3,
|
Vector3,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
import {
|
import {
|
||||||
|
ANGLE_SNAP_THRESHOLD_DEGREES,
|
||||||
ARROWHEAD,
|
ARROWHEAD,
|
||||||
AXIS_GROUP,
|
AXIS_GROUP,
|
||||||
DRAFT_POINT,
|
DRAFT_POINT,
|
||||||
@ -95,6 +96,7 @@ import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
|||||||
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
||||||
import { SegmentInputs } from 'lang/std/stdTypes'
|
import { SegmentInputs } from 'lang/std/stdTypes'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
import { radToDeg } from 'three/src/math/MathUtils'
|
||||||
|
|
||||||
type DraftSegment = 'line' | 'tangentialArcTo'
|
type DraftSegment = 'line' | 'tangentialArcTo'
|
||||||
|
|
||||||
@ -451,6 +453,7 @@ export class SceneEntities {
|
|||||||
const { modifiedAst } = addStartProfileAtRes
|
const { modifiedAst } = addStartProfileAtRes
|
||||||
|
|
||||||
await kclManager.updateAst(modifiedAst, false)
|
await kclManager.updateAst(modifiedAst, false)
|
||||||
|
|
||||||
this.removeIntersectionPlane()
|
this.removeIntersectionPlane()
|
||||||
this.scene.remove(draftPointGroup)
|
this.scene.remove(draftPointGroup)
|
||||||
|
|
||||||
@ -683,7 +686,7 @@ export class SceneEntities {
|
|||||||
})
|
})
|
||||||
return nextAst
|
return nextAst
|
||||||
}
|
}
|
||||||
setUpDraftSegment = async (
|
setupDraftSegment = async (
|
||||||
sketchPathToNode: PathToNode,
|
sketchPathToNode: PathToNode,
|
||||||
forward: [number, number, number],
|
forward: [number, number, number],
|
||||||
up: [number, number, number],
|
up: [number, number, number],
|
||||||
@ -798,11 +801,24 @@ export class SceneEntities {
|
|||||||
(sceneObject) => sceneObject.object.name === X_AXIS
|
(sceneObject) => sceneObject.object.name === X_AXIS
|
||||||
)
|
)
|
||||||
|
|
||||||
const lastSegment = sketch.paths.slice(-1)[0]
|
const lastSegment = sketch.paths.slice(-1)[0] || sketch.start
|
||||||
const snappedPoint = {
|
const snappedPoint = {
|
||||||
x: intersectsYAxis ? 0 : intersection2d.x,
|
x: intersectsYAxis ? 0 : intersection2d.x,
|
||||||
y: intersectsXAxis ? 0 : intersection2d.y,
|
y: intersectsXAxis ? 0 : intersection2d.y,
|
||||||
}
|
}
|
||||||
|
// Get the angle between the previous segment (or sketch start)'s end and this one's
|
||||||
|
const angle = Math.atan2(
|
||||||
|
snappedPoint.y - lastSegment.to[1],
|
||||||
|
snappedPoint.x - lastSegment.to[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
const isHorizontal =
|
||||||
|
radToDeg(Math.abs(angle)) < ANGLE_SNAP_THRESHOLD_DEGREES ||
|
||||||
|
Math.abs(radToDeg(Math.abs(angle) - Math.PI)) <
|
||||||
|
ANGLE_SNAP_THRESHOLD_DEGREES
|
||||||
|
const isVertical =
|
||||||
|
Math.abs(radToDeg(Math.abs(angle) - Math.PI / 2)) <
|
||||||
|
ANGLE_SNAP_THRESHOLD_DEGREES
|
||||||
|
|
||||||
let resolvedFunctionName: ToolTip = 'line'
|
let resolvedFunctionName: ToolTip = 'line'
|
||||||
|
|
||||||
@ -810,6 +826,12 @@ export class SceneEntities {
|
|||||||
// case-based logic for different segment types
|
// case-based logic for different segment types
|
||||||
if (lastSegment.type === 'TangentialArcTo') {
|
if (lastSegment.type === 'TangentialArcTo') {
|
||||||
resolvedFunctionName = 'tangentialArcTo'
|
resolvedFunctionName = 'tangentialArcTo'
|
||||||
|
} else if (isHorizontal) {
|
||||||
|
// If the angle between is 0 or 180 degrees (+/- the snapping angle), make the line an xLine
|
||||||
|
resolvedFunctionName = 'xLine'
|
||||||
|
} else if (isVertical) {
|
||||||
|
// If the angle between is 90 or 270 degrees (+/- the snapping angle), make the line a yLine
|
||||||
|
resolvedFunctionName = 'yLine'
|
||||||
} else if (snappedPoint.x === 0 || snappedPoint.y === 0) {
|
} else if (snappedPoint.x === 0 || snappedPoint.y === 0) {
|
||||||
// We consider a point placed on axes or origin to be absolute
|
// We consider a point placed on axes or origin to be absolute
|
||||||
resolvedFunctionName = 'lineTo'
|
resolvedFunctionName = 'lineTo'
|
||||||
@ -835,10 +857,11 @@ export class SceneEntities {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await kclManager.executeAstMock(modifiedAst)
|
await kclManager.executeAstMock(modifiedAst)
|
||||||
|
|
||||||
if (intersectsProfileStart) {
|
if (intersectsProfileStart) {
|
||||||
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
sceneInfra.modelingSend({ type: 'CancelSketch' })
|
||||||
} else {
|
} else {
|
||||||
await this.setUpDraftSegment(
|
await this.setupDraftSegment(
|
||||||
sketchPathToNode,
|
sketchPathToNode,
|
||||||
forward,
|
forward,
|
||||||
up,
|
up,
|
||||||
@ -846,6 +869,8 @@ export class SceneEntities {
|
|||||||
segmentName
|
segmentName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
|
||||||
},
|
},
|
||||||
onMove: (args) => {
|
onMove: (args) => {
|
||||||
this.onDragSegment({
|
this.onDragSegment({
|
||||||
@ -970,10 +995,14 @@ export class SceneEntities {
|
|||||||
if (trap(_node)) return
|
if (trap(_node)) return
|
||||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
const sketchInit = _node.node?.declarations?.[0]?.init
|
||||||
|
|
||||||
if (sketchInit.type === 'PipeExpression') {
|
if (sketchInit.type !== 'PipeExpression') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
updateRectangleSketch(sketchInit, x, y, tags[0])
|
updateRectangleSketch(sketchInit, x, y, tags[0])
|
||||||
|
|
||||||
let _recastAst = parse(recast(_ast))
|
const newCode = recast(_ast)
|
||||||
|
let _recastAst = parse(newCode)
|
||||||
if (trap(_recastAst)) return
|
if (trap(_recastAst)) return
|
||||||
_ast = _recastAst
|
_ast = _recastAst
|
||||||
|
|
||||||
@ -981,6 +1010,11 @@ export class SceneEntities {
|
|||||||
await kclManager.executeAstMock(_ast)
|
await kclManager.executeAstMock(_ast)
|
||||||
sceneInfra.modelingSend({ type: 'Finish rectangle' })
|
sceneInfra.modelingSend({ type: 'Finish rectangle' })
|
||||||
|
|
||||||
|
// lee: I had this at the bottom of the function, but it's
|
||||||
|
// possible sketchFromKclValue "fails" when sketching on a face,
|
||||||
|
// and this couldn't wouldn't run.
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
|
||||||
|
|
||||||
const { execState } = await executeAst({
|
const { execState } = await executeAst({
|
||||||
ast: _ast,
|
ast: _ast,
|
||||||
useFakeExecutor: true,
|
useFakeExecutor: true,
|
||||||
@ -1006,7 +1040,6 @@ export class SceneEntities {
|
|||||||
sgPaths.forEach((seg, index) =>
|
sgPaths.forEach((seg, index) =>
|
||||||
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
|
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1166,13 +1199,17 @@ export class SceneEntities {
|
|||||||
if (err(moddedResult)) return
|
if (err(moddedResult)) return
|
||||||
modded = moddedResult.modifiedAst
|
modded = moddedResult.modifiedAst
|
||||||
|
|
||||||
let _recastAst = parse(recast(modded))
|
const newCode = recast(modded)
|
||||||
|
if (err(newCode)) return
|
||||||
|
let _recastAst = parse(newCode)
|
||||||
if (trap(_recastAst)) return Promise.reject(_recastAst)
|
if (trap(_recastAst)) return Promise.reject(_recastAst)
|
||||||
_ast = _recastAst
|
_ast = _recastAst
|
||||||
|
|
||||||
// Update the primary AST and unequip the rectangle tool
|
// Update the primary AST and unequip the rectangle tool
|
||||||
await kclManager.executeAstMock(_ast)
|
await kclManager.executeAstMock(_ast)
|
||||||
sceneInfra.modelingSend({ type: 'Finish circle' })
|
sceneInfra.modelingSend({ type: 'Finish circle' })
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -1208,6 +1245,7 @@ export class SceneEntities {
|
|||||||
forward,
|
forward,
|
||||||
position,
|
position,
|
||||||
})
|
})
|
||||||
|
await codeManager.writeToFile()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDrag: async ({
|
onDrag: async ({
|
||||||
|
@ -50,6 +50,8 @@ export const RAYCASTABLE_PLANE = 'raycastable-plane'
|
|||||||
|
|
||||||
export const X_AXIS = 'xAxis'
|
export const X_AXIS = 'xAxis'
|
||||||
export const Y_AXIS = 'yAxis'
|
export const Y_AXIS = 'yAxis'
|
||||||
|
/** If a segment angle is less than this many degrees off a meanginful angle it'll snap to it */
|
||||||
|
export const ANGLE_SNAP_THRESHOLD_DEGREES = 3
|
||||||
/** the THREEjs representation of the group surrounding a "snapped" point that is not yet placed */
|
/** the THREEjs representation of the group surrounding a "snapped" point that is not yet placed */
|
||||||
export const DRAFT_POINT_GROUP = 'draft-point-group'
|
export const DRAFT_POINT_GROUP = 'draft-point-group'
|
||||||
/** the THREEjs representation of a "snapped" point that is not yet placed */
|
/** the THREEjs representation of a "snapped" point that is not yet placed */
|
||||||
@ -289,14 +291,14 @@ export class SceneInfra {
|
|||||||
engineCommandManager
|
engineCommandManager
|
||||||
)
|
)
|
||||||
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
||||||
this.camControls.camera.layers.enable(SKETCH_LAYER)
|
this.camControls.camera.layers.enable(constants.SKETCH_LAYER)
|
||||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
if (constants.DEBUG_SHOW_INTERSECTION_PLANE)
|
||||||
this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
this.camControls.camera.layers.enable(constants.INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
// RAYCASTERS
|
// RAYCASTERS
|
||||||
this.raycaster.layers.enable(SKETCH_LAYER)
|
this.raycaster.layers.enable(constants.SKETCH_LAYER)
|
||||||
this.raycaster.layers.disable(0)
|
this.raycaster.layers.disable(0)
|
||||||
this.planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
|
this.planeRaycaster.layers.enable(constants.INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
// GRID
|
// GRID
|
||||||
const size = 100
|
const size = 100
|
||||||
@ -331,7 +333,7 @@ export class SceneInfra {
|
|||||||
this.camControls.target
|
this.camControls.target
|
||||||
)
|
)
|
||||||
const axisGroup = this.scene
|
const axisGroup = this.scene
|
||||||
.getObjectByName(AXIS_GROUP)
|
.getObjectByName(constants.AXIS_GROUP)
|
||||||
?.getObjectByName('gridHelper')
|
?.getObjectByName('gridHelper')
|
||||||
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
||||||
}
|
}
|
||||||
@ -342,7 +344,6 @@ export class SceneInfra {
|
|||||||
}
|
}
|
||||||
|
|
||||||
animate = () => {
|
animate = () => {
|
||||||
requestAnimationFrame(this.animate)
|
|
||||||
TWEEN.update() // This will update all tweens during the animation loop
|
TWEEN.update() // This will update all tweens during the animation loop
|
||||||
if (!this.isFovAnimationInProgress) {
|
if (!this.isFovAnimationInProgress) {
|
||||||
// console.log('animation frame', this.cameraControls.camera)
|
// console.log('animation frame', this.cameraControls.camera)
|
||||||
@ -350,6 +351,7 @@ export class SceneInfra {
|
|||||||
this.renderer.render(this.scene, this.camControls.camera)
|
this.renderer.render(this.scene, this.camControls.camera)
|
||||||
this.labelRenderer.render(this.scene, this.camControls.camera)
|
this.labelRenderer.render(this.scene, this.camControls.camera)
|
||||||
}
|
}
|
||||||
|
requestAnimationFrame(this.animate)
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose = () => {
|
dispose = () => {
|
||||||
@ -653,11 +655,11 @@ export class SceneInfra {
|
|||||||
}
|
}
|
||||||
updateOtherSelectionColors = (otherSelections: Axis[]) => {
|
updateOtherSelectionColors = (otherSelections: Axis[]) => {
|
||||||
const axisGroup = this.scene.children.find(
|
const axisGroup = this.scene.children.find(
|
||||||
({ userData }) => userData?.type === AXIS_GROUP
|
({ userData }) => userData?.type === constants.AXIS_GROUP
|
||||||
)
|
)
|
||||||
const axisMap: { [key: string]: Axis } = {
|
const axisMap: { [key: string]: Axis } = {
|
||||||
[X_AXIS]: 'x-axis',
|
[constants.X_AXIS]: 'x-axis',
|
||||||
[Y_AXIS]: 'y-axis',
|
[constants.Y_AXIS]: 'y-axis',
|
||||||
}
|
}
|
||||||
axisGroup?.children.forEach((_mesh) => {
|
axisGroup?.children.forEach((_mesh) => {
|
||||||
const mesh = _mesh as Mesh
|
const mesh = _mesh as Mesh
|
||||||
|
@ -300,7 +300,7 @@ class StraightSegment implements SegmentUtils {
|
|||||||
sceneInfra.updateOverlayDetails({
|
sceneInfra.updateOverlayDetails({
|
||||||
arrowGroup,
|
arrowGroup,
|
||||||
group,
|
group,
|
||||||
isHandlesVisible,
|
isHandlesVisible: true,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
})
|
})
|
||||||
@ -476,7 +476,7 @@ class TangentialArcToSegment implements SegmentUtils {
|
|||||||
sceneInfra.updateOverlayDetails({
|
sceneInfra.updateOverlayDetails({
|
||||||
arrowGroup,
|
arrowGroup,
|
||||||
group,
|
group,
|
||||||
isHandlesVisible,
|
isHandlesVisible: true,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
angle,
|
angle,
|
||||||
@ -542,7 +542,7 @@ class CircleSegment implements SegmentUtils {
|
|||||||
}
|
}
|
||||||
group.name = CIRCLE_SEGMENT
|
group.name = CIRCLE_SEGMENT
|
||||||
|
|
||||||
group.add(arcMesh, arrowGroup, circleCenterGroup, radiusIndicatorGroup)
|
group.add(arcMesh, arrowGroup, circleCenterGroup)
|
||||||
const updateOverlaysCallback = this.update({
|
const updateOverlaysCallback = this.update({
|
||||||
prevSegment,
|
prevSegment,
|
||||||
input,
|
input,
|
||||||
@ -677,7 +677,7 @@ class CircleSegment implements SegmentUtils {
|
|||||||
sceneInfra.updateOverlayDetails({
|
sceneInfra.updateOverlayDetails({
|
||||||
arrowGroup,
|
arrowGroup,
|
||||||
group,
|
group,
|
||||||
isHandlesVisible,
|
isHandlesVisible: true,
|
||||||
from: from,
|
from: from,
|
||||||
to: [center[0], center[1]],
|
to: [center[0], center[1]],
|
||||||
angle: Math.PI / 4,
|
angle: Math.PI / 4,
|
||||||
|
12
src/commandLineArgs.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import yargs from 'yargs'
|
||||||
|
import { hideBin } from 'yargs/helpers'
|
||||||
|
|
||||||
|
const argv = yargs(hideBin(process.argv))
|
||||||
|
.option('telemetry', {
|
||||||
|
alias: 't',
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Writes startup telemetry to file on disk.',
|
||||||
|
})
|
||||||
|
.parse()
|
||||||
|
|
||||||
|
export default argv
|
@ -145,7 +145,7 @@ export function useCalc({
|
|||||||
const _programMem: ProgramMemory = ProgramMemory.empty()
|
const _programMem: ProgramMemory = ProgramMemory.empty()
|
||||||
for (const { key, value } of availableVarInfo.variables) {
|
for (const { key, value } of availableVarInfo.variables) {
|
||||||
const error = _programMem.set(key, {
|
const error = _programMem.set(key, {
|
||||||
type: 'UserVal',
|
type: 'String',
|
||||||
value,
|
value,
|
||||||
__meta: [],
|
__meta: [],
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Dialog, Popover, Transition } from '@headlessui/react'
|
import { Dialog, Popover, Transition } from '@headlessui/react'
|
||||||
import { Fragment, useEffect } from 'react'
|
import { Fragment, useEffect } from 'react'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { EngineConnectionStateType } from 'lang/std/engineConnection'
|
||||||
import CommandBarArgument from './CommandBarArgument'
|
import CommandBarArgument from './CommandBarArgument'
|
||||||
import CommandComboBox from '../CommandComboBox'
|
import CommandComboBox from '../CommandComboBox'
|
||||||
import CommandBarReview from './CommandBarReview'
|
import CommandBarReview from './CommandBarReview'
|
||||||
@ -14,6 +16,7 @@ export const COMMAND_PALETTE_HOTKEY = 'mod+k'
|
|||||||
export const CommandBar = () => {
|
export const CommandBar = () => {
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
|
const { immediateState } = useNetworkContext()
|
||||||
const {
|
const {
|
||||||
context: { selectedCommand, currentArgument, commands },
|
context: { selectedCommand, currentArgument, commands },
|
||||||
} = commandBarState
|
} = commandBarState
|
||||||
@ -25,6 +28,14 @@ export const CommandBar = () => {
|
|||||||
commandBarSend({ type: 'Close' })
|
commandBarSend({ type: 'Close' })
|
||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
|
||||||
|
) {
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
}
|
||||||
|
}, [immediateState])
|
||||||
|
|
||||||
// Hook up keyboard shortcuts
|
// Hook up keyboard shortcuts
|
||||||
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
|
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
|
||||||
if (commandBarState.context.commands.length === 0) return
|
if (commandBarState.context.commands.length === 0) return
|
||||||
|
@ -2,13 +2,20 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
|||||||
import usePlatform from 'hooks/usePlatform'
|
import usePlatform from 'hooks/usePlatform'
|
||||||
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
||||||
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
|
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { EngineConnectionStateType } from 'lang/std/engineConnection'
|
||||||
|
|
||||||
export function CommandBarOpenButton() {
|
export function CommandBarOpenButton() {
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
const { immediateState } = useNetworkContext()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
|
||||||
|
const isDisabled =
|
||||||
|
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
disabled={isDisabled}
|
||||||
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
||||||
onClick={() => commandBarSend({ type: 'Open' })}
|
onClick={() => commandBarSend({ type: 'Open' })}
|
||||||
data-testid="command-bar-open-button"
|
data-testid="command-bar-open-button"
|
||||||
|
@ -1161,6 +1161,29 @@ const CustomIconMap = {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
|
stopwatch: (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M7.95705 5.99046C7.05643 6.44935 6.33654 7.19809 5.91336 8.11602C5.49019 9.03396 5.38838 10.0676 5.62434 11.0505C5.8603 12.0334 6.42029 12.9081 7.21408 13.5339C8.00787 14.1597 8.98922 14.5 10 14.5C11.0108 14.5 11.9921 14.1597 12.7859 13.5339C13.5797 12.9082 14.1397 12.0334 14.3757 11.0505C14.6116 10.0676 14.5098 9.03396 14.0866 8.11603C13.6635 7.19809 12.9436 6.44935 12.043 5.99046L12.497 5.09946C13.5977 5.66032 14.4776 6.57544 14.9948 7.69737C15.512 8.81929 15.6364 10.0827 15.348 11.2839C15.0596 12.4852 14.3752 13.5544 13.405 14.3192C12.4348 15.0841 11.2354 15.5 10 15.5C8.7646 15.5 7.56517 15.0841 6.59499 14.3192C5.6248 13.5544 4.94037 12.4852 4.65197 11.2839C4.36357 10.0827 4.488 8.81929 5.00522 7.69736C5.52243 6.57544 6.40231 5.66032 7.50306 5.09946L7.95705 5.99046Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path d="M10 5.5V4M10 4H8M10 4H12" stroke="currentColor" />
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M12.8536 7.85356L10.3536 10.3536C10.1583 10.5488 9.84171 10.5488 9.64645 10.3536C9.45118 10.1583 9.45118 9.84172 9.64645 9.64645L12.1464 7.14645L12.8536 7.85356Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type CustomIconName = keyof typeof CustomIconMap
|
export type CustomIconName = keyof typeof CustomIconMap
|
||||||
|
293
src/components/EngineStream.tsx
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
import { MouseEventHandler, useEffect, useRef } from 'react'
|
||||||
|
import { useAppState } from 'AppState'
|
||||||
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
|
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
||||||
|
import { btnName } from 'lib/cameraControls'
|
||||||
|
import { trap } from 'lib/trap'
|
||||||
|
import { sendSelectEventToEngine } from 'lib/selections'
|
||||||
|
import { kclManager, engineCommandManager } from 'lib/singletons'
|
||||||
|
import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
|
||||||
|
import { useRouteLoaderData } from 'react-router-dom'
|
||||||
|
import { PATHS } from 'lib/paths'
|
||||||
|
import { IndexLoaderData } from 'lib/types'
|
||||||
|
import useEngineStreamContext, {
|
||||||
|
EngineStreamState,
|
||||||
|
EngineStreamTransition,
|
||||||
|
} from 'hooks/useEngineStreamContext'
|
||||||
|
import { REASONABLE_TIME_TO_REFRESH_STREAM_SIZE } from 'lib/timings'
|
||||||
|
|
||||||
|
export const EngineStream = () => {
|
||||||
|
const { setAppState } = useAppState()
|
||||||
|
|
||||||
|
const { overallState } = useNetworkContext()
|
||||||
|
const { settings } = useSettingsAuthContext()
|
||||||
|
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||||
|
const last = useRef<number>(Date.now())
|
||||||
|
|
||||||
|
const settingsEngine = {
|
||||||
|
theme: settings.context.app.theme.current,
|
||||||
|
enableSSAO: settings.context.app.enableSSAO.current,
|
||||||
|
highlightEdges: settings.context.modeling.highlightEdges.current,
|
||||||
|
showScaleGrid: settings.context.modeling.showScaleGrid.current,
|
||||||
|
cameraProjection: settings.context.modeling.cameraProjection.current,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { state: modelingMachineState, send: modelingMachineActorSend } =
|
||||||
|
useModelingContext()
|
||||||
|
|
||||||
|
const engineStreamActor = useEngineStreamContext.useActorRef()
|
||||||
|
const engineStreamState = engineStreamActor.getSnapshot()
|
||||||
|
|
||||||
|
const streamIdleMode = settings.context.app.streamIdleMode.current
|
||||||
|
|
||||||
|
const configure = () => {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.StartOrReconfigureEngine,
|
||||||
|
modelingMachineActorSend,
|
||||||
|
settings: settingsEngine,
|
||||||
|
setAppState,
|
||||||
|
|
||||||
|
// It's possible a reconnect happens as we drag the window :')
|
||||||
|
onMediaStream(mediaStream: MediaStream) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.SetMediaStream,
|
||||||
|
mediaStream,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const play = () => {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.Play,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
engineCommandManager.addEventListener(
|
||||||
|
EngineCommandManagerEvents.SceneReady,
|
||||||
|
play
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
engineCommandManager.removeEventListener(
|
||||||
|
EngineCommandManagerEvents.SceneReady,
|
||||||
|
play
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const video = engineStreamState.context.videoRef?.current
|
||||||
|
if (!video) return
|
||||||
|
const canvas = engineStreamState.context.canvasRef?.current
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
new ResizeObserver(() => {
|
||||||
|
if (Date.now() - last.current < REASONABLE_TIME_TO_REFRESH_STREAM_SIZE)
|
||||||
|
return
|
||||||
|
last.current = Date.now()
|
||||||
|
|
||||||
|
if (
|
||||||
|
Math.abs(video.width - window.innerWidth) > 4 ||
|
||||||
|
Math.abs(video.height - window.innerHeight) > 4
|
||||||
|
) {
|
||||||
|
timeoutStart.current = Date.now()
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
}).observe(document.body)
|
||||||
|
}, [engineStreamState.value])
|
||||||
|
|
||||||
|
// When the video and canvas element references are set, start the engine.
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
engineStreamState.context.canvasRef.current &&
|
||||||
|
engineStreamState.context.videoRef.current
|
||||||
|
) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.StartOrReconfigureEngine,
|
||||||
|
modelingMachineActorSend,
|
||||||
|
settings: settingsEngine,
|
||||||
|
setAppState,
|
||||||
|
onMediaStream(mediaStream: MediaStream) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.SetMediaStream,
|
||||||
|
mediaStream,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
engineStreamState.context.canvasRef.current,
|
||||||
|
engineStreamState.context.videoRef.current,
|
||||||
|
])
|
||||||
|
|
||||||
|
// On settings change, reconfigure the engine. When paused this gets really tricky,
|
||||||
|
// and also requires onMediaStream to be set!
|
||||||
|
useEffect(() => {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.StartOrReconfigureEngine,
|
||||||
|
modelingMachineActorSend,
|
||||||
|
settings: settingsEngine,
|
||||||
|
setAppState,
|
||||||
|
onMediaStream(mediaStream: MediaStream) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.SetMediaStream,
|
||||||
|
mediaStream,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [settings.context])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to execute code when the file changes
|
||||||
|
* but only if the scene is already ready.
|
||||||
|
* See onSceneReady for the initial scene setup.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (engineCommandManager.engineConnection?.isReady() && file?.path) {
|
||||||
|
console.log('execute on file change')
|
||||||
|
void kclManager.executeCode(true).catch(trap)
|
||||||
|
}
|
||||||
|
}, [file?.path, engineCommandManager.engineConnection])
|
||||||
|
|
||||||
|
const IDLE_TIME_MS = Number(streamIdleMode)
|
||||||
|
|
||||||
|
// When streamIdleMode is changed, setup or teardown the timeouts
|
||||||
|
const timeoutStart = useRef<number | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
timeoutStart.current = streamIdleMode ? Date.now() : null
|
||||||
|
}, [streamIdleMode])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let frameId: ReturnType<typeof window.requestAnimationFrame> = 0
|
||||||
|
const frameLoop = () => {
|
||||||
|
// Do not pause if the user is in the middle of an operation
|
||||||
|
if (!modelingMachineState.matches('idle')) {
|
||||||
|
// In fact, stop the timeout, because we don't want to trigger the
|
||||||
|
// pause when we exit the operation.
|
||||||
|
timeoutStart.current = null
|
||||||
|
} else if (timeoutStart.current) {
|
||||||
|
const elapsed = Date.now() - timeoutStart.current
|
||||||
|
if (elapsed >= IDLE_TIME_MS) {
|
||||||
|
timeoutStart.current = null
|
||||||
|
engineStreamActor.send({ type: EngineStreamTransition.Pause })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frameId = window.requestAnimationFrame(frameLoop)
|
||||||
|
}
|
||||||
|
frameId = window.requestAnimationFrame(frameLoop)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.cancelAnimationFrame(frameId)
|
||||||
|
}
|
||||||
|
}, [modelingMachineState])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!streamIdleMode) return
|
||||||
|
|
||||||
|
const onAnyInput = () => {
|
||||||
|
// Just in case it happens in the middle of the user turning off
|
||||||
|
// idle mode.
|
||||||
|
if (!streamIdleMode) {
|
||||||
|
timeoutStart.current = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (engineStreamState.value === EngineStreamState.Paused) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.StartOrReconfigureEngine,
|
||||||
|
modelingMachineActorSend,
|
||||||
|
settings: settingsEngine,
|
||||||
|
setAppState,
|
||||||
|
onMediaStream(mediaStream: MediaStream) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.SetMediaStream,
|
||||||
|
mediaStream,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutStart.current = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's possible after a reconnect, the user doesn't move their mouse at
|
||||||
|
// all, meaning the timer is not reset to run. We need to set it every
|
||||||
|
// time our effect dependencies change then.
|
||||||
|
timeoutStart.current = Date.now()
|
||||||
|
|
||||||
|
window.document.addEventListener('keydown', onAnyInput)
|
||||||
|
window.document.addEventListener('keyup', onAnyInput)
|
||||||
|
window.document.addEventListener('mousemove', onAnyInput)
|
||||||
|
window.document.addEventListener('mousedown', onAnyInput)
|
||||||
|
window.document.addEventListener('mouseup', onAnyInput)
|
||||||
|
window.document.addEventListener('scroll', onAnyInput)
|
||||||
|
window.document.addEventListener('touchstart', onAnyInput)
|
||||||
|
window.document.addEventListener('touchstop', onAnyInput)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
timeoutStart.current = null
|
||||||
|
window.document.removeEventListener('keydown', onAnyInput)
|
||||||
|
window.document.removeEventListener('keyup', onAnyInput)
|
||||||
|
window.document.removeEventListener('mousemove', onAnyInput)
|
||||||
|
window.document.removeEventListener('mousedown', onAnyInput)
|
||||||
|
window.document.removeEventListener('mouseup', onAnyInput)
|
||||||
|
window.document.removeEventListener('scroll', onAnyInput)
|
||||||
|
window.document.removeEventListener('touchstart', onAnyInput)
|
||||||
|
window.document.removeEventListener('touchstop', onAnyInput)
|
||||||
|
}
|
||||||
|
}, [streamIdleMode, engineStreamState.value])
|
||||||
|
|
||||||
|
const isNetworkOkay =
|
||||||
|
overallState === NetworkHealthState.Ok ||
|
||||||
|
overallState === NetworkHealthState.Weak
|
||||||
|
|
||||||
|
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
|
if (!isNetworkOkay) return
|
||||||
|
if (!engineStreamState.context.videoRef.current) return
|
||||||
|
if (modelingMachineState.matches('Sketch')) return
|
||||||
|
if (modelingMachineState.matches({ idle: 'showPlanes' })) return
|
||||||
|
|
||||||
|
if (btnName(e.nativeEvent).left) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
sendSelectEventToEngine(e, engineStreamState.context.videoRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-0"
|
||||||
|
id="stream"
|
||||||
|
data-testid="stream"
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
onContextMenuCapture={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
autoPlay
|
||||||
|
muted
|
||||||
|
key={engineStreamActor.id + 'video'}
|
||||||
|
ref={engineStreamState.context.videoRef}
|
||||||
|
controls={false}
|
||||||
|
className="cursor-pointer"
|
||||||
|
disablePictureInPicture
|
||||||
|
id="video-stream"
|
||||||
|
/>
|
||||||
|
<canvas
|
||||||
|
key={engineStreamActor.id + 'canvas'}
|
||||||
|
ref={engineStreamState.context.canvasRef}
|
||||||
|
className="cursor-pointer"
|
||||||
|
id="freeze-frame"
|
||||||
|
>
|
||||||
|
No canvas support
|
||||||
|
</canvas>
|
||||||
|
<ClientSideScene
|
||||||
|
cameraControls={settings.context.modeling.mouseControls.current}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -29,6 +29,7 @@ import {
|
|||||||
KclSamplesManifestItem,
|
KclSamplesManifestItem,
|
||||||
} from 'lib/getKclSamplesManifest'
|
} from 'lib/getKclSamplesManifest'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import { markOnce } from 'lib/performance'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -54,6 +55,7 @@ export const FileMachineProvider = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
markOnce('code/didLoadFile')
|
||||||
async function fetchKclSamples() {
|
async function fetchKclSamples() {
|
||||||
setKclSamples(await getKclSamplesManifest())
|
setKclSamples(await getKclSamplesManifest())
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,10 @@ import { Dispatch, useCallback, useRef, useState } from 'react'
|
|||||||
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
||||||
import { Disclosure } from '@headlessui/react'
|
import { Disclosure } from '@headlessui/react'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
|
import { faChevronRight, faPencil } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useFileContext } from 'hooks/useFileContext'
|
import { useFileContext } from 'hooks/useFileContext'
|
||||||
import styles from './FileTree.module.css'
|
import styles from './FileTree.module.css'
|
||||||
import { sortProject } from 'lib/desktopFS'
|
import { sortFilesAndDirectories } from 'lib/desktopFS'
|
||||||
import { FILE_EXT } from 'lib/constants'
|
import { FILE_EXT } from 'lib/constants'
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
import { codeManager, kclManager } from 'lib/singletons'
|
import { codeManager, kclManager } from 'lib/singletons'
|
||||||
@ -22,11 +22,42 @@ import usePlatform from 'hooks/usePlatform'
|
|||||||
import { FileEntry } from 'lib/project'
|
import { FileEntry } from 'lib/project'
|
||||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||||
import { normalizeLineEndings } from 'lib/codeEditor'
|
import { normalizeLineEndings } from 'lib/codeEditor'
|
||||||
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
function getIndentationCSS(level: number) {
|
function getIndentationCSS(level: number) {
|
||||||
return `calc(1rem * ${level + 1})`
|
return `calc(1rem * ${level + 1})`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TreeEntryInput(props: {
|
||||||
|
level: number
|
||||||
|
onSubmit: (value: string) => void
|
||||||
|
}) {
|
||||||
|
const [value, setValue] = useState('')
|
||||||
|
const onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key !== 'Enter') return
|
||||||
|
props.onSubmit(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label>
|
||||||
|
<span className="sr-only">Entry input</span>
|
||||||
|
<input
|
||||||
|
data-testid="tree-input-field"
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
className="w-full py-1 bg-transparent text-chalkboard-100 placeholder:text-chalkboard-70 dark:text-chalkboard-10 dark:placeholder:text-chalkboard-50 focus:outline-none focus:ring-0"
|
||||||
|
onBlur={() => props.onSubmit(value)}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
onKeyPress={onKeyPress}
|
||||||
|
style={{ paddingInlineStart: getIndentationCSS(props.level) }}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function RenameForm({
|
function RenameForm({
|
||||||
fileOrDir,
|
fileOrDir,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@ -113,23 +144,44 @@ function DeleteFileTreeItemDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FileTreeItem = ({
|
const FileTreeItem = ({
|
||||||
|
parentDir,
|
||||||
project,
|
project,
|
||||||
currentFile,
|
currentFile,
|
||||||
|
lastDirectoryClicked,
|
||||||
fileOrDir,
|
fileOrDir,
|
||||||
onNavigateToFile,
|
onNavigateToFile,
|
||||||
|
onClickDirectory,
|
||||||
|
onCreateFile,
|
||||||
|
onCreateFolder,
|
||||||
|
newTreeEntry,
|
||||||
level = 0,
|
level = 0,
|
||||||
|
treeSelection,
|
||||||
|
setTreeSelection,
|
||||||
}: {
|
}: {
|
||||||
|
parentDir: FileEntry | undefined
|
||||||
project?: IndexLoaderData['project']
|
project?: IndexLoaderData['project']
|
||||||
currentFile?: IndexLoaderData['file']
|
currentFile?: IndexLoaderData['file']
|
||||||
|
lastDirectoryClicked?: FileEntry
|
||||||
fileOrDir: FileEntry
|
fileOrDir: FileEntry
|
||||||
onNavigateToFile?: () => void
|
onNavigateToFile?: () => void
|
||||||
|
onClickDirectory: (
|
||||||
|
open: boolean,
|
||||||
|
path: FileEntry,
|
||||||
|
parentDir: FileEntry | undefined
|
||||||
|
) => void
|
||||||
|
onCreateFile: (name: string) => void
|
||||||
|
onCreateFolder: (name: string) => void
|
||||||
|
newTreeEntry: TreeEntry
|
||||||
level?: number
|
level?: number
|
||||||
|
treeSelection: FileEntry | undefined
|
||||||
|
setTreeSelection: Dispatch<React.SetStateAction<FileEntry | undefined>>
|
||||||
}) => {
|
}) => {
|
||||||
const { send: fileSend, context: fileContext } = useFileContext()
|
const { send: fileSend, context: fileContext } = useFileContext()
|
||||||
const { onFileOpen, onFileClose } = useLspContext()
|
const { onFileOpen, onFileClose } = useLspContext()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||||
const isCurrentFile = fileOrDir.path === currentFile?.path
|
const isCurrentFile = fileOrDir.path === currentFile?.path
|
||||||
|
const isFileOrDirHighlighted = treeSelection?.path === fileOrDir?.path
|
||||||
const itemRef = useRef(null)
|
const itemRef = useRef(null)
|
||||||
|
|
||||||
// Since every file or directory gets its own FileTreeItem, we can do this.
|
// Since every file or directory gets its own FileTreeItem, we can do this.
|
||||||
@ -138,8 +190,6 @@ const FileTreeItem = ({
|
|||||||
// the ReactNodes are destroyed, so is this listener :)
|
// the ReactNodes are destroyed, so is this listener :)
|
||||||
useFileSystemWatcher(
|
useFileSystemWatcher(
|
||||||
async (eventType, path) => {
|
async (eventType, path) => {
|
||||||
// Don't try to read a file that was removed.
|
|
||||||
if (isCurrentFile && eventType !== 'unlink') {
|
|
||||||
// Prevents a cyclic read / write causing editor problems such as
|
// Prevents a cyclic read / write causing editor problems such as
|
||||||
// misplaced cursor positions.
|
// misplaced cursor positions.
|
||||||
if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) {
|
if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) {
|
||||||
@ -147,6 +197,7 @@ const FileTreeItem = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isCurrentFile && eventType === 'change') {
|
||||||
let code = await window.electron.readFile(path, { encoding: 'utf-8' })
|
let code = await window.electron.readFile(path, { encoding: 'utf-8' })
|
||||||
code = normalizeLineEndings(code)
|
code = normalizeLineEndings(code)
|
||||||
codeManager.updateCodeStateEditor(code)
|
codeManager.updateCodeStateEditor(code)
|
||||||
@ -156,6 +207,10 @@ const FileTreeItem = ({
|
|||||||
[fileOrDir.path]
|
[fileOrDir.path]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const showNewTreeEntry =
|
||||||
|
newTreeEntry !== undefined &&
|
||||||
|
fileOrDir.path === fileContext.selectedDirectory.path
|
||||||
|
|
||||||
const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path)
|
const isRenaming = fileContext.itemsBeingRenamed.includes(fileOrDir.path)
|
||||||
const removeCurrentItemFromRenaming = useCallback(
|
const removeCurrentItemFromRenaming = useCallback(
|
||||||
() =>
|
() =>
|
||||||
@ -179,13 +234,6 @@ const FileTreeItem = ({
|
|||||||
})
|
})
|
||||||
}, [fileContext.itemsBeingRenamed, fileOrDir.path, fileSend])
|
}, [fileContext.itemsBeingRenamed, fileOrDir.path, fileSend])
|
||||||
|
|
||||||
const clickDirectory = () => {
|
|
||||||
fileSend({
|
|
||||||
type: 'Set selected directory',
|
|
||||||
directory: fileOrDir,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyUp(e: React.KeyboardEvent<HTMLButtonElement>) {
|
function handleKeyUp(e: React.KeyboardEvent<HTMLButtonElement>) {
|
||||||
if (e.metaKey && e.key === 'Backspace') {
|
if (e.metaKey && e.key === 'Backspace') {
|
||||||
// Open confirmation dialog
|
// Open confirmation dialog
|
||||||
@ -194,11 +242,13 @@ const FileTreeItem = ({
|
|||||||
// Show the renaming form
|
// Show the renaming form
|
||||||
addCurrentItemToRenaming()
|
addCurrentItemToRenaming()
|
||||||
} else if (e.code === 'Space') {
|
} else if (e.code === 'Space') {
|
||||||
handleClick()
|
void handleClick().catch(reportRejection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClick() {
|
async function handleClick() {
|
||||||
|
setTreeSelection(fileOrDir)
|
||||||
|
|
||||||
if (fileOrDir.children !== null) return // Don't open directories
|
if (fileOrDir.children !== null) return // Don't open directories
|
||||||
|
|
||||||
if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) {
|
if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) {
|
||||||
@ -208,12 +258,10 @@ const FileTreeItem = ({
|
|||||||
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
|
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
|
||||||
codeManager.code
|
codeManager.code
|
||||||
)
|
)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await codeManager.writeToFile()
|
||||||
codeManager.writeToFile()
|
|
||||||
|
|
||||||
// Prevent seeing the model built one piece at a time when changing files
|
// Prevent seeing the model built one piece at a time when changing files
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await kclManager.executeCode(true)
|
||||||
kclManager.executeCode(true)
|
|
||||||
} else {
|
} else {
|
||||||
// Let the lsp servers know we closed a file.
|
// Let the lsp servers know we closed a file.
|
||||||
onFileClose(currentFile?.path || null, project?.path || null)
|
onFileClose(currentFile?.path || null, project?.path || null)
|
||||||
@ -222,16 +270,19 @@ const FileTreeItem = ({
|
|||||||
// Open kcl files
|
// Open kcl files
|
||||||
navigate(`${PATHS.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
navigate(`${PATHS.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
onNavigateToFile?.()
|
onNavigateToFile?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The below handles both the "root" of all directories and all subs. It's
|
||||||
|
// why some code is duplicated.
|
||||||
return (
|
return (
|
||||||
<div className="contents" data-testid="file-tree-item" ref={itemRef}>
|
<div className="contents" data-testid="file-tree-item" ref={itemRef}>
|
||||||
{fileOrDir.children === null ? (
|
{fileOrDir.children === null ? (
|
||||||
<li
|
<li
|
||||||
className={
|
className={
|
||||||
'group m-0 p-0 border-solid border-0 hover:bg-primary/5 focus-within:bg-primary/5 dark:hover:bg-primary/20 dark:focus-within:bg-primary/20 ' +
|
'group m-0 p-0 border-solid border-0 hover:bg-primary/5 focus-within:bg-primary/5 dark:hover:bg-primary/20 dark:focus-within:bg-primary/20 ' +
|
||||||
(isCurrentFile
|
(isFileOrDirHighlighted || isCurrentFile
|
||||||
? '!bg-primary/10 !text-primary dark:!bg-primary/20 dark:!text-inherit'
|
? '!bg-primary/10 !text-primary dark:!bg-primary/20 dark:!text-inherit'
|
||||||
: '')
|
: '')
|
||||||
}
|
}
|
||||||
@ -242,7 +293,7 @@ const FileTreeItem = ({
|
|||||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.currentTarget.focus()
|
e.currentTarget.focus()
|
||||||
handleClick()
|
void handleClick().catch(reportRejection)
|
||||||
}}
|
}}
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
>
|
>
|
||||||
@ -268,14 +319,13 @@ const FileTreeItem = ({
|
|||||||
<Disclosure.Button
|
<Disclosure.Button
|
||||||
className={
|
className={
|
||||||
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5 dark:hover:text-inherit dark:hover:bg-primary/10' +
|
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5 dark:hover:text-inherit dark:hover:bg-primary/10' +
|
||||||
(fileContext.selectedDirectory.path.includes(fileOrDir.path)
|
(isFileOrDirHighlighted ? ' ui-open:bg-primary/10' : '')
|
||||||
? ' ui-open:bg-primary/10'
|
|
||||||
: '')
|
|
||||||
}
|
}
|
||||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||||
onClick={(e) => e.currentTarget.focus()}
|
onClick={(e) => {
|
||||||
onClickCapture={clickDirectory}
|
e.stopPropagation()
|
||||||
onFocusCapture={clickDirectory}
|
onClickDirectory(open, fileOrDir, parentDir)
|
||||||
|
}}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
>
|
>
|
||||||
@ -317,35 +367,69 @@ const FileTreeItem = ({
|
|||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
className="m-0 p-0"
|
className="m-0 p-0"
|
||||||
onClickCapture={(e) => {
|
onClick={(e) => {
|
||||||
fileSend({
|
e.stopPropagation()
|
||||||
type: 'Set selected directory',
|
onClickDirectory(open, fileOrDir, parentDir)
|
||||||
directory: fileOrDir,
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
onFocusCapture={(e) =>
|
|
||||||
fileSend({
|
|
||||||
type: 'Set selected directory',
|
|
||||||
directory: fileOrDir,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{fileOrDir.children?.map((child) => (
|
{showNewTreeEntry && (
|
||||||
|
<div
|
||||||
|
className="flex items-center"
|
||||||
|
style={{
|
||||||
|
paddingInlineStart: getIndentationCSS(level + 1),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faPencil}
|
||||||
|
className="inline-block mr-2 m-0 p-0 w-2 h-2"
|
||||||
|
/>
|
||||||
|
<TreeEntryInput
|
||||||
|
level={-1}
|
||||||
|
onSubmit={(value: string) =>
|
||||||
|
newTreeEntry === 'file'
|
||||||
|
? onCreateFile(value)
|
||||||
|
: onCreateFolder(value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{sortFilesAndDirectories(fileOrDir.children || []).map(
|
||||||
|
(child) => (
|
||||||
<FileTreeItem
|
<FileTreeItem
|
||||||
|
parentDir={fileOrDir}
|
||||||
fileOrDir={child}
|
fileOrDir={child}
|
||||||
project={project}
|
project={project}
|
||||||
currentFile={currentFile}
|
currentFile={currentFile}
|
||||||
|
onCreateFile={onCreateFile}
|
||||||
|
onCreateFolder={onCreateFolder}
|
||||||
|
newTreeEntry={newTreeEntry}
|
||||||
|
lastDirectoryClicked={lastDirectoryClicked}
|
||||||
|
onClickDirectory={onClickDirectory}
|
||||||
onNavigateToFile={onNavigateToFile}
|
onNavigateToFile={onNavigateToFile}
|
||||||
level={level + 1}
|
level={level + 1}
|
||||||
key={level + '-' + child.path}
|
key={level + '-' + child.path}
|
||||||
|
treeSelection={treeSelection}
|
||||||
|
setTreeSelection={setTreeSelection}
|
||||||
/>
|
/>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
|
{!showNewTreeEntry && fileOrDir.children?.length === 0 && (
|
||||||
|
<div
|
||||||
|
className="flex items-center text-chalkboard-50"
|
||||||
|
style={{
|
||||||
|
paddingInlineStart: getIndentationCSS(level + 1),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>No files</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isConfirmingDelete && (
|
{isConfirmingDelete && (
|
||||||
<DeleteFileTreeItemDialog
|
<DeleteFileTreeItemDialog
|
||||||
fileOrDir={fileOrDir}
|
fileOrDir={fileOrDir}
|
||||||
@ -409,27 +493,15 @@ interface FileTreeProps {
|
|||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileTreeMenu = () => {
|
export const FileTreeMenu = ({
|
||||||
const { send } = useFileContext()
|
onCreateFile,
|
||||||
const { send: modelingSend } = useModelingContext()
|
onCreateFolder,
|
||||||
|
}: {
|
||||||
function createFile() {
|
onCreateFile: () => void
|
||||||
send({
|
onCreateFolder: () => void
|
||||||
type: 'Create file',
|
}) => {
|
||||||
data: { name: '', makeDir: false, shouldSetToRename: true },
|
useHotkeyWrapper(['mod + n'], onCreateFile)
|
||||||
})
|
useHotkeyWrapper(['mod + shift + n'], onCreateFolder)
|
||||||
modelingSend({ type: 'Cancel' })
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFolder() {
|
|
||||||
send({
|
|
||||||
type: 'Create file',
|
|
||||||
data: { name: '', makeDir: true, shouldSetToRename: true },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
useHotkeyWrapper(['mod + n'], createFile)
|
|
||||||
useHotkeyWrapper(['mod + shift + n'], createFolder)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -442,7 +514,7 @@ export const FileTreeMenu = () => {
|
|||||||
bgClassName: 'bg-transparent',
|
bgClassName: 'bg-transparent',
|
||||||
}}
|
}}
|
||||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||||
onClick={createFile}
|
onClick={onCreateFile}
|
||||||
>
|
>
|
||||||
<Tooltip position="bottom-right" delay={750}>
|
<Tooltip position="bottom-right" delay={750}>
|
||||||
Create file
|
Create file
|
||||||
@ -458,7 +530,7 @@ export const FileTreeMenu = () => {
|
|||||||
bgClassName: 'bg-transparent',
|
bgClassName: 'bg-transparent',
|
||||||
}}
|
}}
|
||||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||||
onClick={createFolder}
|
onClick={onCreateFolder}
|
||||||
>
|
>
|
||||||
<Tooltip position="bottom-right" delay={750}>
|
<Tooltip position="bottom-right" delay={750}>
|
||||||
Create folder
|
Create folder
|
||||||
@ -468,30 +540,110 @@ export const FileTreeMenu = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TreeEntry = 'file' | 'folder' | undefined
|
||||||
|
|
||||||
|
export const useFileTreeOperations = () => {
|
||||||
|
const { send } = useFileContext()
|
||||||
|
const { send: modelingSend } = useModelingContext()
|
||||||
|
|
||||||
|
// As long as this is undefined, a new "file tree entry prompt" is not shown.
|
||||||
|
const [newTreeEntry, setNewTreeEntry] = useState<TreeEntry>(undefined)
|
||||||
|
|
||||||
|
function createFile(args: { dryRun: boolean; name?: string }) {
|
||||||
|
if (args.dryRun) {
|
||||||
|
setNewTreeEntry('file')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear so that the entry prompt goes away.
|
||||||
|
setNewTreeEntry(undefined)
|
||||||
|
|
||||||
|
if (!args.name) return
|
||||||
|
|
||||||
|
send({
|
||||||
|
type: 'Create file',
|
||||||
|
data: { name: args.name, makeDir: false, shouldSetToRename: false },
|
||||||
|
})
|
||||||
|
modelingSend({ type: 'Cancel' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFolder(args: { dryRun: boolean; name?: string }) {
|
||||||
|
if (args.dryRun) {
|
||||||
|
setNewTreeEntry('folder')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setNewTreeEntry(undefined)
|
||||||
|
|
||||||
|
if (!args.name) return
|
||||||
|
|
||||||
|
send({
|
||||||
|
type: 'Create file',
|
||||||
|
data: { name: args.name, makeDir: true, shouldSetToRename: false },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createFile,
|
||||||
|
createFolder,
|
||||||
|
newTreeEntry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const FileTree = ({
|
export const FileTree = ({
|
||||||
className = '',
|
className = '',
|
||||||
onNavigateToFile: closePanel,
|
onNavigateToFile: closePanel,
|
||||||
}: FileTreeProps) => {
|
}: FileTreeProps) => {
|
||||||
|
const { createFile, createFolder, newTreeEntry } = useFileTreeOperations()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
|
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
|
||||||
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
||||||
<FileTreeMenu />
|
<FileTreeMenu
|
||||||
|
onCreateFile={() => createFile({ dryRun: true })}
|
||||||
|
onCreateFolder={() => createFolder({ dryRun: true })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FileTreeInner onNavigateToFile={closePanel} />
|
<FileTreeInner
|
||||||
|
onNavigateToFile={closePanel}
|
||||||
|
newTreeEntry={newTreeEntry}
|
||||||
|
onCreateFile={(name: string) => createFile({ dryRun: false, name })}
|
||||||
|
onCreateFolder={(name: string) => createFolder({ dryRun: false, name })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileTreeInner = ({
|
export const FileTreeInner = ({
|
||||||
onNavigateToFile,
|
onNavigateToFile,
|
||||||
|
onCreateFile,
|
||||||
|
onCreateFolder,
|
||||||
|
newTreeEntry,
|
||||||
}: {
|
}: {
|
||||||
|
onCreateFile: (name: string) => void
|
||||||
|
onCreateFolder: (name: string) => void
|
||||||
|
newTreeEntry: TreeEntry
|
||||||
onNavigateToFile?: () => void
|
onNavigateToFile?: () => void
|
||||||
}) => {
|
}) => {
|
||||||
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||||
const { send: fileSend, context: fileContext } = useFileContext()
|
const { send: fileSend, context: fileContext } = useFileContext()
|
||||||
const { send: modelingSend } = useModelingContext()
|
const { send: modelingSend } = useModelingContext()
|
||||||
|
|
||||||
|
const [lastDirectoryClicked, setLastDirectoryClicked] = useState<
|
||||||
|
FileEntry | undefined
|
||||||
|
>(undefined)
|
||||||
|
|
||||||
|
const [treeSelection, setTreeSelection] = useState<FileEntry | undefined>(
|
||||||
|
loaderData.file
|
||||||
|
)
|
||||||
|
|
||||||
|
const onNavigateToFile_ = () => {
|
||||||
|
// Reset modeling state when navigating to a new file
|
||||||
|
onNavigateToFile?.()
|
||||||
|
modelingSend({ type: 'Cancel' })
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh the file tree when there are changes.
|
// Refresh the file tree when there are changes.
|
||||||
useFileSystemWatcher(
|
useFileSystemWatcher(
|
||||||
async (eventType, path) => {
|
async (eventType, path) => {
|
||||||
@ -501,6 +653,13 @@ export const FileTreeInner = ({
|
|||||||
const isCurrentFile = loaderData.file?.path === path
|
const isCurrentFile = loaderData.file?.path === path
|
||||||
const hasChanged = eventType === 'change'
|
const hasChanged = eventType === 'change'
|
||||||
if (isCurrentFile && hasChanged) return
|
if (isCurrentFile && hasChanged) return
|
||||||
|
|
||||||
|
// If it's a settings file we wrote to already from the app ignore it.
|
||||||
|
if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) {
|
||||||
|
codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
fileSend({ type: 'Refresh' })
|
fileSend({ type: 'Refresh' })
|
||||||
},
|
},
|
||||||
[loaderData?.project?.path, fileContext.selectedDirectory.path].filter(
|
[loaderData?.project?.path, fileContext.selectedDirectory.path].filter(
|
||||||
@ -508,34 +667,82 @@ export const FileTreeInner = ({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const clickDirectory = () => {
|
const onTreeEntryInputSubmit = (value: string) => {
|
||||||
|
if (newTreeEntry === 'file') {
|
||||||
|
onCreateFile(value)
|
||||||
|
onNavigateToFile_()
|
||||||
|
} else {
|
||||||
|
onCreateFolder(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClickDirectory = (
|
||||||
|
open_: boolean,
|
||||||
|
fileOrDir: FileEntry,
|
||||||
|
parentDir: FileEntry | undefined
|
||||||
|
) => {
|
||||||
|
// open true is closed... it's broken. Save me. I've inverted it here for
|
||||||
|
// sanity.
|
||||||
|
const open = !open_
|
||||||
|
|
||||||
|
const target = open ? fileOrDir : parentDir
|
||||||
|
|
||||||
|
// We're at the root, can't select anything further
|
||||||
|
if (!target) return
|
||||||
|
|
||||||
|
setTreeSelection(target)
|
||||||
|
setLastDirectoryClicked(target)
|
||||||
fileSend({
|
fileSend({
|
||||||
type: 'Set selected directory',
|
type: 'Set selected directory',
|
||||||
directory: fileContext.project,
|
directory: target,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showNewTreeEntry =
|
||||||
|
newTreeEntry !== undefined &&
|
||||||
|
fileContext.selectedDirectory.path === loaderData.project?.path
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="relative">
|
||||||
<div
|
<div
|
||||||
className="overflow-auto pb-12 absolute inset-0"
|
className="overflow-auto pb-12 absolute inset-0"
|
||||||
data-testid="file-pane-scroll-container"
|
data-testid="file-pane-scroll-container"
|
||||||
>
|
>
|
||||||
<ul className="m-0 p-0 text-sm" onClickCapture={clickDirectory}>
|
<ul className="m-0 p-0 text-sm">
|
||||||
{sortProject(fileContext.project?.children || []).map((fileOrDir) => (
|
{showNewTreeEntry && (
|
||||||
|
<div
|
||||||
|
className="flex items-center"
|
||||||
|
style={{ paddingInlineStart: getIndentationCSS(0) }}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faPencil}
|
||||||
|
className="inline-block mr-2 m-0 p-0 w-2 h-2"
|
||||||
|
/>
|
||||||
|
<TreeEntryInput level={-1} onSubmit={onTreeEntryInputSubmit} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{sortFilesAndDirectories(fileContext.project?.children || []).map(
|
||||||
|
(fileOrDir) => (
|
||||||
<FileTreeItem
|
<FileTreeItem
|
||||||
|
parentDir={fileContext.project}
|
||||||
project={fileContext.project}
|
project={fileContext.project}
|
||||||
currentFile={loaderData?.file}
|
currentFile={loaderData?.file}
|
||||||
|
lastDirectoryClicked={lastDirectoryClicked}
|
||||||
fileOrDir={fileOrDir}
|
fileOrDir={fileOrDir}
|
||||||
onNavigateToFile={() => {
|
onCreateFile={onCreateFile}
|
||||||
// Reset modeling state when navigating to a new file
|
onCreateFolder={onCreateFolder}
|
||||||
modelingSend({ type: 'Cancel' })
|
newTreeEntry={newTreeEntry}
|
||||||
onNavigateToFile?.()
|
onClickDirectory={onClickDirectory}
|
||||||
}}
|
onNavigateToFile={onNavigateToFile_}
|
||||||
key={fileOrDir.path}
|
key={fileOrDir.path}
|
||||||
|
treeSelection={treeSelection}
|
||||||
|
setTreeSelection={setTreeSelection}
|
||||||
/>
|
/>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +96,23 @@ export function LowerRightControls({
|
|||||||
Report a bug
|
Report a bug
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</a>
|
</a>
|
||||||
|
<Link
|
||||||
|
to={
|
||||||
|
location.pathname.includes(PATHS.FILE)
|
||||||
|
? filePath + PATHS.TELEMETRY + '?tab=project'
|
||||||
|
: PATHS.HOME + PATHS.TELEMETRY
|
||||||
|
}
|
||||||
|
data-testid="telemetry-link"
|
||||||
|
>
|
||||||
|
<CustomIcon
|
||||||
|
name="stopwatch"
|
||||||
|
className={`w-5 h-5 ${linkOverrideClassName}`}
|
||||||
|
/>
|
||||||
|
<span className="sr-only">Telemetry</span>
|
||||||
|
<Tooltip position="top" contentClassName="text-xs">
|
||||||
|
Telemetry
|
||||||
|
</Tooltip>
|
||||||
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to={
|
to={
|
||||||
location.pathname.includes(PATHS.FILE)
|
location.pathname.includes(PATHS.FILE)
|
||||||
|
@ -1,40 +1,47 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
import { useEngineCommands } from './EngineCommands'
|
import { useEngineCommands } from './EngineCommands'
|
||||||
import { Spinner } from './Spinner'
|
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
|
import useEngineStreamContext, {
|
||||||
|
EngineStreamState,
|
||||||
|
} from 'hooks/useEngineStreamContext'
|
||||||
|
import { CommandLogType } from 'lang/std/engineConnection'
|
||||||
|
|
||||||
export const ModelStateIndicator = () => {
|
export const ModelStateIndicator = () => {
|
||||||
const [commands] = useEngineCommands()
|
const [commands] = useEngineCommands()
|
||||||
|
const [isDone, setIsDone] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const engineStreamActor = useEngineStreamContext.useActorRef()
|
||||||
|
const engineStreamState = engineStreamActor.getSnapshot()
|
||||||
|
|
||||||
const lastCommandType = commands[commands.length - 1]?.type
|
const lastCommandType = commands[commands.length - 1]?.type
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastCommandType === CommandLogType.SetDefaultSystemProperties) {
|
||||||
|
setIsDone(false)
|
||||||
|
}
|
||||||
|
if (lastCommandType === CommandLogType.ExecutionDone) {
|
||||||
|
setIsDone(true)
|
||||||
|
}
|
||||||
|
}, [lastCommandType])
|
||||||
|
|
||||||
let className = 'w-6 h-6 '
|
let className = 'w-6 h-6 '
|
||||||
let icon = <Spinner className={className} />
|
let icon = <div className={className}></div>
|
||||||
let dataTestId = 'model-state-indicator'
|
let dataTestId = 'model-state-indicator'
|
||||||
|
|
||||||
if (lastCommandType === 'receive-reliable') {
|
if (engineStreamState.value === EngineStreamState.Paused) {
|
||||||
className +=
|
className += 'text-secondary'
|
||||||
'bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
icon = <CustomIcon data-testid={dataTestId + '-paused'} name="parallel" />
|
||||||
icon = (
|
} else if (engineStreamState.value === EngineStreamState.Resuming) {
|
||||||
<CustomIcon
|
className += 'text-secondary'
|
||||||
data-testid={dataTestId + '-receive-reliable'}
|
icon = <CustomIcon data-testid={dataTestId + '-resuming'} name="parallel" />
|
||||||
name="checkmark"
|
} else if (isDone) {
|
||||||
/>
|
className += 'text-secondary'
|
||||||
)
|
|
||||||
} else if (lastCommandType === 'execution-done') {
|
|
||||||
className +=
|
|
||||||
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
|
||||||
icon = (
|
icon = (
|
||||||
<CustomIcon
|
<CustomIcon
|
||||||
data-testid={dataTestId + '-execution-done'}
|
data-testid={dataTestId + '-execution-done'}
|
||||||
name="checkmark"
|
name="checkmark"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else if (lastCommandType === 'export-done') {
|
|
||||||
className +=
|
|
||||||
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
|
||||||
icon = (
|
|
||||||
<CustomIcon data-testid={dataTestId + '-export-done'} name="checkmark" />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -20,7 +20,6 @@ import {
|
|||||||
modelingMachine,
|
modelingMachine,
|
||||||
modelingMachineDefaultContext,
|
modelingMachineDefaultContext,
|
||||||
} from 'machines/modelingMachine'
|
} from 'machines/modelingMachine'
|
||||||
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import {
|
import {
|
||||||
isCursorInSketchCommandRange,
|
isCursorInSketchCommandRange,
|
||||||
@ -112,13 +111,8 @@ export const ModelingMachineProvider = ({
|
|||||||
auth,
|
auth,
|
||||||
settings: {
|
settings: {
|
||||||
context: {
|
context: {
|
||||||
app: { theme, enableSSAO },
|
app: { theme },
|
||||||
modeling: {
|
modeling: { defaultUnit, highlightEdges, cameraProjection },
|
||||||
defaultUnit,
|
|
||||||
cameraProjection,
|
|
||||||
highlightEdges,
|
|
||||||
showScaleGrid,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
@ -129,9 +123,6 @@ export const ModelingMachineProvider = ({
|
|||||||
const streamRef = useRef<HTMLDivElement>(null)
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
const persistedContext = useMemo(() => getPersistedContext(), [])
|
const persistedContext = useMemo(() => getPersistedContext(), [])
|
||||||
|
|
||||||
let [searchParams] = useSearchParams()
|
|
||||||
const pool = searchParams.get('pool')
|
|
||||||
|
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
|
|
||||||
// Settings machine setup
|
// Settings machine setup
|
||||||
@ -158,8 +149,12 @@ export const ModelingMachineProvider = ({
|
|||||||
'enable copilot': () => {
|
'enable copilot': () => {
|
||||||
editorManager.setCopilotEnabled(true)
|
editorManager.setCopilotEnabled(true)
|
||||||
},
|
},
|
||||||
'sketch exit execute': ({ context: { store } }) => {
|
// tsc reports this typing as perfectly fine, but eslint is complaining.
|
||||||
;(async () => {
|
// It's actually nonsensical, so I'm quieting.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
'sketch exit execute': async ({
|
||||||
|
context: { store },
|
||||||
|
}): Promise<void> => {
|
||||||
// When cancelling the sketch mode we should disable sketch mode within the engine.
|
// When cancelling the sketch mode we should disable sketch mode within the engine.
|
||||||
await engineCommandManager.sendSceneCommand({
|
await engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -177,7 +172,7 @@ export const ModelingMachineProvider = ({
|
|||||||
|
|
||||||
store.videoElement?.pause()
|
store.videoElement?.pause()
|
||||||
|
|
||||||
kclManager
|
return kclManager
|
||||||
.executeCode()
|
.executeCode()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (engineCommandManager.engineConnection?.idleMode) return
|
if (engineCommandManager.engineConnection?.idleMode) return
|
||||||
@ -187,7 +182,6 @@ export const ModelingMachineProvider = ({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(reportRejection)
|
.catch(reportRejection)
|
||||||
})().catch(reportRejection)
|
|
||||||
},
|
},
|
||||||
'Set mouse state': assign(({ context, event }) => {
|
'Set mouse state': assign(({ context, event }) => {
|
||||||
if (event.type !== 'Set mouse state') return {}
|
if (event.type !== 'Set mouse state') return {}
|
||||||
@ -301,6 +295,7 @@ export const ModelingMachineProvider = ({
|
|||||||
const dispatchSelection = (selection?: EditorSelection) => {
|
const dispatchSelection = (selection?: EditorSelection) => {
|
||||||
if (!selection) return // TODO less of hack for the below please
|
if (!selection) return // TODO less of hack for the below please
|
||||||
if (!editorManager.editorView) return
|
if (!editorManager.editorView) return
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!editorManager.editorView) return
|
if (!editorManager.editorView) return
|
||||||
editorManager.editorView.dispatch({
|
editorManager.editorView.dispatch({
|
||||||
@ -654,6 +649,9 @@ export const ModelingMachineProvider = ({
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
input.faceId
|
input.faceId
|
||||||
)
|
)
|
||||||
|
await sceneInfra.camControls.centerModelRelativeToPanes({
|
||||||
|
resetLastPaneWidth: true,
|
||||||
|
})
|
||||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: pathToNewSketchNode,
|
sketchPathToNode: pathToNewSketchNode,
|
||||||
@ -674,6 +672,9 @@ export const ModelingMachineProvider = ({
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
input.planeId
|
input.planeId
|
||||||
)
|
)
|
||||||
|
await sceneInfra.camControls.centerModelRelativeToPanes({
|
||||||
|
resetLastPaneWidth: true,
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: pathToNode,
|
sketchPathToNode: pathToNode,
|
||||||
@ -696,6 +697,9 @@ export const ModelingMachineProvider = ({
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
info?.sketchDetails?.faceId || ''
|
info?.sketchDetails?.faceId || ''
|
||||||
)
|
)
|
||||||
|
await sceneInfra.camControls.centerModelRelativeToPanes({
|
||||||
|
resetLastPaneWidth: true,
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: sketchPathToNode || [],
|
sketchPathToNode: sketchPathToNode || [],
|
||||||
zAxis: info.sketchDetails.zAxis || null,
|
zAxis: info.sketchDetails.zAxis || null,
|
||||||
@ -729,6 +733,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -765,6 +774,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -810,6 +824,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -843,6 +862,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -878,6 +902,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -914,6 +943,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -950,6 +984,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -996,6 +1035,11 @@ export const ModelingMachineProvider = ({
|
|||||||
sketchDetails.origin
|
sketchDetails.origin
|
||||||
)
|
)
|
||||||
if (err(updatedAst)) return Promise.reject(updatedAst)
|
if (err(updatedAst)) return Promise.reject(updatedAst)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updatedAst.newAst
|
||||||
|
)
|
||||||
|
|
||||||
const selection = updateSelections(
|
const selection = updateSelections(
|
||||||
{ 0: pathToReplacedNode },
|
{ 0: pathToReplacedNode },
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
@ -1024,21 +1068,6 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
useSetupEngineManager(
|
|
||||||
streamRef,
|
|
||||||
modelingSend,
|
|
||||||
modelingState.context,
|
|
||||||
{
|
|
||||||
pool: pool,
|
|
||||||
theme: theme.current,
|
|
||||||
highlightEdges: highlightEdges.current,
|
|
||||||
enableSSAO: enableSSAO.current,
|
|
||||||
showScaleGrid: showScaleGrid.current,
|
|
||||||
cameraProjection: cameraProjection.current,
|
|
||||||
},
|
|
||||||
token
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
kclManager.registerExecuteCallback(() => {
|
kclManager.registerExecuteCallback(() => {
|
||||||
modelingSend({ type: 'Re-execute' })
|
modelingSend({ type: 'Re-execute' })
|
||||||
|
@ -48,7 +48,7 @@ export const ModelingPaneHeader = ({
|
|||||||
bgClassName: 'bg-transparent dark:bg-transparent',
|
bgClassName: 'bg-transparent dark:bg-transparent',
|
||||||
}}
|
}}
|
||||||
className="!p-0 !bg-transparent hover:text-primary border-transparent dark:!border-transparent hover:!border-primary dark:hover:!border-chalkboard-70 !outline-none"
|
className="!p-0 !bg-transparent hover:text-primary border-transparent dark:!border-transparent hover:!border-primary dark:hover:!border-chalkboard-70 !outline-none"
|
||||||
onClick={onClose}
|
onClick={() => onClose()}
|
||||||
>
|
>
|
||||||
<Tooltip position="bottom-right" delay={750}>
|
<Tooltip position="bottom-right" delay={750}>
|
||||||
Close
|
Close
|
||||||
@ -59,14 +59,12 @@ export const ModelingPaneHeader = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ModelingPane = ({
|
export const ModelingPane = ({
|
||||||
title,
|
|
||||||
icon,
|
|
||||||
id,
|
id,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
Menu,
|
|
||||||
detailsTestId,
|
detailsTestId,
|
||||||
onClose,
|
onClose,
|
||||||
|
title,
|
||||||
...props
|
...props
|
||||||
}: ModelingPaneProps) => {
|
}: ModelingPaneProps) => {
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
@ -78,6 +76,7 @@ export const ModelingPane = ({
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
{...props}
|
{...props}
|
||||||
|
title={title && typeof title === 'string' ? title : ''}
|
||||||
data-testid={detailsTestId}
|
data-testid={detailsTestId}
|
||||||
id={id}
|
id={id}
|
||||||
className={
|
className={
|
||||||
@ -88,14 +87,7 @@ export const ModelingPane = ({
|
|||||||
(className || '')
|
(className || '')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ModelingPaneHeader
|
{children}
|
||||||
id={id}
|
|
||||||
icon={icon}
|
|
||||||
title={title}
|
|
||||||
Menu={Menu}
|
|
||||||
onClose={onClose}
|
|
||||||
/>
|
|
||||||
<div className="relative w-full">{children}</div>
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|