Compare commits

...

4 Commits

Author SHA1 Message Date
da88ffaf49 wait while electron boots 2024-08-13 21:25:48 +10:00
ed6c4397f2 fix tests 2024-08-13 20:51:17 +10:00
7c841b999d fix 2024-08-13 20:41:22 +10:00
4492845bcb Get electron building something at all 2024-08-13 20:38:18 +10:00
146 changed files with 5306 additions and 5080 deletions

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas,.yarn.lock,**/yarn.lock

View File

@ -0,0 +1,404 @@
name: build-test-publish-apps
on:
pull_request:
push:
release:
types: [published]
schedule:
- cron: '0 4 * * *'
# Daily at 04:00 AM UTC
# Will checkout the last commit from the default branch (main as of 2023-10-04)
env:
CUT_RELEASE_PR: ${{ 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 }}
cancel-in-progress: true
jobs:
prepare-json-files:
runs-on: ubuntu-22.04 # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs:
version: ${{ steps.export_version.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Set nightly version
if: github.event_name == 'schedule'
run: |
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
# TODO: see if we need to inject updater nightly URL here https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json
# TODO: see if we ned to add updater test URL here https://dl.zoo.dev/releases/modeling-app/updater-test/last_update.json
- uses: actions/upload-artifact@v3
if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }}
with:
path: |
package.json
- id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
build-test-app-macos:
needs: [prepare-json-files]
runs-on: macos-14
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
if: github.event_name == 'schedule'
- name: Copy updated .json files
if: github.event_name == 'schedule'
run: |
ls -l artifact
cp artifact/package.json package.json
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn' # Set this to npm, yarn or pnpm.
- run: yarn install
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Run build:wasm
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
# TODO: sign the app (and updater bundle potentially)
- name: Add signing certificate
if: ${{ env.BUILD_RELEASE == 'true' }}
run: chmod +x add-osx-cert.sh && ./add-osx-cert.sh
- name: Build the app for arm64
run: "yarn electron-forge make"
- name: Build the app for x64
run: "yarn electron-forge make --arch x64"
- name: List artifacts
run: "ls -R out/make"
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
- uses: actions/upload-artifact@v3
with:
path: "out/make/*/*/*/*"
build-test-app-windows:
needs: [prepare-json-files]
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
- name: Copy updated .json files
if: github.event_name == 'schedule'
run: |
ls -l artifact
cp artifact/package.json package.json
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn' # Set this to npm, yarn or pnpm.
- run: yarn install
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Run build:wasm manually
shell: bash
env:
MODE: ${{ env.BUILD_RELEASE == 'true' && '--release' || '--debug' }}
run: |
mkdir src/wasm-lib/pkg; cd src/wasm-lib
echo "building with ${{ env.MODE }}"
npx wasm-pack build --target web --out-dir pkg ${{ env.MODE }}
cd ../../
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
- name: Prepare certificate and variables (Windows only)
if: ${{ env.BUILD_RELEASE == 'true' }}
run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
shell: bash
- name: Setup certicate with SSM KSP (Windows only)
if: ${{ env.BUILD_RELEASE == 'true' }}
run: |
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
msiexec /i smtools-windows-x64.msi /quiet /qn
smksp_registrar.exe list
smctl.exe keypair ls
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
shell: cmd
- name: Build the app for x64
run: "yarn electron-forge make --arch x64"
- name: Build the app for arm64
run: "yarn electron-forge make --arch arm64"
- name: List artifacts
run: "ls -R out/make"
- name: Sign using Signtool
if: ${{ env.BUILD_RELEASE == 'true' }}
env:
THUMBPRINT: "F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D"
X64_FILE: "D:\\a\\modeling-app\\modeling-app\\out\\make\\squirrel.windows\\x64\\Zoo Modeling App-*Setup.exe"
ARM64_FILE: "D:\\a\\modeling-app\\modeling-app\\out\\make\\squirrel.windows\\arm64\\Zoo Modeling App-*Setup.exe"
run: |
signtool.exe sign /sha1 ${{ env.THUMBPRINT }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "${{ env.X64_FILE }}"
signtool.exe verify /v /pa "${{ env.X64_FILE }}"
signtool.exe sign /sha1 ${{ env.THUMBPRINT }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 "${{ env.ARM64_FILE }}"
signtool.exe verify /v /pa "${{ env.ARM64_FILE }}"
- uses: actions/upload-artifact@v3
with:
path: "out/make/*/*/*"
# TODO: Run e2e tests
build-test-app-ubuntu:
needs: [prepare-json-files]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
if: github.event_name == 'schedule'
- name: Copy updated .json files
if: github.event_name == 'schedule'
run: |
ls -l artifact
cp artifact/package.json package.json
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn' # Set this to npm, yarn or pnpm.
- run: yarn install
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Run build:wasm
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
- name: Build the app for arm64
run: "yarn electron-forge make --arch arm64"
- name: Build the app for x64
run: "yarn electron-forge make --arch x64"
- name: List artifacts
run: "ls -R out/make"
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
# TODO: sign the app (and updater bundle potentially)
- uses: actions/upload-artifact@v3
with:
path: "out/make/*/*/*"
publish-apps-release:
runs-on: ubuntu-22.04
permissions:
contents: write
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
needs: [prepare-json-files, build-test-app-macos, build-test-app-windows, build-test-app-ubuntu]
env:
VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }}
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }}
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
steps:
- uses: actions/download-artifact@v3
- name: Generate the update static endpoint
run: |
ls -l artifact/*/*oo*
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
WINDOWS_X86_64_SIG=`cat artifact/msi/*x64*.msi.zip.sig`
WINDOWS_AARCH64_SIG=`cat artifact/msi/*arm64*.msi.zip.sig`
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \
--arg darwin_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}.app.tar.gz" \
--arg windows_x86_64_sig "$WINDOWS_X86_64_SIG" \
--arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi.zip" \
--arg windows_aarch64_sig "$WINDOWS_AARCH64_SIG" \
--arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi.zip" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"darwin-x86_64": {
"signature": $darwin_sig,
"url": $darwin_url
},
"darwin-aarch64": {
"signature": $darwin_sig,
"url": $darwin_url
},
"windows-x86_64": {
"signature": $windows_x86_64_sig,
"url": $windows_x86_64_url
},
"windows-aarch64": {
"signature": $windows_aarch64_sig,
"url": $windows_aarch64_url
}
}
}' > last_update.json
cat last_update.json
- name: Generate the download static endpoint
run: |
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_universal.dmg" \
--arg windows_x86_64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi" \
--arg windows_aarch64_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_arm64_en-US.msi" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"dmg-universal": {
"url": $darwin_url
},
"msi-x86_64": {
"url": $windows_x86_64_url
},
"msi-aarch64": {
"url": $windows_aarch64_url
}
}
}' > last_download.json
cat last_download.json
- name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.1.3'
with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@v2.1.0
with:
project_id: kittycadapi
- name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.0
with:
path: artifact
glob: '*/Zoo*'
parent: false
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
- name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.0
with:
path: last_update.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.0
with:
path: last_download.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload release files to Github
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2
with:
files: 'artifact/*/Zoo*'
announce_release:
needs: [publish-apps-release]
runs-on: ubuntu-22.04
if: github.event_name == 'release'
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Announce Release
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
RELEASE_VERSION: ${{ github.event.release.tag_name }}
RELEASE_BODY: ${{ github.event.release.body}}
run: python public/announce_release.py

76
.github/workflows/build-test-web.yml vendored Normal file
View File

@ -0,0 +1,76 @@
name: build-test-web
on:
pull_request:
push:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
check-format:
runs-on: 'ubuntu-22.04'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- run: yarn fmt-check
check-types:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn xstate:typegen
- run: yarn tsc
check-typos:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
- name: Install codespell
run: |
python -m pip install codespell
- name: Run codespell
run: codespell --config .codespellrc # Edit this file to tweak the typo list and other configuration.
build-test-web:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn simpleserver:ci
- run: yarn test:nowatch

View File

