Compare commits

..

9 Commits

Author SHA1 Message Date
ae068b7ed6 closer
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 17:02:37 -07:00
5bdc899831 updates
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 16:25:08 -07:00
e0e6acf231 updates
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 16:08:11 -07:00
668bc863df updates
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 16:06:56 -07:00
59ce602d6e updates
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 16:06:44 -07:00
8ce819b28f updates
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 13:58:08 -07:00
ed5e276377 working
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 13:47:38 -07:00
8549bd9486 updates
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 13:41:03 -07:00
56f9078812 init
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-19 13:33:31 -07:00
396 changed files with 6518 additions and 14423 deletions

View File

@ -3,4 +3,3 @@ VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
VITE_KC_SKIP_AUTH=false VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000 VITE_KC_CONNECTION_TIMEOUT_MS=5000
VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local"

View File

@ -1,33 +0,0 @@
name: Build and Store WASM
on:
push:
branches:
- main
jobs:
build-and-upload:
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: 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
# Upload the WASM bundle as an artifact
- uses: actions/upload-artifact@v3
with:
name: wasm-bundle
path: src/wasm-lib/pkg

View File

@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
dir: ['src/wasm-lib', 'src-tauri'] dir: ['src/wasm-lib']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install latest rust - name: Install latest rust
@ -31,22 +31,9 @@ jobs:
- name: install dependencies - name: install dependencies
if: matrix.dir == 'src-tauri' if: matrix.dir == 'src-tauri'
shell: bash
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y \ sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
libgtk-3-dev \
libayatana-appindicator3-dev \
webkit2gtk-driver \
libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev \
libwebkit2gtk-4.1-dev \
at-spi2-core \
xvfb
yarn install
yarn build:wasm
yarn build:local
- name: Rust Cache - name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1 uses: Swatinem/rust-cache@v2.6.1

View File

@ -1,57 +0,0 @@
on:
push:
branches:
- main
paths:
- 'src-tauri/**.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-test-tauri.yml
pull_request:
paths:
- 'src-tauri/**.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- .github/workflows/cargo-test-tauri.yml
workflow_dispatch:
permissions: read-all
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: cargo test of tauri
jobs:
cargotest:
name: cargo test
runs-on: ubuntu-latest-8-cores
strategy:
matrix:
dir: ['src-tauri']
steps:
- uses: actions/checkout@v4
- name: Install latest rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: install dependencies
if: matrix.dir == 'src-tauri'
run: |
sudo apt-get update
sudo apt-get install -y \
libgtk-3-dev \
libayatana-appindicator3-dev \
webkit2gtk-driver \
libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev \
libwebkit2gtk-4.1-dev \
at-spi2-core \
xvfb
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1
- name: cargo test
shell: bash
run: |-
cd "${{ matrix.dir }}"
cargo test --all

View File

@ -13,7 +13,7 @@ on:
# Will checkout the last commit from the default branch (main as of 2023-10-04) # Will checkout the last commit from the default branch (main as of 2023-10-04)
env: 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')) }} 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: concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@ -50,7 +50,7 @@ jobs:
- run: yarn tsc - run: yarn tsc
check-typos: check-typos:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -98,7 +98,7 @@ jobs:
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'yarn' cache: 'yarn'
- name: Set nightly version - name: Set nightly version
if: github.event_name == 'schedule' if: github.event_name == 'schedule'
run: | run: |
@ -130,9 +130,7 @@ jobs:
matrix: matrix:
os: [macos-14, ubuntu-latest, windows-latest] os: [macos-14, ubuntu-latest, windows-latest]
env: env:
# Specific Apple Universal target for macos
TAURI_ARGS_MACOS: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }} TAURI_ARGS_MACOS: ${{ matrix.os == 'macos-14' && '--target universal-apple-darwin' || '' }}
# Only build executable on linux (no appimage or deb)
TAURI_ARGS_UBUNTU: ${{ matrix.os == 'ubuntu-latest' && '--bundles' || '' }} TAURI_ARGS_UBUNTU: ${{ matrix.os == 'ubuntu-latest' && '--bundles' || '' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -145,21 +143,21 @@ jobs:
ls -l artifact ls -l artifact
cp artifact/package.json package.json cp artifact/package.json package.json
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.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 cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
- name: Install ubuntu system dependencies - name: Install ubuntu system dependencies
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
run: | run: >
sudo apt-get update sudo apt-get update &&
sudo apt-get install -y \ sudo apt-get install -y
libgtk-3-dev \ libgtk-3-dev
libayatana-appindicator3-dev \ libayatana-appindicator3-dev
webkit2gtk-driver \ webkit2gtk-driver
libsoup-3.0-dev \ libsoup-3.0-dev
libjavascriptcoregtk-4.1-dev \ libjavascriptcoregtk-4.1-dev
libwebkit2gtk-4.1-dev \ libwebkit2gtk-4.1-dev
at-spi2-core \ at-spi2-core
xvfb xvfb
- name: Sync node version and setup cache - name: Sync node version and setup cache
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -239,96 +237,6 @@ jobs:
includeDebug: true includeDebug: true
args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}" args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
- name: Mac App Store
if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'macos-14' }}
shell: bash
run: |
unset APPLE_SIGNING_IDENTITY
unset APPLE_CERTIFICATE
sign_app="3rd Party Mac Developer Application: KittyCAD Inc (${APPLE_TEAM_ID})"
sign_install="3rd Party Mac Developer Installer: KittyCAD Inc (${APPLE_TEAM_ID})"
profile="src-tauri/entitlements/Mac_App_Distribution.provisionprofile"
mkdir -p src-tauri/entitlements
echo -n "${APPLE_STORE_PROVISIONING_PROFILE}" | base64 --decode -o "${profile}"
echo -n "${APPLE_STORE_DISTRIBUTION_CERT}" | base64 --decode -o "dist.cer"
echo -n "${APPLE_STORE_INSTALLER_CERT}" | base64 --decode -o "installer.cer"
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD="password"
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import "dist.cer" -P "$APPLE_STORE_P12_PASSWORD" -k $KEYCHAIN_PATH -f pkcs12 -t cert -A
security import "installer.cer" -P "$APPLE_STORE_P12_PASSWORD" -k $KEYCHAIN_PATH -f pkcs12 -t cert -A
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
target="universal-apple-darwin"
# Turn off the default target
# We don't want to install the updater for the apple store build
sed -i.bu "s/default =/# default =/" src-tauri/Cargo.toml
rm src-tauri/Cargo.toml.bu
git diff src-tauri/Cargo.toml
yarn tauri build --target "${target}" --verbose --config src-tauri/tauri.app-store.conf.json
app_path="src-tauri/target/${target}/release/bundle/macos/Zoo Modeling App.app"
build_name="src-tauri/target/${target}/release/bundle/macos/Zoo Modeling App.pkg"
cp_dir="src-tauri/target/${target}/release/bundle/macos/Zoo Modeling App.app/Contents/embedded.provisionprofile"
entitlements="src-tauri/entitlements/app-store.entitlements"
cp "${profile}" "${cp_dir}"
codesign --deep --force -s "${sign_app}" --entitlements "${entitlements}" "${app_path}"
productbuild --component "${app_path}" /Applications/ --sign "${sign_install}" "${build_name}"
# Undo the changes to the Cargo.toml
git checkout src-tauri/Cargo.toml
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_STORE_PROVISIONING_PROFILE: ${{ secrets.APPLE_STORE_PROVISIONING_PROFILE }}
APPLE_STORE_DISTRIBUTION_CERT: ${{ secrets.APPLE_STORE_DISTRIBUTION_CERT }}
APPLE_STORE_INSTALLER_CERT: ${{ secrets.APPLE_STORE_INSTALLER_CERT }}
APPLE_STORE_P12_PASSWORD: ${{ secrets.APPLE_STORE_P12_PASSWORD }}
- name: 'Upload app to TestFlight'
uses: apple-actions/upload-testflight-build@v1
if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'macos-14' }}
with:
app-path: 'src-tauri/target/universal-apple-darwin/release/bundle/macos/Zoo Modeling App.pkg'
issuer-id: ${{ secrets.APPLE_STORE_ISSUER_ID }}
api-key-id: ${{ secrets.APPLE_STORE_API_KEY_ID }}
api-private-key: ${{ secrets.APPLE_STORE_API_PRIVATE_KEY }}
app-type: osx
- name: Clean up after Mac App Store
if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'macos-14' }}
shell: bash
run: |
git status
# remove our target builds because we want to make sure the later build
# includes the updater, and that anything we changed with the target
# does not persist
rm -rf src-tauri/target
# Lets get rid of the info.plist for the normal mac builds since its
# being sketchy.
rm src-tauri/Info.plist
# We do this after the apple store because the apple store build is
# specific and we want to overwrite it with the this new build after and
# not upload the apple store build to the public bucket
- name: Build the app (release) and sign - name: Build the app (release) and sign
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
if: ${{ env.BUILD_RELEASE == 'true' }} if: ${{ env.BUILD_RELEASE == 'true' }}
@ -353,10 +261,11 @@ jobs:
with: with:
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*" path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
# TODO: re-enable linux e2e tests when possible
- name: Run e2e tests (linux only) - name: Run e2e tests (linux only)
if: matrix.os == 'ubuntu-latest' if: false
run: | run: |
cargo install tauri-driver --force cargo install tauri-driver
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }} source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
export VITE_KC_API_BASE_URL export VITE_KC_API_BASE_URL
xvfb-run yarn test:e2e:tauri xvfb-run yarn test:e2e:tauri
@ -474,7 +383,7 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: 'artifact/*/Zoo*' files: 'artifact/*/Zoo*'
announce_release: announce_release:
needs: [publish-apps-release] needs: [publish-apps-release]
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -482,17 +391,17 @@ jobs:
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: '3.x' python-version: '3.x'
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install requests pip install requests
- name: Announce Release - name: Announce Release
env: env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}

View File

@ -1,37 +0,0 @@
name: Create Release
on:
push:
branches:
- main
jobs:
create-release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
if: contains(github.event.head_commit.message, 'Cut release v')
steps:
- uses: actions/github-script@v7
name: Read Cut release PR info and create release
with:
script: |
const { owner, repo } = context.repo
const pulls = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner,
repo,
commit_sha: context.sha,
})
const { title, body } = pulls.data[0]
const version = title.split('Cut release ')[1]
const result = await github.rest.repos.createRelease({
owner,
repo,
body,
tag_name: version,
name: version,
draft: true,
})
console.log(result)

View File

