Compare commits

..

1 Commits

Author SHA1 Message Date
73129b9f1c Use HTMLMediaElement.play() on video assignment
This is a bit of a long-shot debugging attempt; but when I've inspected
the state of a frozen KCMA app, the stream had never been told to play.
I don't know that I've been able to interpret the debug messages.

There's an autoPlay set, but I'm not clear enough about the ordering of
how the events fire; so I'm going to add this to avoid doubt; and the
ancidata of smashing f5 has been helpful in seeing the play message get
to the stream now.

Signed-off-by: Paul R. Tagliamonte <paul@kittycad.io>
2023-08-22 14:12:32 -04:00
371 changed files with 13420 additions and 86974 deletions

View File

@ -1,3 +0,0 @@
[codespell]
ignore-words-list: crate,everytime
skip: **/target,node_modules,build

View File

@ -1,6 +1,3 @@
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000
VITE_KC_SENTRY_DSN=

View File

@ -1,6 +1,3 @@
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.kittycad.io
VITE_KC_SITE_BASE_URL=https://kittycad.io
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=15000
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
VITE_KC_SITE_BASE_URL=https://kittycad.io

View File

@ -1 +0,0 @@
src/wasm-lib/*

View File

@ -1,8 +1,4 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"plugins": [
"css-modules"
],
@ -15,16 +11,6 @@
"semi": [
"error",
"never"
],
"react-hooks/exhaustive-deps": "off",
"@typescript-eslint/no-floating-promises": "warn"
},
"overrides": [
{
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
"rules": {
"testing-library/prefer-screen-queries": "off"
}
}
]
]
}
}

View File

@ -15,9 +15,6 @@ on:
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-build.yml
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: cargo build
jobs:
cargobuild:
@ -25,9 +22,9 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
dir: ['src/wasm-lib']
dir: ['src/wasm-lib', 'src-tauri']
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install latest rust
uses: actions-rs/toolchain@v1

View File

@ -15,9 +15,6 @@ on:
- '**/rust-toolchain.toml'
- '**.rs'
- .github/workflows/cargo-build.yml
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: cargo clippy
jobs:
cargoclippy:
@ -25,9 +22,9 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
dir: ['src/wasm-lib']
dir: ['src/wasm-lib', 'src-tauri']
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install latest rust
uses: actions-rs/toolchain@v1
with:
@ -43,18 +40,7 @@ jobs:
- name: Rust Cache
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
run: |
cd "${{ matrix.dir }}"
cargo clippy --all --tests --benches -- -D warnings
cargo clippy --all --tests -- -D warnings

View File

@ -1,40 +0,0 @@
on:
push:
branches:
- main
paths:
- '**.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-criterion.yml
pull_request:
paths:
- '**.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-criterion.yml
workflow_dispatch:
permissions: read-all
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: cargo criterion
jobs:
cargocriterion:
name: cargo criterion
runs-on: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install dependencies
run: |
cargo install cargo-criterion
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: Benchmark kcl library
shell: bash
run: |-
cd src/wasm-lib/kcl; cargo criterion

View File

@ -18,9 +18,6 @@ on:
permissions:
packages: read
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: cargo fmt
jobs:
cargofmt:
@ -30,7 +27,7 @@ jobs:
matrix:
dir: ['src/wasm-lib', 'src-tauri']
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install latest rust
uses: actions-rs/toolchain@v1
with:

View File

@ -17,9 +17,6 @@ on:
- .github/workflows/cargo-test.yml
workflow_dispatch:
permissions: read-all
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: cargo test
jobs:
cargotest:
@ -27,9 +24,9 @@ jobs:
runs-on: ubuntu-latest-8-cores
strategy:
matrix:
dir: ['src/wasm-lib']
dir: ['src/wasm-lib', 'src-tauri']
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install latest rust
uses: actions-rs/toolchain@v1
with:
@ -44,22 +41,8 @@ jobs:
- uses: taiki-e/install-action@nextest
- name: Rust Cache
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
shell: bash
run: |-
cd "${{ matrix.dir }}"
cargo nextest run --workspace --no-fail-fast -P ci
env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
RUST_MIN_STACK: 10485760000
cargo llvm-cov nextest --lcov --output-path lcov.info --test-threads=1 --no-fail-fast

View File