@ -1,586 +0,0 @@
name: CI
on:
pull_request:
push:
branches:
- main
release:
types: [published]
schedule:
- cron: '0 4 * * *'
# Daily at 04:00 AM UTC
# Will checkout the last commit from the default branch (main as of 2023-10-04)
env:
CUT_RELEASE_PR: ${{ 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 }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
actions: read
jobs:
check-format:
runs-on: 'ubuntu-latest'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- run: yarn fmt-check
check-types:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn xstate:typegen
- run: yarn tsc
check-typos:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
- name: Install codespell
run: |
python -m pip install codespell
- name: Run codespell
run: codespell --config .codespellrc # Edit this file to tweak the typo list and other configuration.
build-test-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn simpleserver:ci
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
- name: Install Chromium Browser
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: yarn playwright install chromium --with-deps
- name: run unit tests
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: yarn test:nowatch
env:
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: check for changes
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
id: git-check
run: |
git add src/lang/std/artifactMapGraphs
if git status src/lang/std/artifactMapGraphs | grep -q "Changes to be committed"
then echo "modified=true" >> $GITHUB_OUTPUT
else echo "modified=false" >> $GITHUB_OUTPUT
fi
- name: Commit changes, if any
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' && steps.git-check.outputs.modified == 'true' }}
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git fetch origin
echo ${{ github.head_ref }}
git checkout ${{ github.head_ref }}
# TODO when webkit works on ubuntu remove the os part of the commit message
git commit -am "Look at this (photo)Graph *in the voice of Nickelback*" || true
git push
git push origin ${{ github.head_ref }}
prepare-json-files:
runs-on: ubuntu-latest # seperate job on Ubuntu for easy string manipulations (compared to Windows)
outputs:
version: ${{ steps.export_version.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Set nightly version
if: github.event_name == 'schedule'
run: |
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json' \
'.plugins.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
echo "$(jq --arg id 'dev.zoo.modeling-app-nightly' \
'.identifier=$id' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
echo "$(jq --arg name 'Zoo Modeling App (Nightly)' \
'.productName=$name' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- name: Set updater test version
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
echo "$(jq --arg url 'https://dl.zoo.dev/releases/modeling-app/test/last_update.json' \
'.plugins.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
- uses: actions/upload-artifact@v3
if: ${{ github.event_name == 'schedule' || env.CUT_RELEASE_PR == 'true' }}
with:
path: |
package.json
src-tauri/tauri.conf.json
src-tauri/tauri.release.conf.json
- id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
build-test-apps:
needs: [prepare-json-files]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
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
- uses: actions/download-artifact@v3
if: github.event_name == 'schedule'
- name: Copy updated .json files
if: github.event_name == 'schedule'
run: |
ls -l artifact
cp artifact/package.json package.json
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
- name: Update WebView2 on Windows
if: matrix.os == 'windows-latest'
# Workaround needed to build the tauri windows app with matching edge version.
# From https://github.com/actions/runner-images/issues/9538
run: |
Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'setup.exe'
Start-Process -FilePath setup.exe -Verb RunAs -Wait
- 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
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn' # Set this to npm, yarn or pnpm.
- run: yarn install
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Run build:wasm manually
shell: bash
env:
MODE: ${{ env.BUILD_RELEASE == 'true' && '--release' || '--debug' }}
run: |
mkdir src/wasm-lib/pkg; cd src/wasm-lib
echo "building with ${{ env.MODE }}"
npx wasm-pack build --target web --out-dir pkg ${{ env.MODE }}
cd ../../
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
- name: Run vite build (build:both)
run: yarn vite build --mode ${{ env.BUILD_RELEASE == 'true' && 'production' || 'development' }}
- name: Fix format
run: yarn fmt
- name: Install x86 target for Universal builds (MacOS only)
if: matrix.os == 'macos-14'
run: |
rustup target add x86_64-apple-darwin
- name: Prepare certificate and variables (Windows only)
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /d/Certificate_pkcs12.p12
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
shell: bash
- name: Setup certicate with SSM KSP (Windows only)
if: ${{ matrix.os == 'windows-latest' && env.BUILD_RELEASE == 'true' }}
run: |
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
msiexec /i smtools-windows-x64.msi /quiet /qn
smksp_registrar.exe list
smctl.exe keypair ls
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
shell: cmd
- name: Build the app (debug)
if: ${{ env.BUILD_RELEASE == 'false' }}
run: "yarn tauri build --debug ${{ 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
if: ${{ env.BUILD_RELEASE == 'true' }}
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_CONF_ARGS: "--config ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
- uses: actions/upload-artifact@v3
if: matrix.os != 'ubuntu-latest'
env:
PREFIX: ${{ matrix.os == 'macos-14' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
with:
path: "${{ env.PREFIX }}/${{ env.MODE }}/bundle/*/*"
- name: Run e2e tests (linux only)
if: ${{ matrix.os == 'ubuntu-latest' && github.event_name != 'release' && github.event_name != 'schedule' }}
run: |
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
env:
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/zoo-modeling-app"
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
- name: Run e2e tests (windows only)
if: ${{ matrix.os == 'windows-latest' && github.event_name != 'release' && github.event_name != 'schedule' }}
run: |
cargo install tauri-driver --force
yarn wdio run wdio.conf.ts
env:
E2E_APPLICATION: ".\\src-tauri\\target\\${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}\\Zoo Modeling App.exe"
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_API_BASE_URL: ${{ env.BUILD_RELEASE == 'true' && 'https://api.zoo.dev' || 'https://api.dev.zoo.dev' }}
E2E_TAURI_ENABLED: true
TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}'
- uses: actions/download-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
- name: Copy updated .json file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
ls -l artifact
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
cat src-tauri/tauri.release.conf.json
- name: Build the app (release, updater test)
if: ${{ env.CUT_RELEASE_PR == 'true' && matrix.os != 'ubuntu-latest' }}
env:
TAURI_CONF_ARGS: "-c ${{ matrix.os == 'windows-latest' && 'src-tauri\\tauri.release.conf.json' || 'src-tauri/tauri.release.conf.json' }}"
TAURI_BUNDLE_ARGS: "-b ${{ matrix.os == 'windows-latest' && 'msi' || 'dmg' }}"
run: "yarn tauri build ${{ env.TAURI_CONF_ARGS }} ${{ env.TAURI_BUNDLE_ARGS }} ${{ env.TAURI_ARGS_MACOS }}"
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' && matrix.os != 'ubuntu-latest' }}
with:
path: "${{ matrix.os == 'macos-14' && 'src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg' || 'src-tauri/target/release/bundle/msi/*.msi' }}"
name: updater-test
publish-apps-release:
permissions:
contents: write
runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
needs: [check-format, check-types, check-typos, build-test-web, prepare-json-files, build-test-apps]
env:
VERSION_NO_V: ${{ needs.prepare-json-files.outputs.version }}
VERSION: ${{ github.event_name == 'release' && format('v{0}', needs.prepare-json-files.outputs.version) || needs.prepare-json-files.outputs.version }}
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
WEBSITE_DIR: ${{ github.event_name == 'release' && 'dl.zoo.dev/releases/modeling-app' || 'dl.zoo.dev/releases/modeling-app/nightly' }}
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
steps:
- uses: actions/download-artifact@v3
- name: Generate the update static endpoint
run: |
ls -l artifact/*/*oo*
DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig`
WINDOWS_SIG=`cat artifact/msi/*.msi.zip.sig`
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_sig "$DARWIN_SIG" \
--arg darwin_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}.app.tar.gz" \
--arg windows_sig "$WINDOWS_SIG" \
--arg windows_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi.zip" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"darwin-x86_64": {
"signature": $darwin_sig,
"url": $darwin_url
},
"darwin-aarch64": {
"signature": $darwin_sig,
"url": $darwin_url
},
"windows-x86_64": {
"signature": $windows_sig,
"url": $windows_url
}
}
}' > last_update.json
cat last_update.json
- name: Generate the download static endpoint
run: |
RELEASE_DIR=https://${WEBSITE_DIR}/${VERSION}
jq --null-input \
--arg version "${VERSION}" \
--arg pub_date "${PUB_DATE}" \
--arg notes "${NOTES}" \
--arg darwin_url "$RELEASE_DIR/dmg/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_universal.dmg" \
--arg windows_url "$RELEASE_DIR/msi/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64_en-US.msi" \
'{
"version": $version,
"pub_date": $pub_date,
"notes": $notes,
"platforms": {
"dmg-universal": {
"url": $darwin_url
},
"msi-x86_64": {
"url": $windows_url
}
}
}' > last_download.json
cat last_download.json
- name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v2.1.3'
with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@v2.1.0
with:
project_id: kittycadapi
- name: Upload release files to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: artifact
glob: '*/Zoo*'
parent: false
destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
- name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: last_update.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.1.1
with:
path: last_download.json
destination: ${{ env.BUCKET_DIR }}
- name: Upload release files to Github
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2
with:
files: 'artifact/*/Zoo*'
announce_release:
needs: [publish-apps-release]
runs-on: ubuntu-latest
if: github.event_name == 'release'
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Announce Release
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
RELEASE_VERSION: ${{ github.event.release.tag_name }}
RELEASE_BODY: ${{ github.event.release.body}}
run: python public/announce_release.py

View File

@ -171,7 +171,7 @@ jobs:
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert=@snapshot || true
yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert="@snapshot|@electron" || true
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
@ -186,7 +186,7 @@ jobs:
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --last-failed --grep-invert=@snapshot || true
yarn playwright test --project="Google Chrome" --config=playwright.ci.config.ts --last-failed --grep-invert="@snapshot|@electron" || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
@ -233,6 +233,7 @@ jobs:
retention-days: 30
overwrite: true
playwright-macos:
timeout-minutes: 30
runs-on: macos-14
@ -325,7 +326,7 @@ jobs:
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
yarn playwright test --project="webkit" --config=playwright.ci.config.ts --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert=@snapshot || true
yarn playwright test --project="webkit" --config=playwright.ci.config.ts --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert="@snapshot|@electron" || true
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
@ -340,7 +341,7 @@ jobs:
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="webkit" --config=playwright.ci.config.ts --last-failed --grep-invert=@snapshot || true
yarn playwright test --project="webkit" --config=playwright.ci.config.ts --last-failed --grep-invert="@snapshot|@electron" || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
@ -381,3 +382,156 @@ jobs:
path: playwright-report/
retention-days: 30
overwrite: true
playwright-electron:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: check-rust-changes
steps:
- name: Tune GitHub-hosted runner network
uses: smorimoto/tune-github-hosted-runner-network@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- uses: KittyCAD/action-install-cli@main
- name: Install dependencies
run: yarn
- name: Cache Playwright Browsers
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright/
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
- name: Install Playwright Browsers
run: yarn playwright install chromium --with-deps
- name: Download Wasm Cache
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v6
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 (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- 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: Install vector
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev > /tmp/vector.sh
chmod +x /tmp/vector.sh
/tmp/vector.sh -y -no-modify-path
mkdir -p /tmp/vector
cp .github/workflows/vector.toml /tmp/vector.toml
sed -i "s#GITHUB_WORKFLOW#${GITHUB_WORKFLOW}#g" /tmp/vector.toml
sed -i "s#GITHUB_REPOSITORY#${GITHUB_REPOSITORY}#g" /tmp/vector.toml
sed -i "s#GITHUB_SHA#${GITHUB_SHA}#g" /tmp/vector.toml
sed -i "s#GITHUB_REF_NAME#${GITHUB_REF_NAME}#g" /tmp/vector.toml
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
cat /tmp/vector.toml
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
- 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
- uses: actions/download-artifact@v4
if: always()
continue-on-error: true
with:
name: test-results-ubuntu-${{ github.sha }}
path: test-results/
- name: run electron
run: |
yarn electron:start > electron.log 2>&1 &
while ! grep -q "built in" electron.log; do
sleep 1
done
- name: Run ubuntu/chrome flow (with retries)
id: retry
if: always()
run: |
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
yarn playwright test --project="Google Chrome" --grep=@electron || true
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
retry=1
max_retrys=4
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="Google Chrome" --last-failed --grep=@electron || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
else
echo "retried=false" >>$GITHUB_OUTPUT
exit 0
fi
done
echo "retried=false" >>$GITHUB_OUTPUT
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
# if it still fails after 3 retrys, then fail the job
exit 1
fi
fi
exit 0
env:
CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: send to axiom
if: always()
shell: bash
run: |
node playwrightProcess.mjs | tee /tmp/github-actions.log
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-electron-${{ github.sha }}
path: test-results/
retention-days: 30
overwrite: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-electron-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true

7
.gitignore vendored
View File

@ -62,3 +62,10 @@ Mac_App_Distribution.provisionprofile
*.tsbuildinfo
venv
.vite/
# electron
out/
src-tauri/target
electron-test-projects-dir

View File

@ -89,26 +89,19 @@ enable third-party cookies. You can enable third-party cookies by clicking on
the eye with a slash through it in the URL bar, and clicking on "Enable
Third-Party Cookies".
## Tauri
## Desktop
To spin up up tauri dev, `yarn install` and `yarn build:wasm-dev` need to have been done before hand then
To spin up the desktop app, `yarn install` and `yarn build:wasm-dev` need to have been done before hand then
```
yarn tauri dev
yarn electron:start
```
Will spin up the web app before opening up the tauri dev desktop app. Note that it's probably a good idea to close the browser tab that gets opened since at the time of writing they can conflict.
This will start the application and hot-reload on changed.
The dev instance automatically opens up the browser devtools which can be disabled by [commenting it out](https://github.com/KittyCAD/modeling-app/blob/main/src-tauri/src/main.rs#L92.)
Devtools can be opened with the usual Cmd/Ctrl-Shift-I.
To build, run `yarn tauri build`, or `yarn tauri build --debug` to keep access to the devtools.
Note that these became separate apps on Macos, so make sure you open the right one after a build 😉
![image](https://github.com/KittyCAD/modeling-app/assets/29681384/a08762c5-8d16-42d8-a02f-a5efc9ae5551)
<img width="1232" alt="image" src="https://user-images.githubusercontent.com/29681384/211947063-46164bb4-7bdd-45cb-9a76-2f40c71a24aa.png">
<img width="1232" alt="image (1)" src="https://user-images.githubusercontent.com/29681384/211947073-e76b4933-bef5-4636-bc4d-e930ac8e290f.png">
To build, run `yarn electron:package`.
## Checking out commits / Bisecting

24
add-osx-cert.sh Normal file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env sh
# From https://dev.to/rwwagner90/signing-electron-apps-with-github-actions-4cof
KEY_CHAIN=build.keychain
CERTIFICATE_P12=certificate.p12
# Recreate the certificate from the secure environment variable
echo $APPLE_CERTIFICATE | base64 --decode > $CERTIFICATE_P12
#create a keychain
security create-keychain -p actions $KEY_CHAIN
# Make the keychain the default so identities are found
security default-keychain -s $KEY_CHAIN
# Unlock the keychain
security unlock-keychain -p actions $KEY_CHAIN
security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $APPLE_CERTIFICATE_PASSWORD -T /usr/bin/codesign;
security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN
# remove certs
rm -fr *.p12

View File

@ -0,0 +1,108 @@
import { _electron as electron, test, expect } from '@playwright/test'
import { getUtils, tearDown } from './test-utils'
import fs from 'node:fs'
import { secrets } from './secrets'
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test(
'When the project folder is empty, user can create new project and open it.',
{ tag: '@electron' },
async () => {
// create or otherwise clear the folder ./electron-test-projects-dir
const fileName = './electron-test-projects-dir'
try {
fs.rmdirSync(fileName, { recursive: true })
} catch (e) {
console.error(e)
}
fs.mkdirSync(fileName)
// get full path for ./electron-test-projects-dir
const fullPath = fs.realpathSync(fileName)
const electronApp = await electron.launch({
args: ['.'],
})
await electronApp.evaluate(async ({ app }) => {
return app.getAppPath()
})
const page = await electronApp.firstWindow()
// Set local storage directly using evaluate
await page.evaluate(
(token) => localStorage.setItem('TOKEN_PERSIST_KEY', token),
secrets.token
)
await page.evaluate(
(fullPath) =>
localStorage.setItem(
'APP_SETTINGS_OVERRIDE',
JSON.stringify({
projectDirectory: fullPath,
})
),
fullPath
)
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
// expect to see text "No Projects found"
await expect(page.getByText('No Projects found')).toBeVisible()
await page.getByRole('button', { name: 'New project' }).click()
await expect(page.getByText('Successfully created')).toBeVisible()
await expect(page.getByText('Successfully created')).not.toBeVisible()
await expect(page.getByText('project-000')).toBeVisible()
await page.getByText('project-000').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
await page.locator('.cm-content')
.fill(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([-87.4, 282.92], %)
|> line([324.07, 27.199], %, $seg01)
|> line([118.328, -291.754], %)
|> line([-180.04, -202.08], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(200, sketch001)`)
const pointOnModel = { x: 660, y: 250 }
// check the model loaded by checking it's grey
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [132, 132, 132]), {
timeout: 10_000,
})
.toBeLessThan(10)
await page.mouse.click(pointOnModel.x, pointOnModel.y)
// check user can interact with model by checking it turns yellow
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [176, 180, 132]))
.toBeLessThan(10)
await electronApp.close()
}
)

View File

@ -1,155 +0,0 @@
import { browser, $, expect } from '@wdio/globals'
import fs from 'fs/promises'
import path from 'path'
import os from 'os'
import { click, setDatasetValue } from '../utils'
const isWin32 = os.platform() === 'win32'
const documentsDir = path.join(os.homedir(), 'Documents')
const userSettingsDir = path.join(
os.homedir(),
'.config',
'dev.zoo.modeling-app'
)
const defaultProjectDir = path.join(documentsDir, 'zoo-modeling-app-projects')
const newProjectDir = path.join(documentsDir, 'a-different-directory')
const tmp = process.env.TEMP || '/tmp'
const userCodeDir = path.join(tmp, 'kittycad_user_code')
describe('ZMA sign in flow', () => {
before(async () => {
// 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(userSettingsDir, { force: true, recursive: true })
await fs.mkdir(defaultProjectDir, { recursive: true })
await fs.mkdir(newProjectDir, { recursive: true })
})
it('opens the auth page and signs in', async () => {
const signInButton = await $('[data-testid="sign-in-button"]')
expect(await signInButton.getText()).toEqual('Sign in')
await click(signInButton)
await new Promise((resolve) => setTimeout(resolve, 2000))
// Get from main.rs
const userCode = await (await fs.readFile(userCodeDir)).toString()
console.log(`Found user code ${userCode}`)
// Device flow: verify
const token = process.env.KITTYCAD_API_TOKEN
const headers = {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}
const apiBaseUrl = process.env.VITE_KC_API_BASE_URL
const verifyUrl = `${apiBaseUrl}/oauth2/device/verify?user_code=${userCode}`
console.log(`GET ${verifyUrl}`)
const vr = await fetch(verifyUrl, { headers })
console.log(vr.status)
// Device flow: confirm
const confirmUrl = `${apiBaseUrl}/oauth2/device/confirm`
const data = JSON.stringify({ user_code: userCode })
console.log(`POST ${confirmUrl} ${data}`)
const cr = await fetch(confirmUrl, {
headers,
method: 'POST',
body: data,
})
console.log(cr.status)
// Now should be signed in
await new Promise((resolve) => setTimeout(resolve, 10000))
const newFileButton = await $('[data-testid="home-new-file"]')
expect(await newFileButton.getText()).toEqual('New project')
})
})
describe('ZMA authorized user flows', () => {
// Note: each flow below is intended to start *and* end from the home page
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
await click(menuButton)
const settingsButton = await $('[data-testid="user-settings"]')
await click(settingsButton)
const projectDirInput = await $('[data-testid="project-directory-input"]')
expect(await projectDirInput.getValue()).toEqual(defaultProjectDir)
/*
* We've set up the project directory input (in initialSettings.tsx)
* to be able to skip the folder selection dialog if data-testValue
* has a value, allowing us to test the input otherwise works.
*/
// TODO: understand why we need to force double \ on Windows
await setDatasetValue(
projectDirInput,
'testValue',
isWin32 ? newProjectDir.replaceAll('\\', '\\\\') : newProjectDir
)
const projectDirButton = await $('[data-testid="project-directory-button"]')
await click(projectDirButton)
await new Promise((resolve) => setTimeout(resolve, 500))
// This line is broken. I need a different way to grab the toast
await expect(await $('div*=Set project directory to')).toBeDisplayed()
const nameInput = await $('[data-testid="projects-defaultProjectName"]')
expect(await nameInput.getValue()).toEqual('project-$nnn')
// Setting it back (for back to back local tests)
await new Promise((resolve) => setTimeout(resolve, 5000))
await setDatasetValue(
projectDirInput,
'testValue',
isWin32 ? defaultProjectDir.replaceAll('\\', '\\\\') : newProjectDir
)
await click(projectDirButton)
const closeButton = await $('[data-testid="settings-close-button"]')
await click(closeButton)
})
it('checks that no file exists, creates a new file', async () => {
const homeSection = await $('[data-testid="home-section"]')
expect(await homeSection.getText()).toContain('No Projects found')
const newFileButton = await $('[data-testid="home-new-file"]')
await click(newFileButton)
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(await homeSection.getText()).toContain('project-000')
})
it('opens the new file and expects a loading stream', async () => {
const projectLink = await $('[data-testid="project-link"]')
await click(projectLink)
if (isWin32) {
// TODO: actually do something to check that the stream is up
await new Promise((resolve) => setTimeout(resolve, 5000))
} else {
const errorText = await $('[data-testid="unexpected-error"]')
expect(await errorText.getText()).toContain('unexpected error')
}
const base = isWin32 ? 'http://tauri.localhost' : 'tauri://localhost'
await browser.execute(`window.location.href = "${base}/home"`)
})
})
describe('ZMA sign out flow', () => {
it('signs out', async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
const menuButton = await $('[data-testid="user-sidebar-toggle"]')
await click(menuButton)
const signoutButton = await $('[data-testid="user-sidebar-sign-out"]')
await click(signoutButton)
const newSignInButton = await $('[data-testid="sign-in-button"]')
expect(await newSignInButton.getText()).toEqual('Sign in')
})
})

View File

@ -1,18 +0,0 @@
import { browser } from '@wdio/globals'
export async function click(element: WebdriverIO.Element): Promise<void> {
// Workaround for .click(), see https://github.com/tauri-apps/tauri/issues/6541
await element.waitForClickable()
await browser.execute('arguments[0].click();', element)
}
/* Shoutout to @Sheap on Github for a great workaround utility:
* https://github.com/tauri-apps/tauri/issues/6541#issue-1638944060
*/
export async function setDatasetValue(
field: WebdriverIO.Element,
property: string,
value: string
) {
await browser.execute(`arguments[0].dataset.${property} = "${value}"`, field)
}

66
forge.config.ts Normal file
View File

@ -0,0 +1,66 @@
import type { ForgeConfig } from '@electron-forge/shared-types'
import { MakerSquirrel } from '@electron-forge/maker-squirrel'
import { MakerZIP } from '@electron-forge/maker-zip'
import { MakerDeb } from '@electron-forge/maker-deb'
import { MakerRpm } from '@electron-forge/maker-rpm'
import { VitePlugin } from '@electron-forge/plugin-vite'
import { FusesPlugin } from '@electron-forge/plugin-fuses'
import { FuseV1Options, FuseVersion } from '@electron/fuses'
const config: ForgeConfig = {
packagerConfig: {
asar: true,
osxSign: (process.env.BUILD_RELEASE === 'true' && {}) || undefined,
osxNotarize:
(process.env.BUILD_RELEASE === 'true' && {
appleId: process.env.APPLE_ID || '',
appleIdPassword: process.env.APPLE_PASSWORD || '',
teamId: process.env.APPLE_TEAM_ID || '',
}) ||
undefined,
executableName: 'zoo-modeling-app',
},
rebuildConfig: {},
makers: [
new MakerSquirrel({}),
new MakerZIP({}, ['darwin']),
new MakerRpm({}),
new MakerDeb({}),
],
plugins: [
new VitePlugin({
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
// If you are familiar with Vite configuration, it will look really familiar.
build: [
{
// `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
entry: 'src/main.ts',
config: 'vite.main.config.ts',
},
{
entry: 'src/preload.ts',
config: 'vite.preload.config.ts',
},
],
renderer: [
{
name: 'main_window',
config: 'vite.renderer.config.ts',
},
],
}),
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
}
export default config

35
forge.env.d.ts vendored Normal file
View File

@ -0,0 +1,35 @@
export {} // Make this a module
declare global {
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite
// plugin that tells the Electron app where to look for the Vite-bundled app code (depending on
// whether you're running in development or production).
const MAIN_WINDOW_VITE_DEV_SERVER_URL: string
const MAIN_WINDOW_VITE_NAME: string
namespace NodeJS {
interface Process {
// Used for hot reload after preload scripts.
viteDevServers: Record<string, import('vite').ViteDevServer>
}
}
type VitePluginConfig = ConstructorParameters<
typeof import('@electron-forge/plugin-vite').VitePlugin
>[0]
interface VitePluginRuntimeKeys {
VITE_DEV_SERVER_URL: `${string}_VITE_DEV_SERVER_URL`
VITE_NAME: `${string}_VITE_NAME`
}
}
declare module 'vite' {
interface ConfigEnv<
K extends keyof VitePluginConfig = keyof VitePluginConfig
> {
root: string
forgeConfig: VitePluginConfig
forgeConfigSelf: VitePluginConfig[K][number]
}
}

View File

@ -2,6 +2,10 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- PERPETUAL TODO reconsider this option.
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
-->
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />

46
interface.d.ts vendored Normal file
View File

@ -0,0 +1,46 @@
import fs from 'node:fs/promises'
import path from 'path'
import { dialog, shell } from 'electron'
import kittycad from '@kittycad/lib'
import { MachinesListing } from 'lib/machineManager'
export interface IElectronAPI {
open: typeof dialog.showOpenDialog
save: typeof dialog.showSaveDialog
openExternal: typeof shell.openExternal
showInFolder: typeof shell.showItemInFolder
login: (host: string) => Promise<string>
platform: typeof process.env.platform
arch: typeof process.env.arch
version: typeof process.env.version
readFile: (path: string) => ReturnType<fs.readFile>
writeFile: (
path: string,
data: string | Uint8Array
) => ReturnType<fs.writeFile>
readdir: (path: string) => ReturnType<fs.readdir>
getPath: (name: string) => Promise<string>
rm: typeof fs.rm
stat: (path: string) => ReturnType<fs.stat>
statIsDirectory: (path: string) => Promise<boolean>
path: typeof path
mkdir: typeof fs.mkdir
rename: (prev: string, next: string) => typeof fs.rename
packageJson: {
name: string
}
process: {
env: {
BASE_URL: (value?: string) => string
}
}
kittycad
listMachines: () => Promise<MachinesListing>
getMachineApiIp: () => Promise<string | null>
}
declare global {
interface Window {
electron: IElectronAPI
}
}

View File

@ -57,9 +57,8 @@ 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" '.version=$v' src-tauri/tauri.conf.json --indent 2)" > src-tauri/tauri.conf.json
git add package.json src-tauri/tauri.conf.json
git add package.json
git commit -m "Cut release $new_version"
echo ""

View File

@ -1,7 +1,16 @@
{
"name": "untitled-app",
"version": "0.24.10",
"private": true,
"name": "zoo-modeling-app",
"productName": "Zoo Modeling App",
"version": "0.24.10",
"author": {
"name": "Zoo Corporation",
"email": "info@zoo.dev",
"url": "https://zoo.dev"
},
"description": "Edit CAD visually or with code",
"main": ".vite/build/main.js",
"license": "none",
"dependencies": {
"@codemirror/autocomplete": "^6.17.0",
"@codemirror/commands": "^6.6.0",
@ -22,24 +31,19 @@
"@lezer/lr": "^1.4.1",
"@react-hook/resize-observer": "^2.0.1",
"@replit/codemirror-interact": "^6.3.1",
"@tauri-apps/api": "^2.0.0-beta.14",
"@tauri-apps/plugin-dialog": "^2.0.0-beta.6",
"@tauri-apps/plugin-fs": "^2.0.0-beta.6",
"@tauri-apps/plugin-http": "^2.0.0-beta.7",
"@tauri-apps/plugin-os": "^2.0.0-beta.6",
"@tauri-apps/plugin-process": "^2.0.0-beta.6",
"@tauri-apps/plugin-shell": "^2.0.0-beta.7",
"@tauri-apps/plugin-updater": "^2.0.0-beta.6",
"@ts-stack/markdown": "^1.5.0",
"@tweenjs/tween.js": "^23.1.1",
"@xstate/inspect": "^0.8.0",
"@xstate/react": "^3.2.2",
"bonjour-service": "^1.2.1",
"codemirror": "^6.0.1",
"decamelize": "^6.0.0",
"electron-squirrel-startup": "^1.0.1",
"fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.5",
"json-rpc-2.0": "^1.6.0",
"jszip": "^3.10.1",
"openid-client": "^5.6.5",
"re-resizable": "^6.9.11",
"react": "^18.3.1",
"react-dom": "^18.2.0",
@ -51,7 +55,6 @@
"react-router-dom": "^6.23.1",
"sketch-helpers": "^0.0.4",
"three": "^0.166.1",
"typescript": "^5.4.5",
"ua-parser-js": "^1.0.37",
"uuid": "^9.0.1",
"vscode-jsonrpc": "^8.2.1",
@ -72,8 +75,6 @@
"test": "vitest --mode development",
"test:nowatch": "vitest run --mode development",
"test:rust": "(cd src/wasm-lib && cargo test --all && cargo clippy --all --tests --benches)",
"test: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 *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
@ -88,7 +89,11 @@
"postinstall": "yarn xstate:typegen",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
"make:dev": "make dev",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts"
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"electron:start": "electron-forge start",
"electron:package": "electron-forge package",
"electron:make": "electron-forge make",
"electron:publish": "electron-forge publish"
},
"prettier": {
"trailingComma": "es5",
@ -110,14 +115,23 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.25.0",
"@babel/preset-env": "^7.24.3",
"@electron-forge/cli": "^7.4.0",
"@electron-forge/maker-deb": "^7.4.0",
"@electron-forge/maker-rpm": "^7.4.0",
"@electron-forge/maker-squirrel": "^7.4.0",
"@electron-forge/maker-zip": "^7.4.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
"@electron-forge/plugin-fuses": "^7.4.0",
"@electron-forge/plugin-vite": "^7.4.0",
"@electron/fuses": "^1.8.0",
"@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.1",
"@playwright/test": "^1.45.1",
"@tauri-apps/cli": "==2.0.0-beta.13",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^15.0.2",
"@types/d3-force": "^3.0.10",
"@types/electron": "^1.6.10",
"@types/mocha": "^10.0.6",
"@types/node": "^18.19.31",
"@types/pixelmatch": "^5.2.6",
@ -131,19 +145,17 @@
"@types/wait-on": "^5.3.4",
"@types/wicg-file-system-access": "^2023.10.5",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@vitejs/plugin-react": "^4.3.0",
"@vitest/web-worker": "^1.5.0",
"@wdio/cli": "^8.24.3",
"@wdio/globals": "^8.36.0",
"@wdio/local-runner": "^8.36.0",
"@wdio/mocha-framework": "^8.36.0",
"@wdio/spec-reporter": "^8.36.0",
"@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.19",
"d3-force": "^3.0.0",
"eslint": "^8.57.0",
"electron": "^31.2.1",
"eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.25.0",
"eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^14.3.10",
"http-server": "^14.1.1",
@ -156,7 +168,9 @@
"prettier": "^2.8.8",
"setimmediate": "^1.0.5",
"tailwindcss": "^3.4.1",
"vite": "^5.3.3",
"ts-node": "^10.0.0",
"typescript": "^5.0.0",
"vite": "^5.0.12",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2",

7
public/logo-blue.svg Normal file
View File

@ -0,0 +1,7 @@
<svg width="97" height="32" viewBox="0 0 97 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.3583 5.5893V0.690826H0.00311715V7.59893H17.5057L0.00625718 26.5774H0.00311715V26.5805L-2.28882e-05 26.5837L0.00311715 26.5868V31.2278H4.48709L9.00246 26.3293V31.2278H28.3576V24.3197H10.8582L28.3576 5.3381V0.690826L23.8705 0.697107L19.3583 5.5893Z" fill="#3C73FF"/>
<path d="M36.8417 16.0017C36.8417 10.987 40.9206 6.90811 45.9353 6.90811C47.7 6.90811 49.3485 7.41365 50.7427 8.28659L55.4904 3.17459C52.8214 1.18066 49.5149 0 45.9353 0C37.1118 0 29.9336 7.17815 29.9336 16.0017C29.9336 20.0021 31.4095 23.6665 33.8524 26.4769L38.6001 21.3649C37.4949 19.8608 36.8417 18.005 36.8417 16.0017Z" fill="#3C73FF"/>
<path d="M53.2739 10.6351C54.376 12.1423 55.0291 13.9981 55.0291 16.0014C55.0291 21.013 50.9502 25.0919 45.9356 25.0919C44.1709 25.0919 42.5255 24.5863 41.1314 23.7134L36.3805 28.8285C39.0495 30.8193 42.356 32 45.9356 32C54.7591 32 61.9372 24.8218 61.9372 16.0014C61.9372 11.9979 60.4614 8.33345 58.0185 5.5231L53.2739 10.6351Z" fill="#3C73FF"/>
<path d="M92.4988 5.5231L87.7542 10.6351C88.8564 12.1423 89.5095 13.9981 89.5095 16.0014C89.5095 21.013 85.4306 25.0919 80.416 25.0919C78.6513 25.0919 77.0059 24.5863 75.6117 23.7134L70.8608 28.8285C73.5299 30.8193 76.8363 32 80.416 32C89.2395 32 96.4176 24.8218 96.4176 16.0014C96.4176 11.9979 94.9418 8.33345 92.4988 5.5231Z" fill="#3C73FF"/>
<path d="M71.3225 16.0017C71.3225 10.987 75.4014 6.90811 80.416 6.90811C82.1807 6.90811 83.8292 7.41365 85.2234 8.28659L89.9711 3.17459C87.3021 1.18066 83.9957 0 80.416 0C71.5925 0 64.4144 7.17815 64.4144 16.0017C64.4144 20.0021 65.8902 23.6665 68.3332 26.4769L73.0809 21.3649C71.9756 19.8608 71.3225 18.005 71.3225 16.0017Z" fill="#3C73FF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

7
public/logo-white.svg Normal file
View File

@ -0,0 +1,7 @@
<svg width="97" height="32" viewBox="0 0 97 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.3583 5.5893V0.690826H0.00310952V7.59893H17.5057L0.00624955 26.5774H0.00310952V26.5805L-3.05176e-05 26.5837L0.00310952 26.5868V31.2278H4.48708L9.00245 26.3293V31.2278H28.3576V24.3197H10.8582L28.3576 5.3381V0.690826L23.8705 0.697107L19.3583 5.5893Z" fill="#FCFCFC"/>
<path d="M36.8417 16.0017C36.8417 10.987 40.9206 6.90811 45.9353 6.90811C47.7 6.90811 49.3485 7.41365 50.7427 8.28659L55.4904 3.17459C52.8214 1.18066 49.5149 0 45.9353 0C37.1118 0 29.9337 7.17815 29.9337 16.0017C29.9337 20.0021 31.4095 23.6665 33.8524 26.4769L38.6002 21.3649C37.4949 19.8608 36.8417 18.005 36.8417 16.0017Z" fill="#FCFCFC"/>
<path d="M53.2739 10.6351C54.376 12.1423 55.0291 13.9981 55.0291 16.0014C55.0291 21.013 50.9502 25.0919 45.9356 25.0919C44.1709 25.0919 42.5255 24.5863 41.1314 23.7134L36.3805 28.8285C39.0495 30.8193 42.356 32 45.9356 32C54.7591 32 61.9372 24.8218 61.9372 16.0014C61.9372 11.9979 60.4614 8.33345 58.0185 5.5231L53.2739 10.6351Z" fill="#FCFCFC"/>
<path d="M92.4988 5.5231L87.7542 10.6351C88.8564 12.1423 89.5095 13.9981 89.5095 16.0014C89.5095 21.013 85.4306 25.0919 80.416 25.0919C78.6513 25.0919 77.0059 24.5863 75.6117 23.7134L70.8608 28.8285C73.5299 30.8193 76.8363 32 80.416 32C89.2395 32 96.4176 24.8218 96.4176 16.0014C96.4176 11.9979 94.9418 8.33345 92.4988 5.5231Z" fill="#FCFCFC"/>
<path d="M71.3225 16.0017C71.3225 10.987 75.4014 6.90811 80.416 6.90811C82.1807 6.90811 83.8292 7.41365 85.2234 8.28659L89.9711 3.17459C87.3021 1.18066 83.9957 0 80.416 0C71.5925 0 64.4144 7.17815 64.4144 16.0017C64.4144 20.0021 65.8902 23.6665 68.3332 26.4769L73.0809 21.3649C71.9756 19.8608 71.3225 18.005 71.3225 16.0017Z" fill="#FCFCFC"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,3 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/

View File

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

View File

@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View File

@ -1,127 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Capability for the main window",
"context": "local",
"windows": [
"main"
],
"permissions": [
"cli:default",
"deep-link:default",
"log:default",
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"fs:allow-create",
"fs:allow-read-file",
"fs:allow-read-text-file",
"fs:allow-write-file",
"fs:allow-write-text-file",
"fs:allow-read-dir",
"fs:allow-copy-file",
"fs:allow-mkdir",
"fs:allow-remove",
"fs:allow-rename",
"fs:allow-exists",
"fs:allow-stat",
{
"identifier": "fs:scope",
"allow": [
{
"path": "$TEMP"
},
{
"path": "$TEMP/**/*"
},
{
"path": "$HOME"
},
{
"path": "$HOME/**/*"
},
{
"path": "$HOME/.config"
},
{
"path": "$HOME/.config/**/*"
},
{
"path": "$APPCONFIG"
},
{
"path": "$APPCONFIG/**/*"
},
{
"path": "$DOCUMENT"
},
{
"path": "$DOCUMENT/**/*"
}
]
},
"shell:allow-open",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "open",
"cmd": "open",
"args": [
"-R",
{
"validator": "\\S+"
}
],
"sidecar": false
},
{
"name": "explorer",
"cmd": "explorer",
"args": [
"/select",
{
"validator": "\\S+"
}
],
"sidecar": false
}
]
},
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message",
"dialog:allow-ask",
"dialog:allow-confirm",
{
"identifier": "http:default",
"allow": [
"https://dev.kittycad.io/*",
"https://dev.zoo.dev/*",
"https://kittycad.io/*",
"https://zoo.dev/*",
"https://api.dev.kittycad.io/*",
"https://api.dev.zoo.dev/*"
]
},
"os:allow-platform",
"os:allow-version",
"os:allow-os-type",
"os:allow-family",
"os:allow-arch",
"os:allow-exe-extension",
"os:allow-locale",
"os:allow-hostname",
"process:allow-restart",
"updater:default"
],
"platforms": [
"linux",
"macOS",
"windows"
]
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

View File

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

View File

@ -1,603 +0,0 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
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 tauri::{ipc::InvokeError, Manager};
use tauri_plugin_cli::CliExt;
use tauri_plugin_shell::ShellExt;
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";
#[tauri::command]
async fn rename_project_directory(project_path: &str, new_name: &str) -> Result<PathBuf, InvokeError> {
let project_dir = std::path::Path::new(project_path);
kcl_lib::settings::types::file::rename_project_directory(project_dir, new_name)
.await
.map_err(InvokeError::from_anyhow)
}
#[tauri::command]
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))
}
#[tauri::command]
async fn get_state(app: tauri::AppHandle) -> Result<Option<ProjectState>, InvokeError> {
let store = app.state::<state::Store>();
Ok(store.get().await)
}
#[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);
}
}
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)
}
#[tauri::command]
async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configuration) -> Result<(), InvokeError> {
let settings_path = get_app_settings_file_path(&app).await?;
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
tokio::fs::write(settings_path, contents.as_bytes())
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(())
}
async fn get_project_settings_file_path(project_path: &str) -> Result<PathBuf, InvokeError> {
let project_dir = std::path::Path::new(project_path);
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(project_path: &str) -> Result<ProjectConfiguration, InvokeError> {
let settings_path = get_project_settings_file_path(project_path).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(
project_path: &str,
configuration: ProjectConfiguration,
) -> Result<(), InvokeError> {
let settings_path = get_project_settings_file_path(project_path).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> {
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()))?;
// We can hardcode the client ID.
// This value is safe to be embedded in version control.
// This is the client ID of the KittyCAD app.
let client_id = "2af127fb-e14e-400a-9c57-a9ed08d1a5b7".to_string();
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()))?,
Some(
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
),
)
.set_auth_type(oauth2::AuthType::RequestBody)
.set_device_authorization_url(device_auth_url);
let details: oauth2::devicecode::StandardDeviceAuthorizationResponse = auth_client
.exchange_device_code()
.map_err(|e| InvokeError::from_anyhow(e.into()))?
.request_async(oauth2::reqwest::async_http_client)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
let Some(auth_uri) = details.verification_uri_complete() else {
return Err(InvokeError::from("getting the verification uri failed"));
};
// Open the system browser with the auth_uri.
// We do this in the browser and not a separate window because we want 1password and
// other crap to work well.
// TODO: find a better way to share this value with tauri e2e tests
// Here we're using an env var to enable the /tmp file (windows not supported for now)
// and bypass the shell::open call as it fails on GitHub Actions.
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
if e2e_tauri_enabled {
log::warn!("E2E_TAURI_ENABLED is set, won't open {} externally", auth_uri.secret());
let mut temp = String::from("/tmp");
// Overwrite with Windows variable
match env::var("TEMP") {
Ok(val) => temp = val,
Err(_e) => println!("Fallback to default /tmp"),
}
let path = Path::new(&temp).join("kittycad_user_code");
println!("Writing to {}", path.to_string_lossy());
tokio::fs::write(path, details.user_code().secret())
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
} else {
app.shell()
.open(auth_uri.secret(), None)
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
// Wait for the user to login.
let token = auth_client
.exchange_device_access_token(&details)
.request_async(oauth2::reqwest::async_http_client, tokio::time::sleep, None)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?
.access_token()
.secret()
.to_string();
Ok(token)
}
///This command returns the KittyCAD user info given a token.
/// The string returned from this method is the user info as a json string.
#[tauri::command]
async fn get_user(token: &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() {
DEFAULT_HOST.to_string()
} else {
hostname.to_string()
};
// Change the baseURL to the one we want.
let mut baseurl = host.to_string();
if !host.starts_with("http://") && !host.starts_with("https://") {
baseurl = format!("https://{host}");
if host.starts_with("localhost") {
baseurl = format!("http://{host}")
}
}
log::debug!("Getting user info...");
// use kittycad library to fetch the user info from /user/me
let mut client = kittycad::Client::new(token);
if baseurl != DEFAULT_HOST {
client.set_base_url(&baseurl);
}
let user_info: kittycad::types::User = client
.users()
.get_self()
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(user_info)
}
/// Open the selected path in the system file manager.
/// 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(app: tauri::AppHandle, path: &str) -> Result<(), InvokeError> {
// Check if the file exists.
// If it doesn't, return an error.
if !Path::new(path).exists() {
return Err(InvokeError::from_anyhow(anyhow::anyhow!(
"The file `{}` does not exist",
path
)));
}
#[cfg(not(unix))]
{
app.shell()
.command("explorer")
.args(["/select,", path]) // The comma after select is not a typo
.spawn()
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
#[cfg(unix)]
{
app.shell()
.command("open")
.args(["-R", path])
.spawn()
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(())
}
const SERVICE_NAME: &str = "_machine-api._tcp.local.";
async fn find_machine_api() -> Result<Option<String>> {
println!("Looking for machine API...");
// Timeout if no response is received after 5 seconds.
let timeout_duration = std::time::Duration::from_secs(5);
let mdns = mdns_sd::ServiceDaemon::new()?;
// Browse for a service type.
let receiver = mdns.browse(SERVICE_NAME)?;
let resp = tokio::time::timeout(
timeout_duration,
tokio::spawn(async move {
while let Ok(event) = receiver.recv() {
if let mdns_sd::ServiceEvent::ServiceResolved(info) = event {
if let Some(addr) = info.get_addresses().iter().next() {
return Some(format!("{}:{}", addr, info.get_port()));
}
}
}
None
}),
)
.await;
// Shut down.
mdns.shutdown()?;
let Ok(Ok(Some(addr))) = resp else {
return Ok(None);
};
Ok(Some(addr))
}
#[tauri::command]
async fn get_machine_api_ip() -> Result<Option<String>, InvokeError> {
let machine_api = find_machine_api().await.map_err(InvokeError::from_anyhow)?;
Ok(machine_api)
}
#[tauri::command]
async fn list_machines() -> Result<String, InvokeError> {
let machine_api = find_machine_api().await.map_err(InvokeError::from_anyhow)?;
let Some(machine_api) = machine_api else {
// Empty array.
return Ok("[]".to_string());
};
let client = reqwest::Client::new();
let response = client
.get(format!("http://{}/machines", machine_api))
.send()
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
let text = response.text().await.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(text)
}
#[allow(dead_code)]
fn open_url_sync(app: &tauri::AppHandle, url: &url::Url) {
log::debug!("Opening URL: {:?}", url);
let cloned_url = url.clone();
let runner: tauri::async_runtime::JoinHandle<Result<ProjectState>> = tauri::async_runtime::spawn(async move {
let url_str = cloned_url.path().to_string();
log::debug!("Opening URL path : {}", url_str);
let path = Path::new(url_str.as_str());
ProjectState::new_from_path(path.to_path_buf()).await
});
// Block on the handle.
match tauri::async_runtime::block_on(runner) {
Ok(Ok(store)) => {
// Create a state object to hold the project.
app.manage(state::Store::new(store));
}
Err(e) => {
log::warn!("Error opening URL:{} {:?}", url, e);
}
Ok(Err(e)) => {
log::warn!("Error opening URL:{} {:?}", url, e);
}
}
}
fn main() -> Result<()> {
tauri::Builder::default()
.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_dir_recursive,
show_in_folder,
read_app_settings_file,
write_app_settings_file,
read_project_settings_file,
write_project_settings_file,
rename_project_directory,
get_machine_api_ip,
list_machines
])
.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_persisted_scope::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init())
.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(())
}