@ -12,31 +12,11 @@ concurrency:
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
actions: read
jobs: jobs:
check-rust-changes:
runs-on: ubuntu-latest
outputs:
rust-changed: ${{ steps.filter.outputs.rust }}
steps:
- uses: actions/checkout@v4
- id: filter
name: Check for Rust changes
uses: dorny/paths-filter@v3
with:
filters: |
rust:
- 'src/wasm-lib/**'
playwright-ubuntu: playwright-ubuntu:
timeout-minutes: 60 timeout-minutes: 60
runs-on: ubuntu-latest-8-cores runs-on: ubuntu-latest-8-cores
needs: check-rust-changes
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
@ -48,38 +28,13 @@ jobs:
run: yarn run: yarn
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: yarn playwright install --with-deps run: yarn playwright install --with-deps
- name: Download Wasm Cache
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v3
continue-on-error: true
with:
github_token: ${{secrets.GITHUB_TOKEN}}
name: wasm-bundle
workflow: build-and-store-wasm.yml
branch: main
path: src/wasm-lib/pkg
- name: copy wasm blob
if: needs.check-rust-changes.outputs.rust-changed == 'false'
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
continue-on-error: true
- name: Setup Rust - name: Setup Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Cache Wasm (because rust diff) - name: Cache wasm
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: './src/wasm-lib' workspaces: './src/wasm-lib'
- name: OR Cache Wasm (because wasm cache failed) - name: build wasm
if: steps.download-wasm.outcome == 'failure'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Build Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm
- name: OR Build Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
run: yarn build:wasm run: yarn build:wasm
- name: build web - name: build web
run: yarn build:local run: yarn build:local
@ -134,7 +89,6 @@ jobs:
playwright-macos: playwright-macos:
timeout-minutes: 60 timeout-minutes: 60
runs-on: macos-14 runs-on: macos-14
needs: check-rust-changes
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
@ -145,38 +99,13 @@ jobs:
run: yarn run: yarn
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: yarn playwright install --with-deps run: yarn playwright install --with-deps
- name: Download Wasm Cache
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v3
continue-on-error: true
with:
github_token: ${{secrets.GITHUB_TOKEN}}
name: wasm-bundle
workflow: build-and-store-wasm.yml
branch: main
path: src/wasm-lib/pkg
- name: copy wasm blob
if: needs.check-rust-changes.outputs.rust-changed == 'false'
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
continue-on-error: true
- name: Setup Rust - name: Setup Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Cache Wasm (because rust diff) - name: Cache wasm
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: './src/wasm-lib' workspaces: './src/wasm-lib'
- name: OR Cache Wasm (because wasm cache failed) - name: build wasm
if: steps.download-wasm.outcome == 'failure'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Build Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm
- name: OR Build Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
run: yarn build:wasm run: yarn build:wasm
- name: build web - name: build web
run: yarn build:local run: yarn build:local

1
.gitignore vendored
View File

@ -54,4 +54,3 @@ src/**/*.typegen.ts
src-tauri/gen src-tauri/gen
src/wasm-lib/grackle/stdlib_cube_partial.json src/wasm-lib/grackle/stdlib_cube_partial.json
Mac_App_Distribution.provisionprofile

2
.nvmrc
View File

@ -1 +1 @@
v21.7.1 v20.5.0

View File