@ -1,304 +1,159 @@
name: CI
name: CI
on:
pull_request:
push:
branches:
- main
- main
release:
types: [published]
schedule:
- cron: '0 4 * * *'
# Daily at 04:00 AM UTC
# Will checkout the last commit from the default branch (main as of 2023-10-04)
env:
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && contains(github.event.pull_request.title, 'Cut release v') }}
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
check-format:
runs-on: 'ubuntu-latest'
runs-on: 'ubuntu-20.04'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- run: yarn fmt-check
check-types:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn tsc
check-typos:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
- name: Install codespell
run: |
python -m pip install codespell
- name: Run codespell
run: codespell --config .codespellrc # Edit this file to tweak the typo list and other configuration.
build-test-web:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
outputs:
version: ${{ steps.export_version.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn tsc
- run: yarn simpleserver:ci
- run: yarn test:nowatch
- run: yarn test:cov
prepare-json-files:
runs-on: ubuntu-latest # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs:
version: ${{ steps.export_version.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Set nightly version
if: github.event_name == 'schedule'
run: |
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
echo "$(jq --arg url 'https://dl.kittycad.io/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
- uses: actions/upload-artifact@v3
if: github.event_name == 'schedule'
with:
path: |
package.json
src-tauri/tauri.conf.json
src-tauri/tauri.release.conf.json
- run: yarn test:rust
- id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
build-test-apps:
needs: [prepare-json-files]
build-apps:
needs: [check-format, build-test-web]
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
os: [macos-latest, ubuntu-20.04, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
- uses: actions/checkout@v3
- name: Copy updated .json files
if: github.event_name == 'schedule'
- name: install ubuntu system dependencies
if: matrix.os == 'ubuntu-20.04'
run: |
ls -l artifact
cp artifact/package.json package.json
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
- name: Install ubuntu system dependencies
if: matrix.os == 'ubuntu-latest'
run: >
sudo apt-get update &&
sudo apt-get install -y
libgtk-3-dev
libgtksourceview-3.0-dev
webkit2gtk-4.0
libappindicator3-dev
webkit2gtk-driver
xvfb
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
- name: Sync node version and setup cache
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: 'yarn' # Set this to npm, yarn or pnpm.
- run: yarn install
- name: Setup Rust
- name: Rust setup
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust cache
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Run build:wasm manually
- name: wasm prep
shell: bash
env:
MODE: ${{ env.BUILD_RELEASE == 'true' && '--release' || '--debug' }}
run: |
mkdir src/wasm-lib/pkg; cd src/wasm-lib
echo "building with ${{ env.MODE }}"
npx wasm-pack build --target web --out-dir pkg ${{ env.MODE }}
npx wasm-pack build --target web --out-dir pkg
cd ../../
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
- name: macos sed
if: matrix.os == 'macos-latest'
shell: bash
run: |
sed -i '' 's/import.meta.url//g' "./src/wasm-lib/pkg/wasm_lib.js"
- name: ubuntu and windows sed
if: matrix.os != 'macos-latest'
shell: bash
run: |
sed -i 's/import.meta.url//g' "./src/wasm-lib/pkg/wasm_lib.js"
- name: Fix format
run: yarn fmt
- name: Install Universal target (MacOS only)
if: matrix.os == 'macos-latest'
run: |
rustup target add aarch64-apple-darwin
- name: Prepare certificate and variables (Windows only)
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
shell: bash
- name: Setup certicate with SSM KSP (Windows only)
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
run: |
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
msiexec /i smtools-windows-x64.msi /quiet /qn
smksp_registrar.exe list
smctl.exe keypair ls
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
shell: cmd
- name: Build the app (debug)
- name: Build the app for the current platform (no upload)
uses: tauri-apps/tauri-action@v0
if: ${{ env.BUILD_RELEASE == 'false' }}
with:
includeRelease: false
includeDebug: true
args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
- name: Build the app (release) and sign
uses: tauri-apps/tauri-action@v0
if: ${{ env.BUILD_RELEASE == 'true' }}
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
with:
args: "${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
- uses: actions/upload-artifact@v3
env:
PREFIX: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
with:
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
- name: Run e2e tests (linux only)
if: matrix.os == 'ubuntu-latest'
run: |
cargo install tauri-driver
xvfb-run yarn test:e2e:tauri
env:
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/kittycad-modeling"
KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN }}
path: src-tauri/target/release/bundle/*/*
publish-apps-release:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
needs: [check-format, check-types, check-typos, build-test-web, prepare-json-files, build-test-apps]
runs-on: ubuntu-20.04
if: github.event_name == 'release'
needs: [build-test-web, build-apps]
env:
VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }}
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}
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) }}
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
steps:
- uses: actions/download-artifact@v3
- name: Generate the update static endpoint
run: |
ls -l artifact/*/*itty*
ls -l artifact
ls -l artifact/*
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`
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
WINDOWS_SIG=`cat artifact/nsis/*.nsis.zip.sig`
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg version "v${VERSION_NO_V}" \
--arg darwin_sig "$DARWIN_SIG" \
--arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
--arg darwin_url "$RELEASE_DIR/macos/kittycad-modeling-app.app.tar.gz" \
--arg linux_sig "$LINUX_SIG" \
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling-app_${VERSION_NO_V}_amd64.AppImage.tar.gz" \
--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/nsis/kittycad-modeling-app_${VERSION_NO_V}_x64-setup.nsis.zip" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"darwin-x86_64": {
"signature": $darwin_sig,
"url": $darwin_url
},
"darwin-aarch64": {
"signature": $darwin_sig,
"url": $darwin_url
},
"linux-x86_64": {
"signature": $linux_sig,
"url": $linux_url
@ -311,36 +166,8 @@ jobs:
}' > last_update.json
cat last_update.json
- name: Generate the download static endpoint
run: |
RELEASE_DIR=https://${BUCKET_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage" \
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"dmg-universal": {
"url": $darwin_url
},
"appimage-x86_64": {
"url": $linux_url
},
"msi-x86_64": {
"url": $windows_url
}
}
}' > last_download.json
cat last_download.json
- name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.0.0'
uses: 'google-github-actions/auth@v1.1.1'
with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
@ -348,29 +175,17 @@ jobs:
uses: google-github-actions/setup-gcloud@v1.1.1
with:
project_id: kittycadapi
- name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v1.0.3
with:
path: artifact
glob: '*/*itty*'
glob: '*/kittycad-modeling-app*'
parent: false
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }}
- name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v1.0.3
with:
path: last_update.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v1.0.3
with:
path: last_download.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload release files to Github
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v1
with:
files: artifact/*/*itty*
destination: dl.kittycad.io/releases/modeling-app

View File

@ -1,114 +0,0 @@
name: Playwright Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
playwright-ubuntu:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies
run: yarn
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache wasm
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: build wasm
run: yarn build:wasm
- name: build web
run: yarn build:local
- name: Run ubuntu/chrome snapshots
run: yarn playwright test --project="Google Chrome" --update-snapshots e2e/playwright/snapshot-tests.spec.ts
env:
CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: check for changes
id: git-check
run: |
git add .
if git status | grep -q "Changes to be committed"
then
echo "::set-output name=modified::true"
else
echo "::set-output name=modified::false"
fi
- name: Commit changes, if any
if: steps.git-check.outputs.modified == 'true'
run: |
git add .
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git fetch origin
echo ${{ github.head_ref }}
git checkout ${{ github.head_ref }}
# TODO when safari works on ubuntu remove the os part of the commit message
git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)" || true
git push
git push origin ${{ github.head_ref }}
- name: Run ubuntu/chrome flow
run: yarn playwright test --project="Google Chrome" e2e/playwright/flow-tests.spec.ts
env:
CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
playwright-macos:
timeout-minutes: 60
runs-on: macos-latest
needs: playwright-ubuntu
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies
run: yarn
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache wasm
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: build wasm
run: yarn build:wasm
- name: build web
run: yarn build:local
- name: Run macos/safari flow
# safari doesn't work on Ubuntu because of the same reason tauri doesn't (webRTC issues)
# TODO remove this and the matrix and run all tests on ubuntu when this is fixed
run: yarn playwright test --project="webkit" e2e/playwright/flow-tests.spec.ts
env:
CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

View File

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3.5.0
- shell: bash
run: |
# checkout our branch

14
.gitignore vendored
View File

@ -22,22 +22,8 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
.idea
.vscode
src/wasm-lib/.idea
src/wasm-lib/.vscode
# rust
src/wasm-lib/target
src/wasm-lib/bindings
src/wasm-lib/kcl/bindings
public/wasm_lib_bg.wasm
src/wasm-lib/lcov.info
e2e/playwright/playwright-secrets.env
e2e/playwright/temp1.png
e2e/playwright/temp2.png
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@ -5,9 +5,3 @@ coverage
# Ignore Rust projects:
*.rs
target
src/wasm-lib/pkg
src/wasm-lib/kcl/bindings
e2e/playwright/export-snapshots
# XState generated files
src/machines/modelingMachine.typegen.ts

21
LICENSE
View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2023 The KittyCAD Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

259
README.md
View File

@ -1,73 +1,48 @@
![KittyCAD Modeling App](/public/kcma-logomark.png)
## KittyCAD Modeling App
## Kurt demo project
live at [app.kittycad.io](https://app.kittycad.io/)
A CAD application from the future, brought to you by the [KittyCAD team](https://kittycad.io).
Not sure what to call this, it's both a language/interpreter and a UI that uses the language as the source of truth model the user build with direct-manipulation with the UI.
The KittyCAD modeling app is our take on what a modern modelling experience can be. It is applying several lessons learned in the decades since most major CAD tools came into existence:
It might make sense to split this repo up at some point, but not the lang and the UI are all togther in a react app
- All artifacts—including parts and assemblies—should be represented as human-readable code. At the end of the day, your CAD project should be "plain text"
- This makes version control—which is a solved problem in software engineering—trivial for CAD
- All GUI (or point-and-click) interactions should be actions performed on this code representation under the hood
- This unlocks a hybrid approach to modeling. Whether you point-and-click as you always have or you write your own KCL code, you are performing the same action in KittyCAD Modeling App
- Everything graphics _has_ to be built for the GPU
- Most CAD applications have had to retrofit support for GPUs, but our geometry engine is made for GPUs (primarily Nvidia's Vulkan), getting the order of magnitude rendering performance boost with it
- Make the resource-intensive pieces of an application auto-scaling
- One of the bottlenecks of today's hardware design tools is that they all rely on the local machine's resources to do the hardest parts, which include geometry rendering and analysis. Our geometry engine parallelizes rendering and just sends video frames back to the app (seriously, inspect source, it's just a `<video>` element), and our API will offload analysis as we build it in
Originally Presented on 10/01/2023
We are excited about what a small team of people could build in a short time with our API. We welcome you to try our API, build your own applications, or contribute to ours!
[Video](https://drive.google.com/file/d/183_wjqGdzZ8EEZXSqZ3eDcJocYPCyOdC/view?pli=1)
KittyCAD Modeling App is a _hybrid_ user interface for CAD modeling. You can point-and-click to design parts (and soon assemblies), but everything you make is really just [`kcl` code](https://github.com/KittyCAD/kcl-experiments) under the hood. All of your CAD models can be checked into source control such as GitHub and responsibly versioned, rolled back, and more.
[demo-slides.pdf](https://github.com/KittyCAD/Eng/files/10398178/demo.pdf)
The 3D view in KittyCAD Modeling App is just a video stream from our hosted geometry engine. The app sends new modeling commands to the engine via WebSockets, which returns back video frames of the view within the engine.
## Tools
- UI
- [React](https://react.dev/)
- [Headless UI](https://headlessui.com/)
- [TailwindCSS](https://tailwindcss.com/)
- [XState](https://xstate.js.org/)
- Networking
- WebSockets (via [KittyCAD TS client](https://github.com/KittyCAD/kittycad.ts))
- Code Editor
- [CodeMirror](https://codemirror.net/)
- Custom WASM LSP Server
- Modeling
- [KittyCAD TypeScript client](https://github.com/KittyCAD/kittycad.ts)
[Original demo video](https://drive.google.com/file/d/183_wjqGdzZ8EEZXSqZ3eDcJocYPCyOdC/view?pli=1)
[Original demo slides](https://github.com/KittyCAD/Eng/files/10398178/demo.pdf)
## Get started
We recommend downloading the latest application binary from [our Releases page](https://github.com/KittyCAD/modeling-app/releases). If you don't see your platform or architecture supported there, please file an issue.
## Running a development build
First, [install Rust via `rustup`](https://www.rust-lang.org/tools/install). This project uses a lot of Rust compiled to [WASM](https://webassembly.org/) within it. We always use the latest stable version of Rust, so you may need to run `rustup update stable`. Then, run:
## To run, there are a couple steps since we're compiling rust to WASM, you'll need to have rust stuff installed, then
```
yarn install
```
followed by:
then
```
yarn build:wasm-dev
yarn build:wasm
```
That will build the WASM binary and put in the `public` dir (though gitignored)
finally, to run the web app only, run:
finally
```
yarn start
```
and `yarn test` you would have need to have built the WASM previously. The tests need to download the binary from a server, so if you've already got `yarn start` running, that will work, otherwise running
```
yarn simpleserver
```
in one terminal
and
```
yarn test
```
in another.
If you want to edit the rust files, you can cd into `src/wasm-lib` and then use the usual rust commands, `cargo build`, `cargo test`, when you want to bring the changes back to the web-app, a fresh `yarn build:wasm` in the root will be needed.
Worth noting that the integration of the WASM into this project is very hacky because I'm really pushing create-react-app further than what's practical, but focusing on features atm rather than the setup.
## Developing in Chrome
Chrome is in the process of rolling out a new default which
@ -77,34 +52,13 @@ enable third-party cookies. You can enable third-party cookies by clicking on
the eye with a slash through it in the URL bar, and clicking on "Enable
Third-Party Cookies".
## Running tests
First, start the dev server following "Running a development build" above.
Then in another terminal tab, run:
```
yarn test
```
Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro/) tests, in interactive mode by default.
For running the rust (not tauri rust though) only, you can
```bash
cd src/wasm-lib
cargo test
```
but you will need to have install ffmpeg prior to.
## Tauri
To spin up up tauri dev, `yarn install` and `yarn build:wasm-dev` need to have been done before hand then
To spin up up tauri dev, `yarn install` and `yarn build:wasm` need to have been done before hand then
```
yarn tauri dev
```
Will spin up the web app before opening up the tauri dev desktop app. Note that it's probably a good idea to close the browser tab that gets opened since at the time of writing they can conflict.
Will spin up the web app before opening up the tauri dev desktop app. Note that it's probably a good idea to close the browser tab that gets opened since at the time of writting they can conflict.
The dev instance automatically opens up the browser devtools which can be disabled by [commenting it out](https://github.com/KittyCAD/modeling-app/blob/main/src-tauri/src/main.rs#L92.)
@ -113,175 +67,22 @@ To build, run `yarn tauri build`, or `yarn tauri build --debug` to keep access t
Note that these became separate apps on Macos, so make sure you open the right one after a build 😉
![image](https://github.com/KittyCAD/modeling-app/assets/29681384/a08762c5-8d16-42d8-a02f-a5efc9ae5551)
<img width="1232" alt="image" src="https://user-images.githubusercontent.com/29681384/211947063-46164bb4-7bdd-45cb-9a76-2f40c71a24aa.png">
<img width="1232" alt="image (1)" src="https://user-images.githubusercontent.com/29681384/211947073-e76b4933-bef5-4636-bc4d-e930ac8e290f.png">
## Before submitting a PR
Before you submit a contribution PR to this repo, please ensure that:
- There is a corresponding issue for the changes you want to make, so that discussion of approach can be had before work begins.
- You have separated out refactoring commits from feature commits as much as possible
- You have run all of the following commands locally:
- `yarn fmt`
- `yarn tsc`
- `yarn test`
- Here they are all together: `yarn fmt && yarn tsc && yarn test`
## Release a new version
1. Bump the versions in the .json files by creating a `Cut release v{x}.{y}.{z}` PR, committing the changes from
1. Bump the versions in the .json files by creating a `Bump to v{x}.{y}.{z}` PR, committing the changes from
```bash
VERSION=x.y.z yarn run bump-jsons
```
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
console.log(
'- ' +
Array.from(
document.querySelectorAll('[data-hovercard-type="pull_request"]')
).map((a) => `[${a.innerText}](${a.href})`).join(`
- `)
)
```
grab the md list and delete any that are older than the last bump
The PR may serve as a place to discuss the human-readable changelog and extra QA.
2. Merge the PR
3. Create a new release and tag pointing to the bump version commit using semantic versioning `v{x}.{y}.{z}`
4. A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, uploading artifacts to the release
## Fuzzing the parser
Make sure you install cargo fuzz:
```bash
$ cargo install cargo-fuzz
```
```bash
$ cd src/wasm-lib/kcl
# list the fuzz targets
$ cargo fuzz list
# run the parser fuzzer
$ cargo +nightly fuzz run parser
```
For more information on fuzzing you can check out
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).
### Playwright
First time running plawright locally, you'll need to add the secrets file
```bash
touch ./e2e/playwright/playwright-secrets.env
echo 'token="your-token"' > ./e2e/playwright/playwright-secrets.env
```
then replace "your-token" with a dev token from dev.kittycad.io/account/api-tokens
then:
run playwright
```
yarn playwright test
```
run a specific test suite
```
yarn playwright test src/e2e-tests/example.spec.ts
```
run a specific test change the test from `test('...` to `test.only('...`
(note if you commit this, the tests will instantly fail without running any of the tests)
run headed
```
yarn playwright test --headed
```
run with step through debugger
```
PWDEBUG=1 yarn playwright test
```
However, if you want a debugger I recommend using VSCode and the `playwright` extension, as the above command is a cruder debugger that steps into every function call which is annoying.
With the extension you can set a breakpoint after `waitForDefaultPlanesVisibilityChange` in order to skip app loading, then the vscode debugger's "step over" is much better for being able to stay at the right level of abstraction as you debug the code.
If you want to limit to a single browser use `--project="webkit"` or `firefox`, `Google Chrome`
Or comment out browsers in `playwright.config.ts`.
note chromium has encoder compat issues which is why were testing against the branded 'Google Chrome'
You may consider using the VSCode extension, it's useful for running individual threads, but some some reason the "record a test" is locked to chromium with we can't use. A work around is to us the CI `yarn playwright codegen -b wk --load-storage ./store localhost:3000`
<details>
<summary>
Where `./store` should look like this
</summary>
```JSON
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:3000",
"localStorage": [
{
"name": "store",
"value": "{\"state\":{\"openPanes\":[\"code\"]},\"version\":0}"
},
{
"name": "persistCode",
"value": ""
},
{
"name": "TOKEN_PERSIST_KEY",
"value": "your-token"
}
]
}
]
}
```
</details>
However because much of our tests involve clicking in the stream at specific locations, it's code-gen looks `await page.locator('video').click();` when really we need to use a pixel coord, so I think it's of limited use.
#### Some notes on CI
The tests are broken into snapshot tests and non-snapshot tests, and they run in that order, they automatically commit new snap shots, so if you see an image commit check it was an intended change. If we have non-determinism in the snapshots such that they are always committing new images, hopefully this annoyance makes us fix them asap, if you notice this happening let Kurt know. But for the odd occasion `git reset --hard HEAD~ && git push -f` is your friend.
How to interpret failing playwright tests?
If your tests fail, click through to the action and see that the tests failed on a line that includes `await page.getByTestId('loading').waitFor({ state: 'detached' })`, this means the test fail because the stream never started. It's you choice if you want to re-run the test, or ignore the failure.
We run on ubuntu and macos, because safari doesn't work on linux because of the dreaded "no RTCPeerConnection variable" error. But linux runs first and then macos for the same reason that we limit the number of parallel tests to 1 because we limit stream connections per user, so tests would start failing we if let them run together.
If something fails on CI you can download the artifact, unzip it and then open `playwright-report/data/<UUID>.zip` with https://trace.playwright.dev/ to see what happened.
#### Getting started writing a playwright test in our app
Besides following the instructions above and using the playwright docs, our app is weird because of the whole stream thing, which means our testing is weird. Because we've just figured out this stuff and therefore docs might go stale quick here's a 15min vid/tutorial
https://github.com/KittyCAD/modeling-app/assets/29681384/6f5e8e85-1003-4fd9-be7f-f36ce833942d
<details>
<summary>
Ps for the debug panel, the following JSON is useful for snapping the camera
</summary>
```JSON
{"type":"modeling_cmd_req","cmd_id":"054e5472-e5e9-4071-92d7-1ce3bac61956","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}}}
```
</details>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,75 +0,0 @@
# Types
`KCL` defines the following types and keywords the language.
All these types can be nested in various forms where nesting applies. Like
arrays can hold objects and vice versa.
## Boolean
`true` or `false` work when defining values.
## Variable declaration
Variables are defined with the `let` keyword like so:
```
let myBool = false
```
## Array
An array is defined with `[]` braces. What is inside the brackets can
be of any type. For example, the following is completely valid:
```
let myArray = ["thing", 2, false]
```
If you want to get a value from an array you can use the index like so:
`myArray[0]`.
## Object
An object is defined with `{}` braces. Here is an example object:
```
let myObj = {a: 0, b: "thing"}
```
We support two different ways of getting properties from objects, you can call
`myObj.a` or `myObj["a"]` both work.
## Functions
We also have support for defining your own functions. Functions can take in any
type of argument. Below is an example of the syntax:
```
fn myFn = (x) => {
return x
}
```
As you can see above `myFn` just returns whatever it is given.
## Binary expressions
You can also do math! Let's show an example below:
```
let myMathExpression = 3 + 1 * 2 / 3 - 7
```
You can nest expressions in parenthesis as well:
```
let myMathExpression = 3 + (1 * 2 / (3 - 7))
```
Please if you find any issues using any of the above expressions or syntax
please file an issue with the `ast` label on the [modeling-app
repo](https://github.com/KittyCAD/modeling-app/issues/new).

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -1,189 +0,0 @@
v 0 -4 0
v 0 0 0
v 0 -4 -1
v 0 0 -1
v 3.0950184 -4 -1
v 3.0950184 0 -1
v 5.9513144 -4 -3
v 5.9513144 0 -3
v 9.5 -4 -3
v 9.5 0 -3
v 9.5 -4 -2.5
v 9.5 0 -2.5
v 6.108964 -4 -2.5
v 6.108964 0 -2.5
v 3.4311862 -4 -0.625
v 4.323779 -4 -1.25
v 4.323779 -0 -1.25
v 3.4311862 -0 -0.625
v 2.5385938 0 0
v 2.5385938 -4 0
v 3.342784 -4 0.375
v 4.146974 -4 0.75
v 3.342784 -0 0.375
v 4.146974 -0 0.75
v 5.755354 0 1.5
v 5.755354 -4 1.5
v 9.5 -4 1.5
v 9.5 0 1.5
v 9.5 -4 2
v 9.5 0 2
v 5.644507 -4 2
v 5.644507 0 2
v 3.5 -4 1
v 3.5 0 1
v 0 -4 1
v 0 0 1
vt 0.0127 -0.0508
vt 0.0127 0.0508
vt -0.0127 -0.0508
vt -0.0127 0.0508
vt -0.039306734 0.0508
vt -0.039306734 -0.0508
vt 0.039306734 0.0508
vt 0.039306734 -0.0508
vt -0.04428355 0.0508
vt -0.04428355 -0.0508
vt 0.04428355 0.0508
vt 0.04428355 -0.0508
vt -0.045068305 0.0508
vt -0.045068305 -0.0508
vt 0.045068305 0.0508
vt 0.045068305 -0.0508
vt -0.00635 0.0508
vt -0.00635 -0.0508
vt 0.00635 0.0508
vt 0.00635 -0.0508
vt 0.04306616 -0.0508
vt 0.04306616 0.0508
vt -0.04306616 -0.0508
vt -0.04306616 0.0508
vt -0.027677217 -0.0508
vt 0.000000000000000048572257 -0.0508
vt 0.000000000000000048572257 0.0508
vt 0.055354435 -0.0508
vt 0.055354435 0.0508
vt -0.027677217 0.0508
vt -0.055354435 0.0508
vt -0.055354435 -0.0508
vt -0.02253807 0.0508
vt -0.04507614 0.0508
vt -0.04507614 -0.0508
vt 0.00000000000000005551115 0.0508
vt -0.02253807 -0.0508
vt 0.00000000000000005551115 -0.0508
vt 0.04507614 -0.0508
vt 0.04507614 0.0508
vt -0.047557 0.0508
vt -0.047557 -0.0508
vt 0.047557 0.0508
vt 0.047557 -0.0508
vt 0.04896476 -0.0508
vt 0.04896476 0.0508
vt -0.04896476 -0.0508
vt -0.04896476 0.0508
vt 0.03005076 -0.0508
vt 0.03005076 0.0508
vt -0.03005076 -0.0508
vt -0.03005076 0.0508
vt 0.04445 -0.0508
vt 0.04445 0.0508
vt -0.04445 -0.0508
vt -0.04445 0.0508
vt 0.08490671 0.009525
vt 0.06448028 0
vt 0.0889 0.0254
vt 0.08715213 -0.015875
vt 0.10982399 -0.03175
vt 0.07861347 -0.0254
vt 0.10533314 0.01905
vt 0.15116338 -0.0762
vt 0 -0.0254
vt 0 0
vt 0.2413 -0.0762
vt 0.15516768 -0.0635
vt 0.2413 -0.0635
vt 0.14337048 0.0508
vt 0.146186 0.0381
vt 0.2413 0.0381
vt 0.2413 0.0508
vt 0 0.0254
vn -1 -0 0
vn 0 -0 -1
vn -0.57357645 -0 -0.81915206
vn 1 -0 0
vn 0 -0 1
vn 0.57357645 -0 0.81915206
vn 0.42261827 -0 -0.9063078
vn -0.42261827 -0 0.9063078
vn -0 1 -0
vn 0 -1 0
o Unnamed-0
f 1/1/1 2/2/1 3/3/1
f 3/3/1 2/2/1 4/4/1
f 3/5/2 4/6/2 5/7/2
f 5/7/2 4/6/2 6/8/2
f 5/9/3 6/10/3 7/11/3
f 7/11/3 6/10/3 8/12/3
f 7/13/2 8/14/2 9/15/2
f 9/15/2 8/14/2 10/16/2
f 9/17/4 10/18/4 11/19/4
f 11/19/4 10/18/4 12/20/4
f 11/21/5 12/22/5 13/23/5
f 13/23/5 12/22/5 14/24/5
f 15/25/6 16/26/6 17/27/6
f 16/26/6 13/28/6 14/29/6
f 18/30/6 19/31/6 20/32/6
f 15/25/6 18/30/6 20/32/6
f 16/26/6 14/29/6 17/27/6
f 18/30/6 15/25/6 17/27/6
f 21/33/7 20/34/7 19/35/7
f 22/36/7 21/33/7 23/37/7
f 23/37/7 24/38/7 22/36/7
f 24/38/7 25/39/7 26/40/7
f 21/33/7 19/35/7 23/37/7
f 26/40/7 22/36/7 24/38/7
f 26/41/2 25/42/2 27/43/2
f 27/43/2 25/42/2 28/44/2
f 27/17/4 28/18/4 29/19/4
f 29/19/4 28/18/4 30/20/4
f 29/45/5 30/46/5 31/47/5
f 31/47/5 30/46/5 32/48/5
f 31/49/8 32/50/8 33/51/8
f 33/51/8 32/50/8 34/52/8
f 33/53/5 34/54/5 35/55/5
f 35/55/5 34/54/5 36/56/5
f 35/1/1 36/2/1 1/3/1
f 1/3/1 36/2/1 2/4/1
f 23/57/9 19/58/9 34/59/9
f 18/60/9 17/61/9 6/62/9
f 23/57/9 34/59/9 24/63/9
f 17/61/9 8/64/9 6/62/9
f 4/65/9 19/58/9 6/62/9
f 4/65/9 2/66/9 19/58/9
f 10/67/9 14/68/9 12/69/9
f 10/67/9 8/64/9 14/68/9
f 8/64/9 17/61/9 14/68/9
f 32/70/9 25/71/9 24/63/9
f 6/62/9 19/58/9 18/60/9
f 24/63/9 34/59/9 32/70/9
f 28/72/9 25/71/9 30/73/9
f 25/71/9 32/70/9 30/73/9
f 19/58/9 2/66/9 36/74/9
f 34/59/9 19/58/9 36/74/9
f 21/57/10 33/59/10 20/58/10
f 22/63/10 33/59/10 21/57/10
f 15/60/10 5/62/10 16/61/10
f 22/63/10 26/71/10 31/70/10
f 35/74/10 20/58/10 33/59/10
f 35/74/10 1/66/10 20/58/10
f 31/70/10 26/71/10 29/73/10
f 29/73/10 26/71/10 27/72/10
f 22/63/10 31/70/10 33/59/10
f 20/58/10 5/62/10 15/60/10
f 16/61/10 5/62/10 7/64/10
f 13/68/10 16/61/10 7/64/10
f 11/69/10 13/68/10 9/67/10
f 13/68/10 7/64/10 9/67/10
f 20/58/10 3/65/10 5/62/10
f 3/65/10 20/58/10 1/66/10

View File

@ -1,282 +0,0 @@
ply
format ascii 1.0
comment Generated by kittycad.io
element vertex 204
property float x
property float y
property float z
element face 68
property list uchar uint vertex_indices
end_header
0 0 4
0 0 0
0 -1 4
0 -1 4
0 0 0
0 -1 0
0 -1 4
0 -1 0
3.0950184 -1 4
3.0950184 -1 4
0 -1 0
3.0950184 -1 0
3.0950184 -1 4
3.0950184 -1 0
5.9513144 -3 4
5.9513144 -3 4
3.0950184 -1 0
5.9513144 -3 0
5.9513144 -3 4
5.9513144 -3 0
9.5 -3 4
9.5 -3 4
5.9513144 -3 0
9.5 -3 0
9.5 -3 4
9.5 -3 0
9.5 -2.5 4
9.5 -2.5 4
9.5 -3 0
9.5 -2.5 0
9.5 -2.5 4
9.5 -2.5 0
6.108964 -2.5 4
6.108964 -2.5 4
9.5 -2.5 0
6.108964 -2.5 0
3.4311862 -0.625 4
4.323779 -1.25 4
4.323779 -1.25 0
4.323779 -1.25 4
6.108964 -2.5 4
6.108964 -2.5 0
3.4311862 -0.625 0
2.5385938 0 0
2.5385938 0 4
3.4311862 -0.625 4
3.4311862 -0.625 0
2.5385938 0 4
4.323779 -1.25 4
6.108964 -2.5 0
4.323779 -1.25 0
3.4311862 -0.625 0
3.4311862 -0.625 4
4.323779 -1.25 0
3.342784 0.375 4
2.5385938 0 4
2.5385938 0 0
4.146974 0.75 4
3.342784 0.375 4
3.342784 0.375 0
3.342784 0.375 0
4.146974 0.75 0
4.146974 0.75 4
4.146974 0.75 0
5.755354 1.5 0
5.755354 1.5 4
3.342784 0.375 4
2.5385938 0 0
3.342784 0.375 0
5.755354 1.5 4
4.146974 0.75 4
4.146974 0.75 0
5.755354 1.5 4
5.755354 1.5 0
9.5 1.5 4
9.5 1.5 4
5.755354 1.5 0
9.5 1.5 0
9.5 1.5 4
9.5 1.5 0
9.5 2 4
9.5 2 4
9.5 1.5 0
9.5 2 0
9.5 2 4
9.5 2 0
5.644507 2 4
5.644507 2 4
9.5 2 0
5.644507 2 0
5.644507 2 4
5.644507 2 0
3.5 1 4
3.5 1 4
5.644507 2 0
3.5 1 0
3.5 1 4
3.5 1 0
0 1 4
0 1 4
3.5 1 0
0 1 0
0 1 4
0 1 0
0 0 4
0 0 4
0 1 0
0 0 0
3.342784 0.375 0
2.5385938 0 0
3.5 1 0
3.4311862 -0.625 0
4.323779 -1.25 0
3.0950184 -1 0
3.342784 0.375 0
3.5 1 0
4.146974 0.75 0
4.323779 -1.25 0
5.9513144 -3 0
3.0950184 -1 0
0 -1 0
2.5385938 0 0
3.0950184 -1 0
0 -1 0
0 0 0
2.5385938 0 0
9.5 -3 0
6.108964 -2.5 0
9.5 -2.5 0
9.5 -3 0
5.9513144 -3 0
6.108964 -2.5 0
5.9513144 -3 0
4.323779 -1.25 0
6.108964 -2.5 0
5.644507 2 0
5.755354 1.5 0
4.146974 0.75 0
3.0950184 -1 0
2.5385938 0 0
3.4311862 -0.625 0
4.146974 0.75 0
3.5 1 0
5.644507 2 0
9.5 1.5 0
5.755354 1.5 0
9.5 2 0
5.755354 1.5 0
5.644507 2 0
9.5 2 0
2.5385938 0 0
0 0 0
0 1 0
3.5 1 0
2.5385938 0 0
0 1 0
3.342784 0.375 4
3.5 1 4
2.5385938 0 4
4.146974 0.75 4
3.5 1 4
3.342784 0.375 4
3.4311862 -0.625 4
3.0950184 -1 4
4.323779 -1.25 4
4.146974 0.75 4
5.755354 1.5 4
5.644507 2 4
0 1 4
2.5385938 0 4
3.5 1 4
0 1 4
0 0 4
2.5385938 0 4
5.644507 2 4
5.755354 1.5 4
9.5 2 4
9.5 2 4
5.755354 1.5 4
9.5 1.5 4
4.146974 0.75 4
5.644507 2 4
3.5 1 4
2.5385938 0 4
3.0950184 -1 4
3.4311862 -0.625 4
4.323779 -1.25 4
3.0950184 -1 4
5.9513144 -3 4
6.108964 -2.5 4
4.323779 -1.25 4
5.9513144 -3 4
9.5 -2.5 4
6.108964 -2.5 4
9.5 -3 4
6.108964 -2.5 4
5.9513144 -3 4
9.5 -3 4
2.5385938 0 4
0 -1 4
3.0950184 -1 4
0 -1 4
2.5385938 0 4
0 0 4
3 0 1 2
3 3 4 5
3 6 7 8
3 9 10 11
3 12 13 14
3 15 16 17
3 18 19 20
3 21 22 23
3 24 25 26
3 27 28 29
3 30 31 32
3 33 34 35
3 36 37 38
3 39 40 41
3 42 43 44
3 45 46 47
3 48 49 50
3 51 52 53
3 54 55 56
3 57 58 59
3 60 61 62
3 63 64 65
3 66 67 68
3 69 70 71
3 72 73 74
3 75 76 77
3 78 79 80
3 81 82 83
3 84 85 86
3 87 88 89
3 90 91 92
3 93 94 95
3 96 97 98
3 99 100 101
3 102 103 104
3 105 106 107
3 108 109 110
3 111 112 113
3 114 115 116
3 117 118 119
3 120 121 122
3 123 124 125
3 126 127 128
3 129 130 131
3 132 133 134
3 135 136 137
3 138 139 140
3 141 142 143
3 144 145 146
3 147 148 149
3 150 151 152
3 153 154 155
3 156 157 158
3 159 160 161
3 162 163 164
3 165 166 167
3 168 169 170
3 171 172 173
3 174 175 176
3 177 178 179
3 180 181 182
3 183 184 185
3 186 187 188
3 189 190 191
3 192 193 194
3 195 196 197
3 198 199 200
3 201 202 203

View File

@ -1,494 +0,0 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((('kittycad.io export')), '2;1');
FILE_NAME('dump.step', '1970-01-01T00:00:00.0+00:00', ('Author unknown'), ('Organization unknown'), 'kittycad.io beta', 'kittycad.io', 'Authorization unknown');
FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF'));
ENDSEC;
DATA;
#1 = (
LENGTH_UNIT()
NAMED_UNIT(*)
SI_UNIT($, .METRE.)
);
#2 = UNCERTAINTY_MEASURE_WITH_UNIT(0.00001, #1, 'DISTANCE_ACCURACY_VALUE', $);
#3 = (
GEOMETRIC_REPRESENTATION_CONTEXT(3)
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#2))
GLOBAL_UNIT_ASSIGNED_CONTEXT((#1))
REPRESENTATION_CONTEXT('', '3D')
);
#4 = CARTESIAN_POINT('NONE', (0, 0, -0));
#5 = VERTEX_POINT('NONE', #4);
#6 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
#7 = VERTEX_POINT('NONE', #6);
#8 = CARTESIAN_POINT('NONE', (0, -0.0254, 0.1016));
#9 = VERTEX_POINT('NONE', #8);
#10 = CARTESIAN_POINT('NONE', (0, 0, 0.1016));
#11 = VERTEX_POINT('NONE', #10);
#12 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
#13 = VERTEX_POINT('NONE', #12);
#14 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, 0.1016));
#15 = VERTEX_POINT('NONE', #14);
#16 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, -0));
#17 = VERTEX_POINT('NONE', #16);
#18 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, 0.1016));
#19 = VERTEX_POINT('NONE', #18);
#20 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, -0));
#21 = VERTEX_POINT('NONE', #20);
#22 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, 0.1016));
#23 = VERTEX_POINT('NONE', #22);
#24 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
#25 = VERTEX_POINT('NONE', #24);
#26 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, 0.1016));
#27 = VERTEX_POINT('NONE', #26);
#28 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
#29 = VERTEX_POINT('NONE', #28);
#30 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, 0.1016));
#31 = VERTEX_POINT('NONE', #30);
#32 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
#33 = VERTEX_POINT('NONE', #32);
#34 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, 0.1016));
#35 = VERTEX_POINT('NONE', #34);
#36 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, -0));
#37 = VERTEX_POINT('NONE', #36);
#38 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, 0.1016));
#39 = VERTEX_POINT('NONE', #38);
#40 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, -0));
#41 = VERTEX_POINT('NONE', #40);
#42 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, 0.1016));
#43 = VERTEX_POINT('NONE', #42);
#44 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
#45 = VERTEX_POINT('NONE', #44);
#46 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, 0.1016));
#47 = VERTEX_POINT('NONE', #46);
#48 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
#49 = VERTEX_POINT('NONE', #48);
#50 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, 0.1016));
#51 = VERTEX_POINT('NONE', #50);
#52 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
#53 = VERTEX_POINT('NONE', #52);
#54 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, 0.1016));
#55 = VERTEX_POINT('NONE', #54);
#56 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
#57 = VERTEX_POINT('NONE', #56);
#58 = CARTESIAN_POINT('NONE', (0, 0.0254, 0.1016));
#59 = VERTEX_POINT('NONE', #58);
#60 = DIRECTION('NONE', (0, -1, 0));
#61 = VECTOR('NONE', #60, 0.0254);
#62 = CARTESIAN_POINT('NONE', (0, 0, -0));
#63 = LINE('NONE', #62, #61);
#64 = DIRECTION('NONE', (0, 0, 1));
#65 = VECTOR('NONE', #64, 0.1016);
#66 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
#67 = LINE('NONE', #66, #65);
#68 = DIRECTION('NONE', (0, -1, 0));
#69 = VECTOR('NONE', #68, 0.0254);
#70 = CARTESIAN_POINT('NONE', (0, 0, 0.1016));
#71 = LINE('NONE', #70, #69);
#72 = DIRECTION('NONE', (0, 0, 1));
#73 = VECTOR('NONE', #72, 0.1016);
#74 = CARTESIAN_POINT('NONE', (0, 0, -0));
#75 = LINE('NONE', #74, #73);
#76 = DIRECTION('NONE', (1, 0, 0));
#77 = VECTOR('NONE', #76, 0.07861346939195568);
#78 = CARTESIAN_POINT('NONE', (0, -0.0254, -0));
#79 = LINE('NONE', #78, #77);
#80 = DIRECTION('NONE', (0, 0, 1));
#81 = VECTOR('NONE', #80, 0.1016);
#82 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
#83 = LINE('NONE', #82, #81);
#84 = DIRECTION('NONE', (1, 0, 0));
#85 = VECTOR('NONE', #84, 0.07861346939195568);
#86 = CARTESIAN_POINT('NONE', (0, -0.0254, 0.1016));
#87 = LINE('NONE', #86, #85);
#88 = DIRECTION('NONE', (0.8191520442889919, -0.5735764363510459, 0));
#89 = VECTOR('NONE', #88, 0.08856709721755177);
#90 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, -0));
#91 = LINE('NONE', #90, #89);
#92 = DIRECTION('NONE', (0, 0, 1));
#93 = VECTOR('NONE', #92, 0.1016);
#94 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, -0));
#95 = LINE('NONE', #94, #93);
#96 = DIRECTION('NONE', (0.8191520442889919, -0.5735764363510459, 0));
#97 = VECTOR('NONE', #96, 0.08856709721755177);
#98 = CARTESIAN_POINT('NONE', (0.07861346939195568, -0.0254, 0.1016));
#99 = LINE('NONE', #98, #97);
#100 = DIRECTION('NONE', (1, -0.0000000000000003079278779307945, 0));
#101 = VECTOR('NONE', #100, 0.09013661186554489);
#102 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, -0));
#103 = LINE('NONE', #102, #101);
#104 = DIRECTION('NONE', (0, 0, 1));
#105 = VECTOR('NONE', #104, 0.1016);
#106 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, -0));
#107 = LINE('NONE', #106, #105);
#108 = DIRECTION('NONE', (1, -0.0000000000000003079278779307945, 0));
#109 = VECTOR('NONE', #108, 0.09013661186554489);
#110 = CARTESIAN_POINT('NONE', (0.1511633881344551, -0.07619999999999998, 0.1016));
#111 = LINE('NONE', #110, #109);
#112 = DIRECTION('NONE', (0, 1, 0));
#113 = VECTOR('NONE', #112, 0.012700000000000003);
#114 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, -0));
#115 = LINE('NONE', #114, #113);
#116 = DIRECTION('NONE', (0, 0, 1));
#117 = VECTOR('NONE', #116, 0.1016);
#118 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
#119 = LINE('NONE', #118, #117);
#120 = DIRECTION('NONE', (0, 1, 0));
#121 = VECTOR('NONE', #120, 0.012700000000000003);
#122 = CARTESIAN_POINT('NONE', (0.2413, -0.0762, 0.1016));
#123 = LINE('NONE', #122, #121);
#124 = DIRECTION('NONE', (-1, 0, 0));
#125 = VECTOR('NONE', #124, 0.08613231724678178);
#126 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, -0));
#127 = LINE('NONE', #126, #125);
#128 = DIRECTION('NONE', (0, 0, 1));
#129 = VECTOR('NONE', #128, 0.1016);
#130 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
#131 = LINE('NONE', #130, #129);
#132 = DIRECTION('NONE', (-1, 0, 0));
#133 = VECTOR('NONE', #132, 0.08613231724678178);
#134 = CARTESIAN_POINT('NONE', (0.2413, -0.0635, 0.1016));
#135 = LINE('NONE', #134, #133);
#136 = DIRECTION('NONE', (-0.8191520442889918, 0.573576436351046, 0));
#137 = VECTOR('NONE', #136, 0.11070887152193974);
#138 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, -0));
#139 = LINE('NONE', #138, #137);
#140 = DIRECTION('NONE', (0, 0, 1));
#141 = VECTOR('NONE', #140, 0.1016);
#142 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
#143 = LINE('NONE', #142, #141);
#144 = DIRECTION('NONE', (-0.8191520442889918, 0.573576436351046, 0));
#145 = VECTOR('NONE', #144, 0.11070887152193974);
#146 = CARTESIAN_POINT('NONE', (0.1551676827532182, -0.0635, 0.1016));
#147 = LINE('NONE', #146, #145);
#148 = DIRECTION('NONE', (0.90630778703665, 0.4226182617406993, 0));
#149 = VECTOR('NONE', #148, 0.09015228031811025);
#150 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, -0));
#151 = LINE('NONE', #150, #149);
#152 = DIRECTION('NONE', (0, 0, 1));
#153 = VECTOR('NONE', #152, 0.1016);
#154 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, -0));
#155 = LINE('NONE', #154, #153);
#156 = DIRECTION('NONE', (0.90630778703665, 0.4226182617406993, 0));
#157 = VECTOR('NONE', #156, 0.09015228031811025);
#158 = CARTESIAN_POINT('NONE', (0.06448028432509392, 0, 0.1016));
#159 = LINE('NONE', #158, #157);
#160 = DIRECTION('NONE', (1, -0.00000000000000007295344279228718, 0));
#161 = VECTOR('NONE', #160, 0.09511400200349182);
#162 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, -0));
#163 = LINE('NONE', #162, #161);
#164 = DIRECTION('NONE', (0, 0, 1));
#165 = VECTOR('NONE', #164, 0.1016);
#166 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, -0));
#167 = LINE('NONE', #166, #165);
#168 = DIRECTION('NONE', (1, -0.00000000000000007295344279228718, 0));
#169 = VECTOR('NONE', #168, 0.09511400200349182);
#170 = CARTESIAN_POINT('NONE', (0.14618599799650817, 0.03810000000000001, 0.1016));
#171 = LINE('NONE', #170, #169);
#172 = DIRECTION('NONE', (0, 1, 0));
#173 = VECTOR('NONE', #172, 0.012699999999999996);
#174 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, -0));
#175 = LINE('NONE', #174, #173);
#176 = DIRECTION('NONE', (0, 0, 1));
#177 = VECTOR('NONE', #176, 0.1016);
#178 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
#179 = LINE('NONE', #178, #177);
#180 = DIRECTION('NONE', (0, 1, 0));
#181 = VECTOR('NONE', #180, 0.012699999999999996);
#182 = CARTESIAN_POINT('NONE', (0.2413, 0.0381, 0.1016));
#183 = LINE('NONE', #182, #181);
#184 = DIRECTION('NONE', (-1, 0, 0));
#185 = VECTOR('NONE', #184, 0.0979295242190572);
#186 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, -0));
#187 = LINE('NONE', #186, #185);
#188 = DIRECTION('NONE', (0, 0, 1));
#189 = VECTOR('NONE', #188, 0.1016);
#190 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
#191 = LINE('NONE', #190, #189);
#192 = DIRECTION('NONE', (-1, 0, 0));
#193 = VECTOR('NONE', #192, 0.0979295242190572);
#194 = CARTESIAN_POINT('NONE', (0.2413, 0.0508, 0.1016));
#195 = LINE('NONE', #194, #193);
#196 = DIRECTION('NONE', (-0.9063077870366499, -0.42261826174069944, 0));
#197 = VECTOR('NONE', #196, 0.06010152021207346);
#198 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, -0));
#199 = LINE('NONE', #198, #197);
#200 = DIRECTION('NONE', (0, 0, 1));
#201 = VECTOR('NONE', #200, 0.1016);
#202 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
#203 = LINE('NONE', #202, #201);
#204 = DIRECTION('NONE', (-0.9063077870366499, -0.42261826174069944, 0));
#205 = VECTOR('NONE', #204, 0.06010152021207346);
#206 = CARTESIAN_POINT('NONE', (0.14337047578094278, 0.0508, 0.1016));
#207 = LINE('NONE', #206, #205);
#208 = DIRECTION('NONE', (-1, 0, 0));
#209 = VECTOR('NONE', #208, 0.08889999999999999);
#210 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, -0));
#211 = LINE('NONE', #210, #209);
#212 = DIRECTION('NONE', (0, 0, 1));
#213 = VECTOR('NONE', #212, 0.1016);
#214 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
#215 = LINE('NONE', #214, #213);
#216 = DIRECTION('NONE', (-1, 0, 0));
#217 = VECTOR('NONE', #216, 0.08889999999999999);
#218 = CARTESIAN_POINT('NONE', (0.08889999999999999, 0.0254, 0.1016));
#219 = LINE('NONE', #218, #217);
#220 = DIRECTION('NONE', (0, -1, 0));
#221 = VECTOR('NONE', #220, 0.0254);
#222 = CARTESIAN_POINT('NONE', (0, 0.0254, -0));
#223 = LINE('NONE', #222, #221);
#224 = DIRECTION('NONE', (0, -1, 0));
#225 = VECTOR('NONE', #224, 0.0254);
#226 = CARTESIAN_POINT('NONE', (0, 0.0254, 0.1016));
#227 = LINE('NONE', #226, #225);
#228 = EDGE_CURVE('NONE', #5, #7, #63, .T.);
#229 = EDGE_CURVE('NONE', #7, #9, #67, .T.);
#230 = EDGE_CURVE('NONE', #11, #9, #71, .T.);
#231 = EDGE_CURVE('NONE', #5, #11, #75, .T.);
#232 = EDGE_CURVE('NONE', #7, #13, #79, .T.);
#233 = EDGE_CURVE('NONE', #13, #15, #83, .T.);
#234 = EDGE_CURVE('NONE', #9, #15, #87, .T.);
#235 = EDGE_CURVE('NONE', #13, #17, #91, .T.);
#236 = EDGE_CURVE('NONE', #17, #19, #95, .T.);
#237 = EDGE_CURVE('NONE', #15, #19, #99, .T.);
#238 = EDGE_CURVE('NONE', #17, #21, #103, .T.);
#239 = EDGE_CURVE('NONE', #21, #23, #107, .T.);
#240 = EDGE_CURVE('NONE', #19, #23, #111, .T.);
#241 = EDGE_CURVE('NONE', #21, #25, #115, .T.);
#242 = EDGE_CURVE('NONE', #25, #27, #119, .T.);
#243 = EDGE_CURVE('NONE', #23, #27, #123, .T.);
#244 = EDGE_CURVE('NONE', #25, #29, #127, .T.);
#245 = EDGE_CURVE('NONE', #29, #31, #131, .T.);
#246 = EDGE_CURVE('NONE', #27, #31, #135, .T.);
#247 = EDGE_CURVE('NONE', #29, #33, #139, .T.);
#248 = EDGE_CURVE('NONE', #33, #35, #143, .T.);
#249 = EDGE_CURVE('NONE', #31, #35, #147, .T.);
#250 = EDGE_CURVE('NONE', #33, #37, #151, .T.);
#251 = EDGE_CURVE('NONE', #37, #39, #155, .T.);
#252 = EDGE_CURVE('NONE', #35, #39, #159, .T.);
#253 = EDGE_CURVE('NONE', #37, #41, #163, .T.);
#254 = EDGE_CURVE('NONE', #41, #43, #167, .T.);
#255 = EDGE_CURVE('NONE', #39, #43, #171, .T.);
#256 = EDGE_CURVE('NONE', #41, #45, #175, .T.);
#257 = EDGE_CURVE('NONE', #45, #47, #179, .T.);
#258 = EDGE_CURVE('NONE', #43, #47, #183, .T.);
#259 = EDGE_CURVE('NONE', #45, #49, #187, .T.);
#260 = EDGE_CURVE('NONE', #49, #51, #191, .T.);
#261 = EDGE_CURVE('NONE', #47, #51, #195, .T.);
#262 = EDGE_CURVE('NONE', #49, #53, #199, .T.);
#263 = EDGE_CURVE('NONE', #53, #55, #203, .T.);
#264 = EDGE_CURVE('NONE', #51, #55, #207, .T.);
#265 = EDGE_CURVE('NONE', #53, #57, #211, .T.);
#266 = EDGE_CURVE('NONE', #57, #59, #215, .T.);
#267 = EDGE_CURVE('NONE', #55, #59, #219, .T.);
#268 = EDGE_CURVE('NONE', #57, #5, #223, .T.);
#269 = EDGE_CURVE('NONE', #59, #11, #227, .T.);
#270 = ORIENTED_EDGE('NONE', *, *, #228, .T.);
#271 = ORIENTED_EDGE('NONE', *, *, #229, .T.);
#272 = ORIENTED_EDGE('NONE', *, *, #230, .F.);
#273 = ORIENTED_EDGE('NONE', *, *, #231, .F.);
#274 = EDGE_LOOP('NONE', (#270, #271, #272, #273));
#275 = ORIENTED_EDGE('NONE', *, *, #232, .T.);
#276 = ORIENTED_EDGE('NONE', *, *, #233, .T.);
#277 = ORIENTED_EDGE('NONE', *, *, #234, .F.);
#278 = ORIENTED_EDGE('NONE', *, *, #229, .F.);
#279 = EDGE_LOOP('NONE', (#275, #276, #277, #278));
#280 = ORIENTED_EDGE('NONE', *, *, #235, .T.);
#281 = ORIENTED_EDGE('NONE', *, *, #236, .T.);
#282 = ORIENTED_EDGE('NONE', *, *, #237, .F.);
#283 = ORIENTED_EDGE('NONE', *, *, #233, .F.);
#284 = EDGE_LOOP('NONE', (#280, #281, #282, #283));
#285 = ORIENTED_EDGE('NONE', *, *, #238, .T.);
#286 = ORIENTED_EDGE('NONE', *, *, #239, .T.);
#287 = ORIENTED_EDGE('NONE', *, *, #240, .F.);
#288 = ORIENTED_EDGE('NONE', *, *, #236, .F.);
#289 = EDGE_LOOP('NONE', (#285, #286, #287, #288));
#290 = ORIENTED_EDGE('NONE', *, *, #241, .T.);
#291 = ORIENTED_EDGE('NONE', *, *, #242, .T.);
#292 = ORIENTED_EDGE('NONE', *, *, #243, .F.);
#293 = ORIENTED_EDGE('NONE', *, *, #239, .F.);
#294 = EDGE_LOOP('NONE', (#290, #291, #292, #293));
#295 = ORIENTED_EDGE('NONE', *, *, #244, .T.);
#296 = ORIENTED_EDGE('NONE', *, *, #245, .T.);
#297 = ORIENTED_EDGE('NONE', *, *, #246, .F.);
#298 = ORIENTED_EDGE('NONE', *, *, #242, .F.);
#299 = EDGE_LOOP('NONE', (#295, #296, #297, #298));
#300 = ORIENTED_EDGE('NONE', *, *, #247, .T.);
#301 = ORIENTED_EDGE('NONE', *, *, #248, .T.);
#302 = ORIENTED_EDGE('NONE', *, *, #249, .F.);
#303 = ORIENTED_EDGE('NONE', *, *, #245, .F.);
#304 = EDGE_LOOP('NONE', (#300, #301, #302, #303));
#305 = ORIENTED_EDGE('NONE', *, *, #250, .T.);
#306 = ORIENTED_EDGE('NONE', *, *, #251, .T.);
#307 = ORIENTED_EDGE('NONE', *, *, #252, .F.);
#308 = ORIENTED_EDGE('NONE', *, *, #248, .F.);
#309 = EDGE_LOOP('NONE', (#305, #306, #307, #308));
#310 = ORIENTED_EDGE('NONE', *, *, #253, .T.);
#311 = ORIENTED_EDGE('NONE', *, *, #254, .T.);
#312 = ORIENTED_EDGE('NONE', *, *, #255, .F.);
#313 = ORIENTED_EDGE('NONE', *, *, #251, .F.);
#314 = EDGE_LOOP('NONE', (#310, #311, #312, #313));
#315 = ORIENTED_EDGE('NONE', *, *, #256, .T.);
#316 = ORIENTED_EDGE('NONE', *, *, #257, .T.);
#317 = ORIENTED_EDGE('NONE', *, *, #258, .F.);
#318 = ORIENTED_EDGE('NONE', *, *, #254, .F.);
#319 = EDGE_LOOP('NONE', (#315, #316, #317, #318));
#320 = ORIENTED_EDGE('NONE', *, *, #259, .T.);
#321 = ORIENTED_EDGE('NONE', *, *, #260, .T.);
#322 = ORIENTED_EDGE('NONE', *, *, #261, .F.);
#323 = ORIENTED_EDGE('NONE', *, *, #257, .F.);
#324 = EDGE_LOOP('NONE', (#320, #321, #322, #323));
#325 = ORIENTED_EDGE('NONE', *, *, #262, .T.);
#326 = ORIENTED_EDGE('NONE', *, *, #263, .T.);
#327 = ORIENTED_EDGE('NONE', *, *, #264, .F.);
#328 = ORIENTED_EDGE('NONE', *, *, #260, .F.);
#329 = EDGE_LOOP('NONE', (#325, #326, #327, #328));
#330 = ORIENTED_EDGE('NONE', *, *, #265, .T.);
#331 = ORIENTED_EDGE('NONE', *, *, #266, .T.);
#332 = ORIENTED_EDGE('NONE', *, *, #267, .F.);
#333 = ORIENTED_EDGE('NONE', *, *, #263, .F.);
#334 = EDGE_LOOP('NONE', (#330, #331, #332, #333));
#335 = ORIENTED_EDGE('NONE', *, *, #268, .T.);
#336 = ORIENTED_EDGE('NONE', *, *, #231, .T.);
#337 = ORIENTED_EDGE('NONE', *, *, #269, .F.);
#338 = ORIENTED_EDGE('NONE', *, *, #266, .F.);
#339 = EDGE_LOOP('NONE', (#335, #336, #337, #338));
#340 = ORIENTED_EDGE('NONE', *, *, #228, .T.);
#341 = ORIENTED_EDGE('NONE', *, *, #232, .T.);
#342 = ORIENTED_EDGE('NONE', *, *, #235, .T.);
#343 = ORIENTED_EDGE('NONE', *, *, #238, .T.);
#344 = ORIENTED_EDGE('NONE', *, *, #241, .T.);
#345 = ORIENTED_EDGE('NONE', *, *, #244, .T.);
#346 = ORIENTED_EDGE('NONE', *, *, #247, .T.);
#347 = ORIENTED_EDGE('NONE', *, *, #250, .T.);
#348 = ORIENTED_EDGE('NONE', *, *, #253, .T.);
#349 = ORIENTED_EDGE('NONE', *, *, #256, .T.);
#350 = ORIENTED_EDGE('NONE', *, *, #259, .T.);
#351 = ORIENTED_EDGE('NONE', *, *, #262, .T.);
#352 = ORIENTED_EDGE('NONE', *, *, #265, .T.);
#353 = ORIENTED_EDGE('NONE', *, *, #268, .T.);
#354 = EDGE_LOOP('NONE', (#340, #341, #342, #343, #344, #345, #346, #347, #348, #349, #350, #351, #352, #353));
#355 = ORIENTED_EDGE('NONE', *, *, #230, .T.);
#356 = ORIENTED_EDGE('NONE', *, *, #234, .T.);
#357 = ORIENTED_EDGE('NONE', *, *, #237, .T.);
#358 = ORIENTED_EDGE('NONE', *, *, #240, .T.);
#359 = ORIENTED_EDGE('NONE', *, *, #243, .T.);
#360 = ORIENTED_EDGE('NONE', *, *, #246, .T.);
#361 = ORIENTED_EDGE('NONE', *, *, #249, .T.);
#362 = ORIENTED_EDGE('NONE', *, *, #252, .T.);
#363 = ORIENTED_EDGE('NONE', *, *, #255, .T.);
#364 = ORIENTED_EDGE('NONE', *, *, #258, .T.);
#365 = ORIENTED_EDGE('NONE', *, *, #261, .T.);
#366 = ORIENTED_EDGE('NONE', *, *, #264, .T.);
#367 = ORIENTED_EDGE('NONE', *, *, #267, .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));
#370 = CARTESIAN_POINT('NONE', (0, -0.0127, 0.0508));
#371 = DIRECTION('NONE', (-1, 0, -0));
#372 = AXIS2_PLACEMENT_3D('NONE', #370, #371, $);
#373 = PLANE('NONE', #372);
#374 = CARTESIAN_POINT('NONE', (0.039306734695977924, -0.025399999999999995, 0.0508));
#375 = DIRECTION('NONE', (0, -1, -0));
#376 = AXIS2_PLACEMENT_3D('NONE', #374, #375, $);
#377 = PLANE('NONE', #376);
#378 = CARTESIAN_POINT('NONE', (0.11488842876320533, -0.05079999999999996, 0.05079999999999999));
#379 = DIRECTION('NONE', (-0.5735764363510459, -0.819152044288992, 0));
#380 = AXIS2_PLACEMENT_3D('NONE', #378, #379, $);
#381 = PLANE('NONE', #380);
#382 = CARTESIAN_POINT('NONE', (0.19623169406722757, -0.07619999999999999, 0.0508));
#383 = DIRECTION('NONE', (0, -1, -0));
#384 = AXIS2_PLACEMENT_3D('NONE', #382, #383, $);
#385 = PLANE('NONE', #384);
#386 = CARTESIAN_POINT('NONE', (0.2413, -0.06985, 0.0508));
#387 = DIRECTION('NONE', (1, 0, -0));
#388 = AXIS2_PLACEMENT_3D('NONE', #386, #387, $);
#389 = PLANE('NONE', #388);
#390 = CARTESIAN_POINT('NONE', (0.19823384137660915, -0.0635, 0.0508));
#391 = DIRECTION('NONE', (0, 1, -0));
#392 = AXIS2_PLACEMENT_3D('NONE', #390, #391, $);
#393 = PLANE('NONE', #392);
#394 = CARTESIAN_POINT('NONE', (0.10982398353915601, -0.03174999999999997, 0.0508));
#395 = DIRECTION('NONE', (0.573576436351046, 0.8191520442889918, -0));
#396 = AXIS2_PLACEMENT_3D('NONE', #394, #395, $);
#397 = PLANE('NONE', #396);
#398 = CARTESIAN_POINT('NONE', (0.105333141160801, 0.019049999999999987, 0.0508));
#399 = DIRECTION('NONE', (0.4226182617406993, -0.90630778703665, -0));
#400 = AXIS2_PLACEMENT_3D('NONE', #398, #399, $);
#401 = PLANE('NONE', #400);
#402 = CARTESIAN_POINT('NONE', (0.19374299899825406, 0.0381, 0.0508));
#403 = DIRECTION('NONE', (0, -1, -0));
#404 = AXIS2_PLACEMENT_3D('NONE', #402, #403, $);
#405 = PLANE('NONE', #404);
#406 = CARTESIAN_POINT('NONE', (0.2413, 0.044449999999999996, 0.0508));
#407 = DIRECTION('NONE', (1, 0, -0));
#408 = AXIS2_PLACEMENT_3D('NONE', #406, #407, $);
#409 = PLANE('NONE', #408);
#410 = CARTESIAN_POINT('NONE', (0.19233523789047138, 0.0508, 0.0508));
#411 = DIRECTION('NONE', (0, 1, -0));
#412 = AXIS2_PLACEMENT_3D('NONE', #410, #411, $);
#413 = PLANE('NONE', #412);
#414 = CARTESIAN_POINT('NONE', (0.11613523789047137, 0.0381, 0.05079999999999999));
#415 = DIRECTION('NONE', (-0.42261826174069966, 0.90630778703665, -0));
#416 = AXIS2_PLACEMENT_3D('NONE', #414, #415, $);
#417 = PLANE('NONE', #416);
#418 = CARTESIAN_POINT('NONE', (0.044449999999999996, 0.0254, 0.0508));
#419 = DIRECTION('NONE', (0, 1, -0));
#420 = AXIS2_PLACEMENT_3D('NONE', #418, #419, $);
#421 = PLANE('NONE', #420);
#422 = CARTESIAN_POINT('NONE', (0, 0.0127, 0.0508));
#423 = DIRECTION('NONE', (-1, 0, -0));
#424 = AXIS2_PLACEMENT_3D('NONE', #422, #423, $);
#425 = PLANE('NONE', #424);
#426 = CARTESIAN_POINT('NONE', (0, 0, -0));
#427 = DIRECTION('NONE', (0, 0, 1));
#428 = AXIS2_PLACEMENT_3D('NONE', #426, #427, $);
#429 = PLANE('NONE', #428);
#430 = CARTESIAN_POINT('NONE', (0, 0, 0.1016));
#431 = DIRECTION('NONE', (0, 0, 1));
#432 = AXIS2_PLACEMENT_3D('NONE', #430, #431, $);
#433 = PLANE('NONE', #432);
#434 = FACE_OUTER_BOUND('NONE', #274, .T.);
#435 = ADVANCED_FACE('NONE', (#434), #373, .T.);
#436 = FACE_OUTER_BOUND('NONE', #279, .T.);
#437 = ADVANCED_FACE('NONE', (#436), #377, .T.);
#438 = FACE_OUTER_BOUND('NONE', #284, .T.);
#439 = ADVANCED_FACE('NONE', (#438), #381, .T.);
#440 = FACE_OUTER_BOUND('NONE', #289, .T.);
#441 = ADVANCED_FACE('NONE', (#440), #385, .T.);
#442 = FACE_OUTER_BOUND('NONE', #294, .T.);
#443 = ADVANCED_FACE('NONE', (#442), #389, .T.);
#444 = FACE_OUTER_BOUND('NONE', #299, .T.);
#445 = ADVANCED_FACE('NONE', (#444), #393, .T.);
#446 = FACE_OUTER_BOUND('NONE', #304, .T.);
#447 = ADVANCED_FACE('NONE', (#446), #397, .T.);
#448 = FACE_OUTER_BOUND('NONE', #309, .T.);
#449 = ADVANCED_FACE('NONE', (#448), #401, .T.);
#450 = FACE_OUTER_BOUND('NONE', #314, .T.);
#451 = ADVANCED_FACE('NONE', (#450), #405, .T.);
#452 = FACE_OUTER_BOUND('NONE', #319, .T.);
#453 = ADVANCED_FACE('NONE', (#452), #409, .T.);
#454 = FACE_OUTER_BOUND('NONE', #324, .T.);
#455 = ADVANCED_FACE('NONE', (#454), #413, .T.);
#456 = FACE_OUTER_BOUND('NONE', #329, .T.);
#457 = ADVANCED_FACE('NONE', (#456), #417, .T.);
#458 = FACE_OUTER_BOUND('NONE', #334, .T.);
#459 = ADVANCED_FACE('NONE', (#458), #421, .T.);
#460 = FACE_OUTER_BOUND('NONE', #339, .T.);
#461 = ADVANCED_FACE('NONE', (#460), #425, .T.);
#462 = FACE_OUTER_BOUND('NONE', #354, .T.);
#463 = ADVANCED_FACE('NONE', (#462), #429, .F.);
#464 = FACE_OUTER_BOUND('NONE', #369, .T.);
#465 = ADVANCED_FACE('NONE', (#464), #433, .T.);
#466 = CLOSED_SHELL('NONE', (#435, #437, #439, #441, #443, #445, #447, #449, #451, #453, #455, #457, #459, #461, #463, #465));
#467 = ORIENTED_CLOSED_SHELL('NONE', *, #466, .T.);
#468 = MANIFOLD_SOLID_BREP('NONE', #467);
#469 = APPLICATION_CONTEXT('configuration controlled 3D design of mechanical parts and assemblies');
#470 = PRODUCT_DEFINITION_CONTEXT('part definition', #469, 'design');
#471 = PRODUCT('UNIDENTIFIED_PRODUCT', 'NONE', $, ());
#472 = PRODUCT_DEFINITION_FORMATION('', $, #471);
#473 = PRODUCT_DEFINITION('design', $, #472, #470);
#474 = PRODUCT_DEFINITION_SHAPE('NONE', $, #473);
#475 = ADVANCED_BREP_SHAPE_REPRESENTATION('NONE', (#468), #3);
#476 = SHAPE_DEFINITION_REPRESENTATION(#474, #475);
ENDSEC;
END-ISO-10303-21;

View File

@ -1,478 +0,0 @@
solid unnamed
facet normal -1 0 0
outer loop
vertex 0 -4 0
vertex 0 -0 0
vertex 0 -4 -1
endloop
endfacet
facet normal -1 0 0
outer loop
vertex 0 -4 -1
vertex 0 -0 0
vertex 0 -0 -1
endloop
endfacet
facet normal 0 0 -1
outer loop
vertex 0 -4 -1
vertex 0 -0 -1
vertex 3.0950184 -4 -1
endloop
endfacet
facet normal 0 0 -1
outer loop
vertex 3.0950184 -4 -1
vertex 0 -0 -1
vertex 3.0950184 -0 -1
endloop
endfacet
facet normal -0.57357645 0 -0.81915206
outer loop
vertex 3.0950184 -4 -1
vertex 3.0950184 -0 -1
vertex 5.9513144 -4 -3
endloop
endfacet
facet normal -0.57357645 0 -0.81915206
outer loop
vertex 5.9513144 -4 -3
vertex 3.0950184 -0 -1
vertex 5.9513144 -0 -3
endloop
endfacet
facet normal 0 0 -1
outer loop
vertex 5.9513144 -4 -3
vertex 5.9513144 -0 -3
vertex 9.5 -4 -3
endloop
endfacet
facet normal 0 0 -1
outer loop
vertex 9.5 -4 -3
vertex 5.9513144 -0 -3
vertex 9.5 -0 -3
endloop
endfacet
facet normal 1 0 0
outer loop
vertex 9.5 -4 -3
vertex 9.5 -0 -3
vertex 9.5 -4 -2.5
endloop
endfacet
facet normal 1 -0 0
outer loop
vertex 9.5 -4 -2.5
vertex 9.5 -0 -3
vertex 9.5 -0 -2.5
endloop
endfacet
facet normal 0 -0 0.99999994
outer loop
vertex 9.5 -4 -2.5
vertex 9.5 -0 -2.5
vertex 6.108964 -4 -2.5
endloop
endfacet
facet normal 0 0 0.99999994
outer loop
vertex 6.108964 -4 -2.5
vertex 9.5 -0 -2.5
vertex 6.108964 -0 -2.5
endloop
endfacet
facet normal 0.5735763 0 0.8191522
outer loop
vertex 3.4311862 -4 -0.625
vertex 4.323779 -4 -1.25
vertex 4.323779 -0 -1.25
endloop
endfacet
facet normal 0.57357645 0 0.819152
outer loop
vertex 4.323779 -4 -1.25
vertex 6.108964 -4 -2.5
vertex 6.108964 -0 -2.5
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
endfacet
facet normal 0 -0 1
outer loop
vertex 9.5 -4 2
vertex 9.5 -0 2
vertex 5.644507 -4 2
endloop
endfacet
facet normal 0 0 1
outer loop
vertex 5.644507 -4 2
vertex 9.5 -0 2
vertex 5.644507 -0 2
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
endfacet
facet normal -1 0 0
outer loop
vertex 0 -4 1
vertex 0 -0 1
vertex 0 -4 0
endloop
endfacet
facet normal -1 0 0
outer loop
vertex 0 -4 0
vertex 0 -0 1
vertex 0 -0 0
endloop
endfacet
facet normal 0 1 -0
outer loop
vertex 3.342784 -0 0.375
vertex 2.5385938 -0 0
vertex 3.5 -0 1
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 3.4311862 -0 -0.625
vertex 4.323779 -0 -1.25
vertex 3.0950184 -0 -1
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 3.342784 -0 0.375
vertex 3.5 -0 1
vertex 4.146974 -0 0.75
endloop
endfacet
facet normal 0 0.99999994 0
outer loop
vertex 4.323779 -0 -1.25
vertex 5.9513144 -0 -3
vertex 3.0950184 -0 -1
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 0 -0 -1
vertex 2.5385938 -0 0
vertex 3.0950184 -0 -1
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 0 -0 -1
vertex 0 -0 0
vertex 2.5385938 -0 0
endloop
endfacet
facet normal 0 0.99999994 -0
outer loop
vertex 9.5 -0 -3
vertex 6.108964 -0 -2.5
vertex 9.5 -0 -2.5
endloop
endfacet
facet normal 0 1 0
outer loop
vertex 9.5 -0 -3
vertex 5.9513144 -0 -3
vertex 6.108964 -0 -2.5
endloop
endfacet
facet normal 0 1 -0
outer loop
vertex 5.9513144 -0 -3
vertex 4.323779 -0 -1.25
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
endfacet
facet normal -0 -1 0
outer loop
vertex 3.342784 -4 0.375
vertex 3.5 -4 1
vertex 2.5385938 -4 0
endloop
endfacet
facet normal -0 -1 0
outer loop
vertex 4.146974 -4 0.75
vertex 3.5 -4 1
vertex 3.342784 -4 0.375
endloop
endfacet
facet normal 0 -1 -0
outer loop
vertex 3.4311862 -4 -0.625
vertex 3.0950184 -4 -1
vertex 4.323779 -4 -1.25
endloop
endfacet
facet normal 0 -0.99999994 0
outer loop
vertex 4.146974 -4 0.75
vertex 5.755354 -4 1.5
vertex 5.644507 -4 2
endloop
endfacet
facet normal 0 -1 0
outer loop
vertex 0 -4 1
vertex 2.5385938 -4 0
vertex 3.5 -4 1
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
endfacet
facet normal -0 -1 0
outer loop
vertex 6.108964 -4 -2.5
vertex 4.323779 -4 -1.25
vertex 5.9513144 -4 -3
endloop
endfacet
facet normal -0 -0.99999994 -0
outer loop
vertex 9.5 -4 -2.5
vertex 6.108964 -4 -2.5
vertex 9.5 -4 -3
endloop
endfacet
facet normal 0 -1 -0
outer loop
vertex 6.108964 -4 -2.5
vertex 5.9513144 -4 -3
vertex 9.5 -4 -3
endloop
endfacet
facet normal 0 -1 -0
outer loop
vertex 2.5385938 -4 0
vertex 0 -4 -1
vertex 3.0950184 -4 -1
endloop
endfacet
facet normal 0 -1 0
outer loop
vertex 0 -4 -1
vertex 2.5385938 -4 0
vertex 0 -4 0
endloop
endfacet
endsolid unnamed

View File

@ -1,615 +0,0 @@
import { test, expect } from '@playwright/test'
import { secrets } from './secrets'
import { EngineCommand } from '../../src/lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid'
import { getUtils } from './test-utils'
import waitOn from 'wait-on'
import { Models } from '@kittycad/lib'
import fsp from 'fs/promises'
/*
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
just from the nature of the stream, running the test with debugger and pasting the below
into the console can be useful to get coords
document.addEventListener('mousemove', (e) =>
console.log(`await page.mouse.click(${e.clientX}, ${e.clientY})`)
)
*/
test.beforeEach(async ({ context, page }) => {
// wait for Vite preview server to be up
await waitOn({
resources: ['tcp:3000'],
timeout: 5000,
})
await context.addInitScript(async (token) => {
localStorage.setItem('TOKEN_PERSIST_KEY', token)
localStorage.setItem('persistCode', ``)
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',
})
)
}, secrets.token)
// kill animations, speeds up tests and reduced flakiness
await page.emulateMedia({ reducedMotion: 'reduce' })
})
test.setTimeout(60000)
test('Basic sketch', 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 u.waitForDefaultPlanesVisibilityChange()
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
// click on "Start Sketch" button
await u.clearCommandLogs()
await Promise.all([
u.doAndWaitForImageDiff(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
200
),
u.waitForDefaultPlanesVisibilityChange(),
])
// select a plane
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter')
await u.waitForCmdReceive('set_tool')
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
)
const startXPx = 600
await u.doAndWaitForCmd(
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10),
'mouse_click',
false
)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
const startAt = '[10.97, -14.79]'
const tenish = '11.07'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${tenish}, 0], %)`)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${tenish}, 0], %)
|> line([0, ${tenish}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${tenish}, 0], %)
|> line([0, ${tenish}], %)
|> line([-22.04, 0], %)`)
// deselect line tool
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
)
// click between first two clicks to get center of the line
await u.doAndWaitForCmd(
() => page.mouse.click(startXPx + PUR * 15, 500 - PUR * 10),
'select_with_point'
)
await u.closeDebugPanel()
// hold down shift
await page.keyboard.down('Shift')
// click between the latest two clicks to get center of the line
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 20)
// selected two lines therefore there should be two cursors
await expect(page.locator('.cm-cursor')).toHaveCount(2)
await page.getByRole('button', { name: 'Equal Length' }).click()
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line({ to: [${tenish}, 0], tag: 'seg01' }, %)
|> line([0, ${tenish}], %)
|> angledLine([180, segLen('seg01', %)], %)`)
})
test('if you write invalid kcl you get inlined errors', async ({ page }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
/* add the following code to the editor (# error is not a valid line)
# error
const topAng = 30
const bottomAng = 25
*/
await page.click('.cm-content')
await page.keyboard.type('# error')
// press arrows to clear autocomplete
await page.keyboard.press('ArrowLeft')
await page.keyboard.press('ArrowRight')
await page.keyboard.press('Enter')
await page.keyboard.type('const topAng = 30')
await page.keyboard.press('Enter')
await page.keyboard.type('const bottomAng = 25')
await page.keyboard.press('Enter')
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
await expect(page.getByText("found unknown token '#'")).toBeVisible()
// select the line that's causing the error and delete it
await page.getByText('# error').click()
await page.keyboard.press('End')
await page.keyboard.down('Shift')
await page.keyboard.press('Home')
await page.keyboard.up('Shift')
await page.keyboard.press('Backspace')
// wait for .cm-lint-marker-error not to be visible
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
})
test('executes on load', async ({ page, context }) => {
const u = getUtils(page)
await context.addInitScript(async (token) => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %)
|> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %)`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// expand variables section
await page.getByText('Variables').click()
// can find part001 in the variables summary (pretty-json-container, makes sure we're not looking in the code editor)
// part001 only shows up in the variables summary if it's been executed
await page.waitForFunction(() => {
const variablesElement = document.querySelector(
'.pretty-json-container'
) as HTMLDivElement
return variablesElement.innerHTML.includes('part001')
})
await expect(
page.locator('.pretty-json-container >> text=part001')
).toBeVisible()
})
test('re-executes', async ({ page, context }) => {
const u = getUtils(page)
await context.addInitScript(async (token) => {
localStorage.setItem('persistCode', `const myVar = 5`)
})
await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await page.getByText('Variables').click()
// expect to see "myVar:5"
await expect(
page.locator('.pretty-json-container >> text=myVar:5')
).toBeVisible()
// change 5 to 67
await page.getByText('const myVar').click()
await page.keyboard.press('End')
await page.keyboard.press('Backspace')
await page.keyboard.type('67')
await expect(
page.locator('.pretty-json-container >> text=myVar:67')
).toBeVisible()
})
test('Can create sketches on all planes and their back sides', async ({
page,
}) => {
const u = getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.waitForDefaultPlanesVisibilityChange()
const camCmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 15, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
vantage: { x: 30, y: 30, z: 30 },
},
}
const TestSinglePlane = async ({
viewCmd,
expectedCode,
clickCoords,
}: {
viewCmd: EngineCommand
expectedCode: string
clickCoords: { x: number; y: number }
}) => {
await u.openDebugPanel()
await u.sendCustomCmd(viewCmd)
await u.clearCommandLogs()
// await page.waitForTimeout(200)
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel()
await page.mouse.click(clickCoords.x, clickCoords.y)
await u.openDebugPanel()
await expect(page.getByRole('button', { name: 'Line' })).toBeVisible()
// draw a line
const startXPx = 600
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Line' }).click()
await u.waitForCmdReceive('set_tool')
await u.clearCommandLogs()
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await u.openDebugPanel()
await u.waitForCmdReceive('mouse_click')
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await u.openDebugPanel()
await expect(page.locator('.cm-content')).toHaveText(expectedCode)
await page.getByRole('button', { name: 'Line' }).click()
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearCommandLogs()
await u.removeCurrentCode()
}
const codeTemplate = (
plane = 'XY',
sign = ''
) => `const part001 = startSketchOn('${plane}')
|> startProfileAt([${sign}6.88, -9.29], %)
|> line([${sign}6.95, 0], %)`
await TestSinglePlane({
viewCmd: camCmd,
expectedCode: codeTemplate('XY'),
clickCoords: { x: 700, y: 350 }, // red plane
})
await TestSinglePlane({
viewCmd: camCmd,
expectedCode: codeTemplate('YZ'),
clickCoords: { x: 1000, y: 200 }, // green plane
})
await TestSinglePlane({
viewCmd: camCmd,
expectedCode: codeTemplate('XZ', '-'),
clickCoords: { x: 630, y: 130 }, // blue plane
})
// new camera angle to click the back side of all three planes
const camCmdBackSide: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: -15, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
vantage: { x: -30, y: -30, z: -30 },
},
}
await TestSinglePlane({
viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-XY', '-'),
clickCoords: { x: 705, y: 136 }, // back of red plane
})
await TestSinglePlane({
viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-YZ', '-'),
clickCoords: { x: 1000, y: 350 }, // back of green plane
})
await TestSinglePlane({
viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-XZ'),
clickCoords: { x: 600, y: 400 }, // back of blue plane
})
})
test('Auto complete works', async ({ page }) => {
const u = getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.waitForDefaultPlanesVisibilityChange()
// this test might be brittle as we add and remove functions
// but should also be easy to update.
// tests clicking on an option, selection the first option
// and arrowing down to an option
await page.click('.cm-content')
await page.keyboard.type('const part001 = start')
// expect there to be three auto complete options
await expect(page.locator('.cm-completionLabel')).toHaveCount(3)
await page.getByText('startSketchOn').click()
await page.keyboard.type("('XY')")
await page.keyboard.press('Enter')
await page.keyboard.type(' |> startProfi')
// expect there be a single auto complete option that we can just hit enter on
await expect(page.locator('.cm-completionLabel')).toBeVisible()
await page.keyboard.press('Enter') // accepting the auto complete, not a new line
await page.keyboard.type('([0,0], %)')
await page.keyboard.press('Enter')
await page.keyboard.type(' |> lin')
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible()
// press arrow down twice then enter to accept xLine
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter')
await page.keyboard.type('(5, %)')
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('XY')
|> startProfileAt([0,0], %)
|> xLine(5, %)`)
})
// Onboarding tests
test('Onboarding redirects and code updating', async ({ page, context }) => {
const u = getUtils(page)
// Override beforeEach test setup
await context.addInitScript(async () => {
// Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', 'const sigmaAllow = 15000')
const storedSettings = JSON.parse(
localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
)
storedSettings.onboardingStatus = '/export'
localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings))
})
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// Test that the redirect happened
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/new/onboarding/export`
)
// Test that you come back to this page when you refresh
await page.reload()
await expect(page.url().split(':3000').slice(-1)[0]).toBe(
`/file/new/onboarding/export`
)
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
// Test that the code changes when you advance to the next step
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText('')
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
})
test('Selections work on fresh and edited sketch', async ({ page }) => {
// tests mapping works on fresh sketch and edited sketch
// tests using hovers which is the same as selections, because if
// source ranges are wrong, hovers won't work
const u = getUtils(page)
const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.waitForDefaultPlanesVisibilityChange()
const xAxisClick = () => page.mouse.click(700, 250)
const emptySpaceClick = () => page.mouse.click(700, 300)
const topHorzSegmentClick = () => page.mouse.click(700, 285)
const bottomHorzSegmentClick = () => page.mouse.click(750, 393)
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
// select a plane
await u.doAndWaitForCmd(() => page.mouse.click(700, 200), 'edit_mode_enter')
await u.waitForCmdReceive('set_tool')
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
)
const startXPx = 600
await u.doAndWaitForCmd(
() => page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10),
'mouse_click',
false
)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
const startAt = '[10.97, -14.79]'
const tenish = '11.07'
const twentyish = '22.04'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${tenish}, 0], %)`)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${tenish}, 0], %)
|> line([0, ${tenish}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${tenish}, 0], %)
|> line([0, ${tenish}], %)
|> line([-${twentyish}, 0], %)`)
// deselect line tool
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Line' }).click(),
'set_tool'
)
await u.closeDebugPanel()
const selectionSequence = async () => {
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
await expect(page.getByTestId('hover-highlight')).toBeVisible()
// bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience
// and will be an easy fix if it breaks because we change the colour
await expect(page.locator('.bg-yellow-200')).toBeVisible()
// check mousing off, than mousing onto another line
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 20) // mouse onto another line
await expect(page.getByTestId('hover-highlight')).toBeVisible()
// now check clicking works including axis
// click a segment hold shift and click an axis, see that a relevant constraint is enabled
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false)
await page.keyboard.down('Shift')
const absYButton = page.getByRole('button', { name: 'ABS Y' })
await expect(absYButton).toBeDisabled()
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
await page.keyboard.up('Shift')
await absYButton.and(page.locator(':not([disabled])')).waitFor()
await expect(absYButton).not.toBeDisabled()
// clear selection by clicking on nothing
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
// same selection but click the axis first
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
await expect(absYButton).toBeDisabled()
await page.keyboard.down('Shift')
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_with_point', false)
await page.keyboard.up('Shift')
await expect(absYButton).not.toBeDisabled()
// clear selection by clicking on nothing
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
// check the same selection again by putting cursor in code first then selecting axis
await u.doAndWaitForCmd(
() => page.getByText(` |> line([-${twentyish}, 0], %)`).click(),
'select_clear',
false
)
await page.keyboard.down('Shift')
await expect(absYButton).toBeDisabled()
await u.doAndWaitForCmd(xAxisClick, 'select_with_point', false)
await page.keyboard.up('Shift')
await expect(absYButton).not.toBeDisabled()
// clear selection by clicking on nothing
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
// select segment in editor than another segment in scene and check there are two cursors
await u.doAndWaitForCmd(
() => page.getByText(` |> line([-${twentyish}, 0], %)`).click(),
'select_clear',
false
)
await page.keyboard.down('Shift')
await expect(page.locator('.cm-cursor')).toHaveCount(1)
await u.doAndWaitForCmd(bottomHorzSegmentClick, 'select_with_point', false) // another segment, bottom one
await page.keyboard.up('Shift')
await expect(page.locator('.cm-cursor')).toHaveCount(2)
// clear selection by clicking on nothing
await u.doAndWaitForCmd(emptySpaceClick, 'select_clear', false)
}
await selectionSequence()
// hovering in fresh sketch worked, lets try exiting and re-entering
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Exit Sketch' }).click(),
'edit_mode_exit'
)
// wait for execution done
await u.expectCmdLog('[data-message-type="execution-done"]')
// select a line
await u.doAndWaitForCmd(topHorzSegmentClick, 'select_clear', false)
// enter sketch again
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
'edit_mode_enter',
false
)
// hover again and check it works
await selectionSequence()
})

