Compare commits
189 Commits
derive-doc
...
achalmers/
Author | SHA1 | Date | |
---|---|---|---|
b3101d3fff | |||
16575a8bf8 | |||
e4c5fad8c7 | |||
cc0d601294 | |||
69cefafc19 | |||
b187ca3422 | |||
1edadcaa0f | |||
95c0ded8cf | |||
0ebb4e2cad | |||
f3e0939057 | |||
f5e233d8a0 | |||
1cab3e628f | |||
2ca6ba52b6 | |||
f741ea2e09 | |||
9f2a7781fc | |||
990f2b4154 | |||
0af0f15281 | |||
b558548b94 | |||
29e0f9a270 | |||
9385c32cfb | |||
ce3fb5c353 | |||
f920490518 | |||
d681e667ee | |||
5c6515a60e | |||
eb8a33312d | |||
d351b3bbe4 | |||
47d40eb801 | |||
adc4b6148d | |||
27d0d4a28b | |||
fb609c19ef | |||
8666989c85 | |||
bdf49c2084 | |||
a06b9d560a | |||
b81ff66f2b | |||
c0e6947170 | |||
65ebde0b34 | |||
0d6618b60a | |||
f0c44d11b3 | |||
44e71cd4bc | |||
a9f716dad8 | |||
a2455832e7 | |||
8f5034f997 | |||
af1c2c7ae1 | |||
ff38ae091e | |||
1dd7c95b8c | |||
20042ec87c | |||
fccf3508a7 | |||
8dab5527b8 | |||
f72eb0e8a7 | |||
40479d177f | |||
b88359dee2 | |||
f4c0347104 | |||
ad36b5f5fa | |||
b798cf19d3 | |||
7cfa897561 | |||
0d8804005a | |||
cbd26d29fa | |||
e501a542ac | |||
7cb4f4d101 | |||
1162f5f4c4 | |||
3975e6d8f5 | |||
d68d7a7e00 | |||
b135b97de6 | |||
de5885ce0b | |||
ad7c544754 | |||
4d77875bdc | |||
3377923dcb | |||
c6005660c8 | |||
66e62c6037 | |||
0a4a517bb4 | |||
70f3ded7e2 | |||
095108252b | |||
20b1c93f12 | |||
3747a1b993 | |||
198feb7d44 | |||
c7a8b8313e | |||
1576dc3256 | |||
341a3b7609 | |||
ecb42b89a6 | |||
f00ee3a44a | |||
900e3b96ad | |||
15fae05659 | |||
2730b6d152 | |||
602e7afef6 | |||
d9bcadb062 | |||
19f669b94c | |||
d9ef471385 | |||
39f8b306a2 | |||
19925d22c1 | |||
e1af4b4219 | |||
c699611f5b | |||
00ede7ec1a | |||
f30601bd2c | |||
cfbc77b62f | |||
808830d29e | |||
e714103655 | |||
fbcb96add5 | |||
7386ccf1bf | |||
6e73578933 | |||
b88d5c8799 | |||
5430c1fa66 | |||
c0d4bb6c9f | |||
25260a88c3 | |||
b6d6f0f4c1 | |||
b1276b2ed8 | |||
5f0f3f40d0 | |||
f1ea9b6ece | |||
b94c5be1af | |||
8378eb1e94 | |||
05f98a8c39 | |||
386571fa60 | |||
b0abdf4f70 | |||
81e70e139f | |||
d6bfc38d62 | |||
ada66de92d | |||
8f133f9662 | |||
b360dbb961 | |||
eca3dc2967 | |||
ae36ab6982 | |||
8cb6cf1b8a | |||
3c235c890a | |||
b6dfd30840 | |||
65d128eecd | |||
77b7c602f2 | |||
fa0e61a2be | |||
1cf35a611e | |||
952d0e4c7c | |||
0f85de9df8 | |||
0e8eed3f82 | |||
5b43a5075f | |||
f5ed4e37b2 | |||
19c8da1a86 | |||
a25f89aaba | |||
aeebe5416f | |||
661788b8b0 | |||
ac24563159 | |||
d17342dfb8 | |||
2e93b58ae6 | |||
6593656b08 | |||
47be749ec7 | |||
a03e7f5c41 | |||
b78e9fa131 | |||
c629233eaa | |||
f640f7a5e0 | |||
64398381a9 | |||
0bc5534056 | |||
9fc1df7c1d | |||
a5879ceeda | |||
379c30824e | |||
a4d3263b88 | |||
c1f661ab52 | |||
7d887a1497 | |||
4ca341e132 | |||
c6249f36d2 | |||
dcbe5d7f75 | |||
390cb2d51d | |||
98f7a564ea | |||
05f9e3c290 | |||
09760fc2e9 | |||
18ffc43e89 | |||
de63e4f19f | |||
b70b271e6b | |||
08b7cdc5f6 | |||
6efe6b54c0 | |||
69f72d62e0 | |||
e04b09fcd8 | |||
4903f6b9fc | |||
ef8149f03a | |||
1b75321bf1 | |||
3ed263da6b | |||
d59c4a2258 | |||
9c8351ea40 | |||
db98bcf2a0 | |||
15d96a072d | |||
088968c664 | |||
4bbf98bc34 | |||
ca08f5b337 | |||
a3649d09c0 | |||
635cb58036 | |||
7f050b114f | |||
c999819450 | |||
82905caad6 | |||
519e6d74ac | |||
edb7d68c05 | |||
345dd45caa | |||
b6a5f133f3 | |||
bc6407be6e | |||
038409124a | |||
d5567f8602 |
@ -1,3 +1,3 @@
|
|||||||
[codespell]
|
[codespell]
|
||||||
ignore-words-list: crate,everytime
|
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo
|
||||||
skip: **/target,node_modules,build
|
skip: **/target,node_modules,build,**/Cargo.lock
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
|
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
|
||||||
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
|
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
|
||||||
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
|
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
|
||||||
VITE_KC_SKIP_AUTH=false
|
VITE_KC_SKIP_AUTH=false
|
||||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||||
VITE_KC_SENTRY_DSN=
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
|
VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
|
||||||
VITE_KC_API_BASE_URL=https://api.kittycad.io
|
VITE_KC_API_BASE_URL=https://api.zoo.dev
|
||||||
VITE_KC_SITE_BASE_URL=https://kittycad.io
|
VITE_KC_SITE_BASE_URL=https://zoo.dev
|
||||||
VITE_KC_SKIP_AUTH=false
|
VITE_KC_SKIP_AUTH=false
|
||||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||||
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
|
|
||||||
|
@ -1 +1,2 @@
|
|||||||
src/wasm-lib/*
|
src/wasm-lib/*
|
||||||
|
*.typegen.ts
|
||||||
|
@ -17,12 +17,12 @@
|
|||||||
"never"
|
"never"
|
||||||
],
|
],
|
||||||
"react-hooks/exhaustive-deps": "off",
|
"react-hooks/exhaustive-deps": "off",
|
||||||
"@typescript-eslint/no-floating-promises": "warn"
|
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
|
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"@typescript-eslint/no-floating-promises": "warn",
|
||||||
"testing-library/prefer-screen-queries": "off"
|
"testing-library/prefer-screen-queries": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
85
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: File a bug report for the Zoo Modeling App
|
||||||
|
title: "[BUG]: "
|
||||||
|
labels: ["bug"]
|
||||||
|
assignees: []
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "Thank you for taking the time to report a bug. Please provide as much information as possible to help us resolve it."
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: describe-bug
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug
|
||||||
|
description: A clear and concise description of what the bug is.
|
||||||
|
placeholder: "Explain the bug..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce-bug
|
||||||
|
attributes:
|
||||||
|
label: Steps to Reproduce
|
||||||
|
description: Steps to reproduce the behavior.
|
||||||
|
placeholder: |
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: Description of what you expected to happen.
|
||||||
|
placeholder: "I expected that..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: screenshots
|
||||||
|
attributes:
|
||||||
|
label: Screenshots and Recordings
|
||||||
|
description: If applicable, add screenshots to help explain your problem. Maximum upload size is 10MB.
|
||||||
|
placeholder: "You can attach images or video recordings here."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: desktop-os
|
||||||
|
attributes:
|
||||||
|
label: Desktop OS
|
||||||
|
description: "Your operating system"
|
||||||
|
placeholder: "example: Windows 10, MacOS Big Sur"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: browser
|
||||||
|
attributes:
|
||||||
|
label: Browser
|
||||||
|
description: "If you are using the web version, please specify the browser you are using."
|
||||||
|
placeholder: "example: Chrome, Safari"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
description: "The version of the Zoo Modeling App you're using."
|
||||||
|
placeholder: "example: v0.15.0. You can find this in the settings."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
description: Add any other context about the problem here.
|
||||||
|
placeholder: "Anything else you want to add..."
|
||||||
|
validations:
|
||||||
|
required: false
|
11
.github/workflows/cargo-clippy.yml
vendored
@ -43,17 +43,6 @@ jobs:
|
|||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2.6.1
|
uses: Swatinem/rust-cache@v2.6.1
|
||||||
|
|
||||||
- name: Install ffmpeg
|
|
||||||
run: |
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install \
|
|
||||||
ffmpeg \
|
|
||||||
libavformat-dev \
|
|
||||||
libavutil-dev \
|
|
||||||
libclang-dev \
|
|
||||||
libswscale-dev \
|
|
||||||
--no-install-recommends
|
|
||||||
|
|
||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
run: |
|
run: |
|
||||||
cd "${{ matrix.dir }}"
|
cd "${{ matrix.dir }}"
|
||||||
|
26
.github/workflows/cargo-test.yml
vendored
@ -40,25 +40,29 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
- name: Install vector
|
||||||
|
run: |
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
|
||||||
|
chmod +x /tmp/vector.sh
|
||||||
|
/tmp/vector.sh -y -no-modify-path
|
||||||
|
mkdir -p /tmp/vector
|
||||||
|
cp .github/workflows/vector.toml /tmp/vector.toml
|
||||||
|
sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
|
||||||
|
sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
|
||||||
|
sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
|
||||||
|
sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
|
||||||
|
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
|
||||||
|
cat /tmp/vector.toml
|
||||||
|
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
|
||||||
- uses: taiki-e/install-action@cargo-llvm-cov
|
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||||
- uses: taiki-e/install-action@nextest
|
- uses: taiki-e/install-action@nextest
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2.6.1
|
uses: Swatinem/rust-cache@v2.6.1
|
||||||
- name: Install ffmpeg
|
|
||||||
run: |
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install \
|
|
||||||
ffmpeg \
|
|
||||||
libavformat-dev \
|
|
||||||
libavutil-dev \
|
|
||||||
libclang-dev \
|
|
||||||
libswscale-dev \
|
|
||||||
--no-install-recommends
|
|
||||||
- name: cargo test
|
- name: cargo test
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |-
|
run: |-
|
||||||
cd "${{ matrix.dir }}"
|
cd "${{ matrix.dir }}"
|
||||||
cargo nextest run --workspace --no-fail-fast -P ci
|
cargo nextest run --workspace --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
|
||||||
env:
|
env:
|
||||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||||
RUST_MIN_STACK: 10485760000
|
RUST_MIN_STACK: 10485760000
|
||||||
|
90
.github/workflows/ci.yml
vendored
@ -46,6 +46,7 @@ jobs:
|
|||||||
workspaces: './src/wasm-lib'
|
workspaces: './src/wasm-lib'
|
||||||
|
|
||||||
- run: yarn build:wasm
|
- run: yarn build:wasm
|
||||||
|
- run: yarn xstate:typegen
|
||||||
- run: yarn tsc
|
- run: yarn tsc
|
||||||
|
|
||||||
|
|
||||||
@ -85,8 +86,6 @@ jobs:
|
|||||||
|
|
||||||
- run: yarn test:nowatch
|
- run: yarn test:nowatch
|
||||||
|
|
||||||
- run: yarn test:cov
|
|
||||||
|
|
||||||
|
|
||||||
prepare-json-files:
|
prepare-json-files:
|
||||||
runs-on: ubuntu-latest # seperate job on Ubuntu for easy string manipulations (compared to Windows)
|
runs-on: ubuntu-latest # seperate job on Ubuntu for easy string manipulations (compared to Windows)
|
||||||
@ -104,7 +103,7 @@ jobs:
|
|||||||
if: github.event_name == 'schedule'
|
if: github.event_name == 'schedule'
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
|
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
|
||||||
echo "$(jq --arg url 'https://dl.kittycad.io/releases/modeling-app/nightly/last_update.json' \
|
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json' \
|
||||||
'.tauri.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
|
'.tauri.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
@ -123,8 +122,9 @@ jobs:
|
|||||||
needs: [prepare-json-files]
|
needs: [prepare-json-files]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
os: [macos-14, ubuntu-latest, windows-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@ -187,10 +187,10 @@ jobs:
|
|||||||
- name: Fix format
|
- name: Fix format
|
||||||
run: yarn fmt
|
run: yarn fmt
|
||||||
|
|
||||||
- name: Install Universal target (MacOS only)
|
- name: Install x86 target for Universal builds (MacOS only)
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-14'
|
||||||
run: |
|
run: |
|
||||||
rustup target add aarch64-apple-darwin
|
rustup target add x86_64-apple-darwin
|
||||||
|
|
||||||
- name: Prepare certificate and variables (Windows only)
|
- name: Prepare certificate and variables (Windows only)
|
||||||
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
|
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
|
||||||
@ -224,7 +224,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
includeRelease: false
|
includeRelease: false
|
||||||
includeDebug: true
|
includeDebug: true
|
||||||
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
|
args: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }}
|
||||||
|
|
||||||
- name: Build the app (release) and sign
|
- name: Build the app (release) and sign
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
@ -240,11 +240,12 @@ jobs:
|
|||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
|
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
|
||||||
with:
|
with:
|
||||||
args: "${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
|
args: "${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: matrix.os != 'ubuntu-latest'
|
||||||
env:
|
env:
|
||||||
PREFIX: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
|
PREFIX: ${{ matrix.os == 'macos-14' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
|
||||||
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
|
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
|
||||||
with:
|
with:
|
||||||
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
|
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
|
||||||
@ -252,12 +253,12 @@ jobs:
|
|||||||
- name: Run e2e tests (linux only)
|
- name: Run e2e tests (linux only)
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
cargo install tauri-driver
|
cargo install tauri-driver@0.1.3
|
||||||
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
|
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
|
||||||
export VITE_KC_API_BASE_URL
|
export VITE_KC_API_BASE_URL
|
||||||
xvfb-run yarn test:e2e:tauri
|
xvfb-run yarn test:e2e:tauri
|
||||||
env:
|
env:
|
||||||
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/kittycad-modeling"
|
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/zoo-modeling-app"
|
||||||
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
|
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
|
|
||||||
|
|
||||||
@ -271,26 +272,24 @@ jobs:
|
|||||||
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
|
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
|
||||||
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
|
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
|
||||||
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
|
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
|
||||||
|
WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
|
|
||||||
- name: Generate the update static endpoint
|
- name: Generate the update static endpoint
|
||||||
run: |
|
run: |
|
||||||
ls -l artifact/*/*itty*
|
ls -l artifact/*/*oo*
|
||||||
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
|
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
|
||||||
LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig`
|
|
||||||
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
|
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
|
||||||
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
|
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
|
||||||
jq --null-input \
|
jq --null-input \
|
||||||
--arg version "${VERSION}" \
|
--arg version "${VERSION}" \
|
||||||
--arg pub_date "${PUB_DATE}" \
|
--arg pub_date "${PUB_DATE}" \
|
||||||
--arg notes "${NOTES}" \
|
--arg notes "${NOTES}" \
|
||||||
--arg darwin_sig "$DARWIN_SIG" \
|
--arg darwin_sig "$DARWIN_SIG" \
|
||||||
--arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
|
--arg darwin_url "$RELEASE_DIR/macos/Zoo%20Modeling%20App.app.tar.gz" \
|
||||||
--arg linux_sig "$LINUX_SIG" \
|
|
||||||
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
|
|
||||||
--arg windows_sig "$WINDOWS_SIG" \
|
--arg windows_sig "$WINDOWS_SIG" \
|
||||||
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi.zip" \
|
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi.zip" \
|
||||||
'{
|
'{
|
||||||
"version": $version,
|
"version": $version,
|
||||||
"pub_date": $pub_date,
|
"pub_date": $pub_date,
|
||||||
@ -304,10 +303,6 @@ jobs:
|
|||||||
"signature": $darwin_sig,
|
"signature": $darwin_sig,
|
||||||
"url": $darwin_url
|
"url": $darwin_url
|
||||||
},
|
},
|
||||||
"linux-x86_64": {
|
|
||||||
"signature": $linux_sig,
|
|
||||||
"url": $linux_url
|
|
||||||
},
|
|
||||||
"windows-x86_64": {
|
"windows-x86_64": {
|
||||||
"signature": $windows_sig,
|
"signature": $windows_sig,
|
||||||
"url": $windows_url
|
"url": $windows_url
|
||||||
@ -318,14 +313,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate the download static endpoint
|
- name: Generate the download static endpoint
|
||||||
run: |
|
run: |
|
||||||
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
|
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
|
||||||
jq --null-input \
|
jq --null-input \
|
||||||
--arg version "${VERSION}" \
|
--arg version "${VERSION}" \
|
||||||
--arg pub_date "${PUB_DATE}" \
|
--arg pub_date "${PUB_DATE}" \
|
||||||
--arg notes "${NOTES}" \
|
--arg notes "${NOTES}" \
|
||||||
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
|
--arg darwin_url "$RELEASE_DIR/dmg/Zoo%20Modeling%20App_${VERSION_NO_V}_universal.dmg" \
|
||||||
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage" \
|
--arg windows_url "$RELEASE_DIR/msi/Zoo%20Modeling%20App_${VERSION_NO_V}_x64_en-US.msi" \
|
||||||
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi" \
|
|
||||||
'{
|
'{
|
||||||
"version": $version,
|
"version": $version,
|
||||||
"pub_date": $pub_date,
|
"pub_date": $pub_date,
|
||||||
@ -334,9 +328,6 @@ jobs:
|
|||||||
"dmg-universal": {
|
"dmg-universal": {
|
||||||
"url": $darwin_url
|
"url": $darwin_url
|
||||||
},
|
},
|
||||||
"appimage-x86_64": {
|
|
||||||
"url": $linux_url
|
|
||||||
},
|
|
||||||
"msi-x86_64": {
|
"msi-x86_64": {
|
||||||
"url": $windows_url
|
"url": $windows_url
|
||||||
}
|
}
|
||||||
@ -345,31 +336,31 @@ jobs:
|
|||||||
cat last_download.json
|
cat last_download.json
|
||||||
|
|
||||||
- name: Authenticate to Google Cloud
|
- name: Authenticate to Google Cloud
|
||||||
uses: 'google-github-actions/auth@v2.0.0'
|
uses: 'google-github-actions/auth@v2.1.2'
|
||||||
with:
|
with:
|
||||||
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
||||||
|
|
||||||
- name: Set up Google Cloud SDK
|
- name: Set up Google Cloud SDK
|
||||||
uses: google-github-actions/setup-gcloud@v2.0.0
|
uses: google-github-actions/setup-gcloud@v2.1.0
|
||||||
with:
|
with:
|
||||||
project_id: kittycadapi
|
project_id: kittycadapi
|
||||||
|
|
||||||
- name: Upload release files to public bucket
|
- name: Upload release files to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v2.0.0
|
uses: google-github-actions/upload-cloud-storage@v2.1.0
|
||||||
with:
|
with:
|
||||||
path: artifact
|
path: artifact
|
||||||
glob: '*/*itty*'
|
glob: '*/Zoo*'
|
||||||
parent: false
|
parent: false
|
||||||
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
|
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
|
||||||
|
|
||||||
- name: Upload update endpoint to public bucket
|
- name: Upload update endpoint to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v2.0.0
|
uses: google-github-actions/upload-cloud-storage@v2.1.0
|
||||||
with:
|
with:
|
||||||
path: last_update.json
|
path: last_update.json
|
||||||
destination: ${{ env.BUCKET_DIR }}
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
|
|
||||||
- name: Upload download endpoint to public bucket
|
- name: Upload download endpoint to public bucket
|
||||||
uses: google-github-actions/upload-cloud-storage@v2.0.0
|
uses: google-github-actions/upload-cloud-storage@v2.1.0
|
||||||
with:
|
with:
|
||||||
path: last_download.json
|
path: last_download.json
|
||||||
destination: ${{ env.BUCKET_DIR }}
|
destination: ${{ env.BUCKET_DIR }}
|
||||||
@ -378,4 +369,29 @@ jobs:
|
|||||||
if: ${{ github.event_name == 'release' }}
|
if: ${{ github.event_name == 'release' }}
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
files: artifact/*/*itty*
|
files: 'artifact/*/Zoo*'
|
||||||
|
|
||||||
|
announce_release:
|
||||||
|
needs: [publish-apps-release]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install requests
|
||||||
|
|
||||||
|
- name: Announce Release
|
||||||
|
env:
|
||||||
|
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
|
RELEASE_VERSION: ${{ github.event.release.tag_name }}
|
||||||
|
RELEASE_BODY: ${{ github.event.release.body}}
|
||||||
|
run: python public/announce_release.py
|
9
.github/workflows/playwright.yml
vendored
@ -4,6 +4,11 @@ on:
|
|||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
playwright-ubuntu:
|
playwright-ubuntu:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
@ -14,7 +19,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
- uses: KittyCAD/action-install-cli@v0.2.16
|
- uses: KittyCAD/action-install-cli@v0.2.21
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
@ -79,7 +84,7 @@ jobs:
|
|||||||
|
|
||||||
playwright-macos:
|
playwright-macos:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: macos-latest
|
runs-on: macos-14
|
||||||
needs: playwright-ubuntu
|
needs: playwright-ubuntu
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
21
.github/workflows/vector.toml
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[sources.github-actions-file]
|
||||||
|
type = "file"
|
||||||
|
data_dir = "/tmp/vector"
|
||||||
|
include = ["/tmp/github-actions.log"]
|
||||||
|
|
||||||
|
# Modify the logs to include the action name.
|
||||||
|
[transforms.add-action-name]
|
||||||
|
type = "remap"
|
||||||
|
inputs = [ "github-actions-file" ]
|
||||||
|
source = '''
|
||||||
|
.action = "GITHUB_WORKFLOW"
|
||||||
|
.repo = "GITHUB_REPOSITORY"
|
||||||
|
.sha = "GITHUB_SHA"
|
||||||
|
.ref = "GITHUB_REF_NAME"
|
||||||
|
'''
|
||||||
|
|
||||||
|
[sinks.axiom]
|
||||||
|
type = "axiom"
|
||||||
|
inputs = ["add-action-name"]
|
||||||
|
token = "GH_ACTIONS_AXIOM_TOKEN"
|
||||||
|
dataset = "github-actions"
|
7
.gitignore
vendored
@ -33,6 +33,7 @@ src/wasm-lib/bindings
|
|||||||
src/wasm-lib/kcl/bindings
|
src/wasm-lib/kcl/bindings
|
||||||
public/wasm_lib_bg.wasm
|
public/wasm_lib_bg.wasm
|
||||||
src/wasm-lib/lcov.info
|
src/wasm-lib/lcov.info
|
||||||
|
src/wasm-lib/grackle/*.test.json
|
||||||
|
|
||||||
e2e/playwright/playwright-secrets.env
|
e2e/playwright/playwright-secrets.env
|
||||||
e2e/playwright/temp1.png
|
e2e/playwright/temp1.png
|
||||||
@ -50,3 +51,9 @@ e2e/playwright/export-snapshots/*embedded.gltf
|
|||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
|
|
||||||
|
|
||||||
|
## generated files
|
||||||
|
src/**/*.typegen.ts
|
||||||
|
|
||||||
|
src/wasm-lib/grackle/stdlib_cube_partial.json
|
||||||
|
@ -10,4 +10,4 @@ src/wasm-lib/kcl/bindings
|
|||||||
e2e/playwright/export-snapshots
|
e2e/playwright/export-snapshots
|
||||||
|
|
||||||
# XState generated files
|
# XState generated files
|
||||||
src/machines/modelingMachine.typegen.ts
|
src/machines/**.typegen.ts
|
||||||
|
@ -94,7 +94,6 @@ For running the rust (not tauri rust though) only, you can
|
|||||||
cd src/wasm-lib
|
cd src/wasm-lib
|
||||||
cargo test
|
cargo test
|
||||||
```
|
```
|
||||||
but you will need to have install ffmpeg prior to.
|
|
||||||
|
|
||||||
## Tauri
|
## Tauri
|
||||||
|
|
||||||
@ -137,6 +136,11 @@ Before you submit a contribution PR to this repo, please ensure that:
|
|||||||
VERSION=x.y.z yarn run bump-jsons
|
VERSION=x.y.z yarn run bump-jsons
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Alternatively you can try the experimental `make-release.sh` bash script that will create the branch with the updated json files for you.
|
||||||
|
run `./make-release.sh` for a patch update
|
||||||
|
run `./make-release.sh "minor"` for minor
|
||||||
|
run `./make-release.sh "major"` for major
|
||||||
|
|
||||||
The PR may serve as a place to discuss the human-readable changelog and extra QA. A quick way of getting PR's merged since the last bump is to [use this PR filter](https://github.com/KittyCAD/modeling-app/pulls?q=is%3Apr+sort%3Aupdated-desc+is%3Amerged+), open up the browser console and past in the following
|
The PR may serve as a place to discuss the human-readable changelog and extra QA. A quick way of getting PR's merged since the last bump is to [use this PR filter](https://github.com/KittyCAD/modeling-app/pulls?q=is%3Apr+sort%3Aupdated-desc+is%3Amerged+), open up the browser console and past in the following
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@ -183,7 +187,7 @@ For more information on fuzzing you can check out
|
|||||||
First time running plawright locally, you'll need to add the secrets file
|
First time running plawright locally, you'll need to add the secrets file
|
||||||
```bash
|
```bash
|
||||||
touch ./e2e/playwright/playwright-secrets.env
|
touch ./e2e/playwright/playwright-secrets.env
|
||||||
echo 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets2.env
|
printf 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets.env
|
||||||
```
|
```
|
||||||
then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens
|
then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens
|
||||||
|
|
||||||
|
BIN
app-icon.png
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 120 KiB |
14
docs/kcl/KNOWN-ISSUES.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Known Issues
|
||||||
|
|
||||||
|
The following are bugs that are not in modeling-app or kcl itself. These bugs
|
||||||
|
once fixed in engine will just start working here with no language changes.
|
||||||
|
|
||||||
|
- **Sketch on Face**: If your sketch is outside the edges of the face (on which you
|
||||||
|
are sketching) you will get multiple models returned instead of one single
|
||||||
|
model for that sketch and its underlying 3D object.
|
||||||
|
If you see a red line around your model, it means this is happening.
|
||||||
|
|
||||||
|
- **Import**: Right now you can import a file, even if that file has brep data
|
||||||
|
you cannot edit it, after v1, the engine will account for this. You also cannot
|
||||||
|
currently move or transform the imported objects at all, once we have assemblies
|
||||||
|
this will work.
|
35878
docs/kcl/std.json
6431
docs/kcl/std.md
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 193 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 193 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 259 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 220 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 193 KiB |
@ -1,7 +1,7 @@
|
|||||||
ISO-10303-21;
|
ISO-10303-21;
|
||||||
HEADER;
|
HEADER;
|
||||||
FILE_DESCRIPTION((('kittycad.io export')), '2;1');
|
FILE_DESCRIPTION((('zoo.dev export')), '2;1');
|
||||||
FILE_NAME('dump.step', '1970-01-01T00:00:00.0+00:00', ('Author unknown'), ('Organization unknown'), 'kittycad.io beta', 'kittycad.io', 'Authorization unknown');
|
FILE_NAME('dump.step', '1970-01-01T00:00:00.0+00:00', ('Author unknown'), ('Organization unknown'), 'zoo.dev beta', 'zoo.dev', 'Authorization unknown');
|
||||||
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
|
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
|
||||||
ENDSEC;
|
ENDSEC;
|
||||||
DATA;
|
DATA;
|
||||||
@ -19,59 +19,59 @@ DATA;
|
|||||||
);
|
);
|
||||||
#4 = CARTESIAN_POINT('NONE', (0, 0, -0));
|
#4 = CARTESIAN_POINT('NONE', (0, 0, -0));
|
||||||
#5 = VERTEX_POINT('NONE', #4);
|
#5 = VERTEX_POINT('NONE', #4);
|
||||||
#6 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
|
#6 = CARTESIAN_POINT('NONE', (0, -0.64516, -0));
|
||||||
#7 = VERTEX_POINT('NONE', #6);
|
#7 = VERTEX_POINT('NONE', #6);
|
||||||
#8 = CARTESIAN_POINT('NONE', (0, -0.0254, 0.1016));
|
#8 = CARTESIAN_POINT('NONE', (0, -0.64516, 2.58064));
|
||||||
#9 = VERTEX_POINT('NONE', #8);
|
#9 = VERTEX_POINT('NONE', #8);
|
||||||
#10 = CARTESIAN_POINT('NONE', (0, 0, 0.1016));
|
#10 = CARTESIAN_POINT('NONE', (0, 0, 2.58064));
|
||||||
#11 = VERTEX_POINT('NONE', #10);
|
#11 = VERTEX_POINT('NONE', #10);
|
||||||
#12 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
|
#12 = CARTESIAN_POINT('NONE', (1.996782122555674, -0.64516, -0));
|
||||||
#13 = VERTEX_POINT('NONE', #12);
|
#13 = VERTEX_POINT('NONE', #12);
|
||||||
#14 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, 0.1016));
|
#14 = CARTESIAN_POINT('NONE', (1.996782122555674, -0.64516, 2.58064));
|
||||||
#15 = VERTEX_POINT('NONE', #14);
|
#15 = VERTEX_POINT('NONE', #14);
|
||||||
#16 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, -0));
|
#16 = CARTESIAN_POINT('NONE', (3.839550058615159, -1.9354799999999992, -0));
|
||||||
#17 = VERTEX_POINT('NONE', #16);
|
#17 = VERTEX_POINT('NONE', #16);
|
||||||
#18 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, 0.1016));
|
#18 = CARTESIAN_POINT('NONE', (3.839550058615159, -1.9354799999999992, 2.58064));
|
||||||
#19 = VERTEX_POINT('NONE', #18);
|
#19 = VERTEX_POINT('NONE', #18);
|
||||||
#20 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, -0));
|
#20 = CARTESIAN_POINT('NONE', (6.12902, -1.93548, -0));
|
||||||
#21 = VERTEX_POINT('NONE', #20);
|
#21 = VERTEX_POINT('NONE', #20);
|
||||||
#22 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, 0.1016));
|
#22 = CARTESIAN_POINT('NONE', (6.12902, -1.93548, 2.58064));
|
||||||
#23 = VERTEX_POINT('NONE', #22);
|
#23 = VERTEX_POINT('NONE', #22);
|
||||||
#24 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
|
#24 = CARTESIAN_POINT('NONE', (6.12902, -1.6129, -0));
|
||||||
#25 = VERTEX_POINT('NONE', #24);
|
#25 = VERTEX_POINT('NONE', #24);
|
||||||
#26 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, 0.1016));
|
#26 = CARTESIAN_POINT('NONE', (6.12902, -1.6129, 2.58064));
|
||||||
#27 = VERTEX_POINT('NONE', #26);
|
#27 = VERTEX_POINT('NONE', #26);
|
||||||
#28 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
|
#28 = CARTESIAN_POINT('NONE', (3.9412591419317424, -1.6129, -0));
|
||||||
#29 = VERTEX_POINT('NONE', #28);
|
#29 = VERTEX_POINT('NONE', #28);
|
||||||
#30 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, 0.1016));
|
#30 = CARTESIAN_POINT('NONE', (3.9412591419317424, -1.6129, 2.58064));
|
||||||
#31 = VERTEX_POINT('NONE', #30);
|
#31 = VERTEX_POINT('NONE', #30);
|
||||||
#32 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
|
#32 = CARTESIAN_POINT('NONE', (1.6377992218573856, 0, -0));
|
||||||
#33 = VERTEX_POINT('NONE', #32);
|
#33 = VERTEX_POINT('NONE', #32);
|
||||||
#34 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, 0.1016));
|
#34 = CARTESIAN_POINT('NONE', (1.6377992218573856, 0, 2.58064));
|
||||||
#35 = VERTEX_POINT('NONE', #34);
|
#35 = VERTEX_POINT('NONE', #34);
|
||||||
#36 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, -0));
|
#36 = CARTESIAN_POINT('NONE', (3.7131243491113075, 0.9677400000000002, -0));
|
||||||
#37 = VERTEX_POINT('NONE', #36);
|
#37 = VERTEX_POINT('NONE', #36);
|
||||||
#38 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, 0.1016));
|
#38 = CARTESIAN_POINT('NONE', (3.7131243491113075, 0.9677400000000002, 2.58064));
|
||||||
#39 = VERTEX_POINT('NONE', #38);
|
#39 = VERTEX_POINT('NONE', #38);
|
||||||
#40 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, -0));
|
#40 = CARTESIAN_POINT('NONE', (6.12902, 0.9677399999999998, -0));
|
||||||
#41 = VERTEX_POINT('NONE', #40);
|
#41 = VERTEX_POINT('NONE', #40);
|
||||||
#42 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, 0.1016));
|
#42 = CARTESIAN_POINT('NONE', (6.12902, 0.9677399999999998, 2.58064));
|
||||||
#43 = VERTEX_POINT('NONE', #42);
|
#43 = VERTEX_POINT('NONE', #42);
|
||||||
#44 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
|
#44 = CARTESIAN_POINT('NONE', (6.12902, 1.29032, -0));
|
||||||
#45 = VERTEX_POINT('NONE', #44);
|
#45 = VERTEX_POINT('NONE', #44);
|
||||||
#46 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, 0.1016));
|
#46 = CARTESIAN_POINT('NONE', (6.12902, 1.29032, 2.58064));
|
||||||
#47 = VERTEX_POINT('NONE', #46);
|
#47 = VERTEX_POINT('NONE', #46);
|
||||||
#48 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
|
#48 = CARTESIAN_POINT('NONE', (3.6416100848359463, 1.29032, -0));
|
||||||
#49 = VERTEX_POINT('NONE', #48);
|
#49 = VERTEX_POINT('NONE', #48);
|
||||||
#50 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, 0.1016));
|
#50 = CARTESIAN_POINT('NONE', (3.6416100848359463, 1.29032, 2.58064));
|
||||||
#51 = VERTEX_POINT('NONE', #50);
|
#51 = VERTEX_POINT('NONE', #50);
|
||||||
#52 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
|
#52 = CARTESIAN_POINT('NONE', (2.2580599999999995, 0.64516, -0));
|
||||||
#53 = VERTEX_POINT('NONE', #52);
|
#53 = VERTEX_POINT('NONE', #52);
|
||||||
#54 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, 0.1016));
|
#54 = CARTESIAN_POINT('NONE', (2.2580599999999995, 0.64516, 2.58064));
|
||||||
#55 = VERTEX_POINT('NONE', #54);
|
#55 = VERTEX_POINT('NONE', #54);
|
||||||
#56 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
|
#56 = CARTESIAN_POINT('NONE', (0, 0.64516, -0));
|
||||||
#57 = VERTEX_POINT('NONE', #56);
|
#57 = VERTEX_POINT('NONE', #56);
|
||||||
#58 = CARTESIAN_POINT('NONE', (0, 0.0254, 0.1016));
|
#58 = CARTESIAN_POINT('NONE', (0, 0.64516, 2.58064));
|
||||||
#59 = VERTEX_POINT('NONE', #58);
|
#59 = VERTEX_POINT('NONE', #58);
|
||||||
#60 = DIRECTION('NONE', (0, -1, 0));
|
#60 = DIRECTION('NONE', (0, -1, 0));
|
||||||
#61 = VECTOR('NONE', #60, 1);
|
#61 = VECTOR('NONE', #60, 1);
|
||||||
@ -79,11 +79,11 @@ DATA;
|
|||||||
#63 = LINE('NONE', #62, #61);
|
#63 = LINE('NONE', #62, #61);
|
||||||
#64 = DIRECTION('NONE', (0, 0, 1));
|
#64 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#65 = VECTOR('NONE', #64, 1);
|
#65 = VECTOR('NONE', #64, 1);
|
||||||
#66 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
|
#66 = CARTESIAN_POINT('NONE', (0, -0.64516, -0));
|
||||||
#67 = LINE('NONE', #66, #65);
|
#67 = LINE('NONE', #66, #65);
|
||||||
#68 = DIRECTION('NONE', (0, -1, 0));
|
#68 = DIRECTION('NONE', (0, -1, 0));
|
||||||
#69 = VECTOR('NONE', #68, 1);
|
#69 = VECTOR('NONE', #68, 1);
|
||||||
#70 = CARTESIAN_POINT('NONE', (0, 0, 0.1016));
|
#70 = CARTESIAN_POINT('NONE', (0, 0, 2.58064));
|
||||||
#71 = LINE('NONE', #70, #69);
|
#71 = LINE('NONE', #70, #69);
|
||||||
#72 = DIRECTION('NONE', (0, 0, 1));
|
#72 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#73 = VECTOR('NONE', #72, 1);
|
#73 = VECTOR('NONE', #72, 1);
|
||||||
@ -91,155 +91,155 @@ DATA;
|
|||||||
#75 = LINE('NONE', #74, #73);
|
#75 = LINE('NONE', #74, #73);
|
||||||
#76 = DIRECTION('NONE', (1, 0, 0));
|
#76 = DIRECTION('NONE', (1, 0, 0));
|
||||||
#77 = VECTOR('NONE', #76, 1);
|
#77 = VECTOR('NONE', #76, 1);
|
||||||
#78 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
|
#78 = CARTESIAN_POINT('NONE', (0, -0.64516, -0));
|
||||||
#79 = LINE('NONE', #78, #77);
|
#79 = LINE('NONE', #78, #77);
|
||||||
#80 = DIRECTION('NONE', (0, 0, 1));
|
#80 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#81 = VECTOR('NONE', #80, 1);
|
#81 = VECTOR('NONE', #80, 1);
|
||||||
#82 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
|
#82 = CARTESIAN_POINT('NONE', (1.996782122555674, -0.64516, -0));
|
||||||
#83 = LINE('NONE', #82, #81);
|
#83 = LINE('NONE', #82, #81);
|
||||||
#84 = DIRECTION('NONE', (1, 0, 0));
|
#84 = DIRECTION('NONE', (1, 0, 0));
|
||||||
#85 = VECTOR('NONE', #84, 1);
|
#85 = VECTOR('NONE', #84, 1);
|
||||||
#86 = CARTESIAN_POINT('NONE', (0, -0.0254, 0.1016));
|
#86 = CARTESIAN_POINT('NONE', (0, -0.64516, 2.58064));
|
||||||
#87 = LINE('NONE', #86, #85);
|
#87 = LINE('NONE', #86, #85);
|
||||||
#88 = DIRECTION('NONE', (0.8191520442889919, -0.5735764363510459, 0));
|
#88 = DIRECTION('NONE', (0.819152044288992, -0.5735764363510459, 0));
|
||||||
#89 = VECTOR('NONE', #88, 1);
|
#89 = VECTOR('NONE', #88, 1);
|
||||||
#90 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
|
#90 = CARTESIAN_POINT('NONE', (1.996782122555674, -0.64516, -0));
|
||||||
#91 = LINE('NONE', #90, #89);
|
#91 = LINE('NONE', #90, #89);
|
||||||
#92 = DIRECTION('NONE', (0, 0, 1));
|
#92 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#93 = VECTOR('NONE', #92, 1);
|
#93 = VECTOR('NONE', #92, 1);
|
||||||
#94 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, -0));
|
#94 = CARTESIAN_POINT('NONE', (3.839550058615159, -1.9354799999999992, -0));
|
||||||
#95 = LINE('NONE', #94, #93);
|
#95 = LINE('NONE', #94, #93);
|
||||||
#96 = DIRECTION('NONE', (0.8191520442889919, -0.5735764363510459, 0));
|
#96 = DIRECTION('NONE', (0.819152044288992, -0.5735764363510459, 0));
|
||||||
#97 = VECTOR('NONE', #96, 1);
|
#97 = VECTOR('NONE', #96, 1);
|
||||||
#98 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, 0.1016));
|
#98 = CARTESIAN_POINT('NONE', (1.996782122555674, -0.64516, 2.58064));
|
||||||
#99 = LINE('NONE', #98, #97);
|
#99 = LINE('NONE', #98, #97);
|
||||||
#100 = DIRECTION('NONE', (1, -0.0000000000000003079278779307945, 0));
|
#100 = DIRECTION('NONE', (1, -0.00000000000000038794063361359933, 0));
|
||||||
#101 = VECTOR('NONE', #100, 1);
|
#101 = VECTOR('NONE', #100, 1);
|
||||||
#102 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, -0));
|
#102 = CARTESIAN_POINT('NONE', (3.839550058615159, -1.9354799999999992, -0));
|
||||||
#103 = LINE('NONE', #102, #101);
|
#103 = LINE('NONE', #102, #101);
|
||||||
#104 = DIRECTION('NONE', (0, 0, 1));
|
#104 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#105 = VECTOR('NONE', #104, 1);
|
#105 = VECTOR('NONE', #104, 1);
|
||||||
#106 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, -0));
|
#106 = CARTESIAN_POINT('NONE', (6.12902, -1.93548, -0));
|
||||||
#107 = LINE('NONE', #106, #105);
|
#107 = LINE('NONE', #106, #105);
|
||||||
#108 = DIRECTION('NONE', (1, -0.0000000000000003079278779307945, 0));
|
#108 = DIRECTION('NONE', (1, -0.00000000000000038794063361359933, 0));
|
||||||
#109 = VECTOR('NONE', #108, 1);
|
#109 = VECTOR('NONE', #108, 1);
|
||||||
#110 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, 0.1016));
|
#110 = CARTESIAN_POINT('NONE', (3.839550058615159, -1.9354799999999992, 2.58064));
|
||||||
#111 = LINE('NONE', #110, #109);
|
#111 = LINE('NONE', #110, #109);
|
||||||
#112 = DIRECTION('NONE', (0, 1, 0));
|
#112 = DIRECTION('NONE', (0, 1, 0));
|
||||||
#113 = VECTOR('NONE', #112, 1);
|
#113 = VECTOR('NONE', #112, 1);
|
||||||
#114 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, -0));
|
#114 = CARTESIAN_POINT('NONE', (6.12902, -1.93548, -0));
|
||||||
#115 = LINE('NONE', #114, #113);
|
#115 = LINE('NONE', #114, #113);
|
||||||
#116 = DIRECTION('NONE', (0, 0, 1));
|
#116 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#117 = VECTOR('NONE', #116, 1);
|
#117 = VECTOR('NONE', #116, 1);
|
||||||
#118 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
|
#118 = CARTESIAN_POINT('NONE', (6.12902, -1.6129, -0));
|
||||||
#119 = LINE('NONE', #118, #117);
|
#119 = LINE('NONE', #118, #117);
|
||||||
#120 = DIRECTION('NONE', (0, 1, 0));
|
#120 = DIRECTION('NONE', (0, 1, 0));
|
||||||
#121 = VECTOR('NONE', #120, 1);
|
#121 = VECTOR('NONE', #120, 1);
|
||||||
#122 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, 0.1016));
|
#122 = CARTESIAN_POINT('NONE', (6.12902, -1.93548, 2.58064));
|
||||||
#123 = LINE('NONE', #122, #121);
|
#123 = LINE('NONE', #122, #121);
|
||||||
#124 = DIRECTION('NONE', (-1, 0, 0));
|
#124 = DIRECTION('NONE', (-1, 0, 0));
|
||||||
#125 = VECTOR('NONE', #124, 1);
|
#125 = VECTOR('NONE', #124, 1);
|
||||||
#126 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
|
#126 = CARTESIAN_POINT('NONE', (6.12902, -1.6129, -0));
|
||||||
#127 = LINE('NONE', #126, #125);
|
#127 = LINE('NONE', #126, #125);
|
||||||
#128 = DIRECTION('NONE', (0, 0, 1));
|
#128 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#129 = VECTOR('NONE', #128, 1);
|
#129 = VECTOR('NONE', #128, 1);
|
||||||
#130 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
|
#130 = CARTESIAN_POINT('NONE', (3.9412591419317424, -1.6129, -0));
|
||||||
#131 = LINE('NONE', #130, #129);
|
#131 = LINE('NONE', #130, #129);
|
||||||
#132 = DIRECTION('NONE', (-1, 0, 0));
|
#132 = DIRECTION('NONE', (-1, 0, 0));
|
||||||
#133 = VECTOR('NONE', #132, 1);
|
#133 = VECTOR('NONE', #132, 1);
|
||||||
#134 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, 0.1016));
|
#134 = CARTESIAN_POINT('NONE', (6.12902, -1.6129, 2.58064));
|
||||||
#135 = LINE('NONE', #134, #133);
|
#135 = LINE('NONE', #134, #133);
|
||||||
#136 = DIRECTION('NONE', (-0.8191520442889919, 0.573576436351046, 0));
|
#136 = DIRECTION('NONE', (-0.8191520442889919, 0.573576436351046, 0));
|
||||||
#137 = VECTOR('NONE', #136, 1);
|
#137 = VECTOR('NONE', #136, 1);
|
||||||
#138 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
|
#138 = CARTESIAN_POINT('NONE', (3.9412591419317424, -1.6129, -0));
|
||||||
#139 = LINE('NONE', #138, #137);
|
#139 = LINE('NONE', #138, #137);
|
||||||
#140 = DIRECTION('NONE', (0, 0, 1));
|
#140 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#141 = VECTOR('NONE', #140, 1);
|
#141 = VECTOR('NONE', #140, 1);
|
||||||
#142 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
|
#142 = CARTESIAN_POINT('NONE', (1.6377992218573856, 0, -0));
|
||||||
#143 = LINE('NONE', #142, #141);
|
#143 = LINE('NONE', #142, #141);
|
||||||
#144 = DIRECTION('NONE', (-0.8191520442889919, 0.573576436351046, 0));
|
#144 = DIRECTION('NONE', (-0.8191520442889919, 0.573576436351046, 0));
|
||||||
#145 = VECTOR('NONE', #144, 1);
|
#145 = VECTOR('NONE', #144, 1);
|
||||||
#146 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, 0.1016));
|
#146 = CARTESIAN_POINT('NONE', (3.9412591419317424, -1.6129, 2.58064));
|
||||||
#147 = LINE('NONE', #146, #145);
|
#147 = LINE('NONE', #146, #145);
|
||||||
#148 = DIRECTION('NONE', (0.90630778703665, 0.4226182617406993, 0));
|
#148 = DIRECTION('NONE', (0.90630778703665, 0.4226182617406992, 0));
|
||||||
#149 = VECTOR('NONE', #148, 1);
|
#149 = VECTOR('NONE', #148, 1);
|
||||||
#150 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
|
#150 = CARTESIAN_POINT('NONE', (1.6377992218573856, 0, -0));
|
||||||
#151 = LINE('NONE', #150, #149);
|
#151 = LINE('NONE', #150, #149);
|
||||||
#152 = DIRECTION('NONE', (0, 0, 1));
|
#152 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#153 = VECTOR('NONE', #152, 1);
|
#153 = VECTOR('NONE', #152, 1);
|
||||||
#154 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, -0));
|
#154 = CARTESIAN_POINT('NONE', (3.7131243491113075, 0.9677400000000002, -0));
|
||||||
#155 = LINE('NONE', #154, #153);
|
#155 = LINE('NONE', #154, #153);
|
||||||
#156 = DIRECTION('NONE', (0.90630778703665, 0.4226182617406993, 0));
|
#156 = DIRECTION('NONE', (0.90630778703665, 0.4226182617406992, 0));
|
||||||
#157 = VECTOR('NONE', #156, 1);
|
#157 = VECTOR('NONE', #156, 1);
|
||||||
#158 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, 0.1016));
|
#158 = CARTESIAN_POINT('NONE', (1.6377992218573856, 0, 2.58064));
|
||||||
#159 = LINE('NONE', #158, #157);
|
#159 = LINE('NONE', #158, #157);
|
||||||
#160 = DIRECTION('NONE', (1, -0.00000000000000007295344279228718, 0));
|
#160 = DIRECTION('NONE', (1, -0.0000000000000001378647737807002, 0));
|
||||||
#161 = VECTOR('NONE', #160, 1);
|
#161 = VECTOR('NONE', #160, 1);
|
||||||
#162 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, -0));
|
#162 = CARTESIAN_POINT('NONE', (3.7131243491113075, 0.9677400000000002, -0));
|
||||||
#163 = LINE('NONE', #162, #161);
|
#163 = LINE('NONE', #162, #161);
|
||||||
#164 = DIRECTION('NONE', (0, 0, 1));
|
#164 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#165 = VECTOR('NONE', #164, 1);
|
#165 = VECTOR('NONE', #164, 1);
|
||||||
#166 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, -0));
|
#166 = CARTESIAN_POINT('NONE', (6.12902, 0.9677399999999998, -0));
|
||||||
#167 = LINE('NONE', #166, #165);
|
#167 = LINE('NONE', #166, #165);
|
||||||
#168 = DIRECTION('NONE', (1, -0.00000000000000007295344279228718, 0));
|
#168 = DIRECTION('NONE', (1, -0.0000000000000001378647737807002, 0));
|
||||||
#169 = VECTOR('NONE', #168, 1);
|
#169 = VECTOR('NONE', #168, 1);
|
||||||
#170 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, 0.1016));
|
#170 = CARTESIAN_POINT('NONE', (3.7131243491113075, 0.9677400000000002, 2.58064));
|
||||||
#171 = LINE('NONE', #170, #169);
|
#171 = LINE('NONE', #170, #169);
|
||||||
#172 = DIRECTION('NONE', (0, 1, 0));
|
#172 = DIRECTION('NONE', (0, 1, 0));
|
||||||
#173 = VECTOR('NONE', #172, 1);
|
#173 = VECTOR('NONE', #172, 1);
|
||||||
#174 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, -0));
|
#174 = CARTESIAN_POINT('NONE', (6.12902, 0.9677399999999998, -0));
|
||||||
#175 = LINE('NONE', #174, #173);
|
#175 = LINE('NONE', #174, #173);
|
||||||
#176 = DIRECTION('NONE', (0, 0, 1));
|
#176 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#177 = VECTOR('NONE', #176, 1);
|
#177 = VECTOR('NONE', #176, 1);
|
||||||
#178 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
|
#178 = CARTESIAN_POINT('NONE', (6.12902, 1.29032, -0));
|
||||||
#179 = LINE('NONE', #178, #177);
|
#179 = LINE('NONE', #178, #177);
|
||||||
#180 = DIRECTION('NONE', (0, 1, 0));
|
#180 = DIRECTION('NONE', (0, 1, 0));
|
||||||
#181 = VECTOR('NONE', #180, 1);
|
#181 = VECTOR('NONE', #180, 1);
|
||||||
#182 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, 0.1016));
|
#182 = CARTESIAN_POINT('NONE', (6.12902, 0.9677399999999998, 2.58064));
|
||||||
#183 = LINE('NONE', #182, #181);
|
#183 = LINE('NONE', #182, #181);
|
||||||
#184 = DIRECTION('NONE', (-1, 0, 0));
|
#184 = DIRECTION('NONE', (-1, 0, 0));
|
||||||
#185 = VECTOR('NONE', #184, 1);
|
#185 = VECTOR('NONE', #184, 1);
|
||||||
#186 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
|
#186 = CARTESIAN_POINT('NONE', (6.12902, 1.29032, -0));
|
||||||
#187 = LINE('NONE', #186, #185);
|
#187 = LINE('NONE', #186, #185);
|
||||||
#188 = DIRECTION('NONE', (0, 0, 1));
|
#188 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#189 = VECTOR('NONE', #188, 1);
|
#189 = VECTOR('NONE', #188, 1);
|
||||||
#190 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
|
#190 = CARTESIAN_POINT('NONE', (3.6416100848359463, 1.29032, -0));
|
||||||
#191 = LINE('NONE', #190, #189);
|
#191 = LINE('NONE', #190, #189);
|
||||||
#192 = DIRECTION('NONE', (-1, 0, 0));
|
#192 = DIRECTION('NONE', (-1, 0, 0));
|
||||||
#193 = VECTOR('NONE', #192, 1);
|
#193 = VECTOR('NONE', #192, 1);
|
||||||
#194 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, 0.1016));
|
#194 = CARTESIAN_POINT('NONE', (6.12902, 1.29032, 2.58064));
|
||||||
#195 = LINE('NONE', #194, #193);
|
#195 = LINE('NONE', #194, #193);
|
||||||
#196 = DIRECTION('NONE', (-0.90630778703665, -0.42261826174069944, 0));
|
#196 = DIRECTION('NONE', (-0.90630778703665, -0.4226182617406995, 0));
|
||||||
#197 = VECTOR('NONE', #196, 1);
|
#197 = VECTOR('NONE', #196, 1);
|
||||||
#198 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
|
#198 = CARTESIAN_POINT('NONE', (3.6416100848359463, 1.29032, -0));
|
||||||
#199 = LINE('NONE', #198, #197);
|
#199 = LINE('NONE', #198, #197);
|
||||||
#200 = DIRECTION('NONE', (0, 0, 1));
|
#200 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#201 = VECTOR('NONE', #200, 1);
|
#201 = VECTOR('NONE', #200, 1);
|
||||||
#202 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
|
#202 = CARTESIAN_POINT('NONE', (2.2580599999999995, 0.64516, -0));
|
||||||
#203 = LINE('NONE', #202, #201);
|
#203 = LINE('NONE', #202, #201);
|
||||||
#204 = DIRECTION('NONE', (-0.90630778703665, -0.42261826174069944, 0));
|
#204 = DIRECTION('NONE', (-0.90630778703665, -0.4226182617406995, 0));
|
||||||
#205 = VECTOR('NONE', #204, 1);
|
#205 = VECTOR('NONE', #204, 1);
|
||||||
#206 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, 0.1016));
|
#206 = CARTESIAN_POINT('NONE', (3.6416100848359463, 1.29032, 2.58064));
|
||||||
#207 = LINE('NONE', #206, #205);
|
#207 = LINE('NONE', #206, #205);
|
||||||
#208 = DIRECTION('NONE', (-1, 0, 0));
|
#208 = DIRECTION('NONE', (-1, 0, 0));
|
||||||
#209 = VECTOR('NONE', #208, 1);
|
#209 = VECTOR('NONE', #208, 1);
|
||||||
#210 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
|
#210 = CARTESIAN_POINT('NONE', (2.2580599999999995, 0.64516, -0));
|
||||||
#211 = LINE('NONE', #210, #209);
|
#211 = LINE('NONE', #210, #209);
|
||||||
#212 = DIRECTION('NONE', (0, 0, 1));
|
#212 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#213 = VECTOR('NONE', #212, 1);
|
#213 = VECTOR('NONE', #212, 1);
|
||||||
#214 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
|
#214 = CARTESIAN_POINT('NONE', (0, 0.64516, -0));
|
||||||
#215 = LINE('NONE', #214, #213);
|
#215 = LINE('NONE', #214, #213);
|
||||||
#216 = DIRECTION('NONE', (-1, 0, 0));
|
#216 = DIRECTION('NONE', (-1, 0, 0));
|
||||||
#217 = VECTOR('NONE', #216, 1);
|
#217 = VECTOR('NONE', #216, 1);
|
||||||
#218 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, 0.1016));
|
#218 = CARTESIAN_POINT('NONE', (2.2580599999999995, 0.64516, 2.58064));
|
||||||
#219 = LINE('NONE', #218, #217);
|
#219 = LINE('NONE', #218, #217);
|
||||||
#220 = DIRECTION('NONE', (0, -1, 0));
|
#220 = DIRECTION('NONE', (0, -1, 0));
|
||||||
#221 = VECTOR('NONE', #220, 1);
|
#221 = VECTOR('NONE', #220, 1);
|
||||||
#222 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
|
#222 = CARTESIAN_POINT('NONE', (0, 0.64516, -0));
|
||||||
#223 = LINE('NONE', #222, #221);
|
#223 = LINE('NONE', #222, #221);
|
||||||
#224 = DIRECTION('NONE', (0, -1, 0));
|
#224 = DIRECTION('NONE', (0, -1, 0));
|
||||||
#225 = VECTOR('NONE', #224, 1);
|
#225 = VECTOR('NONE', #224, 1);
|
||||||
#226 = CARTESIAN_POINT('NONE', (0, 0.0254, 0.1016));
|
#226 = CARTESIAN_POINT('NONE', (0, 0.64516, 2.58064));
|
||||||
#227 = LINE('NONE', #226, #225);
|
#227 = LINE('NONE', #226, #225);
|
||||||
#228 = EDGE_CURVE('NONE', #5, #7, #63, .T.);
|
#228 = EDGE_CURVE('NONE', #5, #7, #63, .T.);
|
||||||
#229 = EDGE_CURVE('NONE', #7, #9, #67, .T.);
|
#229 = EDGE_CURVE('NONE', #7, #9, #67, .T.);
|
||||||
@ -383,67 +383,67 @@ DATA;
|
|||||||
#367 = ORIENTED_EDGE('NONE', *, *, #267, .T.);
|
#367 = ORIENTED_EDGE('NONE', *, *, #267, .T.);
|
||||||
#368 = ORIENTED_EDGE('NONE', *, *, #269, .T.);
|
#368 = ORIENTED_EDGE('NONE', *, *, #269, .T.);
|
||||||
#369 = EDGE_LOOP('NONE', (#355, #356, #357, #358, #359, #360, #361, #362, #363, #364, #365, #366, #367, #368));
|
#369 = EDGE_LOOP('NONE', (#355, #356, #357, #358, #359, #360, #361, #362, #363, #364, #365, #366, #367, #368));
|
||||||
#370 = CARTESIAN_POINT('NONE', (0, -0.0127, 0.0508));
|
#370 = CARTESIAN_POINT('NONE', (0, -0.3225799999999985, 1.2903199999999995));
|
||||||
#371 = DIRECTION('NONE', (-1, 0, -0));
|
#371 = DIRECTION('NONE', (-1, -0, 0));
|
||||||
#372 = AXIS2_PLACEMENT_3D('NONE', #370, #371, $);
|
#372 = AXIS2_PLACEMENT_3D('NONE', #370, #371, $);
|
||||||
#373 = PLANE('NONE', #372);
|
#373 = PLANE('NONE', #372);
|
||||||
#374 = CARTESIAN_POINT('NONE', (0.039306734695977924, -0.025399999999999995, 0.0508));
|
#374 = CARTESIAN_POINT('NONE', (0.9983910612778368, -0.6451599999999998, 1.2903199999999997));
|
||||||
#375 = DIRECTION('NONE', (0, -1, -0));
|
#375 = DIRECTION('NONE', (0, -1, 0));
|
||||||
#376 = AXIS2_PLACEMENT_3D('NONE', #374, #375, $);
|
#376 = AXIS2_PLACEMENT_3D('NONE', #374, #375, $);
|
||||||
#377 = PLANE('NONE', #376);
|
#377 = PLANE('NONE', #376);
|
||||||
#378 = CARTESIAN_POINT('NONE', (0.11488842876320533, -0.05079999999999996, 0.05079999999999999));
|
#378 = CARTESIAN_POINT('NONE', (2.918166090585415, -1.2903199999999988, 1.2903199999999997));
|
||||||
#379 = DIRECTION('NONE', (-0.5735764363510459, -0.819152044288992, 0));
|
#379 = DIRECTION('NONE', (-0.5735764363510459, -0.8191520442889919, 0));
|
||||||
#380 = AXIS2_PLACEMENT_3D('NONE', #378, #379, $);
|
#380 = AXIS2_PLACEMENT_3D('NONE', #378, #379, $);
|
||||||
#381 = PLANE('NONE', #380);
|
#381 = PLANE('NONE', #380);
|
||||||
#382 = CARTESIAN_POINT('NONE', (0.19623169406722757, -0.07619999999999999, 0.0508));
|
#382 = CARTESIAN_POINT('NONE', (4.984285029307579, -1.9354799999999992, 1.2903199999999997));
|
||||||
#383 = DIRECTION('NONE', (0, -1, -0));
|
#383 = DIRECTION('NONE', (0, -1, 0));
|
||||||
#384 = AXIS2_PLACEMENT_3D('NONE', #382, #383, $);
|
#384 = AXIS2_PLACEMENT_3D('NONE', #382, #383, $);
|
||||||
#385 = PLANE('NONE', #384);
|
#385 = PLANE('NONE', #384);
|
||||||
#386 = CARTESIAN_POINT('NONE', (0.2413, -0.06985, 0.0508));
|
#386 = CARTESIAN_POINT('NONE', (6.129019999999999, -1.7741899999999997, 1.2903199999999997));
|
||||||
#387 = DIRECTION('NONE', (1, 0, -0));
|
#387 = DIRECTION('NONE', (1, -0, 0));
|
||||||
#388 = AXIS2_PLACEMENT_3D('NONE', #386, #387, $);
|
#388 = AXIS2_PLACEMENT_3D('NONE', #386, #387, $);
|
||||||
#389 = PLANE('NONE', #388);
|
#389 = PLANE('NONE', #388);
|
||||||
#390 = CARTESIAN_POINT('NONE', (0.19823384137660915, -0.0635, 0.0508));
|
#390 = CARTESIAN_POINT('NONE', (5.035139570965871, -1.6128999999999998, 1.2903199999999997));
|
||||||
#391 = DIRECTION('NONE', (0, 1, -0));
|
#391 = DIRECTION('NONE', (0, 1, -0));
|
||||||
#392 = AXIS2_PLACEMENT_3D('NONE', #390, #391, $);
|
#392 = AXIS2_PLACEMENT_3D('NONE', #390, #391, $);
|
||||||
#393 = PLANE('NONE', #392);
|
#393 = PLANE('NONE', #392);
|
||||||
#394 = CARTESIAN_POINT('NONE', (0.10982398353915601, -0.03174999999999997, 0.0508));
|
#394 = CARTESIAN_POINT('NONE', (2.7895291818945633, -0.8064499999999998, 1.2903199999999995));
|
||||||
#395 = DIRECTION('NONE', (0.573576436351046, 0.8191520442889918, -0));
|
#395 = DIRECTION('NONE', (0.5735764363510459, 0.8191520442889918, -0));
|
||||||
#396 = AXIS2_PLACEMENT_3D('NONE', #394, #395, $);
|
#396 = AXIS2_PLACEMENT_3D('NONE', #394, #395, $);
|
||||||
#397 = PLANE('NONE', #396);
|
#397 = PLANE('NONE', #396);
|
||||||
#398 = CARTESIAN_POINT('NONE', (0.105333141160801, 0.019049999999999987, 0.0508));
|
#398 = CARTESIAN_POINT('NONE', (2.6754617854843468, 0.4838700000000003, 1.2903199999999997));
|
||||||
#399 = DIRECTION('NONE', (0.4226182617406993, -0.90630778703665, -0));
|
#399 = DIRECTION('NONE', (0.4226182617406992, -0.90630778703665, 0));
|
||||||
#400 = AXIS2_PLACEMENT_3D('NONE', #398, #399, $);
|
#400 = AXIS2_PLACEMENT_3D('NONE', #398, #399, $);
|
||||||
#401 = PLANE('NONE', #400);
|
#401 = PLANE('NONE', #400);
|
||||||
#402 = CARTESIAN_POINT('NONE', (0.19374299899825406, 0.0381, 0.0508));
|
#402 = CARTESIAN_POINT('NONE', (4.921072174555653, 0.9677399999999998, 1.2903199999999995));
|
||||||
#403 = DIRECTION('NONE', (0, -1, -0));
|
#403 = DIRECTION('NONE', (0, -1, 0));
|
||||||
#404 = AXIS2_PLACEMENT_3D('NONE', #402, #403, $);
|
#404 = AXIS2_PLACEMENT_3D('NONE', #402, #403, $);
|
||||||
#405 = PLANE('NONE', #404);
|
#405 = PLANE('NONE', #404);
|
||||||
#406 = CARTESIAN_POINT('NONE', (0.2413, 0.044449999999999996, 0.0508));
|
#406 = CARTESIAN_POINT('NONE', (6.129019999999998, 1.1290299999999989, 1.2903199999999995));
|
||||||
#407 = DIRECTION('NONE', (1, 0, -0));
|
#407 = DIRECTION('NONE', (1, -0, 0));
|
||||||
#408 = AXIS2_PLACEMENT_3D('NONE', #406, #407, $);
|
#408 = AXIS2_PLACEMENT_3D('NONE', #406, #407, $);
|
||||||
#409 = PLANE('NONE', #408);
|
#409 = PLANE('NONE', #408);
|
||||||
#410 = CARTESIAN_POINT('NONE', (0.19233523789047138, 0.0508, 0.0508));
|
#410 = CARTESIAN_POINT('NONE', (4.8853150424179725, 1.2903199999999997, 1.2903199999999997));
|
||||||
#411 = DIRECTION('NONE', (0, 1, -0));
|
#411 = DIRECTION('NONE', (0, 1, -0));
|
||||||
#412 = AXIS2_PLACEMENT_3D('NONE', #410, #411, $);
|
#412 = AXIS2_PLACEMENT_3D('NONE', #410, #411, $);
|
||||||
#413 = PLANE('NONE', #412);
|
#413 = PLANE('NONE', #412);
|
||||||
#414 = CARTESIAN_POINT('NONE', (0.11613523789047137, 0.0381, 0.05079999999999999));
|
#414 = CARTESIAN_POINT('NONE', (2.9498350424179733, 0.9677399999999998, 1.2903199999999997));
|
||||||
#415 = DIRECTION('NONE', (-0.42261826174069966, 0.90630778703665, -0));
|
#415 = DIRECTION('NONE', (-0.42261826174069933, 0.9063077870366499, -0));
|
||||||
#416 = AXIS2_PLACEMENT_3D('NONE', #414, #415, $);
|
#416 = AXIS2_PLACEMENT_3D('NONE', #414, #415, $);
|
||||||
#417 = PLANE('NONE', #416);
|
#417 = PLANE('NONE', #416);
|
||||||
#418 = CARTESIAN_POINT('NONE', (0.044449999999999996, 0.0254, 0.0508));
|
#418 = CARTESIAN_POINT('NONE', (1.1290299999999998, 0.6451599999999998, 1.29032));
|
||||||
#419 = DIRECTION('NONE', (0, 1, -0));
|
#419 = DIRECTION('NONE', (0, 1, -0));
|
||||||
#420 = AXIS2_PLACEMENT_3D('NONE', #418, #419, $);
|
#420 = AXIS2_PLACEMENT_3D('NONE', #418, #419, $);
|
||||||
#421 = PLANE('NONE', #420);
|
#421 = PLANE('NONE', #420);
|
||||||
#422 = CARTESIAN_POINT('NONE', (0, 0.0127, 0.0508));
|
#422 = CARTESIAN_POINT('NONE', (0, 0.32257999999999987, 1.2903199999999995));
|
||||||
#423 = DIRECTION('NONE', (-1, 0, -0));
|
#423 = DIRECTION('NONE', (-1, -0, 0));
|
||||||
#424 = AXIS2_PLACEMENT_3D('NONE', #422, #423, $);
|
#424 = AXIS2_PLACEMENT_3D('NONE', #422, #423, $);
|
||||||
#425 = PLANE('NONE', #424);
|
#425 = PLANE('NONE', #424);
|
||||||
#426 = CARTESIAN_POINT('NONE', (0, 0, -0));
|
#426 = CARTESIAN_POINT('NONE', (0, 0, -0));
|
||||||
#427 = DIRECTION('NONE', (0, 0, 1));
|
#427 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#428 = AXIS2_PLACEMENT_3D('NONE', #426, #427, $);
|
#428 = AXIS2_PLACEMENT_3D('NONE', #426, #427, $);
|
||||||
#429 = PLANE('NONE', #428);
|
#429 = PLANE('NONE', #428);
|
||||||
#430 = CARTESIAN_POINT('NONE', (0, 0, 0.1016));
|
#430 = CARTESIAN_POINT('NONE', (0, 0, 2.58064));
|
||||||
#431 = DIRECTION('NONE', (0, 0, 1));
|
#431 = DIRECTION('NONE', (0, 0, 1));
|
||||||
#432 = AXIS2_PLACEMENT_3D('NONE', #430, #431, $);
|
#432 = AXIS2_PLACEMENT_3D('NONE', #430, #431, $);
|
||||||
#433 = PLANE('NONE', #432);
|
#433 = PLANE('NONE', #432);
|
||||||
@ -475,7 +475,7 @@ DATA;
|
|||||||
#459 = ADVANCED_FACE('NONE', (#458), #421, .T.);
|
#459 = ADVANCED_FACE('NONE', (#458), #421, .T.);
|
||||||
#460 = FACE_OUTER_BOUND('NONE', #339, .T.);
|
#460 = FACE_OUTER_BOUND('NONE', #339, .T.);
|
||||||
#461 = ADVANCED_FACE('NONE', (#460), #425, .T.);
|
#461 = ADVANCED_FACE('NONE', (#460), #425, .T.);
|
||||||
#462 = FACE_OUTER_BOUND('NONE', #354, .T.);
|
#462 = FACE_OUTER_BOUND('NONE', #354, .F.);
|
||||||
#463 = ADVANCED_FACE('NONE', (#462), #429, .F.);
|
#463 = ADVANCED_FACE('NONE', (#462), #429, .F.);
|
||||||
#464 = FACE_OUTER_BOUND('NONE', #369, .T.);
|
#464 = FACE_OUTER_BOUND('NONE', #369, .T.);
|
||||||
#465 = ADVANCED_FACE('NONE', (#464), #433, .T.);
|
#465 = ADVANCED_FACE('NONE', (#464), #433, .T.);
|
||||||
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 221 KiB |
@ -1,478 +1,478 @@
|
|||||||
solid unnamed
|
solid unnamed
|
||||||
facet normal -1 0 0
|
facet normal -1 0 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 0 -4 0
|
vertex 0 -101.600006 0
|
||||||
vertex 0 -0 0
|
vertex 0 -0 0
|
||||||
vertex 0 -4 -1
|
vertex 0 -101.600006 -25.400002
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal -1 0 0
|
facet normal -1 0 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 0 -4 -1
|
vertex 0 -101.600006 -25.400002
|
||||||
vertex 0 -0 0
|
vertex 0 -0 0
|
||||||
vertex 0 -0 -1
|
vertex 0 -0 -25.400002
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 0 -1
|
facet normal 0 0 -1
|
||||||
outer loop
|
outer loop
|
||||||
vertex 0 -4 -1
|
vertex 0 -101.600006 -25.400002
|
||||||
vertex 0 -0 -1
|
vertex 0 -0 -25.400002
|
||||||
vertex 3.0950184 -4 -1
|
vertex 78.613464 -101.600006 -25.400002
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 0 -1
|
facet normal 0 0 -1
|
||||||
outer loop
|
outer loop
|
||||||
vertex 3.0950184 -4 -1
|
vertex 78.613464 -101.600006 -25.400002
|
||||||
vertex 0 -0 -1
|
vertex 0 -0 -25.400002
|
||||||
vertex 3.0950184 -0 -1
|
vertex 78.613464 -0 -25.400002
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal -0.57357645 0 -0.81915206
|
facet normal -0.5735764 0 -0.8191522
|
||||||
outer loop
|
outer loop
|
||||||
vertex 3.0950184 -4 -1
|
vertex 78.613464 -101.600006 -25.400002
|
||||||
vertex 3.0950184 -0 -1
|
vertex 78.613464 -0 -25.400002
|
||||||
vertex 5.9513144 -4 -3
|
vertex 151.16339 -101.600006 -76.2
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal -0.57357645 0 -0.81915206
|
facet normal -0.5735764 0 -0.8191522
|
||||||
outer loop
|
outer loop
|
||||||
vertex 5.9513144 -4 -3
|
vertex 151.16339 -101.600006 -76.2
|
||||||
vertex 3.0950184 -0 -1
|
vertex 78.613464 -0 -25.400002
|
||||||
vertex 5.9513144 -0 -3
|
vertex 151.16339 -0 -76.2
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 0 -1
|
facet normal 0 0 -1
|
||||||
outer loop
|
outer loop
|
||||||
vertex 5.9513144 -4 -3
|
vertex 151.16339 -101.600006 -76.2
|
||||||
vertex 5.9513144 -0 -3
|
vertex 151.16339 -0 -76.2
|
||||||
vertex 9.5 -4 -3
|
vertex 241.3 -101.600006 -76.2
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 0 -1
|
facet normal 0 0 -1
|
||||||
outer loop
|
outer loop
|
||||||
vertex 9.5 -4 -3
|
vertex 241.3 -101.600006 -76.2
|
||||||
vertex 5.9513144 -0 -3
|
vertex 151.16339 -0 -76.2
|
||||||
vertex 9.5 -0 -3
|
vertex 241.3 -0 -76.2
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 1 0 0
|
facet normal 1 0 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 9.5 -4 -3
|
vertex 241.3 -101.600006 -76.2
|
||||||
vertex 9.5 -0 -3
|
vertex 241.3 -0 -76.2
|
||||||
vertex 9.5 -4 -2.5
|
vertex 241.3 -101.600006 -63.5
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 1 -0 0
|
facet normal 1 -0 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 9.5 -4 -2.5
|
vertex 241.3 -101.600006 -63.5
|
||||||
vertex 9.5 -0 -3
|
vertex 241.3 -0 -76.2
|
||||||
vertex 9.5 -0 -2.5
|
vertex 241.3 -0 -63.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -0 1
|
||||||
|
outer loop
|
||||||
|
vertex 241.3 -101.600006 -63.5
|
||||||
|
vertex 241.3 -0 -63.5
|
||||||
|
vertex 155.16768 -101.600006 -63.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 0 1
|
||||||
|
outer loop
|
||||||
|
vertex 155.16768 -101.600006 -63.5
|
||||||
|
vertex 241.3 -0 -63.5
|
||||||
|
vertex 155.16768 -0 -63.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.5735765 0 0.81915194
|
||||||
|
outer loop
|
||||||
|
vertex 87.15214 -101.600006 -15.875
|
||||||
|
vertex 109.82398 -101.600006 -31.75
|
||||||
|
vertex 109.82398 -0 -31.75
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.57357645 0 0.819152
|
||||||
|
outer loop
|
||||||
|
vertex 109.82398 -101.600006 -31.75
|
||||||
|
vertex 155.16768 -101.600006 -63.5
|
||||||
|
vertex 155.16768 -0 -63.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.57357645 0 0.81915206
|
||||||
|
outer loop
|
||||||
|
vertex 87.15214 -0 -15.875
|
||||||
|
vertex 64.480286 -101.600006 0
|
||||||
|
vertex 87.15214 -101.600006 -15.875
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.5735765 0 0.81915194
|
||||||
|
outer loop
|
||||||
|
vertex 109.82398 -0 -31.75
|
||||||
|
vertex 87.15214 -0 -15.875
|
||||||
|
vertex 87.15214 -101.600006 -15.875
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.57357645 -0 0.819152
|
||||||
|
outer loop
|
||||||
|
vertex 109.82398 -101.600006 -31.75
|
||||||
|
vertex 155.16768 -0 -63.5
|
||||||
|
vertex 109.82398 -0 -31.75
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.57357645 -0 0.81915206
|
||||||
|
outer loop
|
||||||
|
vertex 64.480286 -101.600006 0
|
||||||
|
vertex 87.15214 -0 -15.875
|
||||||
|
vertex 64.480286 -0 0
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.4226182 0 -0.9063078
|
||||||
|
outer loop
|
||||||
|
vertex 84.906715 -101.600006 9.525
|
||||||
|
vertex 64.480286 -101.600006 0
|
||||||
|
vertex 64.480286 -0 0
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.42261833 0 -0.90630776
|
||||||
|
outer loop
|
||||||
|
vertex 105.33314 -101.600006 19.05
|
||||||
|
vertex 84.906715 -101.600006 9.525
|
||||||
|
vertex 84.906715 -0 9.525
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.4226182 0 -0.9063078
|
||||||
|
outer loop
|
||||||
|
vertex 84.906715 -0 9.525
|
||||||
|
vertex 84.906715 -101.600006 9.525
|
||||||
|
vertex 64.480286 -0 0
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.4226183 0 -0.9063078
|
||||||
|
outer loop
|
||||||
|
vertex 105.33314 -0 19.05
|
||||||
|
vertex 146.18599 -101.600006 38.1
|
||||||
|
vertex 105.33314 -101.600006 19.05
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.42261833 0 -0.90630776
|
||||||
|
outer loop
|
||||||
|
vertex 105.33314 -101.600006 19.05
|
||||||
|
vertex 84.906715 -0 9.525
|
||||||
|
vertex 105.33314 -0 19.05
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0.4226183 0 -0.9063078
|
||||||
|
outer loop
|
||||||
|
vertex 146.18599 -101.600006 38.1
|
||||||
|
vertex 105.33314 -0 19.05
|
||||||
|
vertex 146.18599 -0 38.1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 0 -1
|
||||||
|
outer loop
|
||||||
|
vertex 146.18599 -101.600006 38.1
|
||||||
|
vertex 146.18599 -0 38.1
|
||||||
|
vertex 241.3 -101.600006 38.1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 0 -1
|
||||||
|
outer loop
|
||||||
|
vertex 241.3 -101.600006 38.1
|
||||||
|
vertex 146.18599 -0 38.1
|
||||||
|
vertex 241.3 -0 38.1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 1 0 0
|
||||||
|
outer loop
|
||||||
|
vertex 241.3 -101.600006 38.1
|
||||||
|
vertex 241.3 -0 38.1
|
||||||
|
vertex 241.3 -101.600006 50.800003
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 1 -0 0
|
||||||
|
outer loop
|
||||||
|
vertex 241.3 -101.600006 50.800003
|
||||||
|
vertex 241.3 -0 38.1
|
||||||
|
vertex 241.3 -0 50.800003
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 -0 0.99999994
|
facet normal 0 -0 0.99999994
|
||||||
outer loop
|
outer loop
|
||||||
vertex 9.5 -4 -2.5
|
vertex 241.3 -101.600006 50.800003
|
||||||
vertex 9.5 -0 -2.5
|
vertex 241.3 -0 50.800003
|
||||||
vertex 6.108964 -4 -2.5
|
vertex 143.37048 -101.600006 50.800003
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 0 0.99999994
|
facet normal 0 0 0.99999994
|
||||||
outer loop
|
outer loop
|
||||||
vertex 6.108964 -4 -2.5
|
vertex 143.37048 -101.600006 50.800003
|
||||||
vertex 9.5 -0 -2.5
|
vertex 241.3 -0 50.800003
|
||||||
vertex 6.108964 -0 -2.5
|
vertex 143.37048 -0 50.800003
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0.5735763 0 0.8191522
|
facet normal -0.42261827 0 0.9063078
|
||||||
outer loop
|
outer loop
|
||||||
vertex 3.4311862 -4 -0.625
|
vertex 143.37048 -101.600006 50.800003
|
||||||
vertex 4.323779 -4 -1.25
|
vertex 143.37048 -0 50.800003
|
||||||
vertex 4.323779 -0 -1.25
|
vertex 88.9 -101.600006 25.400002
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0.57357645 0 0.819152
|
facet normal -0.42261827 0 0.9063078
|
||||||
outer loop
|
outer loop
|
||||||
vertex 4.323779 -4 -1.25
|
vertex 88.9 -101.600006 25.400002
|
||||||
vertex 6.108964 -4 -2.5
|
vertex 143.37048 -0 50.800003
|
||||||
vertex 6.108964 -0 -2.5
|
vertex 88.9 -0 25.400002
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0.57357645 0 0.819152
|
|
||||||
outer loop
|
|
||||||
vertex 3.4311862 -0 -0.625
|
|
||||||
vertex 2.5385938 -0 0
|
|
||||||
vertex 2.5385938 -4 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0.57357645 -0 0.819152
|
|
||||||
outer loop
|
|
||||||
vertex 3.4311862 -4 -0.625
|
|
||||||
vertex 3.4311862 -0 -0.625
|
|
||||||
vertex 2.5385938 -4 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0.57357645 -0 0.819152
|
|
||||||
outer loop
|
|
||||||
vertex 4.323779 -4 -1.25
|
|
||||||
vertex 6.108964 -0 -2.5
|
|
||||||
vertex 4.323779 -0 -1.25
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0.5735763 0 0.8191522
|
|
||||||
outer loop
|
|
||||||
vertex 3.4311862 -0 -0.625
|
|
||||||
vertex 3.4311862 -4 -0.625
|
|
||||||
vertex 4.323779 -0 -1.25
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0.42261824 0 -0.9063078
|
|
||||||
outer loop
|
|
||||||
vertex 3.342784 -4 0.375
|
|
||||||
vertex 2.5385938 -4 0
|
|
||||||
vertex 2.5385938 -0 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0.42261824 0 -0.9063078
|
|
||||||
outer loop
|
|
||||||
vertex 4.146974 -4 0.75
|
|
||||||
vertex 3.342784 -4 0.375
|
|
||||||
vertex 3.342784 -0 0.375
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0.42261824 0 -0.9063078
|
|
||||||
outer loop
|
|
||||||
vertex 3.342784 -0 0.375
|
|
||||||
vertex 4.146974 -0 0.75
|
|
||||||
vertex 4.146974 -4 0.75
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0.42261833 0 -0.90630776
|
|
||||||
outer loop
|
|
||||||
vertex 4.146974 -0 0.75
|
|
||||||
vertex 5.755354 -0 1.5
|
|
||||||
vertex 5.755354 -4 1.5
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0.42261824 0 -0.9063078
|
|
||||||
outer loop
|
|
||||||
vertex 3.342784 -4 0.375
|
|
||||||
vertex 2.5385938 -0 0
|
|
||||||
vertex 3.342784 -0 0.375
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0.42261833 0 -0.90630776
|
|
||||||
outer loop
|
|
||||||
vertex 5.755354 -4 1.5
|
|
||||||
vertex 4.146974 -4 0.75
|
|
||||||
vertex 4.146974 -0 0.75
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 0 -1
|
|
||||||
outer loop
|
|
||||||
vertex 5.755354 -4 1.5
|
|
||||||
vertex 5.755354 -0 1.5
|
|
||||||
vertex 9.5 -4 1.5
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 0 -1
|
|
||||||
outer loop
|
|
||||||
vertex 9.5 -4 1.5
|
|
||||||
vertex 5.755354 -0 1.5
|
|
||||||
vertex 9.5 -0 1.5
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 1 0 0
|
|
||||||
outer loop
|
|
||||||
vertex 9.5 -4 1.5
|
|
||||||
vertex 9.5 -0 1.5
|
|
||||||
vertex 9.5 -4 2
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 1 -0 0
|
|
||||||
outer loop
|
|
||||||
vertex 9.5 -4 2
|
|
||||||
vertex 9.5 -0 1.5
|
|
||||||
vertex 9.5 -0 2
|
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 -0 1
|
facet normal 0 -0 1
|
||||||
outer loop
|
outer loop
|
||||||
vertex 9.5 -4 2
|
vertex 88.9 -101.600006 25.400002
|
||||||
vertex 9.5 -0 2
|
vertex 88.9 -0 25.400002
|
||||||
vertex 5.644507 -4 2
|
vertex 0 -101.600006 25.400002
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 0 1
|
facet normal 0 0 1
|
||||||
outer loop
|
outer loop
|
||||||
vertex 5.644507 -4 2
|
vertex 0 -101.600006 25.400002
|
||||||
vertex 9.5 -0 2
|
vertex 88.9 -0 25.400002
|
||||||
vertex 5.644507 -0 2
|
vertex 0 -0 25.400002
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0.42261824 0 0.90630776
|
|
||||||
outer loop
|
|
||||||
vertex 5.644507 -4 2
|
|
||||||
vertex 5.644507 -0 2
|
|
||||||
vertex 3.5 -4 1
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0.42261824 0 0.90630776
|
|
||||||
outer loop
|
|
||||||
vertex 3.5 -4 1
|
|
||||||
vertex 5.644507 -0 2
|
|
||||||
vertex 3.5 -0 1
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 -0 1
|
|
||||||
outer loop
|
|
||||||
vertex 3.5 -4 1
|
|
||||||
vertex 3.5 -0 1
|
|
||||||
vertex 0 -4 1
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 0 1
|
|
||||||
outer loop
|
|
||||||
vertex 0 -4 1
|
|
||||||
vertex 3.5 -0 1
|
|
||||||
vertex 0 -0 1
|
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal -1 0 0
|
facet normal -1 0 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 0 -4 1
|
vertex 0 -101.600006 25.400002
|
||||||
vertex 0 -0 1
|
vertex 0 -0 25.400002
|
||||||
vertex 0 -4 0
|
vertex 0 -101.600006 0
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal -1 0 0
|
facet normal -1 0 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 0 -4 0
|
vertex 0 -101.600006 0
|
||||||
vertex 0 -0 1
|
vertex 0 -0 25.400002
|
||||||
vertex 0 -0 0
|
vertex 0 -0 0
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 1 -0
|
facet normal 0 1 -0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 3.342784 -0 0.375
|
vertex 84.906715 -0 9.525
|
||||||
vertex 2.5385938 -0 0
|
vertex 64.480286 -0 0
|
||||||
vertex 3.5 -0 1
|
vertex 88.9 -0 25.400002
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 1 0
|
facet normal 0 1 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 3.4311862 -0 -0.625
|
vertex 105.33314 -0 19.05
|
||||||
vertex 4.323779 -0 -1.25
|
vertex 84.906715 -0 9.525
|
||||||
vertex 3.0950184 -0 -1
|
vertex 88.9 -0 25.400002
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 1 0
|
facet normal 0 1 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 3.342784 -0 0.375
|
vertex 87.15214 -0 -15.875
|
||||||
vertex 3.5 -0 1
|
vertex 109.82398 -0 -31.75
|
||||||
vertex 4.146974 -0 0.75
|
vertex 78.613464 -0 -25.400002
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 105.33314 -0 19.05
|
||||||
|
vertex 143.37048 -0 50.800003
|
||||||
|
vertex 146.18599 -0 38.1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal -0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 0 -0 25.400002
|
||||||
|
vertex 88.9 -0 25.400002
|
||||||
|
vertex 64.480286 -0 0
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 0 -0 25.400002
|
||||||
|
vertex 64.480286 -0 0
|
||||||
|
vertex 0 -0 0
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal -0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 143.37048 -0 50.800003
|
||||||
|
vertex 241.3 -0 50.800003
|
||||||
|
vertex 146.18599 -0 38.1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 241.3 -0 50.800003
|
||||||
|
vertex 241.3 -0 38.1
|
||||||
|
vertex 146.18599 -0 38.1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 -0
|
||||||
|
outer loop
|
||||||
|
vertex 105.33314 -0 19.05
|
||||||
|
vertex 88.9 -0 25.400002
|
||||||
|
vertex 143.37048 -0 50.800003
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 0.99999994 0
|
facet normal 0 0.99999994 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 4.323779 -0 -1.25
|
vertex 64.480286 -0 0
|
||||||
vertex 5.9513144 -0 -3
|
vertex 87.15214 -0 -15.875
|
||||||
vertex 3.0950184 -0 -1
|
vertex 78.613464 -0 -25.400002
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 1 0
|
facet normal 0 1 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 0 -0 -1
|
vertex 109.82398 -0 -31.75
|
||||||
vertex 2.5385938 -0 0
|
vertex 151.16339 -0 -76.2
|
||||||
vertex 3.0950184 -0 -1
|
vertex 78.613464 -0 -25.400002
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 1 0
|
facet normal 0 1 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 0 -0 -1
|
vertex 155.16768 -0 -63.5
|
||||||
|
vertex 151.16339 -0 -76.2
|
||||||
|
vertex 109.82398 -0 -31.75
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 241.3 -0 -63.5
|
||||||
|
vertex 241.3 -0 -76.2
|
||||||
|
vertex 155.16768 -0 -63.5
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 155.16768 -0 -63.5
|
||||||
|
vertex 241.3 -0 -76.2
|
||||||
|
vertex 151.16339 -0 -76.2
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 64.480286 -0 0
|
||||||
|
vertex 78.613464 -0 -25.400002
|
||||||
|
vertex 0 -0 -25.400002
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 1 0
|
||||||
|
outer loop
|
||||||
|
vertex 0 -0 -25.400002
|
||||||
vertex 0 -0 0
|
vertex 0 -0 0
|
||||||
vertex 2.5385938 -0 0
|
vertex 64.480286 -0 0
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 0.99999994 -0
|
|
||||||
outer loop
|
|
||||||
vertex 9.5 -0 -3
|
|
||||||
vertex 6.108964 -0 -2.5
|
|
||||||
vertex 9.5 -0 -2.5
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 1 0
|
|
||||||
outer loop
|
|
||||||
vertex 9.5 -0 -3
|
|
||||||
vertex 5.9513144 -0 -3
|
|
||||||
vertex 6.108964 -0 -2.5
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 1 -0
|
|
||||||
outer loop
|
|
||||||
vertex 5.9513144 -0 -3
|
|
||||||
vertex 4.323779 -0 -1.25
|
|
||||||
vertex 6.108964 -0 -2.5
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 1 0
|
|
||||||
outer loop
|
|
||||||
vertex 5.644507 -0 2
|
|
||||||
vertex 5.755354 -0 1.5
|
|
||||||
vertex 4.146974 -0 0.75
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 0.99999994 -0
|
|
||||||
outer loop
|
|
||||||
vertex 3.0950184 -0 -1
|
|
||||||
vertex 2.5385938 -0 0
|
|
||||||
vertex 3.4311862 -0 -0.625
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 1 -0
|
|
||||||
outer loop
|
|
||||||
vertex 4.146974 -0 0.75
|
|
||||||
vertex 3.5 -0 1
|
|
||||||
vertex 5.644507 -0 2
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 1 -0
|
|
||||||
outer loop
|
|
||||||
vertex 9.5 -0 1.5
|
|
||||||
vertex 5.755354 -0 1.5
|
|
||||||
vertex 9.5 -0 2
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 1 -0
|
|
||||||
outer loop
|
|
||||||
vertex 5.755354 -0 1.5
|
|
||||||
vertex 5.644507 -0 2
|
|
||||||
vertex 9.5 -0 2
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 1 0
|
|
||||||
outer loop
|
|
||||||
vertex 2.5385938 -0 0
|
|
||||||
vertex 0 -0 0
|
|
||||||
vertex 0 -0 1
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 1 0
|
|
||||||
outer loop
|
|
||||||
vertex 3.5 -0 1
|
|
||||||
vertex 2.5385938 -0 0
|
|
||||||
vertex 0 -0 1
|
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal -0 -1 0
|
facet normal -0 -1 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 3.342784 -4 0.375
|
vertex 84.906715 -101.600006 9.525
|
||||||
vertex 3.5 -4 1
|
vertex 88.9 -101.600006 25.400002
|
||||||
vertex 2.5385938 -4 0
|
vertex 64.480286 -101.600006 0
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal -0 -1 0
|
facet normal -0 -1 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 4.146974 -4 0.75
|
vertex 105.33314 -101.600006 19.05
|
||||||
vertex 3.5 -4 1
|
vertex 88.9 -101.600006 25.400002
|
||||||
vertex 3.342784 -4 0.375
|
vertex 84.906715 -101.600006 9.525
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 -1 -0
|
facet normal 0 -1 -0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 3.4311862 -4 -0.625
|
vertex 87.15214 -101.600006 -15.875
|
||||||
vertex 3.0950184 -4 -1
|
vertex 78.613464 -101.600006 -25.400002
|
||||||
vertex 4.323779 -4 -1.25
|
vertex 109.82398 -101.600006 -31.75
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -1 0
|
||||||
|
outer loop
|
||||||
|
vertex 105.33314 -101.600006 19.05
|
||||||
|
vertex 146.18599 -101.600006 38.1
|
||||||
|
vertex 143.37048 -101.600006 50.800003
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -1 0
|
||||||
|
outer loop
|
||||||
|
vertex 0 -101.600006 25.400002
|
||||||
|
vertex 64.480286 -101.600006 0
|
||||||
|
vertex 88.9 -101.600006 25.400002
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -1 0
|
||||||
|
outer loop
|
||||||
|
vertex 0 -101.600006 25.400002
|
||||||
|
vertex 0 -101.600006 0
|
||||||
|
vertex 64.480286 -101.600006 0
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -1 0
|
||||||
|
outer loop
|
||||||
|
vertex 143.37048 -101.600006 50.800003
|
||||||
|
vertex 146.18599 -101.600006 38.1
|
||||||
|
vertex 241.3 -101.600006 50.800003
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -1 -0
|
||||||
|
outer loop
|
||||||
|
vertex 241.3 -101.600006 50.800003
|
||||||
|
vertex 146.18599 -101.600006 38.1
|
||||||
|
vertex 241.3 -101.600006 38.1
|
||||||
|
endloop
|
||||||
|
endfacet
|
||||||
|
facet normal 0 -1 0
|
||||||
|
outer loop
|
||||||
|
vertex 105.33314 -101.600006 19.05
|
||||||
|
vertex 143.37048 -101.600006 50.800003
|
||||||
|
vertex 88.9 -101.600006 25.400002
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 -0.99999994 0
|
facet normal 0 -0.99999994 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 4.146974 -4 0.75
|
vertex 64.480286 -101.600006 0
|
||||||
vertex 5.755354 -4 1.5
|
vertex 78.613464 -101.600006 -25.400002
|
||||||
vertex 5.644507 -4 2
|
vertex 87.15214 -101.600006 -15.875
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 -1 0
|
facet normal -0 -1 -0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 0 -4 1
|
vertex 109.82398 -101.600006 -31.75
|
||||||
vertex 2.5385938 -4 0
|
vertex 78.613464 -101.600006 -25.400002
|
||||||
vertex 3.5 -4 1
|
vertex 151.16339 -101.600006 -76.2
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 -1 0
|
|
||||||
outer loop
|
|
||||||
vertex 0 -4 1
|
|
||||||
vertex 0 -4 0
|
|
||||||
vertex 2.5385938 -4 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 -1 0
|
|
||||||
outer loop
|
|
||||||
vertex 5.644507 -4 2
|
|
||||||
vertex 5.755354 -4 1.5
|
|
||||||
vertex 9.5 -4 2
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 -1 -0
|
|
||||||
outer loop
|
|
||||||
vertex 9.5 -4 2
|
|
||||||
vertex 5.755354 -4 1.5
|
|
||||||
vertex 9.5 -4 1.5
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 -1 0
|
|
||||||
outer loop
|
|
||||||
vertex 4.146974 -4 0.75
|
|
||||||
vertex 5.644507 -4 2
|
|
||||||
vertex 3.5 -4 1
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 -0.99999994 0
|
|
||||||
outer loop
|
|
||||||
vertex 2.5385938 -4 0
|
|
||||||
vertex 3.0950184 -4 -1
|
|
||||||
vertex 3.4311862 -4 -0.625
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0 -0.99999994 -0
|
|
||||||
outer loop
|
|
||||||
vertex 4.323779 -4 -1.25
|
|
||||||
vertex 3.0950184 -4 -1
|
|
||||||
vertex 5.9513144 -4 -3
|
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal -0 -1 0
|
facet normal -0 -1 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 6.108964 -4 -2.5
|
vertex 155.16768 -101.600006 -63.5
|
||||||
vertex 4.323779 -4 -1.25
|
vertex 109.82398 -101.600006 -31.75
|
||||||
vertex 5.9513144 -4 -3
|
vertex 151.16339 -101.600006 -76.2
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal -0 -0.99999994 -0
|
facet normal -0 -1 -0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 9.5 -4 -2.5
|
vertex 241.3 -101.600006 -63.5
|
||||||
vertex 6.108964 -4 -2.5
|
vertex 155.16768 -101.600006 -63.5
|
||||||
vertex 9.5 -4 -3
|
vertex 241.3 -101.600006 -76.2
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 -1 -0
|
facet normal 0 -1 -0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 6.108964 -4 -2.5
|
vertex 155.16768 -101.600006 -63.5
|
||||||
vertex 5.9513144 -4 -3
|
vertex 151.16339 -101.600006 -76.2
|
||||||
vertex 9.5 -4 -3
|
vertex 241.3 -101.600006 -76.2
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 -1 -0
|
facet normal 0 -1 -0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 2.5385938 -4 0
|
vertex 64.480286 -101.600006 0
|
||||||
vertex 0 -4 -1
|
vertex 0 -101.600006 -25.400002
|
||||||
vertex 3.0950184 -4 -1
|
vertex 78.613464 -101.600006 -25.400002
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
facet normal 0 -1 0
|
facet normal 0 -1 0
|
||||||
outer loop
|
outer loop
|
||||||
vertex 0 -4 -1
|
vertex 0 -101.600006 -25.400002
|
||||||
vertex 2.5385938 -4 0
|
vertex 64.480286 -101.600006 0
|
||||||
vertex 0 -4 0
|
vertex 0 -101.600006 0
|
||||||
endloop
|
endloop
|
||||||
endfacet
|
endfacet
|
||||||
endsolid unnamed
|
endsolid unnamed
|
||||||
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 221 KiB |
@ -1,10 +1,9 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
import { test, expect } from '@playwright/test'
|
||||||
import { secrets } from './secrets'
|
import { secrets } from './secrets'
|
||||||
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { getUtils } from './test-utils'
|
import { getUtils } from './test-utils'
|
||||||
import waitOn from 'wait-on'
|
import waitOn from 'wait-on'
|
||||||
import { Themes } from '../../src/lib/theme'
|
import { Themes } from '../../src/lib/theme'
|
||||||
|
import { roundOff } from 'lib/utils'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
||||||
@ -16,6 +15,12 @@ document.addEventListener('mousemove', (e) =>
|
|||||||
)
|
)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const commonPoints = {
|
||||||
|
startAt: '[0.93, -1.26]',
|
||||||
|
num1: 0.95,
|
||||||
|
num2: 1.88,
|
||||||
|
}
|
||||||
|
|
||||||
test.beforeEach(async ({ context, page }) => {
|
test.beforeEach(async ({ context, page }) => {
|
||||||
// wait for Vite preview server to be up
|
// wait for Vite preview server to be up
|
||||||
await waitOn({
|
await waitOn({
|
||||||
@ -53,71 +58,66 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
await u.waitForDefaultPlanesVisibilityChange()
|
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||||
|
|
||||||
// click on "Start Sketch" button
|
// click on "Start Sketch" button
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
await Promise.all([
|
await u.doAndWaitForImageDiff(
|
||||||
u.doAndWaitForImageDiff(
|
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
200
|
||||||
200
|
)
|
||||||
),
|
|
||||||
u.waitForDefaultPlanesVisibilityChange(),
|
|
||||||
])
|
|
||||||
|
|
||||||
// select a plane
|
// select a plane
|
||||||
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter')
|
await page.mouse.click(700, 200)
|
||||||
await u.waitForCmdReceive('set_tool')
|
|
||||||
|
|
||||||
await u.doAndWaitForCmd(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
() => page.getByRole('button', { name: 'Line' }).click(),
|
`const part001 = startSketchOn('-XZ')`
|
||||||
'set_tool'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||||
|
|
||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await u.doAndWaitForCmd(
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10),
|
|
||||||
'mouse_click',
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
|
||||||
|
|
||||||
const startAt = '[18.26, -24.63]'
|
|
||||||
const num = '18.43'
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||||
|> line([${num}, 0], %)`)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const num = 26.63
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|
|> line([${commonPoints.num1}, 0], %)`)
|
||||||
|
|
||||||
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(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${num}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${num}], %)`)
|
|> line([0, ${commonPoints.num1 - 0.01}], %)`)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${num}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${num}], %)
|
|> line([0, ${commonPoints.num1 - 0.01}], %)
|
||||||
|> line([-36.69, 0], %)`)
|
|> line([-${commonPoints.num2}, 0], %)`)
|
||||||
|
|
||||||
// deselect line tool
|
// deselect line tool
|
||||||
await u.doAndWaitForCmd(
|
await page.getByRole('button', { name: 'Line' }).click()
|
||||||
() => page.getByRole('button', { name: 'Line' }).click(),
|
await page.waitForTimeout(100)
|
||||||
'set_tool'
|
|
||||||
)
|
|
||||||
|
|
||||||
// click between first two clicks to get center of the line
|
// click between first two clicks to get center of the line
|
||||||
await u.doAndWaitForCmd(
|
await page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10)
|
||||||
() => page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10),
|
await page.waitForTimeout(100)
|
||||||
'select_with_point'
|
|
||||||
)
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
// hold down shift
|
// hold down shift
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
@ -131,9 +131,9 @@ test('Basic sketch', async ({ page }) => {
|
|||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line({ to: [${num}, 0], tag: 'seg01' }, %)
|
|> line({ to: [${commonPoints.num1}, 0], tag: 'seg01' }, %)
|
||||||
|> line([0, ${num}], %)
|
|> line([0, ${commonPoints.num1 - 0.01}], %)
|
||||||
|> angledLine([180, segLen('seg01', %)], %)`)
|
|> angledLine([180, segLen('seg01', %)], %)`)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -206,7 +206,7 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
|||||||
|
|
||||||
test('executes on load', async ({ page, context }) => {
|
test('executes on load', async ({ page, context }) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
await context.addInitScript(async (token) => {
|
await context.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
`const part001 = startSketchOn('-XZ')
|
`const part001 = startSketchOn('-XZ')
|
||||||
@ -271,59 +271,40 @@ test('Can create sketches on all planes and their back sides', async ({
|
|||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
await u.waitForDefaultPlanesVisibilityChange()
|
|
||||||
|
|
||||||
const camCmd: EngineCommand = {
|
const camPos: [number, number, number] = [100, 100, 100]
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_look_at',
|
|
||||||
center: { x: 15, y: 0, z: 0 },
|
|
||||||
up: { x: 0, y: 0, z: 1 },
|
|
||||||
vantage: { x: 30, y: 30, z: 30 },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const TestSinglePlane = async ({
|
const TestSinglePlane = async ({
|
||||||
viewCmd,
|
viewCmd,
|
||||||
expectedCode,
|
expectedCode,
|
||||||
clickCoords,
|
clickCoords,
|
||||||
}: {
|
}: {
|
||||||
viewCmd: EngineCommand
|
viewCmd: [number, number, number]
|
||||||
expectedCode: string
|
expectedCode: string
|
||||||
clickCoords: { x: number; y: number }
|
clickCoords: { x: number; y: number }
|
||||||
}) => {
|
}) => {
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
await u.sendCustomCmd(viewCmd)
|
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
// await page.waitForTimeout(200)
|
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
await u.waitForDefaultPlanesVisibilityChange()
|
await u.updateCamPosition(viewCmd)
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
await page.mouse.click(clickCoords.x, clickCoords.y)
|
await page.mouse.click(clickCoords.x, clickCoords.y)
|
||||||
await u.openDebugPanel()
|
await page.waitForTimeout(300) // wait for animation
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Line' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Line' })).toBeVisible()
|
||||||
|
|
||||||
// draw a line
|
// draw a line
|
||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await u.clearCommandLogs()
|
|
||||||
await page.getByRole('button', { name: 'Line' }).click()
|
|
||||||
await u.waitForCmdReceive('set_tool')
|
|
||||||
await u.clearCommandLogs()
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
await u.openDebugPanel()
|
|
||||||
await u.waitForCmdReceive('mouse_click')
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
|
||||||
await u.openDebugPanel()
|
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(expectedCode)
|
await expect(page.locator('.cm-content')).toHaveText(expectedCode)
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Line' }).click()
|
await page.getByRole('button', { name: 'Line' }).click()
|
||||||
await u.clearCommandLogs()
|
await u.openAndClearDebugPanel()
|
||||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
|
||||||
@ -332,52 +313,40 @@ test('Can create sketches on all planes and their back sides', async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const codeTemplate = (
|
const codeTemplate = (
|
||||||
plane = 'XY',
|
plane = 'XY'
|
||||||
sign = ''
|
|
||||||
) => `const part001 = startSketchOn('${plane}')
|
) => `const part001 = startSketchOn('${plane}')
|
||||||
|> startProfileAt([${sign}6.88, -9.29], %)
|
|> startProfileAt([1.14, -1.54], %)`
|
||||||
|> line([${sign}6.95, 0], %)`
|
|
||||||
await TestSinglePlane({
|
await TestSinglePlane({
|
||||||
viewCmd: camCmd,
|
viewCmd: camPos,
|
||||||
expectedCode: codeTemplate('XY'),
|
expectedCode: codeTemplate('XY'),
|
||||||
clickCoords: { x: 700, y: 350 }, // red plane
|
clickCoords: { x: 600, y: 388 }, // red plane
|
||||||
|
// clickCoords: { x: 600, y: 400 }, // red plane // clicks grid helper and that causes problems, should fix so that these coords work too.
|
||||||
})
|
})
|
||||||
await TestSinglePlane({
|
await TestSinglePlane({
|
||||||
viewCmd: camCmd,
|
viewCmd: camPos,
|
||||||
expectedCode: codeTemplate('YZ'),
|
expectedCode: codeTemplate('YZ'),
|
||||||
clickCoords: { x: 1000, y: 200 }, // green plane
|
clickCoords: { x: 700, y: 250 }, // green plane
|
||||||
})
|
})
|
||||||
await TestSinglePlane({
|
await TestSinglePlane({
|
||||||
viewCmd: camCmd,
|
viewCmd: camPos,
|
||||||
expectedCode: codeTemplate('XZ', '-'),
|
expectedCode: codeTemplate('XZ'),
|
||||||
clickCoords: { x: 630, y: 130 }, // blue plane
|
clickCoords: { x: 700, y: 80 }, // blue plane
|
||||||
})
|
})
|
||||||
|
const camCmdBackSide: [number, number, number] = [-100, -100, -100]
|
||||||
// new camera angle to click the back side of all three planes
|
|
||||||
const camCmdBackSide: EngineCommand = {
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_look_at',
|
|
||||||
center: { x: -15, y: 0, z: 0 },
|
|
||||||
up: { x: 0, y: 0, z: 1 },
|
|
||||||
vantage: { x: -30, y: -30, z: -30 },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await TestSinglePlane({
|
await TestSinglePlane({
|
||||||
viewCmd: camCmdBackSide,
|
viewCmd: camCmdBackSide,
|
||||||
expectedCode: codeTemplate('-XY', '-'),
|
expectedCode: codeTemplate('-XY'),
|
||||||
clickCoords: { x: 705, y: 136 }, // back of red plane
|
clickCoords: { x: 601, y: 118 }, // back of red plane
|
||||||
})
|
})
|
||||||
await TestSinglePlane({
|
await TestSinglePlane({
|
||||||
viewCmd: camCmdBackSide,
|
viewCmd: camCmdBackSide,
|
||||||
expectedCode: codeTemplate('-YZ', '-'),
|
expectedCode: codeTemplate('-YZ'),
|
||||||
clickCoords: { x: 1000, y: 350 }, // back of green plane
|
clickCoords: { x: 730, y: 219 }, // back of green plane
|
||||||
})
|
})
|
||||||
await TestSinglePlane({
|
await TestSinglePlane({
|
||||||
viewCmd: camCmdBackSide,
|
viewCmd: camCmdBackSide,
|
||||||
expectedCode: codeTemplate('-XZ'),
|
expectedCode: codeTemplate('-XZ'),
|
||||||
clickCoords: { x: 600, y: 400 }, // back of blue plane
|
clickCoords: { x: 680, y: 427 }, // back of blue plane
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -387,7 +356,6 @@ test('Auto complete works', async ({ page }) => {
|
|||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.waitForDefaultPlanesVisibilityChange()
|
|
||||||
|
|
||||||
// this test might be brittle as we add and remove functions
|
// this test might be brittle as we add and remove functions
|
||||||
// but should also be easy to update.
|
// but should also be easy to update.
|
||||||
@ -405,6 +373,7 @@ test('Auto complete works', async ({ page }) => {
|
|||||||
await page.keyboard.type(' |> startProfi')
|
await page.keyboard.type(' |> startProfi')
|
||||||
// expect there be a single auto complete option that we can just hit enter on
|
// expect there be a single auto complete option that we can just hit enter on
|
||||||
await expect(page.locator('.cm-completionLabel')).toBeVisible()
|
await expect(page.locator('.cm-completionLabel')).toBeVisible()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
await page.keyboard.press('Enter') // accepting the auto complete, not a new line
|
await page.keyboard.press('Enter') // accepting the auto complete, not a new line
|
||||||
|
|
||||||
await page.keyboard.type('([0,0], %)')
|
await page.keyboard.type('([0,0], %)')
|
||||||
@ -412,16 +381,21 @@ test('Auto complete works', async ({ page }) => {
|
|||||||
await page.keyboard.type(' |> lin')
|
await page.keyboard.type(' |> lin')
|
||||||
|
|
||||||
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible()
|
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
// press arrow down twice then enter to accept xLine
|
// press arrow down twice then enter to accept xLine
|
||||||
await page.keyboard.press('ArrowDown')
|
await page.keyboard.press('ArrowDown')
|
||||||
await page.keyboard.press('ArrowDown')
|
await page.keyboard.press('ArrowDown')
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
await page.keyboard.type('(5, %)')
|
// finish line with comment
|
||||||
|
await page.keyboard.type('(5, %) // lin')
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
// there shouldn't be any auto complete options for 'lin' in the comment
|
||||||
|
await expect(page.locator('.cm-completionLabel')).not.toBeVisible()
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('XY')
|
.toHaveText(`const part001 = startSketchOn('XY')
|
||||||
|> startProfileAt([0,0], %)
|
|> startProfileAt([0,0], %)
|
||||||
|> xLine(5, %)`)
|
|> xLine(5, %) // lin`)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Onboarding tests
|
// Onboarding tests
|
||||||
@ -478,62 +452,57 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
await u.waitForDefaultPlanesVisibilityChange()
|
|
||||||
|
|
||||||
const xAxisClick = () => page.mouse.click(700, 250)
|
const xAxisClick = () =>
|
||||||
const emptySpaceClick = () => page.mouse.click(700, 300)
|
page.mouse.click(700, 250).then(() => page.waitForTimeout(100))
|
||||||
const topHorzSegmentClick = () => page.mouse.click(700, 285)
|
const emptySpaceClick = () =>
|
||||||
const bottomHorzSegmentClick = () => page.mouse.click(750, 393)
|
page.mouse.click(728, 343).then(() => page.waitForTimeout(100))
|
||||||
|
const topHorzSegmentClick = () =>
|
||||||
|
page.mouse.click(709, 289).then(() => page.waitForTimeout(100))
|
||||||
|
const bottomHorzSegmentClick = () =>
|
||||||
|
page.mouse.click(767, 396).then(() => page.waitForTimeout(100))
|
||||||
|
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
await u.waitForDefaultPlanesVisibilityChange()
|
|
||||||
|
|
||||||
// select a plane
|
// select a plane
|
||||||
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter')
|
await page.mouse.click(700, 200)
|
||||||
await u.waitForCmdReceive('set_tool')
|
await page.waitForTimeout(700) // wait for animation
|
||||||
|
|
||||||
await u.doAndWaitForCmd(
|
|
||||||
() => page.getByRole('button', { name: 'Line' }).click(),
|
|
||||||
'set_tool'
|
|
||||||
)
|
|
||||||
|
|
||||||
const startXPx = 600
|
const startXPx = 600
|
||||||
await u.doAndWaitForCmd(
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10),
|
await expect(page.locator('.cm-content'))
|
||||||
'mouse_click',
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
false
|
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||||
)
|
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
|
|
||||||
const startAt = '[18.26, -24.63]'
|
|
||||||
const num = '18.43'
|
|
||||||
const num2 = '36.69'
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${num}, 0], %)`)
|
|> line([${commonPoints.num1}, 0], %)`)
|
||||||
|
|
||||||
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(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${num}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${num}], %)`)
|
|> line([0, ${commonPoints.num1 - 0.01}], %)`)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|> startProfileAt(${startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${num}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([0, ${num}], %)
|
|> line([0, ${commonPoints.num1 - 0.01}], %)
|
||||||
|> line([-${num2}, 0], %)`)
|
|> line([-${commonPoints.num2}, 0], %)`)
|
||||||
|
|
||||||
// deselect line tool
|
// deselect line tool
|
||||||
await u.doAndWaitForCmd(
|
await page.getByRole('button', { name: 'Line' }).click()
|
||||||
() => page.getByRole('button', { name: 'Line' }).click(),
|
|
||||||
'set_tool'
|
|
||||||
)
|
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
const selectionSequence = async () => {
|
const selectionSequence = async () => {
|
||||||
@ -555,79 +524,73 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
|||||||
// now check clicking works including axis
|
// now check clicking works including axis
|
||||||
|
|
||||||
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
|
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
|
||||||
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false)
|
await topHorzSegmentClick()
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
const absYButton = page.getByRole('button', { name: 'ABS Y' })
|
const absYButton = page.getByRole('button', { name: 'ABS Y' })
|
||||||
await expect(absYButton).toBeDisabled()
|
await expect(absYButton).toBeDisabled()
|
||||||
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
|
await xAxisClick()
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
await absYButton.and(page.locator(':not([disabled])')).waitFor()
|
await absYButton.and(page.locator(':not([disabled])')).waitFor()
|
||||||
await expect(absYButton).not.toBeDisabled()
|
await expect(absYButton).not.toBeDisabled()
|
||||||
|
|
||||||
// clear selection by clicking on nothing
|
// clear selection by clicking on nothing
|
||||||
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
|
await emptySpaceClick()
|
||||||
|
|
||||||
// same selection but click the axis first
|
// same selection but click the axis first
|
||||||
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
|
await xAxisClick()
|
||||||
await expect(absYButton).toBeDisabled()
|
await expect(absYButton).toBeDisabled()
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false)
|
await topHorzSegmentClick()
|
||||||
|
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
await expect(absYButton).not.toBeDisabled()
|
await expect(absYButton).not.toBeDisabled()
|
||||||
|
|
||||||
// clear selection by clicking on nothing
|
// clear selection by clicking on nothing
|
||||||
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
|
await emptySpaceClick()
|
||||||
|
|
||||||
// check the same selection again by putting cursor in code first then selecting axis
|
// check the same selection again by putting cursor in code first then selecting axis
|
||||||
await u.doAndWaitForCmd(
|
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
|
||||||
() => page.getByText(` |> line([-${num2}, 0], %)`).click(),
|
|
||||||
'select_clear',
|
|
||||||
false
|
|
||||||
)
|
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await expect(absYButton).toBeDisabled()
|
await expect(absYButton).toBeDisabled()
|
||||||
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
|
await xAxisClick()
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
await expect(absYButton).not.toBeDisabled()
|
await expect(absYButton).not.toBeDisabled()
|
||||||
|
|
||||||
// clear selection by clicking on nothing
|
// clear selection by clicking on nothing
|
||||||
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
|
await emptySpaceClick()
|
||||||
|
|
||||||
// select segment in editor than another segment in scene and check there are two cursors
|
// select segment in editor than another segment in scene and check there are two cursors
|
||||||
await u.doAndWaitForCmd(
|
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
|
||||||
() => page.getByText(` |> line([-${num2}, 0], %)`).click(),
|
await page.waitForTimeout(300)
|
||||||
'select_clear',
|
|
||||||
false
|
|
||||||
)
|
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await expect(page.locator('.cm-cursor')).toHaveCount(1)
|
await expect(page.locator('.cm-cursor')).toHaveCount(1)
|
||||||
await u.doAndWaitForCmd(bottomHorzSegmentClick, 'select_with_point', false) // another segment, bottom one
|
await bottomHorzSegmentClick()
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||||
|
|
||||||
// clear selection by clicking on nothing
|
// clear selection by clicking on nothing
|
||||||
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
|
await emptySpaceClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
await selectionSequence()
|
await selectionSequence()
|
||||||
|
|
||||||
// hovering in fresh sketch worked, lets try exiting and re-entering
|
// hovering in fresh sketch worked, lets try exiting and re-entering
|
||||||
await u.doAndWaitForCmd(
|
await u.openAndClearDebugPanel()
|
||||||
() => page.getByRole('button', { name: 'Exit Sketch' }).click(),
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
'edit_mode_exit'
|
await page.waitForTimeout(200)
|
||||||
)
|
|
||||||
// wait for execution done
|
// wait for execution done
|
||||||
|
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
// select a line
|
// select a line
|
||||||
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_clear', false)
|
// await topHorzSegmentClick()
|
||||||
|
await page.getByText(commonPoints.startAt).click() // TODO remove this and reinstate // await topHorzSegmentClick()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
// enter sketch again
|
// enter sketch again
|
||||||
await u.doAndWaitForCmd(
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
await page.waitForTimeout(300) // wait for animation
|
||||||
'edit_mode_enter',
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
// hover again and check it works
|
// hover again and check it works
|
||||||
await selectionSequence()
|
await selectionSequence()
|
||||||
@ -684,12 +647,15 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|
|||||||
await context.addInitScript(async (token) => {
|
await context.addInitScript(async (token) => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
`const part001 = startSketchOn('-XZ')
|
`
|
||||||
|> startProfileAt([-6.95, 4.98], %)
|
const distance = sqrt(20)
|
||||||
|> line([25.1, 0.41], %)
|
const part001 = startSketchOn('-XZ')
|
||||||
|> line([0.73, -14.93], %)
|
|> startProfileAt([-6.95, 4.98], %)
|
||||||
|> line([-23.44, 0.52], %)
|
|> line([25.1, 0.41], %)
|
||||||
|> close(%)`
|
|> line([0.73, -14.93], %)
|
||||||
|
|> line([-23.44, 0.52], %)
|
||||||
|
|> close(%)
|
||||||
|
`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -697,6 +663,8 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|
|||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
|
||||||
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||||
await page.keyboard.press('Meta+K')
|
await page.keyboard.press('Meta+K')
|
||||||
@ -710,28 +678,381 @@ test('Can extrude from the command bar', async ({ page, context }) => {
|
|||||||
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
|
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
|
||||||
|
|
||||||
// Click to select face and set distance
|
// Click to select face and set distance
|
||||||
await u.openAndClearDebugPanel()
|
|
||||||
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
|
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click()
|
||||||
await u.waitForCmdReceive('select_add')
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click()
|
await page.getByRole('button', { name: 'Continue' }).click()
|
||||||
|
|
||||||
|
// Assert that we're on the distance step
|
||||||
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
|
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
|
||||||
await page.keyboard.press('Enter')
|
|
||||||
|
// Assert that the an alternative variable name is chosen,
|
||||||
|
// since the default variable name is already in use (distance)
|
||||||
|
await page.getByRole('button', { name: 'Create new variable' }).click()
|
||||||
|
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
|
||||||
|
'distance001'
|
||||||
|
)
|
||||||
|
await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled()
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click()
|
||||||
|
|
||||||
// Review step and argument hotkeys
|
// Review step and argument hotkeys
|
||||||
await page.keyboard.press('2')
|
await expect(
|
||||||
await expect(page.getByRole('button', { name: '5' })).toBeDisabled()
|
page.getByRole('button', { name: 'Submit command' })
|
||||||
|
).toBeEnabled()
|
||||||
|
await page.keyboard.press('Backspace')
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Distance 12', exact: false })
|
||||||
|
).toBeDisabled()
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
|
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
||||||
|
|
||||||
// Check that the code was updated
|
// Check that the code was updated
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
|
// Unfortunately this indentation seems to matter for the test
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`const part001 = startSketchOn('-XZ')
|
`const distance = sqrt(20)
|
||||||
|> startProfileAt([-6.95, 4.98], %)
|
const distance001 = 5 + 7
|
||||||
|> line([25.1, 0.41], %)
|
const part001 = startSketchOn('-XZ')
|
||||||
|> line([0.73, -14.93], %)
|
|> startProfileAt([-6.95, 4.98], %)
|
||||||
|> line([-23.44, 0.52], %)
|
|> line([25.1, 0.41], %)
|
||||||
|> close(%)
|
|> line([0.73, -14.93], %)
|
||||||
|> extrude(5, %)`
|
|> line([-23.44, 0.52], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Can add multiple sketches', async ({ page }) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
|
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||||
|
|
||||||
|
// click on "Start Sketch" button
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await u.doAndWaitForImageDiff(
|
||||||
|
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||||
|
200
|
||||||
|
)
|
||||||
|
|
||||||
|
// select a plane
|
||||||
|
await page.mouse.click(700, 200)
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
|
`const part001 = startSketchOn('-XZ')`
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||||
|
|
||||||
|
const startXPx = 600
|
||||||
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|
|> line([${commonPoints.num1}, 0], %)`)
|
||||||
|
|
||||||
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|
|> line([0, ${commonPoints.num1 - 0.01}], %)`)
|
||||||
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
|
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|
|> line([0, ${commonPoints.num1 - 0.01}], %)
|
||||||
|
|> line([-${commonPoints.num2}, 0], %)`
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch)
|
||||||
|
|
||||||
|
// exit the sketch
|
||||||
|
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
|
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
|
||||||
|
await u.updateCamPosition([0, 100, 100])
|
||||||
|
|
||||||
|
// start a new sketch
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await page.mouse.click(673, 384)
|
||||||
|
|
||||||
|
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||||
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
|
const startAt2 = '[0.93,-1.25]'
|
||||||
|
await expect(
|
||||||
|
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||||
|
).toBe(
|
||||||
|
`${finalCodeFirstSketch}
|
||||||
|
const part002 = startSketchOn('XY')
|
||||||
|
|> startProfileAt(${startAt2}, %)`.replace(/\s/g, '')
|
||||||
|
)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
const num2 = 0.94
|
||||||
|
await expect(
|
||||||
|
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||||
|
).toBe(
|
||||||
|
`${finalCodeFirstSketch}
|
||||||
|
const part002 = startSketchOn('XY')
|
||||||
|
|> startProfileAt(${startAt2}, %)
|
||||||
|
|> line([${num2}, 0], %)`.replace(/\s/g, '')
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||||
|
await expect(
|
||||||
|
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||||
|
).toBe(
|
||||||
|
`${finalCodeFirstSketch}
|
||||||
|
const part002 = startSketchOn('XY')
|
||||||
|
|> startProfileAt(${startAt2}, %)
|
||||||
|
|> line([${num2}, 0], %)
|
||||||
|
|> line([0, ${roundOff(num2 - 0.01)}], %)`.replace(/\s/g, '')
|
||||||
|
)
|
||||||
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
|
await expect(
|
||||||
|
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||||
|
).toBe(
|
||||||
|
`${finalCodeFirstSketch}
|
||||||
|
const part002 = startSketchOn('XY')
|
||||||
|
|> startProfileAt(${startAt2}, %)
|
||||||
|
|> line([${num2}, 0], %)
|
||||||
|
|> line([0, ${roundOff(num2 - 0.01)}], %)
|
||||||
|
|> line([-1.87, 0], %)`.replace(/\s/g, '')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ProgramMemory can be serialised', async ({ page, context }) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
await context.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const part = startSketchOn('XY')
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> line([0, 1], %)
|
||||||
|
|> line([1, 0], %)
|
||||||
|
|> line([0, -1], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(1, %)
|
||||||
|
|> patternLinear({
|
||||||
|
axis: [1, 0, 1],
|
||||||
|
repetitions: 3,
|
||||||
|
distance: 6
|
||||||
|
}, %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await page.setViewportSize({ width: 1000, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
const messages: string[] = []
|
||||||
|
|
||||||
|
// Listen for all console events and push the message text to an array
|
||||||
|
page.on('console', (message) => messages.push(message.text()))
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
// wait for execution done
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
|
||||||
|
const forbiddenMessages = ['cannot serialize tagged newtype variant']
|
||||||
|
forbiddenMessages.forEach((forbiddenMessage) => {
|
||||||
|
messages.forEach((message) => {
|
||||||
|
expect(message).not.toContain(forbiddenMessage)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
}) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
const selectionsSnippets = {
|
||||||
|
extrudeAndEditBlocked: '|> startProfileAt([10.81, 32.99], %)',
|
||||||
|
extrudeAndEditBlockedInFunction: '|> startProfileAt(pos, %)',
|
||||||
|
extrudeAndEditAllowed: '|> startProfileAt([15.72, 4.7], %)',
|
||||||
|
editOnly: '|> startProfileAt([15.79, -14.6], %)',
|
||||||
|
}
|
||||||
|
await context.addInitScript(
|
||||||
|
async ({
|
||||||
|
extrudeAndEditBlocked,
|
||||||
|
extrudeAndEditBlockedInFunction,
|
||||||
|
extrudeAndEditAllowed,
|
||||||
|
editOnly,
|
||||||
|
}: any) => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const part001 = startSketchOn('-XZ')
|
||||||
|
${extrudeAndEditBlocked}
|
||||||
|
|> line([25.96, 2.93], %)
|
||||||
|
|> line([5.25, -5.72], %)
|
||||||
|
|> line([-2.01, -10.35], %)
|
||||||
|
|> line([-27.65, -2.78], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(5, %)
|
||||||
|
const part002 = startSketchOn('-XZ')
|
||||||
|
${extrudeAndEditAllowed}
|
||||||
|
|> line([10.32, 6.47], %)
|
||||||
|
|> line([9.71, -6.16], %)
|
||||||
|
|> line([-3.08, -9.86], %)
|
||||||
|
|> line([-12.02, -1.54], %)
|
||||||
|
|> close(%)
|
||||||
|
const part003 = startSketchOn('-XZ')
|
||||||
|
${editOnly}
|
||||||
|
|> line([27.55, -1.65], %)
|
||||||
|
|> line([4.95, -8], %)
|
||||||
|
|> line([-20.38, -10.12], %)
|
||||||
|
|> line([-15.79, 17.08], %)
|
||||||
|
|
||||||
|
fn yohey = (pos) => {
|
||||||
|
const part004 = startSketchOn('-XZ')
|
||||||
|
${extrudeAndEditBlockedInFunction}
|
||||||
|
|> line([27.55, -1.65], %)
|
||||||
|
|> line([4.95, -10.53], %)
|
||||||
|
|> line([-20.38, -8], %)
|
||||||
|
|> line([-15.79, 17.08], %)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
yohey([15.79, -34.6])
|
||||||
|
`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
selectionsSnippets
|
||||||
|
)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
// wait for execution done
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
// wait for start sketch as a proxy for the stream being ready
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
|
|
||||||
|
await page.getByText(selectionsSnippets.extrudeAndEditBlocked).click()
|
||||||
|
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
|
).not.toBeVisible()
|
||||||
|
|
||||||
|
await page.getByText(selectionsSnippets.extrudeAndEditAllowed).click()
|
||||||
|
await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
|
|
||||||
|
await page.getByText(selectionsSnippets.editOnly).click()
|
||||||
|
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByText(selectionsSnippets.extrudeAndEditBlockedInFunction)
|
||||||
|
.click()
|
||||||
|
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
|
).not.toBeVisible()
|
||||||
|
|
||||||
|
// selecting an editable sketch but clicking "start sktech" should start a new sketch and not edit the existing one
|
||||||
|
await page.getByText(selectionsSnippets.extrudeAndEditAllowed).click()
|
||||||
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
await page.mouse.click(700, 200)
|
||||||
|
// expect main content to contain `part005` i.e. started a new sketch
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
|
/part005 = startSketchOn\('-XZ'\)/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Deselecting line tool should mean nothing happens on click', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
|
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||||
|
|
||||||
|
// click on "Start Sketch" button
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await u.doAndWaitForImageDiff(
|
||||||
|
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||||
|
200
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.mouse.click(700, 200)
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
|
`const part001 = startSketchOn('-XZ')`
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
|
||||||
|
let previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
// deselect the line tool by clicking it
|
||||||
|
await page.getByRole('button', { name: 'Line' }).click()
|
||||||
|
|
||||||
|
await page.mouse.click(700, 200)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await page.mouse.click(700, 250)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await page.mouse.click(750, 200)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
// expect no change
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText(previousCodeContent)
|
||||||
|
|
||||||
|
// select line tool again
|
||||||
|
await page.getByRole('button', { name: 'Line' }).click()
|
||||||
|
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
// line tool should work as expected again
|
||||||
|
await page.mouse.click(700, 200)
|
||||||
|
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||||
|
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
await page.mouse.click(700, 300)
|
||||||
|
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||||
|
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
|
await page.mouse.click(750, 300)
|
||||||
|
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||||
|
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
})
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
import { test, expect } from '@playwright/test'
|
||||||
import { secrets } from './secrets'
|
import { secrets } from './secrets'
|
||||||
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { getUtils } from './test-utils'
|
import { getUtils } from './test-utils'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import fsp from 'fs/promises'
|
import fsp from 'fs/promises'
|
||||||
@ -33,6 +31,12 @@ test.beforeEach(async ({ context, page }) => {
|
|||||||
|
|
||||||
test.setTimeout(60000)
|
test.setTimeout(60000)
|
||||||
|
|
||||||
|
const commonPoints = {
|
||||||
|
startAt: '[26.38, -35.59]',
|
||||||
|
num1: 26.63,
|
||||||
|
num2: 53.01,
|
||||||
|
}
|
||||||
|
|
||||||
test('change camera, show planes', async ({ page, context }) => {
|
test('change camera, show planes', async ({ page, context }) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
@ -40,19 +44,8 @@ test('change camera, show planes', async ({ page, context }) => {
|
|||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
|
|
||||||
const camCmd: EngineCommand = {
|
const camPos: [number, number, number] = [0, 85, 85]
|
||||||
type: 'modeling_cmd_req',
|
await u.updateCamPosition(camPos)
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_look_at',
|
|
||||||
center: { x: 0, y: 0, z: 0 },
|
|
||||||
up: { x: 0, y: 0, z: 1 },
|
|
||||||
vantage: { x: 0, y: 85, z: 85 },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
await u.sendCustomCmd(camCmd)
|
|
||||||
await u.waitForCmdReceive('default_camera_look_at')
|
|
||||||
|
|
||||||
// rotate
|
// rotate
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
@ -62,13 +55,11 @@ test('change camera, show planes', async ({ page, context }) => {
|
|||||||
await page.mouse.up({ button: 'right' })
|
await page.mouse.up({ button: 'right' })
|
||||||
|
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
await u.waitForCmdReceive('camera_drag_end')
|
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
|
||||||
await u.waitForDefaultPlanesVisibilityChange()
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await expect(page).toHaveScreenshot({
|
await expect(page).toHaveScreenshot({
|
||||||
@ -77,10 +68,8 @@ test('change camera, show planes', async ({ page, context }) => {
|
|||||||
|
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
await u.waitForDefaultPlanesVisibilityChange()
|
|
||||||
|
|
||||||
await u.sendCustomCmd(camCmd)
|
await u.updateCamPosition(camPos)
|
||||||
await u.waitForCmdReceive('default_camera_look_at')
|
|
||||||
|
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
@ -93,12 +82,10 @@ test('change camera, show planes', async ({ page, context }) => {
|
|||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
|
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
await u.waitForCmdReceive('camera_drag_end')
|
|
||||||
await page.waitForTimeout(300)
|
await page.waitForTimeout(300)
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
await u.waitForDefaultPlanesVisibilityChange()
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await expect(page).toHaveScreenshot({
|
await expect(page).toHaveScreenshot({
|
||||||
@ -107,10 +94,8 @@ test('change camera, show planes', async ({ page, context }) => {
|
|||||||
|
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
await u.waitForDefaultPlanesVisibilityChange()
|
|
||||||
|
|
||||||
await u.sendCustomCmd(camCmd)
|
await u.updateCamPosition(camPos)
|
||||||
await u.waitForCmdReceive('default_camera_look_at')
|
|
||||||
|
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
@ -119,17 +104,15 @@ test('change camera, show planes', async ({ page, context }) => {
|
|||||||
await page.keyboard.down('Control')
|
await page.keyboard.down('Control')
|
||||||
await page.mouse.move(700, 400)
|
await page.mouse.move(700, 400)
|
||||||
await page.mouse.down({ button: 'right' })
|
await page.mouse.down({ button: 'right' })
|
||||||
await page.mouse.move(700, 350)
|
await page.mouse.move(700, 300)
|
||||||
await page.mouse.up({ button: 'right' })
|
await page.mouse.up({ button: 'right' })
|
||||||
await page.keyboard.up('Control')
|
await page.keyboard.up('Control')
|
||||||
|
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
await u.waitForCmdReceive('camera_drag_end')
|
|
||||||
await page.waitForTimeout(300)
|
await page.waitForTimeout(300)
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
await u.waitForDefaultPlanesVisibilityChange()
|
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await expect(page).toHaveScreenshot({
|
await expect(page).toHaveScreenshot({
|
||||||
@ -164,11 +147,11 @@ const part001 = startSketchOn('-XZ')
|
|||||||
|> xLineTo({ to: totalLen, tag: 'seg03' }, %)
|
|> xLineTo({ to: totalLen, tag: 'seg03' }, %)
|
||||||
|> yLine({ length: -armThick, tag: 'seg01' }, %)
|
|> yLine({ length: -armThick, tag: 'seg01' }, %)
|
||||||
|> angledLineThatIntersects({
|
|> angledLineThatIntersects({
|
||||||
angle: _180,
|
angle: HALF_TURN,
|
||||||
offset: -armThick,
|
offset: -armThick,
|
||||||
intersectTag: 'seg04'
|
intersectTag: 'seg04'
|
||||||
}, %)
|
}, %)
|
||||||
|> angledLineToY([segAng('seg04', %) + 180, _0], %)
|
|> angledLineToY([segAng('seg04', %) + 180, ZERO], %)
|
||||||
|> angledLineToY({
|
|> angledLineToY({
|
||||||
angle: -bottomAng,
|
angle: -bottomAng,
|
||||||
to: -totalHeightHalf - armThick,
|
to: -totalHeightHalf - armThick,
|
||||||
@ -177,13 +160,13 @@ const part001 = startSketchOn('-XZ')
|
|||||||
|> xLineTo(segEndX('seg03', %) + 0, %)
|
|> xLineTo(segEndX('seg03', %) + 0, %)
|
||||||
|> yLine(-segLen('seg01', %), %)
|
|> yLine(-segLen('seg01', %), %)
|
||||||
|> angledLineThatIntersects({
|
|> angledLineThatIntersects({
|
||||||
angle: _180,
|
angle: HALF_TURN,
|
||||||
offset: -armThick,
|
offset: -armThick,
|
||||||
intersectTag: 'seg02'
|
intersectTag: 'seg02'
|
||||||
}, %)
|
}, %)
|
||||||
|> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %)
|
|> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %)
|
||||||
|> xLineTo(_0, %)
|
|> xLineTo(ZERO, %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(4, %)`
|
|> extrude(4, %)`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -191,7 +174,6 @@ const part001 = startSketchOn('-XZ')
|
|||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
await u.waitForDefaultPlanesVisibilityChange()
|
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
await u.waitForCmdReceive('extrude')
|
await u.waitForCmdReceive('extrude')
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
@ -366,23 +348,356 @@ const part001 = startSketchOn('-XZ')
|
|||||||
// snapshot exports, good compromise to capture that exports are healthy without getting bogged down in "did the formatting change" changes
|
// snapshot exports, good compromise to capture that exports are healthy without getting bogged down in "did the formatting change" changes
|
||||||
// context: https://github.com/KittyCAD/modeling-app/issues/1222
|
// context: https://github.com/KittyCAD/modeling-app/issues/1222
|
||||||
for (const { modelPath, imagePath, outputType } of exportLocations) {
|
for (const { modelPath, imagePath, outputType } of exportLocations) {
|
||||||
const cliCommand = `export KITTYCAD_TOKEN=${secrets.snapshottoken} && kittycad file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}`
|
console.log(
|
||||||
|
`taking snapshot of using: "zoo file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}"`
|
||||||
|
)
|
||||||
|
const cliCommand = `export ZOO_TOKEN=${secrets.snapshottoken} && zoo file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}`
|
||||||
const child = spawn(cliCommand, { shell: true })
|
const child = spawn(cliCommand, { shell: true })
|
||||||
await new Promise((resolve, reject) => {
|
const result = await new Promise<string>((resolve, reject) => {
|
||||||
child.on('error', (code: any, msg: any) => {
|
child.on('error', (code: any, msg: any) => {
|
||||||
console.log('error', code, msg)
|
console.log('error', code, msg)
|
||||||
reject()
|
reject('error')
|
||||||
})
|
})
|
||||||
child.on('exit', (code, msg) => {
|
child.on('exit', (code, msg) => {
|
||||||
console.log('exit', code, msg)
|
console.log('exit', code, msg)
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
reject(`exit code ${code} for model ${modelPath}`)
|
reject(`exit code ${code} for model ${modelPath}`)
|
||||||
} else {
|
} else {
|
||||||
resolve(true)
|
resolve('success')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
child.stderr.on('data', (data) => console.log(`stderr: ${data}`))
|
child.stderr.on('data', (data) => console.log(`stderr: ${data}`))
|
||||||
child.stdout.on('data', (data) => console.log(`stdout: ${data}`))
|
child.stdout.on('data', (data) => console.log(`stdout: ${data}`))
|
||||||
})
|
})
|
||||||
|
expect(result).toBe('success')
|
||||||
|
if (result === 'success') {
|
||||||
|
console.log(`snapshot taken for ${modelPath}`)
|
||||||
|
} else {
|
||||||
|
console.log(`snapshot failed for ${modelPath}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('extrude on each default plane should be stable', async ({
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
}) => {
|
||||||
|
const u = getUtils(page)
|
||||||
|
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|
||||||
|
|> startProfileAt([0.70, 0.44], %)
|
||||||
|
|> line([0.66, -0.02], %)
|
||||||
|
|> line([0.28, 0.50], %)
|
||||||
|
|> line([-0.56, 0.44], %)
|
||||||
|
|> line([-0.54, -0.38], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(1.00, %)
|
||||||
|
`
|
||||||
|
await context.addInitScript(async (code) => {
|
||||||
|
localStorage.setItem('persistCode', code)
|
||||||
|
}, makeCode('XY'))
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
// wait for execution done
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
|
await page.getByText('Code').click()
|
||||||
|
await expect(page).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
|
await page.getByText('Code').click()
|
||||||
|
|
||||||
|
const runSnapshotsForOtherPlanes = async (plane = 'XY') => {
|
||||||
|
// clear code
|
||||||
|
await u.removeCurrentCode()
|
||||||
|
// add makeCode('XZ')
|
||||||
|
await page.locator('.cm-content').fill(makeCode(plane))
|
||||||
|
// wait for execution done
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
|
await page.getByText('Code').click()
|
||||||
|
await expect(page).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
|
await page.getByText('Code').click()
|
||||||
|
}
|
||||||
|
await runSnapshotsForOtherPlanes('-XY')
|
||||||
|
|
||||||
|
await runSnapshotsForOtherPlanes('XZ')
|
||||||
|
await runSnapshotsForOtherPlanes('-XZ')
|
||||||
|
|
||||||
|
await runSnapshotsForOtherPlanes('YZ')
|
||||||
|
await runSnapshotsForOtherPlanes('-YZ')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Draft segments should look right', async ({ page, context }) => {
|
||||||
|
await context.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'SETTINGS_PERSIST_KEY',
|
||||||
|
JSON.stringify({
|
||||||
|
baseUnit: 'in',
|
||||||
|
cameraControls: 'KittyCAD',
|
||||||
|
defaultDirectory: '',
|
||||||
|
defaultProjectName: 'project-$nnn',
|
||||||
|
onboardingStatus: 'dismissed',
|
||||||
|
showDebugPanel: true,
|
||||||
|
textWrapping: 'On',
|
||||||
|
theme: 'system',
|
||||||
|
unitSystem: 'imperial',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const u = getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
|
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||||
|
|
||||||
|
// click on "Start Sketch" button
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await u.doAndWaitForImageDiff(
|
||||||
|
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||||
|
200
|
||||||
|
)
|
||||||
|
|
||||||
|
// select a plane
|
||||||
|
await page.mouse.click(700, 200)
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
|
`const part001 = startSketchOn('-XZ')`
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||||
|
|
||||||
|
const startXPx = 600
|
||||||
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([0.93, -1.26], %)`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await page.mouse.move(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
|
await expect(page).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([0.93, -1.26], %)
|
||||||
|
|> line([0.95, 0], %)`)
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||||
|
|
||||||
|
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
|
||||||
|
|
||||||
|
await expect(page).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Client side scene scale should match engine scale inch', async ({
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
}) => {
|
||||||
|
await context.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'SETTINGS_PERSIST_KEY',
|
||||||
|
JSON.stringify({
|
||||||
|
baseUnit: 'in',
|
||||||
|
cameraControls: 'KittyCAD',
|
||||||
|
defaultDirectory: '',
|
||||||
|
defaultProjectName: 'project-$nnn',
|
||||||
|
onboardingStatus: 'dismissed',
|
||||||
|
showDebugPanel: true,
|
||||||
|
textWrapping: 'On',
|
||||||
|
theme: 'system',
|
||||||
|
unitSystem: 'imperial',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const u = getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
|
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||||
|
|
||||||
|
// click on "Start Sketch" button
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await u.doAndWaitForImageDiff(
|
||||||
|
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||||
|
200
|
||||||
|
)
|
||||||
|
|
||||||
|
// select a plane
|
||||||
|
await page.mouse.click(700, 200)
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
|
`const part001 = startSketchOn('-XZ')`
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||||
|
|
||||||
|
const startXPx = 600
|
||||||
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([0.93, -1.26], %)`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([0.93, -1.26], %)
|
||||||
|
|> line([0.95, 0], %)`)
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([0.93, -1.26], %)
|
||||||
|
|> line([0.95, 0], %)
|
||||||
|
|> tangentialArcTo([2.82, -0.32], %)`)
|
||||||
|
|
||||||
|
// screen shot should show the sketch
|
||||||
|
await expect(page).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
// exit sketch
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
|
|
||||||
|
// wait for execution done
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.clearAndCloseDebugPanel()
|
||||||
|
await page.waitForTimeout(200)
|
||||||
|
|
||||||
|
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||||
|
await expect(page).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Client side scene scale should match engine scale mm', async ({
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
}) => {
|
||||||
|
await context.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'SETTINGS_PERSIST_KEY',
|
||||||
|
JSON.stringify({
|
||||||
|
baseUnit: 'mm',
|
||||||
|
cameraControls: 'KittyCAD',
|
||||||
|
defaultDirectory: '',
|
||||||
|
defaultProjectName: 'project-$nnn',
|
||||||
|
onboardingStatus: 'dismissed',
|
||||||
|
showDebugPanel: true,
|
||||||
|
textWrapping: 'On',
|
||||||
|
theme: 'system',
|
||||||
|
unitSystem: 'metric',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const u = getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).not.toBeDisabled()
|
||||||
|
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||||
|
|
||||||
|
// click on "Start Sketch" button
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
await u.doAndWaitForImageDiff(
|
||||||
|
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||||
|
200
|
||||||
|
)
|
||||||
|
|
||||||
|
// select a plane
|
||||||
|
await page.mouse.click(700, 200)
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
|
`const part001 = startSketchOn('-XZ')`
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||||
|
|
||||||
|
const startXPx = 600
|
||||||
|
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([0.93, -1.26], %)`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([0.93, -1.26], %)
|
||||||
|
|> line([0.95, 0], %)`)
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Tangential Arc' }).click()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content'))
|
||||||
|
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([0.93, -1.26], %)
|
||||||
|
|> line([0.95, 0], %)
|
||||||
|
|> tangentialArcTo([2.82, -0.32], %)`)
|
||||||
|
|
||||||
|
// screen shot should show the sketch
|
||||||
|
await expect(page).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
// exit sketch
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
|
|
||||||
|
// wait for execution done
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.clearAndCloseDebugPanel()
|
||||||
|
await page.waitForTimeout(200)
|
||||||
|
|
||||||
|
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||||
|
await expect(page).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 120 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 52 KiB |
@ -80,10 +80,21 @@ export function getUtils(page: Page) {
|
|||||||
waitForAuthSkipAppStart: () => waitForPageLoad(page),
|
waitForAuthSkipAppStart: () => waitForPageLoad(page),
|
||||||
removeCurrentCode: () => removeCurrentCode(page),
|
removeCurrentCode: () => removeCurrentCode(page),
|
||||||
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
|
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
|
||||||
|
updateCamPosition: async (xyz: [number, number, number]) => {
|
||||||
|
const fillInput = async () => {
|
||||||
|
await page.fill('[data-testid="cam-x-position"]', String(xyz[0]))
|
||||||
|
await page.fill('[data-testid="cam-y-position"]', String(xyz[1]))
|
||||||
|
await page.fill('[data-testid="cam-z-position"]', String(xyz[2]))
|
||||||
|
}
|
||||||
|
await fillInput()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await fillInput()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await fillInput()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
},
|
||||||
clearCommandLogs: () => clearCommandLogs(page),
|
clearCommandLogs: () => clearCommandLogs(page),
|
||||||
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr),
|
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr),
|
||||||
waitForDefaultPlanesVisibilityChange: () =>
|
|
||||||
waitForDefaultPlanesToBeVisible(page),
|
|
||||||
openDebugPanel: () => openDebugPanel(page),
|
openDebugPanel: () => openDebugPanel(page),
|
||||||
closeDebugPanel: () => closeDebugPanel(page),
|
closeDebugPanel: () => closeDebugPanel(page),
|
||||||
openAndClearDebugPanel: async () => {
|
openAndClearDebugPanel: async () => {
|
||||||
|
@ -1,19 +1,26 @@
|
|||||||
import { browser, $, expect } from '@wdio/globals'
|
import { browser, $, expect } from '@wdio/globals'
|
||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
|
|
||||||
describe('KCMA (Tauri, Linux)', () => {
|
const defaultDir = `${process.env.HOME}/Documents/zoo-modeling-app-projects`
|
||||||
it('opens the auth page, signs in, and signs out', async () => {
|
const userCodeDir = '/tmp/kittycad_user_code'
|
||||||
// Clean up previous tests
|
|
||||||
|
async function click(element: WebdriverIO.Element): Promise<void> {
|
||||||
|
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
|
||||||
|
await element.waitForClickable()
|
||||||
|
await browser.execute('arguments[0].click();', element)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ZMA (Tauri, Linux)', () => {
|
||||||
|
it('opens the auth page and signs in', async () => {
|
||||||
|
// Clean up filesystem from previous tests
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
await fs.rm('/tmp/kittycad_user_code', { force: true })
|
await fs.rm(defaultDir, { force: true, recursive: true })
|
||||||
await browser.execute('window.localStorage.clear()')
|
await fs.rm(userCodeDir, { force: true })
|
||||||
|
|
||||||
const signInButton = await $('[data-testid="sign-in-button"]')
|
const signInButton = await $('[data-testid="sign-in-button"]')
|
||||||
expect(await signInButton.getText()).toEqual('Sign in')
|
expect(await signInButton.getText()).toEqual('Sign in')
|
||||||
|
|
||||||
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
|
await click(signInButton)
|
||||||
await signInButton.waitForClickable()
|
|
||||||
await browser.execute('arguments[0].click();', signInButton)
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
// Get from main.rs
|
// Get from main.rs
|
||||||
@ -49,14 +56,49 @@ describe('KCMA (Tauri, Linux)', () => {
|
|||||||
// Now should be signed in
|
// Now should be signed in
|
||||||
const newFileButton = await $('[data-testid="home-new-file"]')
|
const newFileButton = await $('[data-testid="home-new-file"]')
|
||||||
expect(await newFileButton.getText()).toEqual('New file')
|
expect(await newFileButton.getText()).toEqual('New file')
|
||||||
|
})
|
||||||
|
|
||||||
// So let's sign out!
|
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
|
||||||
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
|
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
|
||||||
await menuButton.waitForClickable()
|
await click(menuButton)
|
||||||
await browser.execute('arguments[0].click();', menuButton)
|
|
||||||
|
const settingsButton = await $('[data-testid="settings-button"]')
|
||||||
|
await click(settingsButton)
|
||||||
|
|
||||||
|
const defaultDirInput = await $('[data-testid="default-directory-input"]')
|
||||||
|
expect(await defaultDirInput.getValue()).toEqual(defaultDir)
|
||||||
|
|
||||||
|
const nameInput = await $('[data-testid="name-input"]')
|
||||||
|
expect(await nameInput.getValue()).toEqual('project-$nnn')
|
||||||
|
|
||||||
|
const closeButton = await $('[data-testid="close-button"]')
|
||||||
|
await click(closeButton)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('checks that no file exists, creates a new file', async () => {
|
||||||
|
const homeSection = await $('[data-testid="home-section"]')
|
||||||
|
expect(await homeSection.getText()).toContain('No Projects found')
|
||||||
|
|
||||||
|
const newFileButton = await $('[data-testid="home-new-file"]')
|
||||||
|
await click(newFileButton)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
expect(await homeSection.getText()).toContain('project-000')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens the new file and expects a loading stream', async () => {
|
||||||
|
const projectLink = await $('[data-testid="project-link"]')
|
||||||
|
await click(projectLink)
|
||||||
|
const loadingText = await $('[data-testid="loading-stream"]')
|
||||||
|
expect(await loadingText.getText()).toContain('Loading stream...')
|
||||||
|
await browser.execute('window.location.href = "tauri://localhost/home"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('signs out', async () => {
|
||||||
|
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
|
||||||
|
await click(menuButton)
|
||||||
const signoutButton = await $('[data-testid="user-sidebar-sign-out"]')
|
const signoutButton = await $('[data-testid="user-sidebar-sign-out"]')
|
||||||
await signoutButton.waitForClickable()
|
await click(signoutButton)
|
||||||
await browser.execute('arguments[0].click();', signoutButton)
|
|
||||||
const newSignInButton = await $('[data-testid="sign-in-button"]')
|
const newSignInButton = await $('[data-testid="sign-in-button"]')
|
||||||
expect(await newSignInButton.getText()).toEqual('Sign in')
|
expect(await newSignInButton.getText()).toEqual('Sign in')
|
||||||
})
|
})
|
||||||
|
@ -11,8 +11,13 @@
|
|||||||
/>
|
/>
|
||||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<script defer data-domain="app.kittycad.io" src="https://plausible.corp.kittycad.io/js/script.js"></script>
|
<link rel="stylesheet" href="https://use.typekit.net/zzv8rvm.css" />
|
||||||
<title>Modeling App</title>
|
<script
|
||||||
|
defer
|
||||||
|
data-domain="app.zoo.dev"
|
||||||
|
src="https://plausible.corp.zoo.dev/js/script.js"
|
||||||
|
></script>
|
||||||
|
<title>Zoo Modeling App</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="body-bg">
|
<body class="body-bg">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
69
make-release.sh
Executable file
@ -0,0 +1,69 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if ! git diff-index --quiet HEAD --; then
|
||||||
|
echo "Please stash uncommitted changes before running release script"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git checkout main
|
||||||
|
git pull
|
||||||
|
git fetch --all
|
||||||
|
|
||||||
|
# Get the latest semver tag from git
|
||||||
|
latest_tag=$(jq -r '.version' package.json)
|
||||||
|
latest_tag="v$latest_tag"
|
||||||
|
|
||||||
|
# Print the latest semver tag
|
||||||
|
echo "Latest semver tag: $latest_tag"
|
||||||
|
|
||||||
|
# Function to bump version numbers
|
||||||
|
bump_version() {
|
||||||
|
local version=$1
|
||||||
|
local bump_type=$2
|
||||||
|
local major=$(echo $version | cut -d '.' -f 1 | sed 's/v//')
|
||||||
|
local minor=$(echo $version | cut -d '.' -f 2)
|
||||||
|
local patch=$(echo $version | cut -d '.' -f 3)
|
||||||
|
|
||||||
|
case "$bump_type" in
|
||||||
|
major)
|
||||||
|
major=$((major + 1))
|
||||||
|
minor=0
|
||||||
|
patch=0
|
||||||
|
;;
|
||||||
|
minor)
|
||||||
|
minor=$((minor + 1))
|
||||||
|
patch=0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
patch=$((patch + 1))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "v${major}.${minor}.${patch}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine the type of bump based on the argument
|
||||||
|
bump_type=${1:-patch}
|
||||||
|
|
||||||
|
# Bump the version
|
||||||
|
new_version=$(bump_version $latest_tag $bump_type)
|
||||||
|
|
||||||
|
# Print the new semver tag
|
||||||
|
echo "New semver tag: $new_version"
|
||||||
|
new_version_number=${new_version:1}
|
||||||
|
echo "New version number without 'v': $new_version_number"
|
||||||
|
|
||||||
|
|
||||||
|
git checkout -b "cut-release-$new_version"
|
||||||
|
|
||||||
|
echo "$(jq --arg v "$new_version_number" '.version=$v' package.json --indent 2)" > package.json
|
||||||
|
echo "$(jq --arg v "$new_version_number" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json
|
||||||
|
|
||||||
|
git add package.json src-tauri/tauri.conf.json
|
||||||
|
git commit -m "Cut release $new_version"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Versions has been bumped in relevant json files, a branch has been created and committed to."
|
||||||
|
echo ""
|
||||||
|
echo "What's left for you to do is, push the branch and make the release PR."
|
||||||
|
echo ""
|
28
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.13.0",
|
"version": "0.15.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.10.2",
|
"@codemirror/autocomplete": "^6.10.2",
|
||||||
@ -10,17 +10,17 @@
|
|||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.7.17",
|
"@headlessui/react": "^1.7.17",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@kittycad/lib": "^0.0.46",
|
"@kittycad/lib": "^0.0.54",
|
||||||
"@lezer/javascript": "^1.4.9",
|
"@lezer/javascript": "^1.4.9",
|
||||||
"@open-rpc/client-js": "^1.8.1",
|
"@open-rpc/client-js": "^1.8.1",
|
||||||
"@react-hook/resize-observer": "^1.2.6",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
"@replit/codemirror-interact": "^6.3.0",
|
"@replit/codemirror-interact": "^6.3.0",
|
||||||
"@sentry/react": "^7.77.0",
|
|
||||||
"@tauri-apps/api": "^1.5.1",
|
"@tauri-apps/api": "^1.5.1",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/user-event": "^14.5.1",
|
"@testing-library/user-event": "^14.5.1",
|
||||||
"@ts-stack/markdown": "^1.5.0",
|
"@ts-stack/markdown": "^1.5.0",
|
||||||
|
"@tweenjs/tween.js": "^23.1.1",
|
||||||
"@types/node": "^16.7.13",
|
"@types/node": "^16.7.13",
|
||||||
"@types/react": "^18.2.41",
|
"@types/react": "^18.2.41",
|
||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
@ -33,6 +33,7 @@
|
|||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"json-rpc-2.0": "^1.6.0",
|
"json-rpc-2.0": "^1.6.0",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
"re-resizable": "^6.9.11",
|
"re-resizable": "^6.9.11",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@ -45,11 +46,12 @@
|
|||||||
"sketch-helpers": "^0.0.4",
|
"sketch-helpers": "^0.0.4",
|
||||||
"swr": "^2.2.2",
|
"swr": "^2.2.2",
|
||||||
"tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1",
|
"tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1",
|
||||||
|
"three": "^0.160.0",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"vitest": "^0.34.6",
|
"vitest": "^1.3.1",
|
||||||
"vscode-jsonrpc": "^8.1.0",
|
"vscode-jsonrpc": "^8.1.0",
|
||||||
"vscode-languageserver-protocol": "^3.17.5",
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
"wasm-pack": "^0.12.1",
|
"wasm-pack": "^0.12.1",
|
||||||
@ -70,7 +72,6 @@
|
|||||||
"test": "vitest --mode development",
|
"test": "vitest --mode development",
|
||||||
"test:nowatch": "vitest run --mode development",
|
"test:nowatch": "vitest run --mode development",
|
||||||
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests --benches)",
|
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests --benches)",
|
||||||
"test:cov": "vitest run --coverage --mode development",
|
|
||||||
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
|
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
|
||||||
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
||||||
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
||||||
@ -82,7 +83,9 @@
|
|||||||
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
||||||
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||||
"lint": "eslint --fix src",
|
"lint": "eslint --fix src",
|
||||||
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"
|
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
|
||||||
|
"postinstall": "yarn xstate:typegen",
|
||||||
|
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
@ -109,21 +112,21 @@
|
|||||||
"@tauri-apps/cli": "^1.5.6",
|
"@tauri-apps/cli": "^1.5.6",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/debounce-promise": "^3.1.8",
|
"@types/debounce-promise": "^3.1.8",
|
||||||
"@types/isomorphic-fetch": "^0.0.36",
|
|
||||||
"@types/pixelmatch": "^5.2.6",
|
"@types/pixelmatch": "^5.2.6",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
"@types/react-modal": "^3.16.3",
|
"@types/react-modal": "^3.16.3",
|
||||||
|
"@types/three": "^0.160.0",
|
||||||
"@types/uuid": "^9.0.4",
|
"@types/uuid": "^9.0.4",
|
||||||
"@types/wait-on": "^5.3.4",
|
"@types/wait-on": "^5.3.4",
|
||||||
"@types/wicg-file-system-access": "^2020.9.6",
|
"@types/wicg-file-system-access": "^2020.9.6",
|
||||||
"@types/ws": "^8.5.5",
|
"@types/ws": "^8.5.5",
|
||||||
"@vitejs/plugin-react": "^4.1.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"@vitest/coverage-istanbul": "^0.34.6",
|
|
||||||
"@wdio/cli": "^8.24.3",
|
"@wdio/cli": "^8.24.3",
|
||||||
"@wdio/globals": "^8.24.3",
|
"@wdio/globals": "^8.24.3",
|
||||||
"@wdio/local-runner": "^8.24.3",
|
"@wdio/local-runner": "^8.24.3",
|
||||||
"@wdio/mocha-framework": "^8.24.3",
|
"@wdio/mocha-framework": "^8.24.3",
|
||||||
"@wdio/spec-reporter": "^8.24.2",
|
"@wdio/spec-reporter": "^8.24.2",
|
||||||
|
"@xstate/cli": "^0.5.17",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"eslint": "^8.53.0",
|
"eslint": "^8.53.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
@ -133,12 +136,15 @@
|
|||||||
"pixelmatch": "^5.3.0",
|
"pixelmatch": "^5.3.0",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
"setimmediate": "^1.0.5",
|
"setimmediate": "^1.0.5",
|
||||||
"tailwindcss": "^3.3.6",
|
"tailwindcss": "^3.3.6",
|
||||||
"vite": "^4.5.0",
|
"vite": "^5.1.3",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-tsconfig-paths": "^4.2.1",
|
"vite-plugin-package-version": "^1.1.0",
|
||||||
|
"vite-tsconfig-paths": "^4.3.1",
|
||||||
|
"vitest-webgl-canvas-mock": "^1.1.0",
|
||||||
"wait-on": "^7.2.0",
|
"wait-on": "^7.2.0",
|
||||||
"yarn": "^1.22.19"
|
"yarn": "^1.22.19"
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read environment variables from file.
|
* Read environment variables from file.
|
||||||
@ -78,5 +78,4 @@ export default defineConfig({
|
|||||||
// url: 'http://127.0.0.1:3000',
|
// url: 'http://127.0.0.1:3000',
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
26
public/announce_release.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import requests
|
||||||
|
import os
|
||||||
|
|
||||||
|
webhook_url = os.getenv('DISCORD_WEBHOOK_URL')
|
||||||
|
release_version = os.getenv('RELEASE_VERSION')
|
||||||
|
release_body = os.getenv('RELEASE_BODY')
|
||||||
|
|
||||||
|
# message to send to Discord
|
||||||
|
data = {
|
||||||
|
"content":
|
||||||
|
f'''
|
||||||
|
**{release_version}** is now available! Check out the latest features and improvements here: https://zoo.dev/modeling-app/download
|
||||||
|
{release_body}
|
||||||
|
''',
|
||||||
|
"username": "Modeling App Release Updates",
|
||||||
|
"avatar_url": "https://raw.githubusercontent.com/KittyCAD/modeling-app/main/public/discord-avatar.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
# POST request to the Discord webhook
|
||||||
|
response = requests.post(webhook_url, json=data)
|
||||||
|
|
||||||
|
# Check for success
|
||||||
|
if response.status_code == 204:
|
||||||
|
print("Successfully sent the message to Discord.")
|
||||||
|
else:
|
||||||
|
print("Failed to send the message to Discord.")
|
BIN
public/discord-avatar.png
Normal file
After Width: | Height: | Size: 11 KiB |
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
First off, thank you so much for your interest in being a part of the closed Alpha program! We are thrilled to have others use our product and see what you build with it (and truthfully, how you break it too).
|
First off, thank you so much for your interest in being a part of the closed Alpha program! We are thrilled to have others use our product and see what you build with it (and truthfully, how you break it too).
|
||||||
|
|
||||||
### KittyCAD Modeling App (KCMA)
|
### Zoo Modeling App (ZMA)
|
||||||
|
|
||||||
What we are introducing to you is our KittyCAD Modeling App (KCMA). KCMA is a CAD application that expresses a hybrid style of traditional CAD interface along with a code-CAD interface. KCMA is a great way for us to test our own APIs as well as inspire others to develop their own applications.
|
What we are introducing to you is our Zoo Modeling App (ZMA). ZMA is a CAD application that expresses a hybrid style of traditional CAD interface along with a code-CAD interface. ZMA is a great way for us to test our own APIs as well as inspire others to develop their own applications.
|
||||||
|
|
||||||
### Why Code?
|
### Why Code?
|
||||||
|
|
||||||
@ -18,11 +18,11 @@ Plenty of you have professional CAD experience, and may not understand why codin
|
|||||||
- Reproducibility
|
- Reproducibility
|
||||||
- Easier integration with other tools
|
- Easier integration with other tools
|
||||||
|
|
||||||
### Before You Use KCMA
|
### Before You Use ZMA
|
||||||
|
|
||||||
Before you dive straight into the app, we wanted to lay some expectations out for you.
|
Before you dive straight into the app, we wanted to lay some expectations out for you.
|
||||||
|
|
||||||
- KCMA is in early development. Kurt pitched the idea back in January, and the team has been working hard on it since then. KCMA has really basic CAD features for now, but we have plenty of features on our roadmap. Most of the features that you may be currently used to in your CAD workflow today will be available down the road.
|
- ZMA is in early development. Kurt pitched the idea back in January, and the team has been working hard on it since then. ZMA has really basic CAD features for now, but we have plenty of features on our roadmap. Most of the features that you may be currently used to in your CAD workflow today will be available down the road.
|
||||||
- For a list of all scripting functions, please reference our [documentation](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md). For a basic rundown of our types, please reference [this document](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/types.md).
|
- For a list of all scripting functions, please reference our [documentation](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md). For a basic rundown of our types, please reference [this document](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/types.md).
|
||||||
- With that being said, we have created an external new features list in [GH Discussions](https://github.com/KittyCAD/modeling-app/discussions). For our current priority list, please click [here](https://github.com/KittyCAD/modeling-app/blob/main/public/roadmap.md). Please upvote any features in the GH Discussions page that you would like to see implemented first. We will prioritize the highest upvoted items or items that are foundational for other features on the list. You can also add your own, but we will review it to make sure it’s not a duplicate or it’s feasible for the current state of the app.
|
- With that being said, we have created an external new features list in [GH Discussions](https://github.com/KittyCAD/modeling-app/discussions). For our current priority list, please click [here](https://github.com/KittyCAD/modeling-app/blob/main/public/roadmap.md). Please upvote any features in the GH Discussions page that you would like to see implemented first. We will prioritize the highest upvoted items or items that are foundational for other features on the list. You can also add your own, but we will review it to make sure it’s not a duplicate or it’s feasible for the current state of the app.
|
||||||
- Please report any and all bugs/issues you find. Even the smallest bugs are important! You can report them in a GH Issue [here](https://github.com/KittyCAD/modeling-app/issues/new). You are more than welcome to link your GH Issue in the **bugs** section of our Discord, but if you want to discuss the bug further, please keep that in the GH Issue thread. Please include the severity of the bug in your GH Issue ticket (High, Medium, or Low). If you are having trouble deciding what severity the bug is, use this guideline:
|
- Please report any and all bugs/issues you find. Even the smallest bugs are important! You can report them in a GH Issue [here](https://github.com/KittyCAD/modeling-app/issues/new). You are more than welcome to link your GH Issue in the **bugs** section of our Discord, but if you want to discuss the bug further, please keep that in the GH Issue thread. Please include the severity of the bug in your GH Issue ticket (High, Medium, or Low). If you are having trouble deciding what severity the bug is, use this guideline:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "KCMA",
|
"short_name": "ZMA",
|
||||||
"name": "KittyCAD Modeling App",
|
"name": "Zoo Modeling App",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
|
110
src-tauri/Cargo.lock
generated
@ -67,9 +67,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.75"
|
version = "1.0.80"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "app"
|
name = "app"
|
||||||
@ -95,7 +95,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -542,7 +542,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -552,7 +552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e"
|
checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -582,7 +582,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim",
|
"strsim",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -593,7 +593,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -899,7 +899,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1242,9 +1242,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.20"
|
version = "0.3.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
|
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@ -1252,7 +1252,7 @@ dependencies = [
|
|||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"indexmap 1.9.3",
|
"indexmap 2.0.0",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@ -1664,9 +1664,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad"
|
name = "kittycad"
|
||||||
version = "0.2.42"
|
version = "0.2.58"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6aa554d86b6dbbd976a659c912ae25ce817b4378eb12a5684907e263410f0a7b"
|
checksum = "049c3881ffbe77bf1c3a968372a246ce906eceb79f61cd0bc5fa229bec3504cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -2215,7 +2215,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2368,7 +2368,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"regex-syntax 0.7.5",
|
"regex-syntax 0.7.5",
|
||||||
"structmeta",
|
"structmeta",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2487,7 +2487,7 @@ dependencies = [
|
|||||||
"phf_shared 0.11.2",
|
"phf_shared 0.11.2",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2555,7 +2555,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2657,9 +2657,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.67"
|
version = "1.0.78"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
|
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@ -2675,9 +2675,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.33"
|
version = "1.0.35"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
|
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@ -3235,9 +3235,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.193"
|
version = "1.0.197"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
|
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
@ -3253,13 +3253,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.193"
|
version = "1.0.197"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
|
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3275,9 +3275,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.108"
|
version = "1.0.114"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
|
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa 1.0.6",
|
"itoa 1.0.6",
|
||||||
"ryu",
|
"ryu",
|
||||||
@ -3302,7 +3302,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3352,7 +3352,7 @@ dependencies = [
|
|||||||
"darling",
|
"darling",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3556,7 +3556,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"structmeta-derive",
|
"structmeta-derive",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3567,7 +3567,7 @@ checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3605,9 +3605,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.33"
|
version = "2.0.48"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668"
|
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3760,9 +3760,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri"
|
name = "tauri"
|
||||||
version = "1.5.3"
|
version = "1.5.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32d563b672acde8d0cc4c1b1f5b855976923f67e8d6fe1eba51df0211e197be2"
|
checksum = "fd27c04b9543776a972c86ccf70660b517ecabbeced9fb58d8b961a13ad129af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -3812,9 +3812,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-build"
|
name = "tauri-build"
|
||||||
version = "1.5.0"
|
version = "1.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "defbfc551bd38ab997e5f8e458f87396d2559d05ce32095076ad6c30f7fc5f9c"
|
checksum = "e9914a4715e0b75d9f387a285c7e26b5bbfeb1249ad9f842675a82481565c532"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
@ -3831,9 +3831,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-codegen"
|
name = "tauri-codegen"
|
||||||
version = "1.4.1"
|
version = "1.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b3475e55acec0b4a50fb96435f19631fb58cbcd31923e1a213de5c382536bbb"
|
checksum = "a1554c5857f65dbc377cefb6b97c8ac77b1cb2a90d30d3448114d5d6b48a77fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.2",
|
"base64 0.21.2",
|
||||||
"brotli",
|
"brotli",
|
||||||
@ -3857,9 +3857,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-macros"
|
name = "tauri-macros"
|
||||||
version = "1.4.2"
|
version = "1.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acea6445eececebd72ed7720cfcca46eee3b5bad8eb408be8f7ef2e3f7411500"
|
checksum = "277abf361a3a6993ec16bcbb179de0d6518009b851090a01adfea12ac89fa875"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -3872,7 +3872,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-fs-extra"
|
name = "tauri-plugin-fs-extra"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#537053d3171a7374a1a86fed422523e7b45a4fb8"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#ed682dd96eb765e7cd3cdbc3cc64f794a0d6f9df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
@ -3883,9 +3883,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "0.14.1"
|
version = "0.14.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07f8e9e53e00e9f41212c115749e87d5cd2a9eebccafca77a19722eeecd56d43"
|
checksum = "cf2d0652aa2891ff3e9caa2401405257ea29ab8372cce01f186a5825f1bd0e76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gtk",
|
"gtk",
|
||||||
"http",
|
"http",
|
||||||
@ -3904,9 +3904,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime-wry"
|
name = "tauri-runtime-wry"
|
||||||
version = "0.14.2"
|
version = "0.14.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "803a01101bc611ba03e13329951a1bde44287a54234189b9024b78619c1bc206"
|
checksum = "6cae61fbc731f690a4899681c9052dde6d05b159b44563ace8186fc1bfb7d158"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"gtk",
|
"gtk",
|
||||||
@ -3924,9 +3924,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-utils"
|
name = "tauri-utils"
|
||||||
version = "1.5.1"
|
version = "1.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a52165bb340e6f6a75f1f5eeeab1bb49f861c12abe3a176067d53642b5454986"
|
checksum = "ece74810b1d3d44f29f732a7ae09a63183d63949bbdd59c61f8ed2a1b70150db"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"ctor",
|
"ctor",
|
||||||
@ -4009,7 +4009,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4051,9 +4051,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.34.0"
|
version = "1.36.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
|
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -4193,7 +4193,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4443,7 +4443,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4477,7 +4477,7 @@ checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.33",
|
"syn 2.0.48",
|
||||||
"wasm-bindgen-backend",
|
"wasm-bindgen-backend",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
@ -12,17 +12,17 @@ rust-version = "1.60"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "1.5.0", features = [] }
|
tauri-build = { version = "1.5.1", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
kittycad = "0.2.42"
|
kittycad = "0.2.58"
|
||||||
oauth2 = "4.4.2"
|
oauth2 = "4.4.2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tauri = { version = "1.5.3", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] }
|
tauri = { version = "1.5.4", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] }
|
||||||
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
tokio = { version = "1.34.0", features = ["time"] }
|
tokio = { version = "1.36.0", features = ["time"] }
|
||||||
toml = "0.8.2"
|
toml = "0.8.2"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 69 KiB |
@ -7,6 +7,7 @@ use std::io::Read;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use oauth2::TokenResponse;
|
use oauth2::TokenResponse;
|
||||||
|
use std::process::Command;
|
||||||
use tauri::{InvokeError, Manager};
|
use tauri::{InvokeError, Manager};
|
||||||
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
"distDir": "../build"
|
"distDir": "../build"
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "kittycad-modeling",
|
"productName": "zoo-modeling-app",
|
||||||
"version": "0.13.0"
|
"version": "0.15.4"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
@ -84,7 +84,7 @@
|
|||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
"height": 1200,
|
"height": 1200,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "KittyCAD Modeling",
|
"title": "Zoo Modeling App",
|
||||||
"width": 1800
|
"width": 1800
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "KittyCAD Modeling"
|
"productName": "Zoo Modeling App"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"updater": {
|
"updater": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://dl.kittycad.io/releases/modeling-app/last_update.json"
|
"https://dl.zoo.dev/releases/modeling-app/last_update.json"
|
||||||
],
|
],
|
||||||
"dialog": true,
|
"dialog": true,
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "KittyCAD Modeling"
|
"productName": "Zoo Modeling App"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,73 +0,0 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
|
||||||
import { App } from './App'
|
|
||||||
import { describe, test, vi } from 'vitest'
|
|
||||||
import {
|
|
||||||
Route,
|
|
||||||
RouterProvider,
|
|
||||||
createMemoryRouter,
|
|
||||||
createRoutesFromElements,
|
|
||||||
} from 'react-router-dom'
|
|
||||||
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
|
||||||
import CommandBarProvider from 'components/CommandBar/CommandBar'
|
|
||||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
|
||||||
import { BROWSER_FILE_NAME } from 'Router'
|
|
||||||
|
|
||||||
let listener: ((rect: any) => void) | undefined = undefined
|
|
||||||
;(global as any).ResizeObserver = class ResizeObserver {
|
|
||||||
constructor(ls: ((rect: any) => void) | undefined) {
|
|
||||||
listener = ls
|
|
||||||
}
|
|
||||||
observe() {}
|
|
||||||
unobserve() {}
|
|
||||||
disconnect() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('App tests', () => {
|
|
||||||
test('Renders the modeling app screen, including "Variables" pane.', () => {
|
|
||||||
vi.mock('react-router-dom', async () => {
|
|
||||||
const actual = (await vi.importActual('react-router-dom')) as Record<
|
|
||||||
string,
|
|
||||||
any
|
|
||||||
>
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
useParams: () => ({ id: BROWSER_FILE_NAME }),
|
|
||||||
useLoaderData: () => ({ code: null }),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
render(
|
|
||||||
<TestWrap>
|
|
||||||
<App />
|
|
||||||
</TestWrap>
|
|
||||||
)
|
|
||||||
const linkElement = screen.getByText(/Variables/i)
|
|
||||||
expect(linkElement).toBeInTheDocument()
|
|
||||||
|
|
||||||
vi.restoreAllMocks()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
|
||||||
// We have to use a memory router in the testing environment,
|
|
||||||
// and we have to use the createMemoryRouter function instead of <MemoryRouter /> as of react-router v6.4:
|
|
||||||
// https://reactrouter.com/en/6.16.0/routers/picking-a-router#using-v64-data-apis
|
|
||||||
const router = createMemoryRouter(
|
|
||||||
createRoutesFromElements(
|
|
||||||
<Route
|
|
||||||
path="/file/:id"
|
|
||||||
element={
|
|
||||||
<CommandBarProvider>
|
|
||||||
<GlobalStateProvider>
|
|
||||||
<ModelingMachineProvider>{children}</ModelingMachineProvider>
|
|
||||||
</GlobalStateProvider>
|
|
||||||
</CommandBarProvider>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
{
|
|
||||||
initialEntries: ['/file/new'],
|
|
||||||
initialIndex: 0,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return <RouterProvider router={router} />
|
|
||||||
}
|
|
84
src/App.tsx
@ -19,21 +19,24 @@ import {
|
|||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { getNormalisedCoordinates } from './lib/utils'
|
import { getNormalisedCoordinates } from './lib/utils'
|
||||||
import { useLoaderData } from 'react-router-dom'
|
import { useLoaderData, useNavigate } from 'react-router-dom'
|
||||||
import { IndexLoaderData } from './Router'
|
import { type IndexLoaderData } from 'lib/types'
|
||||||
|
import { paths } from 'lib/paths'
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import { onboardingPaths } from 'routes/Onboarding'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
|
||||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
|
||||||
import { CodeMenu } from 'components/CodeMenu'
|
import { CodeMenu } from 'components/CodeMenu'
|
||||||
import { TextEditor } from 'components/TextEditor'
|
import { TextEditor } from 'components/TextEditor'
|
||||||
import { Themes, getSystemTheme } from 'lib/theme'
|
import { Themes, getSystemTheme } from 'lib/theme'
|
||||||
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||||
import { engineCommandManager } from './lang/std/engineConnection'
|
import { engineCommandManager } from './lang/std/engineConnection'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||||
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { project, file } = useLoaderData() as IndexLoaderData
|
const { project, file } = useLoaderData() as IndexLoaderData
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const filePath = useAbsoluteFilePath()
|
||||||
|
|
||||||
useHotKeyListener()
|
useHotKeyListener()
|
||||||
const {
|
const {
|
||||||
@ -51,8 +54,7 @@ export function App() {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const { settings } = useGlobalStateContext()
|
const { settings } = useGlobalStateContext()
|
||||||
const { showDebugPanel, onboardingStatus, cameraControls, theme } =
|
const { showDebugPanel, onboardingStatus, theme } = settings?.context || {}
|
||||||
settings?.context || {}
|
|
||||||
const { state, send } = useModelingContext()
|
const { state, send } = useModelingContext()
|
||||||
|
|
||||||
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||||
@ -71,6 +73,16 @@ export function App() {
|
|||||||
useHotkeys('shift + e', () => togglePane('kclErrors'))
|
useHotkeys('shift + e', () => togglePane('kclErrors'))
|
||||||
useHotkeys('shift + d', () => togglePane('debug'))
|
useHotkeys('shift + d', () => togglePane('debug'))
|
||||||
useHotkeys('esc', () => send('Cancel'))
|
useHotkeys('esc', () => send('Cancel'))
|
||||||
|
useHotkeys('backspace', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
})
|
||||||
|
useHotkeys(
|
||||||
|
isTauri() ? 'mod + ,' : 'shift + mod + ,',
|
||||||
|
() => navigate(filePath + paths.SETTINGS),
|
||||||
|
{
|
||||||
|
splitKey: '|',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
|
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
|
||||||
(p) => p === onboardingStatus
|
(p) => p === onboardingStatus
|
||||||
@ -84,9 +96,11 @@ export function App() {
|
|||||||
|
|
||||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||||
engineCommandManager.sendSceneCommand(message)
|
engineCommandManager.sendSceneCommand(message)
|
||||||
}, 16)
|
}, 1000 / 15)
|
||||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
e.nativeEvent.preventDefault()
|
if (state.matches('Sketch')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const { x, y } = getNormalisedCoordinates({
|
const { x, y } = getNormalisedCoordinates({
|
||||||
clientX: e.clientX,
|
clientX: e.clientX,
|
||||||
@ -97,58 +111,11 @@ export function App() {
|
|||||||
|
|
||||||
const newCmdId = uuidv4()
|
const newCmdId = uuidv4()
|
||||||
if (buttonDownInStream === undefined) {
|
if (buttonDownInStream === undefined) {
|
||||||
if (state.matches('Sketch.Line Tool')) {
|
|
||||||
debounceSocketSend({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: newCmdId,
|
|
||||||
cmd: {
|
|
||||||
type: 'mouse_move',
|
|
||||||
window: { x, y },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
debounceSocketSend({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'highlight_set_entity',
|
|
||||||
selected_at_window: { x, y },
|
|
||||||
},
|
|
||||||
cmd_id: newCmdId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (state.matches('Sketch.Move Tool')) {
|
|
||||||
debounceSocketSend({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: newCmdId,
|
|
||||||
cmd: {
|
|
||||||
type: 'handle_mouse_drag_move',
|
|
||||||
window: { x, y },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
|
||||||
let interaction: CameraDragInteractionType_type
|
|
||||||
|
|
||||||
const eWithButton = { ...e, button: buttonDownInStream }
|
|
||||||
|
|
||||||
if (interactionGuards.pan.callback(eWithButton)) {
|
|
||||||
interaction = 'pan'
|
|
||||||
} else if (interactionGuards.rotate.callback(eWithButton)) {
|
|
||||||
interaction = 'rotate'
|
|
||||||
} else if (interactionGuards.zoom.dragCallback(eWithButton)) {
|
|
||||||
interaction = 'zoom'
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
debounceSocketSend({
|
debounceSocketSend({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'camera_drag_move',
|
type: 'highlight_set_entity',
|
||||||
interaction,
|
selected_at_window: { x, y },
|
||||||
window: { x, y },
|
|
||||||
},
|
},
|
||||||
cmd_id: newCmdId,
|
cmd_id: newCmdId,
|
||||||
})
|
})
|
||||||
@ -238,6 +205,7 @@ export function App() {
|
|||||||
open={openPanes.includes('debug')}
|
open={openPanes.includes('debug')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{/* <CamToggle /> */}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
109
src/Router.tsx
@ -14,10 +14,7 @@ import {
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { ErrorPage } from './components/ErrorPage'
|
import { ErrorPage } from './components/ErrorPage'
|
||||||
import { Settings } from './routes/Settings'
|
import { Settings } from './routes/Settings'
|
||||||
import Onboarding, {
|
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
|
||||||
onboardingRoutes,
|
|
||||||
onboardingPaths,
|
|
||||||
} from './routes/Onboarding'
|
|
||||||
import SignIn from './routes/SignIn'
|
import SignIn from './routes/SignIn'
|
||||||
import { Auth } from './Auth'
|
import { Auth } from './Auth'
|
||||||
import { isTauri } from './lib/isTauri'
|
import { isTauri } from './lib/isTauri'
|
||||||
@ -29,7 +26,7 @@ import {
|
|||||||
isProjectDirectory,
|
isProjectDirectory,
|
||||||
PROJECT_ENTRYPOINT,
|
PROJECT_ENTRYPOINT,
|
||||||
} from './lib/tauriFS'
|
} from './lib/tauriFS'
|
||||||
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
|
import { metadata } from 'tauri-plugin-fs-extra-api'
|
||||||
import DownloadAppBanner from './components/DownloadAppBanner'
|
import DownloadAppBanner from './components/DownloadAppBanner'
|
||||||
import { WasmErrBanner } from './components/WasmErrBanner'
|
import { WasmErrBanner } from './components/WasmErrBanner'
|
||||||
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||||
@ -38,82 +35,19 @@ import {
|
|||||||
settingsMachine,
|
settingsMachine,
|
||||||
} from './machines/settingsMachine'
|
} from './machines/settingsMachine'
|
||||||
import { ContextFrom } from 'xstate'
|
import { ContextFrom } from 'xstate'
|
||||||
import CommandBarProvider from 'components/CommandBar/CommandBar'
|
import CommandBarProvider, {
|
||||||
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
CommandBar,
|
||||||
import * as Sentry from '@sentry/react'
|
} from 'components/CommandBar/CommandBar'
|
||||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||||
import { KclContextProvider, kclManager } from 'lang/KclSinglton'
|
import { KclContextProvider, kclManager } from 'lang/KclSingleton'
|
||||||
import FileMachineProvider from 'components/FileMachineProvider'
|
import FileMachineProvider from 'components/FileMachineProvider'
|
||||||
import { sep } from '@tauri-apps/api/path'
|
import { sep } from '@tauri-apps/api/path'
|
||||||
|
import { paths } from 'lib/paths'
|
||||||
if (VITE_KC_SENTRY_DSN && !TEST) {
|
import { IndexLoaderData, HomeLoaderData } from 'lib/types'
|
||||||
Sentry.init({
|
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||||
dsn: VITE_KC_SENTRY_DSN,
|
|
||||||
// TODO(paultag): pass in the right env here.
|
|
||||||
// environment: "production",
|
|
||||||
integrations: [
|
|
||||||
new Sentry.BrowserTracing({
|
|
||||||
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
|
|
||||||
useEffect,
|
|
||||||
useLocation,
|
|
||||||
useNavigationType,
|
|
||||||
createRoutesFromChildren,
|
|
||||||
matchRoutes
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
new Sentry.Replay(),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Set tracesSampleRate to 1.0 to capture 100%
|
|
||||||
// of transactions for performance monitoring.
|
|
||||||
tracesSampleRate: 1.0,
|
|
||||||
|
|
||||||
// TODO: Add in kittycad.io endpoints
|
|
||||||
tracePropagationTargets: ['localhost'],
|
|
||||||
|
|
||||||
// Capture Replay for 10% of all sessions,
|
|
||||||
// plus for 100% of sessions with an error
|
|
||||||
replaysSessionSampleRate: 0.1,
|
|
||||||
replaysOnErrorSampleRate: 1.0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const prependRoutes =
|
|
||||||
(routesObject: Record<string, string>) => (prepend: string) => {
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(routesObject).map(([constName, path]) => [
|
|
||||||
constName,
|
|
||||||
prepend + path,
|
|
||||||
])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const paths = {
|
|
||||||
INDEX: '/',
|
|
||||||
HOME: '/home',
|
|
||||||
FILE: '/file',
|
|
||||||
SETTINGS: '/settings',
|
|
||||||
SIGN_IN: '/signin',
|
|
||||||
ONBOARDING: prependRoutes(onboardingPaths)(
|
|
||||||
'/onboarding'
|
|
||||||
) as typeof onboardingPaths,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BROWSER_FILE_NAME = 'new'
|
export const BROWSER_FILE_NAME = 'new'
|
||||||
|
|
||||||
export type IndexLoaderData = {
|
|
||||||
code: string | null
|
|
||||||
project?: ProjectWithEntryPointMetadata
|
|
||||||
file?: FileEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ProjectWithEntryPointMetadata = FileEntry & {
|
|
||||||
entrypointMetadata: Metadata
|
|
||||||
}
|
|
||||||
export type HomeLoaderData = {
|
|
||||||
projects: ProjectWithEntryPointMetadata[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
|
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
|
||||||
|
|
||||||
const addGlobalContextToElements = (
|
const addGlobalContextToElements = (
|
||||||
@ -145,18 +79,19 @@ const router = createBrowserRouter(
|
|||||||
{
|
{
|
||||||
path: paths.FILE + '/:id',
|
path: paths.FILE + '/:id',
|
||||||
element: (
|
element: (
|
||||||
<Auth>
|
<KclContextProvider>
|
||||||
<FileMachineProvider>
|
<Auth>
|
||||||
<KclContextProvider>
|
<FileMachineProvider>
|
||||||
<ModelingMachineProvider>
|
<ModelingMachineProvider>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<App />
|
<App />
|
||||||
|
<CommandBar />
|
||||||
</ModelingMachineProvider>
|
</ModelingMachineProvider>
|
||||||
<WasmErrBanner />
|
<WasmErrBanner />
|
||||||
</KclContextProvider>
|
</FileMachineProvider>
|
||||||
</FileMachineProvider>
|
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
||||||
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
</Auth>
|
||||||
</Auth>
|
</KclContextProvider>
|
||||||
),
|
),
|
||||||
id: paths.FILE,
|
id: paths.FILE,
|
||||||
loader: async ({
|
loader: async ({
|
||||||
@ -209,6 +144,10 @@ const router = createBrowserRouter(
|
|||||||
const children = await readDir(projectPath, { recursive: true })
|
const children = await readDir(projectPath, { recursive: true })
|
||||||
kclManager.setCodeAndExecute(code, false)
|
kclManager.setCodeAndExecute(code, false)
|
||||||
|
|
||||||
|
// Set the file system manager to the project path
|
||||||
|
// So that WASM gets an updated path for operations
|
||||||
|
fileSystemManager.dir = projectPath
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code,
|
code,
|
||||||
project: {
|
project: {
|
||||||
@ -246,9 +185,10 @@ const router = createBrowserRouter(
|
|||||||
<Auth>
|
<Auth>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<Home />
|
<Home />
|
||||||
|
<CommandBar />
|
||||||
</Auth>
|
</Auth>
|
||||||
),
|
),
|
||||||
loader: async () => {
|
loader: async (): Promise<HomeLoaderData | Response> => {
|
||||||
if (!isTauri()) {
|
if (!isTauri()) {
|
||||||
return redirect(paths.FILE + '/' + BROWSER_FILE_NAME)
|
return redirect(paths.FILE + '/' + BROWSER_FILE_NAME)
|
||||||
}
|
}
|
||||||
@ -259,6 +199,7 @@ const router = createBrowserRouter(
|
|||||||
const projectDir = await initializeProjectDirectory(
|
const projectDir = await initializeProjectDirectory(
|
||||||
persistedSettings.defaultDirectory || ''
|
persistedSettings.defaultDirectory || ''
|
||||||
)
|
)
|
||||||
|
let newDefaultDirectory: string | undefined = undefined
|
||||||
if (projectDir !== persistedSettings.defaultDirectory) {
|
if (projectDir !== persistedSettings.defaultDirectory) {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
SETTINGS_PERSIST_KEY,
|
SETTINGS_PERSIST_KEY,
|
||||||
@ -267,6 +208,7 @@ const router = createBrowserRouter(
|
|||||||
defaultDirectory: projectDir,
|
defaultDirectory: projectDir,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
newDefaultDirectory = projectDir
|
||||||
}
|
}
|
||||||
const projectsNoMeta = (await readDir(projectDir)).filter(
|
const projectsNoMeta = (await readDir(projectDir)).filter(
|
||||||
isProjectDirectory
|
isProjectDirectory
|
||||||
@ -282,6 +224,7 @@ const router = createBrowserRouter(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
projects,
|
projects,
|
||||||
|
newDefaultDirectory,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
126
src/Toolbar.tsx
@ -5,6 +5,13 @@ import { useModelingContext } from 'hooks/useModelingContext'
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import usePlatform from 'hooks/usePlatform'
|
import usePlatform from 'hooks/usePlatform'
|
||||||
|
import { isSingleCursorInPipe } from 'lang/queryAst'
|
||||||
|
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||||
|
import {
|
||||||
|
NetworkHealthState,
|
||||||
|
useNetworkStatus,
|
||||||
|
} from 'components/NetworkHealthIndicator'
|
||||||
|
import { useStore } from 'useStore'
|
||||||
|
|
||||||
export const Toolbar = () => {
|
export const Toolbar = () => {
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
@ -13,14 +20,22 @@ export const Toolbar = () => {
|
|||||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||||
const bgClassName =
|
const bgClassName =
|
||||||
'group-enabled:group-hover:bg-energy-10 group-pressed:bg-energy-10 dark:group-enabled:group-hover:bg-chalkboard-80 dark:group-pressed:bg-chalkboard-80'
|
'group-enabled:group-hover:bg-energy-10 group-pressed:bg-energy-10 dark:group-enabled:group-hover:bg-chalkboard-80 dark:group-pressed:bg-chalkboard-80'
|
||||||
const pathId = useMemo(
|
const pathId = useMemo(() => {
|
||||||
() =>
|
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) {
|
||||||
isCursorInSketchCommandRange(
|
return false
|
||||||
engineCommandManager.artifactMap,
|
}
|
||||||
context.selectionRanges
|
return isCursorInSketchCommandRange(
|
||||||
),
|
engineCommandManager.artifactMap,
|
||||||
[engineCommandManager.artifactMap, context.selectionRanges]
|
context.selectionRanges
|
||||||
)
|
)
|
||||||
|
}, [engineCommandManager.artifactMap, context.selectionRanges])
|
||||||
|
const { overallState } = useNetworkStatus()
|
||||||
|
const { isExecuting } = useKclContext()
|
||||||
|
const { isStreamReady } = useStore((s) => ({
|
||||||
|
isStreamReady: s.isStreamReady,
|
||||||
|
}))
|
||||||
|
const disableAllButtons =
|
||||||
|
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
|
||||||
|
|
||||||
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
|
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
|
||||||
const span = toolbarButtonsRef.current
|
const span = toolbarButtonsRef.current
|
||||||
@ -50,11 +65,14 @@ export const Toolbar = () => {
|
|||||||
<li className="contents">
|
<li className="contents">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => send({ type: 'Enter sketch' })}
|
onClick={() =>
|
||||||
|
send({ type: 'Enter sketch', data: { forceNewSketch: true } })
|
||||||
|
}
|
||||||
icon={{
|
icon={{
|
||||||
icon: 'sketch',
|
icon: 'sketch',
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
|
disabled={disableAllButtons}
|
||||||
>
|
>
|
||||||
<span data-testid="start-sketch">Start Sketch</span>
|
<span data-testid="start-sketch">Start Sketch</span>
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
@ -69,6 +87,7 @@ export const Toolbar = () => {
|
|||||||
icon: 'sketch',
|
icon: 'sketch',
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
|
disabled={disableAllButtons}
|
||||||
>
|
>
|
||||||
Edit Sketch
|
Edit Sketch
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
@ -83,50 +102,57 @@ export const Toolbar = () => {
|
|||||||
icon: 'arrowLeft',
|
icon: 'arrowLeft',
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
|
disabled={disableAllButtons}
|
||||||
>
|
>
|
||||||
Exit Sketch
|
Exit Sketch
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{state.matches('Sketch') && !state.matches('idle') && (
|
{state.matches('Sketch') && !state.matches('idle') && (
|
||||||
<li className="contents">
|
<>
|
||||||
<ActionButton
|
<li className="contents" key="line-button">
|
||||||
Element="button"
|
<ActionButton
|
||||||
onClick={() =>
|
Element="button"
|
||||||
state.matches('Sketch.Line Tool')
|
onClick={() =>
|
||||||
? send('CancelSketch')
|
state?.matches('Sketch.Line tool')
|
||||||
: send('Equip tool')
|
? send('CancelSketch')
|
||||||
}
|
: send('Equip Line tool')
|
||||||
aria-pressed={state.matches('Sketch.Line Tool')}
|
}
|
||||||
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
aria-pressed={state?.matches('Sketch.Line tool')}
|
||||||
icon={{
|
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
||||||
icon: 'line',
|
icon={{
|
||||||
bgClassName,
|
icon: 'line',
|
||||||
}}
|
bgClassName,
|
||||||
>
|
}}
|
||||||
Line
|
disabled={disableAllButtons}
|
||||||
</ActionButton>
|
>
|
||||||
</li>
|
Line
|
||||||
)}
|
</ActionButton>
|
||||||
{state.matches('Sketch') && (
|
</li>
|
||||||
<li className="contents">
|
<li className="contents" key="tangential-arc-button">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
state.matches('Sketch.Move Tool')
|
state.matches('Sketch.Tangential arc to')
|
||||||
? send('CancelSketch')
|
? send('CancelSketch')
|
||||||
: send('Equip move tool')
|
: send('Equip tangential arc to')
|
||||||
}
|
}
|
||||||
aria-pressed={state.matches('Sketch.Move Tool')}
|
aria-pressed={state.matches('Sketch.Tangential arc to')}
|
||||||
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
className="pressed:bg-energy-10/20 dark:pressed:bg-energy-80"
|
||||||
icon={{
|
icon={{
|
||||||
icon: 'move',
|
icon: 'arc',
|
||||||
bgClassName,
|
bgClassName,
|
||||||
}}
|
}}
|
||||||
>
|
disabled={
|
||||||
Move
|
(!state.can('Equip tangential arc to') &&
|
||||||
</ActionButton>
|
!state.matches('Sketch.Tangential arc to')) ||
|
||||||
</li>
|
disableAllButtons
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Tangential Arc
|
||||||
|
</ActionButton>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{state.matches('Sketch.SketchIdle') &&
|
{state.matches('Sketch.SketchIdle') &&
|
||||||
state.nextEvents
|
state.nextEvents
|
||||||
@ -151,7 +177,7 @@ export const Toolbar = () => {
|
|||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
.map((eventName) => (
|
.map((eventName) => (
|
||||||
<li className="contents">
|
<li className="contents" key={eventName}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
@ -160,7 +186,7 @@ export const Toolbar = () => {
|
|||||||
disabled={
|
disabled={
|
||||||
!state.nextEvents
|
!state.nextEvents
|
||||||
.filter((event) => state.can(event as any))
|
.filter((event) => state.can(event as any))
|
||||||
.includes(eventName)
|
.includes(eventName) || disableAllButtons
|
||||||
}
|
}
|
||||||
title={eventName}
|
title={eventName}
|
||||||
icon={{
|
icon={{
|
||||||
@ -185,7 +211,7 @@ export const Toolbar = () => {
|
|||||||
data: { name: 'Extrude', ownerMachine: 'modeling' },
|
data: { name: 'Extrude', ownerMachine: 'modeling' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={!state.can('Extrude')}
|
disabled={!state.can('Extrude') || disableAllButtons}
|
||||||
title={
|
title={
|
||||||
state.can('Extrude')
|
state.can('Extrude')
|
||||||
? 'extrude'
|
? 'extrude'
|
||||||
|
898
src/clientSideScene/CameraControls.ts
Normal file
@ -0,0 +1,898 @@
|
|||||||
|
import { MouseGuard } from 'lib/cameraControls'
|
||||||
|
import {
|
||||||
|
Euler,
|
||||||
|
MathUtils,
|
||||||
|
Matrix4,
|
||||||
|
OrthographicCamera,
|
||||||
|
PerspectiveCamera,
|
||||||
|
Quaternion,
|
||||||
|
Spherical,
|
||||||
|
Vector2,
|
||||||
|
Vector3,
|
||||||
|
} from 'three'
|
||||||
|
import {
|
||||||
|
DEBUG_SHOW_INTERSECTION_PLANE,
|
||||||
|
INTERSECTION_PLANE_LAYER,
|
||||||
|
SKETCH_LAYER,
|
||||||
|
ZOOM_MAGIC_NUMBER,
|
||||||
|
} from './sceneInfra'
|
||||||
|
import { EngineCommand, engineCommandManager } from 'lang/std/engineConnection'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { deg2Rad } from 'lib/utils2d'
|
||||||
|
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
||||||
|
import * as TWEEN from '@tweenjs/tween.js'
|
||||||
|
import { isQuaternionVertical } from './helpers'
|
||||||
|
|
||||||
|
const ORTHOGRAPHIC_CAMERA_SIZE = 20
|
||||||
|
const FRAMES_TO_ANIMATE_IN = 30
|
||||||
|
|
||||||
|
const tempQuaternion = new Quaternion() // just used for maths
|
||||||
|
|
||||||
|
interface ThreeCamValues {
|
||||||
|
position: Vector3
|
||||||
|
quaternion: Quaternion
|
||||||
|
zoom: number
|
||||||
|
isPerspective: boolean
|
||||||
|
target: Vector3
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReactCameraProperties =
|
||||||
|
| {
|
||||||
|
type: 'perspective'
|
||||||
|
fov?: number
|
||||||
|
position: [number, number, number]
|
||||||
|
quaternion: [number, number, number, number]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'orthographic'
|
||||||
|
zoom?: number
|
||||||
|
position: [number, number, number]
|
||||||
|
quaternion: [number, number, number, number]
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastCmdDelay = 50
|
||||||
|
|
||||||
|
const throttledUpdateEngineCamera = throttle((threeValues: ThreeCamValues) => {
|
||||||
|
const cmd: EngineCommand = {
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
...convertThreeCamValuesToEngineCam(threeValues),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
engineCommandManager.sendSceneCommand(cmd)
|
||||||
|
}, 1000 / 15)
|
||||||
|
|
||||||
|
let lastPerspectiveCmd: EngineCommand | null = null
|
||||||
|
let lastPerspectiveCmdTime: number = Date.now()
|
||||||
|
let lastPerspectiveCmdTimeoutId: number | null = null
|
||||||
|
|
||||||
|
const sendLastPerspectiveReliableChannel = () => {
|
||||||
|
if (
|
||||||
|
lastPerspectiveCmd &&
|
||||||
|
Date.now() - lastPerspectiveCmdTime >= lastCmdDelay
|
||||||
|
) {
|
||||||
|
engineCommandManager.sendSceneCommand(lastPerspectiveCmd, true)
|
||||||
|
lastPerspectiveCmdTime = Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const throttledUpdateEngineFov = throttle(
|
||||||
|
(vals: {
|
||||||
|
position: Vector3
|
||||||
|
quaternion: Quaternion
|
||||||
|
zoom: number
|
||||||
|
fov: number
|
||||||
|
target: Vector3
|
||||||
|
}) => {
|
||||||
|
const cmd: EngineCommand = {
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_perspective_settings',
|
||||||
|
...convertThreeCamValuesToEngineCam({
|
||||||
|
...vals,
|
||||||
|
isPerspective: true,
|
||||||
|
}),
|
||||||
|
fov_y: vals.fov,
|
||||||
|
...calculateNearFarFromFOV(vals.fov),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
engineCommandManager.sendSceneCommand(cmd)
|
||||||
|
lastPerspectiveCmd = cmd
|
||||||
|
lastPerspectiveCmdTime = Date.now()
|
||||||
|
if (lastPerspectiveCmdTimeoutId !== null) {
|
||||||
|
clearTimeout(lastPerspectiveCmdTimeoutId)
|
||||||
|
}
|
||||||
|
lastPerspectiveCmdTimeoutId = setTimeout(
|
||||||
|
sendLastPerspectiveReliableChannel,
|
||||||
|
lastCmdDelay
|
||||||
|
) as any as number
|
||||||
|
},
|
||||||
|
1000 / 15
|
||||||
|
)
|
||||||
|
|
||||||
|
export class CameraControls {
|
||||||
|
camera: PerspectiveCamera | OrthographicCamera
|
||||||
|
target: Vector3
|
||||||
|
domElement: HTMLCanvasElement
|
||||||
|
isDragging: boolean
|
||||||
|
mouseDownPosition: Vector2
|
||||||
|
mouseNewPosition: Vector2
|
||||||
|
rotationSpeed = 0.3
|
||||||
|
enableRotate = true
|
||||||
|
enablePan = true
|
||||||
|
enableZoom = true
|
||||||
|
lastPerspectiveFov: number = 45
|
||||||
|
pendingZoom: number | null = null
|
||||||
|
pendingRotation: Vector2 | null = null
|
||||||
|
pendingPan: Vector2 | null = null
|
||||||
|
interactionGuards: MouseGuard = {
|
||||||
|
pan: {
|
||||||
|
description: 'Right click + Shift + drag or middle click + drag',
|
||||||
|
callback: (e) => !!(e.buttons & 4) && !e.ctrlKey,
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
description: 'Scroll wheel or Right click + Ctrl + drag',
|
||||||
|
dragCallback: (e) => e.button === 2 && e.ctrlKey,
|
||||||
|
scrollCallback: () => true,
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
description: 'Right click + drag',
|
||||||
|
callback: (e) => {
|
||||||
|
console.log('event', e)
|
||||||
|
return !!(e.buttons & 2)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
isFovAnimationInProgress = false
|
||||||
|
fovBeforeOrtho = 45
|
||||||
|
get isPerspective() {
|
||||||
|
return this.camera instanceof PerspectiveCamera
|
||||||
|
}
|
||||||
|
private debounceTimer = 0
|
||||||
|
|
||||||
|
handleStart = () => {
|
||||||
|
if (this.debounceTimer) clearTimeout(this.debounceTimer)
|
||||||
|
this._isCamMovingCallback(true, false)
|
||||||
|
}
|
||||||
|
handleEnd = () => {
|
||||||
|
this.debounceTimer = setTimeout(() => {
|
||||||
|
this._isCamMovingCallback(false, false)
|
||||||
|
}, 400) as any as number
|
||||||
|
}
|
||||||
|
|
||||||
|
// reacts hooks into some of this singleton's properties
|
||||||
|
reactCameraProperties: ReactCameraProperties = {
|
||||||
|
type: 'perspective',
|
||||||
|
fov: 12,
|
||||||
|
position: [0, 0, 0],
|
||||||
|
quaternion: [0, 0, 0, 1],
|
||||||
|
}
|
||||||
|
|
||||||
|
setCam = (camProps: ReactCameraProperties) => {
|
||||||
|
if (
|
||||||
|
camProps.type === 'perspective' &&
|
||||||
|
this.camera instanceof OrthographicCamera
|
||||||
|
) {
|
||||||
|
this.usePerspectiveCamera()
|
||||||
|
} else if (
|
||||||
|
camProps.type === 'orthographic' &&
|
||||||
|
this.camera instanceof PerspectiveCamera
|
||||||
|
) {
|
||||||
|
this.useOrthographicCamera()
|
||||||
|
}
|
||||||
|
this.camera.position.set(...camProps.position)
|
||||||
|
this.camera.quaternion.set(...camProps.quaternion)
|
||||||
|
if (
|
||||||
|
camProps.type === 'perspective' &&
|
||||||
|
this.camera instanceof PerspectiveCamera
|
||||||
|
) {
|
||||||
|
// not sure what to do here, calling dollyZoom here is buggy because it updates the position
|
||||||
|
// at the same time
|
||||||
|
} else if (
|
||||||
|
camProps.type === 'orthographic' &&
|
||||||
|
this.camera instanceof OrthographicCamera
|
||||||
|
) {
|
||||||
|
this.camera.zoom = camProps.zoom || 1
|
||||||
|
}
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
this.update(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(isOrtho = false, domElement: HTMLCanvasElement) {
|
||||||
|
this.camera = isOrtho ? new OrthographicCamera() : new PerspectiveCamera()
|
||||||
|
this.camera.up.set(0, 0, 1)
|
||||||
|
this.camera.far = 20000
|
||||||
|
this.target = new Vector3()
|
||||||
|
this.domElement = domElement
|
||||||
|
this.isDragging = false
|
||||||
|
this.mouseDownPosition = new Vector2()
|
||||||
|
this.mouseNewPosition = new Vector2()
|
||||||
|
|
||||||
|
this.domElement.addEventListener('pointerdown', this.onMouseDown)
|
||||||
|
this.domElement.addEventListener('pointermove', this.onMouseMove)
|
||||||
|
this.domElement.addEventListener('pointerup', this.onMouseUp)
|
||||||
|
this.domElement.addEventListener('wheel', this.onMouseWheel)
|
||||||
|
|
||||||
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
|
this.onWindowResize()
|
||||||
|
|
||||||
|
this.update()
|
||||||
|
this._usePerspectiveCamera()
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void =
|
||||||
|
() => {}
|
||||||
|
setIsCamMovingCallback(cb: (isMoving: boolean, isTween: boolean) => void) {
|
||||||
|
this._isCamMovingCallback = cb
|
||||||
|
}
|
||||||
|
private _camChangeCallbacks: { [key: string]: () => void } = {}
|
||||||
|
subscribeToCamChange(cb: () => void) {
|
||||||
|
const cbId = uuidv4()
|
||||||
|
this._camChangeCallbacks[cbId] = cb
|
||||||
|
const unsubscribe = () => {
|
||||||
|
delete this._camChangeCallbacks[cbId]
|
||||||
|
}
|
||||||
|
return unsubscribe
|
||||||
|
}
|
||||||
|
|
||||||
|
onWindowResize = () => {
|
||||||
|
if (this.camera instanceof PerspectiveCamera) {
|
||||||
|
this.camera.aspect = window.innerWidth / window.innerHeight
|
||||||
|
} else if (this.camera instanceof OrthographicCamera) {
|
||||||
|
const aspect = window.innerWidth / window.innerHeight
|
||||||
|
this.camera.left = -ORTHOGRAPHIC_CAMERA_SIZE * aspect
|
||||||
|
this.camera.right = ORTHOGRAPHIC_CAMERA_SIZE * aspect
|
||||||
|
this.camera.top = ORTHOGRAPHIC_CAMERA_SIZE
|
||||||
|
this.camera.bottom = -ORTHOGRAPHIC_CAMERA_SIZE
|
||||||
|
}
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseDown = (event: MouseEvent) => {
|
||||||
|
this.isDragging = true
|
||||||
|
this.mouseDownPosition.set(event.clientX, event.clientY)
|
||||||
|
this.handleStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove = (event: MouseEvent) => {
|
||||||
|
if (this.isDragging) {
|
||||||
|
this.mouseNewPosition.set(event.clientX, event.clientY)
|
||||||
|
const deltaMove = this.mouseNewPosition
|
||||||
|
.clone()
|
||||||
|
.sub(this.mouseDownPosition)
|
||||||
|
this.mouseDownPosition.copy(this.mouseNewPosition)
|
||||||
|
|
||||||
|
let state: 'pan' | 'rotate' | 'zoom' = 'pan'
|
||||||
|
|
||||||
|
if (this.interactionGuards.pan.callback(event as any)) {
|
||||||
|
if (this.enablePan === false) return
|
||||||
|
// handleMouseDownPan(event)
|
||||||
|
state = 'pan'
|
||||||
|
} else if (this.interactionGuards.rotate.callback(event as any)) {
|
||||||
|
if (this.enableRotate === false) return
|
||||||
|
// handleMouseDownRotate(event)
|
||||||
|
state = 'rotate'
|
||||||
|
} else if (this.interactionGuards.zoom.dragCallback(event as any)) {
|
||||||
|
if (this.enableZoom === false) return
|
||||||
|
// handleMouseDownDolly(event)
|
||||||
|
state = 'zoom'
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement camera movement logic here based on deltaMove
|
||||||
|
// For example, for rotating the camera around the target:
|
||||||
|
if (state === 'rotate') {
|
||||||
|
this.pendingRotation = this.pendingRotation
|
||||||
|
? this.pendingRotation
|
||||||
|
: new Vector2()
|
||||||
|
this.pendingRotation.x += deltaMove.x
|
||||||
|
this.pendingRotation.y += deltaMove.y
|
||||||
|
} else if (state === 'zoom') {
|
||||||
|
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
|
||||||
|
this.pendingZoom *= 1 + deltaMove.y * 0.01
|
||||||
|
} else if (state === 'pan') {
|
||||||
|
this.pendingPan = this.pendingPan ? this.pendingPan : new Vector2()
|
||||||
|
let distance = this.camera.position.distanceTo(this.target)
|
||||||
|
if (this.camera instanceof OrthographicCamera) {
|
||||||
|
const zoomFudgeFactor = 2280
|
||||||
|
distance = zoomFudgeFactor / (this.camera.zoom * 45)
|
||||||
|
}
|
||||||
|
const panSpeed = (distance / 1000 / 45) * this.fovBeforeOrtho
|
||||||
|
this.pendingPan.x += -deltaMove.x * panSpeed
|
||||||
|
this.pendingPan.y += deltaMove.y * panSpeed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUp = (event: MouseEvent) => {
|
||||||
|
this.isDragging = false
|
||||||
|
this.handleEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseWheel = (event: WheelEvent) => {
|
||||||
|
// Assume trackpad if the deltas are small and integers
|
||||||
|
this.handleStart()
|
||||||
|
const isTrackpad = Math.abs(event.deltaY) <= 1 || event.deltaY % 1 === 0
|
||||||
|
|
||||||
|
const zoomSpeed = isTrackpad ? 0.02 : 0.1 // Reduced zoom speed for trackpad
|
||||||
|
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
|
||||||
|
this.pendingZoom *= 1 + (event.deltaY > 0 ? zoomSpeed : -zoomSpeed)
|
||||||
|
this.handleEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
useOrthographicCamera = () => {
|
||||||
|
if (this.camera instanceof OrthographicCamera) return
|
||||||
|
const { x: px, y: py, z: pz } = this.camera.position
|
||||||
|
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
|
||||||
|
const aspect = window.innerWidth / window.innerHeight
|
||||||
|
this.lastPerspectiveFov = this.camera.fov
|
||||||
|
const { z_near, z_far } = calculateNearFarFromFOV(this.lastPerspectiveFov)
|
||||||
|
this.camera = new OrthographicCamera(
|
||||||
|
-ORTHOGRAPHIC_CAMERA_SIZE * aspect,
|
||||||
|
ORTHOGRAPHIC_CAMERA_SIZE * aspect,
|
||||||
|
ORTHOGRAPHIC_CAMERA_SIZE,
|
||||||
|
-ORTHOGRAPHIC_CAMERA_SIZE,
|
||||||
|
z_near,
|
||||||
|
z_far
|
||||||
|
)
|
||||||
|
this.camera.up.set(0, 0, 1)
|
||||||
|
this.camera.layers.enable(SKETCH_LAYER)
|
||||||
|
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||||
|
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
this.camera.position.set(px, py, pz)
|
||||||
|
const distance = this.camera.position.distanceTo(this.target.clone())
|
||||||
|
const fovFactor = 45 / this.lastPerspectiveFov
|
||||||
|
this.camera.zoom = (ZOOM_MAGIC_NUMBER * fovFactor * 0.8) / distance
|
||||||
|
|
||||||
|
this.camera.quaternion.set(qx, qy, qz, qw)
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_set_orthographic',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.onCameraChange()
|
||||||
|
}
|
||||||
|
private createPerspectiveCamera = () => {
|
||||||
|
const { z_near, z_far } = calculateNearFarFromFOV(this.lastPerspectiveFov)
|
||||||
|
this.camera = new PerspectiveCamera(
|
||||||
|
this.lastPerspectiveFov,
|
||||||
|
window.innerWidth / window.innerHeight,
|
||||||
|
z_near,
|
||||||
|
z_far
|
||||||
|
)
|
||||||
|
this.camera.up.set(0, 0, 1)
|
||||||
|
this.camera.layers.enable(SKETCH_LAYER)
|
||||||
|
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||||
|
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
|
return this.camera
|
||||||
|
}
|
||||||
|
_usePerspectiveCamera = () => {
|
||||||
|
const { x: px, y: py, z: pz } = this.camera.position
|
||||||
|
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
|
||||||
|
const zoom = this.camera.zoom
|
||||||
|
this.camera = this.createPerspectiveCamera()
|
||||||
|
|
||||||
|
this.camera.position.set(px, py, pz)
|
||||||
|
this.camera.quaternion.set(qx, qy, qz, qw)
|
||||||
|
const zoomFudgeFactor = 2280
|
||||||
|
const distance = zoomFudgeFactor / (zoom * this.lastPerspectiveFov)
|
||||||
|
const direction = new Vector3().subVectors(
|
||||||
|
this.camera.position,
|
||||||
|
this.target
|
||||||
|
)
|
||||||
|
direction.normalize()
|
||||||
|
this.camera.position.copy(this.target).addScaledVector(direction, distance)
|
||||||
|
}
|
||||||
|
usePerspectiveCamera = () => {
|
||||||
|
this._usePerspectiveCamera()
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_set_perspective',
|
||||||
|
parameters: {
|
||||||
|
fov_y:
|
||||||
|
this.camera instanceof PerspectiveCamera ? this.camera.fov : 45,
|
||||||
|
...calculateNearFarFromFOV(this.lastPerspectiveFov),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.onCameraChange()
|
||||||
|
this.update()
|
||||||
|
return this.camera
|
||||||
|
}
|
||||||
|
|
||||||
|
dollyZoom = (newFov: number) => {
|
||||||
|
if (!(this.camera instanceof PerspectiveCamera)) {
|
||||||
|
console.warn('Dolly zoom is only applicable to perspective cameras.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.lastPerspectiveFov = newFov
|
||||||
|
|
||||||
|
// Calculate the direction vector from the camera towards the controls target
|
||||||
|
const direction = new Vector3()
|
||||||
|
.subVectors(this.target, this.camera.position)
|
||||||
|
.normalize()
|
||||||
|
|
||||||
|
// Calculate the distance to the controls target before changing the FOV
|
||||||
|
const distanceBefore = this.camera.position.distanceTo(this.target)
|
||||||
|
|
||||||
|
// Calculate the scale factor for the new FOV compared to the old one
|
||||||
|
// This needs to be calculated before updating the camera's FOV
|
||||||
|
const oldFov = this.camera.fov
|
||||||
|
|
||||||
|
const viewHeightFactor = (fov: number) => {
|
||||||
|
/* *
|
||||||
|
/|
|
||||||
|
/ |
|
||||||
|
/ |
|
||||||
|
/ |
|
||||||
|
/ | viewHeight/2
|
||||||
|
/ |
|
||||||
|
/ |
|
||||||
|
/↙️fov/2 |
|
||||||
|
/________|
|
||||||
|
\ |
|
||||||
|
\._._._.|
|
||||||
|
*/
|
||||||
|
return Math.tan(deg2Rad(fov / 2))
|
||||||
|
}
|
||||||
|
const scaleFactor = viewHeightFactor(oldFov) / viewHeightFactor(newFov)
|
||||||
|
|
||||||
|
this.camera.fov = newFov
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
|
||||||
|
const distanceAfter = distanceBefore * scaleFactor
|
||||||
|
|
||||||
|
const newPosition = this.target
|
||||||
|
.clone()
|
||||||
|
.add(direction.multiplyScalar(-distanceAfter))
|
||||||
|
this.camera.position.copy(newPosition)
|
||||||
|
|
||||||
|
const { z_near, z_far } = calculateNearFarFromFOV(this.lastPerspectiveFov)
|
||||||
|
this.camera.near = z_near
|
||||||
|
this.camera.far = z_far
|
||||||
|
|
||||||
|
throttledUpdateEngineFov({
|
||||||
|
fov: newFov,
|
||||||
|
position: newPosition,
|
||||||
|
quaternion: this.camera.quaternion,
|
||||||
|
zoom: this.camera.zoom,
|
||||||
|
target: this.target,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
update = (forceUpdate = false) => {
|
||||||
|
// If there are any changes that need to be applied to the camera, apply them here.
|
||||||
|
|
||||||
|
let didChange = forceUpdate
|
||||||
|
if (this.pendingRotation) {
|
||||||
|
this.rotateCamera(this.pendingRotation.x, this.pendingRotation.y)
|
||||||
|
this.pendingRotation = null // Clear the pending rotation after applying it
|
||||||
|
didChange = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pendingZoom) {
|
||||||
|
if (this.camera instanceof PerspectiveCamera) {
|
||||||
|
// move camera towards or away from the target
|
||||||
|
const distance = this.camera.position.distanceTo(this.target)
|
||||||
|
const newDistance = distance * this.pendingZoom
|
||||||
|
const direction = this.camera.position
|
||||||
|
.clone()
|
||||||
|
.sub(this.target)
|
||||||
|
.normalize()
|
||||||
|
const newPosition = this.target
|
||||||
|
.clone()
|
||||||
|
.add(direction.multiplyScalar(newDistance))
|
||||||
|
this.camera.position.copy(newPosition)
|
||||||
|
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
this.pendingZoom = null // Clear the pending zoom after applying it
|
||||||
|
} else {
|
||||||
|
// TODO change ortho zoom
|
||||||
|
this.camera.zoom = this.camera.zoom / this.pendingZoom
|
||||||
|
this.pendingZoom = null
|
||||||
|
}
|
||||||
|
didChange = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pendingPan) {
|
||||||
|
// move camera left/right and up/down
|
||||||
|
const offset = this.camera.position.clone().sub(this.target)
|
||||||
|
const direction = offset.clone().normalize()
|
||||||
|
const cameraQuaternion = this.camera.quaternion
|
||||||
|
const up = new Vector3(0, 1, 0).applyQuaternion(cameraQuaternion)
|
||||||
|
const right = new Vector3().crossVectors(up, direction)
|
||||||
|
right.multiplyScalar(this.pendingPan.x)
|
||||||
|
up.multiplyScalar(this.pendingPan.y)
|
||||||
|
const newPosition = this.camera.position.clone().add(right).add(up)
|
||||||
|
this.target.add(right)
|
||||||
|
this.target.add(up)
|
||||||
|
this.camera.position.copy(newPosition)
|
||||||
|
this.pendingPan = null
|
||||||
|
didChange = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.safeLookAtTarget()
|
||||||
|
|
||||||
|
// Update the camera's matrices
|
||||||
|
this.camera.updateMatrixWorld()
|
||||||
|
if (didChange) {
|
||||||
|
this.onCameraChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
// damping would be implemented here in update if we choose to add it.
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateCamera = (deltaX: number, deltaY: number) => {
|
||||||
|
const quat = new Quaternion().setFromUnitVectors(
|
||||||
|
new Vector3(0, 0, 1),
|
||||||
|
new Vector3(0, 1, 0)
|
||||||
|
)
|
||||||
|
const quatInverse = quat.clone().invert()
|
||||||
|
|
||||||
|
const angleX = deltaX * this.rotationSpeed // rotationSpeed is a constant that defines how fast the camera rotates
|
||||||
|
const angleY = deltaY * this.rotationSpeed
|
||||||
|
|
||||||
|
// Convert angles to radians
|
||||||
|
const radianX = MathUtils.degToRad(angleX)
|
||||||
|
const radianY = MathUtils.degToRad(angleY)
|
||||||
|
|
||||||
|
// Get the offset from the camera to the target
|
||||||
|
const offset = new Vector3().subVectors(this.camera.position, this.target)
|
||||||
|
|
||||||
|
// spherical is a y-up paradigm, need to conform to that for now
|
||||||
|
offset.applyQuaternion(quat)
|
||||||
|
|
||||||
|
// Convert offset to spherical coordinates
|
||||||
|
const spherical = new Spherical().setFromVector3(offset)
|
||||||
|
|
||||||
|
// Apply the rotations
|
||||||
|
spherical.theta -= radianX
|
||||||
|
spherical.phi -= radianY
|
||||||
|
|
||||||
|
// Restrict the phi angle to avoid the camera flipping at the poles
|
||||||
|
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi))
|
||||||
|
|
||||||
|
// Convert back to Cartesian coordinates
|
||||||
|
offset.setFromSpherical(spherical)
|
||||||
|
|
||||||
|
// put the offset back into the z-up paradigm
|
||||||
|
offset.applyQuaternion(quatInverse)
|
||||||
|
|
||||||
|
// Update the camera's position
|
||||||
|
this.camera.position.copy(this.target).add(offset)
|
||||||
|
|
||||||
|
// Look at the target
|
||||||
|
this.camera.updateMatrixWorld()
|
||||||
|
}
|
||||||
|
|
||||||
|
safeLookAtTarget(up = new Vector3(0, 0, 1)) {
|
||||||
|
const quaternion = _lookAt(this.camera.position, this.target, up)
|
||||||
|
this.camera.quaternion.copy(quaternion)
|
||||||
|
this.camera.updateMatrixWorld()
|
||||||
|
}
|
||||||
|
|
||||||
|
tweenCamToNegYAxis(
|
||||||
|
// -90 degrees from the x axis puts the camera on the negative y axis
|
||||||
|
targetAngle = -Math.PI / 2,
|
||||||
|
duration = 500
|
||||||
|
): Promise<void> {
|
||||||
|
// should tween the camera so that it has an xPosition of 0, and forcing it's yPosition to be negative
|
||||||
|
// zPosition should stay the same
|
||||||
|
const xyRadius = Math.sqrt(
|
||||||
|
(this.target.x - this.camera.position.x) ** 2 +
|
||||||
|
(this.target.y - this.camera.position.y) ** 2
|
||||||
|
)
|
||||||
|
const xyAngle = Math.atan2(
|
||||||
|
this.camera.position.y - this.target.y,
|
||||||
|
this.camera.position.x - this.target.x
|
||||||
|
)
|
||||||
|
this._isCamMovingCallback(true, true)
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
new TWEEN.Tween({ angle: xyAngle })
|
||||||
|
.to({ angle: targetAngle }, duration)
|
||||||
|
.onUpdate((obj) => {
|
||||||
|
const x = xyRadius * Math.cos(obj.angle)
|
||||||
|
const y = xyRadius * Math.sin(obj.angle)
|
||||||
|
this.camera.position.set(
|
||||||
|
this.target.x + x,
|
||||||
|
this.target.y + y,
|
||||||
|
this.camera.position.z
|
||||||
|
)
|
||||||
|
this.update()
|
||||||
|
this.onCameraChange()
|
||||||
|
})
|
||||||
|
.onComplete((obj) => {
|
||||||
|
const x = xyRadius * Math.cos(obj.angle)
|
||||||
|
const y = xyRadius * Math.sin(obj.angle)
|
||||||
|
this.camera.position.set(
|
||||||
|
this.target.x + x,
|
||||||
|
this.target.y + y,
|
||||||
|
this.camera.position.z
|
||||||
|
)
|
||||||
|
this.update()
|
||||||
|
this.onCameraChange()
|
||||||
|
this._isCamMovingCallback(false, true)
|
||||||
|
|
||||||
|
// resolve after a couple of frames
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => resolve())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.start()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async tweenCameraToQuaternion(
|
||||||
|
targetQuaternion: Quaternion,
|
||||||
|
duration = 500,
|
||||||
|
toOrthographic = true
|
||||||
|
): Promise<void> {
|
||||||
|
const isVertical = isQuaternionVertical(targetQuaternion)
|
||||||
|
let remainingDuration = duration
|
||||||
|
if (isVertical) {
|
||||||
|
remainingDuration = duration * 0.5
|
||||||
|
const orbitRotationDuration = duration * 0.65
|
||||||
|
let targetAngle = -Math.PI / 2
|
||||||
|
const v = new Vector3(0, 0, 1).applyQuaternion(targetQuaternion)
|
||||||
|
if (v.z < 0) targetAngle = Math.PI / 2
|
||||||
|
await this.tweenCamToNegYAxis(targetAngle, orbitRotationDuration)
|
||||||
|
}
|
||||||
|
await this._tweenCameraToQuaternion(
|
||||||
|
targetQuaternion,
|
||||||
|
remainingDuration,
|
||||||
|
toOrthographic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_tweenCameraToQuaternion(
|
||||||
|
targetQuaternion: Quaternion,
|
||||||
|
duration = 500,
|
||||||
|
toOrthographic = false
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const camera = this.camera
|
||||||
|
this._isCamMovingCallback(true, true)
|
||||||
|
const initialQuaternion = camera.quaternion.clone()
|
||||||
|
const isVertical = isQuaternionVertical(targetQuaternion)
|
||||||
|
let tweenEnd = isVertical ? 0.99 : 1
|
||||||
|
const controlsTarget = this.target.clone()
|
||||||
|
const initialDistance = controlsTarget.distanceTo(camera.position.clone())
|
||||||
|
|
||||||
|
const cameraAtTime = (animationProgress: number /* 0 - 1 */) => {
|
||||||
|
const currentQ = tempQuaternion.slerpQuaternions(
|
||||||
|
initialQuaternion,
|
||||||
|
targetQuaternion,
|
||||||
|
animationProgress
|
||||||
|
)
|
||||||
|
if (this.camera instanceof PerspectiveCamera)
|
||||||
|
// changing the camera position back when it's orthographic doesn't do anything
|
||||||
|
// and it messes up animating back to perspective later
|
||||||
|
this.camera.position
|
||||||
|
.set(0, 0, 1)
|
||||||
|
.applyQuaternion(currentQ)
|
||||||
|
.multiplyScalar(initialDistance)
|
||||||
|
.add(controlsTarget)
|
||||||
|
|
||||||
|
this.camera.up.set(0, 1, 0).applyQuaternion(currentQ).normalize()
|
||||||
|
this.camera.quaternion.copy(currentQ)
|
||||||
|
this.target.copy(controlsTarget)
|
||||||
|
// this.controls.update()
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
this.update()
|
||||||
|
this.onCameraChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onComplete = async () => {
|
||||||
|
if (isReducedMotion() && toOrthographic) {
|
||||||
|
cameraAtTime(0.99)
|
||||||
|
this.useOrthographicCamera()
|
||||||
|
} else if (toOrthographic) {
|
||||||
|
await this.animateToOrthographic()
|
||||||
|
}
|
||||||
|
this.enableRotate = false
|
||||||
|
this._isCamMovingCallback(false, true)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isReducedMotion()) {
|
||||||
|
onComplete()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
new TWEEN.Tween({ t: 0 })
|
||||||
|
.to({ t: tweenEnd }, duration)
|
||||||
|
.easing(TWEEN.Easing.Quadratic.InOut)
|
||||||
|
.onUpdate(({ t }) => cameraAtTime(t))
|
||||||
|
.onComplete(onComplete)
|
||||||
|
.start()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
animateToOrthographic = () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
this.isFovAnimationInProgress = true
|
||||||
|
let currentFov = this.lastPerspectiveFov
|
||||||
|
this.fovBeforeOrtho = currentFov
|
||||||
|
|
||||||
|
const targetFov = 4
|
||||||
|
const fovAnimationStep = (currentFov - targetFov) / FRAMES_TO_ANIMATE_IN
|
||||||
|
let frameWaitOnFinish = 10
|
||||||
|
|
||||||
|
const animateFovChange = () => {
|
||||||
|
if (this.camera instanceof PerspectiveCamera) {
|
||||||
|
if (this.camera.fov > targetFov) {
|
||||||
|
// Decrease the FOV
|
||||||
|
currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
this.dollyZoom(currentFov)
|
||||||
|
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||||
|
} else if (frameWaitOnFinish > 0) {
|
||||||
|
frameWaitOnFinish--
|
||||||
|
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||||
|
} else {
|
||||||
|
// Once the target FOV is reached, switch to the orthographic camera
|
||||||
|
// Needs to wait a couple frames after the FOV animation is complete
|
||||||
|
this.useOrthographicCamera()
|
||||||
|
this.isFovAnimationInProgress = false
|
||||||
|
resolve(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animateFovChange() // Start the animation
|
||||||
|
})
|
||||||
|
animateToPerspective = () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
this.isFovAnimationInProgress = true
|
||||||
|
// Immediately set the camera to perspective with a very low FOV
|
||||||
|
const targetFov = this.fovBeforeOrtho // Target FOV for perspective
|
||||||
|
this.lastPerspectiveFov = 4
|
||||||
|
let currentFov = 4
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
const fovAnimationStep = (targetFov - currentFov) / FRAMES_TO_ANIMATE_IN
|
||||||
|
this.usePerspectiveCamera()
|
||||||
|
|
||||||
|
const animateFovChange = () => {
|
||||||
|
if (this.camera instanceof OrthographicCamera) return
|
||||||
|
if (this.camera.fov < targetFov) {
|
||||||
|
// Increase the FOV
|
||||||
|
currentFov = Math.min(currentFov + fovAnimationStep, targetFov)
|
||||||
|
// this.camera.fov = currentFov
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
this.dollyZoom(currentFov)
|
||||||
|
requestAnimationFrame(animateFovChange) // Continue the animation
|
||||||
|
} else {
|
||||||
|
// Set the flag to false as the FOV animation is complete
|
||||||
|
this.isFovAnimationInProgress = false
|
||||||
|
resolve(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animateFovChange() // Start the animation
|
||||||
|
})
|
||||||
|
|
||||||
|
reactCameraPropertiesCallback: (a: ReactCameraProperties) => void = () => {}
|
||||||
|
setReactCameraPropertiesCallback = (
|
||||||
|
cb: (a: ReactCameraProperties) => void
|
||||||
|
) => {
|
||||||
|
this.reactCameraPropertiesCallback = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
deferReactUpdate = throttle((a: ReactCameraProperties) => {
|
||||||
|
this.reactCameraPropertiesCallback(a)
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
onCameraChange = () => {
|
||||||
|
const distance = this.target.distanceTo(this.camera.position)
|
||||||
|
if (this.camera.far / 2.1 < distance || this.camera.far / 1.9 > distance) {
|
||||||
|
this.camera.far = distance * 2
|
||||||
|
this.camera.near = distance / 10
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
}
|
||||||
|
|
||||||
|
throttledUpdateEngineCamera({
|
||||||
|
quaternion: this.camera.quaternion,
|
||||||
|
position: this.camera.position,
|
||||||
|
zoom: this.camera.zoom,
|
||||||
|
isPerspective: this.isPerspective,
|
||||||
|
target: this.target,
|
||||||
|
})
|
||||||
|
this.deferReactUpdate({
|
||||||
|
type: this.isPerspective ? 'perspective' : 'orthographic',
|
||||||
|
[this.isPerspective ? 'fov' : 'zoom']:
|
||||||
|
this.camera instanceof PerspectiveCamera
|
||||||
|
? this.camera.fov
|
||||||
|
: this.camera.zoom,
|
||||||
|
position: [
|
||||||
|
roundOff(this.camera.position.x, 2),
|
||||||
|
roundOff(this.camera.position.y, 2),
|
||||||
|
roundOff(this.camera.position.z, 2),
|
||||||
|
],
|
||||||
|
quaternion: [
|
||||||
|
roundOff(this.camera.quaternion.x, 2),
|
||||||
|
roundOff(this.camera.quaternion.y, 2),
|
||||||
|
roundOff(this.camera.quaternion.z, 2),
|
||||||
|
roundOff(this.camera.quaternion.w, 2),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
Object.values(this._camChangeCallbacks).forEach((cb) => cb())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// currently duplicated, delete one
|
||||||
|
function calculateNearFarFromFOV(fov: number) {
|
||||||
|
const nearFarRatio = (fov - 3) / (45 - 3)
|
||||||
|
// const z_near = 0.1 + nearFarRatio * (5 - 0.1)
|
||||||
|
const z_far = 1000 + nearFarRatio * (100000 - 1000)
|
||||||
|
return { z_near: 0.1, z_far }
|
||||||
|
}
|
||||||
|
|
||||||
|
// currently duplicated, delete one
|
||||||
|
function convertThreeCamValuesToEngineCam({
|
||||||
|
target,
|
||||||
|
position,
|
||||||
|
quaternion,
|
||||||
|
zoom,
|
||||||
|
isPerspective,
|
||||||
|
}: ThreeCamValues): {
|
||||||
|
center: Vector3
|
||||||
|
up: Vector3
|
||||||
|
vantage: Vector3
|
||||||
|
} {
|
||||||
|
// Something to consider is that the orbit controls have a target,
|
||||||
|
// we're kind of deriving the target/lookAtVector here when it might not be needed
|
||||||
|
// leaving for now since it's working but maybe revisit later
|
||||||
|
const euler = new Euler().setFromQuaternion(quaternion, 'XYZ')
|
||||||
|
|
||||||
|
const lookAtVector = new Vector3(0, 0, -1)
|
||||||
|
.applyEuler(euler)
|
||||||
|
.normalize()
|
||||||
|
.add(position)
|
||||||
|
|
||||||
|
const upVector = new Vector3(0, 1, 0).applyEuler(euler).normalize()
|
||||||
|
if (isPerspective) {
|
||||||
|
return {
|
||||||
|
center: target,
|
||||||
|
up: upVector,
|
||||||
|
vantage: position,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fudgeFactor2 = zoom * 0.9979224466814468 - 0.03473692325839295
|
||||||
|
const zoomFactor = (-ZOOM_MAGIC_NUMBER + fudgeFactor2) / zoom
|
||||||
|
const direction = lookAtVector.clone().sub(position).normalize()
|
||||||
|
const newVantage = position.clone().add(direction.multiplyScalar(zoomFactor))
|
||||||
|
return {
|
||||||
|
center: lookAtVector,
|
||||||
|
up: upVector,
|
||||||
|
vantage: newVantage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pure function helpers
|
||||||
|
|
||||||
|
function _lookAt(position: Vector3, target: Vector3, up: Vector3): Quaternion {
|
||||||
|
// Direction from position to target, normalized.
|
||||||
|
let direction = new Vector3().subVectors(target, position).normalize()
|
||||||
|
|
||||||
|
// Calculate a new "effective" up vector that is orthogonal to the direction.
|
||||||
|
// This step ensures that the up vector does not affect the direction the camera is looking.
|
||||||
|
let right = new Vector3().crossVectors(direction, up).normalize()
|
||||||
|
let orthogonalUp = new Vector3().crossVectors(right, direction).normalize()
|
||||||
|
|
||||||
|
// Create a lookAt matrix using the position, and the recalculated orthogonal up vector.
|
||||||
|
let lookAtMatrix = new Matrix4()
|
||||||
|
lookAtMatrix.lookAt(position, target, orthogonalUp)
|
||||||
|
|
||||||
|
// Create a quaternion from the lookAt matrix.
|
||||||
|
let quaternion = new Quaternion().setFromRotationMatrix(lookAtMatrix)
|
||||||
|
|
||||||
|
return quaternion
|
||||||
|
}
|
250
src/clientSideScene/ClientSideSceneComp.tsx
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
import { useRef, useEffect, useState } from 'react'
|
||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
|
||||||
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
import { useStore } from 'useStore'
|
||||||
|
import { DEBUG_SHOW_BOTH_SCENES, sceneInfra } from './sceneInfra'
|
||||||
|
import { ReactCameraProperties } from './CameraControls'
|
||||||
|
import { throttle } from 'lib/utils'
|
||||||
|
|
||||||
|
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||||
|
const [isCamMoving, setIsCamMoving] = useState(false)
|
||||||
|
const [isTween, setIsTween] = useState(false)
|
||||||
|
|
||||||
|
const { state } = useModelingContext()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sceneInfra.camControls.setIsCamMovingCallback((isMoving, isTween) => {
|
||||||
|
setIsCamMoving(isMoving)
|
||||||
|
setIsTween(isTween)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (DEBUG_SHOW_BOTH_SCENES || !isCamMoving)
|
||||||
|
return { hideClient: false, hideServer: false }
|
||||||
|
let hideServer = state.matches('Sketch')
|
||||||
|
if (isTween) {
|
||||||
|
hideServer = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return { hideClient: !hideServer, hideServer }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ClientSideScene = ({
|
||||||
|
cameraControls,
|
||||||
|
}: {
|
||||||
|
cameraControls: ReturnType<
|
||||||
|
typeof useGlobalStateContext
|
||||||
|
>['settings']['context']['cameraControls']
|
||||||
|
}) => {
|
||||||
|
const canvasRef = useRef<HTMLDivElement>(null)
|
||||||
|
const { state, send } = useModelingContext()
|
||||||
|
const { hideClient, hideServer } = useShouldHideScene()
|
||||||
|
const { setHighlightRange } = useStore((s) => ({
|
||||||
|
setHighlightRange: s.setHighlightRange,
|
||||||
|
highlightRange: s.highlightRange,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Listen for changes to the camera controls setting
|
||||||
|
// and update the client-side scene's controls accordingly.
|
||||||
|
useEffect(() => {
|
||||||
|
sceneInfra.camControls.interactionGuards =
|
||||||
|
cameraMouseDragGuards[cameraControls]
|
||||||
|
}, [cameraControls])
|
||||||
|
useEffect(() => {
|
||||||
|
sceneInfra.updateOtherSelectionColors(
|
||||||
|
state?.context?.selectionRanges?.otherSelections || []
|
||||||
|
)
|
||||||
|
}, [state?.context?.selectionRanges?.otherSelections])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canvasRef.current) return
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
canvas.appendChild(sceneInfra.renderer.domElement)
|
||||||
|
sceneInfra.animate()
|
||||||
|
sceneInfra.setHighlightCallback(setHighlightRange)
|
||||||
|
canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false)
|
||||||
|
canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false)
|
||||||
|
canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false)
|
||||||
|
sceneInfra.setSend(send)
|
||||||
|
return () => {
|
||||||
|
canvas?.removeEventListener('mousemove', sceneInfra.onMouseMove)
|
||||||
|
canvas?.removeEventListener('mousedown', sceneInfra.onMouseDown)
|
||||||
|
canvas?.removeEventListener('mouseup', sceneInfra.onMouseUp)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={canvasRef}
|
||||||
|
className={`absolute inset-0 h-full w-full transition-all duration-300 ${
|
||||||
|
hideClient ? 'opacity-0' : 'opacity-100'
|
||||||
|
} ${hideServer ? 'bg-black' : ''} ${
|
||||||
|
!hideClient && !hideServer && state.matches('Sketch')
|
||||||
|
? 'bg-black/80'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const throttled = throttle((a: ReactCameraProperties) => {
|
||||||
|
if (a.type === 'perspective' && a.fov) {
|
||||||
|
sceneInfra.camControls.dollyZoom(a.fov)
|
||||||
|
}
|
||||||
|
}, 1000 / 15)
|
||||||
|
|
||||||
|
export const CamDebugSettings = () => {
|
||||||
|
const [camSettings, setCamSettings] = useState<ReactCameraProperties>({
|
||||||
|
type: 'perspective',
|
||||||
|
fov: 12,
|
||||||
|
position: [0, 0, 0],
|
||||||
|
quaternion: [0, 0, 0, 1],
|
||||||
|
})
|
||||||
|
const [fov, setFov] = useState(12)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
|
||||||
|
}, [sceneInfra])
|
||||||
|
useEffect(() => {
|
||||||
|
if (camSettings.type === 'perspective' && camSettings.fov) {
|
||||||
|
setFov(camSettings.fov)
|
||||||
|
}
|
||||||
|
}, [(camSettings as any)?.fov])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>cam settings</h3>
|
||||||
|
perspective cam
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={camSettings.type === 'perspective'}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (camSettings.type === 'perspective') {
|
||||||
|
sceneInfra.camControls.useOrthographicCamera()
|
||||||
|
} else {
|
||||||
|
sceneInfra.camControls.usePerspectiveCamera()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{camSettings.type === 'perspective' && (
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="4"
|
||||||
|
max="90"
|
||||||
|
step={0.5}
|
||||||
|
value={fov}
|
||||||
|
onChange={(e) => {
|
||||||
|
setFov(parseFloat(e.target.value))
|
||||||
|
|
||||||
|
throttled({
|
||||||
|
...camSettings,
|
||||||
|
fov: parseFloat(e.target.value),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className="w-full cursor-pointer pointer-events-auto"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{camSettings.type === 'perspective' && (
|
||||||
|
<div>
|
||||||
|
<span>fov</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={camSettings.fov}
|
||||||
|
className="text-black w-16"
|
||||||
|
onChange={(e) => {
|
||||||
|
sceneInfra.camControls.setCam({
|
||||||
|
...camSettings,
|
||||||
|
fov: parseFloat(e.target.value),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{camSettings.type === 'orthographic' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<span>fov</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={camSettings.zoom}
|
||||||
|
className="text-black w-16"
|
||||||
|
onChange={(e) => {
|
||||||
|
sceneInfra.camControls.setCam({
|
||||||
|
...camSettings,
|
||||||
|
zoom: parseFloat(e.target.value),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
Position
|
||||||
|
<ul className="flex">
|
||||||
|
<li>
|
||||||
|
<span className="pl-2 pr-1">x:</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step={5}
|
||||||
|
data-testid="cam-x-position"
|
||||||
|
value={camSettings.position[0]}
|
||||||
|
className="text-black w-16"
|
||||||
|
onChange={(e) => {
|
||||||
|
sceneInfra.camControls.setCam({
|
||||||
|
...camSettings,
|
||||||
|
position: [
|
||||||
|
parseFloat(e.target.value),
|
||||||
|
camSettings.position[1],
|
||||||
|
camSettings.position[2],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="pl-2 pr-1">y:</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step={5}
|
||||||
|
data-testid="cam-y-position"
|
||||||
|
value={camSettings.position[1]}
|
||||||
|
className="text-black w-16"
|
||||||
|
onChange={(e) => {
|
||||||
|
sceneInfra.camControls.setCam({
|
||||||
|
...camSettings,
|
||||||
|
position: [
|
||||||
|
camSettings.position[0],
|
||||||
|
parseFloat(e.target.value),
|
||||||
|
camSettings.position[2],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="pl-2 pr-1">z:</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step={5}
|
||||||
|
data-testid="cam-z-position"
|
||||||
|
value={camSettings.position[2]}
|
||||||
|
className="text-black w-16"
|
||||||
|
onChange={(e) => {
|
||||||
|
sceneInfra.camControls.setCam({
|
||||||
|
...camSettings,
|
||||||
|
position: [
|
||||||
|
camSettings.position[0],
|
||||||
|
camSettings.position[1],
|
||||||
|
parseFloat(e.target.value),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
28
src/clientSideScene/helpers.test.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Quaternion } from 'three'
|
||||||
|
import { isQuaternionVertical } from './helpers'
|
||||||
|
|
||||||
|
describe('isQuaternionVertical', () => {
|
||||||
|
it('should identify vertical quaternions', () => {
|
||||||
|
const verticalQuaternions = [
|
||||||
|
new Quaternion(1, 0, 0, 0).normalize(), // bottom
|
||||||
|
new Quaternion(-0.7, 0.7, 0, 0).normalize(), // bottom 2
|
||||||
|
new Quaternion(0, 1, 0, 0).normalize(), // bottom 3
|
||||||
|
new Quaternion(0, 0, 0, 1).normalize(), // look from top
|
||||||
|
]
|
||||||
|
verticalQuaternions.forEach((quaternion) => {
|
||||||
|
expect(isQuaternionVertical(quaternion)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should identify non-vertical quaternions', () => {
|
||||||
|
const nonVerticalQuaternions = [
|
||||||
|
new Quaternion(0.7, 0, 0, 0.7).normalize(), // front
|
||||||
|
new Quaternion(0, 0.7, 0.7, 0).normalize(), // back
|
||||||
|
new Quaternion(-0.5, 0.5, 0.5, -0.5).normalize(), // left side
|
||||||
|
new Quaternion(0.5, 0.5, 0.5, 0.5).normalize(), // right side
|
||||||
|
]
|
||||||
|
nonVerticalQuaternions.forEach((quaternion) => {
|
||||||
|
expect(isQuaternionVertical(quaternion)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
42
src/clientSideScene/helpers.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||||
|
import {
|
||||||
|
GridHelper,
|
||||||
|
LineBasicMaterial,
|
||||||
|
OrthographicCamera,
|
||||||
|
PerspectiveCamera,
|
||||||
|
Group,
|
||||||
|
Mesh,
|
||||||
|
Quaternion,
|
||||||
|
Vector3,
|
||||||
|
} from 'three'
|
||||||
|
|
||||||
|
export function createGridHelper({
|
||||||
|
size,
|
||||||
|
divisions,
|
||||||
|
}: {
|
||||||
|
size: number
|
||||||
|
divisions: number
|
||||||
|
}) {
|
||||||
|
const gridHelperMaterial = new LineBasicMaterial({
|
||||||
|
color: 0xaaaaaa,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.5,
|
||||||
|
depthTest: false,
|
||||||
|
})
|
||||||
|
const gridHelper = new GridHelper(size, divisions, 0x0000ff, 0xffffff)
|
||||||
|
gridHelper.material = gridHelperMaterial
|
||||||
|
gridHelper.rotation.x = Math.PI / 2
|
||||||
|
return gridHelper
|
||||||
|
}
|
||||||
|
|
||||||
|
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
|
||||||
|
0.55 / cam.zoom
|
||||||
|
|
||||||
|
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
|
||||||
|
(group.position.distanceTo(cam.position) * cam.fov) / 4000
|
||||||
|
|
||||||
|
export function isQuaternionVertical(q: Quaternion) {
|
||||||
|
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
||||||
|
// no x or y components means it's vertical
|
||||||
|
return compareVec2Epsilon2([v.x, v.y], [0, 0])
|
||||||
|
}
|
1040
src/clientSideScene/sceneEntities.ts
Normal file
600
src/clientSideScene/sceneInfra.ts
Normal file
@ -0,0 +1,600 @@
|
|||||||
|
import {
|
||||||
|
AmbientLight,
|
||||||
|
Color,
|
||||||
|
GridHelper,
|
||||||
|
LineBasicMaterial,
|
||||||
|
OrthographicCamera,
|
||||||
|
PerspectiveCamera,
|
||||||
|
Scene,
|
||||||
|
Vector3,
|
||||||
|
WebGLRenderer,
|
||||||
|
Raycaster,
|
||||||
|
Vector2,
|
||||||
|
Group,
|
||||||
|
PlaneGeometry,
|
||||||
|
MeshBasicMaterial,
|
||||||
|
Mesh,
|
||||||
|
DoubleSide,
|
||||||
|
Intersection,
|
||||||
|
Object3D,
|
||||||
|
Object3DEventMap,
|
||||||
|
} from 'three'
|
||||||
|
import { Coords2d, compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import * as TWEEN from '@tweenjs/tween.js'
|
||||||
|
import { SourceRange } from 'lang/wasm'
|
||||||
|
import { Axis } from 'lib/selections'
|
||||||
|
import { BaseUnit, SETTINGS_PERSIST_KEY } from 'machines/settingsMachine'
|
||||||
|
import { CameraControls } from './CameraControls'
|
||||||
|
|
||||||
|
type SendType = ReturnType<typeof useModelingContext>['send']
|
||||||
|
|
||||||
|
// 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 DEBUG_SHOW_INTERSECTION_PLANE = false
|
||||||
|
export const DEBUG_SHOW_BOTH_SCENES = false
|
||||||
|
|
||||||
|
export const RAYCASTABLE_PLANE = 'raycastable-plane'
|
||||||
|
export const DEFAULT_PLANES = 'default-planes'
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
interface BaseCallbackArgs2 {
|
||||||
|
object: any
|
||||||
|
event: any
|
||||||
|
}
|
||||||
|
interface BaseCallbackArgs {
|
||||||
|
event: any
|
||||||
|
}
|
||||||
|
interface OnDragCallbackArgs extends BaseCallbackArgs {
|
||||||
|
object: any
|
||||||
|
intersection2d: Vector2
|
||||||
|
intersectPoint: Vector3
|
||||||
|
intersection: Intersection<Object3D<Object3DEventMap>>
|
||||||
|
}
|
||||||
|
interface OnClickCallbackArgs extends BaseCallbackArgs {
|
||||||
|
intersection2d?: Vector2
|
||||||
|
intersectPoint: Vector3
|
||||||
|
intersection: Intersection<Object3D<Object3DEventMap>>
|
||||||
|
object?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface onMoveCallbackArgs {
|
||||||
|
event: any
|
||||||
|
intersection2d: Vector2
|
||||||
|
intersectPoint: Vector3
|
||||||
|
intersection: Intersection<Object3D<Object3DEventMap>>
|
||||||
|
}
|
||||||
|
|
||||||
|
// This singleton class is responsible for all of the under the hood setup for the client side scene.
|
||||||
|
// That is the cameras and switching between them, raycasters for click mouse events and their abstractions (onClick etc), setting up controls.
|
||||||
|
// Anything that added the the scene for the user to interact with is probably in SceneEntities.ts
|
||||||
|
class SceneInfra {
|
||||||
|
static instance: SceneInfra
|
||||||
|
scene: Scene
|
||||||
|
renderer: WebGLRenderer
|
||||||
|
camControls: CameraControls
|
||||||
|
isPerspective = true
|
||||||
|
fov = 45
|
||||||
|
fovBeforeAnimate = 45
|
||||||
|
isFovAnimationInProgress = false
|
||||||
|
_baseUnit: BaseUnit = 'mm'
|
||||||
|
_baseUnitMultiplier = 1
|
||||||
|
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||||
|
onMoveCallback: (arg: onMoveCallbackArgs) => void = () => {}
|
||||||
|
onClickCallback: (arg?: OnClickCallbackArgs) => void = () => {}
|
||||||
|
onMouseEnter: (arg: BaseCallbackArgs2) => void = () => {}
|
||||||
|
onMouseLeave: (arg: BaseCallbackArgs2) => void = () => {}
|
||||||
|
setCallbacks = (callbacks: {
|
||||||
|
onDrag?: (arg: OnDragCallbackArgs) => void
|
||||||
|
onMove?: (arg: onMoveCallbackArgs) => void
|
||||||
|
onClick?: (arg?: OnClickCallbackArgs) => void
|
||||||
|
onMouseEnter?: (arg: BaseCallbackArgs2) => void
|
||||||
|
onMouseLeave?: (arg: BaseCallbackArgs2) => void
|
||||||
|
}) => {
|
||||||
|
this.onDragCallback = callbacks.onDrag || this.onDragCallback
|
||||||
|
this.onMoveCallback = callbacks.onMove || this.onMoveCallback
|
||||||
|
this.onClickCallback = callbacks.onClick || this.onClickCallback
|
||||||
|
this.onMouseEnter = callbacks.onMouseEnter || this.onMouseEnter
|
||||||
|
this.onMouseLeave = callbacks.onMouseLeave || this.onMouseLeave
|
||||||
|
this.selected = null // following selections between callbacks being set is too tricky
|
||||||
|
}
|
||||||
|
set baseUnit(unit: BaseUnit) {
|
||||||
|
this._baseUnit = unit
|
||||||
|
this._baseUnitMultiplier = baseUnitTomm(unit)
|
||||||
|
this.scene.scale.set(
|
||||||
|
this._baseUnitMultiplier,
|
||||||
|
this._baseUnitMultiplier,
|
||||||
|
this._baseUnitMultiplier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
resetMouseListeners = () => {
|
||||||
|
sceneInfra.setCallbacks({
|
||||||
|
onDrag: () => {},
|
||||||
|
onMove: () => {},
|
||||||
|
onClick: () => {},
|
||||||
|
onMouseEnter: () => {},
|
||||||
|
onMouseLeave: () => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
highlightCallback: (a: SourceRange) => void = () => {}
|
||||||
|
setHighlightCallback(cb: (a: SourceRange) => void) {
|
||||||
|
this.highlightCallback = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
modelingSend: SendType = (() => {}) as any
|
||||||
|
setSend(send: SendType) {
|
||||||
|
this.modelingSend = send
|
||||||
|
}
|
||||||
|
|
||||||
|
hoveredObject: null | any = null
|
||||||
|
raycaster = new Raycaster()
|
||||||
|
planeRaycaster = new Raycaster()
|
||||||
|
currentMouseVector = new Vector2()
|
||||||
|
selected: {
|
||||||
|
mouseDownVector: Vector2
|
||||||
|
object: any
|
||||||
|
hasBeenDragged: boolean
|
||||||
|
} | null = null
|
||||||
|
selectedObject: null | any = null
|
||||||
|
mouseDownVector: null | Vector2 = null
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// SCENE
|
||||||
|
this.scene = new Scene()
|
||||||
|
this.scene.background = new Color(0x000000)
|
||||||
|
this.scene.background = null
|
||||||
|
|
||||||
|
// RENDERER
|
||||||
|
this.renderer = new WebGLRenderer({ antialias: true, alpha: true }) // Enable transparency
|
||||||
|
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
|
this.renderer.setClearColor(0x000000, 0) // Set clear color to black with 0 alpha (fully transparent)
|
||||||
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
|
|
||||||
|
// CAMERA
|
||||||
|
const camHeightDistanceRatio = 0.5
|
||||||
|
const baseUnit: BaseUnit =
|
||||||
|
JSON.parse(localStorage?.getItem(SETTINGS_PERSIST_KEY) || ('{}' as any))
|
||||||
|
.baseUnit || 'mm'
|
||||||
|
const baseRadius = 5.6
|
||||||
|
const length = baseUnitTomm(baseUnit) * baseRadius
|
||||||
|
const ang = Math.atan(camHeightDistanceRatio)
|
||||||
|
const x = Math.cos(ang) * length
|
||||||
|
const y = Math.sin(ang) * length
|
||||||
|
|
||||||
|
this.camControls = new CameraControls(false, this.renderer.domElement)
|
||||||
|
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
||||||
|
this.camControls.camera.layers.enable(SKETCH_LAYER)
|
||||||
|
this.camControls.camera.position.set(0, -x, y)
|
||||||
|
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
||||||
|
this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
|
// RAYCASTERS
|
||||||
|
this.raycaster.layers.enable(SKETCH_LAYER)
|
||||||
|
this.raycaster.layers.disable(0)
|
||||||
|
this.planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
|
// GRID
|
||||||
|
const size = 100
|
||||||
|
const divisions = 10
|
||||||
|
const gridHelperMaterial = new LineBasicMaterial({
|
||||||
|
color: 0x0000ff,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.5,
|
||||||
|
})
|
||||||
|
|
||||||
|
const gridHelper = new GridHelper(size, divisions, 0x0000ff, 0xffffff)
|
||||||
|
gridHelper.material = gridHelperMaterial
|
||||||
|
gridHelper.rotation.x = Math.PI / 2
|
||||||
|
// this.scene.add(gridHelper) // more of a debug thing, but maybe useful
|
||||||
|
|
||||||
|
const light = new AmbientLight(0x505050) // soft white light
|
||||||
|
this.scene.add(light)
|
||||||
|
|
||||||
|
SceneInfra.instance = this
|
||||||
|
}
|
||||||
|
|
||||||
|
onCameraChange = () => {
|
||||||
|
const scale = getSceneScale(
|
||||||
|
this.camControls.camera,
|
||||||
|
this.camControls.target
|
||||||
|
)
|
||||||
|
const planesGroup = this.scene.getObjectByName(DEFAULT_PLANES)
|
||||||
|
const axisGroup = this.scene
|
||||||
|
.getObjectByName(AXIS_GROUP)
|
||||||
|
?.getObjectByName('gridHelper')
|
||||||
|
planesGroup &&
|
||||||
|
planesGroup.scale.set(
|
||||||
|
scale / sceneInfra._baseUnitMultiplier,
|
||||||
|
scale / sceneInfra._baseUnitMultiplier,
|
||||||
|
scale / sceneInfra._baseUnitMultiplier
|
||||||
|
)
|
||||||
|
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
onWindowResize = () => {
|
||||||
|
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
animate = () => {
|
||||||
|
requestAnimationFrame(this.animate)
|
||||||
|
TWEEN.update() // This will update all tweens during the animation loop
|
||||||
|
if (!this.isFovAnimationInProgress) {
|
||||||
|
// console.log('animation frame', this.cameraControls.camera)
|
||||||
|
this.camControls.update()
|
||||||
|
this.renderer.render(this.scene, this.camControls.camera)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose = () => {
|
||||||
|
// Dispose of scene resources, renderer, and controls
|
||||||
|
this.renderer.dispose()
|
||||||
|
window.removeEventListener('resize', this.onWindowResize)
|
||||||
|
// Dispose of any other resources like geometries, materials, textures
|
||||||
|
}
|
||||||
|
getPlaneIntersectPoint = (): {
|
||||||
|
intersection2d?: Vector2
|
||||||
|
intersectPoint: Vector3
|
||||||
|
intersection: Intersection<Object3D<Object3DEventMap>>
|
||||||
|
} | null => {
|
||||||
|
this.planeRaycaster.setFromCamera(
|
||||||
|
this.currentMouseVector,
|
||||||
|
sceneInfra.camControls.camera
|
||||||
|
)
|
||||||
|
const planeIntersects = this.planeRaycaster.intersectObjects(
|
||||||
|
this.scene.children,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
planeIntersects.length > 0 &&
|
||||||
|
planeIntersects[0].object.userData.type !== RAYCASTABLE_PLANE
|
||||||
|
) {
|
||||||
|
const intersect = planeIntersects[0]
|
||||||
|
return {
|
||||||
|
intersectPoint: intersect.point,
|
||||||
|
intersection: intersect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
planeIntersects.length > 0 &&
|
||||||
|
planeIntersects[0].object.userData.type === RAYCASTABLE_PLANE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
const planePosition = planeIntersects[0].object.position
|
||||||
|
const inversePlaneQuaternion = planeIntersects[0].object.quaternion
|
||||||
|
.clone()
|
||||||
|
.invert()
|
||||||
|
const intersectPoint = planeIntersects[0].point
|
||||||
|
let transformedPoint = intersectPoint.clone()
|
||||||
|
if (transformedPoint) {
|
||||||
|
transformedPoint.applyQuaternion(inversePlaneQuaternion)
|
||||||
|
transformedPoint?.sub(
|
||||||
|
new Vector3(...planePosition).applyQuaternion(inversePlaneQuaternion)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
intersection2d: new Vector2(
|
||||||
|
transformedPoint.x / this._baseUnitMultiplier,
|
||||||
|
transformedPoint.y / this._baseUnitMultiplier
|
||||||
|
), // z should be 0
|
||||||
|
intersectPoint: intersectPoint.divideScalar(this._baseUnitMultiplier),
|
||||||
|
intersection: planeIntersects[0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMouseMove = (event: MouseEvent) => {
|
||||||
|
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1
|
||||||
|
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1
|
||||||
|
|
||||||
|
const planeIntersectPoint = this.getPlaneIntersectPoint()
|
||||||
|
|
||||||
|
if (this.selected) {
|
||||||
|
const hasBeenDragged = !compareVec2Epsilon2(
|
||||||
|
[this.currentMouseVector.x, this.currentMouseVector.y],
|
||||||
|
[this.selected.mouseDownVector.x, this.selected.mouseDownVector.y],
|
||||||
|
0.02
|
||||||
|
)
|
||||||
|
if (!this.selected.hasBeenDragged && hasBeenDragged) {
|
||||||
|
this.selected.hasBeenDragged = true
|
||||||
|
// this is where we could fire a onDragStart event
|
||||||
|
// console.log('onDragStart', this.selected)
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
hasBeenDragged &&
|
||||||
|
planeIntersectPoint &&
|
||||||
|
planeIntersectPoint.intersection2d
|
||||||
|
) {
|
||||||
|
// // console.log('onDrag', this.selected)
|
||||||
|
|
||||||
|
this.onDragCallback({
|
||||||
|
object: this.selected.object,
|
||||||
|
event,
|
||||||
|
intersection2d: planeIntersectPoint.intersection2d,
|
||||||
|
...planeIntersectPoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (planeIntersectPoint && planeIntersectPoint.intersection2d) {
|
||||||
|
this.onMoveCallback({
|
||||||
|
event,
|
||||||
|
intersection2d: planeIntersectPoint.intersection2d,
|
||||||
|
...planeIntersectPoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const intersect = this.raycastRing()
|
||||||
|
|
||||||
|
if (intersect) {
|
||||||
|
const firstIntersectObject = intersect.object
|
||||||
|
if (this.hoveredObject !== firstIntersectObject) {
|
||||||
|
if (this.hoveredObject) {
|
||||||
|
this.onMouseLeave({
|
||||||
|
object: this.hoveredObject,
|
||||||
|
event,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.hoveredObject = firstIntersectObject
|
||||||
|
this.onMouseEnter({
|
||||||
|
object: this.hoveredObject,
|
||||||
|
event,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.hoveredObject) {
|
||||||
|
this.onMouseLeave({
|
||||||
|
object: this.hoveredObject,
|
||||||
|
event,
|
||||||
|
})
|
||||||
|
this.hoveredObject = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
raycastRing = (
|
||||||
|
pixelRadius = 8,
|
||||||
|
rayRingCount = 32
|
||||||
|
): Intersection<Object3D<Object3DEventMap>> | undefined => {
|
||||||
|
const mouseDownVector = this.currentMouseVector.clone()
|
||||||
|
let closestIntersection:
|
||||||
|
| Intersection<Object3D<Object3DEventMap>>
|
||||||
|
| undefined = undefined
|
||||||
|
let closestDistance = Infinity
|
||||||
|
|
||||||
|
const updateClosestIntersection = (
|
||||||
|
intersections: Intersection<Object3D<Object3DEventMap>>[]
|
||||||
|
) => {
|
||||||
|
let intersection = null
|
||||||
|
for (let i = 0; i < intersections.length; i++) {
|
||||||
|
if (intersections[i].object.type !== 'GridHelper') {
|
||||||
|
intersection = intersections[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!intersection) return
|
||||||
|
|
||||||
|
if (intersection.distance < closestDistance) {
|
||||||
|
closestDistance = intersection.distance
|
||||||
|
closestIntersection = intersection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the center point
|
||||||
|
this.raycaster.setFromCamera(mouseDownVector, this.camControls.camera)
|
||||||
|
updateClosestIntersection(
|
||||||
|
this.raycaster.intersectObjects(this.scene.children, true)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check the ring points
|
||||||
|
for (let i = 0; i < rayRingCount; i++) {
|
||||||
|
const angle = (i / rayRingCount) * Math.PI * 2
|
||||||
|
|
||||||
|
const offsetX = ((pixelRadius * Math.cos(angle)) / window.innerWidth) * 2
|
||||||
|
const offsetY = ((pixelRadius * Math.sin(angle)) / window.innerHeight) * 2
|
||||||
|
const ringVector = new Vector2(
|
||||||
|
mouseDownVector.x + offsetX,
|
||||||
|
mouseDownVector.y - offsetY
|
||||||
|
)
|
||||||
|
this.raycaster.setFromCamera(ringVector, this.camControls.camera)
|
||||||
|
updateClosestIntersection(
|
||||||
|
this.raycaster.intersectObjects(this.scene.children, true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return closestIntersection
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseDown = (event: MouseEvent) => {
|
||||||
|
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1
|
||||||
|
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1
|
||||||
|
|
||||||
|
const mouseDownVector = this.currentMouseVector.clone()
|
||||||
|
const intersect = this.raycastRing()
|
||||||
|
|
||||||
|
if (intersect) {
|
||||||
|
const intersectParent = intersect?.object?.parent as Group
|
||||||
|
this.selected = intersectParent.isGroup
|
||||||
|
? {
|
||||||
|
mouseDownVector,
|
||||||
|
object: intersect?.object,
|
||||||
|
hasBeenDragged: false,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUp = (event: MouseEvent) => {
|
||||||
|
this.currentMouseVector.x = (event.clientX / window.innerWidth) * 2 - 1
|
||||||
|
this.currentMouseVector.y = -(event.clientY / window.innerHeight) * 2 + 1
|
||||||
|
const planeIntersectPoint = this.getPlaneIntersectPoint()
|
||||||
|
|
||||||
|
if (this.selected) {
|
||||||
|
if (this.selected.hasBeenDragged) {
|
||||||
|
// this is where we could fire a onDragEnd event
|
||||||
|
// console.log('onDragEnd', this.selected)
|
||||||
|
} else if (planeIntersectPoint) {
|
||||||
|
// fire onClick event as there was no drags
|
||||||
|
this.onClickCallback({
|
||||||
|
object: this.selected?.object,
|
||||||
|
event,
|
||||||
|
...planeIntersectPoint,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.onClickCallback()
|
||||||
|
}
|
||||||
|
// Clear the selected state whether it was dragged or not
|
||||||
|
this.selected = null
|
||||||
|
} else if (planeIntersectPoint) {
|
||||||
|
this.onClickCallback({
|
||||||
|
event,
|
||||||
|
...planeIntersectPoint,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.onClickCallback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showDefaultPlanes() {
|
||||||
|
const addPlane = (
|
||||||
|
rotation: { x: number; y: number; z: number }, //
|
||||||
|
type: DefaultPlane
|
||||||
|
): Mesh => {
|
||||||
|
const planeGeometry = new PlaneGeometry(100, 100)
|
||||||
|
const planeMaterial = new MeshBasicMaterial({
|
||||||
|
color: defaultPlaneColor(type),
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.0,
|
||||||
|
side: DoubleSide,
|
||||||
|
depthTest: false, // needed to avoid transparency issues
|
||||||
|
})
|
||||||
|
const plane = new Mesh(planeGeometry, planeMaterial)
|
||||||
|
plane.rotation.x = rotation.x
|
||||||
|
plane.rotation.y = rotation.y
|
||||||
|
plane.rotation.z = rotation.z
|
||||||
|
plane.userData.type = type
|
||||||
|
plane.name = type
|
||||||
|
return plane
|
||||||
|
}
|
||||||
|
const planes = [
|
||||||
|
addPlane({ x: 0, y: Math.PI / 2, z: 0 }, YZ_PLANE),
|
||||||
|
addPlane({ x: 0, y: 0, z: 0 }, XY_PLANE),
|
||||||
|
addPlane({ x: -Math.PI / 2, y: 0, z: 0 }, XZ_PLANE),
|
||||||
|
]
|
||||||
|
const planesGroup = new Group()
|
||||||
|
planesGroup.userData.type = DEFAULT_PLANES
|
||||||
|
planesGroup.name = DEFAULT_PLANES
|
||||||
|
planesGroup.add(...planes)
|
||||||
|
planesGroup.traverse((child) => {
|
||||||
|
if (child instanceof Mesh) {
|
||||||
|
child.layers.enable(SKETCH_LAYER)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
planesGroup.layers.enable(SKETCH_LAYER)
|
||||||
|
const sceneScale = getSceneScale(
|
||||||
|
this.camControls.camera,
|
||||||
|
this.camControls.target
|
||||||
|
)
|
||||||
|
planesGroup.scale.set(
|
||||||
|
sceneScale / sceneInfra._baseUnitMultiplier,
|
||||||
|
sceneScale / sceneInfra._baseUnitMultiplier,
|
||||||
|
sceneScale / sceneInfra._baseUnitMultiplier
|
||||||
|
)
|
||||||
|
this.scene.add(planesGroup)
|
||||||
|
}
|
||||||
|
removeDefaultPlanes() {
|
||||||
|
const planesGroup = this.scene.children.find(
|
||||||
|
({ userData }) => userData.type === DEFAULT_PLANES
|
||||||
|
)
|
||||||
|
if (planesGroup) this.scene.remove(planesGroup)
|
||||||
|
}
|
||||||
|
updateOtherSelectionColors = (otherSelections: Axis[]) => {
|
||||||
|
const axisGroup = sceneInfra.scene.children.find(
|
||||||
|
({ userData }) => userData?.type === AXIS_GROUP
|
||||||
|
)
|
||||||
|
const axisMap: { [key: string]: Axis } = {
|
||||||
|
[X_AXIS]: 'x-axis',
|
||||||
|
[Y_AXIS]: 'y-axis',
|
||||||
|
}
|
||||||
|
axisGroup?.children.forEach((_mesh) => {
|
||||||
|
const mesh = _mesh as Mesh
|
||||||
|
const mat = mesh.material as MeshBasicMaterial
|
||||||
|
if (otherSelections.includes(axisMap[mesh.userData?.type])) {
|
||||||
|
mat.color.set(mesh?.userData?.baseColor)
|
||||||
|
mat.color.offsetHSL(0, 0, 0.2)
|
||||||
|
mesh.userData.isSelected = true
|
||||||
|
} else {
|
||||||
|
mat.color.set(mesh?.userData?.baseColor)
|
||||||
|
mesh.userData.isSelected = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sceneInfra = new SceneInfra()
|
||||||
|
|
||||||
|
export function getSceneScale(
|
||||||
|
camera: PerspectiveCamera | OrthographicCamera,
|
||||||
|
target: Vector3
|
||||||
|
): number {
|
||||||
|
const distance =
|
||||||
|
camera instanceof PerspectiveCamera
|
||||||
|
? camera.position.distanceTo(target)
|
||||||
|
: 63.7942123 / camera.zoom
|
||||||
|
|
||||||
|
if (distance <= 20) return 0.1
|
||||||
|
else if (distance > 20 && distance <= 200) return 1
|
||||||
|
else if (distance > 200 && distance <= 2000) return 10
|
||||||
|
else if (distance > 2000 && distance <= 20000) return 100
|
||||||
|
else if (distance > 20000) return 1000
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function baseUnitTomm(baseUnit: BaseUnit) {
|
||||||
|
switch (baseUnit) {
|
||||||
|
case 'mm':
|
||||||
|
return 1
|
||||||
|
case 'cm':
|
||||||
|
return 10
|
||||||
|
case 'm':
|
||||||
|
return 1000
|
||||||
|
case 'in':
|
||||||
|
return 25.4
|
||||||
|
case 'ft':
|
||||||
|
return 304.8
|
||||||
|
case 'yd':
|
||||||
|
return 914.4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DefaultPlane =
|
||||||
|
| 'xy-default-plane'
|
||||||
|
| 'xz-default-plane'
|
||||||
|
| 'yz-default-plane'
|
||||||
|
|
||||||
|
export const XY_PLANE: DefaultPlane = 'xy-default-plane'
|
||||||
|
export const XZ_PLANE: DefaultPlane = 'xz-default-plane'
|
||||||
|
export const YZ_PLANE: DefaultPlane = 'yz-default-plane'
|
||||||
|
|
||||||
|
export function defaultPlaneColor(
|
||||||
|
plane: DefaultPlane,
|
||||||
|
lowCh = 0.1,
|
||||||
|
highCh = 0.7
|
||||||
|
): Color {
|
||||||
|
switch (plane) {
|
||||||
|
case XY_PLANE:
|
||||||
|
return new Color(highCh, lowCh, lowCh)
|
||||||
|
case XZ_PLANE:
|
||||||
|
return new Color(lowCh, lowCh, highCh)
|
||||||
|
case YZ_PLANE:
|
||||||
|
return new Color(lowCh, highCh, lowCh)
|
||||||
|
}
|
||||||
|
return new Color(lowCh, lowCh, lowCh)
|
||||||
|
}
|
358
src/clientSideScene/segments.ts
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
import { Coords2d } from 'lang/std/sketch'
|
||||||
|
import {
|
||||||
|
BufferGeometry,
|
||||||
|
CatmullRomCurve3,
|
||||||
|
ConeGeometry,
|
||||||
|
CurvePath,
|
||||||
|
EllipseCurve,
|
||||||
|
ExtrudeGeometry,
|
||||||
|
Group,
|
||||||
|
LineCurve3,
|
||||||
|
Mesh,
|
||||||
|
MeshBasicMaterial,
|
||||||
|
NormalBufferAttributes,
|
||||||
|
Shape,
|
||||||
|
SphereGeometry,
|
||||||
|
Vector2,
|
||||||
|
Vector3,
|
||||||
|
} from 'three'
|
||||||
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
||||||
|
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
|
||||||
|
import {
|
||||||
|
STRAIGHT_SEGMENT,
|
||||||
|
STRAIGHT_SEGMENT_BODY,
|
||||||
|
STRAIGHT_SEGMENT_DASH,
|
||||||
|
TANGENTIAL_ARC_TO_SEGMENT,
|
||||||
|
TANGENTIAL_ARC_TO_SEGMENT_BODY,
|
||||||
|
TANGENTIAL_ARC_TO__SEGMENT_DASH,
|
||||||
|
} from './sceneEntities'
|
||||||
|
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
||||||
|
import { ARROWHEAD } from './sceneInfra'
|
||||||
|
|
||||||
|
export function straightSegment({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
id,
|
||||||
|
pathToNode,
|
||||||
|
isDraftSegment,
|
||||||
|
scale = 1,
|
||||||
|
}: {
|
||||||
|
from: Coords2d
|
||||||
|
to: Coords2d
|
||||||
|
id: string
|
||||||
|
pathToNode: PathToNode
|
||||||
|
isDraftSegment?: boolean
|
||||||
|
scale?: number
|
||||||
|
}): Group {
|
||||||
|
const group = new Group()
|
||||||
|
|
||||||
|
const shape = new Shape()
|
||||||
|
shape.moveTo(0, -0.08 * scale)
|
||||||
|
shape.lineTo(0, 0.08 * scale) // The width of the line
|
||||||
|
|
||||||
|
let geometry
|
||||||
|
if (isDraftSegment) {
|
||||||
|
geometry = dashedStraight(from, to, shape, scale)
|
||||||
|
} else {
|
||||||
|
const line = new LineCurve3(
|
||||||
|
new Vector3(from[0], from[1], 0),
|
||||||
|
new Vector3(to[0], to[1], 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
geometry = new ExtrudeGeometry(shape, {
|
||||||
|
steps: 2,
|
||||||
|
bevelEnabled: false,
|
||||||
|
extrudePath: line,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = new MeshBasicMaterial({ color: 0xffffff })
|
||||||
|
const mesh = new Mesh(geometry, body)
|
||||||
|
mesh.userData.type = isDraftSegment
|
||||||
|
? STRAIGHT_SEGMENT_DASH
|
||||||
|
: STRAIGHT_SEGMENT_BODY
|
||||||
|
mesh.name = STRAIGHT_SEGMENT_BODY
|
||||||
|
|
||||||
|
group.userData = {
|
||||||
|
type: STRAIGHT_SEGMENT,
|
||||||
|
id,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
pathToNode,
|
||||||
|
isSelected: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrowGroup = createArrowhead(scale)
|
||||||
|
arrowGroup.position.set(to[0], to[1], 0)
|
||||||
|
const dir = new Vector3()
|
||||||
|
.subVectors(new Vector3(to[0], to[1], 0), new Vector3(from[0], from[1], 0))
|
||||||
|
.normalize()
|
||||||
|
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
||||||
|
|
||||||
|
group.add(mesh, arrowGroup)
|
||||||
|
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
|
function createArrowhead(scale = 1): Group {
|
||||||
|
const arrowMaterial = new MeshBasicMaterial({ color: 0xffffff })
|
||||||
|
const arrowheadMesh = new Mesh(new ConeGeometry(0.31, 1.5, 12), arrowMaterial)
|
||||||
|
arrowheadMesh.position.set(0, -0.6, 0)
|
||||||
|
const sphereMesh = new Mesh(new SphereGeometry(0.27, 12, 12), arrowMaterial)
|
||||||
|
|
||||||
|
const arrowGroup = new Group()
|
||||||
|
arrowGroup.userData.type = ARROWHEAD
|
||||||
|
arrowGroup.name = ARROWHEAD
|
||||||
|
arrowGroup.add(arrowheadMesh, sphereMesh)
|
||||||
|
arrowGroup.lookAt(new Vector3(0, 1, 0))
|
||||||
|
arrowGroup.scale.set(scale, scale, scale)
|
||||||
|
return arrowGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tangentialArcToSegment({
|
||||||
|
prevSegment,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
id,
|
||||||
|
pathToNode,
|
||||||
|
isDraftSegment,
|
||||||
|
scale = 1,
|
||||||
|
}: {
|
||||||
|
prevSegment: SketchGroup['value'][number]
|
||||||
|
from: Coords2d
|
||||||
|
to: Coords2d
|
||||||
|
id: string
|
||||||
|
pathToNode: PathToNode
|
||||||
|
isDraftSegment?: boolean
|
||||||
|
scale?: number
|
||||||
|
}): Group {
|
||||||
|
const group = new Group()
|
||||||
|
|
||||||
|
const previousPoint =
|
||||||
|
prevSegment?.type === 'TangentialArcTo'
|
||||||
|
? getTangentPointFromPreviousArc(
|
||||||
|
prevSegment.center,
|
||||||
|
prevSegment.ccw,
|
||||||
|
prevSegment.to
|
||||||
|
)
|
||||||
|
: prevSegment.from
|
||||||
|
|
||||||
|
const { center, radius, startAngle, endAngle, ccw } = getTangentialArcToInfo({
|
||||||
|
arcStartPoint: from,
|
||||||
|
arcEndPoint: to,
|
||||||
|
tanPreviousPoint: previousPoint,
|
||||||
|
obtuse: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const geometry = createArcGeometry({
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
startAngle,
|
||||||
|
endAngle,
|
||||||
|
ccw,
|
||||||
|
isDashed: isDraftSegment,
|
||||||
|
scale,
|
||||||
|
})
|
||||||
|
|
||||||
|
const body = new MeshBasicMaterial({ color: 0xffffff })
|
||||||
|
const mesh = new Mesh(geometry, body)
|
||||||
|
mesh.userData.type = isDraftSegment
|
||||||
|
? TANGENTIAL_ARC_TO__SEGMENT_DASH
|
||||||
|
: TANGENTIAL_ARC_TO_SEGMENT_BODY
|
||||||
|
|
||||||
|
group.userData = {
|
||||||
|
type: TANGENTIAL_ARC_TO_SEGMENT,
|
||||||
|
id,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
prevSegment,
|
||||||
|
pathToNode,
|
||||||
|
isSelected: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrowGroup = createArrowhead(scale)
|
||||||
|
arrowGroup.position.set(to[0], to[1], 0)
|
||||||
|
const arrowheadAngle = endAngle + (Math.PI / 2) * (ccw ? 1 : -1)
|
||||||
|
arrowGroup.quaternion.setFromUnitVectors(
|
||||||
|
new Vector3(0, 1, 0),
|
||||||
|
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
group.add(mesh, arrowGroup)
|
||||||
|
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createArcGeometry({
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
startAngle,
|
||||||
|
endAngle,
|
||||||
|
ccw,
|
||||||
|
isDashed = false,
|
||||||
|
scale = 1,
|
||||||
|
}: {
|
||||||
|
center: Coords2d
|
||||||
|
radius: number
|
||||||
|
startAngle: number
|
||||||
|
endAngle: number
|
||||||
|
ccw: boolean
|
||||||
|
isDashed?: boolean
|
||||||
|
scale?: number
|
||||||
|
}): BufferGeometry {
|
||||||
|
const dashSize = 1.2 * scale
|
||||||
|
const gapSize = 1.2 * scale
|
||||||
|
const arcStart = new EllipseCurve(
|
||||||
|
center[0],
|
||||||
|
center[1],
|
||||||
|
radius,
|
||||||
|
radius,
|
||||||
|
startAngle,
|
||||||
|
endAngle,
|
||||||
|
!ccw,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const arcEnd = new EllipseCurve(
|
||||||
|
center[0],
|
||||||
|
center[1],
|
||||||
|
radius,
|
||||||
|
radius,
|
||||||
|
endAngle,
|
||||||
|
startAngle,
|
||||||
|
ccw,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const shape = new Shape()
|
||||||
|
shape.moveTo(0, -0.08 * scale)
|
||||||
|
shape.lineTo(0, 0.08 * scale) // The width of the line
|
||||||
|
|
||||||
|
if (!isDashed) {
|
||||||
|
const points = arcStart.getPoints(50)
|
||||||
|
const path = new CurvePath<Vector3>()
|
||||||
|
path.add(new CatmullRomCurve3(points.map((p) => new Vector3(p.x, p.y, 0))))
|
||||||
|
|
||||||
|
return new ExtrudeGeometry(shape, {
|
||||||
|
steps: 100,
|
||||||
|
bevelEnabled: false,
|
||||||
|
extrudePath: path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const length = arcStart.getLength()
|
||||||
|
const totalDashes = length / (dashSize + gapSize) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place
|
||||||
|
const dashesAtEachEnd = Math.min(100, totalDashes / 2) // Assuming we want 50 dashes total, 25 at each end
|
||||||
|
|
||||||
|
const dashGeometries = []
|
||||||
|
|
||||||
|
// Function to create a dash at a specific t value (0 to 1 along the curve)
|
||||||
|
const createDashAt = (t: number, curve: EllipseCurve) => {
|
||||||
|
const startVec = curve.getPoint(t)
|
||||||
|
const endVec = curve.getPoint(Math.min(0.5, t + dashSize / length))
|
||||||
|
const midVec = curve.getPoint(Math.min(0.5, t + dashSize / length / 2))
|
||||||
|
const dashCurve = new CurvePath<Vector3>()
|
||||||
|
dashCurve.add(
|
||||||
|
new CatmullRomCurve3([
|
||||||
|
new Vector3(startVec.x, startVec.y, 0),
|
||||||
|
new Vector3(midVec.x, midVec.y, 0),
|
||||||
|
new Vector3(endVec.x, endVec.y, 0),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
return new ExtrudeGeometry(shape, {
|
||||||
|
steps: 3,
|
||||||
|
bevelEnabled: false,
|
||||||
|
extrudePath: dashCurve,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create dashes at the start of the arc
|
||||||
|
for (let i = 0; i < dashesAtEachEnd; i++) {
|
||||||
|
const t = i / totalDashes
|
||||||
|
dashGeometries.push(createDashAt(t, arcStart))
|
||||||
|
dashGeometries.push(createDashAt(t, arcEnd))
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill in the remaining arc
|
||||||
|
const remainingArcLength = length - dashesAtEachEnd * 2 * (dashSize + gapSize)
|
||||||
|
if (remainingArcLength > 0) {
|
||||||
|
const remainingArcStartT = dashesAtEachEnd / totalDashes
|
||||||
|
const remainingArcEndT = 1 - remainingArcStartT
|
||||||
|
const centerVec = new Vector2(center[0], center[1])
|
||||||
|
const remainingArcStartVec = arcStart.getPoint(remainingArcStartT)
|
||||||
|
const remainingArcEndVec = arcStart.getPoint(remainingArcEndT)
|
||||||
|
const remainingArcCurve = new EllipseCurve(
|
||||||
|
arcStart.aX,
|
||||||
|
arcStart.aY,
|
||||||
|
arcStart.xRadius,
|
||||||
|
arcStart.yRadius,
|
||||||
|
new Vector2().subVectors(centerVec, remainingArcStartVec).angle() +
|
||||||
|
Math.PI,
|
||||||
|
new Vector2().subVectors(centerVec, remainingArcEndVec).angle() + Math.PI,
|
||||||
|
!ccw
|
||||||
|
)
|
||||||
|
const remainingArcPoints = remainingArcCurve.getPoints(50)
|
||||||
|
const remainingArcPath = new CurvePath<Vector3>()
|
||||||
|
remainingArcPath.add(
|
||||||
|
new CatmullRomCurve3(
|
||||||
|
remainingArcPoints.map((p) => new Vector3(p.x, p.y, 0))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const remainingArcGeometry = new ExtrudeGeometry(shape, {
|
||||||
|
steps: 50,
|
||||||
|
bevelEnabled: false,
|
||||||
|
extrudePath: remainingArcPath,
|
||||||
|
})
|
||||||
|
dashGeometries.push(remainingArcGeometry)
|
||||||
|
}
|
||||||
|
|
||||||
|
const geo = dashGeometries.length
|
||||||
|
? mergeGeometries(dashGeometries)
|
||||||
|
: new BufferGeometry()
|
||||||
|
geo.userData.type = 'dashed'
|
||||||
|
return geo
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dashedStraight(
|
||||||
|
from: Coords2d,
|
||||||
|
to: Coords2d,
|
||||||
|
shape: Shape,
|
||||||
|
scale = 1
|
||||||
|
): BufferGeometry<NormalBufferAttributes> {
|
||||||
|
const dashSize = 1.2 * scale
|
||||||
|
const gapSize = 1.2 * scale // todo: gabSize is not respected
|
||||||
|
const dashLine = new LineCurve3(
|
||||||
|
new Vector3(from[0], from[1], 0),
|
||||||
|
new Vector3(to[0], to[1], 0)
|
||||||
|
)
|
||||||
|
const length = dashLine.getLength()
|
||||||
|
const numberOfPoints = (length / (dashSize + gapSize)) * 2
|
||||||
|
const startOfLine = new Vector3(from[0], from[1], 0)
|
||||||
|
const endOfLine = new Vector3(to[0], to[1], 0)
|
||||||
|
const dashGeometries = []
|
||||||
|
const dashComponent = (xOrY: number, pointIndex: number) =>
|
||||||
|
((to[xOrY] - from[xOrY]) / numberOfPoints) * pointIndex + from[xOrY]
|
||||||
|
for (let i = 0; i < numberOfPoints; i += 2) {
|
||||||
|
const dashStart = new Vector3(dashComponent(0, i), dashComponent(1, i), 0)
|
||||||
|
let dashEnd = new Vector3(
|
||||||
|
dashComponent(0, i + 1),
|
||||||
|
dashComponent(1, i + 1),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
if (startOfLine.distanceTo(dashEnd) > startOfLine.distanceTo(endOfLine))
|
||||||
|
dashEnd = endOfLine
|
||||||
|
|
||||||
|
if (dashEnd) {
|
||||||
|
const dashCurve = new LineCurve3(dashStart, dashEnd)
|
||||||
|
const dashGeometry = new ExtrudeGeometry(shape, {
|
||||||
|
steps: 1,
|
||||||
|
bevelEnabled: false,
|
||||||
|
extrudePath: dashCurve,
|
||||||
|
})
|
||||||
|
dashGeometries.push(dashGeometry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const geo = dashGeometries.length
|
||||||
|
? mergeGeometries(dashGeometries)
|
||||||
|
: new BufferGeometry()
|
||||||
|
geo.userData.type = 'dashed'
|
||||||
|
return geo
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { paths } from '../Router'
|
import { paths } from 'lib/paths'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import type { LinkProps } from 'react-router-dom'
|
import type { LinkProps } from 'react-router-dom'
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Toolbar } from '../Toolbar'
|
import { Toolbar } from '../Toolbar'
|
||||||
import UserSidebarMenu from './UserSidebarMenu'
|
import UserSidebarMenu from './UserSidebarMenu'
|
||||||
import { IndexLoaderData } from '../Router'
|
import { type IndexLoaderData } from 'lib/types'
|
||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import styles from './AppHeader.module.css'
|
import styles from './AppHeader.module.css'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
import { kclManager } from 'lang/KclSingleton'
|
||||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from '../lang/modifyAst'
|
} from '../lang/modifyAst'
|
||||||
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
|
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
|
||||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||||
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { executeAst } from 'useStore'
|
import { executeAst } from 'useStore'
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ export function useCalc({
|
|||||||
inputRef: React.RefObject<HTMLInputElement>
|
inputRef: React.RefObject<HTMLInputElement>
|
||||||
valueNode: Value | null
|
valueNode: Value | null
|
||||||
calcResult: string
|
calcResult: string
|
||||||
prevVariables: PrevVariable<any>[]
|
prevVariables: PrevVariable<unknown>[]
|
||||||
newVariableName: string
|
newVariableName: string
|
||||||
isNewVariableNameUnique: boolean
|
isNewVariableNameUnique: boolean
|
||||||
newVariableInsertIndex: number
|
newVariableInsertIndex: number
|
||||||
@ -148,7 +148,6 @@ export function useCalc({
|
|||||||
executeAst({
|
executeAst({
|
||||||
ast,
|
ast,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
defaultPlanes: kclManager.defaultPlanes,
|
|
||||||
useFakeExecutor: true,
|
useFakeExecutor: true,
|
||||||
programMemoryOverride: JSON.parse(
|
programMemoryOverride: JSON.parse(
|
||||||
JSON.stringify(kclManager.programMemory)
|
JSON.stringify(kclManager.programMemory)
|
||||||
|
75
src/components/CamToggle.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { sceneInfra } from '../clientSideScene/sceneInfra'
|
||||||
|
import { engineCommandManager } from 'lang/std/engineConnection'
|
||||||
|
import { throttle, isReducedMotion } from 'lib/utils'
|
||||||
|
|
||||||
|
const updateDollyZoom = throttle(
|
||||||
|
(newFov: number) => sceneInfra.camControls.dollyZoom(newFov),
|
||||||
|
1000 / 15
|
||||||
|
)
|
||||||
|
|
||||||
|
export const CamToggle = () => {
|
||||||
|
const [isPerspective, setIsPerspective] = useState(true)
|
||||||
|
const [fov, setFov] = useState(40)
|
||||||
|
const [enableRotate, setEnableRotate] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
engineCommandManager.waitForReady.then(async () => {
|
||||||
|
sceneInfra.camControls.dollyZoom(fov)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const toggleCamera = () => {
|
||||||
|
if (isPerspective) {
|
||||||
|
isReducedMotion()
|
||||||
|
? sceneInfra.camControls.useOrthographicCamera()
|
||||||
|
: sceneInfra.camControls.animateToOrthographic()
|
||||||
|
} else {
|
||||||
|
isReducedMotion()
|
||||||
|
? sceneInfra.camControls.usePerspectiveCamera()
|
||||||
|
: sceneInfra.camControls.animateToPerspective()
|
||||||
|
}
|
||||||
|
setIsPerspective(!isPerspective)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFovChange = (newFov: number) => {
|
||||||
|
setFov(newFov)
|
||||||
|
updateDollyZoom(newFov)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute right-14 bottom-3">
|
||||||
|
{isPerspective && (
|
||||||
|
<div className="">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="4"
|
||||||
|
max="90"
|
||||||
|
step={0.5}
|
||||||
|
value={fov}
|
||||||
|
onChange={(e) => handleFovChange(Number(e.target.value))}
|
||||||
|
className="w-full cursor-pointer pointer-events-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button onClick={toggleCamera} className="">
|
||||||
|
{isPerspective
|
||||||
|
? 'Switch to Orthographic Camera'
|
||||||
|
: 'Switch to Perspective Camera'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (enableRotate) {
|
||||||
|
sceneInfra.camControls.enableRotate = false
|
||||||
|
} else {
|
||||||
|
sceneInfra.camControls.enableRotate = true
|
||||||
|
}
|
||||||
|
setEnableRotate(!enableRotate)
|
||||||
|
}}
|
||||||
|
className=""
|
||||||
|
>
|
||||||
|
{enableRotate ? 'Disable Rotation' : 'Enable Rotation'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -9,7 +9,7 @@ import styles from './CodeMenu.module.css'
|
|||||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||||
import { editorShortcutMeta } from './TextEditor'
|
import { editorShortcutMeta } from './TextEditor'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
import { kclManager } from 'lang/KclSingleton'
|
||||||
|
|
||||||
export const CodeMenu = ({ children }: PropsWithChildren) => {
|
export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||||
@ -77,6 +77,24 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
|
|||||||
</small>
|
</small>
|
||||||
</a>
|
</a>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<a
|
||||||
|
className={styles.button}
|
||||||
|
href="https://github.com/KittyCAD/kcl-samples"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<span>KCL samples</span>
|
||||||
|
<small>
|
||||||
|
On GitHub
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="ml-1 align-text-top"
|
||||||
|
width={12}
|
||||||
|
/>
|
||||||
|
</small>
|
||||||
|
</a>
|
||||||
|
</Menu.Item>
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
</div>
|
</div>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
@ -27,6 +27,7 @@ export const CommandBarProvider = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
|
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
|
||||||
|
devTools: true,
|
||||||
guards: {
|
guards: {
|
||||||
'Arguments are ready': (context, _) => {
|
'Arguments are ready': (context, _) => {
|
||||||
return context.selectedCommand?.args
|
return context.selectedCommand?.args
|
||||||
@ -56,12 +57,11 @@ export const CommandBarProvider = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<CommandBar />
|
|
||||||
</CommandsContext.Provider>
|
</CommandsContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommandBar = () => {
|
export const CommandBar = () => {
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
const {
|
const {
|
||||||
context: { selectedCommand, currentArgument, commands },
|
context: { selectedCommand, currentArgument, commands },
|
||||||
@ -83,17 +83,25 @@ const CommandBar = () => {
|
|||||||
if (commandBarState.matches('Review')) {
|
if (commandBarState.matches('Review')) {
|
||||||
const entries = Object.entries(selectedCommand?.args || {})
|
const entries = Object.entries(selectedCommand?.args || {})
|
||||||
|
|
||||||
commandBarSend({
|
const currentArgName = entries[entries.length - 1][0]
|
||||||
type: commandBarState.matches('Review')
|
const currentArg = {
|
||||||
? 'Edit argument'
|
name: currentArgName,
|
||||||
: 'Change current argument',
|
...entries[entries.length - 1][1],
|
||||||
data: {
|
}
|
||||||
arg: {
|
|
||||||
name: entries[entries.length - 1][0],
|
if (commandBarState.matches('Review')) {
|
||||||
...entries[entries.length - 1][1],
|
commandBarSend({
|
||||||
|
type: 'Edit argument',
|
||||||
|
data: {
|
||||||
|
arg: currentArg,
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
} else {
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Remove argument',
|
||||||
|
data: { [currentArgName]: currentArg },
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
commandBarSend({ type: 'Deselect command' })
|
commandBarSend({ type: 'Deselect command' })
|
||||||
}
|
}
|
||||||
@ -116,6 +124,11 @@ const CommandBar = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => console.log(commandBarState.context.argumentsToSubmit),
|
||||||
|
[commandBarState.context.argumentsToSubmit]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root
|
<Transition.Root
|
||||||
show={!commandBarState.matches('Closed') || false}
|
show={!commandBarState.matches('Closed') || false}
|
||||||
|