@ -59,10 +59,6 @@ followed by:
``` ```
yarn build:wasm-dev yarn build:wasm-dev
``` ```
or if you have the gh cli installed
```
./get-latest-wasm-bundle.sh # this will download the latest main wasm bundle
```
That will build the WASM binary and put in the `public` dir (though gitignored) That will build the WASM binary and put in the `public` dir (though gitignored)
@ -72,13 +68,7 @@ finally, to run the web app only, run:
yarn start yarn start
``` ```
If you're not an KittyCAD employee you won't be able to access the dev environment, you should copy everything from `.env.production` to `.env.development` to make it point to production instead, then when you navigate to `localhost:3000` the easiest way to sign in is to paste `localStorage.setItem('TOKEN_PERSIST_KEY', "your-token-from-https://zoo.dev/account/api-tokens")` replacing the with a real token from https://zoo.dev/account/api-tokens ofcourse, then navigate to localhost:3000 again. Note that navigating to localhost:3000/signin removes your token so you will need to set the token again. ## Developing in Chrome
### Development environment variables
The Copilot LSP plugin in the editor requires a Zoo API token to run. In production, we authenticate this with a token via cookie in the browser and device auth token in the desktop environment, but this token is inaccessible in the dev browser version because the cookie is considered "cross-site" (from `localhost` to `dev.zoo.dev`). There is an optional environment variable called `VITE_KC_DEV_TOKEN` that you can populate with a dev token in a `.env.development.local` file to not check it into Git, which will use that token instead of other methods for the LSP service.
### Developing in Chrome
Chrome is in the process of rolling out a new default which Chrome is in the process of rolling out a new default which
[blocks Third-Party Cookies](https://developer.chrome.com/en/docs/privacy-sandbox/third-party-cookie-phase-out/). [blocks Third-Party Cookies](https://developer.chrome.com/en/docs/privacy-sandbox/third-party-cookie-phase-out/).

View File

@ -1,3 +1,3 @@
module.exports = { module.exports = {
presets: ['@babel/preset-env'], presets: ["@babel/preset-env"],
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { makeTemplate, getUtils } from './test-utils' import { getUtils } from './test-utils'
import waitOn from 'wait-on' import waitOn from 'wait-on'
import { roundOff } from 'lib/utils' import { roundOff } from 'lib/utils'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes' import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
@ -8,8 +8,7 @@ import {
TEST_SETTINGS, TEST_SETTINGS,
TEST_SETTINGS_KEY, TEST_SETTINGS_KEY,
TEST_SETTINGS_CORRUPTED, TEST_SETTINGS_CORRUPTED,
TEST_SETTINGS_ONBOARDING_EXPORT, TEST_SETTINGS_ONBOARDING,
TEST_SETTINGS_ONBOARDING_START,
} from './storageStates' } from './storageStates'
import * as TOML from '@iarna/toml' import * as TOML from '@iarna/toml'
@ -105,7 +104,6 @@ test('Basic sketch', async ({ page }) => {
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`) |> line([0, ${commonPoints.num1}], %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
@ -279,7 +277,7 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
const bottomAng = 25 const bottomAng = 25
*/ */
await page.click('.cm-content') await page.click('.cm-content')
await page.keyboard.type('$ error') await page.keyboard.type('# error')
// press arrows to clear autocomplete // press arrows to clear autocomplete
await page.keyboard.press('ArrowLeft') await page.keyboard.press('ArrowLeft')
@ -296,10 +294,10 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
await expect(page.getByText("found unknown token '$'")).toBeVisible() await expect(page.getByText("found unknown token '#'")).toBeVisible()
// select the line that's causing the error and delete it // select the line that's causing the error and delete it
await page.getByText('$ error').click() await page.getByText('# error').click()
await page.keyboard.press('End') await page.keyboard.press('End')
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await page.keyboard.press('Home') await page.keyboard.press('Home')
@ -330,7 +328,9 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
}) })
test('if your kcl gets an error from the engine it is inlined', async ({ /* Ignore this test for now since its causing engine to crash
*
* test('if your kcl gets an error from the engine it is inlined', async ({
page, page,
}) => { }) => {
const u = getUtils(page) const u = getUtils(page)
@ -349,7 +349,7 @@ const sketch001 = startSketchOn(box, "revolveAxis")
|> startProfileAt([5, 10], %) |> startProfileAt([5, 10], %)
|> line([0, -10], %) |> line([0, -10], %)
|> line([2, 0], %) |> line([2, 0], %)
|> line([0, -10], %) |> line([0, 10], %)
|> close(%) |> close(%)
|> revolve({ |> revolve({
axis: getEdge('revolveAxis', box), axis: getEdge('revolveAxis', box),
@ -364,7 +364,7 @@ angle: 90
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel() await u.closeDebugPanel()
@ -378,7 +378,7 @@ angle: 90
'sketch profile must lie entirely on one side of the revolution axis' 'sketch profile must lie entirely on one side of the revolution axis'
) )
).toBeVisible() ).toBeVisible()
}) })*/
test('executes on load', async ({ page }) => { test('executes on load', async ({ page }) => {
const u = getUtils(page) const u = getUtils(page)
@ -566,9 +566,7 @@ test('Auto complete works', async ({ page }) => {
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.keyboard.type('12') await page.keyboard.type('12')
await page.waitForTimeout(100)
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
@ -597,12 +595,13 @@ test('Auto complete works', async ({ page }) => {
test('Stored settings are validated and fall back to defaults', async ({ test('Stored settings are validated and fall back to defaults', async ({
page, page,
context,
}) => { }) => {
const u = getUtils(page) const u = getUtils(page)
// Override beforeEach test setup // Override beforeEach test setup
// with corrupted settings // with corrupted settings
await page.addInitScript( await context.addInitScript(
async ({ settingsKey, settings }) => { async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings) localStorage.setItem(settingsKey, settings)
}, },
@ -619,18 +618,18 @@ test('Stored settings are validated and fall back to defaults', async ({
// Check the settings were reset // Check the settings were reset
const storedSettings = TOML.parse( const storedSettings = TOML.parse(
await page.evaluate( await page.evaluate(
({ settingsKey }) => localStorage.getItem(settingsKey) || '', ({ settingsKey }) => localStorage.getItem(settingsKey) || '{}',
{ settingsKey: TEST_SETTINGS_KEY } { settingsKey: TEST_SETTINGS_KEY }
) )
) as { settings: SaveSettingsPayload } ) as { settings: SaveSettingsPayload }
expect(storedSettings.settings?.app?.theme).toBe(undefined) expect(storedSettings.settings.app?.theme).toBe('dark')
// Check that the invalid settings were removed // Check that the invalid settings were removed
expect(storedSettings.settings?.modeling?.defaultUnit).toBe(undefined) expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined)
expect(storedSettings.settings?.modeling?.mouseControls).toBe(undefined) expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined)
expect(storedSettings.settings?.app?.projectDirectory).toBe(undefined) expect(storedSettings.settings.app?.projectDirectory).toBe(undefined)
expect(storedSettings.settings?.projects?.defaultProjectName).toBe(undefined) expect(storedSettings.settings.projects?.defaultProjectName).toBe(undefined)
}) })
test('Project settings can be set and override user settings', async ({ test('Project settings can be set and override user settings', async ({
@ -681,45 +680,6 @@ test('Project settings can be set and override user settings', async ({
await expect(page.locator('select[name="app-theme"]')).toHaveValue('light') await expect(page.locator('select[name="app-theme"]')).toHaveValue('light')
}) })
test('Click through each onboarding step', async ({ page }) => {
const u = getUtils(page)
// Override beforeEach test setup
await page.addInitScript(
async ({ settingsKey, settings }) => {
// Give no initial code, so that the onboarding start is shown immediately
localStorage.setItem('persistCode', '')
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_START }),
}
)
await page.setViewportSize({ width: 1200, height: 1080 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
const nextButton = page.getByTestId('onboarding-next')
while ((await nextButton.innerText()) !== 'Finish') {
await expect(nextButton).toBeVisible()
await nextButton.click()
}
// Finish the onboarding
await expect(nextButton).toBeVisible()
await nextButton.click()
// Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect(page.url()).not.toContain('onboarding')
})
test('Onboarding redirects and code updating', async ({ page }) => { test('Onboarding redirects and code updating', async ({ page }) => {
const u = getUtils(page) const u = getUtils(page)
@ -732,7 +692,7 @@ test('Onboarding redirects and code updating', async ({ page }) => {
}, },
{ {
settingsKey: TEST_SETTINGS_KEY, settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_EXPORT }), settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING }),
} }
) )
@ -776,7 +736,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await u.openDebugPanel() await u.openDebugPanel()
const xAxisClick = () => const xAxisClick = () =>
page.mouse.click(700, 253).then(() => page.waitForTimeout(100)) page.mouse.click(700, 250).then(() => page.waitForTimeout(100))
const emptySpaceClick = () => const emptySpaceClick = () =>
page.mouse.click(728, 343).then(() => page.waitForTimeout(100)) page.mouse.click(728, 343).then(() => page.waitForTimeout(100))
const topHorzSegmentClick = () => const topHorzSegmentClick = () =>
@ -801,7 +761,6 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %)`) |> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
@ -809,14 +768,12 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`) |> line([${commonPoints.num1}, 0], %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`) |> line([0, ${commonPoints.num1}], %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
@ -829,14 +786,10 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await page.getByRole('button', { name: 'Line' }).click() await page.getByRole('button', { name: 'Line' }).click()
await u.closeDebugPanel() await u.closeDebugPanel()
const selectionSequence = async (isSecondTime = false) => { const selectionSequence = async () => {
await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.waitForTimeout(100) await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
await page.mouse.move(
startXPx + PUR * 15,
isSecondTime ? 430 : 500 - PUR * 10
)
await expect(page.getByTestId('hover-highlight')).toBeVisible() await expect(page.getByTestId('hover-highlight')).toBeVisible()
// bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience // bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience
@ -846,10 +799,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
// check mousing off, than mousing onto another line // check mousing off, than mousing onto another line
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
await expect(page.getByTestId('hover-highlight')).not.toBeVisible() await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
await page.mouse.move( await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 20) // mouse onto another line
startXPx + PUR * 10,
isSecondTime ? 295 : 500 - PUR * 20
) // mouse onto another line
await expect(page.getByTestId('hover-highlight')).toBeVisible() await expect(page.getByTestId('hover-highlight')).toBeVisible()
// now check clicking works including axis // now check clicking works including axis
@ -859,7 +809,6 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
const absYButton = page.getByRole('button', { name: 'ABS Y' }) const absYButton = page.getByRole('button', { name: 'ABS Y' })
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await page.waitForTimeout(100)
await xAxisClick() await xAxisClick()
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await absYButton.and(page.locator(':not([disabled])')).waitFor() await absYButton.and(page.locator(':not([disabled])')).waitFor()
@ -868,12 +817,10 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
// clear selection by clicking on nothing // clear selection by clicking on nothing
await emptySpaceClick() await emptySpaceClick()
await page.waitForTimeout(100)
// same selection but click the axis first // same selection but click the axis first
await xAxisClick() await xAxisClick()
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await page.waitForTimeout(100)
await topHorzSegmentClick() await topHorzSegmentClick()
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
@ -886,7 +833,6 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click() await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await page.waitForTimeout(100)
await xAxisClick() await xAxisClick()
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await expect(absYButton).not.toBeDisabled() await expect(absYButton).not.toBeDisabled()
@ -929,16 +875,11 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await page.waitForTimeout(100) await page.waitForTimeout(100)
// enter sketch again // enter sketch again
await u.doAndWaitForCmd( await page.getByRole('button', { name: 'Edit Sketch' }).click()
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
'default_camera_get_settings'
)
await page.waitForTimeout(150)
await page.waitForTimeout(300) // wait for animation await page.waitForTimeout(300) // wait for animation
// hover again and check it works // hover again and check it works
await selectionSequence(true) await selectionSequence()
}) })
test.describe('Command bar tests', () => { test.describe('Command bar tests', () => {
@ -1074,7 +1015,6 @@ const part001 = startSketchOn('-XZ')
}) })
test('Can add multiple sketches', async ({ page }) => { test('Can add multiple sketches', async ({ page }) => {
test.skip(process.platform === 'darwin', 'Can add multiple sketches')
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -1125,7 +1065,6 @@ test('Can add multiple sketches', async ({ page }) => {
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %) |> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`) |> line([0, ${commonPoints.num1}], %)`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ') const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|> startProfileAt(${commonPoints.startAt}, %) |> startProfileAt(${commonPoints.startAt}, %)
@ -1141,33 +1080,24 @@ test('Can add multiple sketches', async ({ page }) => {
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.updateCamPosition([100, 100, 100]) await u.updateCamPosition([0, 100, 100])
await page.waitForTimeout(250)
// start a new sketch // start a new sketch
await u.clearCommandLogs() await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(400) await page.waitForTimeout(100)
await page.mouse.click(650, 450) await page.mouse.click(673, 384)
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await u.clearAndCloseDebugPanel() await u.clearAndCloseDebugPanel()
// on mock os there are issues with getting the camera to update
// it should not be selecting the 'XZ' plane here if the camera updated
// properly, but if we just role with it we can still verify everything
// in the rest of the test
const plane = process.platform === 'darwin' ? 'XZ' : 'XY'
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt2 = const startAt2 = '[0.93,-1.25]'
process.platform === 'darwin' ? '[9.75, -13.16]' : '[0.93, -1.25]'
await expect( await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe( ).toBe(
`${finalCodeFirstSketch} `${finalCodeFirstSketch}
const part002 = startSketchOn('${plane}') const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %)`.replace(/\s/g, '') |> startProfileAt(${startAt2}, %)`.replace(/\s/g, '')
) )
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -1176,12 +1106,12 @@ const part002 = startSketchOn('${plane}')
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100) await page.waitForTimeout(100)
const num2 = process.platform === 'darwin' ? 9.84 : 0.94 const num2 = 0.94
await expect( await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe( ).toBe(
`${finalCodeFirstSketch} `${finalCodeFirstSketch}
const part002 = startSketchOn('${plane}') const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %) |> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)`.replace(/\s/g, '') |> line([${num2}, 0], %)`.replace(/\s/g, '')
) )
@ -1191,29 +1121,21 @@ const part002 = startSketchOn('${plane}')
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe( ).toBe(
`${finalCodeFirstSketch} `${finalCodeFirstSketch}
const part002 = startSketchOn('${plane}') const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %) |> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %) |> line([${num2}, 0], %)
|> line([0, ${roundOff( |> line([0, ${roundOff(num2 - 0.01)}], %)`.replace(/\s/g, '')
num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
)}], %)`.replace(/\s/g, '')
) )
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
await expect( await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '') (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe( ).toBe(
`${finalCodeFirstSketch} `${finalCodeFirstSketch}
const part002 = startSketchOn('${plane}') const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %) |> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %) |> line([${num2}, 0], %)
|> line([0, ${roundOff( |> line([0, ${roundOff(num2 - 0.01)}], %)
num2 + (process.platform === 'darwin' ? 0.01 : -0.01) |> line([-1.87, 0], %)`.replace(/\s/g, '')
)}], %)
|> line([-${process.platform === 'darwin' ? 19.59 : 1.87}, 0], %)`.replace(
/\s/g,
''
)
) )
}) })
@ -1417,12 +1339,10 @@ test('Deselecting line tool should mean nothing happens on click', async ({
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(700, 300) await page.mouse.click(700, 300)
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(750, 300) await page.mouse.click(750, 300)
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
@ -1447,16 +1367,16 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
const startPX = [665, 458] const startPX = [652, 418]
const lineEndPX = [842, 458] const lineEndPX = [794, 416]
const arcEndPX = [971, 342] const arcEndPX = [893, 318]
const dragPX = 30 const dragPX = 30
await page.getByText('startProfileAt([4.61, -14.01], %)').click() await page.getByText('startProfileAt([4.61, -14.01], %)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400) await page.waitForTimeout(100)
let prevContent = await page.locator('.cm-content').innerText() let prevContent = await page.locator('.cm-content').innerText()
const step5 = { steps: 5 } const step5 = { steps: 5 }
@ -1466,7 +1386,7 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
await page.mouse.down() await page.mouse.down()
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5) await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
await page.mouse.up() await page.mouse.up()
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText() prevContent = await page.locator('.cm-content').innerText()
@ -1494,9 +1414,9 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
// expect the code to have changed // expect the code to have changed
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ') .toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt([6.44, -12.07], %) |> startProfileAt([7.01, -11.79], %)
|> line([14.04, 2.03], %) |> line([14.69, 2.73], %)
|> tangentialArcTo([27.19, -4.2], %)`) |> tangentialArcTo([27.6, -3.25], %)`)
}) })
const doSnapAtDifferentScales = async ( const doSnapAtDifferentScales = async (
@ -1615,46 +1535,38 @@ test('Sketch on face', async ({ page }) => {
).not.toBeDisabled() ).not.toBeDisabled()
await page.getByRole('button', { name: 'Start Sketch' }).click() await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(300)
let previousCodeContent = await page.locator('.cm-content').innerText() let previousCodeContent = await page.locator('.cm-content').innerText()
await u.openAndClearDebugPanel() await page.mouse.click(793, 133)
await u.doAndWaitForCmd(
() => page.mouse.click(793, 133),
'default_camera_get_settings',
true
)
await page.waitForTimeout(150)
const firstClickPosition = [612, 238] const firstClickPosition = [612, 238]
const secondClickPosition = [661, 242] const secondClickPosition = [661, 242]
const thirdClickPosition = [609, 267] const thirdClickPosition = [609, 267]
await page.waitForTimeout(300)
await page.mouse.click(firstClickPosition[0], firstClickPosition[1]) await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(secondClickPosition[0], secondClickPosition[1]) await page.mouse.click(secondClickPosition[0], secondClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(thirdClickPosition[0], thirdClickPosition[1]) await page.mouse.click(thirdClickPosition[0], thirdClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
await page.waitForTimeout(100)
await page.mouse.click(firstClickPosition[0], firstClickPosition[1]) await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
.toContainText(`const part002 = startSketchOn(part001, 'seg01') .toContainText(`const part002 = startSketchOn(part001, 'seg01')
|> startProfileAt([-12.83, 6.7], %) |> startProfileAt([1.03, 1.03], %)
|> line([2.87, -0.23], %) |> line([4.18, -0.35], %)
|> line([-3.05, -1.47], %) |> line([-4.44, -2.13], %)
|> close(%)`) |> close(%)`)
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
@ -1664,14 +1576,9 @@ test('Sketch on face', async ({ page }) => {
await u.updateCamPosition([1049, 239, 686]) await u.updateCamPosition([1049, 239, 686])
await u.closeDebugPanel() await u.closeDebugPanel()
await page.getByText('startProfileAt([-12.83, 6.7], %)').click() await page.getByText('startProfileAt([1.03, 1.03], %)').click()
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible() await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
await u.doAndWaitForCmd( await page.getByRole('button', { name: 'Edit Sketch' }).click()
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
'default_camera_get_settings',
true
)
await page.waitForTimeout(150)
await page.setViewportSize({ width: 1200, height: 1200 }) await page.setViewportSize({ width: 1200, height: 1200 })
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await u.updateCamPosition([452, -152, 1166]) await u.updateCamPosition([452, -152, 1166])
@ -1689,20 +1596,21 @@ test('Sketch on face', async ({ page }) => {
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent) await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
previousCodeContent = await page.locator('.cm-content').innerText() previousCodeContent = await page.locator('.cm-content').innerText()
const result = makeTemplate`const part002 = startSketchOn(part001, 'seg01') await expect(page.locator('.cm-content'))
|> startProfileAt([-12.83, 6.7], %) .toContainText(`const part002 = startSketchOn(part001, 'seg01')
|> line([${[2.28, 2.35]}, -${0.07}], %) |> startProfileAt([1.03, 1.03], %)
|> line([-3.05, -1.47], %) |> line([${process?.env?.CI ? 2.74 : 2.93}, -${
|> close(%)` process?.env?.CI ? 0.24 : 0.2
}], %)
await expect(page.locator('.cm-content')).toHaveText(result.regExp) |> line([-4.44, -2.13], %)
|> close(%)`)
// exit sketch // exit sketch
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await page.getByText('startProfileAt([-12.83, 6.7], %)').click() await page.getByText('startProfileAt([1.03, 1.03], %)').click()
await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled() await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
await page.getByRole('button', { name: 'Extrude' }).click() await page.getByRole('button', { name: 'Extrude' }).click()
@ -1714,9 +1622,15 @@ test('Sketch on face', async ({ page }) => {
await expect(page.getByText('Confirm Extrude')).toBeVisible() await expect(page.getByText('Confirm Extrude')).toBeVisible()
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
const result2 = result.genNext` await expect(page.locator('.cm-content'))
|> extrude(${[5, 5]} + 7, %)` .toContainText(`const part002 = startSketchOn(part001, 'seg01')
await expect(page.locator('.cm-content')).toHaveText(result2.regExp) |> startProfileAt([1.03, 1.03], %)
|> line([${process?.env?.CI ? 2.74 : 2.93}, -${
process?.env?.CI ? 0.24 : 0.2
}], %)
|> line([-4.44, -2.13], %)
|> close(%)
|> extrude(5 + 7, %)`)
}) })
test('Can code mod a line length', async ({ page }) => { test('Can code mod a line length', async ({ page }) => {
@ -1747,11 +1661,11 @@ test('Can code mod a line length', async ({ page }) => {
// enter sketch again // enter sketch again
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(350) // wait for animation await page.waitForTimeout(300) // wait for animation
const startXPx = 500 const startXPx = 500
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10) await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
await page.mouse.click(615, 102) await page.mouse.click(615, 133)
await page.getByRole('button', { name: 'length', exact: true }).click() await page.getByRole('button', { name: 'length', exact: true }).click()
await page.getByText('Add constraining value').click() await page.getByText('Add constraining value').click()
@ -1759,42 +1673,3 @@ test('Can code mod a line length', async ({ page }) => {
`const length001 = 20const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> line([0, 20], %) |> xLine(-length001, %)` `const length001 = 20const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> line([0, 20], %) |> xLine(-length001, %)`
) )
}) })
test('Extrude from command bar selects extrude line after', async ({
page,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> xLine(-20, %)
|> close(%)
`
)
})
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// Click the line of code for xLine.
await page.getByText(`close(%)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Extrude' }).click()
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toHaveText(
` |> extrude(5 + 7, %)`
)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -1,13 +1,12 @@
import { SaveSettingsPayload } from 'lib/settings/settingsTypes' import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
export const TEST_SETTINGS_KEY = '/settings.toml' export const TEST_SETTINGS_KEY = '/user.toml'
export const TEST_SETTINGS = { export const TEST_SETTINGS = {
app: { app: {
theme: Themes.Dark, theme: Themes.Dark,
onboardingStatus: 'dismissed', onboardingStatus: 'dismissed',
projectDirectory: '', projectDirectory: '',
enableSSAO: false,
}, },
modeling: { modeling: {
defaultUnit: 'in', defaultUnit: 'in',
@ -22,14 +21,9 @@ export const TEST_SETTINGS = {
}, },
} satisfies Partial<SaveSettingsPayload> } satisfies Partial<SaveSettingsPayload>
export const TEST_SETTINGS_ONBOARDING_EXPORT = { export const TEST_SETTINGS_ONBOARDING = {
...TEST_SETTINGS, ...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export' }, app: { ...TEST_SETTINGS.app, onboardingStatus: '/export ' },
} satisfies Partial<SaveSettingsPayload>
export const TEST_SETTINGS_ONBOARDING_START = {
...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, onboardingStatus: '' },
} satisfies Partial<SaveSettingsPayload> } satisfies Partial<SaveSettingsPayload>
export const TEST_SETTINGS_CORRUPTED = { export const TEST_SETTINGS_CORRUPTED = {

View File

@ -182,76 +182,3 @@ export function getUtils(page: Page) {
}), }),
} }
} }
type TemplateOptions = Array<number | Array<number>>
type makeTemplateReturn = {
regExp: RegExp
genNext: (
templateParts: TemplateStringsArray,
...options: TemplateOptions
) => makeTemplateReturn
}
const escapeRegExp = (string: string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
}
const _makeTemplate = (
templateParts: TemplateStringsArray,
...options: TemplateOptions
) => {
const length = Math.max(...options.map((a) => (Array.isArray(a) ? a[0] : 0)))
let reExpTemplate = ''
for (let i = 0; i < length; i++) {
const currentStr = templateParts.map((str, index) => {
const currentOptions = options[index]
return (
escapeRegExp(str) +
String(
Array.isArray(currentOptions)
? currentOptions[i]
: typeof currentOptions === 'number'
? currentOptions
: ''
)
)
})
reExpTemplate += '|' + currentStr.join('')
}
return new RegExp(reExpTemplate)
}
/**
* Tool for making templates to match code snippets in the editor with some fudge factor,
* as there's some level of non-determinism.
*
* Usage is as such:
* ```typescript
* const result = makeTemplate`const myVar = aFunc(${[1, 2, 3]})`
* await expect(page.locator('.cm-content')).toHaveText(result.regExp)
* ```
* Where the value `1`, `2` or `3` are all valid and should make the test pass.
*
* The function also has a `genNext` function that allows you to chain multiple templates
* together without having to repeat previous parts of the template.
* ```typescript
* const result2 = result.genNext`const myVar2 = aFunc(${[4, 5, 6]})`
* ```
*/
export const makeTemplate: (
templateParts: TemplateStringsArray,
...values: TemplateOptions
) => makeTemplateReturn = (templateParts, ...options) => {
return {
regExp: _makeTemplate(templateParts, ...options),
genNext: (
nextTemplateParts: TemplateStringsArray,
...nextOptions: TemplateOptions
) =>
makeTemplate(
[...templateParts, ...nextTemplateParts] as any as TemplateStringsArray,
[...options, ...nextOptions] as any
),
}
}

View File

@ -2,7 +2,7 @@ import { browser, $, expect } from '@wdio/globals'
import fs from 'fs/promises' import fs from 'fs/promises'
const documentsDir = `${process.env.HOME}/Documents` const documentsDir = `${process.env.HOME}/Documents`
const userSettingsDir = `${process.env.HOME}/.config/dev.zoo.modeling-app` const userSettingsFile = `${process.env.HOME}/.config/dev.zoo.modeling-app/user.toml`
const defaultProjectDir = `${documentsDir}/zoo-modeling-app-projects` const defaultProjectDir = `${documentsDir}/zoo-modeling-app-projects`
const newProjectDir = `${documentsDir}/a-different-directory` const newProjectDir = `${documentsDir}/a-different-directory`
const userCodeDir = '/tmp/kittycad_user_code' const userCodeDir = '/tmp/kittycad_user_code'
@ -29,10 +29,8 @@ describe('ZMA (Tauri, Linux)', () => {
// Clean up filesystem from previous tests // Clean up filesystem from previous tests
await new Promise((resolve) => setTimeout(resolve, 100)) await new Promise((resolve) => setTimeout(resolve, 100))
await fs.rm(defaultProjectDir, { force: true, recursive: true }) await fs.rm(defaultProjectDir, { force: true, recursive: true })
await fs.rm(newProjectDir, { force: true, recursive: true })
await fs.rm(userCodeDir, { force: true }) await fs.rm(userCodeDir, { force: true })
await fs.rm(userSettingsDir, { force: true, recursive: true }) await fs.rm(userSettingsFile, { force: true })
await fs.mkdir(defaultProjectDir, { recursive: true })
await fs.mkdir(newProjectDir, { recursive: true }) await fs.mkdir(newProjectDir, { recursive: true })
const signInButton = await $('[data-testid="sign-in-button"]') const signInButton = await $('[data-testid="sign-in-button"]')
@ -72,9 +70,8 @@ describe('ZMA (Tauri, Linux)', () => {
console.log(cr.status) console.log(cr.status)
// Now should be signed in // Now should be signed in
await new Promise((resolve) => setTimeout(resolve, 10000))
const newFileButton = await $('[data-testid="home-new-file"]') const newFileButton = await $('[data-testid="home-new-file"]')
expect(await newFileButton.getText()).toEqual('New project') expect(await newFileButton.getText()).toEqual('New file')
}) })
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => { it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
@ -120,8 +117,8 @@ describe('ZMA (Tauri, Linux)', () => {
it('opens the new file and expects a loading stream', async () => { it('opens the new file and expects a loading stream', async () => {
const projectLink = await $('[data-testid="project-link"]') const projectLink = await $('[data-testid="project-link"]')
await click(projectLink) await click(projectLink)
const errorText = await $('[data-testid="unexpected-error"]') const loadingText = await $('[data-testid="loading-stream"]')
expect(await errorText.getText()).toContain('unexpected error') expect(await loadingText.getText()).toContain('Loading stream...')
await browser.execute('window.location.href = "tauri://localhost/home"') await browser.execute('window.location.href = "tauri://localhost/home"')
}) })

View File

@ -1,24 +0,0 @@
#!/bin/bash
# Set the repository owner and name
REPO_OWNER="KittyCAD"
REPO_NAME="modeling-app"
WORKFLOW_NAME="build-and-store-wasm.yml"
ARTIFACT_NAME="wasm-bundle"
# Fetch the latest completed workflow run ID for the specified workflow
# RUN_ID=$(gh api repos/$REPO_OWNER/$REPO_NAME/actions/workflows/$WORKFLOW_NAME/runs --paginate --jq '.workflow_runs[] | select(.status=="completed") | .id' | head -n 1)
RUN_ID=$(gh api repos/$REPO_OWNER/$REPO_NAME/actions/workflows/$WORKFLOW_NAME/runs --paginate --jq '.workflow_runs[] | select(.status=="completed" and .conclusion=="success") | .id' | head -n 1)
echo $RUN_ID
# Check if a valid RUN_ID was found
if [ -z "$RUN_ID" ]; then
echo "Failed to find a workflow run for $WORKFLOW_NAME."
exit 1
fi
gh run download $RUN_ID --repo $REPO_OWNER/$REPO_NAME --name $ARTIFACT_NAME --dir ./src/wasm-lib/pkg
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
echo "latest wasm copied to public folder"

View File

@ -57,7 +57,7 @@ echo "New version number without 'v': $new_version_number"
git checkout -b "cut-release-$new_version" git checkout -b "cut-release-$new_version"
echo "$(jq --arg v "$new_version_number" '.version=$v' package.json --indent 2)" > package.json echo "$(jq --arg v "$new_version_number" '.version=$v' package.json --indent 2)" > package.json
echo "$(jq --arg v "$new_version_number" '.version=$v' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json echo "$(jq --arg v "$new_version_number" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json
git add package.json src-tauri/tauri.conf.json git add package.json src-tauri/tauri.conf.json
git commit -m "Cut release $new_version" git commit -m "Cut release $new_version"

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.20.1", "version": "0.17.3",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.16.0", "@codemirror/autocomplete": "^6.16.0",
@ -8,9 +8,9 @@
"@fortawesome/free-brands-svg-icons": "^6.5.2", "@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19", "@headlessui/react": "^1.7.18",
"@headlessui/tailwindcss": "^0.2.0", "@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.60", "@kittycad/lib": "^0.0.58",
"@lezer/javascript": "^1.4.9", "@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1", "@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^1.2.6", "@react-hook/resize-observer": "^1.2.6",
@ -84,9 +84,8 @@
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts", "test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &", "simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000", "simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e", "fmt": "prettier --write ./src && prettier --write ./e2e",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e", "fmt-check": "prettier --check ./src && prettier --check ./e2e",
"fetch:wasm": "./get-latest-wasm-bundle.sh",
"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-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": "(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", "build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
@ -123,7 +122,6 @@
"@tauri-apps/cli": "^2.0.0-beta.13", "@tauri-apps/cli": "^2.0.0-beta.13",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/debounce-promise": "^3.1.9", "@types/debounce-promise": "^3.1.9",
"@types/mocha": "^10.0.6",
"@types/pixelmatch": "^5.2.6", "@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
"@types/react-modal": "^3.16.3", "@types/react-modal": "^3.16.3",
@ -134,7 +132,6 @@
"@types/wicg-file-system-access": "^2023.10.5", "@types/wicg-file-system-access": "^2023.10.5",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"@vitest/web-worker": "^1.5.0",
"@wdio/cli": "^8.24.3", "@wdio/cli": "^8.24.3",
"@wdio/globals": "^8.36.0", "@wdio/globals": "^8.36.0",
"@wdio/local-runner": "^8.36.0", "@wdio/local-runner": "^8.36.0",

View File

@ -27,7 +27,7 @@ export default defineConfig({
baseURL: 'http://localhost:3000', baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'retain-on-failure', trace: 'on-first-retry',
}, },
/* Configure projects for major browsers */ /* Configure projects for major browsers */
@ -49,6 +49,8 @@ export default defineConfig({
// use: { ...devices['Desktop Chrome'] }, // use: { ...devices['Desktop Chrome'] },
// }, // },
/* Test against mobile viewports. */ /* Test against mobile viewports. */
// { // {
// name: 'Mobile Chrome', // name: 'Mobile Chrome',
@ -76,4 +78,4 @@ export default defineConfig({
// url: 'http://127.0.0.1:3000', // url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },
}) })

View File

@ -1,15 +0,0 @@
{
"applinks": {
"details": [
{
"appIDs": ["92H8YB3B95.dev.zoo.modeling-app"],
"components": [
{
"/": "/file/*",
"comment": "Matches any URL whose path starts with /file/"
}
]
}
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 867 KiB

After

Width:  |  Height:  |  Size: 142 KiB

2732
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,44 +1,38 @@
[package] [package]
name = "app" name = "app"
version = "0.1.0" version = "0.1.0"
description = "The Zoo Modeling App" description = "A Tauri App"
authors = ["Zoo Engineers <eng@zoo.dev>"] authors = ["you"]
license = "" license = ""
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
default-run = "app" default-run = "app"
edition = "2021" edition = "2021"
rust-version = "1.70" rust-version = "1.70"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.0-beta.13", features = [] } tauri-build = { version = "2.0.0-beta.12", features = [] }
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
kcl-lib = { version = "0.1.53", path = "../src/wasm-lib/kcl" } kittycad = "0.2.67"
kittycad = "0.3.0"
log = "0.4.21"
oauth2 = "4.4.2" oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] } tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
tauri-plugin-cli = { version = "2.0.0-beta.3" } tauri-plugin-dialog = { version = "2.0.0-beta.5" }
tauri-plugin-deep-link = { version = "2.0.0-beta.3" } tauri-plugin-fs = { version = "2.0.0-beta.5" }
tauri-plugin-dialog = { version = "2.0.0-beta.6" } tauri-plugin-http = { version = "2.0.0-beta.5" }
tauri-plugin-fs = { version = "2.0.0-beta.6" }
tauri-plugin-http = { version = "2.0.0-beta.6" }
tauri-plugin-log = { version = "2.0.0-beta.4" }
tauri-plugin-os = { version = "2.0.0-beta.2" } tauri-plugin-os = { version = "2.0.0-beta.2" }
tauri-plugin-process = { version = "2.0.0-beta.2" } tauri-plugin-process = { version = "2.0.0-beta.2" }
tauri-plugin-shell = { version = "2.0.0-beta.2" } tauri-plugin-shell = { version = "2.0.0-beta.2" }
tauri-plugin-updater = { version = "2.0.0-beta.4" } tauri-plugin-updater = { version = "2.0.0-beta.4" }
tokio = { version = "1.37.0", features = ["time", "fs", "process"] } tokio = { version = "1.37.0", features = ["time"] }
toml = "0.8.2" toml = "0.8.2"
url = "2.5.0"
[features] [features]
default = ["updater"]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
# DO NOT REMOVE!! # DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
updater = []

View File

@ -1,376 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>NSDesktopFolderUsageDescription</key>
<string>Zoo Modeling App accesses the Desktop to load and save your project files and/or exported files here</string>
<key>NSDocumentsFolderUsageDescription</key>
<string>Zoo Modeling App accesses the Documents folder to load and save your project files and/or exported files here</string>
<key>NSDownloadsFolderUsageDescription</key>
<string>Zoo Modeling App accesses the Downloads folder to load and save your project files and/or exported files here</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>DTXcode</key>
<string>1501</string>
<key>DTXcodeBuild</key>
<string>15A507</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>dev.zoo.modeling-app</string>
<key>CFBundleURLSchemes</key>
<array>
<string>zoo-modeling-app</string>
<string>zoo</string>
</array>
</dict>
</array>
<key>LSFileQuarantineEnabled</key>
<false/>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.kcl</string>
</array>
<key>CFBundleTypeName</key>
<string>KCL</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Owner</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.toml</string>
</array>
<key>CFBundleTypeName</key>
<string>TOML</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.gltf</string>
</array>
<key>CFBundleTypeName</key>
<string>glTF</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.glb</string>
</array>
<key>CFBundleTypeName</key>
<string>glb</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.step</string>
</array>
<key>CFBundleTypeName</key>
<string>STEP</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.fbx</string>
</array>
<key>CFBundleTypeName</key>
<string>FBX</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>dev.zoo.sldprt</string>
</array>
<key>CFBundleTypeName</key>
<string>Solidworks Part</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.geometry-definition-format</string>
</array>
<key>CFBundleTypeName</key>
<string>OBJ</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.polygon-file-format</string>
</array>
<key>CFBundleTypeName</key>
<string>PLY</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.standard-tesselated-geometry-format</string>
</array>
<key>CFBundleTypeName</key>
<string>STL</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<false/>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.folder</string>
</array>
<key>CFBundleTypeName</key>
<string>Folders</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
</dict>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.kcl</string>
<key>UTTypeReferenceURL</key>
<string>https://zoo.dev/docs/kcl</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.source-code</string>
<string>public.data</string>
<string>public.text</string>
<string>public.plain-text</string>
<string>public.3d-content</string>
<string>public.script</string>
</array>
<key>UTTypeDescription</key>
<string>KCL (KittyCAD Language) document</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>kcl</string>
</array>
<key>public.mime-type</key>
<array>
<string>text/vnd.zoo.kcl</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.gltf</string>
<key>UTTypeReferenceURL</key>
<string>https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.text</string>
<string>public.plain-text</string>
<string>public.3d-content</string>
<string>public.json</string>
</array>
<key>UTTypeDescription</key>
<string>Graphics Library Transmission Format (glTF)</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>gltf</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/gltf+json</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.glb</string>
<key>UTTypeReferenceURL</key>
<string>https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
</array>
<key>UTTypeDescription</key>
<string>Graphics Library Transmission Format (glTF) binary</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>glb</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/gltf-binary</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.step</string>
<key>UTTypeReferenceURL</key>
<string>https://www.loc.gov/preservation/digital/formats/fdd/fdd000448.shtml</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
<string>public.text</string>
<string>public.plain-text</string>
</array>
<key>UTTypeDescription</key>
<string>STEP-file, ISO 10303-21</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>step</string>
<string>stp</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/step</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.sldprt</string>
<key>UTTypeReferenceURL</key>
<string>https://docs.fileformat.com/cad/sldprt/</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
</array>
<key>UTTypeDescription</key>
<string>Solidworks Part</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>sldprt</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/vnd.solidworks.sldprt</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.fbx</string>
<key>UTTypeReferenceURL</key>
<string>https://en.wikipedia.org/wiki/FBX</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.3d-content</string>
</array>
<key>UTTypeDescription</key>
<string>Autodesk Filmbox (FBX) format</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>fbx</string>
<string>fbxb</string>
</array>
<key>public.mime-type</key>
<array>
<string>model/vnd.autodesk.fbx</string>
</array>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>dev.zoo.toml</string>
<key>UTTypeReferenceURL</key>
<string>https://toml.io/en/</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.text</string>
<string>public.plain-text</string>
</array>
<key>UTTypeDescription</key>
<string>Tom's Obvious Minimal Language</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>kcl</string>
</array>
<key>public.mime-type</key>
<array>
<string>text/toml</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@ -7,9 +7,6 @@
"main" "main"
], ],
"permissions": [ "permissions": [
"cli:default",
"deep-link:default",
"log:default",
"path:default", "path:default",
"event:default", "event:default",
"window:default", "window:default",
@ -26,6 +23,7 @@
"fs:allow-copy-file", "fs:allow-copy-file",
"fs:allow-mkdir", "fs:allow-mkdir",
"fs:allow-remove", "fs:allow-remove",
"fs:allow-remove",
"fs:allow-rename", "fs:allow-rename",
"fs:allow-exists", "fs:allow-exists",
"fs:allow-stat", "fs:allow-stat",

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.application-identifier</key>
<string>92H8YB3B95.dev.zoo.modeling-app</string>
<key>com.apple.developer.team-identifier</key>
<string>92H8YB3B95</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:app.zoo.dev</string>
</array>
</dict>
</plist>

View File

@ -1,6 +0,0 @@
max_width = 120
edition = "2018"
format_code_in_doc_comments = true
format_strings = false
imports_granularity = "Crate"
group_imports = "StdExternalCrate"

View File

@ -1,232 +1,98 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!! // Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
pub(crate) mod state; use std::env;
use std::fs;
use std::{ use std::io::Read;
env, use std::path::Path;
path::{Path, PathBuf}, use std::path::PathBuf;
};
use anyhow::Result; use anyhow::Result;
use kcl_lib::settings::types::{
file::{FileEntry, Project, ProjectRoute, ProjectState},
project::ProjectConfiguration,
Configuration,
};
use oauth2::TokenResponse; use oauth2::TokenResponse;
use tauri::{ipc::InvokeError, Manager}; use serde::Serialize;
use tauri_plugin_cli::CliExt; use std::process::Command;
use tauri::ipc::InvokeError;
use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::ShellExt;
use tokio::process::Command; const DEFAULT_HOST: &str = "https://api.kittycad.io";
const DEFAULT_HOST: &str = "https://api.zoo.dev";
const SETTINGS_FILE_NAME: &str = "settings.toml";
const PROJECT_SETTINGS_FILE_NAME: &str = "project.toml";
const PROJECT_FOLDER: &str = "zoo-modeling-app-projects";
/// This command returns the a json string parse from a toml file at the path.
#[tauri::command] #[tauri::command]
fn get_initial_default_dir(app: tauri::AppHandle) -> Result<PathBuf, InvokeError> { fn read_toml(path: &str) -> Result<String, InvokeError> {
let dir = match app.path().document_dir() { let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(dir) => dir, let mut contents = String::new();
Err(_) => { file.read_to_string(&mut contents)
// for headless Linux (eg. Github Actions) .map_err(|e| InvokeError::from_anyhow(e.into()))?;
let home_dir = app.path().home_dir()?; let value =
home_dir.join("Documents") toml::from_str::<toml::Value>(&contents).map_err(|e| InvokeError::from_anyhow(e.into()))?;
} let value = serde_json::to_string(&value).map_err(|e| InvokeError::from_anyhow(e.into()))?;
}; Ok(value)
Ok(dir.join(PROJECT_FOLDER))
} }
#[tauri::command] /// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
async fn get_state(app: tauri::AppHandle) -> Result<Option<ProjectState>, InvokeError> { /// Removed from tauri v2
let store = app.state::<state::Store>(); #[derive(Debug, Serialize)]
Ok(store.get().await) pub struct DiskEntry {
/// The path to the entry.
pub path: PathBuf,
/// The name of the entry (file name with extension or directory name).
pub name: Option<String>,
/// The children of this entry if it's a directory.
#[serde(skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<DiskEntry>>,
} }
#[tauri::command] /// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
async fn set_state(app: tauri::AppHandle, state: Option<ProjectState>) -> Result<(), InvokeError> { /// Removed from tauri v2
let store = app.state::<state::Store>(); fn is_dir<P: AsRef<Path>>(path: P) -> Result<bool> {
store.set(state).await; std::fs::metadata(path)
Ok(()) .map(|md| md.is_dir())
} .map_err(Into::into)
async fn get_app_settings_file_path(app: &tauri::AppHandle) -> Result<PathBuf, InvokeError> {
let app_config_dir = app.path().app_config_dir()?;
// Ensure this directory exists.
if !app_config_dir.exists() {
tokio::fs::create_dir_all(&app_config_dir)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(app_config_dir.join(SETTINGS_FILE_NAME))
} }
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
/// Removed from tauri v2
#[tauri::command] #[tauri::command]
async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration, InvokeError> { fn read_dir_recursive(path: &str) -> Result<Vec<DiskEntry>, InvokeError> {
let mut settings_path = get_app_settings_file_path(&app).await?; let mut files_and_dirs: Vec<DiskEntry> = vec![];
let mut needs_migration = false; // let path = path.as_ref();
for entry in fs::read_dir(path).map_err(|e| InvokeError::from_anyhow(e.into()))? {
let path = entry
.map_err(|e| InvokeError::from_anyhow(e.into()))?
.path();
// Check if this file exists. if let Ok(flag) = is_dir(&path) {
if !settings_path.exists() { files_and_dirs.push(DiskEntry {
// Try the backwards compatible path. path: path.clone(),
// TODO: Remove this after a few releases. children: if flag {
let app_config_dir = app.path().app_config_dir()?; Some(read_dir_recursive(path.to_str().expect("No path"))?)
settings_path = format!( } else {
"{}user.toml", None
app_config_dir.display().to_string().trim_end_matches('/') },
) name: path
.into(); .file_name()
needs_migration = true; .map(|name| name.to_string_lossy())
// Check if this path exists. .map(|name| name.to_string()),
if !settings_path.exists() { });
let mut default = Configuration::default();
default.settings.project.directory = get_initial_default_dir(app.clone())?;
// Return the default configuration.
return Ok(default);
} }
} }
Ok(files_and_dirs)
}
let contents = tokio::fs::read_to_string(&settings_path) /// This command returns a string that is the contents of a file at the path.
.await #[tauri::command]
fn read_txt_file(path: &str) -> Result<String, InvokeError> {
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|e| InvokeError::from_anyhow(e.into()))?; .map_err(|e| InvokeError::from_anyhow(e.into()))?;
let mut parsed = Configuration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?; Ok(contents)
if parsed.settings.project.directory == PathBuf::new() {
parsed.settings.project.directory = get_initial_default_dir(app.clone())?;
}
// TODO: Remove this after a few releases.
if needs_migration {
write_app_settings_file(app, parsed.clone()).await?;
// Delete the old file.
tokio::fs::remove_file(settings_path)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(parsed)
}
#[tauri::command]
async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configuration) -> Result<(), InvokeError> {
let settings_path = get_app_settings_file_path(&app).await?;
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
tokio::fs::write(settings_path, contents.as_bytes())
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(())
}
async fn get_project_settings_file_path(
app_settings: Configuration,
project_name: &str,
) -> Result<PathBuf, InvokeError> {
let project_dir = app_settings.settings.project.directory.join(project_name);
if !project_dir.exists() {
tokio::fs::create_dir_all(&project_dir)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(project_dir.join(PROJECT_SETTINGS_FILE_NAME))
}
#[tauri::command]
async fn read_project_settings_file(
app_settings: Configuration,
project_name: &str,
) -> Result<ProjectConfiguration, InvokeError> {
let settings_path = get_project_settings_file_path(app_settings, project_name).await?;
// Check if this file exists.
if !settings_path.exists() {
// Return the default configuration.
return Ok(ProjectConfiguration::default());
}
let contents = tokio::fs::read_to_string(&settings_path)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
let parsed = ProjectConfiguration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?;
Ok(parsed)
}
#[tauri::command]
async fn write_project_settings_file(
app_settings: Configuration,
project_name: &str,
configuration: ProjectConfiguration,
) -> Result<(), InvokeError> {
let settings_path = get_project_settings_file_path(app_settings, project_name).await?;
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
tokio::fs::write(settings_path, contents.as_bytes())
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(())
}
/// Initialize the directory that holds all the projects.
#[tauri::command]
async fn initialize_project_directory(configuration: Configuration) -> Result<PathBuf, InvokeError> {
configuration
.ensure_project_directory_exists()
.await
.map_err(InvokeError::from_anyhow)
}
/// Create a new project directory.
#[tauri::command]
async fn create_new_project_directory(
configuration: Configuration,
project_name: &str,
initial_code: Option<&str>,
) -> Result<Project, InvokeError> {
configuration
.create_new_project_directory(project_name, initial_code)
.await
.map_err(InvokeError::from_anyhow)
}
/// List all the projects in the project directory.
#[tauri::command]
async fn list_projects(configuration: Configuration) -> Result<Vec<Project>, InvokeError> {
configuration.list_projects().await.map_err(InvokeError::from_anyhow)
}
/// Get information about a project.
#[tauri::command]
async fn get_project_info(configuration: Configuration, project_path: &str) -> Result<Project, InvokeError> {
configuration
.get_project_info(project_path)
.await
.map_err(InvokeError::from_anyhow)
}
/// Parse the project route.
#[tauri::command]
async fn parse_project_route(configuration: Configuration, route: &str) -> Result<ProjectRoute, InvokeError> {
ProjectRoute::from_route(&configuration, route).map_err(InvokeError::from_anyhow)
}
#[tauri::command]
async fn read_dir_recursive(path: &str) -> Result<FileEntry, InvokeError> {
kcl_lib::settings::utils::walk_dir(&Path::new(path).to_path_buf())
.await
.map_err(InvokeError::from_anyhow)
} }
/// This command instantiates a new window with auth. /// This command instantiates a new window with auth.
/// The string returned from this method is the access token. /// The string returned from this method is the access token.
#[tauri::command] #[tauri::command]
async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError> { async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError> {
log::debug!("Logging in..."); println!("Logging in...");
// Do an OAuth 2.0 Device Authorization Grant dance to get a token. // Do an OAuth 2.0 Device Authorization Grant dance to get a token.
let device_auth_url = oauth2::DeviceAuthorizationUrl::new(format!("{host}/oauth2/device/auth")) let device_auth_url = oauth2::DeviceAuthorizationUrl::new(format!("{host}/oauth2/device/auth"))
.map_err(|e| InvokeError::from_anyhow(e.into()))?; .map_err(|e| InvokeError::from_anyhow(e.into()))?;
@ -237,7 +103,8 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
let auth_client = oauth2::basic::BasicClient::new( let auth_client = oauth2::basic::BasicClient::new(
oauth2::ClientId::new(client_id), oauth2::ClientId::new(client_id),
None, None,
oauth2::AuthUrl::new(format!("{host}/authorize")).map_err(|e| InvokeError::from_anyhow(e.into()))?, oauth2::AuthUrl::new(format!("{host}/authorize"))
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
Some( Some(
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token")) oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
.map_err(|e| InvokeError::from_anyhow(e.into()))?, .map_err(|e| InvokeError::from_anyhow(e.into()))?,
@ -265,10 +132,12 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
// and bypass the shell::open call as it fails on GitHub Actions. // and bypass the shell::open call as it fails on GitHub Actions.
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok(); let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
if e2e_tauri_enabled { if e2e_tauri_enabled {
log::warn!("E2E_TAURI_ENABLED is set, won't open {} externally", auth_uri.secret()); println!(
tokio::fs::write("/tmp/kittycad_user_code", details.user_code().secret()) "E2E_TAURI_ENABLED is set, won't open {} externally",
.await auth_uri.secret()
.map_err(|e| InvokeError::from_anyhow(e.into()))?; );
fs::write("/tmp/kittycad_user_code", details.user_code().secret())
.expect("Unable to write /tmp/kittycad_user_code file");
} else { } else {
app.shell() app.shell()
.open(auth_uri.secret(), None) .open(auth_uri.secret(), None)
@ -291,7 +160,10 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
///This command returns the KittyCAD user info given a 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. /// The string returned from this method is the user info as a json string.
#[tauri::command] #[tauri::command]
async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::User, InvokeError> { async fn get_user(
token: Option<String>,
hostname: &str,
) -> Result<kittycad::types::User, InvokeError> {
// Use the host passed in if it's set. // Use the host passed in if it's set.
// Otherwise, use the default host. // Otherwise, use the default host.
let host = if hostname.is_empty() { let host = if hostname.is_empty() {
@ -308,10 +180,10 @@ async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::User,
baseurl = format!("http://{host}") baseurl = format!("http://{host}")
} }
} }
log::debug!("Getting user info..."); println!("Getting user info...");
// use kittycad library to fetch the user info from /user/me // use kittycad library to fetch the user info from /user/me
let mut client = kittycad::Client::new(token); let mut client = kittycad::Client::new(token.unwrap());
if baseurl != DEFAULT_HOST { if baseurl != DEFAULT_HOST {
client.set_base_url(&baseurl); client.set_base_url(&baseurl);
@ -330,186 +202,50 @@ async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::User,
/// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169 /// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169
/// But with the Linux support removed since we don't need it for now. /// But with the Linux support removed since we don't need it for now.
#[tauri::command] #[tauri::command]
fn show_in_folder(path: &str) -> Result<(), InvokeError> { fn show_in_folder(path: String) {
#[cfg(not(unix))] #[cfg(target_os = "windows")]
{ {
Command::new("explorer") Command::new("explorer")
.args(["/select,", path]) // The comma after select is not a typo .args(["/select,", &path]) // The comma after select is not a typo
.spawn() .spawn()
.map_err(|e| InvokeError::from_anyhow(e.into()))?; .unwrap();
} }
#[cfg(unix)] #[cfg(target_os = "macos")]
{ {
Command::new("open") Command::new("open").args(["-R", &path]).spawn().unwrap();
.args(["-R", path])
.spawn()
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(())
}
#[allow(dead_code)]
fn open_url_sync(app: &tauri::AppHandle, url: &url::Url) {
log::debug!("Opening URL: {:?}", url);
let cloned_url = url.clone();
let runner: tauri::async_runtime::JoinHandle<Result<ProjectState>> = tauri::async_runtime::spawn(async move {
let url_str = cloned_url.path().to_string();
log::debug!("Opening URL path : {}", url_str);
let path = Path::new(url_str.as_str());
ProjectState::new_from_path(path.to_path_buf()).await
});
// Block on the handle.
match tauri::async_runtime::block_on(runner) {
Ok(Ok(store)) => {
// Create a state object to hold the project.
app.manage(state::Store::new(store));
}
Err(e) => {
log::warn!("Error opening URL:{} {:?}", url, e);
}
Ok(Err(e)) => {
log::warn!("Error opening URL:{} {:?}", url, e);
}
} }
} }
fn main() -> Result<()> { fn main() {
tauri::Builder::default() tauri::Builder::default()
.setup(|_app| {
#[cfg(debug_assertions)]
{
use tauri::Manager;
_app.get_webview("main").unwrap().open_devtools();
}
#[cfg(not(debug_assertions))]
{
_app.handle()
.plugin(tauri_plugin_updater::Builder::new().build())?;
}
Ok(())
})
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
get_state,
set_state,
get_initial_default_dir,
initialize_project_directory,
create_new_project_directory,
list_projects,
get_project_info,
parse_project_route,
get_user, get_user,
login, login,
read_toml,
read_txt_file,
read_dir_recursive, read_dir_recursive,
show_in_folder, show_in_folder,
read_app_settings_file,
write_app_settings_file,
read_project_settings_file,
write_project_settings_file,
]) ])
.plugin(tauri_plugin_cli::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_http::init())
.plugin(
tauri_plugin_log::Builder::new()
.targets([
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None }),
])
.level(log::LevelFilter::Debug)
.build(),
)
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.setup(|app| { .run(tauri::generate_context!())
// Do update things. .expect("error while running tauri application");
#[cfg(debug_assertions)]
{
app.get_webview("main").unwrap().open_devtools();
}
#[cfg(not(debug_assertions))]
#[cfg(feature = "updater")]
{
app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
}
let mut verbose = false;
let mut source_path: Option<PathBuf> = None;
match app.cli().matches() {
// `matches` here is a Struct with { args, subcommand }.
// `args` is `HashMap<String, ArgData>` where `ArgData` is a struct with { value, occurrences }.
// `subcommand` is `Option<Box<SubcommandMatches>>` where `SubcommandMatches` is a struct with { name, matches }.
Ok(matches) => {
if let Some(verbose_flag) = matches.args.get("verbose") {
let Some(value) = verbose_flag.value.as_bool() else {
return Err(
anyhow::anyhow!("Error parsing CLI arguments: verbose flag is not a boolean").into(),
);
};
verbose = value;
}
// Get the path we are trying to open.
if let Some(source_arg) = matches.args.get("source") {
// We don't do an else here because this can be null.
if let Some(value) = source_arg.value.as_str() {
log::info!("Got path in cli argument: {}", value);
source_path = Some(Path::new(value).to_path_buf());
}
}
}
Err(err) => {
return Err(anyhow::anyhow!("Error parsing CLI arguments: {:?}", err).into());
}
}
if verbose {
log::debug!("Verbose mode enabled.");
}
// If we have a source path to open, make sure it exists.
let Some(source_path) = source_path else {
// The user didn't provide a source path to open.
// Run the app as normal.
app.manage(state::Store::default());
return Ok(());
};
if !source_path.exists() {
return Err(anyhow::anyhow!(
"Error: the path `{}` you are trying to open does not exist",
source_path.display()
)
.into());
}
let runner: tauri::async_runtime::JoinHandle<Result<ProjectState>> =
tauri::async_runtime::spawn(async move { ProjectState::new_from_path(source_path).await });
// Block on the handle.
let store = tauri::async_runtime::block_on(runner)??;
// Create a state object to hold the project.
app.manage(state::Store::new(store));
// Listen on the deep links.
app.listen("deep-link://new-url", |event| {
log::info!("got deep-link url: {:?}", event);
// TODO: open_url_sync(app.handle(), event.url);
});
Ok(())
})
.build(tauri::generate_context!())?
.run(
#[allow(unused_variables)]
|app, event| {
#[cfg(any(target_os = "macos", target_os = "ios"))]
if let tauri::RunEvent::Opened { urls } = event {
log::info!("Opened URLs: {:?}", urls);
// Handle the first URL.
// TODO: do we want to handle more than one URL?
// Under what conditions would we even have more than one?
if let Some(url) = urls.first() {
open_url_sync(app, url);
}
}
},
);
Ok(())
} }

View File

@ -1,21 +0,0 @@
//! State management for the application.
use kcl_lib::settings::types::file::ProjectState;
use tokio::sync::Mutex;
#[derive(Debug, Default)]
pub struct Store(Mutex<Option<ProjectState>>);
impl Store {
pub fn new(p: ProjectState) -> Self {
Self(Mutex::new(Some(p)))
}
pub async fn get(&self) -> Option<ProjectState> {
self.0.lock().await.clone()
}
pub async fn set(&self, p: Option<ProjectState>) {
*self.0.lock().await = p;
}
}

View File

@ -1,8 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"bundle": {
"macOS": {
"entitlements": "entitlements/app-store.entitlements"
}
}
}

View File

@ -37,42 +37,23 @@
} }
}, },
"longDescription": "", "longDescription": "",
"macOS": {}, "macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [], "resources": [],
"shortDescription": "", "shortDescription": "",
"targets": "all" "targets": "all"
}, },
"identifier": "dev.zoo.modeling-app", "identifier": "dev.zoo.modeling-app",
"plugins": { "plugins": {
"cli": {
"description": "Zoo Modeling App CLI",
"args": [
{
"short": "v",
"name": "verbose",
"description": "Verbosity level"
},
{
"name": "source",
"description": "The file or directory to open",
"required": false,
"index": 1,
"takesValue": true
}
],
"subcommands": {}
},
"deep-link": {
"domains": [
{
"host": "app.zoo.dev"
}
]
},
"shell": { "shell": {
"open": true "open": true
} }
}, },
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"version": "0.20.1" "version": "0.17.3"
} }

View File

@ -5,45 +5,7 @@
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D", "certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
"digestAlgorithm": "sha256", "digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com" "timestampUrl": "http://timestamp.digicert.com"
}, }
"fileAssociations": [
{
"ext": ["kcl"],
"mimeType": "text/vnd.zoo.kcl"
},
{
"ext": ["obj"],
"mimeType": "model/obj"
},
{
"ext": ["gltf"],
"mimeType": "model/gltf+json"
},
{
"ext": ["glb"],
"mimeType": "model/gltf+binary"
},
{
"ext": ["fbx", "fbxb"],
"mimeType": "model/fbx"
},
{
"ext": ["stl"],
"mimeType": "model/stl"
},
{
"ext": ["ply"],
"mimeType": "model/ply"
},
{
"ext": ["step", "stp"],
"mimeType": "model/step"
},
{
"ext": ["sldprt"],
"mimeType": "model/sldprt"
}
]
}, },
"plugins": { "plugins": {
"updater": { "updater": {

View File

@ -30,7 +30,6 @@ import SettingsAuthProvider from 'components/SettingsAuthProvider'
import LspProvider from 'components/LspProvider' import LspProvider from 'components/LspProvider'
import { KclContextProvider } from 'lang/KclProvider' import { KclContextProvider } from 'lang/KclProvider'
import { BROWSER_PROJECT_NAME } from 'lib/constants' import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { getState, setState } from 'lib/tauri'
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@ -53,29 +52,10 @@ const router = createBrowserRouter([
children: [ children: [
{ {
path: paths.INDEX, path: paths.INDEX,
loader: async () => { loader: () =>
const inTauri = isTauri() isTauri()
if (inTauri) {
const appState = await getState()
if (appState) {
// Reset the state.
// We do this so that we load the initial state from the cli but everything
// else we can ignore.
await setState(undefined)
// Redirect to the file if we have a file path.
if (appState.current_file) {
return redirect(
paths.FILE + '/' + encodeURIComponent(appState.current_file)
)
}
}
}
return inTauri
? redirect(paths.HOME) ? redirect(paths.HOME)
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME) : redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME),
},
}, },
{ {
loader: fileLoader, loader: fileLoader,

View File

@ -246,31 +246,13 @@ export class CameraControls {
camSettings.center.y, camSettings.center.y,
camSettings.center.z camSettings.center.z
) )
this.camera.up.set(camSettings.up.x, camSettings.up.y, camSettings.up.z)
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {
this.useOrthographicCamera()
}
if (this.camera instanceof OrthographicCamera && !camSettings.ortho) {
this.usePerspectiveCamera()
}
if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) { if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
this.camera.fov = camSettings.fov_y this.camera.fov = camSettings.fov_y
} else if ( } else if (
this.camera instanceof OrthographicCamera && this.camera instanceof OrthographicCamera &&
camSettings.ortho_scale camSettings.ortho_scale
) { ) {
const distanceToTarget = new Vector3( this.camera.zoom = camSettings.ortho_scale
camSettings.pos.x,
camSettings.pos.y,
camSettings.pos.z
).distanceTo(
new Vector3(
camSettings.center.x,
camSettings.center.y,
camSettings.center.z
)
)
this.camera.zoom = (camSettings.ortho_scale * 40) / distanceToTarget
} }
this.onCameraChange() this.onCameraChange()
} }
@ -983,10 +965,10 @@ export class CameraControls {
// Pure function helpers // Pure function helpers
function calculateNearFarFromFOV(fov: number) { function calculateNearFarFromFOV(fov: number) {
// const nearFarRatio = (fov - 3) / (45 - 3) const nearFarRatio = (fov - 3) / (45 - 3)
// const z_near = 0.1 + nearFarRatio * (5 - 0.1) // const z_near = 0.1 + nearFarRatio * (5 - 0.1)
// const z_far = 1000 + nearFarRatio * (100000 - 1000) const z_far = 1000 + nearFarRatio * (100000 - 1000)
return { z_near: 0.1, z_far: 1000 } return { z_near: 0.1, z_far }
} }
function convertThreeCamValuesToEngineCam({ function convertThreeCamValuesToEngineCam({
@ -1061,62 +1043,3 @@ function _getInteractionType(
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom' if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom'
return state return state
} }
/**
* Tells the engine to fire it's animation waits for it to finish and then requests camera settings
* to ensure the client-side camera is synchronized with the engine's camera state.
*
* @param engineCommandManager Our websocket singleton
* @param entityId - The ID of face or sketchPlane.
*/
export async function letEngineAnimateAndSyncCamAfter(
engineCommandManager: EngineCommandManager,
entityId: string
) {
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'enable_sketch_mode',
adjust_camera: true,
animated: !isReducedMotion(),
ortho: false,
entity_id: entityId,
},
})
// wait 600ms (animation takes 500, + 100 for safety)
await new Promise((resolve) =>
setTimeout(resolve, isReducedMotion() ? 100 : 600)
)
await engineCommandManager.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'enable_sketch_mode',
adjust_camera: true,
animated: false,
ortho: true,
entity_id: entityId,
},
})
await new Promise((resolve) => setTimeout(resolve, 50))
await engineCommandManager.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
}

View File

@ -3,6 +3,7 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { cameraMouseDragGuards } from 'lib/cameraControls' import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useStore } from 'useStore'
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra' import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
import { ReactCameraProperties } from './CameraControls' import { ReactCameraProperties } from './CameraControls'
import { throttle } from 'lib/utils' import { throttle } from 'lib/utils'
@ -46,6 +47,10 @@ export const ClientSideScene = ({
const canvasRef = useRef<HTMLDivElement>(null) const canvasRef = useRef<HTMLDivElement>(null)
const { state, send, context } = useModelingContext() const { state, send, context } = useModelingContext()
const { hideClient, hideServer } = useShouldHideScene() const { hideClient, hideServer } = useShouldHideScene()
const { setHighlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
highlightRange: s.highlightRange,
}))
// Listen for changes to the camera controls setting // Listen for changes to the camera controls setting
// and update the client-side scene's controls accordingly. // and update the client-side scene's controls accordingly.
@ -64,6 +69,7 @@ export const ClientSideScene = ({
const canvas = canvasRef.current const canvas = canvasRef.current
canvas.appendChild(sceneInfra.renderer.domElement) canvas.appendChild(sceneInfra.renderer.domElement)
sceneInfra.animate() sceneInfra.animate()
sceneInfra.setHighlightCallback(setHighlightRange)
canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false) canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false)
canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false) canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false)
canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false) canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false)