View File

@ -1,20 +0,0 @@
import { readFileSync } from 'fs'
const secrets: Record<string, string> = {}
try {
const file = readFileSync('./e2e/playwright/playwright-secrets.env', 'utf8')
file
.split('\n')
.filter((line) => line && line.length > 1)
.forEach((line) => {
const [key, value] = line.split('=')
// prefer env vars over secrets file
secrets[key] = process.env[key] || (value as any).replaceAll('"', '')
})
} catch (err) {
// probably running in CI
secrets.token = process.env.token || ''
// add more env vars here to make them available in CI
}
export { secrets }

View File

@ -1,324 +0,0 @@
import { test, expect } from '@playwright/test'
import { secrets } from './secrets'
import { EngineCommand } from '../../src/lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid'
import { getUtils } from './test-utils'
import { Models } from '@kittycad/lib'
import fsp from 'fs/promises'
test.beforeEach(async ({ context, page }) => {
await context.addInitScript(async (token) => {
localStorage.setItem('TOKEN_PERSIST_KEY', token)
localStorage.setItem('persistCode', ``)
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',
})
)
}, secrets.token)
// reducedMotion kills animations, which speeds up tests and reduces flakiness
await page.emulateMedia({ reducedMotion: 'reduce' })
})
test.setTimeout(60000)
test('change camera, show planes', async ({ page, context }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openAndClearDebugPanel()
const camCmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
vantage: { x: 0, y: 50, z: 50 },
},
}
await u.sendCustomCmd(camCmd)
await u.waitForCmdReceive('default_camera_look_at')
// rotate
await u.closeDebugPanel()
await page.mouse.move(700, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(600, 300)
await page.mouse.up({ button: 'right' })
await u.openDebugPanel()
await u.waitForCmdReceive('camera_drag_end')
await page.waitForTimeout(500)
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.sendCustomCmd(camCmd)
await u.waitForCmdReceive('default_camera_look_at')
await u.clearCommandLogs()
await u.closeDebugPanel()
// pan
await page.keyboard.down('Shift')
await page.mouse.move(600, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 200)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
await u.openDebugPanel()
await u.waitForCmdReceive('camera_drag_end')
await page.waitForTimeout(300)
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.sendCustomCmd(camCmd)
await u.waitForCmdReceive('default_camera_look_at')
await u.clearCommandLogs()
await u.closeDebugPanel()
// zoom
await page.keyboard.down('Control')
await page.mouse.move(700, 400)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 350)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Control')
await u.openDebugPanel()
await u.waitForCmdReceive('camera_drag_end')
await page.waitForTimeout(300)
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.waitForDefaultPlanesVisibilityChange()
await u.closeDebugPanel()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
})
})
test('exports of each format should work', async ({ page, context }) => {
// FYI this test doesn't work with only engine running locally
const u = getUtils(page)
await context.addInitScript(async () => {
;(window as any).playwrightSkipFilePicker = true
localStorage.setItem(
'persistCode',
`const topAng = 25
const bottomAng = 35
const baseLen = 3.5
const baseHeight = 1
const totalHeightHalf = 2
const armThick = 0.5
const totalLen = 9.5
const part001 = startSketchOn('-XZ')
|> startProfileAt([0, 0], %)
|> yLine(baseHeight, %)
|> xLine(baseLen, %)
|> angledLineToY({
angle: topAng,
to: totalHeightHalf,
tag: 'seg04'
}, %)
|> xLineTo({ to: totalLen, tag: 'seg03' }, %)
|> yLine({ length: -armThick, tag: 'seg01' }, %)
|> angledLineThatIntersects({
angle: _180,
offset: -armThick,
intersectTag: 'seg04'
}, %)
|> angledLineToY([segAng('seg04', %) + 180, _0], %)
|> angledLineToY({
angle: -bottomAng,
to: -totalHeightHalf - armThick,
tag: 'seg02'
}, %)
|> xLineTo(segEndX('seg03', %) + 0, %)
|> yLine(-segLen('seg01', %), %)
|> angledLineThatIntersects({
angle: _180,
offset: -armThick,
intersectTag: 'seg02'
}, %)
|> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %)
|> xLineTo(_0, %)
|> close(%)
|> extrude(4, %)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.waitForDefaultPlanesVisibilityChange()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.waitForCmdReceive('extrude')
await page.waitForTimeout(1000)
await u.clearAndCloseDebugPanel()
await page.getByRole('button', { name: 'KittyCAD Modeling App' }).click()
const doExport = async (output: Models['OutputFormat_type']) => {
await page.getByRole('button', { name: 'Export Model' }).click()
const exportSelect = page.getByTestId('export-type')
await exportSelect.selectOption({ label: output.type })
if ('storage' in output) {
const storageSelect = page.getByTestId('export-storage')
await storageSelect.selectOption({ label: output.storage })
}
const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'Export', exact: true }).click()
const download = await downloadPromise
const downloadLocationer = (extra = '') =>
`./e2e/playwright/export-snapshots/${output.type}-${
'storage' in output ? output.storage : ''
}${extra}.${output.type}`
const downloadLocation = downloadLocationer()
const downloadLocation2 = downloadLocationer('-2')
if (output.type === 'gltf' && output.storage === 'standard') {
// wait for second download
const download2 = await page.waitForEvent('download')
await download.saveAs(downloadLocation)
await download2.saveAs(downloadLocation2)
// rewrite uri to reference our file name
const fileContents = await fsp.readFile(downloadLocation, 'utf-8')
const isJson = fileContents.includes('buffers')
let contents = fileContents
let reWriteLocation = downloadLocation
let uri = downloadLocation2.split('/').pop()
if (!isJson) {
contents = await fsp.readFile(downloadLocation2, 'utf-8')
reWriteLocation = downloadLocation2
uri = downloadLocation.split('/').pop()
}
contents = contents.replace(/"uri": ".*"/g, `"uri": "${uri}"`)
await fsp.writeFile(reWriteLocation, contents)
} else {
await download.saveAs(downloadLocation)
}
if (output.type === 'step') {
// stable timestamps for step files
const fileContents = await fsp.readFile(downloadLocation, 'utf-8')
const newFileContents = fileContents.replace(
/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+[0-9]+[0-9]\+[0-9]{2}:[0-9]{2}/g,
'1970-01-01T00:00:00.0+00:00'
)
await fsp.writeFile(downloadLocation, newFileContents)
}
}
const axisDirectionPair: Models['AxisDirectionPair_type'] = {
axis: 'z',
direction: 'positive',
}
const sysType: Models['System_type'] = {
forward: axisDirectionPair,
up: axisDirectionPair,
}
// NOTE it was easiest to leverage existing types and have doExport take Models['OutputFormat_type'] as in input
// just note that only `type` and `storage` are used for selecting the drop downs is the app
// the rest are only there to make typescript happy
await doExport({
type: 'step',
coords: sysType,
})
await doExport({
type: 'gltf',
storage: 'embedded',
presentation: 'pretty',
})
await doExport({
type: 'gltf',
storage: 'binary',
presentation: 'pretty',
})
await doExport({
type: 'gltf',
storage: 'standard',
presentation: 'pretty',
})
await doExport({
type: 'ply',
coords: sysType,
selection: { type: 'default_scene' },
storage: 'ascii',
units: 'in',
})
await doExport({
type: 'ply',
storage: 'binary_little_endian',
coords: sysType,
selection: { type: 'default_scene' },
units: 'in',
})
await doExport({
type: 'ply',
storage: 'binary_big_endian',
coords: sysType,
selection: { type: 'default_scene' },
units: 'in',
})
await doExport({
type: 'stl',
storage: 'ascii',
coords: sysType,
units: 'in',
selection: { type: 'default_scene' },
})
await doExport({
type: 'stl',
storage: 'binary',
coords: sysType,
units: 'in',
selection: { type: 'default_scene' },
})
await doExport({
// obj seems to be a little flaky, times out tests sometimes
type: 'obj',
coords: sysType,
units: 'in',
})
})

View File

@ -1,156 +0,0 @@
import { expect, Page } from '@playwright/test'
import { EngineCommand } from '../../src/lang/std/engineConnection'
import fsp from 'fs/promises'
import pixelMatch from 'pixelmatch'
import { PNG } from 'pngjs'
async function waitForPageLoad(page: Page) {
// wait for 'Loading stream...' spinner
await page.getByTestId('loading-stream').waitFor()
// wait for all spinners to be gone
await page.getByTestId('loading').waitFor({ state: 'detached' })
await page.getByTestId('start-sketch').waitFor()
}
async function removeCurrentCode(page: Page) {
const hotkey = process.platform === 'darwin' ? 'Meta' : 'Control'
await page.click('.cm-content')
await page.keyboard.down(hotkey)
await page.keyboard.press('a')
await page.keyboard.up(hotkey)
await page.keyboard.press('Backspace')
await expect(page.locator('.cm-content')).toHaveText('')
}
async function sendCustomCmd(page: Page, cmd: EngineCommand) {
await page.fill('[data-testid="custom-cmd-input"]', JSON.stringify(cmd))
await page.click('[data-testid="custom-cmd-send-button"]')
}
async function clearCommandLogs(page: Page) {
await page.click('[data-testid="clear-commands"]')
}
async function expectCmdLog(page: Page, locatorStr: string) {
await expect(page.locator(locatorStr)).toBeVisible()
}
async function waitForDefaultPlanesToBeVisible(page: Page) {
await page.waitForFunction(
() =>
document.querySelectorAll('[data-receive-command-type="object_visible"]')
.length >= 3
)
}
async function openDebugPanel(page: Page) {
const isOpen =
(await page
.locator('[data-testid="debug-panel"]')
?.getAttribute('open')) === ''
if (!isOpen) {
await page.getByText('Debug').click()
await page.getByTestId('debug-panel').and(page.locator('[open]')).waitFor()
}
}
async function closeDebugPanel(page: Page) {
const isOpen =
(await page.getByTestId('debug-panel')?.getAttribute('open')) === ''
if (isOpen) {
await page.getByText('Debug').click()
await page
.getByTestId('debug-panel')
.and(page.locator(':not([open])'))
.waitFor()
}
}
async function waitForCmdReceive(page: Page, commandType: string) {
return page
.locator(`[data-receive-command-type="${commandType}"]`)
.first()
.waitFor()
}
export function getUtils(page: Page) {
return {
waitForAuthSkipAppStart: () => waitForPageLoad(page),
removeCurrentCode: () => removeCurrentCode(page),
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
clearCommandLogs: () => clearCommandLogs(page),
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr),
waitForDefaultPlanesVisibilityChange: () =>
waitForDefaultPlanesToBeVisible(page),
openDebugPanel: () => openDebugPanel(page),
closeDebugPanel: () => closeDebugPanel(page),
openAndClearDebugPanel: async () => {
await openDebugPanel(page)
return clearCommandLogs(page)
},
clearAndCloseDebugPanel: async () => {
await clearCommandLogs(page)
return closeDebugPanel(page)
},
waitForCmdReceive: (commandType: string) =>
waitForCmdReceive(page, commandType),
doAndWaitForCmd: async (
fn: () => Promise<void>,
commandType: string,
endWithDebugPanelOpen = true
) => {
await openDebugPanel(page)
await clearCommandLogs(page)
await closeDebugPanel(page)
await fn()
await openDebugPanel(page)
await waitForCmdReceive(page, commandType)
if (!endWithDebugPanelOpen) {
await closeDebugPanel(page)
}
},
doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) =>
new Promise(async (resolve) => {
await page.screenshot({
path: './e2e/playwright/temp1.png',
fullPage: true,
})
await fn()
const isImageDiff = async () => {
await page.screenshot({
path: './e2e/playwright/temp2.png',
fullPage: true,
})
const screenshot1 = PNG.sync.read(
await fsp.readFile('./e2e/playwright/temp1.png')
)
const screenshot2 = PNG.sync.read(
await fsp.readFile('./e2e/playwright/temp2.png')
)
const actualDiffCount = pixelMatch(
screenshot1.data,
screenshot2.data,
null,
screenshot1.width,
screenshot2.height
)
return actualDiffCount > diffCount
}
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
let count = 0
const interval = setInterval(async () => {
count++
if (await isImageDiff()) {
clearInterval(interval)
resolve(true)
} else if (count > 100) {
clearInterval(interval)
resolve(false)
}
}, 50)
}),
}
}

View File

@ -1,62 +0,0 @@
import { browser, $, expect } from '@wdio/globals'
import fs from 'fs/promises'
describe('KCMA (Tauri, Linux)', () => {
it('opens the auth page, signs in, and signs out', async () => {
// Clean up previous tests
await new Promise((resolve) => setTimeout(resolve, 100))
await fs.rm('/tmp/kittycad_user_code', { force: true })
await browser.execute('window.localStorage.clear()')
const signInButton = await $('[data-testid="sign-in-button"]')
expect(await signInButton.getText()).toEqual('Sign in')
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
await signInButton.waitForClickable()
await browser.execute('arguments[0].click();', signInButton)
await new Promise((resolve) => setTimeout(resolve, 2000))
// Get from main.rs
const userCode = await (
await fs.readFile('/tmp/kittycad_user_code')
).toString()
console.log(`Found user code ${userCode}`)
// Device flow: verify
const token = process.env.KITTYCAD_API_TOKEN
const headers = {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}
const verifyUrl = `https://api.kittycad.io/oauth2/device/verify?user_code=${userCode}`
console.log(`GET ${verifyUrl}`)
const vr = await fetch(verifyUrl, { headers })
console.log(vr.status)
// Device flow: confirm
const confirmUrl = 'https://api.kittycad.io/oauth2/device/confirm'
const data = JSON.stringify({ user_code: userCode })
console.log(`POST ${confirmUrl} ${data}`)
const cr = await fetch(confirmUrl, {
headers,
method: 'POST',
body: data,
})
console.log(cr.status)
// Now should be signed in
const newFileButton = await $('[data-testid="home-new-file"]')
expect(await newFileButton.getText()).toEqual('New file')
// So let's sign out!
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
await menuButton.waitForClickable()
await browser.execute('arguments[0].click();', menuButton)
const signoutButton = await $('[data-testid="user-sidebar-sign-out"]')
await signoutButton.waitForClickable()
await browser.execute('arguments[0].click();', signoutButton)
const newSignInButton = await $('[data-testid="sign-in-button"]')
expect(await newSignInButton.getText()).toEqual('Sign in')
})
})

View File

@ -11,7 +11,6 @@
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<script defer data-domain="app.kittycad.io" src="https://plausible.corp.kittycad.io/js/script.js"></script>
<title>KittyCAD Modeling App</title>
</head>
<body class="body-bg">

View File

@ -1,39 +1,29 @@
{
"name": "untitled-app",
"version": "0.12.0",
"version": "0.0.4",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.10.2",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.17",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.46",
"@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1",
"@headlessui/react": "^1.7.13",
"@kittycad/lib": "^0.0.29",
"@react-hook/resize-observer": "^1.2.6",
"@replit/codemirror-interact": "^6.3.0",
"@sentry/react": "^7.77.0",
"@tauri-apps/api": "^1.5.1",
"@tauri-apps/api": "^1.3.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.1",
"@ts-stack/markdown": "^1.5.0",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
"@types/node": "^16.7.13",
"@types/react": "^18.2.41",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@uiw/react-codemirror": "^4.21.20",
"@xstate/inspect": "^0.8.0",
"@uiw/codemirror-extensions-langs": "^4.21.9",
"@uiw/react-codemirror": "^4.15.1",
"@xstate/react": "^3.2.2",
"crypto-js": "^4.2.0",
"debounce-promise": "^3.1.2",
"crypto-js": "^4.1.1",
"formik": "^2.4.3",
"fuse.js": "^7.0.0",
"http-server": "^14.1.1",
"json-rpc-2.0": "^1.6.0",
"re-resizable": "^6.9.11",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
@ -43,43 +33,36 @@
"react-modal-promise": "^1.0.2",
"react-router-dom": "^6.14.2",
"sketch-helpers": "^0.0.4",
"swr": "^2.2.2",
"swr": "^2.0.4",
"tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1",
"toml": "^3.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"uuid": "^9.0.1",
"vitest": "^0.34.6",
"vscode-jsonrpc": "^8.1.0",
"vscode-languageserver-protocol": "^3.17.5",
"typescript": "^4.4.2",
"uuid": "^9.0.0",
"vitest": "^0.34.1",
"wasm-pack": "^0.12.1",
"web-vitals": "^3.5.0",
"web-vitals": "^2.1.0",
"ws": "^8.13.0",
"xstate": "^4.38.2",
"zustand": "^4.4.5"
"zustand": "^4.1.4"
},
"scripts": {
"start": "vite",
"serve": "vite serve --port=3000",
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
"build:local": "vite build",
"build:both": "vite build",
"build:both:local": "yarn build:wasm && vite build",
"pretest": "yarn remove-importmeta",
"test": "vitest --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 && cargo clippy)",
"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",
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
"fmt": "prettier --write ./src && prettier --write ./e2e",
"fmt-check": "prettier --check ./src && prettier --check ./e2e",
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
"build:wasm": "(cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
"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",
"simpleserver:ci": "http-server ./public --cors -p 3000 &",
"simpleserver": "http-server ./public --cors -p 3000",
"fmt": "prettier --write ./src",
"fmt-check": "prettier --check ./src",
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test --all) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
"remove-importmeta": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url//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/bindings",
"lint": "eslint --fix src",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"
},
@ -103,42 +86,29 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.23.3",
"@playwright/test": "^1.39.0",
"@tauri-apps/cli": "^1.5.6",
"@babel/preset-env": "^7.22.9",
"@tauri-apps/cli": "^1.3.1",
"@types/crypto-js": "^4.1.1",
"@types/debounce-promise": "^3.1.8",
"@types/isomorphic-fetch": "^0.0.36",
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4",
"@types/react-modal": "^3.16.3",
"@types/uuid": "^9.0.4",
"@types/wait-on": "^5.3.4",
"@types/react-modal": "^3.16.0",
"@types/uuid": "^9.0.1",
"@types/wicg-file-system-access": "^2020.9.6",
"@types/ws": "^8.5.5",
"@vitejs/plugin-react": "^4.1.1",
"@vitest/coverage-istanbul": "^0.34.6",
"@wdio/cli": "^8.24.3",
"@wdio/globals": "^8.24.3",
"@wdio/local-runner": "^8.24.3",
"@wdio/mocha-framework": "^8.24.3",
"@wdio/spec-reporter": "^8.24.2",
"@vitejs/plugin-react": "^4.0.3",
"@vitest/coverage-istanbul": "^0.34.1",
"autoprefixer": "^10.4.13",
"eslint": "^8.53.0",
"eslint": "^8.44.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-css-modules": "^2.11.0",
"happy-dom": "^10.8.0",
"husky": "^8.0.3",
"pixelmatch": "^5.3.0",
"pngjs": "^7.0.0",
"postcss": "^8.4.31",
"postcss": "^8.4.19",
"prettier": "^2.8.0",
"setimmediate": "^1.0.5",
"tailwindcss": "^3.3.5",
"vite": "^4.5.0",
"tailwindcss": "^3.2.4",
"vite": "^4.4.3",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.1",
"wait-on": "^7.2.0",
"vite-tsconfig-paths": "^4.2.0",
"yarn": "^1.22.19"
}
}

