Compare commits
101 Commits
parser-in-
...
v0.21.0
Author | SHA1 | Date | |
---|---|---|---|
da323e22d4 | |||
8dc3628e9b | |||
253744867b | |||
c45eb1e3e3 | |||
758aac9328 | |||
309943cf2c | |||
b3d4ab91fc | |||
5e73fa45f0 | |||
17d23a17db | |||
0460f8eaee | |||
2077cdb6fc | |||
cb0b7e8169 | |||
3a05211d30 | |||
d12d103cba | |||
04f6d3dcc8 | |||
9c9ffa0d03 | |||
c62b9f1f04 | |||
fcac3c72e4 | |||
1e2f577a9f | |||
1814f340fb | |||
43928f88aa | |||
6959036688 | |||
570d0473c6 | |||
44f0d7c25c | |||
3ccb04c4e7 | |||
00058f699a | |||
5a478fe0b3 | |||
723cf4f746 | |||
3950de0a4d | |||
901d474986 | |||
e7ab645267 | |||
cf830f9895 | |||
2c1f53f0f0 | |||
d39e2502d0 | |||
51fed9c541 | |||
b3a09abe01 | |||
cd3a2fea07 | |||
c29c4a8567 | |||
39ccd94884 | |||
d99ab22b56 | |||
20a8f2aa6a | |||
93266a9819 | |||
a9c7a7cb13 | |||
8dd9b8d192 | |||
23181d8144 | |||
834967df6a | |||
deacaac33a | |||
c55603853b | |||
93f652647e | |||
67cea620a6 | |||
ed0c7d038d | |||
d3aa789761 | |||
cd68f80b71 | |||
d341681c0d | |||
0578e9d2a1 | |||
b413538e9e | |||
c4e7754fc5 | |||
94515b5490 | |||
aa52407fda | |||
e45be831d0 | |||
005944f3a3 | |||
755ef8ce7f | |||
005d1f0ca7 | |||
e158f6f513 | |||
879d7ec4f4 | |||
f6838b9b14 | |||
cb75c47631 | |||
9b95ec1083 | |||
a3eeff65c8 | |||
fab3d2b130 | |||
0a96dc6fd2 | |||
e123a00d4b | |||
b950cc0583 | |||
c89780a489 | |||
1afed68dd7 | |||
dcbed4f06f | |||
379f154a5c | |||
60c4969322 | |||
cc6dee8ad4 | |||
2fc7c0d5fd | |||
bf2dcd808f | |||
ee21e486d4 | |||
b5a3eb9e9c | |||
c85645c9f2 | |||
cfa4dd2e33 | |||
c620f7269c | |||
2d8d29b345 | |||
00da062586 | |||
aafbaf6c50 | |||
2894c84a4e | |||
c01084feb0 | |||
c461db5f54 | |||
03fcb73aca | |||
8065e7e51a | |||
2d0ac249df | |||
3d73b82c23 | |||
0b235dc1cd | |||
0857415793 | |||
1da4fd03ef | |||
39d84c12ab | |||
537d86c8ff |
@ -3,3 +3,4 @@ VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
|
||||
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
|
||||
VITE_KC_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||
VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local"
|
||||
|
33
.github/workflows/build-and-store-wasm.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
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
|
17
.github/workflows/cargo-clippy.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
dir: ['src/wasm-lib']
|
||||
dir: ['src/wasm-lib', 'src-tauri']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install latest rust
|
||||
@ -31,9 +31,22 @@ jobs:
|
||||
|
||||
- name: install dependencies
|
||||
if: matrix.dir == 'src-tauri'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
sudo apt-get install -y \
|
||||
libgtk-3-dev \
|
||||
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
|
||||
uses: Swatinem/rust-cache@v2.6.1
|
||||
|
||||
|
57
.github/workflows/cargo-test-tauri.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
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
|
135
.github/workflows/ci.yml
vendored
@ -13,7 +13,7 @@ on:
|
||||
# Will checkout the last commit from the default branch (main as of 2023-10-04)
|
||||
|
||||
env:
|
||||
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && contains(github.event.pull_request.title, 'Cut release v') }}
|
||||
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
@ -50,7 +50,7 @@ jobs:
|
||||
- run: yarn tsc
|
||||
|
||||
|
||||
check-typos:
|
||||
check-typos:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@ -98,7 +98,7 @@ jobs:
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
|
||||
- name: Set nightly version
|
||||
if: github.event_name == 'schedule'
|
||||
run: |
|
||||
@ -130,7 +130,9 @@ jobs:
|
||||
matrix:
|
||||
os: [macos-14, ubuntu-latest, windows-latest]
|
||||
env:
|
||||
# Specific Apple Universal target for macos
|
||||
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' || '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -143,21 +145,21 @@ jobs:
|
||||
ls -l artifact
|
||||
cp artifact/package.json package.json
|
||||
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
|
||||
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
|
||||
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
|
||||
|
||||
- name: Install ubuntu system dependencies
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: >
|
||||
sudo apt-get update &&
|
||||
sudo apt-get install -y
|
||||
libgtk-3-dev
|
||||
libayatana-appindicator3-dev
|
||||
webkit2gtk-driver
|
||||
libsoup-3.0-dev
|
||||
libjavascriptcoregtk-4.1-dev
|
||||
libwebkit2gtk-4.1-dev
|
||||
at-spi2-core
|
||||
xvfb
|
||||
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: Sync node version and setup cache
|
||||
uses: actions/setup-node@v4
|
||||
@ -237,6 +239,96 @@ jobs:
|
||||
includeDebug: true
|
||||
args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
|
||||
|
||||
- name: Build for Mac TestFlight (nightly)
|
||||
if: ${{ github.event_name == 'schedule' && 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 to Mac TestFlight (nightly)'
|
||||
uses: apple-actions/upload-testflight-build@v1
|
||||
if: ${{ github.event_name == 'schedule' && 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 TestFlight (nightly)
|
||||
if: ${{ github.event_name == 'schedule' && 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
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||
@ -261,11 +353,10 @@ jobs:
|
||||
with:
|
||||
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
|
||||
|
||||
# TODO: re-enable linux e2e tests when possible
|
||||
- name: Run e2e tests (linux only)
|
||||
if: false
|
||||
if: ${{ matrix.os == 'ubuntu-latest' && github.event_name != 'release' && github.event_name != 'schedule' }}
|
||||
run: |
|
||||
cargo install tauri-driver
|
||||
cargo install tauri-driver --force
|
||||
source .env.${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
|
||||
export VITE_KC_API_BASE_URL
|
||||
xvfb-run yarn test:e2e:tauri
|
||||
@ -383,7 +474,7 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: 'artifact/*/Zoo*'
|
||||
|
||||
|
||||
announce_release:
|
||||
needs: [publish-apps-release]
|
||||
runs-on: ubuntu-latest
|
||||
@ -391,17 +482,17 @@ jobs:
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests
|
||||
|
||||
|
||||
- name: Announce Release
|
||||
env:
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
|
37
.github/workflows/create-release.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
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)
|
79
.github/workflows/playwright.yml
vendored
@ -12,11 +12,31 @@ concurrency:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
|
||||
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:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
needs: check-rust-changes
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
@ -28,13 +48,38 @@ jobs:
|
||||
run: yarn
|
||||
- name: Install Playwright Browsers
|
||||
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
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cache wasm
|
||||
- name: Cache Wasm (because rust diff)
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
- name: build wasm
|
||||
- name: OR Cache Wasm (because wasm cache failed)
|
||||
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
|
||||
- name: build web
|
||||
run: yarn build:local
|
||||
@ -89,6 +134,7 @@ jobs:
|
||||
playwright-macos:
|
||||
timeout-minutes: 60
|
||||
runs-on: macos-14
|
||||
needs: check-rust-changes
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
@ -99,13 +145,38 @@ jobs:
|
||||
run: yarn
|
||||
- name: Install Playwright Browsers
|
||||
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
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cache wasm
|
||||
- name: Cache Wasm (because rust diff)
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
- name: build wasm
|
||||
- name: OR Cache Wasm (because wasm cache failed)
|
||||
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
|
||||
- name: build web
|
||||
run: yarn build:local
|
||||
|
1
.gitignore
vendored
@ -54,3 +54,4 @@ src/**/*.typegen.ts
|
||||
src-tauri/gen
|
||||
|
||||
src/wasm-lib/grackle/stdlib_cube_partial.json
|
||||
Mac_App_Distribution.provisionprofile
|
||||
|
12
README.md
@ -59,6 +59,10 @@ followed by:
|
||||
```
|
||||
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)
|
||||
|
||||
@ -68,7 +72,13 @@ finally, to run the web app only, run:
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Developing in Chrome
|
||||
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.
|
||||
|
||||
### 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
|
||||
[blocks Third-Party Cookies](https://developer.chrome.com/en/docs/privacy-sandbox/third-party-cookie-phase-out/).
|
||||
|
@ -1,3 +1,3 @@
|
||||
module.exports = {
|
||||
presets: ["@babel/preset-env"],
|
||||
presets: ['@babel/preset-env'],
|
||||
}
|
||||
|
7410
docs/kcl/std.json
@ -1,5 +1,5 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { getUtils } from './test-utils'
|
||||
import { makeTemplate, getUtils } from './test-utils'
|
||||
import waitOn from 'wait-on'
|
||||
import { roundOff } from 'lib/utils'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
@ -8,9 +8,11 @@ import {
|
||||
TEST_SETTINGS,
|
||||
TEST_SETTINGS_KEY,
|
||||
TEST_SETTINGS_CORRUPTED,
|
||||
TEST_SETTINGS_ONBOARDING,
|
||||
TEST_SETTINGS_ONBOARDING_EXPORT,
|
||||
TEST_SETTINGS_ONBOARDING_START,
|
||||
} from './storageStates'
|
||||
import * as TOML from '@iarna/toml'
|
||||
import { Coords2d } from 'lang/std/sketch'
|
||||
|
||||
/*
|
||||
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
||||
@ -104,6 +106,7 @@ test('Basic sketch', async ({ page }) => {
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
@ -277,7 +280,7 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
const bottomAng = 25
|
||||
*/
|
||||
await page.click('.cm-content')
|
||||
await page.keyboard.type('# error')
|
||||
await page.keyboard.type('$ error')
|
||||
|
||||
// press arrows to clear autocomplete
|
||||
await page.keyboard.press('ArrowLeft')
|
||||
@ -294,10 +297,10 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
|
||||
// error text on hover
|
||||
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
|
||||
await page.getByText('# error').click()
|
||||
await page.getByText('$ error').click()
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.down('Shift')
|
||||
await page.keyboard.press('Home')
|
||||
@ -328,9 +331,7 @@ test('if you write invalid kcl you get inlined errors', async ({ page }) => {
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
})
|
||||
|
||||
/* 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 ({
|
||||
test('if your kcl gets an error from the engine it is inlined', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = getUtils(page)
|
||||
@ -349,7 +350,7 @@ const sketch001 = startSketchOn(box, "revolveAxis")
|
||||
|> startProfileAt([5, 10], %)
|
||||
|> line([0, -10], %)
|
||||
|> line([2, 0], %)
|
||||
|> line([0, 10], %)
|
||||
|> line([0, -10], %)
|
||||
|> close(%)
|
||||
|> revolve({
|
||||
axis: getEdge('revolveAxis', box),
|
||||
@ -364,7 +365,7 @@ angle: 90
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
u.openDebugPanel()
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
@ -378,7 +379,7 @@ angle: 90
|
||||
'sketch profile must lie entirely on one side of the revolution axis'
|
||||
)
|
||||
).toBeVisible()
|
||||
})*/
|
||||
})
|
||||
|
||||
test('executes on load', async ({ page }) => {
|
||||
const u = getUtils(page)
|
||||
@ -528,6 +529,10 @@ test.describe('Can create sketches on all planes and their back sides', () => {
|
||||
})
|
||||
|
||||
test('Auto complete works', async ({ page }) => {
|
||||
test.skip(
|
||||
true,
|
||||
'CORS issue stopping the kcl lsp from working, enable again later'
|
||||
)
|
||||
const u = getUtils(page)
|
||||
// const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
@ -566,7 +571,9 @@ test('Auto complete works', async ({ page }) => {
|
||||
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.type('12')
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Tab')
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.press('Tab')
|
||||
await page.keyboard.press('Enter')
|
||||
@ -595,13 +602,12 @@ test('Auto complete works', async ({ page }) => {
|
||||
|
||||
test('Stored settings are validated and fall back to defaults', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const u = getUtils(page)
|
||||
|
||||
// Override beforeEach test setup
|
||||
// with corrupted settings
|
||||
await context.addInitScript(
|
||||
await page.addInitScript(
|
||||
async ({ settingsKey, settings }) => {
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
},
|
||||
@ -618,18 +624,18 @@ test('Stored settings are validated and fall back to defaults', async ({
|
||||
// Check the settings were reset
|
||||
const storedSettings = TOML.parse(
|
||||
await page.evaluate(
|
||||
({ settingsKey }) => localStorage.getItem(settingsKey) || '{}',
|
||||
({ settingsKey }) => localStorage.getItem(settingsKey) || '',
|
||||
{ settingsKey: TEST_SETTINGS_KEY }
|
||||
)
|
||||
) as { settings: SaveSettingsPayload }
|
||||
|
||||
expect(storedSettings.settings.app?.theme).toBe('dark')
|
||||
expect(storedSettings.settings?.app?.theme).toBe(undefined)
|
||||
|
||||
// Check that the invalid settings were removed
|
||||
expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined)
|
||||
expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined)
|
||||
expect(storedSettings.settings.app?.projectDirectory).toBe(undefined)
|
||||
expect(storedSettings.settings.projects?.defaultProjectName).toBe(undefined)
|
||||
expect(storedSettings.settings?.modeling?.defaultUnit).toBe(undefined)
|
||||
expect(storedSettings.settings?.modeling?.mouseControls).toBe(undefined)
|
||||
expect(storedSettings.settings?.app?.projectDirectory).toBe(undefined)
|
||||
expect(storedSettings.settings?.projects?.defaultProjectName).toBe(undefined)
|
||||
})
|
||||
|
||||
test('Project settings can be set and override user settings', async ({
|
||||
@ -680,6 +686,45 @@ test('Project settings can be set and override user settings', async ({
|
||||
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 }) => {
|
||||
const u = getUtils(page)
|
||||
|
||||
@ -692,7 +737,7 @@ test('Onboarding redirects and code updating', async ({ page }) => {
|
||||
},
|
||||
{
|
||||
settingsKey: TEST_SETTINGS_KEY,
|
||||
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING }),
|
||||
settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_EXPORT }),
|
||||
}
|
||||
)
|
||||
|
||||
@ -736,7 +781,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
await u.openDebugPanel()
|
||||
|
||||
const xAxisClick = () =>
|
||||
page.mouse.click(700, 250).then(() => page.waitForTimeout(100))
|
||||
page.mouse.click(700, 253).then(() => page.waitForTimeout(100))
|
||||
const emptySpaceClick = () =>
|
||||
page.mouse.click(728, 343).then(() => page.waitForTimeout(100))
|
||||
const topHorzSegmentClick = () =>
|
||||
@ -761,6 +806,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
@ -768,12 +814,14 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)`)
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
@ -786,10 +834,14 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Line' }).click()
|
||||
|
||||
await u.closeDebugPanel()
|
||||
const selectionSequence = async () => {
|
||||
const selectionSequence = async (isSecondTime = false) => {
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
|
||||
await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.move(
|
||||
startXPx + PUR * 15,
|
||||
isSecondTime ? 430 : 500 - PUR * 10
|
||||
)
|
||||
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
// bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience
|
||||
@ -799,7 +851,10 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
// check mousing off, than mousing onto another line
|
||||
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 20) // mouse onto another line
|
||||
await page.mouse.move(
|
||||
startXPx + PUR * 10,
|
||||
isSecondTime ? 295 : 500 - PUR * 20
|
||||
) // mouse onto another line
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
|
||||
// now check clicking works including axis
|
||||
@ -809,6 +864,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
await page.keyboard.down('Shift')
|
||||
const absYButton = page.getByRole('button', { name: 'ABS Y' })
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await page.waitForTimeout(100)
|
||||
await xAxisClick()
|
||||
await page.keyboard.up('Shift')
|
||||
await absYButton.and(page.locator(':not([disabled])')).waitFor()
|
||||
@ -817,10 +873,12 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
// clear selection by clicking on nothing
|
||||
await emptySpaceClick()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
// same selection but click the axis first
|
||||
await xAxisClick()
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await page.keyboard.down('Shift')
|
||||
await page.waitForTimeout(100)
|
||||
await topHorzSegmentClick()
|
||||
|
||||
await page.keyboard.up('Shift')
|
||||
@ -833,6 +891,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
|
||||
await page.keyboard.down('Shift')
|
||||
await expect(absYButton).toBeDisabled()
|
||||
await page.waitForTimeout(100)
|
||||
await xAxisClick()
|
||||
await page.keyboard.up('Shift')
|
||||
await expect(absYButton).not.toBeDisabled()
|
||||
@ -875,11 +934,16 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// enter sketch again
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
|
||||
'default_camera_get_settings'
|
||||
)
|
||||
await page.waitForTimeout(150)
|
||||
|
||||
await page.waitForTimeout(300) // wait for animation
|
||||
|
||||
// hover again and check it works
|
||||
await selectionSequence()
|
||||
await selectionSequence(true)
|
||||
})
|
||||
|
||||
test.describe('Command bar tests', () => {
|
||||
@ -891,9 +955,8 @@ test.describe('Command bar tests', () => {
|
||||
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
|
||||
// First try opening the command bar and closing it
|
||||
// It has a different label on mac and windows/linux, "Meta+K" and "Ctrl+/" respectively
|
||||
await page
|
||||
.getByRole('button', { name: 'Ctrl+/' })
|
||||
.getByRole('button', { name: 'Commands', exact: false })
|
||||
.or(page.getByRole('button', { name: '⌘K' }))
|
||||
.click()
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
@ -938,13 +1001,13 @@ test.describe('Command bar tests', () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const distance = sqrt(20)
|
||||
const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([-6.95, 4.98], %)
|
||||
|> line([25.1, 0.41], %)
|
||||
|> line([0.73, -14.93], %)
|
||||
|> line([-23.44, 0.52], %)
|
||||
|> close(%)
|
||||
`
|
||||
const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([-6.95, 10.98], %)
|
||||
|> line([25.1, 0.41], %)
|
||||
|> line([0.73, -20.93], %)
|
||||
|> line([-23.44, 0.52], %)
|
||||
|> close(%)
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
@ -961,7 +1024,6 @@ test.describe('Command bar tests', () => {
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await u.clearCommandLogs()
|
||||
await page.getByText('|> line([0.73, -14.93], %)').click()
|
||||
await page.getByRole('button', { name: 'Extrude' }).isEnabled()
|
||||
|
||||
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
@ -971,6 +1033,12 @@ test.describe('Command bar tests', () => {
|
||||
// Search for extrude command and choose it
|
||||
await page.getByRole('option', { name: 'Extrude' }).click()
|
||||
|
||||
// Assert that we're on the selection step
|
||||
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
|
||||
// Select a face
|
||||
await page.mouse.move(700, 200)
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
// Assert that we're on the distance step
|
||||
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
|
||||
|
||||
@ -1004,9 +1072,9 @@ test.describe('Command bar tests', () => {
|
||||
`const distance = sqrt(20)
|
||||
const distance001 = 5 + 7
|
||||
const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([-6.95, 4.98], %)
|
||||
|> startProfileAt([-6.95, 10.98], %)
|
||||
|> line([25.1, 0.41], %)
|
||||
|> line([0.73, -14.93], %)
|
||||
|> line([0.73, -20.93], %)
|
||||
|> line([-23.44, 0.52], %)
|
||||
|> close(%)
|
||||
|> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines
|
||||
@ -1015,6 +1083,7 @@ const part001 = startSketchOn('-XZ')
|
||||
})
|
||||
|
||||
test('Can add multiple sketches', async ({ page }) => {
|
||||
test.skip(process.platform === 'darwin', 'Can add multiple sketches')
|
||||
const u = getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
@ -1065,6 +1134,7 @@ test('Can add multiple sketches', async ({ page }) => {
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
|> line([${commonPoints.num1}, 0], %)
|
||||
|> line([0, ${commonPoints.num1}], %)`)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
@ -1080,24 +1150,33 @@ test('Can add multiple sketches', async ({ page }) => {
|
||||
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
|
||||
await u.updateCamPosition([0, 100, 100])
|
||||
await u.updateCamPosition([100, 100, 100])
|
||||
await page.waitForTimeout(250)
|
||||
|
||||
// start a new sketch
|
||||
await u.clearCommandLogs()
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(673, 384)
|
||||
await page.waitForTimeout(400)
|
||||
await page.mouse.click(650, 450)
|
||||
|
||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||
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)
|
||||
const startAt2 = '[0.93,-1.25]'
|
||||
const startAt2 =
|
||||
process.platform === 'darwin' ? '[9.75, -13.16]' : '[0.93, -1.25]'
|
||||
await expect(
|
||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||
).toBe(
|
||||
`${finalCodeFirstSketch}
|
||||
const part002 = startSketchOn('XY')
|
||||
const part002 = startSketchOn('${plane}')
|
||||
|> startProfileAt(${startAt2}, %)`.replace(/\s/g, '')
|
||||
)
|
||||
await page.waitForTimeout(100)
|
||||
@ -1106,12 +1185,12 @@ const part002 = startSketchOn('XY')
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const num2 = 0.94
|
||||
const num2 = process.platform === 'darwin' ? 9.84 : 0.94
|
||||
await expect(
|
||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||
).toBe(
|
||||
`${finalCodeFirstSketch}
|
||||
const part002 = startSketchOn('XY')
|
||||
const part002 = startSketchOn('${plane}')
|
||||
|> startProfileAt(${startAt2}, %)
|
||||
|> line([${num2}, 0], %)`.replace(/\s/g, '')
|
||||
)
|
||||
@ -1121,21 +1200,29 @@ const part002 = startSketchOn('XY')
|
||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||
).toBe(
|
||||
`${finalCodeFirstSketch}
|
||||
const part002 = startSketchOn('XY')
|
||||
const part002 = startSketchOn('${plane}')
|
||||
|> startProfileAt(${startAt2}, %)
|
||||
|> line([${num2}, 0], %)
|
||||
|> line([0, ${roundOff(num2 - 0.01)}], %)`.replace(/\s/g, '')
|
||||
|> line([0, ${roundOff(
|
||||
num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
|
||||
)}], %)`.replace(/\s/g, '')
|
||||
)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(
|
||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
||||
).toBe(
|
||||
`${finalCodeFirstSketch}
|
||||
const part002 = startSketchOn('XY')
|
||||
const part002 = startSketchOn('${plane}')
|
||||
|> startProfileAt(${startAt2}, %)
|
||||
|> line([${num2}, 0], %)
|
||||
|> line([0, ${roundOff(num2 - 0.01)}], %)
|
||||
|> line([-1.87, 0], %)`.replace(/\s/g, '')
|
||||
|> line([0, ${roundOff(
|
||||
num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
|
||||
)}], %)
|
||||
|> line([-${process.platform === 'darwin' ? 19.59 : 1.87}, 0], %)`.replace(
|
||||
/\s/g,
|
||||
''
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
@ -1178,6 +1265,72 @@ test('ProgramMemory can be serialised', async ({ page }) => {
|
||||
})
|
||||
})
|
||||
|
||||
test('Hovering over 3d features highlights code', async ({ page }) => {
|
||||
const u = getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([20, 0], %)
|
||||
|> line([7.13, 4 + 0], %)
|
||||
|> angledLine({ angle: 3 + 0, length: 3.14 + 0 }, %)
|
||||
|> lineTo([20.14 + 0, -0.14 + 0], %)
|
||||
|> xLineTo(29 + 0, %)
|
||||
|> yLine(-3.14 + 0, %, 'a')
|
||||
|> xLine(1.63, %)
|
||||
|> angledLineOfXLength({ angle: 3 + 0, length: 3.14 }, %)
|
||||
|> angledLineOfYLength({ angle: 30, length: 3 + 0 }, %)
|
||||
|> angledLineToX({ angle: 22.14 + 0, to: 12 }, %)
|
||||
|> angledLineToY({ angle: 30, to: 11.14 }, %)
|
||||
|> angledLineThatIntersects({
|
||||
angle: 3.14,
|
||||
intersectTag: 'a',
|
||||
offset: 0
|
||||
}, %)
|
||||
|> tangentialArcTo([13.14 + 0, 13.14], %)
|
||||
|> close(%)
|
||||
|> extrude(5 + 7, %)
|
||||
`
|
||||
)
|
||||
})
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
const extrusionTop: Coords2d = [800, 240]
|
||||
const flatExtrusionFace: Coords2d = [960, 160]
|
||||
const arc: Coords2d = [840, 160]
|
||||
const close: Coords2d = [720, 200]
|
||||
const nothing: Coords2d = [600, 200]
|
||||
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await page.mouse.click(nothing[0], nothing[1])
|
||||
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await page.mouse.move(extrusionTop[0], extrusionTop[1])
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
|
||||
await page.mouse.move(arc[0], arc[1])
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
|
||||
await page.mouse.move(close[0], close[1])
|
||||
await expect(page.getByTestId('hover-highlight')).toBeVisible()
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
|
||||
await page.mouse.move(flatExtrusionFace[0], flatExtrusionFace[1])
|
||||
await expect(page.getByTestId('hover-highlight')).toHaveCount(5) // multiple lines
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test("Various pipe expressions should and shouldn't allow edit and or extrude", async ({
|
||||
page,
|
||||
}) => {
|
||||
@ -1312,7 +1465,7 @@ test('Deselecting line tool should mean nothing happens on click', async ({
|
||||
`const part001 = startSketchOn('-XZ')`
|
||||
)
|
||||
|
||||
await page.waitForTimeout(300)
|
||||
await page.waitForTimeout(600)
|
||||
|
||||
let previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
@ -1339,10 +1492,12 @@ test('Deselecting line tool should mean nothing happens on click', async ({
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(700, 300)
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(750, 300)
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
@ -1367,16 +1522,16 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
|
||||
const startPX = [652, 418]
|
||||
const lineEndPX = [794, 416]
|
||||
const arcEndPX = [893, 318]
|
||||
const startPX = [665, 458]
|
||||
const lineEndPX = [842, 458]
|
||||
const arcEndPX = [971, 342]
|
||||
|
||||
const dragPX = 30
|
||||
|
||||
await page.getByText('startProfileAt([4.61, -14.01], %)').click()
|
||||
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await page.waitForTimeout(400)
|
||||
let prevContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
const step5 = { steps: 5 }
|
||||
@ -1386,7 +1541,7 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
||||
await page.mouse.up()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
prevContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
@ -1414,9 +1569,9 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
|
||||
// expect the code to have changed
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([7.01, -11.79], %)
|
||||
|> line([14.69, 2.73], %)
|
||||
|> tangentialArcTo([27.6, -3.25], %)`)
|
||||
|> startProfileAt([6.44, -12.07], %)
|
||||
|> line([14.04, 2.03], %)
|
||||
|> tangentialArcTo([27.19, -4.2], %)`)
|
||||
})
|
||||
|
||||
const doSnapAtDifferentScales = async (
|
||||
@ -1535,38 +1690,46 @@ test('Sketch on face', async ({ page }) => {
|
||||
).not.toBeDisabled()
|
||||
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
let previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await page.mouse.click(793, 133)
|
||||
await u.openAndClearDebugPanel()
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.mouse.click(793, 133),
|
||||
'default_camera_get_settings',
|
||||
true
|
||||
)
|
||||
await page.waitForTimeout(150)
|
||||
|
||||
const firstClickPosition = [612, 238]
|
||||
const secondClickPosition = [661, 242]
|
||||
const thirdClickPosition = [609, 267]
|
||||
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(secondClickPosition[0], secondClickPosition[1])
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(thirdClickPosition[0], thirdClickPosition[1])
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(firstClickPosition[0], firstClickPosition[1])
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
||||
|> startProfileAt([1.03, 1.03], %)
|
||||
|> line([4.18, -0.35], %)
|
||||
|> line([-4.44, -2.13], %)
|
||||
|> startProfileAt([-12.83, 6.7], %)
|
||||
|> line([2.87, -0.23], %)
|
||||
|> line([-3.05, -1.47], %)
|
||||
|> close(%)`)
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
@ -1576,9 +1739,14 @@ test('Sketch on face', async ({ page }) => {
|
||||
await u.updateCamPosition([1049, 239, 686])
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page.getByText('startProfileAt([1.03, 1.03], %)').click()
|
||||
await page.getByText('startProfileAt([-12.83, 6.7], %)').click()
|
||||
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await u.doAndWaitForCmd(
|
||||
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
|
||||
'default_camera_get_settings',
|
||||
true
|
||||
)
|
||||
await page.waitForTimeout(150)
|
||||
await page.setViewportSize({ width: 1200, height: 1200 })
|
||||
await u.openAndClearDebugPanel()
|
||||
await u.updateCamPosition([452, -152, 1166])
|
||||
@ -1596,21 +1764,20 @@ test('Sketch on face', async ({ page }) => {
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(previousCodeContent)
|
||||
previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
||||
|> 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(%)`)
|
||||
const result = makeTemplate`const part002 = startSketchOn(part001, 'seg01')
|
||||
|> startProfileAt([-12.83, 6.7], %)
|
||||
|> line([${[2.28, 2.35]}, -${0.07}], %)
|
||||
|> line([-3.05, -1.47], %)
|
||||
|> close(%)`
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(result.regExp)
|
||||
|
||||
// exit sketch
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
|
||||
await page.getByText('startProfileAt([1.03, 1.03], %)').click()
|
||||
await page.getByText('startProfileAt([-12.83, 6.7], %)').click()
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).not.toBeDisabled()
|
||||
await page.getByRole('button', { name: 'Extrude' }).click()
|
||||
@ -1622,15 +1789,9 @@ test('Sketch on face', async ({ page }) => {
|
||||
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
||||
|> 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, %)`)
|
||||
const result2 = result.genNext`
|
||||
|> extrude(${[5, 5]} + 7, %)`
|
||||
await expect(page.locator('.cm-content')).toHaveText(result2.regExp)
|
||||
})
|
||||
|
||||
test('Can code mod a line length', async ({ page }) => {
|
||||
@ -1661,11 +1822,11 @@ test('Can code mod a line length', async ({ page }) => {
|
||||
|
||||
// enter sketch again
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(300) // wait for animation
|
||||
await page.waitForTimeout(350) // wait for animation
|
||||
|
||||
const startXPx = 500
|
||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
||||
await page.mouse.click(615, 133)
|
||||
await page.mouse.click(615, 102)
|
||||
await page.getByRole('button', { name: 'length', exact: true }).click()
|
||||
await page.getByText('Add constraining value').click()
|
||||
|
||||
@ -1673,3 +1834,42 @@ 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, %)`
|
||||
)
|
||||
})
|
||||
|
||||
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, %)`
|
||||
)
|
||||
})
|
||||
|
@ -507,7 +507,7 @@ test('Draft rectangles should look right', async ({ page, context }) => {
|
||||
`const part001 = startSketchOn('-XZ')`
|
||||
)
|
||||
|
||||
await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
|
||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const startXPx = 600
|
||||
@ -597,12 +597,15 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
|
||||
// exit sketch
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await u.doAndWaitForImageDiff(
|
||||
() => page.getByRole('button', { name: 'Exit Sketch' }).click(),
|
||||
200
|
||||
)
|
||||
|
||||
// wait for execution done
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
await page.waitForTimeout(200)
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||
await expect(page).toHaveScreenshot({
|
||||
@ -696,12 +699,15 @@ test.describe('Client side scene scale should match engine scale', () => {
|
||||
|
||||
// exit sketch
|
||||
await u.openAndClearDebugPanel()
|
||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||
await u.doAndWaitForImageDiff(
|
||||
() => page.getByRole('button', { name: 'Exit Sketch' }).click(),
|
||||
200
|
||||
)
|
||||
|
||||
// wait for execution done
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
await page.waitForTimeout(200)
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||
await expect(page).toHaveScreenshot({
|
||||
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 48 KiB |
@ -1,12 +1,13 @@
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
import { Themes } from 'lib/theme'
|
||||
|
||||
export const TEST_SETTINGS_KEY = '/user.toml'
|
||||
export const TEST_SETTINGS_KEY = '/settings.toml'
|
||||
export const TEST_SETTINGS = {
|
||||
app: {
|
||||
theme: Themes.Dark,
|
||||
onboardingStatus: 'dismissed',
|
||||
projectDirectory: '',
|
||||
enableSSAO: false,
|
||||
},
|
||||
modeling: {
|
||||
defaultUnit: 'in',
|
||||
@ -21,9 +22,14 @@ export const TEST_SETTINGS = {
|
||||
},
|
||||
} satisfies Partial<SaveSettingsPayload>
|
||||
|
||||
export const TEST_SETTINGS_ONBOARDING = {
|
||||
export const TEST_SETTINGS_ONBOARDING_EXPORT = {
|
||||
...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>
|
||||
|
||||
export const TEST_SETTINGS_CORRUPTED = {
|
||||
|
@ -182,3 +182,76 @@ 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
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { browser, $, expect } from '@wdio/globals'
|
||||
import fs from 'fs/promises'
|
||||
|
||||
const documentsDir = `${process.env.HOME}/Documents`
|
||||
const userSettingsFile = `${process.env.HOME}/.config/dev.zoo.modeling-app/user.toml`
|
||||
const userSettingsDir = `${process.env.HOME}/.config/dev.zoo.modeling-app`
|
||||
const defaultProjectDir = `${documentsDir}/zoo-modeling-app-projects`
|
||||
const newProjectDir = `${documentsDir}/a-different-directory`
|
||||
const userCodeDir = '/tmp/kittycad_user_code'
|
||||
@ -29,8 +29,10 @@ describe('ZMA (Tauri, Linux)', () => {
|
||||
// Clean up filesystem from previous tests
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
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(userSettingsFile, { force: true })
|
||||
await fs.rm(userSettingsDir, { force: true, recursive: true })
|
||||
await fs.mkdir(defaultProjectDir, { recursive: true })
|
||||
await fs.mkdir(newProjectDir, { recursive: true })
|
||||
|
||||
const signInButton = await $('[data-testid="sign-in-button"]')
|
||||
@ -70,8 +72,9 @@ describe('ZMA (Tauri, Linux)', () => {
|
||||
console.log(cr.status)
|
||||
|
||||
// Now should be signed in
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000))
|
||||
const newFileButton = await $('[data-testid="home-new-file"]')
|
||||
expect(await newFileButton.getText()).toEqual('New file')
|
||||
expect(await newFileButton.getText()).toEqual('New project')
|
||||
})
|
||||
|
||||
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
|
||||
@ -117,8 +120,8 @@ describe('ZMA (Tauri, Linux)', () => {
|
||||
it('opens the new file and expects a loading stream', async () => {
|
||||
const projectLink = await $('[data-testid="project-link"]')
|
||||
await click(projectLink)
|
||||
const loadingText = await $('[data-testid="loading-stream"]')
|
||||
expect(await loadingText.getText()).toContain('Loading stream...')
|
||||
const errorText = await $('[data-testid="unexpected-error"]')
|
||||
expect(await errorText.getText()).toContain('unexpected error')
|
||||
await browser.execute('window.location.href = "tauri://localhost/home"')
|
||||
})
|
||||
|
||||
|
24
get-latest-wasm-bundle.sh
Executable file
@ -0,0 +1,24 @@
|
||||
#!/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"
|
@ -15,7 +15,7 @@
|
||||
<script
|
||||
defer
|
||||
data-domain="app.zoo.dev"
|
||||
src="https://plausible.corp.zoo.dev/js/script.js"
|
||||
src="https://plausible.corp.zoo.dev/js/script.tagged-events.js"
|
||||
></script>
|
||||
<title>Zoo Modeling App</title>
|
||||
</head>
|
||||
|
@ -57,7 +57,7 @@ echo "New version number without 'v': $new_version_number"
|
||||
git checkout -b "cut-release-$new_version"
|
||||
|
||||
echo "$(jq --arg v "$new_version_number" '.version=$v' package.json --indent 2)" > package.json
|
||||
echo "$(jq --arg v "$new_version_number" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json
|
||||
echo "$(jq --arg v "$new_version_number" '.version=$v' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json
|
||||
|
||||
git add package.json src-tauri/tauri.conf.json
|
||||
git commit -m "Cut release $new_version"
|
||||
|
13
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.17.3",
|
||||
"version": "0.21.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.16.0",
|
||||
@ -8,9 +8,9 @@
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "^0.0.58",
|
||||
"@kittycad/lib": "^0.0.60",
|
||||
"@lezer/javascript": "^1.4.9",
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
@ -84,8 +84,9 @@
|
||||
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
|
||||
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
||||
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
||||
"fmt": "prettier --write ./src && prettier --write ./e2e",
|
||||
"fmt-check": "prettier --check ./src && prettier --check ./e2e",
|
||||
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e",
|
||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./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": "(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",
|
||||
@ -122,6 +123,7 @@
|
||||
"@tauri-apps/cli": "^2.0.0-beta.13",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/debounce-promise": "^3.1.9",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/pixelmatch": "^5.2.6",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/react-modal": "^3.16.3",
|
||||
@ -132,6 +134,7 @@
|
||||
"@types/wicg-file-system-access": "^2023.10.5",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@vitest/web-worker": "^1.5.0",
|
||||
"@wdio/cli": "^8.24.3",
|
||||
"@wdio/globals": "^8.36.0",
|
||||
"@wdio/local-runner": "^8.36.0",
|
||||
|
@ -27,7 +27,7 @@ export default defineConfig({
|
||||
baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
trace: 'retain-on-failure',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
@ -49,8 +49,6 @@ export default defineConfig({
|
||||
// use: { ...devices['Desktop Chrome'] },
|
||||
// },
|
||||
|
||||
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
@ -78,4 +76,4 @@ export default defineConfig({
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
15
public/.well-known/apple-app-site-association
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"applinks": {
|
||||
"details": [
|
||||
{
|
||||
"appIDs": ["92H8YB3B95.dev.zoo.modeling-app"],
|
||||
"components": [
|
||||
{
|
||||
"/": "/file/*",
|
||||
"comment": "Matches any URL whose path starts with /file/"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
BIN
public/onboarding-bracket-dimensions-dark.png
Normal file
After Width: | Height: | Size: 150 KiB |
BIN
public/onboarding-bracket-dimensions.png
Normal file
After Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 867 KiB |
2760
src-tauri/Cargo.lock
generated
@ -1,38 +1,44 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
description = "The Zoo Modeling App"
|
||||
authors = ["Zoo Engineers <eng@zoo.dev>"]
|
||||
license = ""
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
default-run = "app"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0-beta.12", features = [] }
|
||||
tauri-build = { version = "2.0.0-beta.13", features = [] }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
kittycad = "0.2.67"
|
||||
kcl-lib = { version = "0.1.53", path = "../src/wasm-lib/kcl" }
|
||||
kittycad = "0.3.0"
|
||||
log = "0.4.21"
|
||||
oauth2 = "4.4.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
|
||||
tauri-plugin-dialog = { version = "2.0.0-beta.5" }
|
||||
tauri-plugin-fs = { version = "2.0.0-beta.5" }
|
||||
tauri-plugin-http = { version = "2.0.0-beta.5" }
|
||||
tauri-plugin-cli = { version = "2.0.0-beta.3" }
|
||||
tauri-plugin-deep-link = { version = "2.0.0-beta.3" }
|
||||
tauri-plugin-dialog = { version = "2.0.0-beta.6" }
|
||||
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-process = { version = "2.0.0-beta.2" }
|
||||
tauri-plugin-shell = { version = "2.0.0-beta.2" }
|
||||
tauri-plugin-updater = { version = "2.0.0-beta.4" }
|
||||
tokio = { version = "1.37.0", features = ["time"] }
|
||||
tokio = { version = "1.37.0", features = ["time", "fs", "process"] }
|
||||
toml = "0.8.2"
|
||||
url = "2.5.0"
|
||||
|
||||
[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.
|
||||
# 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!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
updater = []
|
||||
|
376
src-tauri/Info.plist
Normal file
@ -0,0 +1,376 @@
|
||||
<?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>
|
@ -7,6 +7,9 @@
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"cli:default",
|
||||
"deep-link:default",
|
||||
"log:default",
|
||||
"path:default",
|
||||
"event:default",
|
||||
"window:default",
|
||||
@ -23,7 +26,6 @@
|
||||
"fs:allow-copy-file",
|
||||
"fs:allow-mkdir",
|
||||
"fs:allow-remove",
|
||||
"fs:allow-remove",
|
||||
"fs:allow-rename",
|
||||
"fs:allow-exists",
|
||||
"fs:allow-stat",
|
||||
|
24
src-tauri/entitlements/app-store.entitlements
Normal file
@ -0,0 +1,24 @@
|
||||
<?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>
|
6
src-tauri/rustfmt.toml
Normal file
@ -0,0 +1,6 @@
|
||||
max_width = 120
|
||||
edition = "2018"
|
||||
format_code_in_doc_comments = true
|
||||
format_strings = false
|
||||
imports_granularity = "Crate"
|
||||
group_imports = "StdExternalCrate"
|
@ -1,98 +1,232 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
pub(crate) mod state;
|
||||
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use kcl_lib::settings::types::{
|
||||
file::{FileEntry, Project, ProjectRoute, ProjectState},
|
||||
project::ProjectConfiguration,
|
||||
Configuration,
|
||||
};
|
||||
use oauth2::TokenResponse;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
use tauri::ipc::InvokeError;
|
||||
use tauri::{ipc::InvokeError, Manager};
|
||||
use tauri_plugin_cli::CliExt;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
||||
use tokio::process::Command;
|
||||
|
||||
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]
|
||||
fn read_toml(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()))?;
|
||||
let value =
|
||||
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)
|
||||
fn get_initial_default_dir(app: tauri::AppHandle) -> Result<PathBuf, InvokeError> {
|
||||
let dir = match app.path().document_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => {
|
||||
// for headless Linux (eg. Github Actions)
|
||||
let home_dir = app.path().home_dir()?;
|
||||
home_dir.join("Documents")
|
||||
}
|
||||
};
|
||||
|
||||
Ok(dir.join(PROJECT_FOLDER))
|
||||
}
|
||||
|
||||
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
||||
/// Removed from tauri v2
|
||||
#[derive(Debug, Serialize)]
|
||||
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>>,
|
||||
}
|
||||
|
||||
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
||||
/// Removed from tauri v2
|
||||
fn is_dir<P: AsRef<Path>>(path: P) -> Result<bool> {
|
||||
std::fs::metadata(path)
|
||||
.map(|md| md.is_dir())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
||||
/// Removed from tauri v2
|
||||
#[tauri::command]
|
||||
fn read_dir_recursive(path: &str) -> Result<Vec<DiskEntry>, InvokeError> {
|
||||
let mut files_and_dirs: Vec<DiskEntry> = vec![];
|
||||
// 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();
|
||||
async fn get_state(app: tauri::AppHandle) -> Result<Option<ProjectState>, InvokeError> {
|
||||
let store = app.state::<state::Store>();
|
||||
Ok(store.get().await)
|
||||
}
|
||||
|
||||
if let Ok(flag) = is_dir(&path) {
|
||||
files_and_dirs.push(DiskEntry {
|
||||
path: path.clone(),
|
||||
children: if flag {
|
||||
Some(read_dir_recursive(path.to_str().expect("No path"))?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
name: path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy())
|
||||
.map(|name| name.to_string()),
|
||||
});
|
||||
#[tauri::command]
|
||||
async fn set_state(app: tauri::AppHandle, state: Option<ProjectState>) -> Result<(), InvokeError> {
|
||||
let store = app.state::<state::Store>();
|
||||
store.set(state).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration, InvokeError> {
|
||||
let mut settings_path = get_app_settings_file_path(&app).await?;
|
||||
let mut needs_migration = false;
|
||||
|
||||
// Check if this file exists.
|
||||
if !settings_path.exists() {
|
||||
// Try the backwards compatible path.
|
||||
// TODO: Remove this after a few releases.
|
||||
let app_config_dir = app.path().app_config_dir()?;
|
||||
settings_path = format!(
|
||||
"{}user.toml",
|
||||
app_config_dir.display().to_string().trim_end_matches('/')
|
||||
)
|
||||
.into();
|
||||
needs_migration = true;
|
||||
// Check if this path exists.
|
||||
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)
|
||||
.await
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
let mut parsed = Configuration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?;
|
||||
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)
|
||||
}
|
||||
|
||||
/// This command returns a string that is the contents of a file at the path.
|
||||
#[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)
|
||||
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(contents)
|
||||
|
||||
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.
|
||||
/// The string returned from this method is the access token.
|
||||
#[tauri::command]
|
||||
async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError> {
|
||||
println!("Logging in...");
|
||||
log::debug!("Logging in...");
|
||||
// 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"))
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
@ -103,8 +237,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
||||
let auth_client = oauth2::basic::BasicClient::new(
|
||||
oauth2::ClientId::new(client_id),
|
||||
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(
|
||||
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
||||
@ -132,12 +265,10 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
||||
// and bypass the shell::open call as it fails on GitHub Actions.
|
||||
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
|
||||
if e2e_tauri_enabled {
|
||||
println!(
|
||||
"E2E_TAURI_ENABLED is set, won't open {} externally",
|
||||
auth_uri.secret()
|
||||
);
|
||||
fs::write("/tmp/kittycad_user_code", details.user_code().secret())
|
||||
.expect("Unable to write /tmp/kittycad_user_code file");
|
||||
log::warn!("E2E_TAURI_ENABLED is set, won't open {} externally", auth_uri.secret());
|
||||
tokio::fs::write("/tmp/kittycad_user_code", details.user_code().secret())
|
||||
.await
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
} else {
|
||||
app.shell()
|
||||
.open(auth_uri.secret(), None)
|
||||
@ -160,10 +291,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
||||
///This command returns the KittyCAD user info given a token.
|
||||
/// The string returned from this method is the user info as a json string.
|
||||
#[tauri::command]
|
||||
async fn get_user(
|
||||
token: Option<String>,
|
||||
hostname: &str,
|
||||
) -> Result<kittycad::types::User, InvokeError> {
|
||||
async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::User, InvokeError> {
|
||||
// Use the host passed in if it's set.
|
||||
// Otherwise, use the default host.
|
||||
let host = if hostname.is_empty() {
|
||||
@ -180,10 +308,10 @@ async fn get_user(
|
||||
baseurl = format!("http://{host}")
|
||||
}
|
||||
}
|
||||
println!("Getting user info...");
|
||||
log::debug!("Getting user info...");
|
||||
|
||||
// use kittycad library to fetch the user info from /user/me
|
||||
let mut client = kittycad::Client::new(token.unwrap());
|
||||
let mut client = kittycad::Client::new(token);
|
||||
|
||||
if baseurl != DEFAULT_HOST {
|
||||
client.set_base_url(&baseurl);
|
||||
@ -202,50 +330,186 @@ async fn get_user(
|
||||
/// 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.
|
||||
#[tauri::command]
|
||||
fn show_in_folder(path: String) {
|
||||
#[cfg(target_os = "windows")]
|
||||
fn show_in_folder(path: &str) -> Result<(), InvokeError> {
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
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()
|
||||
.unwrap();
|
||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(unix)]
|
||||
{
|
||||
Command::new("open").args(["-R", &path]).spawn().unwrap();
|
||||
Command::new("open")
|
||||
.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() {
|
||||
fn main() -> Result<()> {
|
||||
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![
|
||||
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,
|
||||
login,
|
||||
read_toml,
|
||||
read_txt_file,
|
||||
read_dir_recursive,
|
||||
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_fs::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_process::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
.setup(|app| {
|
||||
// Do update things.
|
||||
#[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(())
|
||||
}
|
||||
|
21
src-tauri/src/state.rs
Normal file
@ -0,0 +1,21 @@
|
||||
//! 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;
|
||||
}
|
||||
}
|
8
src-tauri/tauri.app-store.conf.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"bundle": {
|
||||
"macOS": {
|
||||
"entitlements": "entitlements/app-store.entitlements"
|
||||
}
|
||||
}
|
||||
}
|
@ -37,23 +37,42 @@
|
||||
}
|
||||
},
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"entitlements": null,
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"macOS": {},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"targets": "all"
|
||||
},
|
||||
"identifier": "dev.zoo.modeling-app",
|
||||
"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": {
|
||||
"open": true
|
||||
}
|
||||
},
|
||||
"productName": "Zoo Modeling App",
|
||||
"version": "0.17.3"
|
||||
"version": "0.21.0"
|
||||
}
|
||||
|
@ -5,7 +5,45 @@
|
||||
"certificateThumbprint": "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D",
|
||||
"digestAlgorithm": "sha256",
|
||||
"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": {
|
||||
"updater": {
|
||||
|
@ -30,6 +30,7 @@ import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
||||
import LspProvider from 'components/LspProvider'
|
||||
import { KclContextProvider } from 'lang/KclProvider'
|
||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
import { getState, setState } from 'lib/tauri'
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@ -52,10 +53,29 @@ const router = createBrowserRouter([
|
||||
children: [
|
||||
{
|
||||
path: paths.INDEX,
|
||||
loader: () =>
|
||||
isTauri()
|
||||
loader: async () => {
|
||||
const inTauri = 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.FILE + '/%2F' + BROWSER_PROJECT_NAME),
|
||||
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: fileLoader,
|
||||
|
@ -4,7 +4,6 @@ import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
import { isSingleCursorInPipe } from 'lang/queryAst'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import {
|
||||
@ -14,7 +13,6 @@ import {
|
||||
import { useStore } from 'useStore'
|
||||
|
||||
export const Toolbar = () => {
|
||||
const platform = usePlatform()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { state, send, context } = useModelingContext()
|
||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||
@ -274,17 +272,8 @@ export const Toolbar = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-full flex items-stretch rounded-l-sm rounded-r-full bg-chalkboard-10/80 dark:bg-chalkboard-110/70 relative">
|
||||
<menu className="flex-1 pl-1 pr-2 py-0 overflow-hidden rounded-l-sm whitespace-nowrap border-solid border border-primary/30 dark:border-chalkboard-90 border-r-0">
|
||||
<ToolbarButtons />
|
||||
</menu>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => commandBarSend({ type: 'Open' })}
|
||||
className="rounded-r-full pr-4 self-stretch border-primary/30 hover:border-primary dark:border-chalkboard-80 dark:bg-chalkboard-80 text-primary"
|
||||
>
|
||||
{platform === 'macos' ? '⌘K' : 'Ctrl+/'}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<menu className="max-w-full overflow-hidden whitespace-nowrap rounded px-1.5 py-0.5 backdrop-blur-sm bg-chalkboard-10/80 dark:bg-chalkboard-110/70 relative">
|
||||
<ToolbarButtons />
|
||||
</menu>
|
||||
)
|
||||
}
|
||||
|
@ -246,13 +246,31 @@ export class CameraControls {
|
||||
camSettings.center.y,
|
||||
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) {
|
||||
this.camera.fov = camSettings.fov_y
|
||||
} else if (
|
||||
this.camera instanceof OrthographicCamera &&
|
||||
camSettings.ortho_scale
|
||||
) {
|
||||
this.camera.zoom = camSettings.ortho_scale
|
||||
const distanceToTarget = new Vector3(
|
||||
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()
|
||||
}
|
||||
@ -965,10 +983,10 @@ export class CameraControls {
|
||||
// Pure function helpers
|
||||
|
||||
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_far = 1000 + nearFarRatio * (100000 - 1000)
|
||||
return { z_near: 0.1, z_far }
|
||||
// const z_far = 1000 + nearFarRatio * (100000 - 1000)
|
||||
return { z_near: 0.1, z_far: 1000 }
|
||||
}
|
||||
|
||||
function convertThreeCamValuesToEngineCam({
|
||||
@ -1043,3 +1061,62 @@ function _getInteractionType(
|
||||
if (enableZoom && interactionGuards.zoom.dragCallback(event)) return 'zoom'
|
||||
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',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { useModelingContext } from 'hooks/useModelingContext'
|
||||
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { useStore } from 'useStore'
|
||||
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
|
||||
import { ReactCameraProperties } from './CameraControls'
|
||||
import { throttle } from 'lib/utils'
|
||||
@ -47,10 +46,6 @@ export const ClientSideScene = ({
|
||||
const canvasRef = useRef<HTMLDivElement>(null)
|
||||
const { state, send, context } = useModelingContext()
|
||||
const { hideClient, hideServer } = useShouldHideScene()
|
||||
const { setHighlightRange } = useStore((s) => ({
|
||||
setHighlightRange: s.setHighlightRange,
|
||||
highlightRange: s.highlightRange,
|
||||
}))
|
||||
|
||||
// Listen for changes to the camera controls setting
|
||||
// and update the client-side scene's controls accordingly.
|
||||
@ -69,7 +64,6 @@ export const ClientSideScene = ({
|
||||
const canvas = canvasRef.current
|
||||
canvas.appendChild(sceneInfra.renderer.domElement)
|
||||
sceneInfra.animate()
|
||||
sceneInfra.setHighlightCallback(setHighlightRange)
|
||||
canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false)
|
||||
canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false)
|
||||
canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false)
|
||||
@ -96,7 +90,8 @@ export const ClientSideScene = ({
|
||||
cursor = 'grabbing'
|
||||
} else if (
|
||||
state.matches('Sketch.Line tool') ||
|
||||
state.matches('Sketch.Tangential arc to')
|
||||
state.matches('Sketch.Tangential arc to') ||
|
||||
state.matches('Sketch.Rectangle tool')
|
||||
) {
|
||||
cursor = 'crosshair'
|
||||
} else {
|
||||
@ -110,9 +105,9 @@ export const ClientSideScene = ({
|
||||
style={{ cursor: cursor }}
|
||||
className={`absolute inset-0 h-full w-full transition-all duration-300 ${
|
||||
hideClient ? 'opacity-0' : 'opacity-100'
|
||||
} ${hideServer ? 'bg-black' : ''} ${
|
||||
} ${hideServer ? 'bg-chalkboard-10 dark:bg-chalkboard-100' : ''} ${
|
||||
!hideClient && !hideServer && state.matches('Sketch')
|
||||
? 'bg-black/80'
|
||||
? 'bg-chalkboard-10/80 dark:bg-chalkboard-100/80'
|
||||
: ''
|
||||
}`}
|
||||
></div>
|
||||
|
@ -57,6 +57,7 @@ import {
|
||||
kclManager,
|
||||
sceneInfra,
|
||||
codeManager,
|
||||
editorManager,
|
||||
} from 'lib/singletons'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { executeAst, useStore } from 'useStore'
|
||||
@ -96,6 +97,7 @@ import {
|
||||
getRectangleCallExpressions,
|
||||
updateRectangleSketch,
|
||||
} from 'lib/rectangleTool'
|
||||
import { getThemeColorForThreeJs } from 'lib/theme'
|
||||
|
||||
type DraftSegment = 'line' | 'tangentialArcTo'
|
||||
|
||||
@ -214,8 +216,9 @@ export class SceneEntities {
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
const baseXColor = 0x000055
|
||||
const baseYColor = 0x550000
|
||||
const xAxisGeometry = new BoxGeometry(100000, 0.3, 0.01)
|
||||
const yAxisGeometry = new BoxGeometry(0.3, 100000, 0.01)
|
||||
const axisPixelWidth = 1.6
|
||||
const xAxisGeometry = new BoxGeometry(100000, axisPixelWidth, 0.01)
|
||||
const yAxisGeometry = new BoxGeometry(axisPixelWidth, 100000, 0.01)
|
||||
const xAxisMaterial = new MeshBasicMaterial({
|
||||
color: baseXColor,
|
||||
depthTest: false,
|
||||
@ -354,6 +357,7 @@ export class SceneEntities {
|
||||
id: sketchGroup.start.__geoMeta.id,
|
||||
pathToNode: segPathToNode,
|
||||
scale: factor,
|
||||
theme: sceneInfra._theme,
|
||||
})
|
||||
_profileStart.layers.set(SKETCH_LAYER)
|
||||
_profileStart.traverse((child) => {
|
||||
@ -404,6 +408,7 @@ export class SceneEntities {
|
||||
isDraftSegment,
|
||||
scale: factor,
|
||||
texture: sceneInfra.extraSegmentTexture,
|
||||
theme: sceneInfra._theme,
|
||||
})
|
||||
} else {
|
||||
seg = straightSegment({
|
||||
@ -415,6 +420,7 @@ export class SceneEntities {
|
||||
scale: factor,
|
||||
callExpName,
|
||||
texture: sceneInfra.extraSegmentTexture,
|
||||
theme: sceneInfra._theme,
|
||||
})
|
||||
}
|
||||
seg.layers.set(SKETCH_LAYER)
|
||||
@ -962,7 +968,7 @@ export class SceneEntities {
|
||||
if (!draftInfo)
|
||||
// don't want to mod the user's code yet as they have't committed to the change yet
|
||||
// plus this would be the truncated ast being recast, it would be wrong
|
||||
codeManager.updateCodeStateEditor(code)
|
||||
codeManager.updateCodeEditor(code)
|
||||
const { programMemory } = await executeAst({
|
||||
ast: truncatedAst,
|
||||
useFakeExecutor: true,
|
||||
@ -1323,30 +1329,31 @@ export class SceneEntities {
|
||||
selected.material.color = defaultPlaneColor(type)
|
||||
},
|
||||
onClick: async (args) => {
|
||||
const checkExtrudeFaceClick = async (): Promise<boolean> => {
|
||||
const checkExtrudeFaceClick = async (): Promise<
|
||||
['face' | 'plane' | 'other', string]
|
||||
> => {
|
||||
const { streamDimensions } = useStore.getState()
|
||||
const { entity_id } = await sendSelectEventToEngine(
|
||||
args?.mouseEvent,
|
||||
document.getElementById('video-stream') as HTMLVideoElement,
|
||||
streamDimensions
|
||||
)
|
||||
if (!entity_id) return false
|
||||
if (!entity_id) return ['other', '']
|
||||
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]
|
||||
if (artifact?.commandType !== 'solid3d_get_extrusion_face_info')
|
||||
return false
|
||||
const faceInfo: Models['FaceIsPlanar_type'] = (
|
||||
await this.engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'face_is_planar',
|
||||
object_id: entity_id,
|
||||
},
|
||||
})
|
||||
)?.data?.data
|
||||
return ['other', entity_id]
|
||||
|
||||
const faceInfo = await getFaceDetails(entity_id)
|
||||
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
||||
return false
|
||||
const { z_axis, origin, y_axis } = faceInfo
|
||||
return ['other', entity_id]
|
||||
const { z_axis, y_axis, origin } = faceInfo
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
artifact.range
|
||||
@ -1366,12 +1373,15 @@ export class SceneEntities {
|
||||
artifact?.additionalData?.type === 'cap'
|
||||
? artifact.additionalData.info
|
||||
: 'none',
|
||||
faceId: entity_id,
|
||||
},
|
||||
})
|
||||
return true
|
||||
return ['face', entity_id]
|
||||
}
|
||||
|
||||
if (await checkExtrudeFaceClick()) return
|
||||
const faceResult = await checkExtrudeFaceClick()
|
||||
console.log('faceResult', faceResult)
|
||||
if (faceResult[0] === 'face') return
|
||||
|
||||
if (!args || !args.intersects?.[0]) return
|
||||
if (args.mouseEvent.which !== 1) return
|
||||
@ -1397,6 +1407,7 @@ export class SceneEntities {
|
||||
plane: planeString,
|
||||
zAxis,
|
||||
yAxis,
|
||||
planeId: faceResult[1],
|
||||
},
|
||||
})
|
||||
},
|
||||
@ -1423,7 +1434,7 @@ export class SceneEntities {
|
||||
parent.userData.pathToNode,
|
||||
'CallExpression'
|
||||
).node
|
||||
sceneInfra.highlightCallback([node.start, node.end])
|
||||
editorManager.setHighlightRange([node.start, node.end])
|
||||
const yellow = 0xffff00
|
||||
colorSegment(selected, yellow)
|
||||
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||
@ -1459,10 +1470,10 @@ export class SceneEntities {
|
||||
}
|
||||
return
|
||||
}
|
||||
sceneInfra.highlightCallback([0, 0])
|
||||
editorManager.setHighlightRange([0, 0])
|
||||
},
|
||||
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
|
||||
sceneInfra.highlightCallback([0, 0])
|
||||
editorManager.setHighlightRange([0, 0])
|
||||
const parent = getParentGroup(selected, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
@ -1496,7 +1507,10 @@ export class SceneEntities {
|
||||
const isSelected = parent?.userData?.isSelected
|
||||
colorSegment(
|
||||
selected,
|
||||
isSelected ? 0x0000ff : parent?.userData?.baseColor || 0xffffff
|
||||
isSelected
|
||||
? 0x0000ff
|
||||
: parent?.userData?.baseColor ||
|
||||
getThemeColorForThreeJs(sceneInfra._theme)
|
||||
)
|
||||
const extraSegmentGroup = parent?.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||
if (extraSegmentGroup) {
|
||||
@ -1680,7 +1694,7 @@ export async function getSketchOrientationDetails(
|
||||
sketchPathToNode: PathToNode
|
||||
): Promise<{
|
||||
quat: Quaternion
|
||||
sketchDetails: SketchDetails
|
||||
sketchDetails: SketchDetails & { faceId?: string }
|
||||
}> {
|
||||
const sketchGroup = sketchGroupFromPathToNode({
|
||||
pathToNode: sketchPathToNode,
|
||||
@ -1696,20 +1710,13 @@ export async function getSketchOrientationDetails(
|
||||
zAxis: [zAxis.x, zAxis.y, zAxis.z],
|
||||
yAxis: [sketchGroup.yAxis.x, sketchGroup.yAxis.y, sketchGroup.yAxis.z],
|
||||
origin: [0, 0, 0],
|
||||
faceId: sketchGroup.on.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
if (sketchGroup.on.type === 'face') {
|
||||
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
|
||||
const faceInfo = await getFaceDetails(sketchGroup.on.faceId)
|
||||
|
||||
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
||||
throw new Error('faceInfo')
|
||||
const { z_axis, y_axis, origin } = faceInfo
|
||||
@ -1724,6 +1731,7 @@ export async function getSketchOrientationDetails(
|
||||
zAxis: [z_axis.x, z_axis.y, z_axis.z],
|
||||
yAxis: [y_axis.x, y_axis.y, y_axis.z],
|
||||
origin: [origin.x, origin.y, origin.z],
|
||||
faceId: sketchGroup.on.faceId,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1732,6 +1740,46 @@ 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 {
|
||||
const dummyCam = new PerspectiveCamera()
|
||||
dummyCam.up.set(0, 0, 1)
|
||||
|
@ -24,13 +24,13 @@ import {
|
||||
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
import { SourceRange } from 'lang/wasm'
|
||||
import { Axis } from 'lib/selections'
|
||||
import { type BaseUnit } from 'lib/settings/settingsTypes'
|
||||
import { CameraControls } from './CameraControls'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import { settings } from 'lib/settings/initialSettings'
|
||||
import { MouseState } from 'machines/modelingMachine'
|
||||
import { Themes } from 'lib/theme'
|
||||
|
||||
type SendType = ReturnType<typeof useModelingContext>['send']
|
||||
|
||||
@ -102,6 +102,7 @@ export class SceneInfra {
|
||||
isFovAnimationInProgress = false
|
||||
_baseUnit: BaseUnit = 'mm'
|
||||
_baseUnitMultiplier = 1
|
||||
_theme: Themes = Themes.System
|
||||
extraSegmentTexture: Texture
|
||||
lastMouseState: MouseState = { type: 'idle' }
|
||||
onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||
@ -138,6 +139,9 @@ export class SceneInfra {
|
||||
this._baseUnitMultiplier
|
||||
)
|
||||
}
|
||||
set theme(theme: Themes) {
|
||||
this._theme = theme
|
||||
}
|
||||
resetMouseListeners = () => {
|
||||
this.setCallbacks({
|
||||
onDragStart: () => {},
|
||||
@ -149,10 +153,6 @@ export class SceneInfra {
|
||||
onMouseLeave: () => {},
|
||||
})
|
||||
}
|
||||
highlightCallback: (a: SourceRange) => void = () => {}
|
||||
setHighlightCallback(cb: (a: SourceRange) => void) {
|
||||
this.highlightCallback = cb
|
||||
}
|
||||
|
||||
modelingSend: SendType = (() => {}) as any
|
||||
setSend(send: SendType) {
|
||||
|