View File

@ -57,7 +57,6 @@ import {
kclManager, kclManager,
sceneInfra, sceneInfra,
codeManager, codeManager,
editorManager,
} from 'lib/singletons' } from 'lib/singletons'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { executeAst, useStore } from 'useStore' import { executeAst, useStore } from 'useStore'
@ -215,9 +214,8 @@ export class SceneEntities {
const orthoFactor = orthoScale(sceneInfra.camControls.camera) const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const baseXColor = 0x000055 const baseXColor = 0x000055
const baseYColor = 0x550000 const baseYColor = 0x550000
const axisPixelWidth = 1.6 const xAxisGeometry = new BoxGeometry(100000, 0.3, 0.01)
const xAxisGeometry = new BoxGeometry(100000, axisPixelWidth, 0.01) const yAxisGeometry = new BoxGeometry(0.3, 100000, 0.01)
const yAxisGeometry = new BoxGeometry(axisPixelWidth, 100000, 0.01)
const xAxisMaterial = new MeshBasicMaterial({ const xAxisMaterial = new MeshBasicMaterial({
color: baseXColor, color: baseXColor,
depthTest: false, depthTest: false,
@ -1325,31 +1323,30 @@ export class SceneEntities {
selected.material.color = defaultPlaneColor(type) selected.material.color = defaultPlaneColor(type)
}, },
onClick: async (args) => { onClick: async (args) => {
const checkExtrudeFaceClick = async (): Promise< const checkExtrudeFaceClick = async (): Promise<boolean> => {
['face' | 'plane' | 'other', string]
> => {
const { streamDimensions } = useStore.getState() const { streamDimensions } = useStore.getState()
const { entity_id } = await sendSelectEventToEngine( const { entity_id } = await sendSelectEventToEngine(
args?.mouseEvent, args?.mouseEvent,
document.getElementById('video-stream') as HTMLVideoElement, document.getElementById('video-stream') as HTMLVideoElement,
streamDimensions streamDimensions
) )
if (!entity_id) return ['other', ''] if (!entity_id) return false
if (
engineCommandManager.defaultPlanes?.xy === entity_id ||
engineCommandManager.defaultPlanes?.xz === entity_id ||
engineCommandManager.defaultPlanes?.yz === entity_id
) {
return ['plane', entity_id]
}
const artifact = this.engineCommandManager.artifactMap[entity_id] const artifact = this.engineCommandManager.artifactMap[entity_id]
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info') if (artifact?.commandType !== 'solid3d_get_extrusion_face_info')
return ['other', entity_id] return false
const faceInfo: Models['FaceIsPlanar_type'] = (
const faceInfo = await getFaceDetails(entity_id) await this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'face_is_planar',
object_id: entity_id,
},
})
)?.data?.data
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis) if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return ['other', entity_id] return false
const { z_axis, y_axis, origin } = faceInfo const { z_axis, origin, y_axis } = faceInfo
const pathToNode = getNodePathFromSourceRange( const pathToNode = getNodePathFromSourceRange(
kclManager.ast, kclManager.ast,
artifact.range artifact.range
@ -1369,15 +1366,12 @@ export class SceneEntities {
artifact?.additionalData?.type === 'cap' artifact?.additionalData?.type === 'cap'
? artifact.additionalData.info ? artifact.additionalData.info
: 'none', : 'none',
faceId: entity_id,
}, },
}) })
return ['face', entity_id] return true
} }
const faceResult = await checkExtrudeFaceClick() if (await checkExtrudeFaceClick()) return
console.log('faceResult', faceResult)
if (faceResult[0] === 'face') return
if (!args || !args.intersects?.[0]) return if (!args || !args.intersects?.[0]) return
if (args.mouseEvent.which !== 1) return if (args.mouseEvent.which !== 1) return
@ -1403,7 +1397,6 @@ export class SceneEntities {
plane: planeString, plane: planeString,
zAxis, zAxis,
yAxis, yAxis,
planeId: faceResult[1],
}, },
}) })
}, },
@ -1430,7 +1423,7 @@ export class SceneEntities {
parent.userData.pathToNode, parent.userData.pathToNode,
'CallExpression' 'CallExpression'
).node ).node
editorManager.setHighlightRange([node.start, node.end]) sceneInfra.highlightCallback([node.start, node.end])
const yellow = 0xffff00 const yellow = 0xffff00
colorSegment(selected, yellow) colorSegment(selected, yellow)
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE) const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
@ -1466,10 +1459,10 @@ export class SceneEntities {
} }
return return
} }
editorManager.setHighlightRange([0, 0]) sceneInfra.highlightCallback([0, 0])
}, },
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => { onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
editorManager.setHighlightRange([0, 0]) sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(selected, [ const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT, STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT,
@ -1687,7 +1680,7 @@ export async function getSketchOrientationDetails(
sketchPathToNode: PathToNode sketchPathToNode: PathToNode
): Promise<{ ): Promise<{
quat: Quaternion quat: Quaternion
sketchDetails: SketchDetails & { faceId?: string } sketchDetails: SketchDetails
}> { }> {
const sketchGroup = sketchGroupFromPathToNode({ const sketchGroup = sketchGroupFromPathToNode({
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
@ -1703,13 +1696,20 @@ export async function getSketchOrientationDetails(
zAxis: [zAxis.x, zAxis.y, zAxis.z], zAxis: [zAxis.x, zAxis.y, zAxis.z],
yAxis: [sketchGroup.yAxis.x, sketchGroup.yAxis.y, sketchGroup.yAxis.z], yAxis: [sketchGroup.yAxis.x, sketchGroup.yAxis.y, sketchGroup.yAxis.z],
origin: [0, 0, 0], origin: [0, 0, 0],
faceId: sketchGroup.on.id,
}, },
} }
} }
if (sketchGroup.on.type === 'face') { if (sketchGroup.on.type === 'face') {
const faceInfo = await getFaceDetails(sketchGroup.on.faceId) const faceInfo: Models['FaceIsPlanar_type'] = (
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'face_is_planar',
object_id: sketchGroup.on.faceId,
},
})
)?.data?.data
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis) if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
throw new Error('faceInfo') throw new Error('faceInfo')
const { z_axis, y_axis, origin } = faceInfo const { z_axis, y_axis, origin } = faceInfo
@ -1724,7 +1724,6 @@ export async function getSketchOrientationDetails(
zAxis: [z_axis.x, z_axis.y, z_axis.z], zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z], yAxis: [y_axis.x, y_axis.y, y_axis.z],
origin: [origin.x, origin.y, origin.z], origin: [origin.x, origin.y, origin.z],
faceId: sketchGroup.on.faceId,
}, },
} }
} }
@ -1733,46 +1732,6 @@ export async function getSketchOrientationDetails(
) )
} }
/**
* Retrieves orientation details for a given entity representing a face (brep face or default plane).
* This function asynchronously fetches and returns the origin, x-axis, y-axis, and z-axis details
* for a specified entity ID. It is primarily used to obtain the orientation of a face in the scene,
* which is essential for calculating the correct positioning and alignment of the client side sketch.
*
* @param entityId - The ID of the entity for which orientation details are being fetched.
* @returns A promise that resolves with the orientation details of the face.
*/
async function getFaceDetails(
entityId: string
): Promise<Models['FaceIsPlanar_type']> {
// TODO mode engine connection to allow batching returns and batch the following
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'enable_sketch_mode',
adjust_camera: false,
animated: false,
ortho: false,
entity_id: entityId,
},
})
// TODO change typing to get_sketch_mode_plane once lib is updated
const faceInfo: Models['FaceIsPlanar_type'] = (
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'get_sketch_mode_plane' },
})
)?.data?.data
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
return faceInfo
}
export function getQuaternionFromZAxis(zAxis: Vector3): Quaternion { export function getQuaternionFromZAxis(zAxis: Vector3): Quaternion {
const dummyCam = new PerspectiveCamera() const dummyCam = new PerspectiveCamera()
dummyCam.up.set(0, 0, 1) dummyCam.up.set(0, 0, 1)

View File

@ -24,6 +24,7 @@ import {
import { compareVec2Epsilon2 } from 'lang/std/sketch' import { compareVec2Epsilon2 } from 'lang/std/sketch'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import * as TWEEN from '@tweenjs/tween.js' import * as TWEEN from '@tweenjs/tween.js'
import { SourceRange } from 'lang/wasm'
import { Axis } from 'lib/selections' import { Axis } from 'lib/selections'
import { type BaseUnit } from 'lib/settings/settingsTypes' import { type BaseUnit } from 'lib/settings/settingsTypes'
import { CameraControls } from './CameraControls' import { CameraControls } from './CameraControls'
@ -148,6 +149,10 @@ export class SceneInfra {
onMouseLeave: () => {}, onMouseLeave: () => {},
}) })
} }
highlightCallback: (a: SourceRange) => void = () => {}
setHighlightCallback(cb: (a: SourceRange) => void) {
this.highlightCallback = cb
}
modelingSend: SendType = (() => {}) as any modelingSend: SendType = (() => {}) as any
setSend(send: SendType) { setSend(send: SendType) {

View File

@ -1,9 +1,11 @@
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { editorManager, kclManager } from 'lib/singletons' import { kclManager } from 'lib/singletons'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useStore } from 'useStore'
export function AstExplorer() { export function AstExplorer() {
const setHighlightRange = useStore((s) => s.setHighlightRange)
const { context } = useModelingContext() const { context } = useModelingContext()
const pathToNode = getNodePathFromSourceRange( const pathToNode = getNodePathFromSourceRange(
// TODO maybe need to have callback to make sure it stays in sync // TODO maybe need to have callback to make sure it stays in sync
@ -40,7 +42,7 @@ export function AstExplorer() {
<div <div
className="h-full relative" className="h-full relative"
onMouseLeave={(e) => { onMouseLeave={(e) => {
editorManager.setHighlightRange([0, 0]) setHighlightRange([0, 0])
}} }}
> >
<pre className="text-xs"> <pre className="text-xs">
@ -86,6 +88,7 @@ function DisplayObj({
filterKeys: string[] filterKeys: string[]
node: any node: any
}) { }) {
const setHighlightRange = useStore((s) => s.setHighlightRange)
const { send } = useModelingContext() const { send } = useModelingContext()
const ref = useRef<HTMLPreElement>(null) const ref = useRef<HTMLPreElement>(null)
const [hasCursor, setHasCursor] = useState(false) const [hasCursor, setHasCursor] = useState(false)
@ -109,12 +112,12 @@ function DisplayObj({
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : '' hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
}`} }`}
onMouseEnter={(e) => { onMouseEnter={(e) => {
editorManager.setHighlightRange([obj?.start || 0, obj.end]) setHighlightRange([obj?.start || 0, obj.end])
e.stopPropagation() e.stopPropagation()
}} }}
onMouseMove={(e) => { onMouseMove={(e) => {
e.stopPropagation() e.stopPropagation()
editorManager.setHighlightRange([obj?.start || 0, obj.end]) setHighlightRange([obj?.start || 0, obj.end])
}} }}
onClick={(e) => { onClick={(e) => {
send({ send({

View File

@ -137,33 +137,34 @@ export function useCalc({
setAvailableVarInfo(varInfo) setAvailableVarInfo(varInfo)
}, [kclManager.ast, kclManager.programMemory, selectionRange]) }, [kclManager.ast, kclManager.programMemory, selectionRange])
useEffect(() => { useEffect(async () => {
try { try {
const code = `const __result__ = ${value}` const code = `const __result__ = ${value}`
const ast = parse(code) parse(code).then((ast) => {
const _programMem: any = { root: {}, return: null } const _programMem: any = { root: {}, return: null }
availableVarInfo.variables.forEach(({ key, value }) => { availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] } _programMem.root[key] = { type: 'userVal', value, __meta: [] }
}) })
executeAst({ executeAst({
ast, ast,
engineCommandManager, engineCommandManager,
useFakeExecutor: true, useFakeExecutor: true,
programMemoryOverride: JSON.parse( programMemoryOverride: JSON.parse(
JSON.stringify(kclManager.programMemory) JSON.stringify(kclManager.programMemory)
), ),
}).then(({ programMemory }) => { }).then(({ programMemory }) => {
const resultDeclaration = ast.body.find( const resultDeclaration = ast.body.find(
(a) => (a) =>
a.type === 'VariableDeclaration' && a.type === 'VariableDeclaration' &&
a.declarations?.[0]?.id?.name === '__result__' a.declarations?.[0]?.id?.name === '__result__'
) )
const init = const init =
resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.root?.__result__?.value const result = programMemory?.root?.__result__?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN') setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init) init && setValueNode(init)
})
}) })
} catch (e) { } catch (e) {
setCalcResult('NAN') setCalcResult('NAN')

View File

@ -1,7 +1,6 @@
import { useMachine } from '@xstate/react' import { useMachine } from '@xstate/react'
import { editorManager } from 'lib/singletons'
import { commandBarMachine } from 'machines/commandBarMachine' import { commandBarMachine } from 'machines/commandBarMachine'
import { createContext, useEffect } from 'react' import { createContext } from 'react'
import { EventFrom, StateFrom } from 'xstate' import { EventFrom, StateFrom } from 'xstate'
type CommandsContextType = { type CommandsContextType = {
@ -31,10 +30,6 @@ export const CommandBarProvider = ({
}, },
}) })
useEffect(() => {
editorManager.setCommandBarSend(commandBarSend)
})
return ( return (
<CommandsContext.Provider <CommandsContext.Provider
value={{ value={{

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