View File

@ -1,82 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './e2e/playwright',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 3 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'Google Chrome',
use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // or 'chrome-beta'
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'chromium', // compat issue with encoding atm, so we're using the branded 'Google Chrome' instead
// use: { ...devices['Desktop Chrome'] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: 'yarn serve',
// url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
},
});

View File

@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 475 B

View File

@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 469 B

View File

@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 3.5H4.5V16.5H15.5V8.00001M11 3.5L15.5 8.00001M11 3.5V8.00001H15.5" stroke="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 200 B

View File

@ -1,42 +0,0 @@
## Alpha Users Expectations
### Welcome
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)
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.
### Why Code?
Plenty of you have professional CAD experience, and may not understand why coding your model would be helpful. The "code-CAD" paradigm isnt as popular as traditional CAD programs (SolidWorks, NX, CREO, OnShape, etc.), but it certainly has its benefits. Some benefits include:
- Automation and parametric design
- Customization and flexibility
- Algorithmic and generative design
- Reproducibility
- Easier integration with other tools
### Before You Use KCMA
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.
- 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 its not a duplicate or its 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:
- **High:** The bug is blocking you from continuing.
- Example: Every time I click the extrude button with two faces selected, the app crashes.
- **Medium:** You can find a workaround to the problem, but it increases your time spent working or makes it unenjoyable.
- Example: When the app is full screen on Mac, the settings are not showing properly. It works if I have the app windowed.
- **Low:** The bug is annoying but doesnt affect workflow or block you from continuing (usually you can say “It would be nice if ___, but its not needed”)
- Example: It would be nice if the camera would orient normal to the sketching surface when I select a face/plane and click “sketch”.
- We want you all to be aware that we may reach out to you in regard to issues, bugs, problems, and satisfaction. This will typically be for further clarification so we can really nail things down.
### Discord
We will be using Discord a lot more now that the Alpha has been released to people outside of the company. Please feel free to discuss and talk with us in the **alpha users** section of the server. We highly encourage you to engage with us on Discord!
### Thank You!
Once again, from all of us to you, thank you for being a part of the closed Alpha. We are happy to chat with you all, hear your feedback, and see some of your projects!

View File

@ -1,3 +0,0 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M40 0H0V40H40V0ZM7.34715 27.2143V15.6577L2.976 15.987V36.7949H7.34715V32.0645L8.00582 31.5256C8.24533 31.326 8.47487 31.1264 8.69442 30.9268L12.1075 36.7949H17.0475C16.1893 35.3978 15.311 33.9906 14.4128 32.5735C13.5346 31.1563 12.6664 29.7392 11.8081 28.3221L15.8499 24.9389C15.4308 24.4399 15.0017 23.931 14.5625 23.412L13.3051 21.8552L7.34715 27.2143ZM22.2581 26.6754C22.8769 25.9169 23.6753 25.5377 24.6533 25.5377C25.272 25.5377 25.8309 25.6175 26.3299 25.7772C26.8289 25.9169 27.4177 26.1465 28.0963 26.4658L29.3238 23.3521C28.5853 22.7933 27.7371 22.4041 26.779 22.1845C25.8409 21.9649 25.0625 21.8552 24.4437 21.8552C22.0885 21.8552 20.2223 22.5537 18.845 23.9509C17.4878 25.3281 16.8092 27.1944 16.8092 29.5496C16.8092 31.9048 17.4878 33.7611 18.845 35.1183C20.2223 36.4756 22.0885 37.1542 24.4437 37.1542C25.0625 37.1542 25.8509 37.0444 26.8089 36.8249C27.767 36.6053 28.6053 36.2161 29.3238 35.6572L28.0963 32.5435C27.4177 32.8629 26.8289 33.0924 26.3299 33.2321C25.8309 33.3718 25.272 33.4417 24.6533 33.4417C23.6753 33.4417 22.8769 33.0924 22.2581 32.3938C21.6594 31.6753 21.36 30.7272 21.36 29.5496C21.36 28.372 21.6594 27.4139 22.2581 26.6754ZM36.2796 36.7949V15.6577L31.9085 15.987V36.7949H36.2796Z" fill="#D0FF00"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

View File

