Compare commits

...

37 Commits

Author SHA1 Message Date
a21c2c50b3 start of url scheme
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 14:38:03 -07:00
aa52407fda Cut release v0.19.3 (#2251) 2024-04-25 13:28:42 -07:00
e45be831d0 Pass the ?pool query param through to the backend. (#2246)
Pass the ?pool query param through to the backend.

This will slice off the ?pool= param and pass it to the WebSocket
request, which requests that the Zoo API use a particular pool of
engines. This isn't something any users of the zoo api require; but it's
needed for the internal engine Zoo development workflow. This may be
used in the future, but for now this'll be always enabled. Passing any
value in the production servers will result in a "no backend" error for
now.
2024-04-25 19:51:33 +00:00
005944f3a3 fix the updater (#2250)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 12:41:22 -07:00
755ef8ce7f download-wasm if there's no rust changes (#2234)
* download-wasm if there's no rust changes

* typo

* typo

* artifact stuff

* add needs

* permissions

* hmm

* more logic

* same for ubuntu
2024-04-26 05:37:32 +10:00
005d1f0ca7 Filter files and folders that start with a . (#2249) 2024-04-25 19:01:50 +00:00
e158f6f513 Better rust parsing of route uris for files (#2248)
* refactors

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fiex;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fiex;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 18:55:11 +00:00
879d7ec4f4 Cut release v0.19.2 (#2247) 2024-04-25 14:38:25 -04:00
f6838b9b14 always ensure the dirs exist (#2245)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 17:07:24 +00:00
cb75c47631 fix env vars for lsp server to match other .env vars (#2243)
fix env vars for lsp

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 16:41:39 +00:00
9b95ec1083 fix relevant extensions (#2241)
* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 08:36:45 -07:00
a3eeff65c8 Cut release v0.19.1 (#2240) 2024-04-25 10:40:31 -04:00
fab3d2b130 Make FileTree a pane (desktop only) (#2232) 2024-04-25 09:56:55 -04:00
0a96dc6fd2 Project state improvements (#2239) 2024-04-25 12:52:08 +00:00
e123a00d4b Cut release v0.19.0 (#2206)
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-04-25 07:30:49 -04:00
b950cc0583 able to go to and from app settings to executor settings for cli (#2236)
* settings from and into exefcutor settings

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* settings

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* ipdates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* ensure all have extrudes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 09:31:18 +00:00
c89780a489 upload artifact to same action (#2235) 2024-04-25 17:43:48 +10:00
1afed68dd7 Settings move to rust (for read/write from files) (#2220)
* start of settings types

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add validator

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* start of settings in rust

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix wasm

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix wasm

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* derive docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* configuration

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* read and write functions with migration

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* make more dry

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more parsing of app settings

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more things

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* trim end

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* project settings

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup tauri commands

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* refactor

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* refactor

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* change to files

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* better

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup more

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* get rid of dead code

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixed

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup some more shit

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add validation

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* validation

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* validate

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* validate

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* clippuy

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* clippuy

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 07:13:09 +00:00
dcbed4f06f build and store wasm (#2233) 2024-04-25 07:01:42 +00:00
379f154a5c more parallel playw tests (#2179) 2024-04-25 07:00:41 +10:00
60c4969322 Adding dark edge lines in light mode + enabling SSAO (#2219)
* adding dark edge lines in light mode + enabling SSAO

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Rerun CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: Frank Noirot <frank@kittycad.io>
2024-04-24 16:59:25 -04:00
cc6dee8ad4 Make it impossible to crash app while extruding (#2224)
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-04-24 16:34:56 -04:00
2fc7c0d5fd fix gizmo (#2221) 2024-04-24 20:09:39 +00:00
bf2dcd808f Bump tauri-plugin-http from 2.0.0-beta.5 to 2.0.0-beta.6 in /src-tauri (#2222)
Bumps [tauri-plugin-http](https://github.com/tauri-apps/plugins-workspace) from 2.0.0-beta.5 to 2.0.0-beta.6.
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](https://github.com/tauri-apps/plugins-workspace/compare/fs-v2.0.0-beta.5...fs-v2.0.0-beta.6)

---
updated-dependencies:
- dependency-name: tauri-plugin-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-24 05:54:18 -04:00
ee21e486d4 math u32 fix (#2218)
* math fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* empty

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* empty

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-23 22:59:12 +00:00
b5a3eb9e9c Bump tauri-build from 2.0.0-beta.12 to 2.0.0-beta.13 in /src-tauri (#2204)
Bumps [tauri-build](https://github.com/tauri-apps/tauri) from 2.0.0-beta.12 to 2.0.0-beta.13.
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-build-v2.0.0-beta.12...tauri-build-v2.0.0-beta.13)

---
updated-dependencies:
- dependency-name: tauri-build
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 20:57:23 +00:00
c85645c9f2 Bump tauri-plugin-dialog from 2.0.0-beta.5 to 2.0.0-beta.6 in /src-tauri (#2203)
Bumps [tauri-plugin-dialog](https://github.com/tauri-apps/plugins-workspace) from 2.0.0-beta.5 to 2.0.0-beta.6.
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](https://github.com/tauri-apps/plugins-workspace/compare/fs-v2.0.0-beta.5...fs-v2.0.0-beta.6)

---
updated-dependencies:
- dependency-name: tauri-plugin-dialog
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 18:48:00 +00:00
cfa4dd2e33 Bump kittycad-modeling-cmds from 0.2.20 to 0.2.21 in /src/wasm-lib (#2199)
Bumps [kittycad-modeling-cmds](https://github.com/KittyCAD/modeling-api) from 0.2.20 to 0.2.21.
- [Commits](https://github.com/KittyCAD/modeling-api/compare/kittycad-modeling-cmds-0.2.20...kittycad-modeling-cmds-0.2.21)

---
updated-dependencies:
- dependency-name: kittycad-modeling-cmds
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 17:55:03 +00:00
c620f7269c Patterns fixes (2d) (#2197)
* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* patterns fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* better autocomplete

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-23 17:31:20 +00:00
2d8d29b345 Bump tauri-plugin-fs from 2.0.0-beta.5 to 2.0.0-beta.6 in /src-tauri (#2205)
Bumps [tauri-plugin-fs](https://github.com/tauri-apps/plugins-workspace) from 2.0.0-beta.5 to 2.0.0-beta.6.
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](https://github.com/tauri-apps/plugins-workspace/compare/fs-v2.0.0-beta.5...fs-v2.0.0-beta.6)

---
updated-dependencies:
- dependency-name: tauri-plugin-fs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 16:56:00 +00:00
00da062586 bump kittycad.rs (#2196)
* update lib

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-23 03:46:54 +00:00
aafbaf6c50 human speed completions (#2193)
* human speed completions

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add slowness

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* empty

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-23 00:21:24 +00:00
2894c84a4e fix recast (#2194)
* fix recast

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-22 17:14:20 -07:00
c01084feb0 Zoom to fit rust side (#2195)
* zoom to fit

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* zoom to fit

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-22 17:14:10 -07:00
c461db5f54 fix const completion (#2192)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-22 21:53:49 +00:00
03fcb73aca Bump kittycad-modeling-cmds from 0.2.19 to 0.2.20 in /src/wasm-lib (#2186)
Bumps [kittycad-modeling-cmds](https://github.com/KittyCAD/modeling-api) from 0.2.19 to 0.2.20.
- [Commits](https://github.com/KittyCAD/modeling-api/compare/kittycad-modeling-cmds-0.2.19...kittycad-modeling-cmds-0.2.20)

---
updated-dependencies:
- dependency-name: kittycad-modeling-cmds
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-04-22 20:09:53 +00:00
8065e7e51a Bump thiserror from 1.0.58 to 1.0.59 in /src/wasm-lib (#2187)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.58 to 1.0.59.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.58...1.0.59)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-04-22 18:46:00 +00:00
322 changed files with 11147 additions and 5049 deletions

View File

@ -0,0 +1,33 @@
name: Build and Store WASM
on:
push:
branches:
- main
jobs:
build-and-upload:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies
run: yarn
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache wasm
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: build wasm
run: yarn build:wasm
# Upload the WASM bundle as an artifact
- uses: actions/upload-artifact@v2
with:
name: wasm-bundle
path: src/wasm-lib/pkg

View File

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

57
.github/workflows/cargo-test-tauri.yml vendored Normal file
View File

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

View File

@ -147,16 +147,16 @@ jobs:
- name: Install ubuntu system dependencies - name: Install ubuntu system dependencies
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
run: > run: |
sudo apt-get update && sudo apt-get update
sudo apt-get install -y sudo apt-get install -y \
libgtk-3-dev libgtk-3-dev \
libayatana-appindicator3-dev libayatana-appindicator3-dev \
webkit2gtk-driver webkit2gtk-driver \
libsoup-3.0-dev libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev libjavascriptcoregtk-4.1-dev \
libwebkit2gtk-4.1-dev libwebkit2gtk-4.1-dev \
at-spi2-core at-spi2-core \
xvfb xvfb
- name: Sync node version and setup cache - name: Sync node version and setup cache

View File

@ -12,11 +12,31 @@ concurrency:
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
actions: read
jobs: jobs:
check-rust-changes:
runs-on: ubuntu-latest
outputs:
rust-changed: ${{ steps.filter.outputs.rust }}
steps:
- uses: actions/checkout@v4
- id: filter
name: Check for Rust changes
uses: dorny/paths-filter@v2
with:
filters: |
rust:
- 'src/wasm-lib/**'
playwright-ubuntu: playwright-ubuntu:
timeout-minutes: 60 timeout-minutes: 60
runs-on: ubuntu-latest-8-cores runs-on: ubuntu-latest-8-cores
needs: check-rust-changes
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
@ -28,13 +48,39 @@ jobs:
run: yarn run: yarn
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: yarn playwright install --with-deps run: yarn playwright install --with-deps
- name: download wasm
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: actions/download-artifact@v2
continue-on-error: true
with:
name: wasm-bundle
path: src/wasm-lib/pkg
- name: copy wasm blob
if: needs.check-rust-changes.outputs.rust-changed == 'false'
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
continue-on-error: true
- name: Setup Rust - name: Setup Rust
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust
if: steps.download-wasm.outcome == 'failure'
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Cache wasm - name: Cache wasm
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Cache wasm
if: steps.download-wasm.outcome == 'failure'
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: './src/wasm-lib' workspaces: './src/wasm-lib'
- name: build wasm - name: build wasm
if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm
- name: build wasm
if: steps.download-wasm.outcome == 'failure'
run: yarn build:wasm run: yarn build:wasm
- name: build web - name: build web
run: yarn build:local run: yarn build:local
@ -85,10 +131,16 @@ jobs:
name: playwright-report name: playwright-report
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30
- uses: actions/upload-artifact@v4
if: github.ref == 'refs/heads/main'
with:
name: wasm-bundle
path: src/wasm-lib/pkg
playwright-macos: playwright-macos:
timeout-minutes: 60 timeout-minutes: 60
runs-on: macos-14 runs-on: macos-14
needs: check-rust-changes
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
@ -99,13 +151,39 @@ jobs:
run: yarn run: yarn
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: yarn playwright install --with-deps run: yarn playwright install --with-deps
- name: download wasm
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: actions/download-artifact@v4
with:
name: wasm-bundle
path: src/wasm-lib/pkg
continue-on-error: true
- name: copy wasm blob
if: needs.check-rust-changes.outputs.rust-changed == 'false'
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
continue-on-error: true
- name: Setup Rust - name: Setup Rust
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust
if: steps.download-wasm.outcome == 'failure'
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Cache wasm - name: Cache wasm
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Cache wasm
if: steps.download-wasm.outcome == 'failure'
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: './src/wasm-lib' workspaces: './src/wasm-lib'
- name: build wasm - name: build wasm
if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm
- name: build wasm
if: steps.download-wasm.outcome == 'failure'
run: yarn build:wasm run: yarn build:wasm
- name: build web - name: build web
run: yarn build:local run: yarn build:local
@ -122,3 +200,8 @@ jobs:
name: playwright-report name: playwright-report
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30
- uses: actions/upload-artifact@v4
if: github.ref == 'refs/heads/main'
with:
name: wasm-bundle
path: src/wasm-lib/pkg

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -565,7 +565,9 @@ test('Auto complete works', async ({ page }) => {
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.keyboard.type('12') await page.keyboard.type('12')
await page.waitForTimeout(100)
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.waitForTimeout(100)
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.keyboard.press('Tab') await page.keyboard.press('Tab')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
@ -594,13 +596,12 @@ test('Auto complete works', async ({ page }) => {
test('Stored settings are validated and fall back to defaults', async ({ test('Stored settings are validated and fall back to defaults', async ({
page, page,
context,
}) => { }) => {
const u = getUtils(page) const u = getUtils(page)
// Override beforeEach test setup // Override beforeEach test setup
// with corrupted settings // with corrupted settings
await context.addInitScript( await page.addInitScript(
async ({ settingsKey, settings }) => { async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings) localStorage.setItem(settingsKey, settings)
}, },
@ -617,18 +618,18 @@ test('Stored settings are validated and fall back to defaults', async ({
// Check the settings were reset // Check the settings were reset
const storedSettings = TOML.parse( const storedSettings = TOML.parse(
await page.evaluate( await page.evaluate(
({ settingsKey }) => localStorage.getItem(settingsKey) || '{}', ({ settingsKey }) => localStorage.getItem(settingsKey) || '',
{ settingsKey: TEST_SETTINGS_KEY } { settingsKey: TEST_SETTINGS_KEY }
) )
) as { settings: SaveSettingsPayload } ) as { settings: SaveSettingsPayload }
expect(storedSettings.settings.app?.theme).toBe('dark') expect(storedSettings.settings?.app?.theme).toBe(undefined)
// Check that the invalid settings were removed // Check that the invalid settings were removed
expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined) expect(storedSettings.settings?.modeling?.defaultUnit).toBe(undefined)
expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined) expect(storedSettings.settings?.modeling?.mouseControls).toBe(undefined)
expect(storedSettings.settings.app?.projectDirectory).toBe(undefined) expect(storedSettings.settings?.app?.projectDirectory).toBe(undefined)
expect(storedSettings.settings.projects?.defaultProjectName).toBe(undefined) expect(storedSettings.settings?.projects?.defaultProjectName).toBe(undefined)
}) })
test('Project settings can be set and override user settings', async ({ test('Project settings can be set and override user settings', async ({
@ -1033,6 +1034,7 @@ const part001 = startSketchOn('-XZ')
}) })
test('Can add multiple sketches', async ({ page }) => { test('Can add multiple sketches', async ({ page }) => {
test.skip(process.platform === 'darwin', 'Can add multiple sketches')
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -1,12 +1,13 @@
import { SaveSettingsPayload } from 'lib/settings/settingsTypes' import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
export const TEST_SETTINGS_KEY = '/user.toml' export const TEST_SETTINGS_KEY = '/settings.toml'
export const TEST_SETTINGS = { export const TEST_SETTINGS = {
app: { app: {
theme: Themes.Dark, theme: Themes.Dark,
onboardingStatus: 'dismissed', onboardingStatus: 'dismissed',
projectDirectory: '', projectDirectory: '',
enableSSAO: false,
}, },
modeling: { modeling: {
defaultUnit: 'in', defaultUnit: 'in',

View File

@ -6,7 +6,7 @@ import { PNG } from 'pngjs'
async function waitForPageLoad(page: Page) { async function waitForPageLoad(page: Page) {
// wait for 'Loading stream...' spinner // wait for 'Loading stream...' spinner
// await page.getByTestId('loading-stream').waitFor() await page.getByTestId('loading-stream').waitFor()
// wait for all spinners to be gone // wait for all spinners to be gone
await page.getByTestId('loading').waitFor({ state: 'detached' }) await page.getByTestId('loading').waitFor({ state: 'detached' })

View File

@ -71,7 +71,7 @@ describe('ZMA (Tauri, Linux)', () => {
// Now should be signed in // Now should be signed in
const newFileButton = await $('[data-testid="home-new-file"]') const newFileButton = await $('[data-testid="home-new-file"]')
expect(await newFileButton.getText()).toEqual('New file') expect(await newFileButton.getText()).toEqual('New project')
}) })
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => { it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.18.1", "version": "0.19.3",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.16.0", "@codemirror/autocomplete": "^6.16.0",

2458
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,27 +8,27 @@ repository = "https://github.com/KittyCAD/modeling-app"
default-run = "app" default-run = "app"
edition = "2021" edition = "2021"
rust-version = "1.70" rust-version = "1.70"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.0-beta.12", features = [] } tauri-build = { version = "2.0.0-beta.13", features = [] }
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
kittycad = "0.2.67" kcl-lib = { version = "0.1.53", path = "../src/wasm-lib/kcl" }
kittycad = "0.3.0"
oauth2 = "4.4.2" oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] } tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
tauri-plugin-dialog = { version = "2.0.0-beta.5" } tauri-plugin-cli = { version = "2.0.0-beta.3" }
tauri-plugin-fs = { version = "2.0.0-beta.5" } tauri-plugin-dialog = { version = "2.0.0-beta.6" }
tauri-plugin-http = { version = "2.0.0-beta.5" } tauri-plugin-fs = { version = "2.0.0-beta.6" }
tauri-plugin-http = { version = "2.0.0-beta.6" }
tauri-plugin-os = { version = "2.0.0-beta.2" } tauri-plugin-os = { version = "2.0.0-beta.2" }
tauri-plugin-process = { version = "2.0.0-beta.2" } tauri-plugin-process = { version = "2.0.0-beta.2" }
tauri-plugin-shell = { version = "2.0.0-beta.2" } tauri-plugin-shell = { version = "2.0.0-beta.2" }
tauri-plugin-updater = { version = "2.0.0-beta.4" } tauri-plugin-updater = { version = "2.0.0-beta.4" }
tokio = { version = "1.37.0", features = ["time"] } tokio = { version = "1.37.0", features = ["time", "fs"] }
toml = "0.8.2" toml = "0.8.2"
[features] [features]

View File

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

6
src-tauri/rustfmt.toml Normal file
View File

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

View File

@ -1,91 +1,225 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!! // Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::env; pub(crate) mod state;
use std::fs;
use std::io::Read; use std::{
use std::path::Path; env,
use std::path::PathBuf; path::{Path, PathBuf},
process::Command,
};
use anyhow::Result; use anyhow::Result;
use kcl_lib::settings::types::{
file::{FileEntry, Project, ProjectRoute, ProjectState},
project::ProjectConfiguration,
Configuration, DEFAULT_PROJECT_KCL_FILE,
};
use oauth2::TokenResponse; use oauth2::TokenResponse;
use serde::Serialize; use tauri::{ipc::InvokeError, Manager};
use std::process::Command; use tauri_plugin_cli::CliExt;
use tauri::ipc::InvokeError;
use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::ShellExt;
const DEFAULT_HOST: &str = "https://api.kittycad.io";
/// This command returns the a json string parse from a toml file at the path. 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] #[tauri::command]
fn read_toml(path: &str) -> Result<String, InvokeError> { fn get_initial_default_dir(app: tauri::AppHandle) -> Result<PathBuf, InvokeError> {
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?; let dir = match app.path().document_dir() {
let mut contents = String::new(); Ok(dir) => dir,
file.read_to_string(&mut contents) 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()))?; .map_err(|e| InvokeError::from_anyhow(e.into()))?;
let value =
toml::from_str::<toml::Value>(&contents).map_err(|e| InvokeError::from_anyhow(e.into()))?;
let value = serde_json::to_string(&value).map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(value)
} }
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51 Ok(app_config_dir.join(SETTINGS_FILE_NAME))
/// Removed from tauri v2
#[derive(Debug, Serialize)]
pub struct DiskEntry {
/// The path to the entry.
pub path: PathBuf,
/// The name of the entry (file name with extension or directory name).
pub name: Option<String>,
/// The children of this entry if it's a directory.
#[serde(skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<DiskEntry>>,
} }
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
/// Removed from tauri v2
fn is_dir<P: AsRef<Path>>(path: P) -> Result<bool> {
std::fs::metadata(path)
.map(|md| md.is_dir())
.map_err(Into::into)
}
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
/// Removed from tauri v2
#[tauri::command] #[tauri::command]
fn read_dir_recursive(path: &str) -> Result<Vec<DiskEntry>, InvokeError> { async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration, InvokeError> {
let mut files_and_dirs: Vec<DiskEntry> = vec![]; let mut settings_path = get_app_settings_file_path(&app).await?;
// let path = path.as_ref(); let mut needs_migration = false;
for entry in fs::read_dir(path).map_err(|e| InvokeError::from_anyhow(e.into()))? {
let path = entry
.map_err(|e| InvokeError::from_anyhow(e.into()))?
.path();
if let Ok(flag) = is_dir(&path) { // Check if this file exists.
files_and_dirs.push(DiskEntry { if !settings_path.exists() {
path: path.clone(), // Try the backwards compatible path.
children: if flag { // TODO: Remove this after a few releases.
Some(read_dir_recursive(path.to_str().expect("No path"))?) let app_config_dir = app.path().app_config_dir()?;
} else { settings_path = format!(
None "{}user.toml",
}, app_config_dir.display().to_string().trim_end_matches('/')
name: path )
.file_name() .into();
.map(|name| name.to_string_lossy()) needs_migration = true;
.map(|name| name.to_string()), // Check if this path exists.
}); if !settings_path.exists() {
let mut default = Configuration::default();
default.settings.project.directory = get_initial_default_dir(app.clone())?;
// Return the default configuration.
return Ok(default);
} }
} }
Ok(files_and_dirs)
}
/// This command returns a string that is the contents of a file at the path. let contents = tokio::fs::read_to_string(&settings_path)
#[tauri::command] .await
fn read_txt_file(path: &str) -> Result<String, InvokeError> {
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|e| InvokeError::from_anyhow(e.into()))?; .map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(contents) 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(
app_settings: Configuration,
project_name: &str,
) -> Result<PathBuf, InvokeError> {
let project_dir = app_settings.settings.project.directory.join(project_name);
if !project_dir.exists() {
tokio::fs::create_dir_all(&project_dir)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(project_dir.join(PROJECT_SETTINGS_FILE_NAME))
}
#[tauri::command]
async fn read_project_settings_file(
app_settings: Configuration,
project_name: &str,
) -> Result<ProjectConfiguration, InvokeError> {
let settings_path = get_project_settings_file_path(app_settings, project_name).await?;
// Check if this file exists.
if !settings_path.exists() {
// Return the default configuration.
return Ok(ProjectConfiguration::default());
}
let contents = tokio::fs::read_to_string(&settings_path)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
let parsed = ProjectConfiguration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?;
Ok(parsed)
}
#[tauri::command]
async fn write_project_settings_file(
app_settings: Configuration,
project_name: &str,
configuration: ProjectConfiguration,
) -> Result<(), InvokeError> {
let settings_path = get_project_settings_file_path(app_settings, project_name).await?;
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
tokio::fs::write(settings_path, contents.as_bytes())
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(())
}
/// Initialize the directory that holds all the projects.
#[tauri::command]
async fn initialize_project_directory(configuration: Configuration) -> Result<PathBuf, InvokeError> {
configuration
.ensure_project_directory_exists()
.await
.map_err(InvokeError::from_anyhow)
}
/// Create a new project directory.
#[tauri::command]
async fn create_new_project_directory(
configuration: Configuration,
project_name: &str,
initial_code: Option<&str>,
) -> Result<Project, InvokeError> {
configuration
.create_new_project_directory(project_name, initial_code)
.await
.map_err(InvokeError::from_anyhow)
}
/// List all the projects in the project directory.
#[tauri::command]
async fn list_projects(configuration: Configuration) -> Result<Vec<Project>, InvokeError> {
configuration.list_projects().await.map_err(InvokeError::from_anyhow)
}
/// Get information about a project.
#[tauri::command]
async fn get_project_info(configuration: Configuration, project_path: &str) -> Result<Project, InvokeError> {
configuration
.get_project_info(project_path)
.await
.map_err(InvokeError::from_anyhow)
}
/// Parse the project route.
#[tauri::command]
async fn parse_project_route(configuration: Configuration, route: &str) -> Result<ProjectRoute, InvokeError> {
ProjectRoute::from_route(&configuration, route).map_err(InvokeError::from_anyhow)
}
#[tauri::command]
async fn read_dir_recursive(path: &str) -> Result<FileEntry, InvokeError> {
kcl_lib::settings::utils::walk_dir(&Path::new(path).to_path_buf())
.await
.map_err(InvokeError::from_anyhow)
} }
/// This command instantiates a new window with auth. /// This command instantiates a new window with auth.
@ -103,8 +237,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
let auth_client = oauth2::basic::BasicClient::new( let auth_client = oauth2::basic::BasicClient::new(
oauth2::ClientId::new(client_id), oauth2::ClientId::new(client_id),
None, None,
oauth2::AuthUrl::new(format!("{host}/authorize")) oauth2::AuthUrl::new(format!("{host}/authorize")).map_err(|e| InvokeError::from_anyhow(e.into()))?,
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
Some( Some(
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token")) oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
.map_err(|e| InvokeError::from_anyhow(e.into()))?, .map_err(|e| InvokeError::from_anyhow(e.into()))?,
@ -132,12 +265,10 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
// and bypass the shell::open call as it fails on GitHub Actions. // and bypass the shell::open call as it fails on GitHub Actions.
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok(); let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
if e2e_tauri_enabled { if e2e_tauri_enabled {
println!( println!("E2E_TAURI_ENABLED is set, won't open {} externally", auth_uri.secret());
"E2E_TAURI_ENABLED is set, won't open {} externally", tokio::fs::write("/tmp/kittycad_user_code", details.user_code().secret())
auth_uri.secret() .await
); .map_err(|e| InvokeError::from_anyhow(e.into()))?;
fs::write("/tmp/kittycad_user_code", details.user_code().secret())
.expect("Unable to write /tmp/kittycad_user_code file");
} else { } else {
app.shell() app.shell()
.open(auth_uri.secret(), None) .open(auth_uri.secret(), None)
@ -160,10 +291,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
///This command returns the KittyCAD user info given a token. ///This command returns the KittyCAD user info given a token.
/// The string returned from this method is the user info as a json string. /// The string returned from this method is the user info as a json string.
#[tauri::command] #[tauri::command]
async fn get_user( async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::User, InvokeError> {
token: Option<String>,
hostname: &str,
) -> Result<kittycad::types::User, InvokeError> {
// Use the host passed in if it's set. // Use the host passed in if it's set.
// Otherwise, use the default host. // Otherwise, use the default host.
let host = if hostname.is_empty() { let host = if hostname.is_empty() {
@ -183,7 +311,7 @@ async fn get_user(
println!("Getting user info..."); println!("Getting user info...");
// use kittycad library to fetch the user info from /user/me // use kittycad library to fetch the user info from /user/me
let mut client = kittycad::Client::new(token.unwrap()); let mut client = kittycad::Client::new(token);
if baseurl != DEFAULT_HOST { if baseurl != DEFAULT_HOST {
client.set_base_url(&baseurl); client.set_base_url(&baseurl);
@ -202,50 +330,185 @@ async fn get_user(
/// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169 /// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169
/// But with the Linux support removed since we don't need it for now. /// But with the Linux support removed since we don't need it for now.
#[tauri::command] #[tauri::command]
fn show_in_folder(path: String) { fn show_in_folder(path: &str) -> Result<(), InvokeError> {
#[cfg(target_os = "windows")] #[cfg(not(unix))]
{ {
Command::new("explorer") Command::new("explorer")
.args(["/select,", &path]) // The comma after select is not a typo .args(["/select,", &path]) // The comma after select is not a typo
.spawn() .spawn()
.unwrap(); .map_err(|e| InvokeError::from_anyhow(e.into()))?;
} }
#[cfg(target_os = "macos")] #[cfg(unix)]
{ {
Command::new("open").args(["-R", &path]).spawn().unwrap(); Command::new("open")
} .args(["-R", &path])
.spawn()
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
} }
fn main() { Ok(())
}
fn main() -> Result<()> {
tauri::Builder::default() tauri::Builder::default()
.setup(|_app| { .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,
])
.plugin(tauri_plugin_cli::init())
.setup(|app| {
// Do update things.
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {
use tauri::Manager; app.get_webview("main").unwrap().open_devtools();
_app.get_webview("main").unwrap().open_devtools();
} }
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
{ {
_app.handle() app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
.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() {
source_path = Some(Path::new(value).to_path_buf());
}
}
}
Err(err) => {
return Err(anyhow::anyhow!("Error parsing CLI arguments: {:?}", err).into());
}
}
// 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 {
// Fix for "." path, which is the current directory.
let source_path = if source_path == Path::new(".") {
std::env::current_dir()
.map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
} else {
source_path
};
// If the path is a directory, let's assume it is a project directory.
if source_path.is_dir() {
// Load the details about the project from the path.
let project = Project::from_path(&source_path).await.map_err(|e| {
anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e)
})?;
if verbose {
println!("Project loaded from path: {}", source_path.display());
}
// Create the default file in the project.
// Write the initial project file.
let project_file = source_path.join(DEFAULT_PROJECT_KCL_FILE);
tokio::fs::write(&project_file, vec![]).await?;
return Ok(ProjectState {
project,
current_file: Some(project_file.display().to_string()),
});
}
// We were given a file path, not a directory.
// Let's get the parent directory of the file.
let parent = source_path.parent().ok_or_else(|| {
anyhow::anyhow!(
"Error getting the parent directory of the file: {}",
source_path.display()
)
})?;
// Load the details about the project from the parent directory.
let project = Project::from_path(&parent).await.map_err(|e| {
anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e)
})?;
if verbose {
println!(
"Project loaded from path: {}, current file: {}",
parent.display(),
source_path.display()
);
}
Ok(ProjectState {
project,
current_file: Some(source_path.display().to_string()),
})
});
// 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));
Ok(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![ .register_uri_scheme_protocol("zoo-modeling-app", |_app, request| {
get_user, let path = request.uri().path();
login, dbg!(path);
read_toml, println!("Requesting path: {}", path);
read_txt_file,
read_dir_recursive, tauri::http::Response::builder().status(200).body(b"{}").unwrap()
show_in_folder, })
])
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.run(tauri::generate_context!()) .run(tauri::generate_context!())?;
.expect("error while running tauri application");
Ok(())
} }

21
src-tauri/src/state.rs Normal file
View File

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

View File

@ -50,10 +50,26 @@
}, },
"identifier": "dev.zoo.modeling-app", "identifier": "dev.zoo.modeling-app",
"plugins": { "plugins": {
"cli": {
"description": "Zoo Modeling App CLI",
"args": [
{
"short": "v",
"name": "verbose",
"description": "Verbosity level"
},
{
"name": "source",
"index": 1,
"takesValue": true
}
],
"subcommands": {}
},
"shell": { "shell": {
"open": true "open": true
} }
}, },
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"version": "0.18.1" "version": "0.19.3"
} }

View File

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

View File

@ -3,13 +3,12 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { CommandArgument } from 'lib/commandTypes' import { CommandArgument } from 'lib/commandTypes'
import { import {
ResolvedSelectionType,
canSubmitSelectionArg, canSubmitSelectionArg,
getSelectionType, getSelectionType,
getSelectionTypeDisplayText, getSelectionTypeDisplayText,
} from 'lib/selections' } from 'lib/selections'
import { modelingMachine } from 'machines/modelingMachine' import { modelingMachine } from 'machines/modelingMachine'
import { useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { StateFrom } from 'xstate' import { StateFrom } from 'xstate'
@ -30,13 +29,13 @@ function CommandBarSelectionInput({
const { commandBarState, commandBarSend } = useCommandsContext() const { commandBarState, commandBarSend } = useCommandsContext()
const [hasSubmitted, setHasSubmitted] = useState(false) const [hasSubmitted, setHasSubmitted] = useState(false)
const selection = useSelector(arg.machineActor, selectionSelector) const selection = useSelector(arg.machineActor, selectionSelector)
const [selectionsByType, setSelectionsByType] = useState< const initSelectionsByType = useCallback(() => {
'none' | ResolvedSelectionType[] const selectionRangeEnd = selection.codeBasedSelections[0]?.range[1]
>( return !selectionRangeEnd || selectionRangeEnd === code.length
selection.codeBasedSelections[0]?.range[1] === code.length
? 'none' ? 'none'
: getSelectionType(selection) : getSelectionType(selection)
) }, [selection, code])
const selectionsByType = initSelectionsByType()
const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>( const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>(
canSubmitSelectionArg(selectionsByType, arg) canSubmitSelectionArg(selectionsByType, arg)
) )
@ -51,17 +50,14 @@ function CommandBarSelectionInput({
inputRef.current?.focus() inputRef.current?.focus()
}, [selection, inputRef]) }, [selection, inputRef])
useEffect(() => {
setSelectionsByType(
selection.codeBasedSelections[0]?.range[1] === code.length
? 'none'
: getSelectionType(selection)
)
}, [selection])
// Fast-forward through this arg if it's marked as skippable // Fast-forward through this arg if it's marked as skippable
// and we have a valid selection already // and we have a valid selection already
useEffect(() => { useEffect(() => {
console.log('selection input effect', {
selectionsByType,
canSubmitSelection,
arg,
})
setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg)) setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
const argValue = commandBarState.context.argumentsToSubmit[arg.name] const argValue = commandBarState.context.argumentsToSubmit[arg.name]
if (canSubmitSelection && arg.skip && argValue === undefined) { if (canSubmitSelection && arg.skip && argValue === undefined) {

View File

@ -15,10 +15,10 @@ import {
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine' import { fileMachine } from 'machines/fileMachine'
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs' import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
import { readProject } from 'lib/tauriFS'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { join, sep } from '@tauri-apps/api/path' import { join, sep } from '@tauri-apps/api/path'
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants' import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
import { getProjectInfo } from 'lib/tauri'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -62,7 +62,7 @@ export const FileMachineProvider = ({
services: { services: {
readFiles: async (context: ContextFrom<typeof fileMachine>) => { readFiles: async (context: ContextFrom<typeof fileMachine>) => {
const newFiles = isTauri() const newFiles = isTauri()
? await readProject(context.project.path) ? (await getProjectInfo(context.project.path)).children
: [] : []
return { return {
...context.project, ...context.project,

View File

@ -3,7 +3,7 @@ import { paths } from 'lib/paths'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { Dispatch, useEffect, useRef, useState } from 'react' import { Dispatch, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { Dialog, Disclosure } from '@headlessui/react' import { Dialog, Disclosure } from '@headlessui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons' import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
@ -133,18 +133,13 @@ const FileTreeItem = ({
project, project,
currentFile, currentFile,
fileOrDir, fileOrDir,
closePanel, onDoubleClick,
level = 0, level = 0,
}: { }: {
project?: IndexLoaderData['project'] project?: IndexLoaderData['project']
currentFile?: IndexLoaderData['file'] currentFile?: IndexLoaderData['file']
fileOrDir: FileEntry fileOrDir: FileEntry
closePanel: ( onDoubleClick?: () => void
focusableElement?:
| HTMLElement
| React.MutableRefObject<HTMLElement | null>
| undefined
) => void
level?: number level?: number
}) => { }) => {
const { send, context } = useFileContext() const { send, context } = useFileContext()
@ -186,7 +181,7 @@ const FileTreeItem = ({
// Open kcl files // Open kcl files
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`) navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
} }
closePanel() onDoubleClick?.()
} }
return ( return (
@ -194,8 +189,10 @@ const FileTreeItem = ({
{fileOrDir.children === undefined ? ( {fileOrDir.children === undefined ? (
<li <li
className={ className={
'group m-0 p-0 border-solid border-0 hover:text-primary hover:bg-primary/5 focus-within:bg-primary/5 ' + '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 ' +
(isCurrentFile ? '!bg-primary/10 !text-primary' : '') (isCurrentFile
? '!bg-primary/10 !text-primary dark:!bg-primary/20 dark:!text-inherit'
: '')
} }
> >
{!isRenaming ? ( {!isRenaming ? (
@ -227,9 +224,9 @@ const FileTreeItem = ({
{!isRenaming ? ( {!isRenaming ? (
<Disclosure.Button <Disclosure.Button
className={ className={
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5' + ' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5 dark:hover:text-inherit dark:hover:bg-primary/10' +
(context.selectedDirectory.path.includes(fileOrDir.path) (context.selectedDirectory.path.includes(fileOrDir.path)
? ' ui-open:text-primary' ? ' ui-open:bg-primary/10'
: '') : '')
} }
style={{ paddingInlineStart: getIndentationCSS(level) }} style={{ paddingInlineStart: getIndentationCSS(level) }}
@ -293,7 +290,7 @@ const FileTreeItem = ({
fileOrDir={child} fileOrDir={child}
project={project} project={project}
currentFile={currentFile} currentFile={currentFile}
closePanel={closePanel} onDoubleClick={onDoubleClick}
level={level + 1} level={level + 1}
key={level + '-' + child.path} key={level + '-' + child.path}
/> />
@ -325,20 +322,8 @@ interface FileTreeProps {
) => void ) => void
} }
export const FileTree = ({ export const FileTreeMenu = () => {
className = '', const { send } = useFileContext()
file,
closePanel,
}: FileTreeProps) => {
const { send, context } = useFileContext()
const docuemntHasFocus = useDocumentHasFocus()
useHotkeys('meta + n', createFile)
useHotkeys('meta + shift + n', createFolder)
// Refresh the file tree when the document gets focus
useEffect(() => {
send({ type: 'Refresh' })
}, [docuemntHasFocus])
async function createFile() { async function createFile() {
send({ type: 'Create file', data: { name: '', makeDir: false } }) send({ type: 'Create file', data: { name: '', makeDir: false } })
@ -348,10 +333,11 @@ export const FileTree = ({
send({ type: 'Create file', data: { name: '', makeDir: true } }) send({ type: 'Create file', data: { name: '', makeDir: true } })
} }
useHotkeys('meta + n', createFile)
useHotkeys('meta + shift + n', createFolder)
return ( return (
<div className={className}> <>
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
<ActionButton <ActionButton
Element="button" Element="button"
icon={{ icon={{
@ -381,7 +367,37 @@ export const FileTree = ({
Create folder Create folder
</Tooltip> </Tooltip>
</ActionButton> </ActionButton>
</>
)
}
export const FileTree = ({ className = '', closePanel }: FileTreeProps) => {
return (
<div className={className}>
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
<FileTreeMenu />
</div> </div>
<FileTreeInner onDoubleClick={closePanel} />
</div>
)
}
export const FileTreeInner = ({
onDoubleClick,
}: {
onDoubleClick?: () => void
}) => {
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
const { send, context } = useFileContext()
const documentHasFocus = useDocumentHasFocus()
// Refresh the file tree when the document gets focus
useEffect(() => {
send({ type: 'Refresh' })
}, [documentHasFocus])
return (
<div className="overflow-auto max-h-full pb-12"> <div className="overflow-auto max-h-full pb-12">
<ul <ul
className="m-0 p-0 text-sm" className="m-0 p-0 text-sm"
@ -392,14 +408,13 @@ export const FileTree = ({
{sortProject(context.project.children || []).map((fileOrDir) => ( {sortProject(context.project.children || []).map((fileOrDir) => (
<FileTreeItem <FileTreeItem
project={context.project} project={context.project}
currentFile={file} currentFile={loaderData?.file}
fileOrDir={fileOrDir} fileOrDir={fileOrDir}
closePanel={closePanel} onDoubleClick={onDoubleClick}
key={fileOrDir.path} key={fileOrDir.path}
/> />
))} ))}
</ul> </ul>
</div> </div>
</div>
) )
} }

View File

@ -94,10 +94,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
if (isInProject) { if (isInProject) {
navigate('onboarding') navigate('onboarding')
} else { } else {
createAndOpenNewProject( createAndOpenNewProject(navigate)
settings.context.app.projectDirectory.current,
navigate
)
} }
}} }}
> >

View File

@ -3,7 +3,7 @@ import type * as LSP from 'vscode-languageserver-protocol'
import React, { createContext, useMemo, useEffect, useContext } from 'react' import React, { createContext, useMemo, useEffect, useContext } from 'react'
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec' import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
import Client from '../editor/plugins/lsp/client' import Client from '../editor/plugins/lsp/client'
import { DEV, TEST } from 'env' import { TEST, VITE_KC_API_BASE_URL } from 'env'
import kclLanguage from 'editor/plugins/lsp/kcl/language' import kclLanguage from 'editor/plugins/lsp/kcl/language'
import { copilotPlugin } from 'editor/plugins/lsp/copilot' import { copilotPlugin } from 'editor/plugins/lsp/copilot'
import { useStore } from 'useStore' import { useStore } from 'useStore'
@ -103,7 +103,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
wasmUrl: wasmUrl(), wasmUrl: wasmUrl(),
token: token, token: token,
baseUnit: defaultUnit.current, baseUnit: defaultUnit.current,
devMode: DEV, apiBaseUrl: VITE_KC_API_BASE_URL,
} }
lspWorker.postMessage({ lspWorker.postMessage({
worker: LspWorker.Kcl, worker: LspWorker.Kcl,
@ -177,7 +177,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: CopilotWorkerOptions = { const initEvent: CopilotWorkerOptions = {
wasmUrl: wasmUrl(), wasmUrl: wasmUrl(),
token: token, token: token,
devMode: DEV, apiBaseUrl: VITE_KC_API_BASE_URL,
} }
lspWorker.postMessage({ lspWorker.postMessage({
worker: LspWorker.Copilot, worker: LspWorker.Copilot,

View File

@ -56,6 +56,7 @@ import toast from 'react-hot-toast'
import { EditorSelection } from '@uiw/react-codemirror' import { EditorSelection } from '@uiw/react-codemirror'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
@ -77,16 +78,22 @@ export const ModelingMachineProvider = ({
auth, auth,
settings: { settings: {
context: { context: {
app: { theme }, app: { theme, enableSSAO },
modeling: { defaultUnit, highlightEdges }, modeling: { defaultUnit, highlightEdges },
}, },
}, },
} = useSettingsAuthContext() } = useSettingsAuthContext()
const token = auth?.context?.token const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null) const streamRef = useRef<HTMLDivElement>(null)
let [searchParams] = useSearchParams()
const pool = searchParams.get('pool')
useSetupEngineManager(streamRef, token, { useSetupEngineManager(streamRef, token, {
pool: pool,
theme: theme.current, theme: theme.current,
highlightEdges: highlightEdges.current, highlightEdges: highlightEdges.current,
enableSSAO: enableSSAO.current,
}) })
const { htmlRef } = useStore((s) => ({ const { htmlRef } = useStore((s) => ({
htmlRef: s.htmlRef, htmlRef: s.htmlRef,
@ -267,10 +274,12 @@ export const ModelingMachineProvider = ({
'has valid extrude selection': ({ selectionRanges }) => { 'has valid extrude selection': ({ selectionRanges }) => {
// A user can begin extruding if they either have 1+ faces selected or nothing selected // A user can begin extruding if they either have 1+ faces selected or nothing selected
// TODO: I believe this guard only allows for extruding a single face at a time // TODO: I believe this guard only allows for extruding a single face at a time
if (selectionRanges.codeBasedSelections.length < 1) return false
const isPipe = isSketchPipe(selectionRanges) const isPipe = isSketchPipe(selectionRanges)
if (isSelectionLastLine(selectionRanges, codeManager.code)) if (
selectionRanges.codeBasedSelections.length === 0 ||
isSelectionLastLine(selectionRanges, codeManager.code)
)
return true return true
if (!isPipe) return false if (!isPipe) return false

View File

@ -16,7 +16,6 @@ import {
EditorView, EditorView,
dropCursor, dropCursor,
drawSelection, drawSelection,
ViewUpdate,
} from '@codemirror/view' } from '@codemirror/view'
import { import {
indentWithTab, indentWithTab,
@ -191,9 +190,6 @@ export const KclEditorPane = () => {
return extensions return extensions
}, [kclLSP, copilotLSP, textWrapping.current, cursorBlinking.current]) }, [kclLSP, copilotLSP, textWrapping.current, cursorBlinking.current])
let debounceTimer: ReturnType<typeof setTimeout> | null = null
const updateDelay = 100
return ( return (
<div <div
id="code-mirror-override" id="code-mirror-override"
@ -206,17 +202,6 @@ export const KclEditorPane = () => {
onCreateEditor={(_editorView) => onCreateEditor={(_editorView) =>
editorManager.setEditorView(_editorView) editorManager.setEditorView(_editorView)
} }
onUpdate={(view: ViewUpdate) => {
// debounce the view update.
// otherwise it is laggy for typing.
if (debounceTimer) {
clearTimeout(debounceTimer)
}
debounceTimer = setTimeout(() => {
editorManager.handleOnViewUpdate(view)
}, updateDelay)
}}
indentWithTab={false} indentWithTab={false}
basicSetup={false} basicSetup={false}
/> />

View File

@ -10,21 +10,32 @@ import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEdito
import { CustomIconName } from 'components/CustomIcon' import { CustomIconName } from 'components/CustomIcon'
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane' import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import type { PaneType } from 'useStore'
import { MemoryPane } from './MemoryPane' import { MemoryPane } from './MemoryPane'
import { KclErrorsPane, LogsPane } from './LoggingPanes' import { KclErrorsPane, LogsPane } from './LoggingPanes'
import { DebugPane } from './DebugPane' import { DebugPane } from './DebugPane'
import { FileTreeInner, FileTreeMenu } from 'components/FileTree'
export type Pane = { export type SidebarType =
id: PaneType | 'code'
| 'debug'
| 'export'
| 'files'
| 'kclErrors'
| 'logs'
| 'lspMessages'
| 'variables'
export type SidebarPane = {
id: SidebarType
title: string title: string
icon: CustomIconName | IconDefinition icon: CustomIconName | IconDefinition
keybinding: string
Content: ReactNode | React.FC Content: ReactNode | React.FC
Menu?: ReactNode | React.FC Menu?: ReactNode | React.FC
keybinding: string hideOnPlatform?: 'desktop' | 'web'
} }
export const topPanes: Pane[] = [ export const topPanes: SidebarPane[] = [
{ {
id: 'code', id: 'code',
title: 'KCL Code', title: 'KCL Code',
@ -33,9 +44,18 @@ export const topPanes: Pane[] = [
keybinding: 'shift + c', keybinding: 'shift + c',
Menu: KclEditorMenu, Menu: KclEditorMenu,
}, },
{
id: 'files',
title: 'Project Files',
icon: 'folder',
Content: FileTreeInner,
keybinding: 'shift + f',
Menu: FileTreeMenu,
hideOnPlatform: 'web',
},
] ]
export const bottomPanes: Pane[] = [ export const bottomPanes: SidebarPane[] = [
{ {
id: 'variables', id: 'variables',
title: 'Variables', title: 'Variables',

View File

@ -2,13 +2,19 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable' import { Resizable } from 're-resizable'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { PaneType, useStore } from 'useStore' import { useStore } from 'useStore'
import { Tab } from '@headlessui/react' import { Tab } from '@headlessui/react'
import { Pane, bottomPanes, topPanes } from './ModelingPanes' import {
SidebarPane,
SidebarType,
bottomPanes,
topPanes,
} from './ModelingPanes'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
import { ActionIcon } from 'components/ActionIcon' import { ActionIcon } from 'components/ActionIcon'
import styles from './ModelingSidebar.module.css' import styles from './ModelingSidebar.module.css'
import { ModelingPane } from './ModelingPane' import { ModelingPane } from './ModelingPane'
import { isTauri } from 'lib/isTauri'
interface ModelingSidebarProps { interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40' paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -52,7 +58,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
} }
interface ModelingSidebarSectionProps { interface ModelingSidebarSectionProps {
panes: Pane[] panes: SidebarPane[]
alignButtons?: 'start' | 'end' alignButtons?: 'start' | 'end'
} }
@ -69,11 +75,11 @@ function ModelingSidebarSection({
})) }))
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane)) const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
const [currentPane, setCurrentPane] = useState( const [currentPane, setCurrentPane] = useState(
foundOpenPane || ('none' as PaneType | 'none') foundOpenPane || ('none' as SidebarType | 'none')
) )
const togglePane = useCallback( const togglePane = useCallback(
(newPane: PaneType | 'none') => { (newPane: SidebarType | 'none') => {
if (newPane === 'none') { if (newPane === 'none') {
setOpenPanes(openPanes.filter((p) => p !== currentPane)) setOpenPanes(openPanes.filter((p) => p !== currentPane))
setCurrentPane('none') setCurrentPane('none')
@ -90,9 +96,15 @@ function ModelingSidebarSection({
// Filter out the debug panel if it's not supposed to be shown // Filter out the debug panel if it's not supposed to be shown
// TODO: abstract out for allowing user to configure which panes to show // TODO: abstract out for allowing user to configure which panes to show
const filteredPanes = showDebugPanel.current const filteredPanes = (
? panes showDebugPanel.current ? panes : panes.filter((pane) => pane.id !== 'debug')
: panes.filter((pane) => pane.id !== 'debug') ).filter(
(pane) =>
!pane.hideOnPlatform ||
(isTauri()
? pane.hideOnPlatform === 'web'
: pane.hideOnPlatform === 'desktop')
)
useEffect(() => { useEffect(() => {
if ( if (
!showDebugPanel.current && !showDebugPanel.current &&
@ -168,8 +180,8 @@ function ModelingSidebarSection({
} }
interface ModelingPaneButtonProps { interface ModelingPaneButtonProps {
paneConfig: Pane paneConfig: SidebarPane
currentPane: PaneType | 'none' currentPane: SidebarType | 'none'
togglePane: () => void togglePane: () => void
} }

View File

@ -1,5 +1,4 @@
import { FormEvent, useEffect, useRef, useState } from 'react' import { FormEvent, useEffect, useRef, useState } from 'react'
import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
@ -9,11 +8,11 @@ import {
faTrashAlt, faTrashAlt,
faX, faX,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { getPartsCount, readProject } from '../lib/tauriFS'
import { FILE_EXT } from 'lib/constants' import { FILE_EXT } from 'lib/constants'
import { Dialog } from '@headlessui/react' import { Dialog } from '@headlessui/react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { Project } from 'wasm-lib/kcl/bindings/Project'
function ProjectCard({ function ProjectCard({
project, project,
@ -21,17 +20,17 @@ function ProjectCard({
handleDeleteProject, handleDeleteProject,
...props ...props
}: { }: {
project: ProjectWithEntryPointMetadata project: Project
handleRenameProject: ( handleRenameProject: (
e: FormEvent<HTMLFormElement>, e: FormEvent<HTMLFormElement>,
f: ProjectWithEntryPointMetadata f: Project
) => Promise<void> ) => Promise<void>
handleDeleteProject: (f: ProjectWithEntryPointMetadata) => Promise<void> handleDeleteProject: (f: Project) => Promise<void>
}) { }) {
useHotkeys('esc', () => setIsEditing(false)) useHotkeys('esc', () => setIsEditing(false))
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false) const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const [numberOfParts, setNumberOfParts] = useState(1) const [numberOfFiles, setNumberOfFiles] = useState(1)
const [numberOfFolders, setNumberOfFolders] = useState(0) const [numberOfFolders, setNumberOfFolders] = useState(0)
let inputRef = useRef<HTMLInputElement>(null) let inputRef = useRef<HTMLInputElement>(null)
@ -41,7 +40,8 @@ function ProjectCard({
void handleRenameProject(e, project).then(() => setIsEditing(false)) void handleRenameProject(e, project).then(() => setIsEditing(false))
} }
function getDisplayedTime(date: Date) { function getDisplayedTime(dateStr: string) {
const date = new Date(dateStr)
const startOfToday = new Date() const startOfToday = new Date()
startOfToday.setHours(0, 0, 0, 0) startOfToday.setHours(0, 0, 0, 0)
return date.getTime() < startOfToday.getTime() return date.getTime() < startOfToday.getTime()
@ -50,15 +50,12 @@ function ProjectCard({
} }
useEffect(() => { useEffect(() => {
async function getNumberOfParts() { async function getNumberOfFiles() {
const { kclFileCount, kclDirCount } = getPartsCount( setNumberOfFiles(project.kcl_file_count)
await readProject(project.path) setNumberOfFolders(project.directory_count)
)
setNumberOfParts(kclFileCount)
setNumberOfFolders(kclDirCount)
} }
void getNumberOfParts() void getNumberOfFiles()
}, [project.path]) }, [project.kcl_file_count, project.directory_count])
useEffect(() => { useEffect(() => {
if (inputRef.current) { if (inputRef.current) {
@ -129,7 +126,7 @@ function ProjectCard({
{project.name?.replace(FILE_EXT, '')} {project.name?.replace(FILE_EXT, '')}
</Link> </Link>
<span className="text-chalkboard-60 text-xs"> <span className="text-chalkboard-60 text-xs">
{numberOfParts} part{numberOfParts === 1 ? '' : 's'}{' '} {numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '}
{numberOfFolders > 0 && {numberOfFolders > 0 &&
`/ ${numberOfFolders} folder${ `/ ${numberOfFolders} folder${
numberOfFolders === 1 ? '' : 's' numberOfFolders === 1 ? '' : 's'
@ -137,8 +134,8 @@ function ProjectCard({
</span> </span>
<span className="text-chalkboard-60 text-xs"> <span className="text-chalkboard-60 text-xs">
Edited{' '} Edited{' '}
{project.entrypointMetadata.mtime {project.metadata && project.metadata?.modified
? getDisplayedTime(project.entrypointMetadata.mtime) ? getDisplayedTime(project.metadata.modified)
: 'never'} : 'never'}
</span> </span>
<div className="absolute z-10 bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100"> <div className="absolute z-10 bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">

View File

@ -1,10 +1,10 @@
import { fireEvent, render, screen } from '@testing-library/react' import { fireEvent, render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
import ProjectSidebarMenu from './ProjectSidebarMenu' import ProjectSidebarMenu from './ProjectSidebarMenu'
import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { SettingsAuthProviderJest } from './SettingsAuthProvider' import { SettingsAuthProviderJest } from './SettingsAuthProvider'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
import { CommandBarProvider } from './CommandBar/CommandBarProvider' import { CommandBarProvider } from './CommandBar/CommandBarProvider'
import { Project } from 'wasm-lib/kcl/bindings/Project'
const now = new Date() const now = new Date()
const projectWellFormed = { const projectWellFormed = {
@ -14,29 +14,17 @@ const projectWellFormed = {
{ {
name: 'main.kcl', name: 'main.kcl',
path: '/some/path/Simple Box/main.kcl', path: '/some/path/Simple Box/main.kcl',
children: [],
}, },
], ],
entrypointMetadata: { metadata: {
atime: now, created: now.toISOString(),
blksize: 32, modified: now.toISOString(),
blocks: 32,
birthtime: now,
dev: 1,
gid: 1,
ino: 1,
isDirectory: false,
isFile: true,
isSymlink: false,
mode: 1,
mtime: now,
nlink: 1,
readonly: false,
rdev: 1,
size: 32, size: 32,
uid: 1,
fileAttributes: null,
}, },
} satisfies ProjectWithEntryPointMetadata kcl_file_count: 1,
directory_count: 0,
} satisfies Project
describe('ProjectSidebarMenu tests', () => { describe('ProjectSidebarMenu tests', () => {
test('Renders the project name', () => { test('Renders the project name', () => {

View File

@ -133,13 +133,13 @@ function ProjectMenuPopover({
<p className="m-0 text-mono" data-testid="projectName"> <p className="m-0 text-mono" data-testid="projectName">
{project?.name ? project.name : APP_NAME} {project?.name ? project.name : APP_NAME}
</p> </p>
{project?.entrypointMetadata && ( {project?.metadata && project.metadata.created && (
<p <p
className="m-0 text-xs text-chalkboard-80 dark:text-chalkboard-40" className="m-0 text-xs text-chalkboard-80 dark:text-chalkboard-40"
data-testid="createdAt" data-testid="createdAt"
> >
Created{' '} Created{' '}
{project.entrypointMetadata.birthtime?.toLocaleDateString()} {new Date(project.metadata.created).toLocaleDateString()}
</p> </p>
)} )}
</div> </div>

View File

@ -7,7 +7,12 @@ import React, { createContext, useEffect } from 'react'
import useStateMachineCommands from '../hooks/useStateMachineCommands' import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { settingsMachine } from 'machines/settingsMachine' import { settingsMachine } from 'machines/settingsMachine'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { getThemeColorForEngine, setThemeClass, Themes } from 'lib/theme' import {
getThemeColorForEngine,
getOppositeTheme,
setThemeClass,
Themes,
} from 'lib/theme'
import decamelize from 'decamelize' import decamelize from 'decamelize'
import { import {
AnyStateMachine, AnyStateMachine,
@ -99,6 +104,9 @@ export const SettingsAuthProviderBase = ({
{ {
context: loadedSettings, context: loadedSettings,
actions: { actions: {
//TODO: batch all these and if that's difficult to do from tsx,
// make it easy to do
setClientSideSceneUnits: (context, event) => { setClientSideSceneUnits: (context, event) => {
const newBaseUnit = const newBaseUnit =
event.type === 'set.modeling.defaultUnit' event.type === 'set.modeling.defaultUnit'
@ -115,6 +123,16 @@ export const SettingsAuthProviderBase = ({
color: getThemeColorForEngine(context.app.theme.current), color: getThemeColorForEngine(context.app.theme.current),
}, },
}) })
const opposingTheme = getOppositeTheme(context.app.theme.current)
engineCommandManager.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'set_default_system_properties',
color: getThemeColorForEngine(opposingTheme),
},
})
}, },
setEngineEdges: (context) => { setEngineEdges: (context) => {
engineCommandManager.sendSceneCommand({ engineCommandManager.sendSceneCommand({
@ -150,7 +168,7 @@ export const SettingsAuthProviderBase = ({
}, },
'Execute AST': () => kclManager.executeCode(true), 'Execute AST': () => kclManager.executeCode(true),
persistSettings: (context) => persistSettings: (context) =>
saveSettings(context, loadedProject?.project?.path), saveSettings(context, loadedProject?.project?.name),
}, },
} }
) )

View File

@ -39,6 +39,8 @@ const CompletionItemKindMap = Object.fromEntries(
) as Record<CompletionItemKind, string> ) as Record<CompletionItemKind, string>
const changesDelay = 600 const changesDelay = 600
let debounceTimer: ReturnType<typeof setTimeout> | null = null
const updateDelay = 100
export class LanguageServerPlugin implements PluginValue { export class LanguageServerPlugin implements PluginValue {
public client: LanguageServerClient public client: LanguageServerClient
@ -47,6 +49,7 @@ export class LanguageServerPlugin implements PluginValue {
public workspaceFolders: LSP.WorkspaceFolder[] public workspaceFolders: LSP.WorkspaceFolder[]
private documentVersion: number private documentVersion: number
private foldingRanges: LSP.FoldingRange[] | null = null private foldingRanges: LSP.FoldingRange[] | null = null
private viewUpdate: ViewUpdate | null = null
private _defferer = deferExecution((code: string) => { private _defferer = deferExecution((code: string) => {
try { try {
// Update the state (not the editor) with the new code. // Update the state (not the editor) with the new code.
@ -57,8 +60,9 @@ export class LanguageServerPlugin implements PluginValue {
}, },
contentChanges: [{ text: code }], contentChanges: [{ text: code }],
}) })
if (editorManager.editorView) {
//editorManager.handleOnViewUpdate(editorManager.editorView) if (this.viewUpdate) {
editorManager.handleOnViewUpdate(this.viewUpdate)
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@ -83,14 +87,27 @@ export class LanguageServerPlugin implements PluginValue {
}) })
} }
update({ docChanged }: ViewUpdate) { update(viewUpdate: ViewUpdate) {
if (!docChanged) return this.viewUpdate = viewUpdate
if (!viewUpdate.docChanged) {
// debounce the view update.
// otherwise it is laggy for typing.
if (debounceTimer) {
clearTimeout(debounceTimer)
}
debounceTimer = setTimeout(() => {
editorManager.handleOnViewUpdate(viewUpdate)
}, updateDelay)
return
}
const newCode = this.view.state.doc.toString() const newCode = this.view.state.doc.toString()
codeManager.code = newCode codeManager.code = newCode
codeManager.writeToFile() codeManager.writeToFile()
kclManager.executeCode() kclManager.executeCode()
this.sendChange({ this.sendChange({
documentText: newCode, documentText: newCode,
}) })

View File

@ -8,13 +8,13 @@ export interface KclWorkerOptions {
wasmUrl: string wasmUrl: string
token: string token: string
baseUnit: UnitLength baseUnit: UnitLength
devMode: boolean apiBaseUrl: string
} }
export interface CopilotWorkerOptions { export interface CopilotWorkerOptions {
wasmUrl: string wasmUrl: string
token: string token: string
devMode: boolean apiBaseUrl: string
} }
export enum LspWorkerEventType { export enum LspWorkerEventType {

View File

@ -28,11 +28,11 @@ const initialise = async (wasmUrl: string) => {
export async function copilotLspRun( export async function copilotLspRun(
config: ServerConfig, config: ServerConfig,
token: string, token: string,
devMode: boolean = false baseUrl: string
) { ) {
try { try {
console.log('starting copilot lsp') console.log('starting copilot lsp')
await copilot_lsp_run(config, token, devMode) await copilot_lsp_run(config, token, baseUrl)
} catch (e: any) { } catch (e: any) {
console.log('copilot lsp failed', e) console.log('copilot lsp failed', e)
// We can't restart here because a moved value, we should do this another way. // We can't restart here because a moved value, we should do this another way.
@ -44,11 +44,11 @@ export async function kclLspRun(
engineCommandManager: EngineCommandManager | null, engineCommandManager: EngineCommandManager | null,
token: string, token: string,
baseUnit: string, baseUnit: string,
devMode: boolean = false baseUrl: string
) { ) {
try { try {
console.log('start kcl lsp') console.log('start kcl lsp')
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, devMode) await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl)
} catch (e: any) { } catch (e: any) {
console.log('kcl lsp failed', e) console.log('kcl lsp failed', e)
// We can't restart here because a moved value, we should do this another way. // We can't restart here because a moved value, we should do this another way.
@ -80,12 +80,12 @@ onmessage = function (event) {
null, null,
kclData.token, kclData.token,
kclData.baseUnit, kclData.baseUnit,
kclData.devMode kclData.apiBaseUrl
) )
break break
case LspWorker.Copilot: case LspWorker.Copilot:
let copilotData = eventData as CopilotWorkerOptions let copilotData = eventData as CopilotWorkerOptions
copilotLspRun(config, copilotData.token, copilotData.devMode) copilotLspRun(config, copilotData.token, copilotData.apiBaseUrl)
break break
} }
}) })

View File

@ -9,11 +9,15 @@ export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>, streamRef: React.RefObject<HTMLDivElement>,
token?: string, token?: string,
settings = { settings = {
pool: null,
theme: Themes.System, theme: Themes.System,
highlightEdges: true, highlightEdges: true,
enableSSAO: true,
} as { } as {
pool: string | null
theme: Themes theme: Themes
highlightEdges: boolean highlightEdges: boolean
enableSSAO: boolean
} }
) { ) {
const { const {
@ -33,6 +37,12 @@ export function useSetupEngineManager(
const hasSetNonZeroDimensions = useRef<boolean>(false) const hasSetNonZeroDimensions = useRef<boolean>(false)
if (settings.pool) {
// override the pool param (?pool=) to request a specific engine instance
// from a particular pool.
engineCommandManager.pool = settings.pool
}
useLayoutEffect(() => { useLayoutEffect(() => {
// Load the engine command manager once with the initial width and height, // Load the engine command manager once with the initial width and height,
// then we do not want to reload it. // then we do not want to reload it.

View File

@ -4,7 +4,7 @@ import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave' import { exportSave } from 'lib/exportSave'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { getNodePathFromSourceRange } from 'lang/queryAst' import { getNodePathFromSourceRange } from 'lang/queryAst'
import { Themes, getThemeColorForEngine } from 'lib/theme' import { Themes, getThemeColorForEngine, getOppositeTheme } from 'lib/theme'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
let lastMessage = '' let lastMessage = ''
@ -888,6 +888,7 @@ export class EngineCommandManager {
sceneCommandArtifacts: ArtifactMap = {} sceneCommandArtifacts: ArtifactMap = {}
outSequence = 1 outSequence = 1
inSequence = 1 inSequence = 1
pool?: string
engineConnection?: EngineConnection engineConnection?: EngineConnection
defaultPlanes: DefaultPlanes | null = null defaultPlanes: DefaultPlanes | null = null
commandLogs: CommandLog[] = [] commandLogs: CommandLog[] = []
@ -914,8 +915,9 @@ export class EngineCommandManager {
callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] = callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] =
[] []
constructor() { constructor(pool?: string) {
this.engineConnection = undefined this.engineConnection = undefined
this.pool = pool
} }
private _camControlsCameraChange = () => {} private _camControlsCameraChange = () => {}
@ -941,6 +943,7 @@ export class EngineCommandManager {
settings = { settings = {
theme: Themes.Dark, theme: Themes.Dark,
highlightEdges: true, highlightEdges: true,
enableSSAO: true,
}, },
}: { }: {
setMediaStream: (stream: MediaStream) => void setMediaStream: (stream: MediaStream) => void
@ -953,6 +956,7 @@ export class EngineCommandManager {
settings?: { settings?: {
theme: Themes theme: Themes
highlightEdges: boolean highlightEdges: boolean
enableSSAO: boolean
} }
}) { }) {
this.makeDefaultPlanes = makeDefaultPlanes this.makeDefaultPlanes = makeDefaultPlanes
@ -969,7 +973,9 @@ export class EngineCommandManager {
return return
} }
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}` const additionalSettings = settings.enableSSAO ? '&post_effect=ssao' : ''
const pool = this.pool == undefined ? '' : `&pool=${this.pool}`
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}${pool}`
this.engineConnection = new EngineConnection({ this.engineConnection = new EngineConnection({
engineCommandManager: this, engineCommandManager: this,
url, url,
@ -989,6 +995,18 @@ export class EngineCommandManager {
color: getThemeColorForEngine(settings.theme), color: getThemeColorForEngine(settings.theme),
}, },
}) })
// Sets the default line colors
const opposingTheme = getOppositeTheme(settings.theme)
this.sendSceneCommand({
cmd_id: uuidv4(),
type: 'modeling_cmd_req',
cmd: {
type: 'set_default_system_properties',
color: getThemeColorForEngine(opposingTheme),
},
})
// Set the edge lines visibility // Set the edge lines visibility
this.sendSceneCommand({ this.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
@ -1326,6 +1344,17 @@ export class EngineCommandManager {
this.lastArtifactMap = this.artifactMap this.lastArtifactMap = this.artifactMap
this.artifactMap = {} this.artifactMap = {}
await this.initPlanes() await this.initPlanes()
await this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'make_axes_gizmo',
clobber: false,
// If true, axes gizmo will be placed in the corner of the screen.
// If false, it will be placed at the origin of the scene.
gizmo_mode: true,
},
})
} }
subscribeTo<T extends ModelTypes>({ subscribeTo<T extends ModelTypes>({
event, event,

View File

@ -1,8 +1,7 @@
import { readFile, exists as tauriExists } from '@tauri-apps/plugin-fs' import { readFile, exists as tauriExists } from '@tauri-apps/plugin-fs'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { join } from '@tauri-apps/api/path' import { join } from '@tauri-apps/api/path'
import { invoke } from '@tauri-apps/api/core' import { readDirRecursive } from 'lib/tauri'
import { FileEntry } from 'lib/types'
/// FileSystemManager is a class that provides a way to read files from the local file system. /// FileSystemManager is a class that provides a way to read files from the local file system.
/// It assumes that you are in a project since it is solely used by the std lib /// It assumes that you are in a project since it is solely used by the std lib
@ -69,9 +68,7 @@ class FileSystemManager {
throw new Error(`Error joining dir: ${error}`) throw new Error(`Error joining dir: ${error}`)
}) })
.then((p) => { .then((p) => {
invoke<FileEntry[]>('read_dir_recursive', { readDirRecursive(p)
path: p,
})
.catch((error) => { .catch((error) => {
throw new Error(`Error reading dir: ${error}`) throw new Error(`Error reading dir: ${error}`)
}) })

View File

@ -10,7 +10,11 @@ import init, {
make_default_planes, make_default_planes,
coredump, coredump,
toml_stringify, toml_stringify,
toml_parse, default_app_settings,
parse_app_settings,
parse_project_settings,
default_project_settings,
parse_project_route,
} from '../wasm-lib/pkg/wasm_lib' } from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors' import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
@ -26,6 +30,9 @@ import { CoreDumpManager } from 'lib/coredump'
import openWindow from 'lib/openWindow' import openWindow from 'lib/openWindow'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { TEST } from 'env' import { TEST } from 'env'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Value } from '../wasm-lib/kcl/bindings/Value' export type { Value } from '../wasm-lib/kcl/bindings/Value'
@ -349,11 +356,53 @@ export function tomlStringify(toml: any): string {
} }
} }
export function tomlParse(toml: string): any { export function defaultAppSettings(): Configuration {
try { try {
const parsed: any = toml_parse(toml) const settings: Configuration = default_app_settings()
return parsed return settings
} catch (e: any) { } catch (e: any) {
throw new Error(`Error parsing toml: ${e}`) throw new Error(`Error getting default app settings: ${e}`)
}
}
export function parseAppSettings(toml: string): Configuration {
try {
const settings: Configuration = parse_app_settings(toml)
return settings
} catch (e: any) {
throw new Error(`Error parsing app settings: ${e}`)
}
}
export function defaultProjectSettings(): ProjectConfiguration {
try {
const settings: ProjectConfiguration = default_project_settings()
return settings
} catch (e: any) {
throw new Error(`Error getting default project settings: ${e}`)
}
}
export function parseProjectSettings(toml: string): ProjectConfiguration {
try {
const settings: ProjectConfiguration = parse_project_settings(toml)
return settings
} catch (e: any) {
throw new Error(`Error parsing project settings: ${e}`)
}
}
export function parseProjectRoute(
configuration: Configuration,
route_str: string
): ProjectRoute {
try {
const route: ProjectRoute = parse_project_route(
JSON.stringify(configuration),
route_str
)
return route
} catch (e: any) {
throw new Error(`Error parsing project route: ${e}`)
} }
} }

View File

@ -1,3 +1,5 @@
import { MouseControlType } from 'wasm-lib/kcl/bindings/MouseControlType'
const noModifiersPressed = (e: React.MouseEvent) => const noModifiersPressed = (e: React.MouseEvent) =>
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
@ -20,6 +22,29 @@ export const cameraSystems: CameraSystem[] = [
'AutoCAD', 'AutoCAD',
] ]
export function mouseControlsToCameraSystem(
mouseControl: MouseControlType | undefined
): CameraSystem | undefined {
switch (mouseControl) {
case 'kitty_cad':
return 'KittyCAD'
case 'on_shape':
return 'OnShape'
case 'trackpad_friendly':
return 'Trackpad Friendly'
case 'solidworks':
return 'Solidworks'
case 'nx':
return 'NX'
case 'creo':
return 'Creo'
case 'auto_cad':
return 'AutoCAD'
default:
return undefined
}
}
interface MouseGuardHandler { interface MouseGuardHandler {
description: string description: string
callback: (e: React.MouseEvent) => boolean callback: (e: React.MouseEvent) => boolean

View File

@ -8,8 +8,6 @@ export const MAX_PADDING = 7
* This is available for users to edit as a setting. * This is available for users to edit as a setting.
*/ */
export const DEFAULT_PROJECT_NAME = 'project-$nnn' export const DEFAULT_PROJECT_NAME = 'project-$nnn'
/** The file name for settings files, both at the user and project level */
export const SETTINGS_FILE_EXT = '.toml'
/** Name given the temporary "project" in the browser version of the app */ /** Name given the temporary "project" in the browser version of the app */
export const BROWSER_PROJECT_NAME = 'browser' export const BROWSER_PROJECT_NAME = 'browser'
/** Name given the temporary file in the browser version of the app */ /** Name given the temporary file in the browser version of the app */

View File

@ -49,6 +49,11 @@ export class CoreDumpManager {
return APP_VERSION return APP_VERSION
} }
// Get the backend pool we've requested.
pool(): string {
return this.engineCommandManager.pool || ''
}
// Get the os information. // Get the os information.
getOsInfo(): Promise<string> { getOsInfo(): Promise<string> {
if (this.isTauri()) { if (this.isTauri()) {

View File

@ -1,7 +1,11 @@
import { sep } from '@tauri-apps/api/path'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { BROWSER_FILE_NAME, BROWSER_PROJECT_NAME, FILE_EXT } from './constants' import { BROWSER_FILE_NAME, BROWSER_PROJECT_NAME, FILE_EXT } from './constants'
import { isTauri } from './isTauri' import { isTauri } from './isTauri'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
import { parseProjectRoute, readAppSettingsFile } from './tauri'
import { parseProjectRoute as parseProjectRouteWasm } from 'lang/wasm'
import { readLocalStorageAppSettingsFile } from './settings/settingsUtils'
const prependRoutes = const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => { (routesObject: Record<string, string>) => (prepend: string) => {
@ -25,28 +29,23 @@ export const paths = {
} as const } as const
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}` export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`
export function getProjectMetaByRouteId(id?: string, defaultDir = '') { export async function getProjectMetaByRouteId(
id?: string,
configuration?: Configuration
): Promise<ProjectRoute | undefined> {
if (!id) return undefined if (!id) return undefined
const s = isTauri() ? sep() : '/'
const decodedId = decodeURIComponent(id).replace(/\/$/, '') // remove trailing slash const inTauri = isTauri()
const projectAndFile =
defaultDir === '/'
? decodedId.replace(defaultDir, '')
: decodedId.replace(defaultDir + s, '')
const filePathParts = projectAndFile.split(s)
const projectName = filePathParts[0]
const projectPath =
(defaultDir === '/' ? defaultDir : defaultDir + s) + projectName
const lastPathPart = filePathParts[filePathParts.length - 1]
const currentFileName =
lastPathPart === projectName ? undefined : lastPathPart
const currentFilePath = lastPathPart === projectName ? undefined : decodedId
return { if (!configuration) {
projectName, configuration = inTauri
projectPath, ? await readAppSettingsFile()
currentFileName, : readLocalStorageAppSettingsFile()
currentFilePath,
} }
const route = inTauri
? await parseProjectRoute(configuration, id)
: parseProjectRouteWasm(configuration, id)
return route
} }

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