View File

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

View File

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

View File

@ -1,84 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"app": {
"security": {
"csp": null
},
"windows": [
{
"fullscreen": false,
"height": 1200,
"resizable": true,
"title": "Zoo Modeling App",
"width": 1800
}
]
},
"build": {
"beforeDevCommand": "yarn start",
"devUrl": "http://localhost:3000",
"frontendDist": "../build"
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"linux": {
"deb": {
"depends": []
}
},
"longDescription": "",
"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": {
"mobile": [
{
"host": "app.zoo.dev"
}
],
"desktop": {
"schemes": [
"zoo",
"zoo-modeling-app"
]
}
},
"shell": {
"open": true
}
},
"productName": "Zoo Modeling App",
"version": "0.24.10"
}

View File

@ -1,57 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"bundle": {
"windows": {
"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": {
"active": true,
"endpoints": [
"https://dl.zoo.dev/releases/modeling-app/last_update.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
}
}
}

View File

@ -16,7 +16,7 @@ import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubsc
import { engineCommandManager } from 'lib/singletons'
import { useModelingContext } from 'hooks/useModelingContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { isTauri } from 'lib/isTauri'
import { isDesktop } from 'lib/isDesktop'
import { useLspContext } from 'components/LspProvider'
import { useRefreshSettings } from 'hooks/useRefreshSettings'
import { ModelingSidebar } from 'components/ModelingSidebar/ModelingSidebar'
@ -28,8 +28,8 @@ import { CoreDumpManager } from 'lib/coredump'
import { UnitsMenu } from 'components/UnitsMenu'
export function App() {
useRefreshSettings(PATHS.FILE + 'SETTINGS')
const { project, file } = useLoaderData() as IndexLoaderData
useRefreshSettings(PATHS.FILE + 'SETTINGS')
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const { onProjectOpen } = useLspContext()
@ -62,7 +62,7 @@ export function App() {
e.preventDefault()
})
useHotkeyWrapper(
[isTauri() ? 'mod + ,' : 'shift + mod + ,'],
[isDesktop() ? 'mod + ,' : 'shift + mod + ,'],
() => navigate(filePath + PATHS.SETTINGS),
{
splitKey: '|',

View File

@ -10,7 +10,7 @@ import { Settings } from './routes/Settings'
import Onboarding, { onboardingRoutes } from './routes/Onboarding'
import SignIn from './routes/SignIn'
import { Auth } from './Auth'
import { isTauri } from './lib/isTauri'
import { isDesktop } from './lib/isDesktop'
import Home from './routes/Home'
import { NetworkContext } from './hooks/useNetworkContext'
import { useNetworkStatus } from './hooks/useNetworkStatus'
@ -32,7 +32,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'
import { getState, setState } from 'lib/desktop'
import { CoreDumpManager } from 'lib/coredump'
import { engineCommandManager } from 'lib/singletons'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -66,8 +66,8 @@ const router = createBrowserRouter([
{
path: PATHS.INDEX,
loader: async () => {
const inTauri = isTauri()
if (inTauri) {
const onDesktop = isDesktop()
if (onDesktop) {
const appState = await getState()
if (appState) {
@ -84,7 +84,7 @@ const router = createBrowserRouter([
}
}
return inTauri
return onDesktop
? redirect(PATHS.HOME)
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
},
@ -101,7 +101,10 @@ const router = createBrowserRouter([
<Outlet />
<App />
<CommandBar />
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
{
// @ts-ignore
!isDesktop() && import.meta.env.PROD && <DownloadAppBanner />
}
</ModelingMachineProvider>
<WasmErrBanner />
</FileMachineProvider>

View File

@ -1,4 +1,5 @@
import { ActionIcon, ActionIconProps } from './ActionIcon'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import React, { ForwardedRef, forwardRef } from 'react'
import { PATHS } from 'lib/paths'
import { Link } from 'react-router-dom'
@ -107,6 +108,7 @@ export const ActionButton = forwardRef((props: ActionButtonProps, ref) => {
ref={ref as ForwardedRef<HTMLAnchorElement>}
to={to || PATHS.INDEX}
className={classNames}
onClick={openExternalBrowserIfDesktop(to as string)}
{...rest}
target="_blank"
>

View File

@ -1,4 +1,4 @@
import { isTauri } from 'lib/isTauri'
import { isDesktop } from 'lib/isDesktop'
import { useRouteError, isRouteErrorResponse } from 'react-router-dom'
import { ActionButton } from './ActionButton'
import {
@ -25,7 +25,7 @@ export const ErrorPage = () => {
</p>
)}
<div className="flex justify-between gap-2 mt-6">
{isTauri() && (
{isDesktop() && (
<ActionButton
Element="link"
to={'/'}

View File

@ -15,11 +15,9 @@ import {
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine'
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
import { isTauri } from 'lib/isTauri'
import { join, sep } from '@tauri-apps/api/path'
import { isDesktop } from 'lib/isDesktop'
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
import { getProjectInfo } from 'lib/tauri'
import { getProjectInfo } from 'lib/desktop'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -51,7 +49,9 @@ export const FileMachineProvider = ({
commandBarSend({ type: 'Close' })
navigate(
`${PATHS.FILE}/${encodeURIComponent(
context.selectedDirectory + sep() + event.data.name
context.selectedDirectory +
window.electron.path.sep +
event.data.name
)}`
)
} else if (
@ -86,7 +86,7 @@ export const FileMachineProvider = ({
},
services: {
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
const newFiles = isTauri()
const newFiles = isDesktop()
? (await getProjectInfo(context.project.path)).children
: []
return {
@ -99,15 +99,18 @@ export const FileMachineProvider = ({
let createdPath: string
if (event.data.makeDir) {
createdPath = await join(context.selectedDirectory.path, createdName)
await mkdir(createdPath)
createdPath = window.electron.path.join(
context.selectedDirectory.path,
createdName
)
await window.electron.mkdir(createdPath)
} else {
createdPath =
context.selectedDirectory.path +
sep() +
window.electron.path.sep +
createdName +
(createdName.endsWith(FILE_EXT) ? '' : FILE_EXT)
await create(createdPath)
await window.electron.writeFile(createdPath, '')
}
return {
@ -121,14 +124,25 @@ export const FileMachineProvider = ({
) => {
const { oldName, newName, isDir } = event.data
const name = newName ? newName : DEFAULT_FILE_NAME
const oldPath = await join(context.selectedDirectory.path, oldName)
const newDirPath = await join(context.selectedDirectory.path, name)
const oldPath = window.electron.path.join(
context.selectedDirectory.path,
oldName
)
const newDirPath = window.electron.path.join(
context.selectedDirectory.path,
name
)
const newPath =
newDirPath + (name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
await rename(oldPath, newPath, {})
await window.electron.rename(oldPath, newPath)
if (oldPath === file?.path && project?.path) {
if (!file) {
return Promise.reject(new Error('file is not defined'))
}
const currentFilePath = window.electron.path.join(file.path, file.name)
if (oldPath === currentFilePath && project?.path) {
// If we just renamed the current file, navigate to the new path
navigate(PATHS.FILE + '/' + encodeURIComponent(newPath))
} else if (file?.path.includes(oldPath)) {
@ -153,13 +167,15 @@ export const FileMachineProvider = ({
const isDir = !!event.data.children
if (isDir) {
await remove(event.data.path, {
await window.electron
.rm(event.data.path, {
recursive: true,
}).catch((e) => console.error('Error deleting directory', e))
})
.catch((e) => console.error('Error deleting directory', e))
} else {
await remove(event.data.path).catch((e) =>
console.error('Error deleting file', e)
)
await window.electron
.rm(event.data.path)
.catch((e) => console.error('Error deleting file', e))
}
// If we just deleted the current file or one of its parent directories,

View File

@ -9,7 +9,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
import { useFileContext } from 'hooks/useFileContext'
import styles from './FileTree.module.css'
import { sortProject } from 'lib/tauriFS'
import { sortProject } from 'lib/desktopFS'
import { FILE_EXT } from 'lib/constants'
import { CustomIcon } from './CustomIcon'
import { codeManager, kclManager } from 'lib/singletons'
@ -44,7 +44,7 @@ function RenameForm({
data: {
oldName: fileOrDir.name || '',
newName: inputRef.current?.value || fileOrDir.name || '',
isDir: fileOrDir.children !== undefined,
isDir: fileOrDir.children !== null,
},
})
}
@ -90,7 +90,7 @@ function DeleteFileTreeItemDialog({
const { send } = useFileContext()
return (
<DeleteConfirmationDialog
title={`Delete ${fileOrDir.children !== undefined ? 'folder' : 'file'}`}
title={`Delete ${fileOrDir.children !== null ? 'folder' : 'file'}`}
onDismiss={() => setIsOpen(false)}
onConfirm={() => {
send({ type: 'Delete file', data: fileOrDir })
@ -99,7 +99,7 @@ function DeleteFileTreeItemDialog({
>
<p className="my-4">
This will permanently delete "{fileOrDir.name || 'this file'}"
{fileOrDir.children !== undefined ? ' and all of its contents. ' : '. '}
{fileOrDir.children !== null ? ' and all of its contents. ' : '. '}
</p>
<p className="my-4">
Are you sure you want to delete "{fileOrDir.name || 'this file'}
@ -165,7 +165,7 @@ const FileTreeItem = ({
}
function handleClick() {
if (fileOrDir.children !== undefined) return // Don't open directories
if (fileOrDir.children !== null) return // Don't open directories
if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) {
// Import non-kcl files
@ -194,7 +194,7 @@ const FileTreeItem = ({
return (
<div className="contents" ref={itemRef}>
{fileOrDir.children === undefined ? (
{fileOrDir.children === null ? (
<li
className={
'group m-0 p-0 border-solid border-0 hover:bg-primary/5 focus-within:bg-primary/5 dark:hover:bg-primary/20 dark:focus-within:bg-primary/20 ' +

View File

@ -3,10 +3,11 @@ import Tooltip from './Tooltip'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { CustomIcon } from './CustomIcon'
import { useLocation, useNavigate } from 'react-router-dom'
import { createAndOpenNewProject } from 'lib/tauriFS'
import { PATHS } from 'lib/paths'
import { createAndOpenNewProject } from 'lib/desktopFS'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { useLspContext } from './LspProvider'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
const HelpMenuDivider = () => (
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
@ -141,6 +142,9 @@ function HelpMenuItem({
{as === 'a' ? (
<a
{...(props as React.ComponentProps<'a'>)}
onClick={openExternalBrowserIfDesktop(
(props as React.ComponentProps<'a'>).href
)}
className={`no-underline text-inherit ${baseClassName} ${className}`}
>
{children}

View File

@ -26,8 +26,12 @@ export function LowerRightControls({
const isPlayWright = window?.localStorage.getItem('playwright') === 'true'
async function reportbug(event: { preventDefault: () => void }) {
async function reportbug(event: {
preventDefault: () => void
stopPropagation: () => void
}) {
event?.preventDefault()
event?.stopPropagation()
if (!coreDumpManager) {
// open default reporting option
@ -88,7 +92,7 @@ export function LowerRightControls({
<Link
to={
location.pathname.includes(PATHS.FILE)
? filePath + PATHS.SETTINGS_PROJECT
? filePath + PATHS.SETTINGS + '?tab=project'
: PATHS.HOME + PATHS.SETTINGS
}
>

View File

@ -25,7 +25,7 @@ import {
import { wasmUrl } from 'lang/wasm'
import { PROJECT_ENTRYPOINT } from 'lib/constants'
import { err } from 'lib/trap'
import { isTauri } from 'lib/isTauri'
import { isDesktop } from 'lib/isDesktop'
import { codeManager } from 'lib/singletons'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
@ -125,7 +125,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
])
useMemo(() => {
if (!isTauri() && isKclLspReady && kclLspClient && codeManager.code) {
if (!isDesktop() && isKclLspReady && kclLspClient && codeManager.code) {
kclLspClient.textDocumentDidOpen({
textDocument: {
uri: `file:///${PROJECT_ENTRYPOINT}`,

View File

@ -7,6 +7,7 @@ import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { editorShortcutMeta } from './KclEditorPane'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { kclManager } from 'lib/singletons'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
@ -60,6 +61,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
href="https://zoo.dev/docs/kcl"
target="_blank"
rel="noopener noreferrer"
onClick={openExternalBrowserIfDesktop()}
>
<span>Read the KCL docs</span>
<small>
@ -78,6 +80,7 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
href="https://zoo.dev/docs/kcl-samples"
target="_blank"
rel="noopener noreferrer"
onClick={openExternalBrowserIfDesktop()}
>
<span>KCL samples</span>
<small>

View File

@ -7,7 +7,7 @@ import Tooltip from 'components/Tooltip'
import { ActionIcon } from 'components/ActionIcon'
import styles from './ModelingSidebar.module.css'
import { ModelingPane } from './ModelingPane'
import { isTauri } from 'lib/isTauri'
import { isDesktop } from 'lib/isDesktop'
import { useModelingContext } from 'hooks/useModelingContext'
import { CustomIconName } from 'components/CustomIcon'
import { useCommandsContext } from 'hooks/useCommandsContext'
@ -71,7 +71,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
(action) =>
(!action.hide || (action.hide instanceof Function && !action.hide())) &&
(!action.hideOnPlatform ||
(isTauri()
(isDesktop()
? action.hideOnPlatform === 'web'
: action.hideOnPlatform === 'desktop'))
)
@ -86,7 +86,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
).filter(
(pane) =>
!pane.hideOnPlatform ||
(isTauri()
(isDesktop()
? pane.hideOnPlatform === 'web'
: pane.hideOnPlatform === 'desktop')
),

View File

@ -1,7 +1,7 @@
import { Popover } from '@headlessui/react'
import Tooltip from './Tooltip'
import { machineManager } from 'lib/machineManager'
import { isTauri } from 'lib/isTauri'
import { isDesktop } from 'lib/isDesktop'
import { CustomIcon } from './CustomIcon'
export const NetworkMachineIndicator = ({
@ -10,7 +10,7 @@ export const NetworkMachineIndicator = ({
className?: string
}) => {
const machineCount = Object.keys(machineManager.machines).length
return isTauri() ? (
return isDesktop() ? (
<Popover className="relative">
<Popover.Button
className={

View File

@ -36,8 +36,8 @@ function ProjectCard({
void handleRenameProject(e, project).then(() => setIsEditing(false))
}
function getDisplayedTime(dateStr: string) {
const date = new Date(dateStr)
function getDisplayedTime(dateTimeMs: number) {
const date = new Date(dateTimeMs)
const startOfToday = new Date()
startOfToday.setHours(0, 0, 0, 0)
return date.getTime() < startOfToday.getTime()
@ -52,7 +52,7 @@ function ProjectCard({
}
// async function setupImageUrl() {
// const projectImagePath = await join(project.path, PROJECT_IMAGE_NAME)
// const projectImagePath = await join(project.file.path, PROJECT_IMAGE_NAME)
// if (await exists(projectImagePath)) {
// const imageData = await readFile(projectImagePath)
// const blob = new Blob([imageData], { type: 'image/jpg' })
@ -113,8 +113,8 @@ function ProjectCard({
</span>
<span className="px-2 text-chalkboard-60 text-xs">
Edited{' '}
{project.metadata && project.metadata?.modified
? getDisplayedTime(project.metadata.modified)
{project.metadata && project.metadata.modified
? getDisplayedTime(parseInt(project.metadata.modified))
: 'never'}
</span>
</div>

View File

@ -3,9 +3,9 @@ import { ActionButton, ActionButtonProps } from './ActionButton'
import { type IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths'
import { isTauri } from '../lib/isTauri'
import { isDesktop } from '../lib/isDesktop'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useMemo } from 'react'
import { sep } from '@tauri-apps/api/path'
import { Logo } from './Logo'
import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext'
@ -55,7 +55,7 @@ function AppLogoLink({
"relative h-full grid place-content-center group p-1.5 before:block before:content-[''] before:absolute before:inset-0 before:bottom-2.5 before:z-[-1] before:bg-primary before:rounded-b-sm"
const logoClassName = 'w-auto h-4 text-chalkboard-10'
return isTauri() ? (
return isDesktop() ? (
<Link
data-testid="app-logo"
onClick={() => {
@ -111,7 +111,7 @@ function ProjectMenuPopover({
<>
<span className="flex-1">Project settings</span>
<kbd className="hotkey">{`${platform === 'macos' ? '⌘' : 'Ctrl'}${
isTauri() ? '' : '⬆'
isDesktop() ? '' : '⬆'
},`}</kbd>
</>
),
@ -150,7 +150,7 @@ function ProjectMenuPopover({
{
id: 'make',
Element: 'button',
className: !isTauri() ? 'hidden' : '',
className: !isDesktop() ? 'hidden' : '',
children: (
<>
<span>Make current part</span>
@ -177,7 +177,7 @@ function ProjectMenuPopover({
id: 'go-home',
Element: 'button',
children: 'Go to Home',
className: !isTauri() ? 'hidden' : '',
className: !isDesktop() ? 'hidden' : '',
onClick: () => {
onProjectClose(file || null, project?.path || null, true)
// Clear the scene and end the session.
@ -195,7 +195,7 @@ function ProjectMenuPopover({
commandBarSend,
engineCommandManager,
onProjectClose,
isTauri,
isDesktop,
]
)
@ -207,11 +207,13 @@ function ProjectMenuPopover({
>
<div className="flex flex-col items-start py-0.5">
<span className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block">
{isTauri() && file?.name
? file.name.slice(file.name.lastIndexOf(sep()) + 1)
{isDesktop() && file?.name
? file.name.slice(
file.name.lastIndexOf(window.electron.path.sep) + 1
)
: APP_NAME}
</span>
{isTauri() && project?.name && (
{isDesktop() && project?.name && (
<span className="hidden text-xs text-chalkboard-70 dark:text-chalkboard-40 whitespace-nowrap lg:block">
{project.name}
</span>

View File

@ -9,16 +9,15 @@ import {
import { Fragment } from 'react/jsx-runtime'
import { SettingsSection } from './SettingsSection'
import { useLocation, useNavigate } from 'react-router-dom'
import { isTauri } from 'lib/isTauri'
import { isDesktop } from 'lib/isDesktop'
import { ActionButton } from 'components/ActionButton'
import { SettingsFieldInput } from './SettingsFieldInput'
import { getInitialDefaultDir, showInFolder } from 'lib/tauri'
import { getInitialDefaultDir } from 'lib/desktop'
import toast from 'react-hot-toast'
import { APP_VERSION } from 'routes/Settings'
import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/tauriFS'
import { PATHS } from 'lib/paths'
import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/desktopFS'
import { useDotDotSlash } from 'hooks/useDotDotSlash'
import { sep } from '@tauri-apps/api/path'
import { ForwardedRef, forwardRef, useEffect } from 'react'
import { useLspContext } from 'components/LspProvider'
@ -41,12 +40,17 @@ export const AllSettingsFields = forwardRef(
} = useSettingsAuthContext()
const projectPath =
isFileSettings && isTauri()
isFileSettings && isDesktop()
? decodeURI(
location.pathname
.replace(PATHS.FILE + '/', '')
.replace(PATHS.SETTINGS, '')
.slice(0, decodeURI(location.pathname).lastIndexOf(sep()))
.slice(
0,
decodeURI(location.pathname).lastIndexOf(
window.electron.path.sep
)
)
)
: undefined
@ -176,21 +180,25 @@ export const AllSettingsFields = forwardRef(
title="Reset settings"
description={`Restore settings to their default values. Your settings are saved in
${
isTauri()
isDesktop()
? ' a file in the app data folder for your OS.'
: " your browser's local storage."
}
`}
>
<div className="flex flex-col items-start gap-4">
{isTauri() && (
{isDesktop() && (
<ActionButton
Element="button"
onClick={async () => {
const paths = await getSettingsFolderPaths(
projectPath ? decodeURIComponent(projectPath) : undefined
)
showInFolder(paths[searchParamTab])
const finalPath = paths[searchParamTab]
if (!finalPath) {
return new Error('finalPath undefined')
}
window.electron.showInFolder(finalPath)
}}
iconStart={{
icon: 'folder',

View File

@ -21,7 +21,7 @@ import {
Prop,
StateFrom,
} from 'xstate'
import { isTauri } from 'lib/isTauri'
import { isDesktop } from 'lib/isDesktop'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
import { uuidv4 } from 'lib/utils'
@ -341,7 +341,7 @@ export default SettingsAuthProvider
export function logout() {
localStorage.removeItem(TOKEN_PERSIST_KEY)
return (
!isTauri() &&
!isDesktop() &&
fetch(withBaseUrl('/logout'), {
method: 'POST',
credentials: 'include',

View File

@ -238,6 +238,7 @@ export const Stream = () => {
if (!videoRef.current) return
if (!mediaStream) return
// The browser complains if we try to load a new stream without pausing first.
// Do not immediately play the stream!
try {
videoRef.current.srcObject = mediaStream

View File

@ -8,7 +8,7 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip'
import usePlatform from 'hooks/usePlatform'
import { isTauri } from 'lib/isTauri'
import { isDesktop } from 'lib/isDesktop'
import { CustomIcon } from './CustomIcon'
type User = Models['User_type']
@ -33,7 +33,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
<>
<span className="flex-1">User settings</span>
<kbd className="hotkey">{`${platform === 'macos' ? '⌘' : 'Ctrl'}${
isTauri() ? '' : '⬆'
isDesktop() ? '' : '⬆'
},`}</kbd>
</>
),

View File

@ -1,15 +1,29 @@
// env vars were centralised so they could be mocked in jest
// but isn't needed anymore with vite, so is now just a convention
// Even though we transpile to a module system that supports import, the
// typescript checker doesn't know that, so it's causing problems
// to properly type check anything with "import.meta". I've tried for a good
// hour to fix this but nothing has worked. This is the pain the JS ecosystem
// gets for like 6 different module systems.
// @ts-ignore
export const VITE_KC_API_WS_MODELING_URL = import.meta.env
.VITE_KC_API_WS_MODELING_URL
// @ts-ignore
export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
// @ts-ignore
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
// @ts-ignore
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
.VITE_KC_CONNECTION_TIMEOUT_MS
// @ts-ignore
export const VITE_KC_DEV_TOKEN = import.meta.env.VITE_KC_DEV_TOKEN as
| string
| undefined
// @ts-ignore
export const TEST = import.meta.env.TEST
// @ts-ignore
export const DEV = import.meta.env.DEV
// @ts-ignore
export const CI = import.meta.env.CI

View File

@ -1,16 +1,17 @@
import { Platform, platform } from '@tauri-apps/plugin-os'
import { isTauri } from 'lib/isTauri'
import { isDesktop } from 'lib/isDesktop'
import { useEffect, useState } from 'react'
export type Platform = 'macos' | 'windows' | 'linux' | ''
export default function usePlatform() {
const [platformName, setPlatformName] = useState<Platform | ''>('')
const [platformName, setPlatformName] = useState<Platform>('')
useEffect(() => {
async function getPlatform() {
setPlatformName(await platform())
setPlatformName((window.electron.platform ?? '') as Platform)
}
if (isTauri()) {
if (isDesktop()) {
void getPlatform()
} else {
if (navigator.userAgent.indexOf('Mac') !== -1) {

View File

@ -104,7 +104,7 @@ export function useSetupEngineManager(
}, [immediateState])
useEffect(() => {
engineCommandManager.settings.theme = settings.theme
engineCommandManager.settings = settings
const handleResize = deferExecution(() => {
const { width, height } = getDimensions(
@ -194,13 +194,7 @@ export function useSetupEngineManager(
// Engine relies on many settings so we should rebind events when it changes
// We have to list out the ones we care about because the settings object holds
// non-settings too...
}, [
settings.enableSSAO,
settings.highlightEdges,
settings.showScaleGrid,
settings.theme,
settings.pool,
])
}, [...Object.values(settings)])
}
function getDimensions(streamWidth?: number, streamHeight?: number) {

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