@ -1,26 +0,0 @@
## KittyCAD Modeling App Roadmap
This document ties into our [GH Discussions Feature List](https://github.com/KittyCAD/modeling-app/discussions). Please upvote any features that you want to see next, or add ones that are not listed and we will review.
### Current Priority List
1. [Sketch on Face](https://github.com/KittyCAD/modeling-app/discussions/477)
2. [Revolve](https://github.com/KittyCAD/modeling-app/discussions/496)
3. [Fillet](https://github.com/KittyCAD/modeling-app/discussions/501)
4. [Linear Pattern](https://github.com/KittyCAD/modeling-app/discussions/256)
5. [Circular Pattern](https://github.com/KittyCAD/modeling-app/discussions/257)
6. [Mirror-Sketch](https://github.com/KittyCAD/modeling-app/discussions/507)
7. [Chamfer](https://github.com/KittyCAD/modeling-app/discussions/502)
8. [Sweep](https://github.com/KittyCAD/modeling-app/discussions/498)
9. [Draft](https://github.com/KittyCAD/modeling-app/discussions/495)
10. [Shell](https://github.com/KittyCAD/modeling-app/discussions/503)
11. [Union](https://github.com/KittyCAD/modeling-app/discussions/509)
12. [Mirror-Model](https://github.com/KittyCAD/modeling-app/discussions/508)
13. [Subtract](https://github.com/KittyCAD/modeling-app/discussions/510)
14. [Intersect](https://github.com/KittyCAD/modeling-app/discussions/511)
15. [Offset](https://github.com/KittyCAD/modeling-app/discussions/512)
16. [Thicken](https://github.com/KittyCAD/modeling-app/discussions/499)
17. [Import](https://github.com/KittyCAD/modeling-app/discussions/478)
18. [Assemblies](https://github.com/KittyCAD/modeling-app/discussions/494)
19. [External Thread](https://github.com/KittyCAD/modeling-app/discussions/505)

1184
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = "https://github.com/KittyCAD/modeling-app"
repository = ""
default-run = "app"
edition = "2021"
rust-version = "1.60"
@ -12,18 +12,17 @@ rust-version = "1.60"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.5.0", features = [] }
tauri-build = { version = "1.3.0", features = [] }
[dependencies]
anyhow = "1"
kittycad = "0.2.42"
oauth2 = "4.4.2"
oauth2 = "4.4.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = { version = "1.5.2", features = [ "os-all", "dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "devtools"] }
tauri = { version = "1.3.0", features = [ "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
tokio = { version = "1.29.1", features = ["time"] }
toml = "0.6.0"
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tokio = { version = "1.34.0", features = ["time"] }
toml = "0.8.2"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.

View File

@ -1,14 +1,11 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::env;
use std::fs;
use std::io::Read;
use anyhow::Result;
use oauth2::TokenResponse;
use tauri::{InvokeError, Manager};
const DEFAULT_HOST: &str = "https://api.kittycad.io";
/// This command returns the a json string parse from a toml file at the path.
#[tauri::command]
@ -70,26 +67,10 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
};
// Open the system browser with the auth_uri.
// We do this in the browser and not a separate window because we want 1password and
// We do this in the browser and not a seperate window because we want 1password and
// other crap to work well.
// TODO: find a better way to share this value with tauri e2e tests
// Here we're using an env var to enable the /tmp file (windows not supported for now)
// and bypass the shell::open call as it fails on GitHub Actions.
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
if (e2e_tauri_enabled) {
println!(
"E2E_TAURI_ENABLED is set, won't open {} externally",
auth_uri.secret()
);
fs::write(
"/tmp/kittycad_user_code",
details.user_code().secret().to_string(),
)
.expect("Unable to write /tmp/kittycad_user_code file");
} else {
tauri::api::shell::open(&app.shell_scope(), auth_uri.secret(), None)
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
tauri::api::shell::open(&app.shell_scope(), auth_uri.secret(), None)
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
// Wait for the user to login.
let token = auth_client
@ -104,65 +85,19 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
Ok(token)
}
///This command returns the KittyCAD user info given a token.
/// The string returned from this method is the user info as a json string.
#[tauri::command]
async fn get_user(
token: Option<String>,
hostname: &str,
) -> Result<kittycad::types::User, InvokeError> {
// Use the host passed in if it's set.
// Otherwise, use the default host.
let host = if hostname.is_empty() {
DEFAULT_HOST.to_string()
} else {
hostname.to_string()
};
// Change the baseURL to the one we want.
let mut baseurl = host.to_string();
if !host.starts_with("http://") && !host.starts_with("https://") {
baseurl = format!("https://{host}");
if host.starts_with("localhost") {
baseurl = format!("http://{host}")
}
}
println!("Getting user info...");
// use kittycad library to fetch the user info from /user/me
let mut client = kittycad::Client::new(token.unwrap());
if baseurl != DEFAULT_HOST {
client.set_base_url(&baseurl);
}
let user_info: kittycad::types::User = client
.users()
.get_self()
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(user_info)
}
fn main() {
tauri::Builder::default()
.setup(|_app| {
.setup(|app| {
#[cfg(debug_assertions)] // only include this code on debug builds
{
let window = _app.get_window("main").unwrap();
let window = app.get_window("main").unwrap();
// comment out the below if you don't devtools to open everytime.
// it's useful because otherwise devtools shuts everytime rust code changes.
window.open_devtools();
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
get_user,
login,
read_toml,
read_txt_file
])
.invoke_handler(tauri::generate_handler![login, read_toml, read_txt_file])
.plugin(tauri_plugin_fs_extra::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@ -7,8 +7,8 @@
"distDir": "../build"
},
"package": {
"productName": "kittycad-modeling",
"version": "0.12.0"
"productName": "kittycad-modeling-app",
"version": "0.0.4"
},
"tauri": {
"allowlist": {
@ -36,9 +36,6 @@
"https://api.dev.kittycad.io/*"
]
},
"os": {
"all": true
},
"shell": {
"open": true
},
@ -72,13 +69,23 @@
},
"resources": [],
"shortDescription": "",
"targets": "all"
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {
"csp": null
},
"updater": {
"active": false
"active": true,
"endpoints": [
"https://dl.kittycad.io/releases/modeling-app/last_update.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
},
"windows": [
{

View File

@ -1,6 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "KittyCAD Modeling"
}
}

View File

@ -1,21 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"updater": {
"active": true,
"endpoints": [
"https://dl.kittycad.io/releases/modeling-app/last_update.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
},
"bundle": {
"identifier": "io.kittycad.modeling-app",
"windows": {
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
"digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com"
}
}
}
}

View File

@ -1,6 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "KittyCAD Modeling"
}
}

View File

@ -1,16 +1,8 @@
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'
import ModelingMachineProvider from 'components/ModelingMachineProvider'
import { BROWSER_FILE_NAME } from 'Router'
import { BrowserRouter } from 'react-router-dom'
import { GlobalStateProvider } from './hooks/useAuthMachine'
let listener: ((rect: any) => void) | undefined = undefined
;(global as any).ResizeObserver = class ResizeObserver {
@ -31,7 +23,7 @@ describe('App tests', () => {
>
return {
...actual,
useParams: () => ({ id: BROWSER_FILE_NAME }),
useParams: () => ({ id: 'new' }),
useLoaderData: () => ({ code: null }),
}
})
@ -48,26 +40,10 @@ describe('App tests', () => {
})
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,
}
// wrap in router and xState context
return (
<BrowserRouter>
<GlobalStateProvider>{children}</GlobalStateProvider>
</BrowserRouter>
)
return <RouterProvider router={router} />
}

View File

@ -1,16 +1,37 @@
import { useCallback, MouseEventHandler } from 'react'
import {
useRef,
useEffect,
useLayoutEffect,
useMemo,
useCallback,
MouseEventHandler,
} from 'react'
import { DebugPanel } from './components/DebugPanel'
import { v4 as uuidv4 } from 'uuid'
import { PaneType, useStore } from './useStore'
import { asyncParser } from './lang/abstractSyntaxTree'
import { _executor } from './lang/executor'
import CodeMirror from '@uiw/react-codemirror'
import { langs } from '@uiw/codemirror-extensions-langs'
import { linter, lintGutter } from '@codemirror/lint'
import { ViewUpdate } from '@codemirror/view'
import {
lineHighlightField,
addLineHighlight,
} from './editor/highlightextension'
import { PaneType, Selections, Themes, useStore } from './useStore'
import { Logs, KCLErrors } from './components/Logs'
import { CollapsiblePanel } from './components/CollapsiblePanel'
import { MemoryPanel } from './components/MemoryPanel'
import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream'
import ModalContainer from 'react-modal-promise'
import { EngineCommand } from './lang/std/engineConnection'
import { throttle } from './lib/utils'
import {
EngineCommand,
EngineCommandManager,
} from './lang/std/engineConnection'
import { isOverlap, throttle } from './lib/utils'
import { AppHeader } from './components/AppHeader'
import { KCLError, kclErrToDiagnostic } from './lang/errors'
import { Resizable } from 're-resizable'
import {
faCode,
@ -18,42 +39,101 @@ import {
faSquareRootVariable,
} from '@fortawesome/free-solid-svg-icons'
import { useHotkeys } from 'react-hotkeys-hook'
import { TEST } from './env'
import { getNormalisedCoordinates } from './lib/utils'
import { useLoaderData } from 'react-router-dom'
import { getSystemTheme } from './lib/getSystemTheme'
import { isTauri } from './lib/isTauri'
import { useLoaderData, useParams } from 'react-router-dom'
import { writeTextFile } from '@tauri-apps/api/fs'
import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
import { IndexLoaderData } from './Router'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { onboardingPaths } from 'routes/Onboarding'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { CodeMenu } from 'components/CodeMenu'
import { TextEditor } from 'components/TextEditor'
import { Themes, getSystemTheme } from 'lib/theme'
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
import { engineCommandManager } from './lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext'
import { toast } from 'react-hot-toast'
import { useAuthMachine } from './hooks/useAuthMachine'
export function App() {
const { project, file } = useLoaderData() as IndexLoaderData
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
const pathParams = useParams()
const streamRef = useRef<HTMLDivElement>(null)
useHotKeyListener()
const {
buttonDownInStream,
editorView,
setEditorView,
setSelectionRanges,
selectionRanges,
addLog,
addKCLError,
code,
setCode,
setAst,
setError,
setProgramMemory,
resetLogs,
resetKCLErrors,
selectionRangeTypeMap,
setArtifactMap,
engineCommandManager,
setEngineCommandManager,
setHighlightRange,
setCursor2,
sourceRangeMap,
setMediaStream,
setIsStreamReady,
isStreamReady,
isMouseDownInStream,
cmdId,
setCmdId,
formatCode,
debugPanel,
theme,
openPanes,
setOpenPanes,
onboardingStatus,
didDragInStream,
setDidDragInStream,
setStreamDimensions,
streamDimensions,
} = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
editorView: s.editorView,
setEditorView: s.setEditorView,
setSelectionRanges: s.setSelectionRanges,
selectionRanges: s.selectionRanges,
setGuiMode: s.setGuiMode,
addLog: s.addLog,
code: s.code,
setCode: s.setCode,
setAst: s.setAst,
setError: s.setError,
setProgramMemory: s.setProgramMemory,
resetLogs: s.resetLogs,
resetKCLErrors: s.resetKCLErrors,
selectionRangeTypeMap: s.selectionRangeTypeMap,
setArtifactMap: s.setArtifactNSourceRangeMaps,
engineCommandManager: s.engineCommandManager,
setEngineCommandManager: s.setEngineCommandManager,
setHighlightRange: s.setHighlightRange,
isShiftDown: s.isShiftDown,
setCursor: s.setCursor,
setCursor2: s.setCursor2,
sourceRangeMap: s.sourceRangeMap,
setMediaStream: s.setMediaStream,
isStreamReady: s.isStreamReady,
setIsStreamReady: s.setIsStreamReady,
isMouseDownInStream: s.isMouseDownInStream,
cmdId: s.cmdId,
setCmdId: s.setCmdId,
formatCode: s.formatCode,
debugPanel: s.debugPanel,
addKCLError: s.addKCLError,
theme: s.theme,
openPanes: s.openPanes,
setOpenPanes: s.setOpenPanes,
onboardingStatus: s.onboardingStatus,
didDragInStream: s.didDragInStream,
setDidDragInStream: s.setDidDragInStream,
setStreamDimensions: s.setStreamDimensions,
streamDimensions: s.streamDimensions,
}))
const { settings } = useGlobalStateContext()
const { showDebugPanel, onboardingStatus, cameraControls, theme } =
settings?.context || {}
const { state, send } = useModelingContext()
const [token] = useAuthMachine((s) => s?.context?.token)
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
@ -70,79 +150,258 @@ export function App() {
useHotkeys('shift + l', () => togglePane('logs'))
useHotkeys('shift + e', () => togglePane('kclErrors'))
useHotkeys('shift + d', () => togglePane('debug'))
useHotkeys('esc', () => send('Cancel'))
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
(p) => p === onboardingStatus
)
? 'opacity-20'
: didDragInStream
? 'opacity-40'
: ''
const paneOpacity =
onboardingStatus === 'camera'
? 'opacity-20'
: didDragInStream
? 'opacity-40'
: ''
useEngineConnectionSubscriptions()
// Use file code loaded from disk
// on mount, and overwrite any locally-stored code
useEffect(() => {
if (isTauri() && loadedCode !== null) {
setCode(loadedCode)
}
return () => {
// Clear code on unmount if in desktop app
if (isTauri()) {
setCode('')
}
}
}, [loadedCode, setCode])
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = (value: string, viewUpdate: ViewUpdate) => {
setCode(value)
if (isTauri() && pathParams.id) {
// Save the file to disk
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
(err) => {
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
console.error('error saving file', err)
toast.error('Error saving file, please check file permissions')
}
)
}
if (editorView) {
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
}
} //, []);
const onUpdate = (viewUpdate: ViewUpdate) => {
if (!editorView) {
setEditorView(viewUpdate.view)
}
const ranges = viewUpdate.state.selection.ranges
const isChange =
ranges.length !== selectionRanges.codeBasedSelections.length ||
ranges.some(({ from, to }, i) => {
return (
from !== selectionRanges.codeBasedSelections[i].range[0] ||
to !== selectionRanges.codeBasedSelections[i].range[1]
)
})
if (!isChange) return
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
({ from, to }) => {
if (selectionRangeTypeMap[to]) {
return {
type: selectionRangeTypeMap[to],
range: [from, to],
}
}
return {
type: 'default',
range: [from, to],
}
}
)
const idBasedSelections = codeBasedSelections
.map(({ type, range }) => {
const hasOverlap = Object.entries(sourceRangeMap).filter(
([_, sourceRange]) => {
return isOverlap(sourceRange, range)
}
)
if (hasOverlap.length) {
return {
type,
id: hasOverlap[0][0],
}
}
})
.filter(Boolean) as any
engineCommandManager?.cusorsSelected({
otherSelections: [],
idBasedSelections,
})
setSelectionRanges({
otherSelections: [],
codeBasedSelections,
})
}
const pixelDensity = window.devicePixelRatio
const streamWidth = streamRef?.current?.offsetWidth
const streamHeight = streamRef?.current?.offsetHeight
const width = streamWidth ? streamWidth * pixelDensity : 0
const quadWidth = Math.round(width / 4) * 4
const height = streamHeight ? streamHeight * pixelDensity : 0
const quadHeight = Math.round(height / 4) * 4
useLayoutEffect(() => {
setStreamDimensions({
streamWidth: quadWidth,
streamHeight: quadHeight,
})
if (!width || !height) return
const eng = new EngineCommandManager({
setMediaStream,
setIsStreamReady,
width: quadWidth,
height: quadHeight,
token,
})
setEngineCommandManager(eng)
return () => {
eng?.tearDown()
}
}, [quadWidth, quadHeight])
useEffect(() => {
if (!isStreamReady) return
const asyncWrap = async () => {
try {
if (!code) {
setAst(null)
return
}
const _ast = await asyncParser(code)
setAst(_ast)
resetLogs()
resetKCLErrors()
if (engineCommandManager) {
engineCommandManager.endSession()
engineCommandManager.startNewSession()
}
if (!engineCommandManager) return
const programMemory = await _executor(
_ast,
{
root: {
log: {
type: 'userVal',
value: (a: any) => {
addLog(a)
},
__meta: [
{
pathToNode: [],
sourceRange: [0, 0],
},
],
},
_0: {
type: 'userVal',
value: 0,
__meta: [],
},
_90: {
type: 'userVal',
value: 90,
__meta: [],
},
_180: {
type: 'userVal',
value: 180,
__meta: [],
},
_270: {
type: 'userVal',
value: 270,
__meta: [],
},
},
pendingMemory: {},
},
engineCommandManager,
{ bodyType: 'root' },
[]
)
const { artifactMap, sourceRangeMap } =
await engineCommandManager.waitForAllCommands()
setArtifactMap({ artifactMap, sourceRangeMap })
engineCommandManager.onHover((id) => {
if (!id) {
setHighlightRange([0, 0])
} else {
const sourceRange = sourceRangeMap[id]
setHighlightRange(sourceRange)
}
})
engineCommandManager.onClick((selections) => {
if (!selections) {
setCursor2()
return
}
const { id, type } = selections
setCursor2({ range: sourceRangeMap[id], type })
})
if (programMemory !== undefined) {
setProgramMemory(programMemory)
}
setError()
} catch (e: any) {
if (e instanceof KCLError) {
addKCLError(e)
} else {
setError('problem')
console.log(e)
addLog(e)
}
}
}
asyncWrap()
}, [code, isStreamReady])
const debounceSocketSend = throttle<EngineCommand>((message) => {
engineCommandManager.sendSceneCommand(message)
engineCommandManager?.sendSceneCommand(message)
}, 16)
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
e.nativeEvent.preventDefault()
const handleMouseMove: MouseEventHandler<HTMLDivElement> = ({
clientX,
clientY,
ctrlKey,
shiftKey,
currentTarget,
nativeEvent,
}) => {
nativeEvent.preventDefault()
if (isMouseDownInStream) {
setDidDragInStream(true)
}
const { x, y } = getNormalisedCoordinates({
clientX: e.clientX,
clientY: e.clientY,
el: e.currentTarget,
clientX,
clientY,
el: currentTarget,
...streamDimensions,
})
const interaction = ctrlKey ? 'zoom' : shiftKey ? 'pan' : 'rotate'
const newCmdId = uuidv4()
if (buttonDownInStream === undefined) {
if (state.matches('Sketch.Line Tool')) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd_id: newCmdId,
cmd: {
type: 'mouse_move',
window: { x, y },
},
})
} else {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'highlight_set_entity',
selected_at_window: { x, y },
},
cmd_id: newCmdId,
})
}
} else {
if (state.matches('Sketch.Move Tool')) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd_id: newCmdId,
cmd: {
type: 'handle_mouse_drag_move',
window: { x, y },
},
})
return
}
const interactionGuards = cameraMouseDragGuards[cameraControls]
let interaction: CameraDragInteractionType_type
const eWithButton = { ...e, button: buttonDownInStream }
if (interactionGuards.pan.callback(eWithButton)) {
interaction = 'pan'
} else if (interactionGuards.rotate.callback(eWithButton)) {
interaction = 'rotate'
} else if (interactionGuards.zoom.dragCallback(eWithButton)) {
interaction = 'zoom'
} else {
return
}
setCmdId(newCmdId)
if (cmdId && isMouseDownInStream) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
@ -152,38 +411,58 @@ export function App() {
},
cmd_id: newCmdId,
})
} else {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'highlight_set_entity',
selected_at_window: { x, y },
},
cmd_id: newCmdId,
})
}
}
const extraExtensions = useMemo(() => {
if (TEST) return []
return [
lintGutter(),
linter((_view) => {
return kclErrToDiagnostic(useStore.getState().kclErrors)
}),
]
}, [])
return (
<div
className="relative h-full flex flex-col"
className="h-screen overflow-hidden relative flex flex-col cursor-pointer select-none"
onMouseMove={handleMouseMove}
ref={streamRef}
>
<AppHeader
className={
'transition-opacity transition-duration-75 ' +
paneOpacity +
(buttonDownInStream ? ' pointer-events-none' : '')
(isMouseDownInStream ? ' pointer-events-none' : '')
}
project={{ project, file }}
project={project}
enableMenu={true}
/>
<ModalContainer />
<Resizable
className={
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
(buttonDownInStream || onboardingStatus === 'camera'
(isMouseDownInStream || onboardingStatus === 'camera'
? ' pointer-events-none '
: ' ') +
paneOpacity
}
defaultSize={{
width: '550px',
width: '400px',
height: 'auto',
}}
minWidth={200}
maxWidth={800}
maxWidth={600}
minHeight={'auto'}
maxHeight={'auto'}
handleClasses={{
@ -191,15 +470,37 @@ export function App() {
'hover:bg-liquid-30/40 dark:hover:bg-liquid-10/40 bg-transparent transition-colors duration-100 transition-ease-out delay-100',
}}
>
<div id="code-pane" className="h-full flex flex-col justify-between">
<div className="h-full flex flex-col justify-between">
<CollapsiblePanel
title="Code"
icon={faCode}
className="open:!mb-2"
open={openPanes.includes('code')}
menu={<CodeMenu />}
>
<TextEditor theme={editorTheme} />
<div className="px-2 py-1">
<button
// disabled={!shouldFormat}
onClick={formatCode}
// className={`${!shouldFormat && 'text-gray-300'}`}
>
format
</button>
</div>
<div id="code-mirror-override">
<CodeMirror
className="h-full"
value={code}
extensions={[
langs.javascript({ jsx: true }),
lineHighlightField,
...extraExtensions,
]}
onChange={onChange}
onUpdate={onUpdate}
theme={editorTheme}
onCreateEditor={(_editorView) => setEditorView(_editorView)}
/>
</div>
</CollapsiblePanel>
<section className="flex flex-col">
<MemoryPanel
@ -224,13 +525,13 @@ export function App() {
</div>
</Resizable>
<Stream className="absolute inset-0 z-0" />
{showDebugPanel && (
{debugPanel && (
<DebugPanel
title="Debug"
className={
'transition-opacity transition-duration-75 ' +
paneOpacity +
(buttonDownInStream ? ' pointer-events-none' : '')
(isMouseDownInStream ? ' pointer-events-none' : '')
}
open={openPanes.includes('debug')}
/>

View File

@ -1,15 +1,12 @@
import Loading from './components/Loading'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useAuthMachine } from './hooks/useAuthMachine'
// Wrapper around protected routes, used in src/Router.tsx
export const Auth = ({ children }: React.PropsWithChildren) => {
const { auth } = useGlobalStateContext()
const isLoggingIn = auth?.state.matches('checkIfLoggedIn')
const [isLoggedIn] = useAuthMachine((s) => s.matches('checkIfLoggedIn'))
return isLoggingIn ? (
<Loading>
<span data-testid="initial-load">Loading KittyCAD Modeling App...</span>
</Loading>
return isLoggedIn ? (
<Loading>Loading KittyCAD Modeling App...</Loading>
) : (
<>{children}</>
)

View File

@ -3,15 +3,8 @@ import {
createBrowserRouter,
Outlet,
redirect,
useLocation,
RouterProvider,
} from 'react-router-dom'
import {
matchRoutes,
createRoutesFromChildren,
useNavigationType,
} from 'react-router'
import { useEffect } from 'react'
import { ErrorPage } from './components/ErrorPage'
import { Settings } from './routes/Settings'
import Onboarding, {
@ -31,52 +24,7 @@ import {
} from './lib/tauriFS'
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
import DownloadAppBanner from './components/DownloadAppBanner'
import { WasmErrBanner } from './components/WasmErrBanner'
import { GlobalStateProvider } from './components/GlobalStateProvider'
import {
SETTINGS_PERSIST_KEY,
settingsMachine,
} from './machines/settingsMachine'
import { ContextFrom } from 'xstate'
import CommandBarProvider from 'components/CommandBar'
import { TEST, VITE_KC_SENTRY_DSN } from './env'
import * as Sentry from '@sentry/react'
import ModelingMachineProvider from 'components/ModelingMachineProvider'
import { KclContextProvider, kclManager } from 'lang/KclSinglton'
import FileMachineProvider from 'components/FileMachineProvider'
import { sep } from '@tauri-apps/api/path'
if (VITE_KC_SENTRY_DSN && !TEST) {
Sentry.init({
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,
})
}
import { GlobalStateProvider } from './hooks/useAuthMachine'
const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => {
@ -99,16 +47,13 @@ export const paths = {
) as typeof onboardingPaths,
}
export const BROWSER_FILE_NAME = 'new'
export type IndexLoaderData = {
code: string | null
project?: ProjectWithEntryPointMetadata
file?: FileEntry
}
export type ProjectWithEntryPointMetadata = FileEntry & {
entrypointMetadata: Metadata
entrypoint_metadata: Metadata
}
export type HomeLoaderData = {
projects: ProjectWithEntryPointMetadata[]
@ -123,11 +68,7 @@ const addGlobalContextToElements = (
'element' in route
? {
...route,
element: (
<CommandBarProvider>
<GlobalStateProvider>{route.element}</GlobalStateProvider>
</CommandBarProvider>
),
element: <GlobalStateProvider>{route.element}</GlobalStateProvider>,
}
: route
)
@ -137,89 +78,60 @@ const router = createBrowserRouter(
{
path: paths.INDEX,
loader: () =>
isTauri()
? redirect(paths.HOME)
: redirect(paths.FILE + '/' + BROWSER_FILE_NAME),
errorElement: <ErrorPage />,
isTauri() ? redirect(paths.HOME) : redirect(paths.FILE + '/new'),
},
{
path: paths.FILE + '/:id',
element: (
<Auth>
<FileMachineProvider>
<KclContextProvider>
<ModelingMachineProvider>
<Outlet />
<App />
</ModelingMachineProvider>
<WasmErrBanner />
</KclContextProvider>
</FileMachineProvider>
<Outlet />
<App />
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
</Auth>
),
errorElement: <ErrorPage />,
id: paths.FILE,
loader: async ({
request,
params,
}): Promise<IndexLoaderData | Response> => {
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
ContextFrom<typeof settingsMachine>
>
const store = localStorage.getItem('store')
if (store === null) {
return redirect(paths.ONBOARDING.INDEX)
} else {
const status = JSON.parse(store).state.onboardingStatus || ''
const notEnRouteToOnboarding =
!request.url.includes(paths.ONBOARDING.INDEX) &&
request.method === 'GET'
// '' is the initial state, 'done' and 'dismissed' are the final states
const hasValidOnboardingStatus =
(status !== undefined && status.length === 0) ||
!(status === 'done' || status === 'dismissed')
const shouldRedirectToOnboarding =
notEnRouteToOnboarding && hasValidOnboardingStatus
const status = persistedSettings.onboardingStatus || ''
const notEnRouteToOnboarding = !request.url.includes(
paths.ONBOARDING.INDEX
)
// '' is the initial state, 'done' and 'dismissed' are the final states
const hasValidOnboardingStatus =
status.length === 0 || !(status === 'done' || status === 'dismissed')
const shouldRedirectToOnboarding =
notEnRouteToOnboarding && hasValidOnboardingStatus
if (shouldRedirectToOnboarding) {
return redirect(
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status.slice(1)
)
if (shouldRedirectToOnboarding) {
return redirect(
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status
)
}
}
const defaultDir = persistedSettings.defaultDirectory || ''
if (params.id && params.id !== BROWSER_FILE_NAME) {
const decodedId = decodeURIComponent(params.id)
const projectAndFile = decodedId.replace(defaultDir + sep, '')
const firstSlashIndex = projectAndFile.indexOf(sep)
const projectName = projectAndFile.slice(0, firstSlashIndex)
const projectPath = defaultDir + sep + projectName
const currentFileName = projectAndFile.slice(firstSlashIndex + 1)
if (firstSlashIndex === -1 || !currentFileName)
return redirect(
`${paths.FILE}/${encodeURIComponent(
`${params.id}${sep}${PROJECT_ENTRYPOINT}`
)}`
)
if (params.id && params.id !== 'new') {
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
const code = await readTextFile(decodedId)
const entrypointMetadata = await metadata(
projectPath + sep + PROJECT_ENTRYPOINT
const code = await readTextFile(params.id + '/' + PROJECT_ENTRYPOINT)
const entrypoint_metadata = await metadata(
params.id + '/' + PROJECT_ENTRYPOINT
)
const children = await readDir(projectPath, { recursive: true })
kclManager.setCodeAndExecute(code, false)
const children = await readDir(params.id)
return {
code,
project: {
name: projectName,
path: projectPath,
children,
entrypointMetadata,
},
file: {
name: currentFileName,
name: params.id.slice(params.id.lastIndexOf('/') + 1),
path: params.id,
children,
entrypoint_metadata,
},
}
}
@ -250,31 +162,17 @@ const router = createBrowserRouter(
),
loader: async () => {
if (!isTauri()) {
return redirect(paths.FILE + '/' + BROWSER_FILE_NAME)
return redirect(paths.FILE + '/new')
}
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
ContextFrom<typeof settingsMachine>
>
const projectDir = await initializeProjectDirectory(
persistedSettings.defaultDirectory || ''
)
if (projectDir !== persistedSettings.defaultDirectory) {
localStorage.setItem(
SETTINGS_PERSIST_KEY,
JSON.stringify({
...persistedSettings,
defaultDirectory: projectDir,
})
)
}
const projectsNoMeta = (await readDir(projectDir)).filter(
const projectDir = await initializeProjectDirectory()
const projectsNoMeta = (await readDir(projectDir.dir)).filter(
isProjectDirectory
)
const projects = await Promise.all(
projectsNoMeta.map(async (p: FileEntry) => ({
entrypointMetadata: await metadata(
p.path + sep + PROJECT_ENTRYPOINT
projectsNoMeta.map(async (p) => ({
entrypoint_metadata: await metadata(
p.path + '/' + PROJECT_ENTRYPOINT
),
...p,
}))

View File

@ -1,106 +0,0 @@
.toolbarWrapper {
@apply relative;
}
.toolbar {
@apply flex gap-4 items-center rounded-full;
@apply border border-cool-20/30 bg-cool-10/50;
}
:global(.dark) .toolbar {
@apply border-cool-100/50 bg-cool-120/50;
}
:global(.sketch) .toolbar {
@apply border-fern-20/20 bg-fern-10/20;
}
:global(.dark .sketch) .toolbar {
@apply border-fern-120/50 bg-fern-100/30;
}
.toolbarCap {
@apply text-sm font-bold;
@apply bg-cool-20/50 text-cool-100;
}
:global(.dark) .toolbarCap {
@apply bg-cool-90/50 text-cool-30;
}
:global(.sketch) .toolbarCap {
@apply bg-fern-20/50 text-fern-100;
}
:global(.dark .sketch) .toolbarCap {
@apply bg-fern-90/50 text-fern-30;
}
.label {
@apply self-stretch flex items-center px-4 py-1;
@apply rounded-l-full;
}
.popoverToggle {
@apply self-stretch m-0 flex items-center px-4 py-1;
@apply rounded-r-full border-none;
@apply hover:bg-cool-20;
}
.toolbarButtons::-webkit-scrollbar {
@apply h-0.5;
}
.toolbarButtons {
@apply flex items-center overflow-x-auto;
scrollbar-width: thin;
}
.toolbarButtons button {
@apply text-chalkboard-90 bg-chalkboard-10/50 border-chalkboard-50 whitespace-nowrap;
display: inline-flex;
align-items: center;
justify-content: center;
@apply gap-1.5 p-0.5 pr-1;
@apply rounded-sm;
}
:global(.dark) .toolbarButtons button {
@apply text-chalkboard-30 bg-chalkboard-90/50 border-chalkboard-50;
}
.toolbarButtons button:hover {
@apply text-cool-90 bg-cool-10;
}
:global(.sketch) .toolbarButtons button:hover {
@apply text-fern-90 bg-fern-10;
}
.toolbarButtons button:disabled {
@apply text-chalkboard-70 bg-chalkboard-30;
}
.toolbarButtons button:disabled:hover {
@apply !bg-inherit !text-inherit cursor-not-allowed;
}
:global(.dark) .toolbarButtons button {
@apply text-chalkboard-20 border-chalkboard-50;
}
:global(.dark) .toolbarButtons button:hover {
@apply text-cool-10 border-chalkboard-50 bg-cool-90;
}
:global(.dark .sketch) .toolbarButtons button:hover {
@apply text-fern-10 border-chalkboard-50 bg-fern-90;
}
:global(.dark) .toolbarButtons button:disabled {
@apply text-chalkboard-40 bg-chalkboard-80;
}
:global(.dark) .popoverToggle {
@apply hover:bg-cool-90;
}
:global(.sketch) .popoverToggle {
@apply hover:bg-fern-20;
}
:global(.dark .sketch) .popoverToggle {
@apply hover:bg-fern-90;
}

View File

@ -1,226 +1,178 @@
import { Fragment, WheelEvent, useRef, useMemo } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
import { Popover, Transition } from '@headlessui/react'
import styles from './Toolbar.module.css'
import { isCursorInSketchCommandRange } from 'lang/util'
import { ActionIcon } from 'components/ActionIcon'
import { engineCommandManager } from './lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext'
export const sketchButtonClassnames = {
background:
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-fern-20 dark:group-hover:bg-fern-10 dark:hover:bg-fern-10 group-disabled:bg-chalkboard-50 dark:group-disabled:bg-chalkboard-60 group-hover:group-disabled:bg-chalkboard-50 dark:group-hover:group-disabled:bg-chalkboard-50',
icon: 'text-fern-20 h-auto group-hover:text-fern-10 hover:text-fern-10 dark:text-chalkboard-100 dark:group-hover:text-chalkboard-100 dark:hover:text-chalkboard-100 group-disabled:bg-chalkboard-60 hover:group-disabled:text-inherit',
}
import { useStore, toolTips } from './useStore'
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
import { getNodePathFromSourceRange } from './lang/queryAst'
import { HorzVert } from './components/Toolbar/HorzVert'
import { RemoveConstrainingValues } from './components/Toolbar/RemoveConstrainingValues'
import { EqualLength } from './components/Toolbar/EqualLength'
import { EqualAngle } from './components/Toolbar/EqualAngle'
import { Intersect } from './components/Toolbar/Intersect'
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
import { SetAngleLength } from './components/Toolbar/setAngleLength'
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
export const Toolbar = () => {
const { state, send, context } = useModelingContext()
const toolbarButtonsRef = useRef<HTMLSpanElement>(null)
const pathId = useMemo(
() =>
isCursorInSketchCommandRange(
engineCommandManager.artifactMap,
context.selectionRanges
),
[engineCommandManager.artifactMap, context.selectionRanges]
)
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
const span = toolbarButtonsRef.current
if (!span) {
return
}
span.scrollLeft = span.scrollLeft += ev.deltaY
}
function ToolbarButtons({ className }: React.HTMLAttributes<HTMLElement>) {
return (
<span
ref={toolbarButtonsRef}
onWheel={handleToolbarButtonsWheelEvent}
className={styles.toolbarButtons + ' ' + className}
>
{state.nextEvents.includes('Enter sketch') && (
<button
onClick={() => send({ type: 'Enter sketch' })}
className="group"
>
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
<span data-testid="start-sketch">Start Sketch</span>
</button>
)}
{state.nextEvents.includes('Enter sketch') && pathId && (
<button
onClick={() => send({ type: 'Enter sketch' })}
className="group"
>
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
Edit Sketch
</button>
)}
{state.nextEvents.includes('Cancel') && !state.matches('idle') && (
<button onClick={() => send({ type: 'Cancel' })} className="group">
<ActionIcon icon="exit" className="!p-0.5" size="md" />
Exit Sketch
</button>
)}
{state.matches('Sketch') && !state.matches('idle') && (
<button
onClick={() =>
state.matches('Sketch.Line Tool')
? send('CancelSketch')
: send('Equip tool')
}
className={
'group ' +
(state.matches('Sketch.Line Tool')
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
: '')
}
>
<ActionIcon icon="line" className="!p-0.5" size="md" />
Line
</button>
)}
{state.matches('Sketch') && (
<button
onClick={() =>
state.matches('Sketch.Move Tool')
? send('CancelSketch')
: send('Equip move tool')
}
className={
'group ' +
(state.matches('Sketch.Move Tool')
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
: '')
}
>
<ActionIcon icon="move" className="!p-0.5" size="md" />
Move
</button>
)}
{state.matches('Sketch.SketchIdle') &&
state.nextEvents
.filter(
(eventName) =>
eventName.includes('Make segment') ||
eventName.includes('Constrain')
)
.sort((a, b) => {
const aisEnabled = state.nextEvents
.filter((event) => state.can(event as any))
.includes(a)
const bIsEnabled = state.nextEvents
.filter((event) => state.can(event as any))
.includes(b)
if (aisEnabled && !bIsEnabled) {
return -1
}
if (!aisEnabled && bIsEnabled) {
return 1
}
return 0
})
.map((eventName) => (
<button
key={eventName}
onClick={() => send(eventName)}
className="group"
disabled={
!state.nextEvents
.filter((event) => state.can(event as any))
.includes(eventName)
}
title={eventName}
>
<ActionIcon
icon={'line'} // TODO
bgClassName={sketchButtonClassnames.background}
iconClassName={sketchButtonClassnames.icon}
size="md"
/>
{eventName
.replace('Make segment ', '')
.replace('Constrain ', '')}
</button>
))}
{state.matches('idle') && (
<button
onClick={() => send('extrude intent')}
disabled={!state.can('extrude intent')}
className="group"
title={
state.can('extrude intent')
? 'extrude'
: 'sketches need to be closed, or not already extruded'
}
>
<ActionIcon icon="extrude" className="!p-0.5" size="md" />
Extrude
</button>
)}
</span>
)
}
const {
setGuiMode,
guiMode,
selectionRanges,
ast,
updateAst,
programMemory,
} = useStore((s) => ({
guiMode: s.guiMode,
setGuiMode: s.setGuiMode,
selectionRanges: s.selectionRanges,
ast: s.ast,
updateAst: s.updateAst,
programMemory: s.programMemory,
}))
return (
<Popover
className={
styles.toolbarWrapper + state.matches('Sketch') ? ' sketch' : ''
}
>
<div className={styles.toolbar}>
<span className={styles.toolbarCap + ' ' + styles.label}>
{state.matches('Sketch') ? '2D' : '3D'}
</span>
<menu className="flex-1 gap-2 py-0.5 overflow-hidden whitespace-nowrap">
<ToolbarButtons />
</menu>
<Popover.Button
className={styles.toolbarCap + ' ' + styles.popoverToggle}
<div>
{guiMode.mode === 'default' && (
<button
onClick={() => {
setGuiMode({
mode: 'sketch',
sketchMode: 'selectFace',
})
}}
>
<FontAwesomeIcon icon={faSearch} />
</Popover.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition ease-out duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Popover.Overlay className="fixed inset-0 bg-chalkboard-110/20 dark:bg-chalkboard-110/50" />
</Transition>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="opacity-0 translate-y-1 scale-95"
enterTo="opacity-100 translate-y-0 scale-100"
leave="transition ease-out duration-75"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-2"
>
<Popover.Panel className="absolute top-0 w-screen max-w-xl left-1/2 -translate-x-1/2 flex flex-col gap-8 bg-chalkboard-10 dark:bg-chalkboard-100 p-5 rounded border border-chalkboard-20/30 dark:border-chalkboard-70/50">
<section className="flex justify-between items-center">
<p
className={`${styles.toolbarCap} ${styles.label} !self-center rounded-r-full w-fit`}
Start Sketch
</button>
)}
{guiMode.mode === 'canEditExtrude' && (
<button
onClick={() => {
if (!ast) return
const pathToNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const { modifiedAst } = sketchOnExtrudedFace(
ast,
pathToNode,
programMemory
)
updateAst(modifiedAst)
}}
>
SketchOnFace
</button>
)}
{(guiMode.mode === 'canEditSketch' || false) && (
<button
onClick={() => {
setGuiMode({
mode: 'sketch',
sketchMode: 'sketchEdit',
pathToNode: guiMode.pathToNode,
rotation: guiMode.rotation,
position: guiMode.position,
})
}}
>
Edit Sketch
</button>
)}
{guiMode.mode === 'canEditSketch' && (
<>
<button
onClick={() => {
if (!ast) return
const pathToNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
ast,
pathToNode
)
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
}}
>
ExtrudeSketch
</button>
<button
onClick={() => {
if (!ast) return
const pathToNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
ast,
pathToNode,
false
)
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
}}
>
ExtrudeSketch (w/o pipe)
</button>
</>
)}
{guiMode.mode === 'sketch' && (
<button onClick={() => setGuiMode({ mode: 'default' })}>
Exit sketch
</button>
)}
{toolTips
.filter(
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
(sketchFnName) => ['line'].includes(sketchFnName)
)
.map((sketchFnName) => {
if (
guiMode.mode !== 'sketch' ||
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
)
return null
return (
<button
key={sketchFnName}
onClick={() =>
setGuiMode({
...guiMode,
...(guiMode.sketchMode === sketchFnName
? {
sketchMode: 'sketchEdit',
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
}
: {
sketchMode: sketchFnName,
isTooltip: true,
}),
})
}
>
You're in {state.matches('Sketch') ? '2D' : '3D'}
</p>
<Popover.Button className="p-2 flex items-center justify-center rounded-sm bg-chalkboard-20 text-chalkboard-110 dark:bg-chalkboard-70 dark:text-chalkboard-20 border-none hover:bg-chalkboard-30 dark:hover:bg-chalkboard-60">
<FontAwesomeIcon icon={faX} className="w-4 h-4" />
</Popover.Button>
</section>
<section>
<ToolbarButtons className="flex-wrap" />
</section>
</Popover.Panel>
</Transition>
</Popover>
{sketchFnName}
{guiMode.sketchMode === sketchFnName && '✅'}
</button>
)
})}
<br></br>
<ConvertToVariable />
<HorzVert horOrVert="horizontal" />
<HorzVert horOrVert="vertical" />
<EqualLength />
<EqualAngle />
<SetHorzVertDistance buttonType="alignEndsVertically" />
<SetHorzVertDistance buttonType="setHorzDistance" />
<SetAbsDistance buttonType="snapToYAxis" />
<SetAbsDistance buttonType="xAbs" />
<SetHorzVertDistance buttonType="alignEndsHorizontally" />
<SetAbsDistance buttonType="snapToXAxis" />
<SetHorzVertDistance buttonType="setVertDistance" />
<SetAbsDistance buttonType="yAbs" />
<SetAngleLength angleOrLength="setAngle" />
<SetAngleLength angleOrLength="setLength" />
<Intersect />
<RemoveConstrainingValues />
<SetAngleBetween />
</div>
)
}

View File

@ -23,7 +23,10 @@ type ActionButtonAsLink = BaseActionButtonProps &
}
type ActionButtonAsExternal = BaseActionButtonProps &
Omit<LinkProps, keyof BaseActionButtonProps> & {
Omit<
React.AnchorHTMLAttributes<HTMLAnchorElement>,
keyof BaseActionButtonProps
> & {
Element: 'externalLink'
}
@ -66,17 +69,12 @@ export const ActionButton = (props: ActionButtonProps) => {
)
}
case 'externalLink': {
const { Element, to, icon, children, className, ...rest } = props
const { Element, icon, children, className, ...rest } = props
return (
<Link
to={to || paths.INDEX}
className={classNames}
{...rest}
target="_blank"
>
<a className={classNames} {...rest}>
{icon && <ActionIcon {...icon} />}
{children}
</Link>
</a>
)
}
default: {

View File

@ -4,18 +4,15 @@ import {
} from '@fortawesome/free-solid-svg-icons'
import { IconDefinition as BrandIconDefinition } from '@fortawesome/free-brands-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { CustomIcon, CustomIconName } from './CustomIcon'
const iconSizes = {
sm: 12,
md: 14.4,
lg: 20,
xl: 28,
lg: 18,
}
export interface ActionIconProps extends React.PropsWithChildren {
icon?: SolidIconDefinition | BrandIconDefinition | CustomIconName
className?: string
icon?: SolidIconDefinition | BrandIconDefinition
bgClassName?: string
iconClassName?: string
size?: keyof typeof iconSizes
@ -23,45 +20,28 @@ export interface ActionIconProps extends React.PropsWithChildren {
export const ActionIcon = ({
icon = faCircleExclamation,
className,
bgClassName,
iconClassName,
size = 'md',
children,
}: ActionIconProps) => {
// By default, we reverse the icon color and background color in dark mode
const computedIconClassName =
iconClassName ||
`text-liquid-20 h-auto group-hover:text-liquid-10 hover:text-liquid-10 dark:text-chalkboard-100 dark:group-hover:text-chalkboard-100 dark:hover:text-chalkboard-100 group-disabled:bg-chalkboard-50 dark:group-disabled:bg-chalkboard-60 group-hover:group-disabled:bg-chalkboard-50 dark:group-hover:group-disabled:bg-chalkboard-50`
const computedBgClassName =
bgClassName ||
`bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10 group-disabled:bg-chalkboard-80 dark:group-disabled:bg-chalkboard-80`
return (
<div
className={
`p-${
size === 'xl' ? '2' : '1'
} w-fit inline-grid place-content-center ${className} ` +
computedBgClassName
'p-1 w-fit inline-grid place-content-center ' +
(bgClassName ||
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10')
}
>
{children ? (
children
) : typeof icon === 'string' ? (
<CustomIcon
name={icon}
width={iconSizes[size]}
height={iconSizes[size]}
className={computedIconClassName}
/>
) : (
{children || (
<FontAwesomeIcon
icon={icon}
width={iconSizes[size]}
height={iconSizes[size]}
className={computedIconClassName}
className={
iconClassName ||
'text-liquid-20 group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100'
}
/>
)}
</div>

View File

@ -1,7 +0,0 @@
/*
Some CSS cannot be represented
in Tailwind, such as complex grid layouts.
*/
.header {
grid-template-columns: 1fr auto 1fr;
}

View File

@ -1,14 +1,12 @@
import { Toolbar } from '../Toolbar'
import UserSidebarMenu from './UserSidebarMenu'
import { IndexLoaderData } from '../Router'
import { ProjectWithEntryPointMetadata } from '../Router'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import styles from './AppHeader.module.css'
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
import { useAuthMachine } from '../hooks/useAuthMachine'
interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean
project?: Omit<IndexLoaderData, 'code'>
project?: ProjectWithEntryPointMetadata
className?: string
enableMenu?: boolean
}
@ -20,36 +18,24 @@ export const AppHeader = ({
className = '',
enableMenu = false,
}: AppHeaderProps) => {
const { auth } = useGlobalStateContext()
const user = auth?.context?.user
const [user] = useAuthMachine((s) => s?.context?.user)
return (
<header
className={
(showToolbar ? 'w-full grid ' : 'flex justify-between ') +
styles.header +
' overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/70 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 items-center ' +
'overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/50 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 flex justify-between items-center ' +
className
}
>
<ProjectSidebarMenu
renderAsLink={!enableMenu}
project={project?.project}
file={project?.file}
/>
<ProjectSidebarMenu renderAsLink={!enableMenu} project={project} />
{/* Toolbar if the context deems it */}
{showToolbar && (
<div className="max-w-lg md:max-w-xl lg:max-w-2xl xl:max-w-4xl 2xl:max-w-5xl">
<div className="max-w-4xl">
<Toolbar />
</div>
)}
{/* If there are children, show them, otherwise show User menu */}
{children || (
<div className="flex items-center gap-1 ml-auto">
<NetworkHealthIndicator />
<UserSidebarMenu user={user} />
</div>
)}
{children || <UserSidebarMenu user={user} />}
</header>
)
}

View File

@ -1,194 +0,0 @@
import { useModelingContext } from 'hooks/useModelingContext'
import { kclManager } from 'lang/KclSinglton'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { useEffect, useRef, useState } from 'react'
import { useStore } from 'useStore'
export function AstExplorer() {
const setHighlightRange = useStore((s) => s.setHighlightRange)
const { context } = useModelingContext()
const pathToNode = getNodePathFromSourceRange(
// TODO maybe need to have callback to make sure it stays in sync
kclManager.ast,
context.selectionRanges.codeBasedSelections?.[0]?.range
)
const node = getNodeFromPath(kclManager.ast, pathToNode).node
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
return (
<div className="relative" style={{ width: '300px' }}>
<div className="">
filter out keys:<div className="w-2 inline-block"></div>
{['start', 'end', 'type'].map((key) => {
return (
<label key={key} className="inline-flex items-center">
<input
type="checkbox"
className="form-checkbox"
checked={filterKeys.includes(key)}
onChange={(e) => {
if (filterKeys.includes(key)) {
setFilterKeys(filterKeys.filter((k) => k !== key))
} else {
setFilterKeys([...filterKeys, key])
}
}}
/>
<span className="mr-2">{key}</span>
</label>
)
})}
</div>
<div
className="h-full relative"
onMouseLeave={(e) => {
setHighlightRange([0, 0])
}}
>
<pre className=" text-xs overflow-y-auto" style={{ width: '300px' }}>
<DisplayObj
obj={kclManager.ast}
filterKeys={filterKeys}
node={node}
/>
</pre>
</div>
</div>
)
}
function DisplayBody({
body,
filterKeys,
node,
}: {
body: { start: number; end: number; [key: string]: any }[]
filterKeys: string[]
node: any
}) {
return (
<>
{body.map((b, index) => {
return (
<div className="my-2" key={index}>
<DisplayObj obj={b} filterKeys={filterKeys} node={node} />
</div>
)
})}
</>
)
}
function DisplayObj({
obj,
filterKeys,
node,
}: {
obj: { start: number; end: number; [key: string]: any }
filterKeys: string[]
node: any
}) {
const setHighlightRange = useStore((s) => s.setHighlightRange)
const { send } = useModelingContext()
const ref = useRef<HTMLPreElement>(null)
const [hasCursor, setHasCursor] = useState(false)
const [isCollapsed, setIsCollapsed] = useState(false)
useEffect(() => {
if (
node?.start === obj?.start &&
node?.end === obj?.end &&
node.type === obj?.type
) {
ref?.current?.scrollIntoView?.({ behavior: 'smooth', block: 'center' })
setHasCursor(true)
} else {
setHasCursor(false)
}
}, [node.start, node.end, node.type])
return (
<pre
ref={ref}
className={`ml-2 border-l border-violet-600 pl-1 ${
hasCursor ? 'bg-violet-100/25' : ''
}`}
onMouseEnter={(e) => {
setHighlightRange([obj?.start || 0, obj.end])
e.stopPropagation()
}}
onMouseMove={(e) => {
e.stopPropagation()
setHighlightRange([obj?.start || 0, obj.end])
}}
onClick={(e) => {
send({
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: {
type: 'default',
range: [obj?.start || 0, obj.end || 0],
},
},
})
e.stopPropagation()
}}
>
{isCollapsed ? (
<button
className="m-0 p-0 border-0"
onClick={() => setIsCollapsed(false)}
>
{'>'}type: {obj.type}
</button>
) : (
<span className="flex">
{/* <button className="m-0 p-0 border-0 mb-auto" onClick={() => setIsCollapsed(true)}>{'⬇️'}</button> */}
<ul className="inline-block">
{Object.entries(obj).map(([key, value]) => {
if (filterKeys.includes(key)) {
return null
} else if (Array.isArray(value)) {
return (
<li key={key}>
{`${key}: [`}
<DisplayBody
body={value}
filterKeys={filterKeys}
node={node}
/>
{']'}
</li>
)
} else if (
typeof value === 'object' &&
value !== null &&
value?.end
) {
return (
<li key={key}>
{key}:
<DisplayObj
obj={value}
filterKeys={filterKeys}
node={node}
/>
</li>
)
} else if (
typeof value === 'string' ||
typeof value === 'number'
) {
return (
<li key={key}>
{key}: {value}
</li>
)
}
return null
})}
</ul>
</span>
)}
</pre>
)
}

View File

@ -1,5 +1,7 @@
import { useEffect, useState, useRef } from 'react'
import { parse, BinaryPart, Value } from '../lang/wasm'
import { parser_wasm } from '../lang/abstractSyntaxTree'
import { BinaryPart, Value } from '../lang/abstractSyntaxTreeTypes'
import { executor } from '../lang/executor'
import {
createIdentifier,
createLiteral,
@ -7,10 +9,7 @@ import {
findUniqueName,
} from '../lang/modifyAst'
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
import { engineCommandManager } from '../lang/std/engineConnection'
import { kclManager, useKclContext } from 'lang/KclSinglton'
import { useModelingContext } from 'hooks/useModelingContext'
import { executeAst } from 'useStore'
import { useStore } from '../useStore'
export const AvailableVars = ({
onVarClick,
@ -93,9 +92,14 @@ export function useCalc({
newVariableInsertIndex: number
setNewVariableName: (a: string) => void
} {
const { programMemory } = useKclContext()
const { context } = useModelingContext()
const selectionRange = context.selectionRanges.codeBasedSelections[0].range
const { ast, programMemory, selectionRange, engineCommandManager } = useStore(
(s) => ({
ast: s.ast,
programMemory: s.programMemory,
selectionRange: s.selectionRanges.codeBasedSelections[0].range,
engineCommandManager: s.engineCommandManager,
})
)
const inputRef = useRef<HTMLInputElement>(null)
const [availableVarInfo, setAvailableVarInfo] = useState<
ReturnType<typeof findAllPreviousVariables>
@ -115,7 +119,9 @@ export function useCalc({
inputRef.current &&
inputRef.current.setSelectionRange(0, String(value).length)
}, 100)
setNewVariableName(findUniqueName(kclManager.ast, valueName))
if (ast) {
setNewVariableName(findUniqueName(ast, valueName))
}
}, [])
useEffect(() => {
@ -128,32 +134,21 @@ export function useCalc({
}, [newVariableName])
useEffect(() => {
if (!programMemory || !selectionRange) return
const varInfo = findAllPreviousVariables(
kclManager.ast,
kclManager.programMemory,
selectionRange
)
if (!ast || !programMemory || !selectionRange) return
const varInfo = findAllPreviousVariables(ast, programMemory, selectionRange)
setAvailableVarInfo(varInfo)
}, [kclManager.ast, kclManager.programMemory, selectionRange])
}, [ast, programMemory, selectionRange])
useEffect(() => {
if (!engineCommandManager) return
try {
const code = `const __result__ = ${value}`
const ast = parse(code)
const _programMem: any = { root: {}, return: null }
const code = `const __result__ = ${value}\nshow(__result__)`
const ast = parser_wasm(code)
const _programMem: any = { root: {} }
availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
})
executeAst({
ast,
engineCommandManager,
defaultPlanes: kclManager.defaultPlanes,
useFakeExecutor: true,
programMemoryOverride: JSON.parse(
JSON.stringify(kclManager.programMemory)
),
}).then(({ programMemory }) => {
executor(ast, _programMem, engineCommandManager).then((programMemory) => {
const resultDeclaration = ast.body.find(
(a) =>
a.type === 'VariableDeclaration' &&
@ -170,7 +165,7 @@ export function useCalc({
setCalcResult('NAN')
setValueNode(null)
}
}, [value, availableVarInfo])
}, [value])
return {
valueNode,
@ -203,33 +198,33 @@ export const CreateNewVariable = ({
isNewVariableNameUnique,
setNewVariableName,
shouldCreateVariable,
setShouldCreateVariable = () => {},
setShouldCreateVariable,
showCheckbox = true,
}: {
isNewVariableNameUnique: boolean
newVariableName: string
setNewVariableName: (a: string) => void
shouldCreateVariable?: boolean
setShouldCreateVariable?: (a: boolean) => void
shouldCreateVariable: boolean
setShouldCreateVariable: (a: boolean) => void
showCheckbox?: boolean
}) => {
return (
<>
<label
htmlFor="create-new-variable"
className="block mt-3 font-mono text-gray-900"
className="block text-sm font-medium text-gray-700 mt-3 font-mono"
>
Create new variable
</label>
<div className="mt-1 flex gap-2 items-center">
<div className="mt-1 flex flex-1">
{showCheckbox && (
<input
type="checkbox"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink"
checked={shouldCreateVariable}
onChange={(e) => {
setShouldCreateVariable(e.target.checked)
}}
className="bg-white text-gray-900"
/>
)}
<input
@ -237,10 +232,7 @@ export const CreateNewVariable = ({
disabled={!shouldCreateVariable}
name="create-new-variable"
id="create-new-variable"
autoFocus={true}
autoCapitalize="off"
autoCorrect="off"
className={`font-mono flex-1 sm:text-sm px-2 py-1 rounded-sm bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-90 dark:text-chalkboard-10 ${
className={`shadow-sm font-[monospace] focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink-0 ${
!shouldCreateVariable ? 'opacity-50' : ''
}`}
value={newVariableName}

View File

@ -1,19 +0,0 @@
.button {
@apply flex justify-between items-center gap-2 px-2 py-1 text-left border-none rounded-sm;
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
@apply ui-active:bg-liquid-10/50 ui-active:text-liquid-90;
@apply transition-colors ease-out;
}
:global(.dark) .button {
@apply text-chalkboard-30;
@apply ui-active:bg-chalkboard-80 ui-active:text-liquid-10;
}
.button small {
@apply text-chalkboard-60;
}
:global(.dark) .button small {
@apply text-chalkboard-40;
}

View File

@ -1,82 +0,0 @@
import { Menu } from '@headlessui/react'
import { PropsWithChildren } from 'react'
import {
faArrowUpRightFromSquare,
faEllipsis,
} from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from './ActionIcon'
import styles from './CodeMenu.module.css'
import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { editorShortcutMeta } from './TextEditor'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { kclManager } from 'lang/KclSinglton'
export const CodeMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
useConvertToVariable()
return (
<Menu>
<div
className="relative"
onClick={(e) => {
const target = e.target as HTMLElement
if (e.eventPhase === 3 && target.closest('a') === null) {
e.stopPropagation()
e.preventDefault()
}
}}
>
<Menu.Button className="p-0 border-none relative">
<ActionIcon
icon={faEllipsis}
bgClassName={
'bg-chalkboard-20 dark:bg-chalkboard-110 hover:bg-liquid-10/50 hover:dark:bg-chalkboard-90 ui-active:bg-chalkboard-80 ui-active:dark:bg-chalkboard-90 rounded'
}
iconClassName={'text-chalkboard-90 dark:text-chalkboard-40'}
/>
</Menu.Button>
<Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50">
<Menu.Item>
<button
onClick={() => kclManager.format()}
className={styles.button}
>
<span>Format code</span>
<small>{editorShortcutMeta.formatCode.display}</small>
</button>
</Menu.Item>
{convertToVarEnabled && (
<Menu.Item>
<button
onClick={handleConvertToVarClick}
className={styles.button}
>
<span>Convert to Variable</span>
<small>{editorShortcutMeta.convertToVariable.display}</small>
</button>
</Menu.Item>
)}
<Menu.Item>
<a
className={styles.button}
href="https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md"
target="_blank"
rel="noopener noreferrer"
>
<span>Read the KCL docs</span>
<small>
On GitHub
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1 align-text-top"
width={12}
/>
</small>
</a>
</Menu.Item>
</Menu.Items>
</div>
</Menu>
)
}

View File

@ -1,15 +1,15 @@
.panel {
@apply relative z-0;
@apply bg-chalkboard-10/70 backdrop-blur-sm;
@apply relative overflow-auto z-0;
@apply bg-chalkboard-20/40;
}
:global(.dark) .panel {
@apply bg-chalkboard-110/50 backdrop-blur-0;
@apply bg-chalkboard-110/50;
}
.header {
@apply sticky top-0 z-10 cursor-pointer;
@apply flex items-center justify-between gap-2 w-full p-2;
@apply flex items-center gap-2 w-full p-2;
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
@apply bg-chalkboard-20;
}

View File

@ -8,8 +8,6 @@ export interface CollapsiblePanelProps
title: string
icon?: IconDefinition
open?: boolean
menu?: React.ReactNode
detailsTestId?: string
iconClassNames?: {
bg?: string
icon?: string
@ -20,27 +18,21 @@ export const PanelHeader = ({
title,
icon,
iconClassNames,
menu,
}: CollapsiblePanelProps) => {
return (
<summary className={styles.header}>
<div className="flex gap-2 align-center flex-1">
<ActionIcon
icon={icon}
bgClassName={
'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
(iconClassNames?.bg || '')
}
iconClassName={
'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
(iconClassNames?.icon || '')
}
/>
{title}
</div>
<div className="group-open:opacity-100 opacity-0 group-open:pointer-events-auto pointer-events-none">
{menu}
</div>
<ActionIcon
icon={icon}
bgClassName={
'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
(iconClassNames?.bg || '')
}
iconClassName={
'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
(iconClassNames?.icon || '')
}
/>
{title}
</summary>
)
}
@ -51,22 +43,14 @@ export const CollapsiblePanel = ({
children,
className,
iconClassNames,
menu,
detailsTestId,
...props
}: CollapsiblePanelProps) => {
return (
<details
{...props}
data-testid={detailsTestId}
className={styles.panel + ' group ' + (className || '')}
>
<PanelHeader
title={title}
icon={icon}
iconClassNames={iconClassNames}
menu={menu}
/>
<PanelHeader title={title} icon={icon} iconClassNames={iconClassNames} />
{children}
</details>
)

View File

@ -1,290 +0,0 @@
import { Combobox, Dialog, Transition } from '@headlessui/react'
import {
Dispatch,
Fragment,
SetStateAction,
createContext,
useState,
} from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { ActionIcon } from './ActionIcon'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import Fuse from 'fuse.js'
import { Command, SubCommand } from '../lib/commands'
import { useCommandsContext } from 'hooks/useCommandsContext'
export type SortedCommand = {
item: Partial<Command | SubCommand> & { name: string }
}
export const CommandsContext = createContext(
{} as {
commands: Command[]
addCommands: (commands: Command[]) => void
removeCommands: (commands: Command[]) => void
commandBarOpen: boolean
setCommandBarOpen: Dispatch<SetStateAction<boolean>>
}
)
export const CommandBarProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const [commands, internalSetCommands] = useState([] as Command[])
const [commandBarOpen, setCommandBarOpen] = useState(false)
const addCommands = (newCommands: Command[]) => {
internalSetCommands((prevCommands) => [...newCommands, ...prevCommands])
}
const removeCommands = (newCommands: Command[]) => {
internalSetCommands((prevCommands) =>
prevCommands.filter((command) => !newCommands.includes(command))
)
}
return (
<CommandsContext.Provider
value={{
commands,
addCommands,
removeCommands,
commandBarOpen,
setCommandBarOpen,
}}
>
{children}
<CommandBar />
</CommandsContext.Provider>
)
}
const CommandBar = () => {
const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
useHotkeys(['meta+k', 'meta+/'], () => {
if (commands.length === 0) return
setCommandBarOpen(!commandBarOpen)
})
const [selectedCommand, setSelectedCommand] = useState<SortedCommand | null>(
null
)
// keep track of the current subcommand index
const [subCommandIndex, setSubCommandIndex] = useState<number>()
const [subCommandData, setSubCommandData] = useState<{
[key: string]: string
}>({})
// if the subcommand index is null, we're not in a subcommand
const inSubCommand =
selectedCommand &&
'meta' in selectedCommand.item &&
selectedCommand.item.meta?.args !== undefined &&
subCommandIndex !== undefined
const currentSubCommand =
inSubCommand && 'meta' in selectedCommand.item
? selectedCommand.item.meta?.args[subCommandIndex]
: undefined
const [query, setQuery] = useState('')
const availableCommands =
inSubCommand && currentSubCommand
? currentSubCommand.type === 'string'
? query
? [{ name: query }]
: currentSubCommand.options
: currentSubCommand.options
: commands
const fuse = new Fuse(availableCommands || [], {
keys: ['name', 'description'],
})
const filteredCommands = query
? fuse.search(query)
: availableCommands?.map((c) => ({ item: c } as SortedCommand))
function clearState() {
setQuery('')
setCommandBarOpen(false)
setSelectedCommand(null)
setSubCommandIndex(undefined)
setSubCommandData({})
}
function handleCommandSelection(entry: SortedCommand) {
// If we have subcommands and have not yet gathered all the
// data required from them, set the selected command to the
// current command and increment the subcommand index
if (selectedCommand === null && 'meta' in entry.item && entry.item.meta) {
setSelectedCommand(entry)
setSubCommandIndex(0)
setQuery('')
return
}
const { item } = entry
// If we have just selected a command with no subcommands, run it
const isCommandWithoutSubcommands =
'callback' in item && !('meta' in item && item.meta)
if (isCommandWithoutSubcommands) {
if (item.callback === undefined) return
item.callback()
setCommandBarOpen(false)
return
}
// If we have subcommands and have not yet gathered all the
// data required from them, set the selected command to the
// current command and increment the subcommand index
if (
selectedCommand &&
subCommandIndex !== undefined &&
'meta' in selectedCommand.item
) {
const subCommand = selectedCommand.item.meta?.args[subCommandIndex]
if (subCommand) {
const newSubCommandData = {
...subCommandData,
[subCommand.name]: item.name,
}
const newSubCommandIndex = subCommandIndex + 1
// If we have subcommands and have gathered all the data required
// from them, run the command with the gathered data
if (
selectedCommand.item.callback &&
selectedCommand.item.meta?.args.length === newSubCommandIndex
) {
selectedCommand.item.callback(newSubCommandData)
setCommandBarOpen(false)
} else {
// Otherwise, set the subcommand data and increment the subcommand index
setSubCommandData(newSubCommandData)
setSubCommandIndex(newSubCommandIndex)
setQuery('')
}
}
}
}
function getDisplayValue(command: Command) {
if (command.meta?.displayValue === undefined || !command.meta.args)
return command.name
return command.meta?.displayValue(
command.meta.args.map((c) =>
subCommandData[c.name] ? subCommandData[c.name] : `<${c.name}>`
)
)
}
return (
<Transition.Root
show={
commandBarOpen &&
availableCommands?.length !== undefined &&
availableCommands.length > 0
}
as={Fragment}
afterLeave={() => clearState()}
>
<Dialog
onClose={() => {
setCommandBarOpen(false)
clearState()
}}
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
>
<Transition.Child
enter="duration-100 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-75 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as={Fragment}
>
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
</Transition.Child>
<Transition.Child
enter="duration-100 ease-out"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-75 ease-in"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
as={Fragment}
>
<Combobox
value={selectedCommand}
onChange={handleCommandSelection}
className="relative w-full max-w-xl p-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
as="div"
>
<div className="flex items-center gap-2">
<ActionIcon icon={faSearch} size="xl" className="rounded-sm" />
<div>
{inSubCommand && (
<p className="text-liquid-70 dark:text-liquid-30">
{selectedCommand.item &&
getDisplayValue(selectedCommand.item as Command)}
</p>
)}
<Combobox.Input
onChange={(event) => setQuery(event.target.value)}
className="w-full bg-transparent focus:outline-none"
onKeyDown={(event) => {
if (event.metaKey && event.key === 'k')
setCommandBarOpen(false)
if (
inSubCommand &&
event.key === 'Backspace' &&
!event.currentTarget.value
) {
setSubCommandIndex(subCommandIndex - 1)
setSelectedCommand(null)
}
}}
displayValue={(command: SortedCommand) =>
command !== null ? command.item.name : ''
}
placeholder={
inSubCommand
? `Enter <${currentSubCommand?.name}>`
: 'Search for a command'
}
value={query}
autoCapitalize="off"
autoComplete="off"
autoCorrect="off"
spellCheck="false"
/>
</div>
</div>
<Combobox.Options static className="overflow-y-auto max-h-96">
{filteredCommands?.map((commandResult) => (
<Combobox.Option
key={commandResult.item.name}
value={commandResult}
className="px-2 py-1 my-2 first:mt-4 last:mb-4 ui-active:bg-liquid-10 dark:ui-active:bg-liquid-90"
>
<p>{commandResult.item.name}</p>
{(commandResult.item as SubCommand).description && (
<p className="mt-0.5 text-liquid-70 dark:text-liquid-30 text-sm">
{(commandResult.item as SubCommand).description}
</p>
)}
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
</Transition.Child>
</Dialog>
</Transition.Root>
)
}
export default CommandBarProvider

View File

@ -1,210 +0,0 @@
export type CustomIconName =
| 'createFile'
| 'createFolder'
| 'equal'
| 'exit'
| 'extrude'
| 'file'
| 'horizontal'
| 'line'
| 'move'
| 'parallel'
| 'sketch'
| 'vertical'
export const CustomIcon = ({
name,
...props
}: {
name: CustomIconName
} & React.SVGProps<SVGSVGElement>) => {
switch (name) {
case 'createFile':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13 12V14H11V15H13V17H14V15H16V14H14V12H13Z"
fill="currentColor"
/>
</svg>
)
case 'createFolder':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.5 3.5H4H7H7.16667L7.3 3.6L9.16667 5H16H16.5V5.5V7.5V10.3773C16.1992 10.1362 15.8628 9.93763 15.5 9.79076V8H4.5V15.5H10.5351C10.7529 15.8764 11.0302 16.2141 11.3542 16.5H4H3.5V16V7.5V4V3.5ZM4.5 4.5V7H15.5V6H9H8.83333L8.7 5.9L6.83333 4.5H4.5ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z"
fill="currentColor"
/>
</svg>
)
case 'equal':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 8.78V7H14.52V8.78H5ZM5 13.02V11.24H14.52V13.02H5Z"
fill="currentColor"
/>
</svg>
)
case 'exit':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17 10L3 10M3 10L6.5 6.5M3 10L6.5 13.5"
stroke="currentColor"
/>
</svg>
)
case 'extrude':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 3L10.3536 3.35355L12.3536 5.35355L11.6465 6.06066L10.5 4.91421V11.5854C11.0826 11.7913 11.5 12.3469 11.5 13C11.5 13.8284 10.8284 14.5 10 14.5C9.17157 14.5 8.5 13.8284 8.5 13C8.5 12.3469 8.91741 11.7913 9.5 11.5854V4.91421L8.35356 6.06066L7.64645 5.35355L9.64645 3.35355L10 3ZM1.95887 12.3282L8 8.63644V9.80838L2.91773 12.9142L10 17.2423L17.0823 12.9142L12 9.80838V8.63644L18.0411 12.3282L19 12.9142L19 14.9683H18V13.5253L10.5 18.1087V19.9683H9.5V18.1087L2 13.5253V14.9683H1L1 12.9142L1.95887 12.3282Z"
fill="currentColor"
/>
</svg>
)
case 'file':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 3.5H4.5V16.5H15.5V8.00001M11 3.5L15.5 8.00001M11 3.5V8.00001H15.5"
stroke="currentColor"
/>
</svg>
)
case 'horizontal':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4 9.5H16V11.5H4V9.5Z"
fill="currentColor"
/>
</svg>
)
case 'line':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.5 6C16.3284 6 17 5.32843 17 4.5C17 3.67157 16.3284 3 15.5 3C14.6716 3 14 3.67157 14 4.5C14 4.73107 14.0522 4.94993 14.1456 5.14543L5.14543 14.1456C4.94993 14.0522 4.73107 14 4.5 14C3.67157 14 3 14.6716 3 15.5C3 16.3284 3.67157 17 4.5 17C5.32843 17 6 16.3284 6 15.5C6 15.2679 5.94729 15.0482 5.8532 14.852L14.852 5.8532C15.0482 5.94729 15.2679 6 15.5 6Z"
fill="currentColor"
/>
</svg>
)
case 'move':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 2.29289L10.3536 2.64645L12.3536 4.64645L11.6465 5.35355L10.5 4.20711V8V9.50001H12L15.7929 9.50001L14.6465 8.35356L15.3536 7.64645L17.3536 9.64645L17.7071 10L17.3536 10.3536L15.3536 12.3536L14.6465 11.6465L15.7929 10.5H12H10.5V12V15.7929L11.6465 14.6464L12.3536 15.3536L10.3536 17.3536L10 17.7071L9.64645 17.3536L7.64645 15.3536L8.35356 14.6464L9.50001 15.7929V12V10.5H8.00001H4.20712L5.35357 11.6465L4.64646 12.3536L2.64646 10.3536L2.29291 10L2.64646 9.64645L4.64646 7.64645L5.35357 8.35356L4.20712 9.50001H8.00001H9.50001V8V4.20711L8.35356 5.35355L7.64645 4.64645L9.64645 2.64645L10 2.29289Z"
fill="currentColor"
/>
</svg>
)
case 'parallel':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 16V4H6V16H8ZM14 16V4H12V16H14Z"
fill="currentColor"
/>
</svg>
)
case 'sketch':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.8037 13.4035L15.5509 14.1635L16.3682 16.8386L13.5521 16.1346L12.8186 15.3885L14.8037 13.4035ZM14.1025 12.6903L12.1175 14.6754L3.48609 5.89624C2.94588 5.34678 2.94963 4.46456 3.49448 3.91971C4.04591 3.36828 4.94112 3.37208 5.48786 3.92817L14.1025 12.6903ZM6.20094 3.22709L16.4357 13.6371L17.5003 17.1216L17.8412 18.2376L16.7091 17.9546L13.0364 17.0364L2.77301 6.59732C1.84793 5.6564 1.85434 4.14564 2.78737 3.2126C3.73167 2.2683 5.26468 2.27481 6.20094 3.22709Z"
fill="currentColor"
/>
</svg>
)
case 'vertical':
return (
<svg
{...props}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11 4V16H9V4H11Z"
fill="currentColor"
/>
</svg>
)
}
}

View File

@ -1,24 +1,137 @@
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { AstExplorer } from './AstExplorer'
import { EngineCommands } from './EngineCommands'
import { useStore } from '../useStore'
import { v4 as uuidv4 } from 'uuid'
import { EngineCommand } from '../lang/std/engineConnection'
import { useState } from 'react'
import { ActionButton } from '../components/ActionButton'
import { faCheck } from '@fortawesome/free-solid-svg-icons'
type SketchModeCmd = Extract<
Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'],
{ type: 'default_camera_enable_sketch_mode' }
>
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
const { engineCommandManager } = useStore((s) => ({
engineCommandManager: s.engineCommandManager,
}))
const [sketchModeCmd, setSketchModeCmd] = useState<SketchModeCmd>({
type: 'default_camera_enable_sketch_mode',
origin: { x: 0, y: 0, z: 0 },
x_axis: { x: 1, y: 0, z: 0 },
y_axis: { x: 0, y: 1, z: 0 },
distance_to_plane: 100,
ortho: true,
animated: true, // TODO #273 get prefers reduced motion from CSS
})
if (!sketchModeCmd) return null
return (
<CollapsiblePanel
{...props}
className={
'!absolute overflow-auto !h-auto bottom-5 right-5 ' + className
}
// header height, top-5, and bottom-5
style={{ maxHeight: 'calc(100% - 3rem - 1.25rem - 1.25rem)' }}
detailsTestId="debug-panel"
className={'!absolute !h-auto bottom-5 right-5 ' + className}
>
<section className="p-4 flex flex-col gap-4">
<EngineCommands />
<div style={{ height: '400px' }} className="overflow-y-auto">
<AstExplorer />
<Xyz
onChange={setSketchModeCmd}
pointKey="origin"
data={sketchModeCmd}
/>
<Xyz
onChange={setSketchModeCmd}
pointKey="x_axis"
data={sketchModeCmd}
/>
<Xyz
onChange={setSketchModeCmd}
pointKey="y_axis"
data={sketchModeCmd}
/>
<div className="flex">
<div className="pr-4">distance_to_plane</div>
<input
className="w-16 dark:bg-chalkboard-90"
type="number"
value={sketchModeCmd.distance_to_plane}
onChange={({ target }) => {
setSketchModeCmd({
...sketchModeCmd,
distance_to_plane: Number(target.value),
})
}}
/>
<div className="pr-4">ortho</div>
<input
className="w-16"
type="checkbox"
checked={sketchModeCmd.ortho}
onChange={(a) => {
console.log(a, (a as any).checked)
setSketchModeCmd({
...sketchModeCmd,
ortho: a.target.checked,
})
}}
/>
</div>
<ActionButton
Element="button"
onClick={() => {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: sketchModeCmd,
cmd_id: uuidv4(),
})
}}
className="hover:border-succeed-50"
icon={{
icon: faCheck,
bgClassName:
'bg-succeed-80 group-hover:bg-succeed-70 hover:bg-succeed-70',
iconClassName:
'text-succeed-20 group-hover:text-succeed-10 hover:text-succeed-10',
}}
>
Send sketch mode command
</ActionButton>
</section>
</CollapsiblePanel>
)
}
const Xyz = ({
pointKey,
data,
onChange,
}: {
pointKey: 'origin' | 'y_axis' | 'x_axis'
data: SketchModeCmd
onChange: (a: SketchModeCmd) => void
}) => {
if (!data) return null
return (
<div className="flex">
<div className="pr-4">{pointKey}</div>
{Object.entries(data[pointKey]).map(([axis, val]) => {
return (
<div key={axis} className="flex">
<div className="w-4">{axis}</div>
<input
className="w-16 dark:bg-chalkboard-90"
type="number"
value={val}
onChange={({ target }) => {
onChange({
...data,
[pointKey]: {
...data[pointKey],
[axis]: Number(target.value),
},
})
}}
/>
</div>
)
})}
</div>
)
}

View File

@ -40,12 +40,12 @@ const DownloadAppBanner = () => {
</code>
, and isn't backed up anywhere! Visit{' '}
<a
href="https://kittycad.io/modeling-app/download"
href="https://github.com/KittyCAD/modeling-app/releases"
rel="noopener noreferrer"
target="_blank"
className="text-warn-80 dark:text-warn-80 dark:hover:text-warn-70 underline"
>
our website
our GitHub repository
</a>{' '}
to download the app for the best experience.
</p>

View File

@ -1,85 +0,0 @@
import { CommandLog, engineCommandManager } from 'lang/std/engineConnection'
import { useState, useEffect } from 'react'
function useEngineCommands(): [CommandLog[], () => void] {
const [engineCommands, setEngineCommands] = useState<CommandLog[]>([])
useEffect(() => {
engineCommandManager.registerCommandLogCallback((commands) =>
setEngineCommands(commands)
)
}, [])
return [engineCommands, () => engineCommandManager.clearCommandLogs()]
}
export const EngineCommands = () => {
const [engineCommands, clearEngineCommands] = useEngineCommands()
const [containsFilter, setContainsFilter] = useState('')
const [customCmd, setCustomCmd] = useState('')
return (
<div>
<input
className="text-gray-800 bg-slate-300 px-2"
data-testid="filter-input"
type="text"
value={containsFilter}
onChange={(e) => setContainsFilter(e.target.value)}
placeholder="Filter"
/>
<div className="max-w-xl max-h-36 overflow-auto">
{engineCommands.map((command, index) => {
const stringer = JSON.stringify(command)
if (containsFilter && !stringer.includes(containsFilter)) return null
return (
<pre className="text-xs" key={index}>
<code
key={index}
data-message-type={command.type}
data-command-type={
(command.type === 'send-modeling' ||
command.type === 'send-scene') &&
command.data.type === 'modeling_cmd_req'
? command?.data?.cmd?.type
: ''
}
data-command-id={
(command.type === 'send-modeling' ||
command.type === 'send-scene') &&
command.data.type === 'modeling_cmd_req'
? command.data.cmd_id
: ''
}
data-receive-command-type={
command.type === 'receive-reliable' ? command.cmd_type : ''
}
>
{JSON.stringify(command, null, 2)}
</code>
</pre>
)
})}
</div>
<button data-testid="clear-commands" onClick={clearEngineCommands}>
Clear
</button>
<br />
<input
className="text-gray-800 bg-slate-300 px-2"
type="text"
value={customCmd}
onChange={(e) => setCustomCmd(e.target.value)}
placeholder="JSON command"
data-testid="custom-cmd-input"
/>
<button
data-testid="custom-cmd-send-button"
onClick={() =>
engineCommandManager.sendSceneCommand(JSON.parse(customCmd))
}
>
Send custom command
</button>
</div>
)
}

View File

@ -1,60 +1,8 @@
import { isTauri } from 'lib/isTauri'
import { useRouteError, isRouteErrorResponse } from 'react-router-dom'
import { ActionButton } from './ActionButton'
import {
faBug,
faHome,
faRefresh,
faTrash,
} from '@fortawesome/free-solid-svg-icons'
export const ErrorPage = () => {
let error = useRouteError()
console.error('error', error)
return (
<div className="flex flex-col items-center justify-center h-screen">
<section className="max-w-full xl:max-w-4xl mx-auto">
<h1 className="text-4xl mb-8 font-bold">
An unexpected error occurred
</h1>
{isRouteErrorResponse(error) && (
<p className="mb-8">
{error.status}: {error.data}
</p>
)}
<div className="flex justify-between gap-2 mt-6">
{isTauri() && (
<ActionButton Element="link" to={'/'} icon={{ icon: faHome }}>
Go Home
</ActionButton>
)}
<ActionButton
Element="button"
icon={{ icon: faRefresh }}
onClick={() => window.location.reload()}
>
Reload
</ActionButton>
<ActionButton
Element="button"
icon={{ icon: faTrash }}
onClick={() => {
window.localStorage.clear()
}}
>
Clear storage
</ActionButton>
<ActionButton
Element="externalLink"
icon={{ icon: faBug }}
to="https://github.com/KittyCAD/modeling-app/issues/new"
>
Report Bug
</ActionButton>
</div>
</section>
<h1 className="text-4xl font-bold">404</h1>
<p className="text-2xl font-bold">Page not found</p>
</div>
)
}

View File

@ -1,40 +1,31 @@
import { v4 as uuidv4 } from 'uuid'
import { useStore } from '../useStore'
import { faFileExport, faXmark } from '@fortawesome/free-solid-svg-icons'
import { ActionButton } from './ActionButton'
import Modal from 'react-modal'
import React from 'react'
import { useFormik } from 'formik'
import { Models } from '@kittycad/lib'
import { engineCommandManager } from '../lang/std/engineConnection'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type']
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
type StorageUnion = ExtractStorageTypes<OutputFormat>
interface ExportButtonProps extends React.PropsWithChildren {
className?: {
button?: string
icon?: string
bg?: string
// If we wanted more classname configuration of sub-elements,
// put them here
}
}
export const ExportButton = ({ children, className }: ExportButtonProps) => {
const { engineCommandManager } = useStore((s) => ({
engineCommandManager: s.engineCommandManager,
}))
const [modalIsOpen, setIsOpen] = React.useState(false)
const {
settings: {
state: {
context: { baseUnit },
},
},
} = useGlobalStateContext()
const defaultType = 'gltf'
const [type, setType] = React.useState<OutputTypeKey>(defaultType)
const defaultStorage = 'embedded'
const [storage, setStorage] = React.useState<StorageUnion>(defaultStorage)
const [type, setType] = React.useState(defaultType)
function openModal() {
setIsOpen(true)
@ -47,8 +38,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
// Default to gltf and embedded.
const initialValues: OutputFormat = {
type: defaultType,
storage: defaultStorage,
presentation: 'pretty',
storage: 'embedded',
}
const formik = useFormik({
initialValues,
@ -75,25 +65,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
},
}
}
if (
values.type === 'obj' ||
values.type === 'stl' ||
values.type === 'ply'
) {
values.units = baseUnit
}
if (
values.type === 'ply' ||
values.type === 'stl' ||
values.type === 'gltf'
) {
// Set the storage type.
values.storage = storage
}
if (values.type === 'ply' || values.type === 'stl') {
values.selection = { type: 'default_scene' }
}
engineCommandManager.sendSceneCommand({
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'export',
@ -102,7 +74,6 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
// in the scene to export. In that case, you'd pass the IDs thru here.
entity_ids: [],
format: values,
source_unit: baseUnit,
},
cmd_id: uuidv4(),
})
@ -116,11 +87,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
<ActionButton
onClick={openModal}
Element="button"
icon={{
icon: faFileExport,
iconClassName: className?.icon,
bgClassName: className?.bg,
}}
icon={{ icon: faFileExport }}
className={className?.button}
>
{children || 'Export'}
@ -140,19 +107,8 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
<select
id="type"
name="type"
data-testid="export-type"
onChange={(e) => {
setType(e.target.value as OutputTypeKey)
if (e.target.value === 'gltf') {
// Set default to embedded.
setStorage('embedded')
} else if (e.target.value === 'ply') {
// Set default to ascii.
setStorage('ascii')
} else if (e.target.value === 'stl') {
// Set default to ascii.
setStorage('ascii')
}
setType(e.target.value)
formik.handleChange(e)
}}
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
@ -170,11 +126,8 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
<select
id="storage"
name="storage"
data-testid="export-storage"
onChange={(e) => {
setStorage(e.target.value as StorageUnion)
formik.handleChange(e)
}}
onChange={formik.handleChange}
value={formik.values.storage}
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
>
{type === 'gltf' && (
@ -184,13 +137,13 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
<option value="standard">standard</option>
</>
)}
{type === 'stl' && (
{type === 'ply' && (
<>
<option value="ascii">ascii</option>
<option value="binary">binary</option>
</>
)}
{type === 'ply' && (
{type === 'stl' && (
<>
<option value="ascii">ascii</option>
<option value="binary_little_endian">

View File

@ -1,158 +0,0 @@
import { useMachine } from '@xstate/react'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { IndexLoaderData, paths } from '../Router'
import React, { createContext } from 'react'
import { toast } from 'react-hot-toast'
import {
AnyStateMachine,
ContextFrom,
EventFrom,
InterpreterFrom,
Prop,
StateFrom,
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { DEFAULT_FILE_NAME, fileMachine } from 'machines/fileMachine'
import {
createDir,
removeDir,
removeFile,
renameFile,
writeFile,
} from '@tauri-apps/api/fs'
import { FILE_EXT, readProject } from 'lib/tauriFS'
import { isTauri } from 'lib/isTauri'
import { sep } from '@tauri-apps/api/path'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
context: ContextFrom<T>
send: Prop<InterpreterFrom<T>, 'send'>
}
export const FileContext = createContext(
{} as MachineContext<typeof fileMachine>
)
export const FileMachineProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const navigate = useNavigate()
const { setCommandBarOpen } = useCommandsContext()
const { project } = useRouteLoaderData(paths.FILE) as IndexLoaderData
const [state, send] = useMachine(fileMachine, {
context: {
project,
selectedDirectory: project,
},
actions: {
navigateToFile: (
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine>
) => {
if (event.data && 'name' in event.data) {
setCommandBarOpen(false)
navigate(
`${paths.FILE}/${encodeURIComponent(
context.selectedDirectory + sep + event.data.name
)}`
)
}
},
toastSuccess: (_, event) =>
event.data && toast.success((event.data || '') + ''),
toastError: (_, event) => toast.error((event.data || '') + ''),
},
services: {
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
const newFiles = isTauri()
? await readProject(context.project.path)
: []
return {
...context.project,
children: newFiles,
}
},
createFile: async (
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine, 'Create file'>
) => {
let name = event.data.name.trim() || DEFAULT_FILE_NAME
if (event.data.makeDir) {
await createDir(context.selectedDirectory.path + sep + name)
} else {
await writeFile(
context.selectedDirectory.path +
sep +
name +
(name.endsWith(FILE_EXT) ? '' : FILE_EXT),
''
)
}
return `Successfully created "${name}"`
},
renameFile: async (
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine, 'Rename file'>
) => {
const { oldName, newName, isDir } = event.data
let name = newName ? newName : DEFAULT_FILE_NAME
await renameFile(
context.selectedDirectory.path + sep + oldName,
context.selectedDirectory.path +
sep +
name +
(name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
)
return (
oldName !== name && `Successfully renamed "${oldName}" to "${name}"`
)
},
deleteFile: async (
context: ContextFrom<typeof fileMachine>,
event: EventFrom<typeof fileMachine, 'Delete file'>
) => {
const isDir = !!event.data.children
if (isDir) {
await removeDir(event.data.path, {
recursive: true,
}).catch((e) => console.error('Error deleting directory', e))
} else {
await removeFile(event.data.path).catch((e) =>
console.error('Error deleting file', e)
)
}
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
event.data.name
}"`
},
},
guards: {
'Has at least 1 file': (_, event: EventFrom<typeof fileMachine>) => {
if (event.type !== 'done.invoke.read-files') return false
return !!event?.data?.children && event.data.children.length > 0
},
},
})
return (
<FileContext.Provider
value={{
send,
state,
context: state.context, // just a convenience, can remove if we need to save on memory
}}
>
{children}
</FileContext.Provider>
)
}
export default FileMachineProvider

View File

@ -1,16 +0,0 @@
.folder {
position: relative;
}
.folder::after {
content: '';
width: 1px;
z-index: -1;
@apply absolute top-0 bottom-0;
left: calc(var(--indent-line-left, 1rem) + 0.25rem);
@apply bg-chalkboard-30;
}
:global(.dark) .folder::after {
@apply bg-chalkboard-80;
}

View File

@ -1,398 +0,0 @@
import { IndexLoaderData, paths } from 'Router'
import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip'
import { FileEntry } from '@tauri-apps/api/fs'
import { Dispatch, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Dialog, Disclosure } from '@headlessui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { useFileContext } from 'hooks/useFileContext'
import { useHotkeys } from 'react-hotkeys-hook'
import styles from './FileTree.module.css'
import { sortProject } from 'lib/tauriFS'
function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})`
}
function RenameForm({
fileOrDir,
setIsRenaming,
level = 0,
}: {
fileOrDir: FileEntry
setIsRenaming: Dispatch<React.SetStateAction<boolean>>
level?: number
}) {
const { send } = useFileContext()
const inputRef = useRef<HTMLInputElement>(null)
function handleRenameSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setIsRenaming(false)
send({
type: 'Rename file',
data: {
oldName: fileOrDir.name || '',
newName: inputRef.current?.value || fileOrDir.name || '',
isDir: fileOrDir.children !== undefined,
},
})
}
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Escape') {
e.stopPropagation()
setIsRenaming(false)
}
}
return (
<form onSubmit={handleRenameSubmit}>
<label>
<span className="sr-only">Rename file</span>
<input
ref={inputRef}
type="text"
autoFocus
placeholder={fileOrDir.name}
className="w-full py-1 bg-transparent text-chalkboard-100 placeholder:text-chalkboard-70 dark:text-chalkboard-10 dark:placeholder:text-chalkboard-50 focus:outline-none focus:ring-0"
onKeyDown={handleKeyDown}
onBlur={() => setIsRenaming(false)}
style={{ paddingInlineStart: getIndentationCSS(level) }}
/>
</label>
<button className="sr-only" type="submit">
Submit
</button>
</form>
)
}
function DeleteConfirmationDialog({
fileOrDir,
setIsOpen,
}: {
fileOrDir: FileEntry
setIsOpen: Dispatch<React.SetStateAction<boolean>>
}) {
const { send } = useFileContext()
return (
<Dialog
open={true}
onClose={() => setIsOpen(false)}
className="relative z-50"
>
<div className="fixed inset-0 bg-chalkboard-110/80 grid place-content-center">
<Dialog.Panel className="rounded p-4 bg-chalkboard-10 dark:bg-chalkboard-100 border border-destroy-80 max-w-2xl">
<Dialog.Title as="h2" className="text-2xl font-bold mb-4">
Delete {fileOrDir.children !== undefined ? 'Folder' : 'File'}
</Dialog.Title>
<Dialog.Description className="my-6">
This will permanently delete "{fileOrDir.name || 'this file'}"
{fileOrDir.children !== undefined
? ' and all of its contents. '
: '. '}
This action cannot be undone.
</Dialog.Description>
<div className="flex justify-between">
<ActionButton
Element="button"
onClick={async () => {
send({ type: 'Delete file', data: fileOrDir })
setIsOpen(false)
}}
icon={{
icon: faTrashAlt,
bgClassName: 'bg-destroy-80',
iconClassName:
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10 dark:text-destroy-20 dark:group-hover:text-destroy-10 dark:hover:text-destroy-10',
}}
className="hover:border-destroy-40 dark:hover:border-destroy-40"
>
Delete
</ActionButton>
<ActionButton Element="button" onClick={() => setIsOpen(false)}>
Cancel
</ActionButton>
</div>
</Dialog.Panel>
</div>
</Dialog>
)
}
const FileTreeItem = ({
project,
currentFile,
fileOrDir,
closePanel,
level = 0,
}: {
project?: IndexLoaderData['project']
currentFile?: IndexLoaderData['file']
fileOrDir: FileEntry
closePanel: (
focusableElement?:
| HTMLElement
| React.MutableRefObject<HTMLElement | null>
| undefined
) => void
level?: number
}) => {
const { send, context } = useFileContext()
const navigate = useNavigate()
const [isRenaming, setIsRenaming] = useState(false)
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const isCurrentFile = fileOrDir.path === currentFile?.path
function handleKeyUp(e: React.KeyboardEvent<HTMLButtonElement>) {
if (e.metaKey && e.key === 'Backspace') {
// Open confirmation dialog
setIsConfirmingDelete(true)
} else if (e.key === 'Enter') {
// Show the renaming form
setIsRenaming(true)
} else if (e.code === 'Space') {
openFile()
}
}
function openFile() {
if (fileOrDir.children !== undefined) return // Don't open directories
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
closePanel()
}
return (
<>
{fileOrDir.children === undefined ? (
<li
className={
'group m-0 p-0 border-solid border-0 text-energy-100 hover:text-energy-70 hover:bg-energy-10/50 dark:text-energy-30 dark:hover:!text-energy-20 dark:hover:bg-energy-90/50 focus-within:bg-energy-10/80 dark:focus-within:bg-energy-80/50 hover:focus-within:bg-energy-10/80 dark:hover:focus-within:bg-energy-80/50 ' +
(isCurrentFile ? 'bg-energy-10/50 dark:bg-energy-90/50' : '')
}
>
{!isRenaming ? (
<button
className="flex gap-1 items-center py-0.5 rounded-none border-none p-0 m-0 text-sm w-full hover:!bg-transparent text-left !text-inherit"
style={{ paddingInlineStart: getIndentationCSS(level) }}
onDoubleClick={openFile}
onClick={(e) => e.currentTarget.focus()}
onKeyUp={handleKeyUp}
>
<KclIcon
className={
'inline-block w-3 ' +
(isCurrentFile
? 'text-energy-90 dark:text-energy-10'
: 'text-energy-50 dark:text-energy-50')
}
/>
{fileOrDir.name}
</button>
) : (
<RenameForm
fileOrDir={fileOrDir}
setIsRenaming={setIsRenaming}
level={level}
/>
)}
</li>
) : (
<Disclosure defaultOpen={currentFile?.path.includes(fileOrDir.path)}>
{({ open }) => (
<div className="group">
{!isRenaming ? (
<Disclosure.Button
className={
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 text-chalkboard-70 dark:text-chalkboard-30 hover:bg-energy-10/50 dark:hover:bg-energy-90/50' +
(context.selectedDirectory.path.includes(fileOrDir.path)
? ' group-focus-within:bg-chalkboard-20/50 dark:group-focus-within:bg-chalkboard-80/20 hover:group-focus-within:bg-chalkboard-20 dark:hover:group-focus-within:bg-chalkboard-80/20 group-active:bg-chalkboard-20/50 dark:group-active:bg-chalkboard-80/20 hover:group-active:bg-chalkboard-20/50 dark:hover:group-active:bg-chalkboard-80/20'
: '')
}
style={{ paddingInlineStart: getIndentationCSS(level) }}
onClick={(e) => e.currentTarget.focus()}
onClickCapture={(e) =>
send({ type: 'Set selected directory', data: fileOrDir })
}
onFocusCapture={(e) =>
send({ type: 'Set selected directory', data: fileOrDir })
}
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
onKeyUp={handleKeyUp}
>
<FontAwesomeIcon
icon={faChevronRight}
className={
'inline-block mr-2 m-0 p-0 w-2 h-2 ' +
(open ? 'transform rotate-90' : '')
}
/>
{fileOrDir.name}
</Disclosure.Button>
) : (
<div
className="flex items-center"
style={{ paddingInlineStart: getIndentationCSS(level) }}
>
<FontAwesomeIcon
icon={faChevronRight}
className={
'inline-block mr-2 m-0 p-0 w-2 h-2 ' +
(open ? 'transform rotate-90' : '')
}
/>
<RenameForm
fileOrDir={fileOrDir}
setIsRenaming={setIsRenaming}
level={-1}
/>
</div>
)}
<Disclosure.Panel
className={styles.folder}
style={
{
'--indent-line-left': getIndentationCSS(level),
} as React.CSSProperties
}
>
<ul
className="m-0 p-0"
onClickCapture={(e) => {
send({ type: 'Set selected directory', data: fileOrDir })
}}
onFocusCapture={(e) =>
send({ type: 'Set selected directory', data: fileOrDir })
}
>
{fileOrDir.children?.map((child) => (
<FileTreeItem
fileOrDir={child}
project={project}
currentFile={currentFile}
closePanel={closePanel}
level={level + 1}
key={level + '-' + child.path}
/>
))}
</ul>
</Disclosure.Panel>
</div>
)}
</Disclosure>
)}
{isConfirmingDelete && (
<DeleteConfirmationDialog
fileOrDir={fileOrDir}
setIsOpen={setIsConfirmingDelete}
/>
)}
</>
)
}
interface FileTreeProps {
className?: string
file?: IndexLoaderData['file']
closePanel: (
focusableElement?:
| HTMLElement
| React.MutableRefObject<HTMLElement | null>
| undefined
) => void
}
export const FileTree = ({
className = '',
file,
closePanel,
}: FileTreeProps) => {
const { send, context } = useFileContext()
useHotkeys('meta + n', createFile)
useHotkeys('meta + shift + n', createFolder)
async function createFile() {
send({ type: 'Create file', data: { name: '', makeDir: false } })
}
async function createFolder() {
send({ type: 'Create file', data: { name: '', makeDir: true } })
}
return (
<div className={className}>
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-30/50 dark:bg-chalkboard-70/50">
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
<ActionButton
Element="button"
icon={{
icon: 'createFile',
iconClassName: '!text-energy-80 dark:!text-energy-20',
bgClassName: 'hover:bg-energy-10/50 dark:hover:bg-transparent',
}}
className="!p-0 border-none bg-transparent !outline-none"
onClick={createFile}
>
<Tooltip position="inlineStart" delay={750}>
Create File
</Tooltip>
</ActionButton>
<ActionButton
Element="button"
icon={{
icon: 'createFolder',
iconClassName: '!text-energy-80 dark:!text-energy-20',
bgClassName: 'hover:bg-energy-10/50 dark:hover:bg-transparent',
}}
className="!p-0 border-none bg-transparent !outline-none"
onClick={createFolder}
>
<Tooltip position="inlineStart" delay={750}>
Create Folder
</Tooltip>
</ActionButton>
</div>
<div className="overflow-auto max-h-full pb-12">
<ul
className="m-0 p-0 text-sm"
onClickCapture={(e) => {
send({ type: 'Set selected directory', data: context.project })
}}
>
{sortProject(context.project.children || []).map((fileOrDir) => (
<FileTreeItem
project={context.project}
currentFile={file}
fileOrDir={fileOrDir}
closePanel={closePanel}
key={fileOrDir.path}
/>
))}
</ul>
</div>
</div>
)
}
function KclIcon({ className = '' }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M40 0H0V40H40V0ZM7.34715 27.2143V15.6577L2.976 15.987V36.7949H7.34715V32.0645L8.00582 31.5256C8.24533 31.326 8.47487 31.1264 8.69442 30.9268L12.1075 36.7949H17.0475C16.1893 35.3978 15.311 33.9906 14.4128 32.5735C13.5346 31.1563 12.6664 29.7392 11.8081 28.3221L15.8499 24.9389C15.4308 24.4399 15.0017 23.931 14.5625 23.412L13.3051 21.8552L7.34715 27.2143ZM22.2581 26.6754C22.8769 25.9169 23.6753 25.5377 24.6533 25.5377C25.272 25.5377 25.8309 25.6175 26.3299 25.7772C26.8289 25.9169 27.4177 26.1465 28.0963 26.4658L29.3238 23.3521C28.5853 22.7933 27.7371 22.4041 26.779 22.1845C25.8409 21.9649 25.0625 21.8552 24.4437 21.8552C22.0885 21.8552 20.2223 22.5537 18.845 23.9509C17.4878 25.3281 16.8092 27.1944 16.8092 29.5496C16.8092 31.9048 17.4878 33.7611 18.845 35.1183C20.2223 36.4756 22.0885 37.1542 24.4437 37.1542C25.0625 37.1542 25.8509 37.0444 26.8089 36.8249C27.767 36.6053 28.6053 36.2161 29.3238 35.6572L28.0963 32.5435C27.4177 32.8629 26.8289 33.0924 26.3299 33.2321C25.8309 33.3718 25.272 33.4417 24.6533 33.4417C23.6753 33.4417 22.8769 33.0924 22.2581 32.3938C21.6594 31.6753 21.36 30.7272 21.36 29.5496C21.36 28.372 21.6594 27.4139 22.2581 26.6754ZM36.2796 36.7949V15.6577L31.9085 15.987V36.7949H36.2796Z"
fill="currentColor"
/>
</svg>
)
}

View File

@ -1,162 +0,0 @@
import { useMachine } from '@xstate/react'
import { useNavigate } from 'react-router-dom'
import { paths } from '../Router'
import {
authCommandBarMeta,
authMachine,
TOKEN_PERSIST_KEY,
} from '../machines/authMachine'
import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect, useRef } from 'react'
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import {
SETTINGS_PERSIST_KEY,
settingsCommandBarMeta,
settingsMachine,
} from 'machines/settingsMachine'
import { toast } from 'react-hot-toast'
import { setThemeClass, Themes } from 'lib/theme'
import {
AnyStateMachine,
ContextFrom,
InterpreterFrom,
Prop,
StateFrom,
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { isTauri } from 'lib/isTauri'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
context: ContextFrom<T>
send: Prop<InterpreterFrom<T>, 'send'>
}
type GlobalContext = {
auth: MachineContext<typeof authMachine>
settings: MachineContext<typeof settingsMachine>
}
export const GlobalStateContext = createContext({} as GlobalContext)
export const GlobalStateProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const navigate = useNavigate()
const { commands } = useCommandsContext()
// Settings machine setup
const retrievedSettings = useRef(
localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}'
)
const persistedSettings = Object.assign(
settingsMachine.initialState.context,
JSON.parse(retrievedSettings.current) as Partial<
(typeof settingsMachine)['context']
>
)
const [settingsState, settingsSend] = useMachine(settingsMachine, {
context: persistedSettings,
actions: {
toastSuccess: (context, event) => {
const truncatedNewValue =
'data' in event && event.data instanceof Object
? (context[Object.keys(event.data)[0] as keyof typeof context]
.toString()
.substring(0, 28) as any)
: undefined
toast.success(
event.type +
(truncatedNewValue
? ` to "${truncatedNewValue}${
truncatedNewValue.length === 28 ? '...' : ''
}"`
: '')
)
},
},
})
useStateMachineCommands({
state: settingsState,
send: settingsSend,
commands,
owner: 'settings',
commandBarMeta: settingsCommandBarMeta,
})
// Listen for changes to the system theme and update the app theme accordingly
// This is only done if the theme setting is set to 'system'.
// It can't be done in XState (in an invoked callback, for example)
// because there doesn't seem to be a good way to listen to
// events outside of the machine that also depend on the machine's context
useEffect(() => {
const matcher = window.matchMedia('(prefers-color-scheme: dark)')
const listener = (e: MediaQueryListEvent) => {
if (settingsState.context.theme !== 'system') return
setThemeClass(e.matches ? Themes.Dark : Themes.Light)
}
matcher.addEventListener('change', listener)
return () => matcher.removeEventListener('change', listener)
}, [settingsState.context])
// Auth machine setup
const [authState, authSend] = useMachine(authMachine, {
actions: {
goToSignInPage: () => {
navigate(paths.SIGN_IN)
logout()
},
goToIndexPage: () => {
if (window.location.pathname.includes(paths.SIGN_IN)) {
navigate(paths.INDEX)
}
},
},
})
useStateMachineCommands({
state: authState,
send: authSend,
commands,
commandBarMeta: authCommandBarMeta,
owner: 'auth',
})
return (
<GlobalStateContext.Provider
value={{
auth: {
state: authState,
context: authState.context,
send: authSend,
},
settings: {
state: settingsState,
context: settingsState.context,
send: settingsSend,
},
}}
>
{children}
</GlobalStateContext.Provider>
)
}
export default GlobalStateProvider
export function logout() {
localStorage.removeItem(TOKEN_PERSIST_KEY)
return (
!isTauri() &&
fetch(withBaseUrl('/logout'), {
method: 'POST',
credentials: 'include',
})
)
}

View File

@ -10,10 +10,7 @@ const Loading = ({ children }: React.PropsWithChildren) => {
return () => clearTimeout(timer)
}, [setHasLongLoadTime])
return (
<div
className="body-bg flex flex-col items-center justify-center h-screen"
data-testid="loading"
>
<div className="body-bg flex flex-col items-center justify-center h-screen">
<svg viewBox="0 0 10 10" className="w-8 h-8">
<circle cx="5" cy="5" r="4" stroke="var(--liquid-20)" fill="none" />
<circle

View File

@ -1,8 +1,7 @@
import ReactJson from 'react-json-view'
import { useEffect } from 'react'
import { Themes, useStore } from '../useStore'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { Themes } from '../lib/theme'
import { useKclContext } from 'lang/KclSinglton'
const ReactJsonTypeHack = ReactJson as any
@ -11,7 +10,9 @@ interface LogPanelProps extends CollapsiblePanelProps {
}
export const Logs = ({ theme = Themes.Light, ...props }: LogPanelProps) => {
const { logs } = useKclContext()
const { logs } = useStore(({ logs }) => ({
logs,
}))
useEffect(() => {
const element = document.querySelector('.console-tile')
if (element) {
@ -45,19 +46,21 @@ export const KCLErrors = ({
theme = Themes.Light,
...props
}: LogPanelProps) => {
const { errors } = useKclContext()
const { kclErrors } = useStore(({ kclErrors }) => ({
kclErrors,
}))
useEffect(() => {
const element = document.querySelector('.console-tile')
if (element) {
element.scrollTop = element.scrollHeight - element.clientHeight
}
}, [errors])
}, [kclErrors])
return (
<CollapsiblePanel {...props}>
<div className="h-full relative">
<div className="absolute inset-0 flex flex-col">
<ReactJsonTypeHack
src={errors}
src={kclErrors}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}

View File

@ -1,6 +1,7 @@
import { processMemory } from './MemoryPanel'
import { parser_wasm } from '../lang/abstractSyntaxTree'
import { enginelessExecutor } from '../lib/testHelpers'
import { initPromise, parse } from '../lang/wasm'
import { initPromise } from '../lang/rust'
beforeAll(() => initPromise)
@ -9,41 +10,48 @@ describe('processMemory', () => {
// Enable rotations #152
const code = `
const myVar = 5
fn myFn = (a) => {
const myFn = (a) => {
return a - 2
}
const otherVar = myFn(5)
const theExtrude = startSketchOn('XY')
|> startProfileAt([0, 0], %)
const theExtrude = startSketchAt([0, 0])
|> lineTo([-2.4, myVar], %)
|> lineTo([-0.76, otherVar], %)
|> extrude(4, %)
const theSketch = startSketchOn('XY')
|> startProfileAt([0, 0], %)
const theSketch = startSketchAt([0, 0])
|> lineTo([-3.35, 0.17], %)
|> lineTo([0.98, 5.16], %)
|> lineTo([2.15, 4.32], %)
// |> rx(90, %)
show(theExtrude, theSketch)`
const ast = parse(code)
const ast = parser_wasm(code)
const programMemory = await enginelessExecutor(ast, {
root: {},
return: null,
root: {
log: {
type: 'userVal',
value: (a: any) => {
console.log('raw log', a)
},
__meta: [],
},
},
pendingMemory: {},
})
const output = processMemory(programMemory)
expect(output.myVar).toEqual(5)
expect(output.myFn).toEqual('__function__')
expect(output.otherVar).toEqual(3)
expect(output).toEqual({
myVar: 5,
myFn: undefined,
myFn: '__function__',
otherVar: 3,
theExtrude: [],
theSketch: [
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0], name: '' },
{ type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17], name: '' },
{ type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16], name: '' },
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0] },
{ type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17] },
{ type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16] },
],
})
})

View File

@ -1,9 +1,8 @@
import ReactJson from 'react-json-view'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { Themes, useStore } from '../useStore'
import { useMemo } from 'react'
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/wasm'
import { Themes } from '../lib/theme'
import { useKclContext } from 'lang/KclSinglton'
import { ProgramMemory } from '../lang/executor'
interface MemoryPanelProps extends CollapsiblePanelProps {
theme?: Exclude<Themes, Themes.System>
@ -13,7 +12,9 @@ export const MemoryPanel = ({
theme = Themes.Light,
...props
}: MemoryPanelProps) => {
const { programMemory } = useKclContext()
const { programMemory } = useStore((s) => ({
programMemory: s.programMemory,
}))
const ProcessedMemory = useMemo(
() => processMemory(programMemory),
[programMemory]
@ -22,11 +23,7 @@ export const MemoryPanel = ({
<CollapsiblePanel {...props}>
<div className="h-full relative">
<div className="absolute inset-0 flex flex-col items-start">
<div
className="overflow-y-auto h-full console-tile w-full"
style={{ marginBottom: 36 }}
>
{/* 36px is the height of PanelHeader */}
<div className=" h-full console-tile w-full">
<ReactJson
src={ProcessedMemory}
collapsed={1}
@ -48,15 +45,11 @@ export const MemoryPanel = ({
export const processMemory = (programMemory: ProgramMemory) => {
const processedMemory: any = {}
Object.keys(programMemory?.root || {}).forEach((key) => {
Object.keys(programMemory.root).forEach((key) => {
const val = programMemory.root[key]
if (typeof val.value !== 'function') {
if (val.type === 'SketchGroup') {
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
return rest
})
} else if (val.type === 'ExtrudeGroup') {
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
if (val.type === 'sketchGroup' || val.type === 'extrudeGroup') {
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }) => {
return rest
})
} else {

View File

@ -1,566 +0,0 @@
import { useMachine } from '@xstate/react'
import React, { createContext, useEffect, useRef } from 'react'
import {
AnyStateMachine,
ContextFrom,
InterpreterFrom,
Prop,
StateFrom,
assign,
} from 'xstate'
import { SetSelections, modelingMachine } from 'machines/modelingMachine'
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { isCursorInSketchCommandRange } from 'lang/util'
import { engineCommandManager } from 'lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid'
import { addStartSketch } from 'lang/modifyAst'
import { roundOff } from 'lib/utils'
import {
recast,
parse,
Program,
PipeExpression,
CallExpression,
} from 'lang/wasm'
import { getNodeFromPath } from 'lang/queryAst'
import {
addCloseToPipe,
addNewSketchLn,
compareVec2Epsilon,
} from 'lang/std/sketch'
import { kclManager } from 'lang/KclSinglton'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import {
angleBetweenInfo,
applyConstraintAngleBetween,
} from './Toolbar/SetAngleBetween'
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
import { toast } from 'react-hot-toast'
import { pathMapToSelections } from 'lang/util'
import { useStore } from 'useStore'
import { handleSelectionBatch, handleSelectionWithShift } from 'lib/selections'
import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
context: ContextFrom<T>
send: Prop<InterpreterFrom<T>, 'send'>
}
export const ModelingMachineContext = createContext(
{} as MachineContext<typeof modelingMachine>
)
export const ModelingMachineProvider = ({
children,
}: {
children: React.ReactNode
}) => {
const { auth } = useGlobalStateContext()
const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null)
useSetupEngineManager(streamRef, token)
const { isShiftDown, editorView } = useStore((s) => ({
isShiftDown: s.isShiftDown,
editorView: s.editorView,
}))
// const { commands } = useCommandsContext()
// Settings machine setup
// const retrievedSettings = useRef(
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
// )
// What should we persist from modeling state? Nothing?
// const persistedSettings = Object.assign(
// settingsMachine.initialState.context,
// JSON.parse(retrievedSettings.current) as Partial<
// (typeof settingsMachine)['context']
// >
// )
const [modelingState, modelingSend] = useMachine(modelingMachine, {
// context: persistedSettings,
actions: {
'Modify AST': () => {},
'Update code selection cursors': () => {},
'show default planes': () => {
kclManager.showPlanes()
},
'create path': assign({
sketchEnginePathId: () => {
const sketchUuid = uuidv4()
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: sketchUuid,
cmd: {
type: 'start_path',
},
})
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'edit_mode_enter',
target: sketchUuid,
},
})
return sketchUuid
},
}),
'AST start new sketch': assign(
({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
if (!axis) {
// Something really weird must have happened for this to happen.
console.error('axis is undefined for starting a new sketch')
return {}
}
if (!segmentId) {
// Something really weird must have happened for this to happen.
console.error('segmentId is undefined for starting a new sketch')
return {}
}
const _addStartSketch = addStartSketch(
kclManager.ast,
axis,
[roundOff(coords[0].x), roundOff(coords[0].y)],
[
roundOff(coords[1].x - coords[0].x),
roundOff(coords[1].y - coords[0].y),
]
)
const _modifiedAst = _addStartSketch.modifiedAst
const _pathToNode = _addStartSketch.pathToNode
const newCode = recast(_modifiedAst)
const astWithUpdatedSource = parse(newCode)
const updatedPipeNode = getNodeFromPath<PipeExpression>(
astWithUpdatedSource,
_pathToNode
).node
const startProfileAtCallExp = updatedPipeNode.body.find(
(exp) =>
exp.type === 'CallExpression' &&
exp.callee.name === 'startProfileAt'
)
if (startProfileAtCallExp)
engineCommandManager.artifactMap[sketchEnginePathId] = {
type: 'result',
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
commandType: 'start_path',
data: null,
raw: {} as any,
}
const lineCallExp = updatedPipeNode.body.find(
(exp) => exp.type === 'CallExpression' && exp.callee.name === 'line'
)
if (lineCallExp)
engineCommandManager.artifactMap[segmentId] = {
type: 'result',
range: [lineCallExp.start, lineCallExp.end],
commandType: 'extend_path',
parentId: sketchEnginePathId,
data: null,
raw: {} as any,
}
kclManager.executeAstMock(astWithUpdatedSource, true)
return {
sketchPathToNode: _pathToNode,
}
}
),
'AST add line segment': async (
{ sketchPathToNode, sketchEnginePathId },
{ data: { coords, segmentId } }
) => {
if (!sketchPathToNode) return
const lastCoord = coords[coords.length - 1]
const pathInfo = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'path_get_info',
path_id: sketchEnginePathId,
},
})
const firstSegment = pathInfo?.data?.data?.segments.find(
(seg: any) => seg.command === 'line_to'
)
const firstSegCoords = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'curve_get_control_points',
curve_id: firstSegment.command_id,
},
})
const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
const isClose = compareVec2Epsilon(
[startPathCoord.x, startPathCoord.y],
[lastCoord.x, lastCoord.y]
)
let _modifiedAst: Program
if (!isClose) {
const newSketchLn = addNewSketchLn({
node: kclManager.ast,
programMemory: kclManager.programMemory,
to: [lastCoord.x, lastCoord.y],
from: [coords[0].x, coords[0].y],
fnName: 'line',
pathToNode: sketchPathToNode,
})
const _modifiedAst = newSketchLn.modifiedAst
kclManager.executeAstMock(_modifiedAst, true).then(() => {
const lineCallExp = getNodeFromPath<CallExpression>(
kclManager.ast,
newSketchLn.pathToNode
).node
if (segmentId)
engineCommandManager.artifactMap[segmentId] = {
type: 'result',
range: [lineCallExp.start, lineCallExp.end],
commandType: 'extend_path',
parentId: sketchEnginePathId,
data: null,
raw: {} as any,
}
})
} else {
_modifiedAst = addCloseToPipe({
node: kclManager.ast,
programMemory: kclManager.programMemory,
pathToNode: sketchPathToNode,
})
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' },
})
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'default_camera_disable_sketch_mode' },
})
kclManager.executeAstMock(_modifiedAst, true)
// updateAst(_modifiedAst, true)
}
},
'sketch exit execute': () => {
kclManager.executeAst()
},
'set tool': () => {}, // TODO
'toast extrude failed': () => {
toast.error(
'Extrude failed, sketches need to be closed, or not already extruded'
)
},
'Set selection': assign(({ selectionRanges }, event) => {
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
const setSelections = event.data
if (!editorView) return {}
if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
return { selectionRanges: setSelections.selection }
else if (setSelections.selectionType === 'otherSelection') {
// TODO KittyCAD/engine/issues/1620: send axis highlight when it's working (if that's what we settle on)
// const axisAddCmd: EngineCommand = {
// type: 'modeling_cmd_req',
// cmd: {
// type: 'highlight_set_entities',
// entities: [
// setSelections.selection === 'x-axis'
// ? X_AXIS_UUID
// : Y_AXIS_UUID,
// ],
// },
// cmd_id: uuidv4(),
// }
// if (!isShiftDown) {
// engineCommandManager
// .sendSceneCommand({
// type: 'modeling_cmd_req',
// cmd: {
// type: 'select_clear',
// },
// cmd_id: uuidv4(),
// })
// .then(() => {
// engineCommandManager.sendSceneCommand(axisAddCmd)
// })
// } else {
// engineCommandManager.sendSceneCommand(axisAddCmd)
// }
const {
codeMirrorSelection,
selectionRangeTypeMap,
otherSelections,
} = handleSelectionWithShift({
otherSelection: setSelections.selection,
currentSelections: selectionRanges,
isShiftDown,
})
setTimeout(() => {
editorView.dispatch({
selection: codeMirrorSelection,
})
})
return {
selectionRangeTypeMap,
selectionRanges: {
codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections,
},
}
} else if (setSelections.selectionType === 'singleCodeCursor') {
// This DOES NOT set the `selectionRanges` in xstate context
// instead it updates/dispatches to the editor, which in turn updates the xstate context
// I've found this the best way to deal with the editor without causing an infinite loop
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
// because we want to respect the user manually placing the cursor too.
// for more details on how selections see `src/lib/selections.ts`.
const {
codeMirrorSelection,
selectionRangeTypeMap,
otherSelections,
} = handleSelectionWithShift({
codeSelection: setSelections.selection,
currentSelections: selectionRanges,
isShiftDown,
})
if (codeMirrorSelection) {
setTimeout(() => {
editorView.dispatch({
selection: codeMirrorSelection,
})
})
}
if (!setSelections.selection) {
return {
selectionRangeTypeMap,
selectionRanges: {
codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections,
},
}
}
return {
selectionRangeTypeMap,
selectionRanges: {
codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections,
},
}
}
// This DOES NOT set the `selectionRanges` in xstate context
// same as comment above
const { codeMirrorSelection, selectionRangeTypeMap } =
handleSelectionBatch({
selections: setSelections.selection,
})
if (codeMirrorSelection) {
setTimeout(() => {
editorView.dispatch({
selection: codeMirrorSelection,
})
})
}
return { selectionRangeTypeMap }
}),
},
guards: {
'Selection contains axis': () => true,
'Selection contains edge': () => true,
'Selection contains face': () => true,
'Selection contains line': () => true,
'Selection contains point': () => true,
'Selection is not empty': () => true,
'Selection is one face': ({ selectionRanges }) => {
return !!isCursorInSketchCommandRange(
engineCommandManager.artifactMap,
selectionRanges
)
},
},
services: {
'Get horizontal info': async ({
selectionRanges,
}): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } =
await applyConstraintHorzVertDistance({
constraint: 'setHorzDistance',
selectionRanges,
})
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get vertical info': async ({
selectionRanges,
}): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } =
await applyConstraintHorzVertDistance({
constraint: 'setVertDistance',
selectionRanges,
})
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get angle info': async ({ selectionRanges }): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({
selectionRanges,
}).enabled
? applyConstraintAngleBetween({
selectionRanges,
})
: applyConstraintAngleLength({
selectionRanges,
angleOrLength: 'setAngle',
}))
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get length info': async ({
selectionRanges,
}): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = await applyConstraintAngleLength(
{ selectionRanges }
)
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get perpendicular distance info': async ({
selectionRanges,
}): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect({
selectionRanges,
})
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get ABS X info': async ({ selectionRanges }): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
{
constraint: 'xAbs',
selectionRanges,
}
)
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
'Get ABS Y info': async ({ selectionRanges }): Promise<SetSelections> => {
const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
{
constraint: 'yAbs',
selectionRanges,
}
)
await kclManager.updateAst(modifiedAst, true)
return {
selectionType: 'completeSelection',
selection: pathMapToSelections(
kclManager.ast,
selectionRanges,
pathToNodeMap
),
}
},
},
devTools: true,
})
useEffect(() => {
engineCommandManager.onPlaneSelected((plane_id: string) => {
if (modelingState.nextEvents.includes('Select default plane')) {
modelingSend({
type: 'Select default plane',
data: { planeId: plane_id },
})
}
})
}, [modelingSend, modelingState.nextEvents])
useEffect(() => {
kclManager.registerExecuteCallback(() => {
modelingSend({ type: 'Re-execute' })
})
}, [modelingSend])
// useStateMachineCommands({
// state: settingsState,
// send: settingsSend,
// commands,
// owner: 'settings',
// commandBarMeta: settingsCommandBarMeta,
// })
return (
<ModelingMachineContext.Provider
value={{
state: modelingState,
context: modelingState.context,
send: modelingSend,
}}
>
{/* TODO #818: maybe pass reff down to children/app.ts or render app.tsx directly?
since realistically it won't ever have generic children that isn't app.tsx */}
<div className="h-screen overflow-hidden select-none" ref={streamRef}>
{children}
</div>
</ModelingMachineContext.Provider>
)
}
export default ModelingMachineProvider

View File

@ -1,51 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react'
import UserSidebarMenu from './UserSidebarMenu'
import { BrowserRouter } from 'react-router-dom'
import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar'
import {
NETWORK_CONTENT,
NetworkHealthIndicator,
} from './NetworkHealthIndicator'
function TestWrap({ children }: { children: React.ReactNode }) {
// wrap in router and xState context
return (
<BrowserRouter>
<CommandBarProvider>
<GlobalStateProvider>{children}</GlobalStateProvider>
</CommandBarProvider>
</BrowserRouter>
)
}
describe('NetworkHealthIndicator tests', () => {
test('Renders the network indicator', () => {
render(
<TestWrap>
<NetworkHealthIndicator />
</TestWrap>
)
fireEvent.click(screen.getByTestId('network-toggle'))
expect(screen.getByTestId('network-good')).toHaveTextContent(
NETWORK_CONTENT.good
)
})
test('Responds to network changes', () => {
render(
<TestWrap>
<NetworkHealthIndicator />
</TestWrap>
)
fireEvent.offline(window)
fireEvent.click(screen.getByTestId('network-toggle'))
expect(screen.getByTestId('network-bad')).toHaveTextContent(
NETWORK_CONTENT.bad
)
})
})

View File

@ -1,112 +0,0 @@
import {
faCheck,
faExclamation,
faWifi,
} from '@fortawesome/free-solid-svg-icons'
import { Popover } from '@headlessui/react'
import { useEffect, useState } from 'react'
import { ActionIcon } from './ActionIcon'
export const NETWORK_CONTENT = {
good: 'Network health is good',
bad: 'Network issue',
}
const NETWORK_MESSAGES = {
offline: 'You are offline',
}
export const NetworkHealthIndicator = () => {
const [networkIssues, setNetworkIssues] = useState<string[]>([])
const hasIssues = [...networkIssues.values()].length > 0
useEffect(() => {
const offlineListener = () =>
setNetworkIssues((issues) => {
return [
...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline),
NETWORK_MESSAGES.offline,
]
})
window.addEventListener('offline', offlineListener)
const onlineListener = () =>
setNetworkIssues((issues) => {
return [...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline)]
})
window.addEventListener('online', onlineListener)
return () => {
window.removeEventListener('offline', offlineListener)
window.removeEventListener('online', onlineListener)
}
}, [])
return (
<Popover className="relative">
<Popover.Button
className={
'p-0 border-none relative ' +
(hasIssues
? 'focus-visible:outline-destroy-80'
: 'focus-visible:outline-succeed-80')
}
data-testid="network-toggle"
>
<span className="sr-only">Network Health</span>
<ActionIcon
icon={faWifi}
iconClassName={
hasIssues
? 'text-destroy-80 dark:text-destroy-30'
: 'text-succeed-80 dark:text-succeed-30'
}
bgClassName={
hasIssues
? 'hover:bg-destroy-10/50 hover:dark:bg-destroy-80/50 rounded'
: 'hover:bg-succeed-10/50 hover:dark:bg-succeed-80/50 rounded'
}
/>
</Popover.Button>
<Popover.Panel className="absolute right-0 left-auto top-full mt-1 w-56 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch py-2 bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm">
{!hasIssues ? (
<span
className="flex items-center justify-center gap-1 px-4"
data-testid="network-good"
>
<ActionIcon
icon={faCheck}
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded'}
iconClassName={'text-succeed-80 dark:text-succeed-30'}
/>
{NETWORK_CONTENT.good}
</span>
) : (
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
<span
className="font-bold text-xs uppercase text-destroy-60 dark:text-destroy-50 px-4"
data-testid="network-bad"
>
{NETWORK_CONTENT.bad}
{networkIssues.length > 1 ? 's' : ''}
</span>
{networkIssues.map((issue) => (
<li
key={issue}
className="flex items-center gap-1 py-2 my-2 last:mb-0"
>
<ActionIcon
icon={faExclamation}
bgClassName={'bg-destroy-10/50 dark:bg-destroy-80/50 rounded'}
iconClassName={'text-destroy-80 dark:text-destroy-30'}
className="ml-4"
/>
<p className="flex-1 mr-4">{issue}</p>
</li>
))}
</ul>
)}
</Popover.Panel>
</Popover>
)
}

View File

@ -0,0 +1,42 @@
import { invoke } from '@tauri-apps/api/tauri'
import { open } from '@tauri-apps/api/dialog'
import { useStore } from '../useStore'
export const OpenFileButton = () => {
const { setCode } = useStore((s) => ({
setCode: s.setCode,
}))
const handleClick = async () => {
const selected = await open({
multiple: false,
directory: false,
filters: [
{
name: 'CAD',
extensions: ['toml'],
},
],
})
if (Array.isArray(selected)) {
// User selected multiple files
// We should not get here, since multiple is false.
} else if (selected === null) {
// User cancelled the selection
// Do nothing.
} else {
// User selected a single file
// We want to invoke our command to read the file.
const json: string = await invoke('read_toml', { path: selected })
const packageDetails = JSON.parse(json).package
if (packageDetails.main) {
const absPath = [
...selected.split('/').slice(0, -1),
packageDetails.main,
].join('/')
const file: string = await invoke('read_txt_file', { path: absPath })
setCode(file)
}
}
}
return <button onClick={() => handleClick()}>Open File</button>
}

View File

@ -1,4 +1,4 @@
import { FormEvent, useEffect, useState } from 'react'
import { FormEvent, useState } from 'react'
import { type ProjectWithEntryPointMetadata, paths } from '../Router'
import { Link } from 'react-router-dom'
import { ActionButton } from './ActionButton'
@ -8,7 +8,7 @@ import {
faTrashAlt,
faX,
} from '@fortawesome/free-solid-svg-icons'
import { FILE_EXT, getPartsCount, readProject } from '../lib/tauriFS'
import { FILE_EXT } from '../lib/tauriFS'
import { Dialog } from '@headlessui/react'
import { useHotkeys } from 'react-hotkeys-hook'
@ -28,8 +28,6 @@ function ProjectCard({
useHotkeys('esc', () => setIsEditing(false))
const [isEditing, setIsEditing] = useState(false)
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const [numberOfParts, setNumberOfParts] = useState(1)
const [numberOfFolders, setNumberOfFolders] = useState(0)
function handleSave(e: FormEvent<HTMLFormElement>) {
e.preventDefault()
@ -44,17 +42,6 @@ function ProjectCard({
: date.toLocaleTimeString()
}
useEffect(() => {
async function getNumberOfParts() {
const { kclFileCount, kclDirCount } = getPartsCount(
await readProject(project.path)
)
setNumberOfParts(kclFileCount)
setNumberOfFolders(kclDirCount)
}
getNumberOfParts()
}, [project.path])
return (
<li
{...props}
@ -89,7 +76,7 @@ function ProjectCard({
</form>
) : (
<>
<div className="p-1 flex flex-col h-full gap-2">
<div className="p-1 flex flex-col gap-2">
<Link
to={`${paths.FILE}/${encodeURIComponent(project.path)}`}
className="flex-1 text-liquid-100"
@ -97,14 +84,7 @@ function ProjectCard({
{project.name?.replace(FILE_EXT, '')}
</Link>
<span className="text-chalkboard-60 text-xs">
{numberOfParts} part{numberOfParts === 1 ? '' : 's'}{' '}
{numberOfFolders > 0 &&
`/ ${numberOfFolders} folder${
numberOfFolders === 1 ? '' : 's'
}`}
</span>
<span className="text-chalkboard-60 text-xs">
Edited {getDisplayedTime(project.entrypointMetadata.modifiedAt)}
Edited {getDisplayedTime(project.entrypoint_metadata.modifiedAt)}
</span>
<div className="absolute bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
<ActionButton

View File

@ -2,8 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { ProjectWithEntryPointMetadata } from '../Router'
import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar'
const now = new Date()
const projectWellFormed = {
@ -15,7 +13,7 @@ const projectWellFormed = {
path: '/some/path/Simple Box/main.kcl',
},
],
entrypointMetadata: {
entrypoint_metadata: {
accessedAt: now,
blksize: 32,
blocks: 32,
@ -40,11 +38,7 @@ describe('ProjectSidebarMenu tests', () => {
test('Renders the project name', () => {
render(
<BrowserRouter>
<CommandBarProvider>
<GlobalStateProvider>
<ProjectSidebarMenu project={projectWellFormed} />
</GlobalStateProvider>
</CommandBarProvider>
<ProjectSidebarMenu project={projectWellFormed} />
</BrowserRouter>
)
@ -61,11 +55,7 @@ describe('ProjectSidebarMenu tests', () => {
test('Renders app name if given no project', () => {
render(
<BrowserRouter>
<CommandBarProvider>
<GlobalStateProvider>
<ProjectSidebarMenu />
</GlobalStateProvider>
</CommandBarProvider>
<ProjectSidebarMenu />
</BrowserRouter>
)
@ -79,14 +69,7 @@ describe('ProjectSidebarMenu tests', () => {
test('Renders as a link if set to do so', () => {
render(
<BrowserRouter>
<CommandBarProvider>
<GlobalStateProvider>
<ProjectSidebarMenu
project={projectWellFormed}
renderAsLink={true}
/>
</GlobalStateProvider>
</CommandBarProvider>
<ProjectSidebarMenu project={projectWellFormed} renderAsLink={true} />
</BrowserRouter>
)

View File

@ -1,36 +1,31 @@
import { Popover, Transition } from '@headlessui/react'
import { Popover } from '@headlessui/react'
import { ActionButton } from './ActionButton'
import { faHome } from '@fortawesome/free-solid-svg-icons'
import { IndexLoaderData, paths } from '../Router'
import { ProjectWithEntryPointMetadata, paths } from '../Router'
import { isTauri } from '../lib/isTauri'
import { Link } from 'react-router-dom'
import { ExportButton } from './ExportButton'
import { Fragment } from 'react'
import { FileTree } from './FileTree'
import { sep } from '@tauri-apps/api/path'
const ProjectSidebarMenu = ({
project,
file,
renderAsLink = false,
}: {
renderAsLink?: boolean
project?: IndexLoaderData['project']
file?: IndexLoaderData['file']
project?: Partial<ProjectWithEntryPointMetadata>
}) => {
return renderAsLink ? (
<Link
to={paths.HOME}
className="h-9 max-h-min min-w-max border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50"
to={'../'}
className="flex items-center gap-4 my-2"
data-testid="project-sidebar-link"
>
<img
src="/kitt-8bit-winking.svg"
alt="KittyCAD App"
className="w-auto h-9"
className="h-9 w-auto"
/>
<span
className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block"
className="text-sm text-chalkboard-110 dark:text-chalkboard-20 min-w-max"
data-testid="project-sidebar-link-name"
>
{project?.name ? project.name : 'KittyCAD Modeling App'}
@ -39,118 +34,66 @@ const ProjectSidebarMenu = ({
) : (
<Popover className="relative">
<Popover.Button
className="h-9 max-h-min min-w-max border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50"
className="border-0 px-1 pr-2 pl-0 flex items-center gap-4 focus:outline-none focus:ring-2 focus:ring-energy-50"
data-testid="project-sidebar-toggle"
>
<img
src="/kitt-8bit-winking.svg"
alt="KittyCAD App"
className="w-auto h-full"
className="h-9 w-auto"
/>
<div className="flex flex-col items-start py-0.5">
<span className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block">
{isTauri() && file?.name
? file.name.slice(file.name.lastIndexOf(sep) + 1)
: 'KittyCAD Modeling App'}
</span>
{isTauri() && project?.name && (
<span className="hidden text-xs text-chalkboard-70 dark:text-chalkboard-40 whitespace-nowrap lg:block">
{project.name}
</span>
<span className="text-sm text-chalkboard-110 dark:text-chalkboard-20 min-w-max">
{isTauri() && project?.name ? project.name : 'KittyCAD Modeling App'}
</span>
</Popover.Button>
<Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" />
<Popover.Panel className="fixed inset-0 right-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-energy-100 shadow-md rounded-r-lg overflow-hidden">
<div className="flex items-center gap-4 px-4 py-3 bg-energy-100">
<img
src="/kitt-8bit-winking.svg"
alt="KittyCAD App"
className="h-9 w-auto"
/>
<div>
<p
className="m-0 text-energy-10 text-mono"
data-testid="projectName"
>
{project?.name ? project.name : 'KittyCAD Modeling App'}
</p>
{project?.entrypoint_metadata && (
<p className="m-0 text-energy-40 text-xs" data-testid="createdAt">
Created{' '}
{project?.entrypoint_metadata.createdAt.toLocaleDateString()}
</p>
)}
</div>
</div>
<div className="p-4 flex flex-col gap-2">
<ExportButton
className={{
button:
'border-transparent dark:border-transparent dark:hover:border-energy-60',
}}
>
Export Model
</ExportButton>
{isTauri() && (
<ActionButton
Element="link"
to={paths.HOME}
icon={{
icon: faHome,
}}
className="border-transparent dark:border-transparent dark:hover:border-energy-60"
>
Go to Home
</ActionButton>
)}
</div>
</Popover.Button>
<Transition
enter="duration-200 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-100 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as={Fragment}
>
<Popover.Overlay className="fixed inset-0 z-20 bg-chalkboard-110/50" />
</Transition>
<Transition
enter="duration-100 ease-out"
enterFrom="opacity-0 -translate-x-1/4"
enterTo="opacity-100 translate-x-0"
leave="duration-75 ease-in"
leaveFrom="opacity-100 translate-x-0"
leaveTo="opacity-0 -translate-x-4"
as={Fragment}
>
<Popover.Panel
className="fixed inset-0 right-auto z-30 grid w-64 h-screen max-h-screen grid-cols-1 border rounded-r-lg shadow-md bg-chalkboard-10 dark:bg-chalkboard-100 border-energy-100 dark:border-energy-100/50"
style={{ gridTemplateRows: 'auto 1fr auto' }}
>
{({ close }) => (
<>
<div className="flex items-center gap-4 px-4 py-3 bg-energy-10/25 dark:bg-energy-110">
<img
src="/kitt-8bit-winking.svg"
alt="KittyCAD App"
className="w-auto h-9"
/>
<div>
<p
className="m-0 text-chalkboard-100 dark:text-energy-10 text-mono"
data-testid="projectName"
>
{project?.name ? project.name : 'KittyCAD Modeling App'}
</p>
{project?.entrypointMetadata && (
<p
className="m-0 text-xs text-chalkboard-100 dark:text-energy-40"
data-testid="createdAt"
>
Created{' '}
{project.entrypointMetadata.createdAt.toLocaleDateString()}
</p>
)}
</div>
</div>
{isTauri() ? (
<FileTree
file={file}
className="overflow-hidden border-0 border-y border-energy-40 dark:border-energy-70"
closePanel={close}
/>
) : (
<div className="flex-1 overflow-hidden" />
)}
<div className="flex flex-col gap-2 p-4 bg-energy-10/25 dark:bg-energy-110">
<ExportButton
className={{
button:
'border-transparent dark:border-transparent hover:border-energy-60',
icon: 'text-energy-10 dark:text-energy-120',
bg: 'bg-energy-120 dark:bg-energy-10',
}}
>
Export Model
</ExportButton>
{isTauri() && (
<ActionButton
Element="link"
to={paths.HOME}
icon={{
icon: faHome,
iconClassName: 'text-energy-10 dark:text-energy-120',
bgClassName: 'bg-energy-120 dark:bg-energy-10',
}}
className="border-transparent dark:border-transparent hover:border-energy-60"
>
Go to Home
</ActionButton>
)}
</div>
</>
)}
</Popover.Panel>
</Transition>
</Popover.Panel>
</Popover>
)
}

View File

@ -1,7 +1,6 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'
import { type InstanceProps, create } from 'react-modal-promise'
import { Value } from '../lang/wasm'
import { Value } from '../lang/abstractSyntaxTreeTypes'
import {
AvailableVars,
addToInputHelper,
@ -10,28 +9,6 @@ import {
CreateNewVariable,
} from './AvailableVarsHelpers'
type ModalResolve = {
value: string
sign: number
valueNode: Value
variableName?: string
newVariableInsertIndex: number
}
type ModalReject = boolean
type SetAngleLengthModalProps = InstanceProps<ModalResolve, ModalReject> & {
value: number
valueName: string
shouldCreateVariable?: boolean
}
export const createSetAngleLengthModal = create<
SetAngleLengthModalProps,
ModalResolve,
ModalReject
>
export const SetAngleLengthModal = ({
isOpen,
onResolve,
@ -39,7 +16,20 @@ export const SetAngleLengthModal = ({
value: initialValue,
valueName,
shouldCreateVariable: initialShouldCreateVariable = false,
}: SetAngleLengthModalProps) => {
}: {
isOpen: boolean
onResolve: (a: {
value: string
sign: number
valueNode: Value
variableName?: string
newVariableInsertIndex: number
}) => void
onReject: (a: any) => void
value: number
valueName: string
shouldCreateVariable: boolean
}) => {
const [sign, setSign] = useState(Math.sign(Number(initialValue)))
const [value, setValue] = useState(String(initialValue * sign))
const [shouldCreateVariable, setShouldCreateVariable] = useState(
@ -108,7 +98,7 @@ export const SetAngleLengthModal = ({
</label>
<div className="mt-1 flex">
<button
className="border border-gray-300 px-2 text-gray-900"
className="border border-gray-300 px-2"
onClick={() => setSign(-sign)}
>
{sign > 0 ? '+' : '-'}
@ -118,7 +108,7 @@ export const SetAngleLengthModal = ({
type="text"
name="val"
id="val"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 text-gray-900"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1"
value={value}
onChange={(e) => {
setValue(e.target.value)

View File

@ -1,7 +1,6 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'
import { type InstanceProps, create } from 'react-modal-promise'
import { Value } from '../lang/wasm'
import { Value } from '../lang/abstractSyntaxTreeTypes'
import {
AvailableVars,
addToInputHelper,
@ -10,30 +9,6 @@ import {
CreateNewVariable,
} from './AvailableVarsHelpers'
type ModalResolve = {
value: string
segName: string
valueNode: Value
variableName?: string
newVariableInsertIndex: number
sign: number
}
type ModalReject = boolean
type GetInfoModalProps = InstanceProps<ModalResolve, ModalReject> & {
segName: string
isSegNameEditable: boolean
value?: number
initialVariableName: string
}
export const createInfoModal = create<
GetInfoModalProps,
ModalResolve,
ModalReject
>
export const GetInfoModal = ({
isOpen,
onResolve,
@ -42,12 +17,25 @@ export const GetInfoModal = ({
isSegNameEditable,
value: initialValue,
initialVariableName,
}: GetInfoModalProps) => {
}: {
isOpen: boolean
onResolve: (a: {
value: string
segName: string
valueNode: Value
variableName?: string
newVariableInsertIndex: number
sign: number
}) => void
onReject: (a: any) => void
segName: string
isSegNameEditable: boolean
value: number
initialVariableName: string
}) => {
const [sign, setSign] = useState(Math.sign(Number(initialValue)))
const [segName, setSegName] = useState(initialSegName)
const [value, setValue] = useState(
initialValue === undefined ? '' : String(Math.abs(initialValue))
)
const [value, setValue] = useState(String(Math.abs(initialValue)))
const [shouldCreateVariable, setShouldCreateVariable] = useState(false)
const {
@ -87,7 +75,7 @@ export const GetInfoModal = ({
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white/90 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
@ -109,7 +97,7 @@ export const GetInfoModal = ({
</label>
<div className="mt-1 flex">
<button
className="border border-gray-400 px-2 mr-1 text-gray-900"
className="border border-gray-300 px-2 mr-1"
onClick={() => setSign(-sign)}
>
{sign > 0 ? '+' : '-'}
@ -119,7 +107,7 @@ export const GetInfoModal = ({
name="val"
id="val"
ref={inputRef}
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm text-gray-900 border-gray-300 rounded-md font-mono"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono"
value={value}
onChange={(e) => {
setValue(e.target.value)
@ -139,7 +127,7 @@ export const GetInfoModal = ({
name="segName"
id="segName"
disabled={!isSegNameEditable}
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm text-gray-900 border-gray-300 rounded-md font-mono"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono"
value={segName}
onChange={(e) => {
setSegName(e.target.value)

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