Compare commits

..

69 Commits

Author SHA1 Message Date
826ad267b4 Add release process and bump to 0.0.3 (#249)
* WIP: Add release process
Test version change in tauri.conf.json

* Add bump-jsons script

* Trigger build on release and upload artifacts there (untested)

* Test

* Revert "Test"

This reverts commit 3c0c2ae39c.

* Bump to 0.0.3

* Update README
2023-08-14 05:11:14 -04:00
2476c12480 Fix the Build jobs (#232)
* WIP Build jobs

* Remove 'add missing import'

* Add yarn build:wasm

* Clean up

* Trying larger runner for ubuntu

* --verbose

* Back to docs

* WIP

* upload artifacts

* WIP Windows

* WIP Windows

* WIP Windows

* WIP Windows

* WIP Windows

* Clean up

* Clean up diff

* Better upload artifact

* Clean up, upload artifacts v3

* Add sed commands back

* Add fmt back

* Add name

* Clean up

* Better name

* Update .github/workflows/build.yml

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-08-11 04:33:09 -04:00
ae460ed02f Franknoirot/video loading (#248)
* Refactor Loading to take children

* Add loading state to stream
2023-08-11 06:22:45 +10:00
3a93839a2d Keep App component loaded while navigating (#247)
* Make /settings not throw away App component

* Make App not reload for Onboarding

* Close sidebar when navigating to /settings

* Use centralized constants for route pathnames

* Clean up a few stray raw path literals
2023-08-10 13:30:32 -04:00
dbb94d7e95 Franknoirot/loading states (#246)
* Add Loading state with long load time messaging

* Make /signin page respect user theme
2023-08-10 05:41:41 +10:00
968a67e654 Add support for system theme (#245)
* Add support for 'system' theme value

* Add ability to set theme to 'system' in settings

* Fix tsc errors for Theme
2023-08-09 13:57:07 -04:00
8ebb8b8b94 re implement selections (#243) 2023-08-09 20:49:10 +10:00
f9259aa869 Implement zoom for mousewheel event on stream (#238) 2023-08-08 18:30:26 -04:00
a6f92e358b Make settings auto-save (#242)
* Feature: settings auto-save as they are updated

* Refactor: get rid of temporary settings states

* Feature: add escape hotkey to settings

* Style: layout tweaks

* Feature: setting unit system updates base unit too
2023-08-08 12:39:11 -04:00
35e4727856 Remove Rust unwraps (#241)
Fixes #222
2023-08-07 23:04:28 -05:00
a986f76e70 Ensure exceptions in the executor are handled properly (#239)
Right now, if the executor throws a KCLError (e.g. for "variable name is not defined" errors), they aren't being caught by the .catch or the try/catch block in asyncWrap. This PR fixes it.

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2023-08-07 20:33:38 -05:00
7a537eea8e Jest to Vitest migration (#230)
* working without clean up

* clean up dependencies

* use test not dev

* add tests for kclErrToDiagnostic

* remove jest config

* remove unneeded @ts-ignore
2023-08-08 10:50:27 +10:00
ca985dd1a8 fix build login (#237)
* fix build login

* fmt
2023-08-08 09:06:14 +10:00
1cba48f513 Make paneOpacity smoothly set lower when dragging (#235) 2023-08-08 07:07:28 +10:00
e7a70a9735 Update README.md (#236) 2023-08-08 06:05:26 +10:00
6e14dbaf77 Have separate SITE_URL env from API_URL (#234) 2023-08-07 13:07:40 -04:00
62dc07e117 Make signin URL environment-based (#233) 2023-08-07 12:40:55 -04:00
391f4ba206 Implement "floating windows" style UI (#224)
* Basic transparent pane styling

* HTML and static asset cleanup

* Convert to collapsibles

* Polish up DebugPanel

* Add hotkey support, remove allotment

* Remove allotment css dependency

* Merge in from main

* Add a different resizable package

* Fix tsc errors introduced by merge

* Stream has to have at least z-index of 0

* App header has to be above stream z-index

* Applied z-index to the wrong element

* Scrollable logs, disable UI while dragging

* Fix test errors from importing CSS Modules in Jest

* Persist open panes configuration

* Style tweaks and fix camera step in onboarding

* Kurt review, make click-drag handler declarative
2023-08-07 11:29:26 +10:00
4c5178ea5c Add automatic dev update (#229) 2023-08-04 09:42:57 -04:00
8c5d7bf648 Display KCL errors as CodeMirror diagnostics in the editor's gutter (#197)
Also updates the CodeMirror version
2023-08-03 15:56:11 -05:00
231371fb16 deploy to prod (#225)
* deploy to prod

* poke CI
2023-08-03 14:36:37 +10:00
cd4672c98d Export (#213)
* initial

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

update

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

* updates

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

* start of tauri

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

* updates

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

* better

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

* set the default type

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

* updates

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

* add comments

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

* fix

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

* updates

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

* dialog for save tauri

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

* partial tsc fix

* bump kittycad lib

* Update src/lib/exportSave.ts

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* updates;

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

* default coords

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2023-08-02 16:23:17 -07:00
c80dd44c59 Kurt pet issue (#216)
* use key names vars

* tweaks
2023-08-03 07:28:06 +10:00
c190122240 Add trickle_ice back (#220)
* Add trickle_ice back

* Only select from relay when provided ice servers

* Format

---------

Co-authored-by: Adam Chalmers <adam.s.chalmers@gmail.com>
2023-08-02 17:27:14 -04:00
36ea343860 fix sdp (#219)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2023-08-02 12:56:14 -07:00
1962af760c snake case fix (#221) 2023-08-03 05:51:52 +10:00
8be7805ac2 Log the Websocket events (#218) 2023-08-02 13:51:05 -05:00
64772f5c98 remove old types (#215) 2023-08-02 18:46:40 +10:00
5419039fae add client lib @kittycad/lib (#214)
* add client lib

* tsc after build

* update after spec update

* remove uneeded check

* updates

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

* fixes

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

* fix camera drag

* fix throttle typing

* comment with link to issue

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
2023-08-02 15:41:59 +10:00
aabb88ee45 Add tests for various user profile setups to sidebar (#212)
* Add tests for various user profile setups to sidebar

* Don't show empty user image if it isn't there

* @adamchalmer review
2023-08-01 13:23:17 -04:00
7408ba50dd Fix 'cannot read property of undefined' (#211)
If a token is undefined, we should not read its source range!
2023-08-01 10:22:51 -05:00
7181ff0c33 Configurable URL for the API (#205)
* Configurable engine URL

Fixes #206

* fix import.meta problem (#210)

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-08-01 09:36:40 -05:00
7ae1b66855 Fix style gaps in dark mode + sidebar updates (#208) 2023-07-31 12:08:16 -04:00
c31f1ad98b Add dark mode (#199)
* Add passive dark mode to everything but codemirror

* Add dark theme support for Codemirror

* Make theme a user setting

* Fix button text size

* guard against undefined window

* Formatting and test fix
2023-07-31 20:33:10 +10:00
894bddb369 Add user menu sidebar (#195) 2023-07-27 18:59:40 -04:00
94918ccb2e Tweak prettier (#201)
I don't think the wildcards and the {}s were being expanded as expected
in the 'scripts' section of package.json.

Also, I think the .prettierignore file is sufficient to ensure we only format
TS/JS/JSX code.
2023-07-27 12:49:53 -05:00
be7605cdef Git pre-push hook to check formatting (#200)
Add formatting check to Git pre-push hook
2023-07-27 12:13:51 -05:00
aca9b9226c Reset KCL logs (#198) 2023-07-26 18:16:20 -05:00
7312035818 Allow Prettier to format JS generated in wasm projects (#196)
Right now I'm getting ESLint errors because wasm-lib generates a JS project with semicolons.
This didn't use to matter, because `yarn prettier` applied to the Rust projects too. But
in my recent PR https://github.com/KittyCAD/untitled-lang/pull/192 I broke that.

This should fix it by ensuring the generated JS code gets formatted in a way that matches
our ESLint requirements.
2023-07-26 14:45:09 -05:00
956e4c46c1 Jest GitHub Action annotation (#194)
Jest now supports a reporter which emits GitHub Actions annotations. So if a test fails, it should annotate your PR when you view it on GitHub. See their example at https://jestjs.io/docs/configuration#github-actions-reporter

Also, use typescript for the config file.
2023-07-26 14:44:54 -05:00
0d010b60e5 Collect structured errors from parsing/executing KCL (#187)
Currently, syntax/semantic errors in the user's source code result in vanilla JS exceptions being thrown, so they show up in the console. Instead, this PR:

- Adds a new type KCLError
- KCL syntax/semantic errors when parsing/executing the source code now throw KCLErrors instead of vanilla JS exceptions.
- KCL errors are caught and logged to a new "Errors" panel, instead of the browser console.
2023-07-26 14:10:30 -05:00
6838e96723 Run yarn prettier, check it in CI (#192) 2023-07-26 11:47:18 -05:00
c2210835ea Fix use of useDismiss in onboarding (#191) 2023-07-25 11:16:52 -04:00
d1e7cb23a1 Tweak default value for onboardingStatus (#190) 2023-07-25 10:51:34 -04:00
9cd3845975 Franknoirot/add walkthrough (#189)
* Barebones onboarding triggering and resetting

* Make onboarding route-based

* Add Camera step, highlighting camera feed

* Implement redirect behavior

* Unify navigation hooks

* Formatting

* add useResizeObserver, convert to custom hook
2023-07-25 10:40:26 -04:00
ca2634d523 Adjust config to open localhost (#188) 2023-07-24 17:26:26 -04:00
48f1d5e623 Add unit setting (#183)
* Add Toggle component

* Add default units to settings

* Add defaultBaseUnit, shorten settings names

* Make debug panel use Toggle, fix colors

* add eslint-plugin-css-modules
2023-07-21 12:48:23 -04:00
87aecf7f50 Integrate sketch mode debug (#186)
integrate sketch mode debug
2023-07-21 16:53:06 +10:00
b89faa4a28 Migrate from CRA to Vite (#170)
* Basic CRA to Vite conversion

* Restore ESLint support

* Remove semicolons from vite config

* Add vite client types to tsconfig

* Migrate to Vitest for testing (not working on Mac)

* some test progress (#175)

* some test progress

* something maybe working

* remove local lib

* small clean up

* tweaks

* fix dependency

* clean up deps

* remove vitest import

* vitest config is needed even though we're not using vitest

* more tweaks to vite config

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-07-21 09:25:04 +10:00
1666e17ca5 Add support for camera Pan (#182) 2023-07-20 14:08:32 -04:00
5d90c0488f port recast to rust 🦀 (#178)
* port recast to rust with massaged serialisation

* remove logs

* fix the last of white space test failures

* remove ts recastor

* clean up imports

* use serde serialise features

* unneeded async

* final clean up recast.ts

* more clean up tweaks

* improve Rust BinaryPart types

* Comments, fix warnings

* Run clippy --fix

* Remove unused variable

* serialise none_code_nodes manual to force strings to numbers

---------

Co-authored-by: Adam Chalmers <adam.s.chalmers@gmail.com>
2023-07-20 12:38:05 +10:00
11cc85a9e8 remove wrtc, can be mocked for tests (#180) 2023-07-20 11:41:05 +10:00
3383becc0f New auth with tauri (#177)
* new auth command

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>
2023-07-13 19:05:56 -07:00
4c65d5b2ef Update App icon and name (#172)
* New "peeking Kitt" icons and favicon

* Change app name within Tauri
2023-07-13 07:23:11 -04:00
59fa51d75a Add settings UI page (#171)
* Add theme colors from Figma

* Rough-in of AppHeader

* Add styled ActionButton

* Add react-router and placeholder Settings page

* Add ability to set persistent defaultDir

* Add react-hot-toast for save success message

* Add defaultProjectName setting

* Handle case of stale empty defaultDir in storage

* Wrap app in BrowserRouter

* Wrap test App in BrowserRouter

* Don't need BrowserRouter outside of testing
because we use RouterProvider
2023-07-13 07:22:08 -04:00
3fc4d71a1e move ast types into seperate ts file (#169) 2023-07-13 16:57:22 +10:00
317dc6d0b2 rough tauri auth (#167) 2023-07-11 20:34:09 +10:00
4f8fe2d155 fix id typo (#165) 2023-07-11 15:13:15 +10:00
cda301997e Get tests passing without engine connection (#155)
We can create a enginelessExecutor that can be used for many of the
executor tests that will be much more performant for tests that don't
need the engine to actually do any modeling work.
2023-07-10 15:15:07 +10:00
a70399bacf onmouse leave (#148) 2023-06-29 20:11:51 +10:00
3510abfcb9 delay execute till after stream ready (#143) 2023-06-23 14:19:15 +10:00
fb3c34d5f3 send sequence with UDP packets (#140) 2023-06-23 10:54:19 +10:00
7289965916 send mouse drag over wrtc (#139)
* send mouse drag over wrtc

* add more debugging code

* wrtc data channel working
2023-06-23 09:56:37 +10:00
2d3c73d46a asyncronise executor (#115)
* Intital async of executor

The execture now sends websocket message instead of calling functions
directly from the engine, When it does so it holds onto the id.
The engine is still returning geo/polys directly but I'm working make it
so that the UI doesn't need to know about that, so that we can switch
over the streaming ui.

Things left to do:
- it is still making both direct function calls and websockets, and the former should be removed.
- It does highlighting of segments and sourceRanges not through websockets and that needs to be fixed.
- Tests have not been adjusted for these changes.
- Selecting the head of a segment is not working correctly again yet.

* Rough engine prep changes (#135)

* rough changes for engine prep

* mouse movements working again

* connect to engine for startsketch, line, close and extrude
2023-06-22 16:43:33 +10:00
dd3117cf03 Steal Jess's mouse move code (#137)
steal Jess's mouse move code
2023-06-21 09:15:02 +10:00
efa3bc7ac6 Use trickle ICE (#136)
* Try trickle ice setup

* Fix typo

* Append auth to api.dev too
2023-06-20 16:51:12 -04:00
0858d32c1e more tauri stuff (#128) 2023-06-19 10:16:45 +10:00
e7c1554129 Mute stream by default for autoplay (#127) 2023-06-15 09:16:14 -04:00
6eaa0e0852 get release to work (#124)
* get release to work

* update skip-sign

* new strategy to avoid signing

* fix

* fix

* get ubuntu build working

* fix

* fix

* fix
2023-06-08 09:52:57 +10:00
157 changed files with 10794 additions and 11467 deletions

3
.env.development Normal file
View File

@ -0,0 +1,3 @@
VITE_KC_API_WS_MODELING_URL=wss://api.dev.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io

3
.env.production Normal file
View File

@ -0,0 +1,3 @@
VITE_KC_API_WS_MODELING_URL=wss://api.kittycad.io/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.kittycad.io
VITE_KC_SITE_BASE_URL=https://kittycad.io

16
.eslintrc Normal file
View File

@ -0,0 +1,16 @@
{
"plugins": [
"css-modules"
],
"extends": [
"react-app",
"react-app/jest",
"plugin:css-modules/recommended"
],
"rules": {
"semi": [
"error",
"never"
]
}
}

View File

@ -1,33 +1,44 @@
name: Build
on:
push:
tags:
- 'v*'
pull_request:
push:
branches:
- main
release:
types: [published]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
os: [macos-latest, ubuntu-20.04, windows-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: install ubuntu system dependencies
if: matrix.os == 'ubuntu-latest'
if: matrix.os == 'ubuntu-20.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev libwebkit2gtk-4.0-dev
# libgdk-pixbuf2.0-dev libsoup2.4-dev libjavascriptcoregtk-4.0-dev
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
- name: Sync node version and setup cache
uses: actions/setup-node@v3
with:
node-version: '18.x'
node-version-file: '.nvmrc'
cache: 'yarn' # Set this to npm, yarn or pnpm.
- run: yarn install
- name: Rust setup
uses: dtolnay/rust-toolchain@stable
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- name: wasm prep
shell: bash
run: |
@ -35,39 +46,36 @@ jobs:
npx wasm-pack build --target web --out-dir pkg
cd ../../
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
- name: macos sed
if: matrix.os == 'macos-latest'
shell: bash
run: |
sed -i '' 's/import.meta.url//g' "./src/wasm-lib/pkg/wasm_lib.js"
- name: ubuntu and windows sed
if: matrix.os != 'macos-latest'
shell: bash
run: |
sed -i 's/import.meta.url//g' "./src/wasm-lib/pkg/wasm_lib.js"
- name: add missing import
shell: bash
run: |
yarn add-missing-import
pwd
ls -la
yarn fmt
# - name: tauri build
# shell: bash
# run: yarn tauri build
# - uses: actions/upload-artifact@v2
# with:
# name: tauri-app
# path: src-tauri/target/release/bundle
- name: Build the app
- name: Fix format
run: yarn fmt
- name: Build the app for the current platform (no upload)
if: github.event_name == 'pull_request'
uses: tauri-apps/tauri-action@v0
- uses: actions/upload-artifact@v3
if: github.event_name == 'pull_request'
with:
path: src-tauri/target/release/bundle
name: modeling-app_macos_linux_windows
- name: Build the app for the current platform and upload to release
if: github.event_name == 'release'
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CI: false
with:
tagName: ${{ github.ref_name }} # This only works if your workflow triggers on new tags.
releaseName: 'App Name v__VERSION__' # tauri-action replaces \_\_VERSION\_\_ with the app version.
releaseBody: 'See the assets to download and install this version.'
releaseDraft: true
prerelease: false
releaseId: ${{ github.event.release.id }}

16
.github/workflows/format.yml vendored Normal file
View File

@ -0,0 +1,16 @@
# on pull requests, setup node, run `yarn prettier --check`
name: Check formatting
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
- run: yarn install
- run: yarn fmt-check

View File

@ -8,12 +8,13 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16.x'
node-version-file: '.nvmrc'
- run: yarn install
- run: yarn build:wasm:ci
- run: yarn build:wasm
- run: yarn tsc
- run: yarn simpleserver:ci
- run: yarn test:nowatch
- run: yarn test:cov

29
.github/workflows/update-dev-branch.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: update-dev-branch
on:
push:
branches:
- main
pull_request:
paths:
- .github/workflows/update-dev-branch.yml
permissions:
contents: write
jobs:
update-branch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.5.0
- shell: bash
run: |
# checkout our branch
git checkout dev || git checkout -b dev
# fetch origin
git fetch origin
# reset to main
git reset --hard origin/main
# force push it
git push -f origin dev

4
.husky/pre-push Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn fmt-check

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v20.5.0

7
.prettierignore Normal file
View File

@ -0,0 +1,7 @@
# Ignore artifacts:
build
coverage
# Ignore Rust projects:
*.rs
target

View File

@ -1,5 +1,7 @@
{
"cSpell.words": [
"geos"
]
],
"editor.tabSize": 2,
"editor.insertSpaces": true,
}

View File

@ -43,6 +43,37 @@ If you want to edit the rust files, you can cd into `src/wasm-lib` and then use
Worth noting that the integration of the WASM into this project is very hacky because I'm really pushing create-react-app further than what's practical, but focusing on features atm rather than the setup.
## Tauri
To spin up up tauri dev, `yarn install` and `yarn build:wasm` need to have been done before hand then
```
yarn tauri dev
```
Will spin up the web app before opening up the tauri dev desktop app. Note that it's probably a good idea to close the browser tab that gets opened since at the time of writting they can conflict.
The dev instance automatically opens up the browser devtools which can be disabled by [commenting it out](https://github.com/KittyCAD/modeling-app/blob/main/src-tauri/src/main.rs#L92.)
To build, run `yarn tauri build`, or `yarn tauri build --debug` to keep access to the devtools.
Note that these became separate apps on Macos, so make sure you open the right one after a build 😉
![image](https://github.com/KittyCAD/modeling-app/assets/29681384/a08762c5-8d16-42d8-a02f-a5efc9ae5551)
<img width="1232" alt="image" src="https://user-images.githubusercontent.com/29681384/211947063-46164bb4-7bdd-45cb-9a76-2f40c71a24aa.png">
<img width="1232" alt="image (1)" src="https://user-images.githubusercontent.com/29681384/211947073-e76b4933-bef5-4636-bc4d-e930ac8e290f.png">
## Release a new version
1. Bump the versions in the .json files by creating a `Bump to v{x}.{y}.{z}` PR, committing the changes from
```bash
VERSION=x.y.z yarn run bump-jsons
```
The PR may serve as a place to discuss the human-readable changelog and extra QA.
2. Merge the PR
3. Create a new release and tag pointing to the bump version commit using semantic versioning `v{x}.{y}.{z}`
4. A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, uploading artifacts to the release

BIN
app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

3
babel.config.js Normal file
View File

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

21
index.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="An open-source CAD modeling tool from the future by KittyCAD."
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>KittyCAD Modeling App</title>
</head>
<body class="body-bg">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="h-screen overflow-y-auto"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@ -1,61 +1,66 @@
{
"name": "untitled-app",
"version": "0.1.0",
"version": "0.0.3",
"private": true,
"dependencies": {
"@codemirror/lang-javascript": "^6.1.1",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.13",
"@react-three/drei": "^9.42.0",
"@react-three/fiber": "^8.9.1",
"@kittycad/lib": "^0.0.27",
"@react-hook/resize-observer": "^1.2.6",
"@tauri-apps/api": "^1.3.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@uiw/codemirror-extensions-langs": "^4.21.9",
"@uiw/react-codemirror": "^4.15.1",
"allotment": "^1.17.0",
"crypto-js": "^4.1.1",
"formik": "^2.4.3",
"http-server": "^14.1.1",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-hotkeys-hook": "^4.4.1",
"react-json-view": "^1.21.3",
"react-modal": "^3.16.1",
"react-modal-promise": "^1.0.2",
"react-scripts": "5.0.1",
"sketch-helpers": "^0.0.2",
"react-router-dom": "^6.14.2",
"sketch-helpers": "^0.0.4",
"swr": "^2.0.4",
"three": "^0.146.0",
"toml": "^3.0.0",
"ts-node": "^10.9.1",
"typescript": "^4.4.2",
"util": "^0.12.5",
"uuid": "^9.0.0",
"wasm-pack": "^0.11.1",
"vitest": "^0.34.1",
"wasm-pack": "^0.12.1",
"web-vitals": "^2.1.0",
"ws": "^8.13.0",
"zustand": "^4.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm:ci && react-scripts build",
"build:local": "react-scripts build",
"build:both": "react-scripts build",
"build:both:local": "yarn build:wasm && react-scripts build",
"test": "react-scripts test",
"test:nowatch": "react-scripts test --watchAll=false",
"start": "vite",
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
"build:local": "vite build",
"build:both": "vite build",
"build:both:local": "yarn build:wasm && vite build",
"test": "vitest --mode development",
"test:nowatch": "vitest run --mode development",
"test:rust": "(cd src/wasm-lib && cargo test && cargo clippy)",
"test:cov": "react-scripts test --watchAll=false --coverage=true",
"test:cov": "vitest run --coverage --mode development",
"simpleserver:ci": "http-server ./public --cors -p 3000 &",
"simpleserver": "http-server ./public --cors -p 3000",
"eject": "react-scripts eject",
"fmt": "prettier --write ./src/**/*.{ts,tsx,js}",
"remove-importmeta": "sed -i '' 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"",
"remove-importmeta:ci": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"",
"add-missing-import": "echo \"import util from 'util'; if (typeof window !== 'undefined' && !window.TextEncoder) { window.TextEncoder = util.TextEncoder; window.TextDecoder = util.TextDecoder}\" | cat - ./src/wasm-lib/pkg/wasm_lib.js > temp && mv temp ./src/wasm-lib/pkg/wasm_lib.js",
"build:wasm:ci": "mkdir src/wasm-lib/pkg; cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cd ../../ && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn remove-importmeta:ci && yarn add-missing-import && yarn fmt",
"build:wasm": "mkdir src/wasm-lib/pkg; cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cd ../../ && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn remove-importmeta && yarn add-missing-import && yarn fmt"
},
"jest": {
"transformIgnorePatterns": [
"node_modules/(?!(three|allotment)/)"
]
"fmt": "prettier --write ./src",
"fmt-check": "prettier --check ./src",
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
"remove-importmeta": "sed -i 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url//g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg",
"lint": "eslint --fix src",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json"
},
"prettier": {
"trailingComma": "es5",
@ -63,18 +68,6 @@
"semi": false,
"singleQuote": true
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"rules": {
"semi": [
"error",
"never"
]
}
},
"browserslist": {
"production": [
">0.2%",
@ -88,14 +81,30 @@
]
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.22.9",
"@tauri-apps/cli": "^1.3.1",
"@types/crypto-js": "^4.1.1",
"@types/three": "^0.146.0",
"@types/isomorphic-fetch": "^0.0.36",
"@types/react-modal": "^3.16.0",
"@types/uuid": "^9.0.1",
"@types/wicg-file-system-access": "^2020.9.6",
"@types/ws": "^8.5.5",
"@vitejs/plugin-react": "^4.0.3",
"@vitest/coverage-istanbul": "^0.34.1",
"autoprefixer": "^10.4.13",
"eslint": "^8.44.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.11.0",
"happy-dom": "^10.8.0",
"husky": "^8.0.3",
"postcss": "^8.4.19",
"prettier": "^2.8.0",
"setimmediate": "^1.0.5",
"tailwindcss": "^3.2.4",
"vite": "^4.4.3",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.0",
"yarn": "^1.22.19"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,37 @@
<svg width="25" height="34" viewBox="0 0 25 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 31V30H1V29H0V8H1V7H2V6H3V5H4V4H21V5H22V6H23V7H24V8H25V29H24V30H22V31H19V32H20V34H14V32H15V31H10V32H11V34H5V32H6V31H3Z" fill="#101412"/>
<path d="M6 31V29.5H10V31H9V32H10V33H6V32H7V31H6Z" fill="#4B4862"/>
<path d="M15 31V29.5H19V31H18V32H19V33H15V32H16V31H15Z" fill="#4B4862"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 24.5V29H3V30H22V29H24V24.5H1ZM9 29V27H16V29H9Z" fill="#9BADB7"/>
<path d="M1 25V23H24V25H1Z" fill="#BECAD0"/>
<path d="M2 27V26H7V27H2Z" fill="#2B3E48"/>
<path d="M4 29V28H7V29H4Z" fill="#2B3E48"/>
<path d="M18 27V26H19V27H18Z" fill="#2B3E48"/>
<path d="M20 27V26H21V27H20Z" fill="#2B3E48"/>
<path d="M22 27V26H23V27H22Z" fill="#2B3E48"/>
<path d="M18 29V28H21V29H18Z" fill="#2B3E48"/>
<path d="M22 7V6H21V5H4V6H3V7H2V8H1V10H24V8H23V7H22Z" fill="#FBF580"/>
<path d="M1 24V22H24V24H1Z" fill="#AEAA4C"/>
<path d="M24 9H1V23H24V9Z" fill="#E5E3A1"/>
<path d="M4 12V11H21V12H22V20H21V21H4V20H3V12H4Z" fill="#1F2320"/>
<rect x="10" y="5" width="5" height="2" fill="#AEAA4C"/>
<path d="M16 13V12H18V16H17L16 13Z" fill="#DBFF3C"/>
<path d="M11 16H14V17H13V19H16V18H17V19H16V20H9V19H8V18H9V19H12V17H11V16Z" fill="#DBFF3C"/>
<path d="M9 15V14H6V15H5V14H6V13H9V14H10V15H9Z" fill="#DBFF3C"/>
<path d="M4 7V6H5V4H6V2H7V1H8V2H9V4H10V6H11V7H4Z" fill="#DBFF3C"/>
<path d="M21 6V7H14V6H15V4H16V2H17V1H18V2H19V4H20V6H21Z" fill="#DBFF3C"/>
<path d="M16 2V0H19V2H20V4H21V5.5H20V4H19V2H18V1H17V2H16V4H15V5.5H14V4H15V2H16Z" fill="#92C51B"/>
<path d="M6 2V0H9V2H10V4H11V5.5H10V4H9V2H8V1H7V2H6V4H5V5.5H4V4H5V2H6Z" fill="#92C51B"/>
<rect x="11" y="6" width="3" height="1" fill="#D0CC6A"/>
<path d="M16 7V6H17V5H18V6H19V7H16Z" fill="#76AA1D"/>
<path d="M7 6V5H8V6H9V7H6V6H7Z" fill="#76AA1D"/>
<path d="M21 7V6H20V5H21V6H22V7H21Z" fill="#76AA1D"/>
<path d="M4 6V7H3V6H4V5H5V6H4Z" fill="#76AA1D"/>
<path d="M10 5H11V6H12V7H11V6H10V5Z" fill="#76AA1D"/>
<path d="M14 5H15V6H14V7H13V6H14V5Z" fill="#76AA1D"/>
<path d="M17 13H16V16H17V13Z" fill="#92C51B"/>
<path d="M2 25V23H1V25H2Z" fill="#D0CC6A"/>
<path d="M23 25V23H24V25H23Z" fill="#D0CC6A"/>
<path d="M4 24V23H7V24H4Z" fill="#D56161"/>
<path d="M4 25V24H7V25H4Z" fill="#AC3232"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,18 @@
<svg width="25" height="34" viewBox="0 0 25 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 31V30H1V29H0V8H1V7H2V6H3V5H4V3H5V2H6V1H7V0H8V1H9V2H10V3H11V4H14V3H15V2H16V1H17V0H18V1H19V2H20V3H21V5H22V6H23V7H24V8H25V29H24V30H22V31H19V32H20V34H14V32H15V31H10V32H11V34H5V32H6V31H3Z" fill="#101412"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 5H14V4H15V3H16V2H17V1H18V2H19V3H20V5H21V6H22V7H23V8H24V9V10V23V24V25V29H22V30H18V31V32H19V33H15V32H16V31V30H9V31V32H10V33H6V32H7V31V30H3V29H1V25V24V23V10V9V8H2V7H3V6H4V5H5V3H6V2H7V1H8V2H9V3H10V4H11V5ZM2 26V27H7V26H2ZM4 28V29H7V28H4ZM18 27V26H19V27H18ZM20 26V27H21V26H20ZM22 27V26H23V27H22ZM18 28V29H21V28H18ZM9 27H16V29H9V27Z" fill="#D0FF00"/>
<path d="M1 24V23H24V24H1Z" fill="#B1E515"/>
<path d="M4 12V11H21V12H22V20H21V21H4V20H3V12H4Z" fill="#1F2320"/>
<path d="M16 16V12H18V16H16Z" fill="#D0FF00"/>
<path d="M11 16H14V17H13V19H16V18H17V19H16V20H9V19H8V18H9V19H12V17H11V16Z" fill="#D0FF00"/>
<path d="M9 15V14H6V15H5V14H6V13H9V14H10V15H9Z" fill="#D0FF00"/>
<path d="M22 8V7H23V8H22Z" fill="#B1E515"/>
<path d="M3 8V7H2V8H3Z" fill="#B1E515"/>
<path d="M21 7V6H22V7H21Z" fill="#92C51B"/>
<path d="M4 7V6H3V7H4Z" fill="#92C51B"/>
<rect x="12" y="5" width="1" height="2" fill="#B1E515"/>
<path d="M16 7V6H17V5H18V6H19V7H16Z" fill="#101412"/>
<path d="M7 6V5H8V6H9V7H6V6H7Z" fill="#101412"/>
<path d="M4 24V23H7V24H4Z" fill="#92C51B"/>
<rect x="11" y="5" width="3" height="1" fill="#92C51B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,26 @@
<svg width="316" height="75" viewBox="0 0 316 75" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.33449 67.7274V65.5747H3.02906V63.4219H0.876343V18.2149H3.02906V16.0622H5.18177V13.9095H7.33449V11.7568H9.4872V7.45137H11.6399V5.29866H13.7926V3.14594H15.9453V0.993229H18.0981V3.14594H20.2508V5.29866H22.4035V7.45137H24.5562V9.60409H31.0143V7.45137H33.1671V5.29866H35.3198V3.14594H37.4725V0.993229H39.6252V3.14594H41.7779V5.29866H43.9306V7.45137H46.0833V11.7568H48.2361V13.9095H50.3888V16.0622H52.5415V18.2149H54.6942V63.4219H52.5415V65.5747H48.2361V67.7274H41.7779V69.8801H43.9306V74.1855H31.0143V69.8801H33.1671V67.7274H22.4035V69.8801H24.5562V74.1855H11.6399V69.8801H13.7926V67.7274H7.33449Z" fill="#101412"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.5563 11.7568H31.0145V9.60409H33.1672V7.45137H35.3199V5.29866H37.4726V3.14594H39.6253V5.29866H41.778V7.45137H43.9307V11.7568H46.0835V13.9095H48.2361V16.0622H50.3888V18.2149H52.5415V20.3677V22.5204V50.5057V52.6584V54.8111V63.4219H48.2361V65.5747H39.6253V67.7274V69.8801H41.778V72.0328H33.1671V69.8801H35.3199V67.7274V65.5747H20.2509V67.7274V69.8801H22.4036V72.0328H13.7927V69.8801H15.9454V67.7274V65.5747H7.33454V63.4219H3.02911V54.8111V52.6584V50.5057V22.5204V20.3677V18.2149H5.18183V16.0622H7.33454V13.9095H9.48726V11.7568L9.48731 11.7568H11.64V7.45137H13.7927V5.29866H15.9455V3.14594H18.0982V5.29866H20.2509V7.45137H22.4036V9.60409H24.5563V11.7568ZM5.18191 56.9638V59.1165H15.9455V56.9638H5.18191ZM9.48734 61.2692V63.4219H15.9455V61.2692H9.48734ZM39.6253 59.1165V56.9638H41.7781V59.1165H39.6253ZM43.9308 56.9638V59.1165H46.0835V56.9638H43.9308ZM48.2362 59.1165V56.9638H50.3889V59.1165H48.2362ZM39.6253 61.2692V63.4219H46.0835V61.2692H39.6253ZM20.2509 59.1165H35.3199V63.4219H20.2509V59.1165Z" fill="#D0FF00"/>
<path d="M3.02911 52.6584V50.5057H52.5415V52.6584H3.02911Z" fill="#B1E515"/>
<path d="M9.48725 26.8258V24.6731H46.0834V26.8258H48.2361V44.0475H46.0834V46.2002H9.48725V44.0475H7.33453V26.8258H9.48725Z" fill="#1F2320"/>
<path d="M35.3198 35.4367V26.8258H39.6252V35.4367H35.3198Z" fill="#D0FF00"/>
<path d="M24.5562 35.4367H31.0144V37.5894H28.8617V41.8948H35.3198V39.7421H37.4725V41.8948H35.3198V44.0475H20.2508V41.8948H18.0981V39.7421H20.2508V41.8948H26.709V37.5894H24.5562V35.4367Z" fill="#D0FF00"/>
<path d="M20.2508 33.2839V31.1312H13.7927V33.2839H11.64V31.1312H13.7927V28.9785H20.2508V31.1312H22.4035V33.2839H20.2508Z" fill="#D0FF00"/>
<path d="M48.2361 18.2149V16.0622H50.3888V18.2149H48.2361Z" fill="#92C51B"/>
<path d="M7.33448 18.2149V16.0622H5.18176V18.2149H7.33448Z" fill="#92C51B"/>
<path d="M46.0834 16.0622V13.9095H48.2361V16.0622H46.0834Z" fill="#92C51B"/>
<path d="M9.48725 16.0622V13.9095H7.33453V16.0622H9.48725Z" fill="#92C51B"/>
<rect x="26.709" y="11.7568" width="2.15271" height="4.30543" fill="#B1E515"/>
<path d="M35.3197 16.0622V13.9095H37.4725V11.7568H39.6252V13.9095H41.7779V16.0622H35.3197Z" fill="#101412"/>
<path d="M15.9453 13.9095V11.7568H18.098V13.9095H20.2507V16.0622H13.7926V13.9095H15.9453Z" fill="#101412"/>
<path d="M9.48718 52.6584V50.5057H15.9453V52.6584H9.48718Z" fill="#92C51B"/>
<rect x="24.5562" y="11.7568" width="6.45814" height="2.15271" fill="#92C51B"/>
<path d="M77.4822 18.0099V33.8657L92.8664 17.1258L99.4091 22.077L86.9721 35.5161L103.535 58.0914H92.6306L81.0777 41.8231L77.4822 45.7133V58.0914H68.8175V18.0099H77.4822Z" fill="#FFFFFA"/>
<path d="M115.158 20.7213C115.158 22.0574 114.666 23.1969 113.684 24.14C112.741 25.0438 111.601 25.4957 110.265 25.4957C108.929 25.4957 107.809 25.0438 106.906 24.14C106.002 23.1969 105.55 22.0574 105.55 20.7213C105.55 19.3853 106.002 18.2457 106.906 17.3026C107.809 16.3595 108.929 15.888 110.265 15.888C111.601 15.888 112.741 16.3595 113.684 17.3026C114.666 18.2457 115.158 19.3853 115.158 20.7213ZM114.627 29.5039V58.0914H105.962V29.5039H114.627Z" fill="#FFFFFA"/>
<path d="M133.871 59.0935C130.335 59.0935 127.407 58.1897 125.089 56.3821C122.809 54.5745 121.67 51.922 121.67 48.4247V36.636H117.603V29.9165H121.67V22.8433L130.276 21.4286V29.9165H136.052L137.938 36.636H130.276V47.128C130.276 48.5033 130.629 49.6429 131.337 50.5467C132.044 51.4112 133.066 51.8434 134.402 51.8434C134.913 51.8434 135.463 51.7648 136.052 51.6077C136.642 51.4505 137.231 51.2343 137.82 50.9593L140.355 57.0894C139.687 57.6395 138.705 58.1111 137.408 58.504C136.111 58.897 134.932 59.0935 133.871 59.0935Z" fill="#FFFFFA"/>
<path d="M156.465 59.0935C152.929 59.0935 150.001 58.1897 147.683 56.3821C145.404 54.5745 144.264 51.922 144.264 48.4247V36.636H140.197V29.9165H144.264V22.8433L152.87 21.4286V29.9165H158.646L160.532 36.636H152.87V47.128C152.87 48.5033 153.223 49.6429 153.931 50.5467C154.638 51.4112 155.66 51.8434 156.996 51.8434C157.507 51.8434 158.057 51.7648 158.646 51.6077C159.236 51.4505 159.825 51.2343 160.415 50.9593L162.949 57.0894C162.281 57.6395 161.299 58.1111 160.002 58.504C158.705 58.897 157.526 59.0935 156.465 59.0935Z" fill="#FFFFFA"/>
<path d="M172.163 59.0345L173.165 56.6178L162.791 30.5649L171.515 29.5039C172.576 32.3332 173.637 35.1625 174.698 37.9917C175.759 40.821 176.8 43.6503 177.822 46.4796L183.834 29.5039H192.793L180.062 61.687C179.119 64.0054 177.488 65.9898 175.169 67.6403C172.851 69.33 170.375 70.4892 167.742 71.1179L164.736 64.1036C166.151 63.5535 167.625 62.8658 169.157 62.0406C170.69 61.2154 171.692 60.2134 172.163 59.0345Z" fill="#FFFFFA"/>
<path d="M203.975 58.0914L197.64 51.7563V21.3723L203.975 15.0371H223.072L229.438 21.3723V31.2748H220.735V25.6162L218.859 23.7402H208.219L206.343 25.6162V47.5124L208.219 49.3883H218.859L220.735 47.5124V41.8538H229.438V51.7563L223.072 58.0914H203.975Z" fill="#FFFFFA"/>
<path d="M236.208 58.0914V21.3723L242.544 15.0371H262.41L268.745 21.3723V58.0914H260.073V45.4212H244.881V58.0914H236.208ZM244.881 36.7488H260.073V25.6162L258.197 23.7402H246.757L244.881 25.6162V36.7488Z" fill="#FFFFFA"/>
<path d="M276.098 58.0914V15.0371H301.716L308.051 21.3723V51.7563L301.716 58.0914H276.098ZM284.802 49.3883H297.503L299.379 47.5124V25.6162L297.503 23.7402H284.802V49.3883Z" fill="#FFFFFA"/>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -0,0 +1,26 @@
<svg width="788" height="183" viewBox="0 0 788 183" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.6075 166.835V161.454H5.84388V156.072H0.462097V43.0543H5.84388V37.6725H11.2257V32.2907H16.6075V26.9089H21.9892V16.1454H27.371V10.7636H32.7528V5.38179H38.1346V0H43.5164V5.38179H48.8982V10.7636H54.28V16.1454H59.6617V21.5271H75.8071V16.1454H81.1889V10.7636H86.5707V5.38179H91.9525V0H97.3342V5.38179H102.716V10.7636H108.098V16.1454H113.48V26.9089H118.861V32.2907H124.243V37.6725H129.625V43.0543H135.007V156.072H129.625V161.454H118.861V166.835H102.716V172.217H108.098V182.981H75.8071V172.217H81.1889V166.835H54.28V172.217H59.6617V182.981H27.371V172.217H32.7528V166.835H16.6075Z" fill="#101412"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.9892 26.9095L21.9892 26.9102V32.2919H16.6075V37.6737H11.2257V43.0555H5.84388V48.4364V53.8191V123.781V129.163V129.164V134.545V156.073H16.6075V161.455H38.1346V166.836V172.217H32.7528V177.599H54.28V172.217H48.8982V166.836V161.455H86.5707V166.836V172.217H81.1889V177.599H102.716V172.217H97.3342V166.836V161.455H118.861V156.073H129.625V134.545V129.164V129.163V123.781V53.8191V48.4364V43.0555H124.243V37.6737H118.861V32.2919H113.48V26.9102V26.9095H108.098V16.1459H102.716V10.7641H97.3342V5.38232H91.9525V10.7641H86.5707V16.1459H81.1889V21.5277H75.8071V26.9102H59.6617V21.5277H54.28V16.1459H48.8982V10.7641H43.5164V5.38232H38.1346V10.7641H32.7528V16.1459H27.371V26.9095H21.9892ZM11.2257 129.164H124.243V129.163H11.2257V129.164ZM11.2257 139.927V145.309H38.1346V139.927H11.2257ZM21.9893 150.691V156.072H38.1346V150.691H21.9893ZM97.3343 145.309V139.927H102.716V145.309H97.3343ZM108.098 139.927V145.309H113.48V139.927H108.098ZM118.861 145.309V139.927H124.243V145.309H118.861ZM97.3343 150.691V156.072H113.48V150.691H97.3343ZM48.8982 145.309H86.5707V156.073H48.8982V145.309Z" fill="#D0FF00"/>
<path d="M5.84388 129.163V123.781H129.625V129.163H5.84388Z" fill="#B1E515"/>
<path d="M21.9892 64.5812V59.1995H113.48V64.5812H118.861V107.636H113.48V113.017H21.9892V107.636H16.6075V64.5812H21.9892Z" fill="#1F2320"/>
<path d="M86.5707 86.1092V64.582H97.3343V86.1092H86.5707Z" fill="#D0FF00"/>
<path d="M59.6617 86.1092H75.8071V91.491H70.4253V102.255H86.5707V96.8727H91.9525V102.255H86.5707V107.636H48.8982V102.255H43.5164V96.8727H48.8982V102.255H65.0435V91.491H59.6617V86.1092Z" fill="#D0FF00"/>
<path d="M48.8982 80.7274V75.3456H32.7528V80.7274H27.371V75.3456H32.7528V69.9638H48.8982V75.3456H54.28V80.7274H48.8982Z" fill="#D0FF00"/>
<path d="M118.861 43.0534V37.6716H124.243V43.0534H118.861Z" fill="#92C51B"/>
<path d="M16.6075 43.0534V37.6716H11.2257V43.0534H16.6075Z" fill="#92C51B"/>
<path d="M113.48 37.6728V32.291H118.861V37.6728H113.48Z" fill="#92C51B"/>
<path d="M21.9892 37.6728V32.291H16.6075V37.6728H21.9892Z" fill="#92C51B"/>
<rect x="65.0435" y="26.9087" width="5.38179" height="10.7636" fill="#B1E515"/>
<path d="M86.5707 37.6723V32.2905H91.9525V26.9087H97.3342V32.2905H102.716V37.6723H86.5707Z" fill="#101412"/>
<path d="M38.1346 32.2905V26.9087H43.5164V32.2905H48.8982V37.6723H32.7528V32.2905H38.1346Z" fill="#101412"/>
<path d="M21.9892 129.163V123.781H38.1346V129.163H21.9892Z" fill="#92C51B"/>
<rect x="59.6617" y="26.9087" width="16.1454" height="5.38179" fill="#92C51B"/>
<path d="M191.977 42.5414V82.1808L230.437 40.331L246.794 52.7091L215.701 86.3068L257.109 142.745H229.848L200.966 102.074L191.977 111.8V142.745H170.315V42.5414H191.977Z" fill="#101412"/>
<path d="M286.165 49.3199C286.165 52.66 284.937 55.5089 282.481 57.8666C280.124 60.1261 277.275 61.2559 273.935 61.2559C270.594 61.2559 267.795 60.1261 265.535 57.8666C263.276 55.5089 262.146 52.66 262.146 49.3199C262.146 45.9797 263.276 43.1308 265.535 40.7731C267.795 38.4153 270.594 37.2365 273.935 37.2365C277.275 37.2365 280.124 38.4153 282.481 40.7731C284.937 43.1308 286.165 45.9797 286.165 49.3199ZM284.839 71.2763V142.745H263.177V71.2763H284.839Z" fill="#101412"/>
<path d="M332.949 145.25C324.108 145.25 316.789 142.991 310.993 138.472C305.295 133.953 302.446 127.322 302.446 118.578V89.1066H292.278V72.3078H302.446V54.6248L323.96 51.0882V72.3078H338.402L343.117 89.1066H323.96V115.336C323.96 118.775 324.845 121.624 326.613 123.883C328.381 126.044 330.935 127.125 334.276 127.125C335.553 127.125 336.928 126.929 338.402 126.536C339.875 126.143 341.349 125.602 342.822 124.915L349.159 140.24C347.489 141.615 345.033 142.794 341.791 143.777C338.549 144.759 335.602 145.25 332.949 145.25Z" fill="#101412"/>
<path d="M389.435 145.25C380.593 145.25 373.274 142.991 367.478 138.472C361.781 133.953 358.932 127.322 358.932 118.578V89.1066H348.764V72.3078H358.932V54.6248L380.446 51.0882V72.3078H394.887L399.602 89.1066H380.446V115.336C380.446 118.775 381.33 121.624 383.098 123.883C384.867 126.044 387.421 127.125 390.761 127.125C392.038 127.125 393.413 126.929 394.887 126.536C396.361 126.143 397.834 125.602 399.308 124.915L405.644 140.24C403.974 141.615 401.518 142.794 398.276 143.777C395.034 144.759 392.087 145.25 389.435 145.25Z" fill="#101412"/>
<path d="M428.679 145.103L431.184 139.061L405.249 73.9287L427.058 71.2763C429.711 78.3495 432.363 85.4227 435.016 92.4959C437.668 99.5691 440.272 106.642 442.826 113.715L457.856 71.2763H480.255L448.425 151.734C446.068 157.53 441.991 162.491 436.195 166.617C430.398 170.841 424.209 173.739 417.627 175.311L410.112 157.776C413.649 156.4 417.333 154.681 421.164 152.618C424.995 150.555 427.5 148.05 428.679 145.103Z" fill="#101412"/>
<path d="M508.208 142.745L492.371 126.907V50.9472L508.208 35.1094H555.953L571.867 50.9472V75.7034H550.109V61.557L545.42 56.8672H518.818L514.128 61.557V116.297L518.818 120.987H545.42L550.109 116.297V102.151H571.867V126.907L555.953 142.745H508.208Z" fill="#101412"/>
<path d="M588.792 142.745V50.9472L604.63 35.1094H654.296L670.134 50.9472V142.745H648.453V111.069H610.473V142.745H588.792ZM610.473 89.3885H648.453V61.557L643.763 56.8672H615.163L610.473 61.557V89.3885Z" fill="#101412"/>
<path d="M688.517 142.745V35.1094H752.561L768.399 50.9472V126.907L752.561 142.745H688.517ZM710.275 120.987H742.028L746.718 116.297V61.557L742.028 56.8672H710.275V120.987Z" fill="#101412"/>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1,6 +1,6 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "KCMA",
"name": "KittyCAD Modeling App",
"icons": [
{
"src": "favicon.ico",

720
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,12 +15,16 @@ rust-version = "1.60"
tauri-build = { version = "1.3.0", features = [] }
[dependencies]
serde_json = "1.0"
anyhow = "1"
oauth2 = "4.4.1"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.3.0", features = [] }
serde_json = "1.0"
tauri = { version = "1.3.0", features = ["dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
tokio = { version = "1.29.1", features = ["time"] }
toml = "0.6.0"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
# DO NOT REMOVE!!
custom-protocol = [ "tauri/custom-protocol" ]
custom-protocol = ["tauri/custom-protocol"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

@ -1,8 +1,103 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running tauri application");
use std::io::Read;
use anyhow::Result;
use oauth2::TokenResponse;
use tauri::{InvokeError, Manager};
/// This command returns the a json string parse from a toml file at the path.
#[tauri::command]
fn read_toml(path: &str) -> Result<String, InvokeError> {
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
let value =
toml::from_str::<toml::Value>(&contents).map_err(|e| InvokeError::from_anyhow(e.into()))?;
let value = serde_json::to_string(&value).map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(value)
}
/// This command returns a string that is the contents of a file at the path.
#[tauri::command]
fn read_txt_file(path: &str) -> Result<String, InvokeError> {
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(contents)
}
/// This command instantiates a new window with auth.
/// The string returned from this method is the access token.
#[tauri::command]
async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError> {
println!("Logging in...");
// Do an OAuth 2.0 Device Authorization Grant dance to get a token.
let device_auth_url = oauth2::DeviceAuthorizationUrl::new(format!("{host}/oauth2/device/auth"))
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
// We can hardcode the client ID.
// This value is safe to be embedded in version control.
// This is the client ID of the KittyCAD app.
let client_id = "2af127fb-e14e-400a-9c57-a9ed08d1a5b7".to_string();
let auth_client = oauth2::basic::BasicClient::new(
oauth2::ClientId::new(client_id),
None,
oauth2::AuthUrl::new(format!("{host}/authorize"))
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
Some(
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
),
)
.set_auth_type(oauth2::AuthType::RequestBody)
.set_device_authorization_url(device_auth_url);
let details: oauth2::devicecode::StandardDeviceAuthorizationResponse = auth_client
.exchange_device_code()
.map_err(|e| InvokeError::from_anyhow(e.into()))?
.request_async(oauth2::reqwest::async_http_client)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
let Some(auth_uri) = details.verification_uri_complete() else {
return Err(InvokeError::from("getting the verification uri failed"));
};
// Open the system browser with the auth_uri.
// We do this in the browser and not a seperate window because we want 1password and
// other crap to work well.
tauri::api::shell::open(&app.shell_scope(), auth_uri.secret(), None)
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
// Wait for the user to login.
let token = auth_client
.exchange_device_access_token(&details)
.request_async(oauth2::reqwest::async_http_client, tokio::time::sleep, None)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?
.access_token()
.secret()
.to_string();
Ok(token)
}
fn main() {
tauri::Builder::default()
.setup(|app| {
#[cfg(debug_assertions)] // only include this code on debug builds
{
let window = app.get_window("main").unwrap();
// comment out the below if you don't devtools to open everytime.
// it's useful because otherwise devtools shuts everytime rust code changes.
window.open_devtools();
}
Ok(())
})
.invoke_handler(tauri::generate_handler![login, read_toml, read_txt_file])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -7,12 +7,37 @@
"distDir": "../build"
},
"package": {
"productName": "untitled-app",
"version": "0.1.0"
"productName": "KittyCAD Modeling",
"version": "0.0.3"
},
"tauri": {
"allowlist": {
"all": false
"all": false,
"dialog": {
"all": true,
"ask": true,
"confirm": true,
"message": true,
"open": true,
"save": true
},
"fs": {
"scope": [
"$HOME/**/*"
],
"all": true
},
"http": {
"request": true,
"scope": [
"https://dev.kittycad.io/*",
"https://kittycad.io/*",
"https://api.dev.kittycad.io/*"
]
},
"shell": {
"open": true
}
},
"bundle": {
"active": true,
@ -29,7 +54,7 @@
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "untitled-app",
"identifier": "KittyCAD-modeling-app",
"longDescription": "",
"macOS": {
"entitlements": null,
@ -56,10 +81,10 @@
"windows": [
{
"fullscreen": false,
"height": 600,
"height": 1200,
"resizable": true,
"title": "untitled-app",
"width": 800
"title": "KittyCAD Modeling",
"width": 1800
}
]
}

View File

@ -1,6 +1,6 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import App from './App'
import { App } from './App'
import { BrowserRouter } from 'react-router-dom'
let listener: ((rect: any) => void) | undefined = undefined
;(global as any).ResizeObserver = class ResizeObserver {
@ -13,7 +13,11 @@ let listener: ((rect: any) => void) | undefined = undefined
}
test('renders learn react link', () => {
render(<App />)
render(
<BrowserRouter>
<App />
</BrowserRouter>
)
const linkElement = screen.getByText(/Variables/i)
expect(linkElement).toBeInTheDocument()
})

View File

@ -1,69 +1,156 @@
import { useRef, useState, useEffect } from 'react'
import { Canvas } from '@react-three/fiber'
import { Allotment } from 'allotment'
import { OrbitControls, OrthographicCamera } from '@react-three/drei'
import {
useRef,
useEffect,
useLayoutEffect,
useMemo,
useCallback,
MouseEventHandler,
} from 'react'
import { DebugPanel } from './components/DebugPanel'
import { v4 as uuidv4 } from 'uuid'
import { asyncLexer } from './lang/tokeniser'
import { abstractSyntaxTree } from './lang/abstractSyntaxTree'
import { executor, ExtrudeGroup, SketchGroup } from './lang/executor'
import { _executor } from './lang/executor'
import CodeMirror from '@uiw/react-codemirror'
import { javascript } from '@codemirror/lang-javascript'
import { langs } from '@uiw/codemirror-extensions-langs'
import { linter, lintGutter } from '@codemirror/lint'
import { ViewUpdate } from '@codemirror/view'
import {
lineHighlightField,
addLineHighlight,
} from './editor/highlightextension'
import { useStore } from './useStore'
import { Toolbar } from './Toolbar'
import { BasePlanes } from './components/BasePlanes'
import { SketchPlane } from './components/SketchPlane'
import { Logs } from './components/Logs'
import { AxisIndicator } from './components/AxisIndicator'
import { RenderViewerArtifacts } from './components/RenderViewerArtifacts'
import { PanelHeader } from './components/PanelHeader'
import { PaneType, Selections, Themes, useStore } from './useStore'
import { Logs, KCLErrors } from './components/Logs'
import { CollapsiblePanel } from './components/CollapsiblePanel'
import { MemoryPanel } from './components/MemoryPanel'
import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream'
import ModalContainer from 'react-modal-promise'
import {
EngineCommand,
EngineCommandManager,
} from './lang/std/engineConnection'
import { isOverlap, throttle } from './lib/utils'
import { AppHeader } from './components/AppHeader'
import { KCLError, kclErrToDiagnostic } from './lang/errors'
import { Resizable } from 're-resizable'
import {
faCode,
faCodeCommit,
faSquareRootVariable,
} from '@fortawesome/free-solid-svg-icons'
import { useHotkeys } from 'react-hotkeys-hook'
import { TEST } from './env'
import { getNormalisedCoordinates } from './lib/utils'
import { getSystemTheme } from './lib/getSystemTheme'
const OrrthographicCamera = OrthographicCamera as any
function App() {
const cam = useRef()
export function App() {
const streamRef = useRef<HTMLDivElement>(null)
useHotKeyListener()
const {
editorView,
setEditorView,
setSelectionRanges,
selectionRanges: selectionRange,
guiMode,
lastGuiMode,
selectionRanges,
addLog,
addKCLError,
code,
setCode,
setAst,
setError,
errorState,
setProgramMemory,
resetLogs,
resetKCLErrors,
selectionRangeTypeMap,
setArtifactMap,
engineCommandManager,
setEngineCommandManager,
setHighlightRange,
setCursor2,
sourceRangeMap,
setMediaStream,
setIsStreamReady,
isStreamReady,
isMouseDownInStream,
fileId,
cmdId,
setCmdId,
token,
formatCode,
debugPanel,
theme,
openPanes,
setOpenPanes,
onboardingStatus,
setDidDragInStream,
setStreamDimensions,
streamDimensions,
} = useStore((s) => ({
editorView: s.editorView,
setEditorView: s.setEditorView,
setSelectionRanges: s.setSelectionRanges,
selectionRanges: s.selectionRanges,
guiMode: s.guiMode,
setGuiMode: s.setGuiMode,
addLog: s.addLog,
code: s.code,
setCode: s.setCode,
setAst: s.setAst,
lastGuiMode: s.lastGuiMode,
setError: s.setError,
errorState: s.errorState,
setProgramMemory: s.setProgramMemory,
resetLogs: s.resetLogs,
resetKCLErrors: s.resetKCLErrors,
selectionRangeTypeMap: s.selectionRangeTypeMap,
setArtifactMap: s.setArtifactNSourceRangeMaps,
engineCommandManager: s.engineCommandManager,
setEngineCommandManager: s.setEngineCommandManager,
setHighlightRange: s.setHighlightRange,
isShiftDown: s.isShiftDown,
setCursor: s.setCursor,
setCursor2: s.setCursor2,
sourceRangeMap: s.sourceRangeMap,
setMediaStream: s.setMediaStream,
isStreamReady: s.isStreamReady,
setIsStreamReady: s.setIsStreamReady,
isMouseDownInStream: s.isMouseDownInStream,
fileId: s.fileId,
cmdId: s.cmdId,
setCmdId: s.setCmdId,
token: s.token,
formatCode: s.formatCode,
debugPanel: s.debugPanel,
addKCLError: s.addKCLError,
theme: s.theme,
openPanes: s.openPanes,
setOpenPanes: s.setOpenPanes,
onboardingStatus: s.onboardingStatus,
setDidDragInStream: s.setDidDragInStream,
setStreamDimensions: s.setStreamDimensions,
streamDimensions: s.streamDimensions,
}))
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
// Pane toggling keyboard shortcuts
const togglePane = useCallback(
(newPane: PaneType) =>
openPanes.includes(newPane)
? setOpenPanes(openPanes.filter((p) => p !== newPane))
: setOpenPanes([...openPanes, newPane]),
[openPanes, setOpenPanes]
)
useHotkeys('shift + c', () => togglePane('code'))
useHotkeys('shift + v', () => togglePane('variables'))
useHotkeys('shift + l', () => togglePane('logs'))
useHotkeys('shift + e', () => togglePane('kclErrors'))
useHotkeys('shift + d', () => togglePane('debug'))
const paneOpacity =
onboardingStatus === 'camera'
? 'opacity-20'
: isMouseDownInStream
? 'opacity-40'
: ''
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = (value: string, viewUpdate: ViewUpdate) => {
setCode(value)
@ -78,18 +165,17 @@ function App() {
const ranges = viewUpdate.state.selection.ranges
const isChange =
ranges.length !== selectionRange.codeBasedSelections.length ||
ranges.length !== selectionRanges.codeBasedSelections.length ||
ranges.some(({ from, to }, i) => {
return (
from !== selectionRange.codeBasedSelections[i].range[0] ||
to !== selectionRange.codeBasedSelections[i].range[1]
from !== selectionRanges.codeBasedSelections[i].range[0] ||
to !== selectionRanges.codeBasedSelections[i].range[1]
)
})
if (!isChange) return
setSelectionRanges({
otherSelections: [],
codeBasedSelections: ranges.map(({ from, to }, i) => {
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
({ from, to }) => {
if (selectionRangeTypeMap[to]) {
return {
type: selectionRangeTypeMap[to],
@ -100,15 +186,67 @@ function App() {
type: 'default',
range: [from, to],
}
}),
}
)
const idBasedSelections = codeBasedSelections
.map(({ type, range }) => {
const hasOverlap = Object.entries(sourceRangeMap).filter(
([_, sourceRange]) => {
return isOverlap(sourceRange, range)
}
)
if (hasOverlap.length) {
return {
type,
id: hasOverlap[0][0],
}
}
})
.filter(Boolean) as any
engineCommandManager?.cusorsSelected({
otherSelections: [],
idBasedSelections,
})
setSelectionRanges({
otherSelections: [],
codeBasedSelections,
})
}
const [geoArray, setGeoArray] = useState<(ExtrudeGroup | SketchGroup)[]>([])
const pixelDensity = window.devicePixelRatio
const streamWidth = streamRef?.current?.offsetWidth
const streamHeight = streamRef?.current?.offsetHeight
const width = streamWidth ? streamWidth * pixelDensity : 0
const quadWidth = Math.round(width / 4) * 4
const height = streamHeight ? streamHeight * pixelDensity : 0
const quadHeight = Math.round(height / 4) * 4
useLayoutEffect(() => {
setStreamDimensions({
streamWidth: quadWidth,
streamHeight: quadHeight,
})
if (!width || !height) return
const eng = new EngineCommandManager({
setMediaStream,
setIsStreamReady,
width: quadWidth,
height: quadHeight,
token,
})
setEngineCommandManager(eng)
return () => {
eng?.tearDown()
}
}, [quadWidth, quadHeight])
useEffect(() => {
if (!isStreamReady) return
const asyncWrap = async () => {
try {
if (!code) {
setGeoArray([])
setAst(null)
return
}
@ -116,151 +254,254 @@ function App() {
const _ast = abstractSyntaxTree(tokens)
setAst(_ast)
resetLogs()
const programMemory = executor(_ast, {
root: {
log: {
type: 'userVal',
value: (a: any) => {
addLog(a)
},
__meta: [
{
pathToNode: [],
sourceRange: [0, 0],
resetKCLErrors()
if (engineCommandManager) {
engineCommandManager.endSession()
engineCommandManager.startNewSession()
}
if (!engineCommandManager) return
const programMemory = await _executor(
_ast,
{
root: {
log: {
type: 'userVal',
value: (a: any) => {
addLog(a)
},
],
},
_0: {
type: 'userVal',
value: 0,
__meta: [],
},
_90: {
type: 'userVal',
value: 90,
__meta: [],
},
_180: {
type: 'userVal',
value: 180,
__meta: [],
},
_270: {
type: 'userVal',
value: 270,
__meta: [],
__meta: [
{
pathToNode: [],
sourceRange: [0, 0],
},
],
},
_0: {
type: 'userVal',
value: 0,
__meta: [],
},
_90: {
type: 'userVal',
value: 90,
__meta: [],
},
_180: {
type: 'userVal',
value: 180,
__meta: [],
},
_270: {
type: 'userVal',
value: 270,
__meta: [],
},
},
pendingMemory: {},
},
_sketch: [],
})
setProgramMemory(programMemory)
const geos = programMemory?.return
?.map(({ name }: { name: string }) => {
const artifact = programMemory?.root?.[name]
if (
artifact.type === 'extrudeGroup' ||
artifact.type === 'sketchGroup'
) {
return artifact
}
return null
})
.filter((a) => a) as (ExtrudeGroup | SketchGroup)[]
engineCommandManager,
{ bodyType: 'root' },
[]
)
const { artifactMap, sourceRangeMap } =
await engineCommandManager.waitForAllCommands()
setArtifactMap({ artifactMap, sourceRangeMap })
engineCommandManager.onHover((id) => {
if (!id) {
setHighlightRange([0, 0])
} else {
const sourceRange = sourceRangeMap[id]
setHighlightRange(sourceRange)
}
})
engineCommandManager.onClick((selections) => {
if (!selections) {
setCursor2()
return
}
const { id, type } = selections
setCursor2({ range: sourceRangeMap[id], type })
})
if (programMemory !== undefined) {
setProgramMemory(programMemory)
}
setGeoArray(geos)
console.log(programMemory)
setError()
} catch (e: any) {
setError('problem')
console.log(e)
addLog(e)
if (e instanceof KCLError) {
addKCLError(e)
} else {
setError('problem')
console.log(e)
addLog(e)
}
}
}
asyncWrap()
}, [code])
}, [code, isStreamReady])
const debounceSocketSend = throttle<EngineCommand>((message) => {
engineCommandManager?.sendSceneCommand(message)
}, 16)
const handleMouseMove: MouseEventHandler<HTMLDivElement> = ({
clientX,
clientY,
ctrlKey,
currentTarget,
}) => {
if (isMouseDownInStream) {
setDidDragInStream(true)
}
const { x, y } = getNormalisedCoordinates({
clientX,
clientY,
el: currentTarget,
...streamDimensions,
})
const interaction = ctrlKey ? 'pan' : 'rotate'
const newCmdId = uuidv4()
setCmdId(newCmdId)
if (cmdId && isMouseDownInStream) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_move',
interaction,
window: { x, y },
},
cmd_id: newCmdId,
file_id: fileId,
})
} else {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'highlight_set_entity',
selected_at_window: { x, y },
},
cmd_id: newCmdId,
file_id: fileId,
})
}
}
const extraExtensions = useMemo(() => {
if (TEST) return []
return [
lintGutter(),
linter((_view) => {
return kclErrToDiagnostic(useStore.getState().kclErrors)
}),
]
}, [])
return (
<div className="h-screen">
<div
className="h-screen overflow-hidden relative flex flex-col"
onMouseMove={handleMouseMove}
ref={streamRef}
>
<AppHeader
className={
'transition-opacity transition-duration-75 ' +
paneOpacity +
(isMouseDownInStream ? ' pointer-events-none' : '')
}
/>
<ModalContainer />
<Allotment snap={true}>
<Allotment vertical defaultSizes={[400, 1, 1]} minSize={20}>
<div className="h-full flex flex-col items-start">
<PanelHeader title="Editor" />
{/* <button
disabled={!shouldFormat}
onClick={formatCode}
className={`${!shouldFormat && 'text-gray-300'}`}
>
format
</button> */}
<div
className="bg-red h-full w-full overflow-auto"
id="code-mirror-override"
>
<Resizable
className={
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
(isMouseDownInStream || onboardingStatus === 'camera'
? ' pointer-events-none '
: ' ') +
paneOpacity
}
defaultSize={{
width: '400px',
height: 'auto',
}}
minWidth={200}
maxWidth={600}
minHeight={'auto'}
maxHeight={'auto'}
handleClasses={{
right:
'hover:bg-liquid-30/40 dark:hover:bg-liquid-10/40 bg-transparent transition-colors duration-100 transition-ease-out delay-100',
}}
>
<div className="h-full flex flex-col justify-between">
<CollapsiblePanel
title="Code"
icon={faCode}
className="open:!mb-2"
open={openPanes.includes('code')}
>
<div className="px-2 py-1">
<button
// disabled={!shouldFormat}
onClick={formatCode}
// className={`${!shouldFormat && 'text-gray-300'}`}
>
format
</button>
</div>
<div id="code-mirror-override">
<CodeMirror
className="h-full"
value={code}
extensions={[javascript({ jsx: true }), lineHighlightField]}
extensions={[
langs.javascript({ jsx: true }),
lineHighlightField,
...extraExtensions,
]}
onChange={onChange}
onUpdate={onUpdate}
theme={editorTheme}
onCreateEditor={(_editorView) => setEditorView(_editorView)}
/>
</div>
</div>
<MemoryPanel />
<Logs />
</Allotment>
<Allotment vertical defaultSizes={[400, 1]} minSize={20}>
<div className="h-full">
<PanelHeader title="Drafting Board" />
<Toolbar />
<div className="border h-full border-gray-300 relative">
<div className="absolute inset-0">
<Canvas>
<OrbitControls
enableDamping={false}
enablePan
enableRotate={
!(
guiMode.mode === 'canEditSketch' ||
guiMode.mode === 'sketch'
)
}
enableZoom
reverseOrbit={false}
/>
<OrrthographicCamera
ref={cam}
makeDefault
position={[0, 0, 1000]}
zoom={100}
rotation={[0, 0, 0]}
far={2000}
/>
<ambientLight />
<pointLight position={[10, 10, 10]} />
<RenderViewerArtifacts artifacts={geoArray} />
<BasePlanes />
<SketchPlane />
<AxisIndicator />
</Canvas>
</div>
{errorState.isError && (
<div className="absolute inset-0 bg-gray-700/20">
<pre>
{'last first: \n\n' +
JSON.stringify(lastGuiMode, null, 2) +
'\n\n' +
JSON.stringify(guiMode)}
</pre>
</div>
)}
</div>
</div>
<Stream />
</Allotment>
</Allotment>
</CollapsiblePanel>
<section className="flex flex-col">
<MemoryPanel
theme={editorTheme}
open={openPanes.includes('variables')}
title="Variables"
icon={faSquareRootVariable}
/>
<Logs
theme={editorTheme}
open={openPanes.includes('logs')}
title="Logs"
icon={faCodeCommit}
/>
<KCLErrors
theme={editorTheme}
open={openPanes.includes('kclErrors')}
title="KCL Errors"
iconClassNames={{ icon: 'group-open:text-destroy-30' }}
/>
</section>
</div>
</Resizable>
<Stream className="absolute inset-0 z-0" />
{debugPanel && (
<DebugPanel
title="Debug"
className={
'transition-opacity transition-duration-75 ' +
paneOpacity +
(isMouseDownInStream ? ' pointer-events-none' : '')
}
open={openPanes.includes('debug')}
/>
)}
</div>
)
}
export default App

View File

@ -1,31 +1,40 @@
import useSWR from 'swr'
import fetcher from './lib/fetcher'
import withBaseUrl from './lib/withBaseURL'
import App from './App'
import { User, useStore } from './useStore'
import { useNavigate } from 'react-router-dom'
import { useEffect } from 'react'
import { isTauri } from './lib/isTauri'
import Loading from './components/Loading'
import { paths } from './Router'
export const Auth = () => {
const { data: user } = useSWR(withBaseUrl('/user'), fetcher) as any
const isLocalHost =
typeof window !== 'undefined' && window.location.hostname === 'localhost'
// Wrapper around protected routes, used in src/Router.tsx
export const Auth = ({ children }: React.PropsWithChildren) => {
const { data: user, isLoading } = useSWR<
User | Partial<{ error_code: string }>
>(withBaseUrl('/user'), fetcher)
const { token, setUser } = useStore((s) => ({
token: s.token,
setUser: s.setUser,
}))
const navigate = useNavigate()
if (!user && !isLocalHost) {
return (
<>
<div className=" bg-gray-800 p-1 px-4 rounded-r-lg pointer-events-auto flex items-center">
<a
className="font-bold mr-2 text-purple-400"
rel="noopener noreferrer"
target={'_self'}
href={`https://dev.kittycad.io/signin?callbackUrl=${encodeURIComponent(
typeof window !== 'undefined' && window.location.href
)}`}
>
Sign in
</a>
</div>
</>
)
}
useEffect(() => {
if (user && 'id' in user) setUser(user)
}, [user, setUser])
return <App />
useEffect(() => {
if (
(isTauri() && !token) ||
(!isTauri() && !isLoading && !(user && 'id' in user))
) {
navigate(paths.SIGN_IN)
}
}, [user, token, navigate, isLoading])
return isLoading ? (
<Loading>Loading KittyCAD Modeling App...</Loading>
) : (
<>{children}</>
)
}

92
src/Router.tsx Normal file
View File

@ -0,0 +1,92 @@
import { App } from './App'
import {
createBrowserRouter,
Outlet,
redirect,
RouterProvider,
} from 'react-router-dom'
import { ErrorPage } from './components/ErrorPage'
import { Settings } from './routes/Settings'
import Onboarding, {
onboardingRoutes,
onboardingPaths,
} from './routes/Onboarding'
import SignIn from './routes/SignIn'
import { Auth } from './Auth'
const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => {
return Object.fromEntries(
Object.entries(routesObject).map(([constName, path]) => [
constName,
prepend + path,
])
)
}
export const paths = {
INDEX: '/',
SETTINGS: '/settings',
SIGN_IN: '/signin',
ONBOARDING: prependRoutes(onboardingPaths)(
'/onboarding/'
) as typeof onboardingPaths,
}
const router = createBrowserRouter([
{
path: paths.INDEX,
element: (
<Auth>
<Outlet />
<App />
</Auth>
),
errorElement: <ErrorPage />,
loader: ({ request }) => {
const store = localStorage.getItem('store')
if (store === null) {
return redirect(paths.ONBOARDING.INDEX)
} else {
const status = JSON.parse(store).state.onboardingStatus || ''
const notEnRouteToOnboarding =
!request.url.includes(paths.ONBOARDING.INDEX) &&
request.method === 'GET'
// '' is the initial state, 'done' and 'dismissed' are the final states
const hasValidOnboardingStatus =
(status !== undefined && status.length === 0) ||
!(status === 'done' || status === 'dismissed')
const shouldRedirectToOnboarding =
notEnRouteToOnboarding && hasValidOnboardingStatus
if (shouldRedirectToOnboarding) {
return redirect(paths.ONBOARDING.INDEX + status)
}
}
return null
},
children: [
{
path: paths.SETTINGS,
element: <Settings />,
},
{
path: paths.ONBOARDING.INDEX,
element: <Onboarding />,
children: onboardingRoutes,
},
],
},
{
path: paths.SIGN_IN,
element: <SignIn />,
},
])
/**
* All routes in the app, used in src/index.tsx
* @returns RouterProvider
*/
export const Router = () => {
return <RouterProvider router={router} />
}

View File

@ -11,6 +11,7 @@ import { SetAngleLength } from './components/Toolbar/setAngleLength'
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
import { ExportButton } from './components/ExportButton'
export const Toolbar = () => {
const {
@ -31,6 +32,7 @@ export const Toolbar = () => {
return (
<div>
<ExportButton />
{guiMode.mode === 'default' && (
<button
onClick={() => {
@ -39,7 +41,6 @@ export const Toolbar = () => {
sketchMode: 'selectFace',
})
}}
className="border m-1 px-1 rounded text-xs"
>
Start Sketch
</button>
@ -59,7 +60,6 @@ export const Toolbar = () => {
)
updateAst(modifiedAst)
}}
className="border m-1 px-1 rounded text-xs"
>
SketchOnFace
</button>
@ -75,7 +75,6 @@ export const Toolbar = () => {
position: guiMode.position,
})
}}
className="border m-1 px-1 rounded text-xs"
>
Edit Sketch
</button>
@ -95,7 +94,6 @@ export const Toolbar = () => {
)
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
}}
className="border m-1 px-1 rounded text-xs"
>
ExtrudeSketch
</button>
@ -113,7 +111,6 @@ export const Toolbar = () => {
)
updateAst(modifiedAst, { focusPath: pathToExtrudeArg })
}}
className="border m-1 px-1 rounded text-xs"
>
ExtrudeSketch (w/o pipe)
</button>
@ -121,10 +118,7 @@ export const Toolbar = () => {
)}
{guiMode.mode === 'sketch' && (
<button
onClick={() => setGuiMode({ mode: 'default' })}
className="border m-1 px-1 rounded text-xs"
>
<button onClick={() => setGuiMode({ mode: 'default' })}>
Exit sketch
</button>
)}
@ -142,9 +136,6 @@ export const Toolbar = () => {
return (
<button
key={sketchFnName}
className={`border m-0.5 px-0.5 rounded text-xs ${
guiMode.sketchMode === sketchFnName && 'bg-gray-400'
}`}
onClick={() =>
setGuiMode({
...guiMode,

263
src/colors.css Normal file
View File

@ -0,0 +1,263 @@
:root {
/*
Generated using Catmosphere Theme Builder
by KittyCAD
https://catmosphere-theme-builder.vercel.app/?colors=%5B%7B%22from%22:%7B%22l%22:1,%22c%22:0.01,%22h%22:78%7D,%22to%22:%7B%22l%22:0.065,%22c%22:0.05,%22h%22:182.6%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.45,%22h%22:122.4%7D,%22to%22:%7B%22l%22:0.13,%22c%22:0.031,%22h%22:137.2%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.13,%22h%22:176%7D,%22to%22:%7B%22l%22:0.116,%22c%22:0.097,%22h%22:213.1%7D,%22stops%22:10,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.169,%22h%22:144.4%7D,%22to%22:%7B%22l%22:0.12,%22c%22:0.45,%22h%22:132.7%7D,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.087,%22h%22:261.6%7D,%22to%22:%7B%22l%22:0.22,%22c%22:0.084,%22h%22:275.5%7D,%22steps%22:12,%22uuid%22:%227tpx9pf1zd6%22%7D,%7B%22from%22:%7B%22l%22:0.954,%22c%22:0.108,%22h%22:280.6%7D,%22to%22:%7B%22l%22:0.166,%22c%22:0.188,%22h%22:263.8%7D,%22steps%22:12,%22uuid%22:%22vu652mebd3%22%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.115,%22h%22:0%7D,%22to%22:%7B%22l%22:0.096,%22c%22:0.261,%22h%22:302%7D,%22steps%22:12%7D,%7B%22from%22:%7B%22l%22:1,%22c%22:0.185,%22h%22:19.8%7D,%22to%22:%7B%22l%22:0.368,%22c%22:0.45,%22h%22:9.4%7D,%22steps%22:8,%22uuid%22:%22g05inkd34l%22%7D,%7B%22from%22:%7B%22l%22:0.912,%22c%22:0.139,%22h%22:87%7D,%22to%22:%7B%22l%22:0.502,%22c%22:0.45,%22h%22:97.7%7D,%22steps%22:8,%22uuid%22:%22l892hcw4ef%22%7D,%7B%22from%22:%7B%22l%22:0.89,%22c%22:0.16,%22h%22:143.4%7D,%22to%22:%7B%22l%22:0.466,%22c%22:0.208,%22h%22:147.7%7D,%22steps%22:8,%22uuid%22:%22hkd09y9ov4h%22%7D%5D
*/
/* Chalkboard */
--chalkboard-10: oklch(99.7% 0.008766 102.8deg);
--chalkboard-20: oklch(91.34% 0.009353 109deg);
--chalkboard-30: oklch(82.99% 0.00994 115.2deg);
--chalkboard-40: oklch(74.63% 0.01053 121.4deg);
--chalkboard-50: oklch(66.27% 0.01111 127.6deg);
--chalkboard-60: oklch(57.92% 0.0117 133.9deg);
--chalkboard-70: oklch(49.56% 0.01229 140.1deg);
--chalkboard-80: oklch(41.21% 0.01288 146.3deg);
--chalkboard-90: oklch(32.85% 0.01346 152.5deg);
--chalkboard-100: oklch(24.49% 0.01405 158.7deg);
--chalkboard-110: oklch(16.14% 0.01464 164.9deg);
--chalkboard-120: oklch(7.783% 0.01522 171.1deg);
/* Energy */
--energy-10: oklch(93.31% 0.227 122.3deg);
--energy-20: oklch(86.01% 0.2092 123.6deg);
--energy-30: oklch(78.71% 0.1914 125deg);
--energy-40: oklch(71.41% 0.1736 126.3deg);
--energy-50: oklch(64.1% 0.1557 127.7deg);
--energy-60: oklch(56.8% 0.1379 129.1deg);
--energy-70: oklch(49.5% 0.1201 130.4deg);
--energy-80: oklch(42.2% 0.1023 131.8deg);
--energy-90: oklch(34.9% 0.08446 133.1deg);
--energy-100: oklch(27.6% 0.06664 134.5deg);
--energy-110: oklch(20.3% 0.04882 135.8deg);
--energy-120: oklch(13% 0.031 137.2deg);
/* Liquid */
--liquid-10: oklch(93.45% 0.1002 193.1deg);
--liquid-20: oklch(86.21% 0.09511 198.7deg);
--liquid-30: oklch(78.97% 0.09003 204.2deg);
--liquid-40: oklch(71.74% 0.08495 209.8deg);
--liquid-50: oklch(64.5% 0.07988 215.3deg);
--liquid-60: oklch(57.26% 0.0748 220.9deg);
--liquid-70: oklch(50.03% 0.06972 226.4deg);
--liquid-80: oklch(42.79% 0.06465 232deg);
--liquid-90: oklch(35.56% 0.05957 237.5deg);
--liquid-100: oklch(28.32% 0.0545 243.1deg);
--liquid-110: oklch(21.08% 0.04942 248.6deg);
--liquid-120: oklch(13.85% 0.04434 254.2deg);
/* Fern */
--fern-10: oklch(93.22% 0.1243 144.8deg);
--fern-20: oklch(86.59% 0.1193 144.6deg);
--fern-30: oklch(79.97% 0.1143 144.4deg);
--fern-40: oklch(73.34% 0.1093 144.2deg);
--fern-50: oklch(66.71% 0.1043 144deg);
--fern-60: oklch(60.09% 0.09927 143.8deg);
--fern-70: oklch(53.46% 0.09425 143.6deg);
--fern-80: oklch(46.83% 0.08924 143.3deg);
--fern-90: oklch(40.21% 0.08422 143.1deg);
--fern-100: oklch(33.58% 0.0792 142.9deg);
--fern-110: oklch(26.95% 0.07419 142.7deg);
--fern-120: oklch(20.33% 0.06917 142.5deg);
/* Cool */
--cool-10: oklch(97.71% 0.03321 196.6deg);
--cool-20: oklch(90.82% 0.03783 203.8deg);
--cool-30: oklch(83.94% 0.04245 211deg);
--cool-40: oklch(77.06% 0.04706 218.1deg);
--cool-50: oklch(70.18% 0.05168 225.3deg);
--cool-60: oklch(63.29% 0.0563 232.5deg);
--cool-70: oklch(56.41% 0.06091 239.6deg);
--cool-80: oklch(49.53% 0.06553 246.8deg);
--cool-90: oklch(42.65% 0.07015 254deg);
--cool-100: oklch(35.76% 0.07477 261.2deg);
--cool-110: oklch(28.88% 0.07938 268.3deg);
--cool-120: oklch(22% 0.084 275.5deg);
/* River */
--river-10: oklch(93.35% 0.03169 273.4deg);
--river-20: oklch(86.91% 0.04221 273.1deg);
--river-30: oklch(80.46% 0.05274 272.7deg);
--river-40: oklch(74.01% 0.06326 272.4deg);
--river-50: oklch(67.57% 0.07378 272deg);
--river-60: oklch(61.12% 0.0843 271.7deg);
--river-70: oklch(54.67% 0.09483 271.4deg);
--river-80: oklch(48.22% 0.1053 271deg);
--river-90: oklch(41.78% 0.1159 270.7deg);
--river-100: oklch(35.33% 0.1264 270.4deg);
--river-110: oklch(28.88% 0.1369 270deg);
--river-120: oklch(22.44% 0.1474 269.7deg);
/* Berry */
--berry-10: oklch(93.77% 0.05212 329deg);
--berry-20: oklch(87.3% 0.05912 325.3deg);
--berry-30: oklch(80.82% 0.06612 321.6deg);
--berry-40: oklch(74.34% 0.07313 317.8deg);
--berry-50: oklch(67.86% 0.08013 314.1deg);
--berry-60: oklch(61.39% 0.08713 310.3deg);
--berry-70: oklch(54.91% 0.09413 306.6deg);
--berry-80: oklch(48.43% 0.1011 302.8deg);
--berry-90: oklch(41.95% 0.1081 299.1deg);
--berry-100: oklch(35.47% 0.1151 295.4deg);
--berry-110: oklch(29% 0.1221 291.6deg);
--berry-120: oklch(22.52% 0.1291 287.9deg);
/* Destroy */
--destroy-10: oklch(88.21% 0.06281 14.85deg);
--destroy-20: oklch(83.23% 0.08511 16.91deg);
--destroy-30: oklch(78.25% 0.1074 18.96deg);
--destroy-40: oklch(73.27% 0.1297 21.01deg);
--destroy-50: oklch(68.29% 0.152 23.07deg);
--destroy-60: oklch(63.31% 0.1743 25.12deg);
--destroy-70: oklch(58.33% 0.1966 27.18deg);
--destroy-80: oklch(53.35% 0.2189 29.23deg);
/* Warn */
--warn-10: oklch(90.19% 0.1361 92deg);
--warn-20: oklch(84.6% 0.1388 84.84deg);
--warn-30: oklch(79.01% 0.1414 77.68deg);
--warn-40: oklch(73.42% 0.144 70.52deg);
--warn-50: oklch(67.83% 0.1466 63.36deg);
--warn-60: oklch(62.24% 0.1492 56.2deg);
--warn-70: oklch(56.65% 0.1518 49.04deg);
--warn-80: oklch(51.06% 0.1544 41.88deg);
/* Succeed */
--succeed-10: oklch(89% 0.16 143.4deg);
--succeed-20: oklch(83.23% 0.1608 143.3deg);
--succeed-30: oklch(77.46% 0.1616 143.1deg);
--succeed-40: oklch(71.69% 0.1623 143deg);
--succeed-50: oklch(65.92% 0.1631 142.9deg);
--succeed-60: oklch(60.16% 0.1639 142.8deg);
--succeed-70: oklch(54.39% 0.1647 142.6deg);
--succeed-80: oklch(48.62% 0.1654 142.5deg);
/* Base values for use with Tailwind. */
/* Chalkboard */
--_chalkboard-10: 99.7% 0.008766 102.8deg;
--_chalkboard-20: 91.34% 0.009353 109deg;
--_chalkboard-30: 82.99% 0.00994 115.2deg;
--_chalkboard-40: 74.63% 0.01053 121.4deg;
--_chalkboard-50: 66.27% 0.01111 127.6deg;
--_chalkboard-60: 57.92% 0.0117 133.9deg;
--_chalkboard-70: 49.56% 0.01229 140.1deg;
--_chalkboard-80: 41.21% 0.01288 146.3deg;
--_chalkboard-90: 32.85% 0.01346 152.5deg;
--_chalkboard-100: 24.49% 0.01405 158.7deg;
--_chalkboard-110: 16.14% 0.01464 164.9deg;
--_chalkboard-120: 7.783% 0.01522 171.1deg;
/* Energy */
--_energy-10: 93.31% 0.227 122.3deg;
--_energy-20: 86.01% 0.2092 123.6deg;
--_energy-30: 78.71% 0.1914 125deg;
--_energy-40: 71.41% 0.1736 126.3deg;
--_energy-50: 64.1% 0.1557 127.7deg;
--_energy-60: 56.8% 0.1379 129.1deg;
--_energy-70: 49.5% 0.1201 130.4deg;
--_energy-80: 42.2% 0.1023 131.8deg;
--_energy-90: 34.9% 0.08446 133.1deg;
--_energy-100: 27.6% 0.06664 134.5deg;
--_energy-110: 20.3% 0.04882 135.8deg;
--_energy-120: 13% 0.031 137.2deg;
/* Liquid */
--_liquid-10: 93.45% 0.1002 193.1deg;
--_liquid-20: 86.21% 0.09511 198.7deg;
--_liquid-30: 78.97% 0.09003 204.2deg;
--_liquid-40: 71.74% 0.08495 209.8deg;
--_liquid-50: 64.5% 0.07988 215.3deg;
--_liquid-60: 57.26% 0.0748 220.9deg;
--_liquid-70: 50.03% 0.06972 226.4deg;
--_liquid-80: 42.79% 0.06465 232deg;
--_liquid-90: 35.56% 0.05957 237.5deg;
--_liquid-100: 28.32% 0.0545 243.1deg;
--_liquid-110: 21.08% 0.04942 248.6deg;
--_liquid-120: 13.85% 0.04434 254.2deg;
/* Fern */
--_fern-10: 93.22% 0.1243 144.8deg;
--_fern-20: 86.59% 0.1193 144.6deg;
--_fern-30: 79.97% 0.1143 144.4deg;
--_fern-40: 73.34% 0.1093 144.2deg;
--_fern-50: 66.71% 0.1043 144deg;
--_fern-60: 60.09% 0.09927 143.8deg;
--_fern-70: 53.46% 0.09425 143.6deg;
--_fern-80: 46.83% 0.08924 143.3deg;
--_fern-90: 40.21% 0.08422 143.1deg;
--_fern-100: 33.58% 0.0792 142.9deg;
--_fern-110: 26.95% 0.07419 142.7deg;
--_fern-120: 20.33% 0.06917 142.5deg;
/* Cool */
--_cool-10: 97.71% 0.03321 196.6deg;
--_cool-20: 90.82% 0.03783 203.8deg;
--_cool-30: 83.94% 0.04245 211deg;
--_cool-40: 77.06% 0.04706 218.1deg;
--_cool-50: 70.18% 0.05168 225.3deg;
--_cool-60: 63.29% 0.0563 232.5deg;
--_cool-70: 56.41% 0.06091 239.6deg;
--_cool-80: 49.53% 0.06553 246.8deg;
--_cool-90: 42.65% 0.07015 254deg;
--_cool-100: 35.76% 0.07477 261.2deg;
--_cool-110: 28.88% 0.07938 268.3deg;
--_cool-120: 22% 0.084 275.5deg;
/* River */
--_river-10: 93.35% 0.03169 273.4deg;
--_river-20: 86.91% 0.04221 273.1deg;
--_river-30: 80.46% 0.05274 272.7deg;
--_river-40: 74.01% 0.06326 272.4deg;
--_river-50: 67.57% 0.07378 272deg;
--_river-60: 61.12% 0.0843 271.7deg;
--_river-70: 54.67% 0.09483 271.4deg;
--_river-80: 48.22% 0.1053 271deg;
--_river-90: 41.78% 0.1159 270.7deg;
--_river-100: 35.33% 0.1264 270.4deg;
--_river-110: 28.88% 0.1369 270deg;
--_river-120: 22.44% 0.1474 269.7deg;
/* Berry */
--_berry-10: 93.77% 0.05212 329deg;
--_berry-20: 87.3% 0.05912 325.3deg;
--_berry-30: 80.82% 0.06612 321.6deg;
--_berry-40: 74.34% 0.07313 317.8deg;
--_berry-50: 67.86% 0.08013 314.1deg;
--_berry-60: 61.39% 0.08713 310.3deg;
--_berry-70: 54.91% 0.09413 306.6deg;
--_berry-80: 48.43% 0.1011 302.8deg;
--_berry-90: 41.95% 0.1081 299.1deg;
--_berry-100: 35.47% 0.1151 295.4deg;
--_berry-110: 29% 0.1221 291.6deg;
--_berry-120: 22.52% 0.1291 287.9deg;
/* Destroy */
--_destroy-10: 88.21% 0.06281 14.85deg;
--_destroy-20: 83.23% 0.08511 16.91deg;
--_destroy-30: 78.25% 0.1074 18.96deg;
--_destroy-40: 73.27% 0.1297 21.01deg;
--_destroy-50: 68.29% 0.152 23.07deg;
--_destroy-60: 63.31% 0.1743 25.12deg;
--_destroy-70: 58.33% 0.1966 27.18deg;
--_destroy-80: 53.35% 0.2189 29.23deg;
/* Warn */
--_warn-10: 90.19% 0.1361 92deg;
--_warn-20: 84.6% 0.1388 84.84deg;
--_warn-30: 79.01% 0.1414 77.68deg;
--_warn-40: 73.42% 0.144 70.52deg;
--_warn-50: 67.83% 0.1466 63.36deg;
--_warn-60: 62.24% 0.1492 56.2deg;
--_warn-70: 56.65% 0.1518 49.04deg;
--_warn-80: 51.06% 0.1544 41.88deg;
/* Succeed */
--_succeed-10: 89% 0.16 143.4deg;
--_succeed-20: 83.23% 0.1608 143.3deg;
--_succeed-30: 77.46% 0.1616 143.1deg;
--_succeed-40: 71.69% 0.1623 143deg;
--_succeed-50: 65.92% 0.1631 142.9deg;
--_succeed-60: 60.16% 0.1639 142.8deg;
--_succeed-70: 54.39% 0.1647 142.6deg;
--_succeed-80: 48.62% 0.1654 142.5deg;
}

View File

@ -0,0 +1,52 @@
import { Link } from 'react-router-dom'
import { ActionIcon, ActionIconProps } from './ActionIcon'
import React from 'react'
import { paths } from '../Router'
interface ActionButtonProps extends React.PropsWithChildren {
icon?: ActionIconProps
className?: string
onClick?: () => void
to?: string
Element?:
| 'button'
| 'link'
| React.ComponentType<React.HTMLAttributes<HTMLButtonElement>>
}
export const ActionButton = ({
icon,
className,
onClick,
to = paths.INDEX,
Element = 'button',
children,
...props
}: ActionButtonProps) => {
const classNames = `group mono text-base flex items-center gap-2 rounded-sm border border-chalkboard-40 dark:border-chalkboard-60 hover:border-liquid-40 dark:hover:bg-chalkboard-90 p-[3px] text-chalkboard-110 dark:text-chalkboard-10 hover:text-chalkboard-110 hover:dark:text-chalkboard-10 ${
icon ? 'pr-2' : 'px-2'
} ${className}`
if (Element === 'button') {
return (
<button onClick={onClick} className={classNames} {...props}>
{icon && <ActionIcon {...icon} />}
{children}
</button>
)
} else if (Element === 'link') {
return (
<Link to={to} className={classNames} {...props}>
{icon && <ActionIcon {...icon} />}
{children}
</Link>
)
} else {
return (
<Element onClick={onClick} className={classNames} {...props}>
{icon && <ActionIcon {...icon} />}
{children}
</Element>
)
}
}

View File

@ -0,0 +1,48 @@
import {
IconDefinition,
faCircleExclamation,
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
const iconSizes = {
sm: 12,
md: 14.4,
lg: 18,
}
export interface ActionIconProps extends React.PropsWithChildren {
icon?: IconDefinition
bgClassName?: string
iconClassName?: string
size?: keyof typeof iconSizes
}
export const ActionIcon = ({
icon,
bgClassName,
iconClassName,
size = 'md',
children,
}: ActionIconProps) => {
return (
<div
className={
'p-1 w-fit inline-grid place-content-center ' +
(bgClassName ||
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10')
}
>
{children || (
<FontAwesomeIcon
icon={icon || faCircleExclamation}
width={iconSizes[size]}
height={iconSizes[size]}
className={
iconClassName ||
'text-liquid-20 group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100'
}
/>
)}
</div>
)
}

View File

@ -0,0 +1,46 @@
import { Link } from 'react-router-dom'
import { Toolbar } from '../Toolbar'
import { useStore } from '../useStore'
import UserSidebarMenu from './UserSidebarMenu'
import { paths } from '../Router'
interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean
className?: string
}
export const AppHeader = ({
showToolbar = true,
children,
className = '',
}: AppHeaderProps) => {
const { user } = useStore((s) => ({
user: s.user,
}))
return (
<header
className={
'overlaid-panes sticky top-0 z-10 py-1 px-5 bg-chalkboard-10/50 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 flex justify-between items-center ' +
className
}
>
<Link to={paths.INDEX}>
<img
src="/kitt-arcade-winking.svg"
alt="KittyCAD App"
className="h-9 w-auto"
/>
<span className="sr-only">KittyCAD App</span>
</Link>
{/* Toolbar if the context deems it */}
{showToolbar && (
<div className="max-w-4xl">
<Toolbar />
</div>
)}
{/* If there are children, show them, otherwise show User menu */}
{children || <UserSidebarMenu user={user} />}
</header>
)
}

View File

@ -1,9 +1,6 @@
import { useEffect, useState, useRef } from 'react'
import {
abstractSyntaxTree,
BinaryPart,
Value,
} from '../lang/abstractSyntaxTree'
import { abstractSyntaxTree } from '../lang/abstractSyntaxTree'
import { BinaryPart, Value } from '../lang/abstractSyntaxTreeTypes'
import { executor } from '../lang/executor'
import {
createIdentifier,
@ -96,11 +93,14 @@ export function useCalc({
newVariableInsertIndex: number
setNewVariableName: (a: string) => void
} {
const { ast, programMemory, selectionRange } = useStore((s) => ({
ast: s.ast,
programMemory: s.programMemory,
selectionRange: s.selectionRanges.codeBasedSelections[0].range,
}))
const { ast, programMemory, selectionRange, engineCommandManager } = useStore(
(s) => ({
ast: s.ast,
programMemory: s.programMemory,
selectionRange: s.selectionRanges.codeBasedSelections[0].range,
engineCommandManager: s.engineCommandManager,
})
)
const inputRef = useRef<HTMLInputElement>(null)
const [availableVarInfo, setAvailableVarInfo] = useState<
ReturnType<typeof findAllPreviousVariables>
@ -141,6 +141,7 @@ export function useCalc({
}, [ast, programMemory, selectionRange])
useEffect(() => {
if (!engineCommandManager) return
try {
const code = `const __result__ = ${value}\nshow(__result__)`
const ast = abstractSyntaxTree(lexer(code))
@ -148,18 +149,19 @@ export function useCalc({
availableVarInfo.variables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
})
const programMemory = executor(ast, _programMem)
const resultDeclaration = ast.body.find(
(a) =>
a.type === 'VariableDeclaration' &&
a.declarations?.[0]?.id?.name === '__result__'
)
const init =
resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.root?.__result__?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init)
executor(ast, _programMem, engineCommandManager).then((programMemory) => {
const resultDeclaration = ast.body.find(
(a) =>
a.type === 'VariableDeclaration' &&
a.declarations?.[0]?.id?.name === '__result__'
)
const init =
resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init
const result = programMemory?.root?.__result__?.value
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init)
})
} catch (e) {
setCalcResult('NAN')
setValueNode(null)

View File

@ -1,16 +0,0 @@
export const AxisIndicator = () => (
<>
<mesh position={[0.5, 0, 0]}>
<boxBufferGeometry args={[1, 0.05, 0.05]} />
<meshStandardMaterial color="red" />
</mesh>
<mesh position={[0, 0.5, 0]}>
<boxBufferGeometry args={[0.05, 1, 0.05]} />
<meshStandardMaterial color="blue" />
</mesh>
<mesh position={[0, 0, 0.5]}>
<boxBufferGeometry args={[0.05, 0.05, 1]} />
<meshStandardMaterial color="green" />
</mesh>
</>
)

View File

@ -1,121 +0,0 @@
import { useState } from 'react'
import { DoubleSide, Vector3 } from 'three'
import { useStore } from '../useStore'
import { Intersection } from '@react-three/fiber'
import { Text } from '@react-three/drei'
import { addSketchTo } from '../lang/modifyAst'
import { Program } from '../lang/abstractSyntaxTree'
import { Quaternion } from 'three'
const opacity = 0.1
export const BasePlanes = () => {
const [axisIndex, setAxisIndex] = useState<null | number>(null)
const { setGuiMode, guiMode, ast, updateAst } = useStore(
({ guiMode, setGuiMode, ast, updateAst }) => ({
guiMode,
setGuiMode,
ast,
updateAst,
})
)
const onPointerEvent = ({
intersections,
}: {
intersections: Intersection[]
}) => {
if (!intersections.length) {
setAxisIndex(null)
return
}
let closestIntersection = intersections[0]
intersections.forEach((intersection) => {
if (intersection.distance < closestIntersection.distance)
closestIntersection = intersection
})
const smallestIndex = Number(closestIntersection.eventObject.name)
setAxisIndex(smallestIndex)
}
const onClick = () => {
if (guiMode.mode !== 'sketch') {
return null
}
if (guiMode.sketchMode !== 'selectFace') {
return null
}
let _ast: Program = ast
? ast
: {
type: 'Program',
start: 0,
end: 0,
body: [],
nonCodeMeta: {},
}
const axis = axisIndex === 0 ? 'xy' : axisIndex === 1 ? 'xz' : 'yz'
const quaternion = new Quaternion()
if (axisIndex === 1) {
quaternion.setFromAxisAngle(new Vector3(1, 0, 0), Math.PI / 2)
} else if (axisIndex === 2) {
quaternion.setFromAxisAngle(new Vector3(0, 1, 0), Math.PI / 2)
}
const { modifiedAst, id, pathToNode } = addSketchTo(_ast, axis)
setGuiMode({
mode: 'sketch',
sketchMode: 'sketchEdit',
rotation: quaternion.toArray() as [number, number, number, number],
position: [0, 0, 0],
pathToNode,
})
updateAst(modifiedAst)
}
if (guiMode.mode !== 'sketch') {
return null
}
if (guiMode.sketchMode !== 'selectFace') {
return null
}
return (
<>
{Array.from({ length: 3 }).map((_, index) => (
<mesh
key={index}
rotation-x={index === 1 ? -Math.PI / 2 : 0}
rotation-y={index === 2 ? -Math.PI / 2 : 0}
onPointerMove={onPointerEvent}
onPointerOut={onPointerEvent}
onClick={onClick}
name={`${index}`}
>
<planeGeometry args={[5, 5]} />
<meshStandardMaterial
color="blue"
side={DoubleSide}
transparent
opacity={opacity + (axisIndex === index ? 0.3 : 0)}
/>
<Text
fontSize={1}
color="#555"
position={[1, 1, 0.01]}
font={'/roboto.woff'}
>
{index === 0 ? 'xy' : index === 1 ? 'xz' : 'yz'}
</Text>
<Text
fontSize={1}
color="#555"
position={[1, 1, -0.01]}
font={'/roboto.woff'}
>
{index === 0 ? 'xy' : index === 1 ? 'xz' : 'yz'}
</Text>
</mesh>
))}
</>
)
}

View File

@ -0,0 +1,52 @@
.panel {
@apply relative overflow-auto z-0;
@apply bg-chalkboard-20/40;
}
:global(.dark) .panel {
@apply bg-chalkboard-110/50;
}
.header {
@apply sticky top-0 z-10 cursor-pointer;
@apply flex items-center gap-2 w-full p-2;
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
@apply bg-chalkboard-20;
}
.header:not(:last-of-type) {
@apply border-b;
}
:global(.dark) .header {
@apply bg-chalkboard-110 border-b-chalkboard-90 text-chalkboard-30;
}
:global(.dark) .header:not(:last-of-type) {
@apply border-b-2;
}
.panel:first-of-type .header {
@apply rounded-t;
}
.panel:last-of-type .header {
@apply rounded-b;
}
.panel[open] .header {
@apply rounded-t rounded-b-none;
}
.panel[open] {
@apply flex-grow max-h-full h-48 my-1 rounded;
}
.panel[open] + .panel[open],
.panel[open]:first-of-type {
@apply mt-0;
}
.panel[open]:last-of-type {
@apply mb-0;
}

View File

@ -0,0 +1,57 @@
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from './ActionIcon'
import styles from './CollapsiblePanel.module.css'
export interface CollapsiblePanelProps
extends React.PropsWithChildren,
React.HTMLAttributes<HTMLDetailsElement> {
title: string
icon?: IconDefinition
open?: boolean
iconClassNames?: {
bg?: string
icon?: string
}
}
export const PanelHeader = ({
title,
icon,
iconClassNames,
}: CollapsiblePanelProps) => {
return (
<summary className={styles.header}>
<ActionIcon
icon={icon}
bgClassName={
'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
(iconClassNames?.bg || '')
}
iconClassName={
'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
(iconClassNames?.icon || '')
}
/>
{title}
</summary>
)
}
export const CollapsiblePanel = ({
title,
icon,
children,
className,
iconClassNames,
...props
}: CollapsiblePanelProps) => {
return (
<details
{...props}
className={styles.panel + ' group ' + (className || '')}
>
<PanelHeader title={title} icon={icon} iconClassNames={iconClassNames} />
{children}
</details>
)
}

View File

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

View File

@ -0,0 +1,8 @@
export const ErrorPage = () => {
return (
<div className="flex flex-col items-center justify-center h-screen">
<h1 className="text-4xl font-bold">404</h1>
<p className="text-2xl font-bold">Page not found</p>
</div>
)
}

View File

@ -0,0 +1,185 @@
import { v4 as uuidv4 } from 'uuid'
import { useStore } from '../useStore'
import { faXmark } from '@fortawesome/free-solid-svg-icons'
import { ActionButton } from './ActionButton'
import Modal from 'react-modal'
import React from 'react'
import { useFormik } from 'formik'
import { Models } from '@kittycad/lib'
type OutputFormat = Models['OutputFormat_type']
export const ExportButton = () => {
const { engineCommandManager } = useStore((s) => ({
engineCommandManager: s.engineCommandManager,
}))
const [modalIsOpen, setIsOpen] = React.useState(false)
const defaultType = 'gltf'
const [type, setType] = React.useState(defaultType)
const customModalStyles = {
content: {
top: '50%',
left: '50%',
right: 'auto',
bottom: 'auto',
marginRight: '-50%',
transform: 'translate(-50%, -50%)',
},
}
function openModal() {
setIsOpen(true)
}
function closeModal() {
setIsOpen(false)
}
// Default to gltf and embedded.
const initialValues: OutputFormat = {
type: defaultType,
storage: 'embedded',
}
const formik = useFormik({
initialValues,
onSubmit: (values: OutputFormat) => {
// Set the default coords.
if (
values.type === 'obj' ||
values.type === 'ply' ||
values.type === 'step' ||
values.type === 'stl'
) {
// Set the default coords.
// In the future we can make this configurable.
// But for now, its probably best to keep it consistent with the
// UI.
values.coords = {
forward: {
axis: 'y',
direction: 'negative',
},
up: {
axis: 'z',
direction: 'positive',
},
}
}
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'export',
// By default let's leave this blank to export the whole scene.
// In the future we might want to let the user choose which entities
// in the scene to export. In that case, you'd pass the IDs thru here.
entity_ids: [],
format: values,
},
cmd_id: uuidv4(),
file_id: uuidv4(),
})
closeModal()
},
})
return (
<>
<button onClick={openModal}>Export</button>
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
contentLabel="Export"
style={customModalStyles}
>
<div className="text-black">
<h1 className="text-2xl font-bold">Export your design</h1>
<form onSubmit={formik.handleSubmit}>
<p>
<label htmlFor="type">Type</label>
</p>
<p>
<select
id="type"
name="type"
onChange={(e) => {
setType(e.target.value)
formik.handleChange(e)
}}
>
<option value="gltf">gltf</option>
<option value="obj">obj</option>
<option value="ply">ply</option>
<option value="step">step</option>
<option value="stl">stl</option>
</select>
</p>
{(type === 'gltf' || type === 'ply' || type === 'stl') && (
<>
<p>
{' '}
<label htmlFor="storage">Storage</label>
</p>
<p>
<select
id="storage"
name="storage"
onChange={formik.handleChange}
value={formik.values.storage}
>
{type === 'gltf' && (
<>
<option value="embedded">embedded</option>
<option value="binary">binary</option>
<option value="standard">standard</option>
</>
)}
{type === 'ply' && (
<>
<option value="ascii">ascii</option>
<option value="binary">binary</option>
</>
)}
{type === 'stl' && (
<>
<option value="ascii">ascii</option>
<option value="binary_little_endian">
binary_little_endian
</option>
<option value="binary_big_endian">
binary_big_endian
</option>
</>
)}
</select>
</p>
</>
)}
<div className="flex justify-between mt-6">
<button type="submit">Submit</button>
</div>
</form>
<div className="flex justify-between mt-6">
<ActionButton
onClick={closeModal}
icon={{
icon: faXmark,
bgClassName: 'bg-destroy-80',
iconClassName:
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
}}
className="hover:border-destroy-40"
>
Close
</ActionButton>
</div>
</div>
</Modal>
</>
)
}

View File

@ -0,0 +1,41 @@
import { useEffect, useState } from 'react'
const Loading = ({ children }: React.PropsWithChildren) => {
const [hasLongLoadTime, setHasLongLoadTime] = useState(false)
useEffect(() => {
const timer = setTimeout(() => {
setHasLongLoadTime(true)
}, 4000)
return () => clearTimeout(timer)
}, [setHasLongLoadTime])
return (
<div className="body-bg flex flex-col items-center justify-center h-screen">
<svg viewBox="0 0 10 10" className="w-8 h-8">
<circle cx="5" cy="5" r="4" stroke="var(--liquid-20)" fill="none" />
<circle
cx="5"
cy="5"
r="4"
stroke="var(--liquid-10)"
fill="none"
strokeDasharray="4, 4"
className="animate-spin origin-center"
/>
</svg>
<p className="text-base mt-4 text-liquid-80 dark:text-liquid-20">
{children || 'Loading'}
</p>
<p
className={
'text-sm mt-4 text-liquid-90 dark:text-liquid-10 transition-opacity duration-500' +
(hasLongLoadTime ? ' opacity-100' : ' opacity-0')
}
>
Loading is taking longer than expected.
</p>
</div>
)
}
export default Loading

View File

@ -1,14 +1,17 @@
import ReactJson from 'react-json-view'
import { useEffect } from 'react'
import { useStore } from '../useStore'
import { PanelHeader } from './PanelHeader'
import { Themes, useStore } from '../useStore'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
const ReactJsonTypeHack = ReactJson as any
export const Logs = () => {
const { logs, resetLogs } = useStore(({ logs, resetLogs }) => ({
interface LogPanelProps extends CollapsiblePanelProps {
theme?: Exclude<Themes, Themes.System>
}
export const Logs = ({ theme = Themes.Light, ...props }: LogPanelProps) => {
const { logs } = useStore(({ logs }) => ({
logs,
resetLogs,
}))
useEffect(() => {
const element = document.querySelector('.console-tile')
@ -17,10 +20,9 @@ export const Logs = () => {
}
}, [logs])
return (
<div>
<PanelHeader title="Logs" />
<div className="h-full relative">
<div className="absolute inset-0 flex flex-col items-start">
<CollapsiblePanel {...props}>
<div className="relative w-full">
<div className="absolute inset-0 flex flex-col">
<ReactJsonTypeHack
src={logs}
collapsed={1}
@ -32,9 +34,46 @@ export const Logs = () => {
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
</div>
</CollapsiblePanel>
)
}
export const KCLErrors = ({
theme = Themes.Light,
...props
}: LogPanelProps) => {
const { kclErrors } = useStore(({ kclErrors }) => ({
kclErrors,
}))
useEffect(() => {
const element = document.querySelector('.console-tile')
if (element) {
element.scrollTop = element.scrollHeight - element.clientHeight
}
}, [kclErrors])
return (
<CollapsiblePanel {...props}>
<div className="h-full relative">
<div className="absolute inset-0 flex flex-col">
<ReactJsonTypeHack
src={kclErrors}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}
displayArrayKey={false}
displayDataTypes={false}
displayObjectSize={true}
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
</CollapsiblePanel>
)
}

View File

@ -1,13 +1,14 @@
import { processMemory } from './MemoryPanel'
import { lexer } from '../lang/tokeniser'
import { abstractSyntaxTree } from '../lang/abstractSyntaxTree'
import { executor } from '../lang/executor'
import { enginelessExecutor } from '../lib/testHelpers'
import { initPromise } from '../lang/rust'
beforeAll(() => initPromise)
describe('processMemory', () => {
it('should grab the values and remove and geo data', () => {
it('should grab the values and remove and geo data', async () => {
// Enable rotations #152
const code = `
const myVar = 5
const myFn = (a) => {
@ -24,11 +25,11 @@ describe('processMemory', () => {
|> lineTo([-3.35, 0.17], %)
|> lineTo([0.98, 5.16], %)
|> lineTo([2.15, 4.32], %)
|> rx(90, %)
// |> rx(90, %)
show(theExtrude, theSketch)`
const tokens = lexer(code)
const ast = abstractSyntaxTree(tokens)
const programMemory = executor(ast, {
const programMemory = await enginelessExecutor(ast, {
root: {
log: {
type: 'userVal',
@ -38,7 +39,7 @@ describe('processMemory', () => {
__meta: [],
},
},
_sketch: [],
pendingMemory: {},
})
const output = processMemory(programMemory)
expect(output.myVar).toEqual(5)
@ -48,24 +49,7 @@ describe('processMemory', () => {
myVar: 5,
myFn: '__function__',
otherVar: 3,
theExtrude: [
{
type: 'extrudePlane',
position: [-1.2, 2.5, 0],
rotation: [
0.5984837231672995, -0.3765862890544571, 0.3765862890544572,
0.5984837231672996,
],
},
{
type: 'extrudePlane',
position: [-1.58, 4, 0],
rotation: [
0.3024567786448806, 0.6391556125481195, -0.6391556125481194,
0.30245677864488063,
],
},
],
theExtrude: [],
theSketch: [
{ type: 'toPoint', to: [-3.35, 0.17], from: [0, 0] },
{ type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17] },

View File

@ -1,10 +1,17 @@
import ReactJson from 'react-json-view'
import { PanelHeader } from './PanelHeader'
import { useStore } from '../useStore'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { Themes, useStore } from '../useStore'
import { useMemo } from 'react'
import { ProgramMemory } from '../lang/executor'
export const MemoryPanel = () => {
interface MemoryPanelProps extends CollapsiblePanelProps {
theme?: Exclude<Themes, Themes.System>
}
export const MemoryPanel = ({
theme = Themes.Light,
...props
}: MemoryPanelProps) => {
const { programMemory } = useStore((s) => ({
programMemory: s.programMemory,
}))
@ -13,11 +20,10 @@ export const MemoryPanel = () => {
[programMemory]
)
return (
<div className="h-full">
<PanelHeader title="Variables" />
<CollapsiblePanel {...props}>
<div className="h-full relative">
<div className="absolute inset-0 flex flex-col items-start">
<div className=" overflow-auto h-full console-tile w-full">
<div className=" h-full console-tile w-full">
<ReactJson
src={ProcessedMemory}
collapsed={1}
@ -28,11 +34,12 @@ export const MemoryPanel = () => {
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
</div>
</div>
</CollapsiblePanel>
)
}

View File

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

View File

@ -1,7 +0,0 @@
export const PanelHeader = ({ title }: { title: string }) => {
return (
<div className="font-mono text-[11px] bg-stone-100 w-full pl-4 h-[20px] text-stone-700 flex items-center">
<span className="pt-1">{title}</span>
</div>
)
}

View File

@ -1,646 +0,0 @@
import { useRef, useState, useEffect, useMemo } from 'react'
import {
CallExpression,
ArrayExpression,
PipeExpression,
} from '../lang/abstractSyntaxTree'
import {
getNodePathFromSourceRange,
getNodeFromPath,
getNodeFromPathCurry,
} from '../lang/queryAst'
import { changeSketchArguments } from '../lang/std/sketch'
import {
ExtrudeGroup,
ExtrudeSurface,
SketchGroup,
Path,
Rotation,
Position,
PathToNode,
SourceRange,
} from '../lang/executor'
import { BufferGeometry } from 'three'
import { useStore } from '../useStore'
import { isOverlap, roundOff } from '../lib/utils'
import { Vector3, DoubleSide, Quaternion } from 'three'
import { useSetCursor } from '../hooks/useSetCursor'
import { getConstraintLevelFromSourceRange } from '../lang/std/sketchcombos'
import { createCallExpression, createPipeSubstitution } from '../lang/modifyAst'
function LineEnd({
geo,
sourceRange,
editorCursor,
rotation,
position,
from,
}: {
geo: BufferGeometry
sourceRange: [number, number]
editorCursor: boolean
rotation: Rotation
position: Position
from: [number, number]
}) {
const ref = useRef<BufferGeometry | undefined>() as any
const detectionPlaneRef = useRef<BufferGeometry | undefined>() as any
const lastPointerRef = useRef<Vector3>(new Vector3())
const point2DRef = useRef<Vector3>(new Vector3())
const [hovered, setHover] = useState(false)
const [isMouseDown, setIsMouseDown] = useState(false)
const baseColor = useConstraintColors(sourceRange)
const setCursor = useSetCursor(sourceRange, 'line-end')
const { setHighlightRange, guiMode, ast, updateAst, programMemory } =
useStore((s) => ({
setHighlightRange: s.setHighlightRange,
guiMode: s.guiMode,
ast: s.ast,
updateAst: s.updateAst,
programMemory: s.programMemory,
}))
const { originalXY } = useMemo(() => {
if (ast) {
const thePath = getNodePathFromSourceRange(ast, sourceRange)
const { node: callExpression } = getNodeFromPath<CallExpression>(
ast,
thePath
)
const [xArg, yArg] =
guiMode.mode === 'sketch'
? callExpression?.arguments || []
: (callExpression?.arguments?.[0] as ArrayExpression)?.elements || []
const x = xArg?.type === 'Literal' ? xArg.value : -1
const y = yArg?.type === 'Literal' ? yArg.value : -1
return {
originalXY: [x, y],
}
}
return {
originalXY: [-1, -1],
}
}, [ast])
useEffect(() => {
const handleMouseUp = () => {
if (isMouseDown && ast) {
const current2d = point2DRef.current.clone()
const inverseQuaternion = new Quaternion()
if (
guiMode.mode === 'canEditSketch' ||
(guiMode.mode === 'sketch' && guiMode.sketchMode === 'sketchEdit')
) {
inverseQuaternion.set(...guiMode.rotation)
inverseQuaternion.invert()
}
current2d.sub(
new Vector3(...position).applyQuaternion(inverseQuaternion)
)
let [x, y] = [roundOff(current2d.x, 2), roundOff(current2d.y, 2)]
let theNewPoints: [number, number] = [x, y]
const { modifiedAst } = changeSketchArguments(
ast,
programMemory,
sourceRange,
theNewPoints,
guiMode,
from
)
if (!(current2d.x === 0 && current2d.y === 0 && current2d.z === 0))
updateAst(modifiedAst)
ref.current.position.set(...position)
}
setIsMouseDown(false)
}
window.addEventListener('mouseup', handleMouseUp)
return () => {
window.removeEventListener('mouseup', handleMouseUp)
}
}, [isMouseDown])
const inEditMode =
guiMode.mode === 'canEditSketch' ||
(guiMode.mode === 'sketch' && guiMode.sketchMode === 'sketchEdit')
let clickDetectPlaneQuaternion = new Quaternion()
if (inEditMode) {
clickDetectPlaneQuaternion = new Quaternion(...rotation)
}
return (
<>
<mesh
position={position}
quaternion={rotation}
ref={ref}
onPointerOver={(event) => {
inEditMode && setHover(true)
setHighlightRange(sourceRange)
}}
onPointerOut={(event) => {
setHover(false)
setHighlightRange([0, 0])
}}
onPointerDown={() => {
inEditMode && setIsMouseDown(true)
setCursor()
}}
>
<primitive object={geo} scale={hovered ? 2 : 1} />
<meshStandardMaterial
color={hovered ? 'hotpink' : editorCursor ? 'skyblue' : baseColor}
/>
</mesh>
{isMouseDown && (
<mesh
quaternion={clickDetectPlaneQuaternion}
onPointerMove={(a) => {
const point = a.point
const transformedPoint = point.clone()
const inverseQuaternion = new Quaternion()
if (
guiMode.mode === 'canEditSketch' ||
(guiMode.mode === 'sketch' && guiMode.sketchMode === 'sketchEdit')
) {
inverseQuaternion.set(...guiMode.rotation)
inverseQuaternion.invert()
}
transformedPoint.applyQuaternion(inverseQuaternion)
point2DRef.current.copy(transformedPoint)
if (
lastPointerRef.current.x === 0 &&
lastPointerRef.current.y === 0 &&
lastPointerRef.current.z === 0
) {
lastPointerRef.current.set(point.x, point.y, point.z)
return
}
if (guiMode.mode)
if (ref.current) {
const diff = new Vector3().subVectors(
point.clone().applyQuaternion(inverseQuaternion),
lastPointerRef.current
.clone()
.applyQuaternion(inverseQuaternion)
)
if (originalXY[0] === -1) {
// x arg is not a literal and should be locked
diff.x = 0
}
if (originalXY[1] === -1) {
// y arg is not a literal and should be locked
diff.y = 0
}
ref.current.position.add(
diff.applyQuaternion(inverseQuaternion.invert())
)
lastPointerRef.current.copy(point.clone())
}
}}
>
<planeGeometry args={[50, 50]} ref={detectionPlaneRef} />
<meshStandardMaterial
side={DoubleSide}
color="blue"
transparent
opacity={0}
/>
</mesh>
)}
</>
)
}
export function RenderViewerArtifacts({
artifacts,
}: {
artifacts: (ExtrudeGroup | SketchGroup)[]
}) {
useSetAppModeFromCursorLocation(artifacts)
return (
<>
{artifacts?.map((artifact, i) => (
<RenderViewerArtifact key={i} artifact={artifact} />
))}
</>
)
}
function RenderViewerArtifact({
artifact,
}: {
artifact: ExtrudeGroup | SketchGroup
}) {
// const { selectionRange, guiMode, ast, setGuiMode } = useStore(
// ({ selectionRange, guiMode, ast, setGuiMode }) => ({
// selectionRange,
// guiMode,
// ast,
// setGuiMode,
// })
// )
// const [editorCursor, setEditorCursor] = useState(false)
// useEffect(() => {
// const shouldHighlight = isOverlapping(
// artifact.__meta.slice(-1)[0].sourceRange,
// selectionRange
// )
// setEditorCursor(shouldHighlight)
// }, [selectionRange, artifact.__meta])
if (artifact.type === 'sketchGroup') {
return (
<>
{artifact.start && (
<PathRender
geoInfo={artifact.start}
forceHighlight={false}
rotation={artifact.rotation}
position={artifact.position}
/>
)}
{artifact.value.map((geoInfo, key) => (
<PathRender
geoInfo={geoInfo}
key={key}
forceHighlight={false}
rotation={artifact.rotation}
position={artifact.position}
/>
))}
</>
)
}
if (artifact.type === 'extrudeGroup') {
return (
<>
{artifact.value.map((geoInfo, key) => (
<WallRender
geoInfo={geoInfo}
key={key}
forceHighlight={false}
rotation={artifact.rotation}
position={artifact.position}
/>
))}
</>
)
}
return null
}
function WallRender({
geoInfo,
forceHighlight = false,
rotation,
position,
}: {
geoInfo: ExtrudeSurface
forceHighlight?: boolean
rotation: Rotation
position: Position
}) {
const { setHighlightRange, selectionRanges } = useStore(
({ setHighlightRange, selectionRanges }) => ({
setHighlightRange,
selectionRanges,
})
)
const onClick = useSetCursor(geoInfo.__geoMeta.sourceRange)
// This reference will give us direct access to the mesh
const ref = useRef<BufferGeometry | undefined>() as any
const [hovered, setHover] = useState(false)
const [editorCursor, setEditorCursor] = useState(false)
useEffect(() => {
const shouldHighlight = selectionRanges.codeBasedSelections.some(
({ range }) => isOverlap(geoInfo.__geoMeta.sourceRange, range)
)
setEditorCursor(shouldHighlight)
}, [selectionRanges, geoInfo])
return (
<>
<mesh
quaternion={rotation}
position={position}
ref={ref}
onPointerOver={(event) => {
setHover(true)
setHighlightRange(geoInfo.__geoMeta.sourceRange)
}}
onPointerOut={(event) => {
setHover(false)
setHighlightRange([0, 0])
}}
onClick={onClick}
>
<primitive object={geoInfo.__geoMeta.geo} />
<meshStandardMaterial
side={DoubleSide}
color={
hovered
? 'hotpink'
: forceHighlight || editorCursor
? 'skyblue'
: 'orange'
}
/>
</mesh>
</>
)
}
function PathRender({
geoInfo,
forceHighlight = false,
rotation,
position,
}: {
geoInfo: Path
forceHighlight?: boolean
rotation: Rotation
position: Position
}) {
const { selectionRanges, updateAstAsync, ast, guiMode } = useStore((s) => ({
selectionRanges: s.selectionRanges,
updateAstAsync: s.updateAstAsync,
ast: s.ast,
guiMode: s.guiMode,
}))
const [editorCursor, setEditorCursor] = useState(false)
const [editorLineCursor, setEditorLineCursor] = useState(false)
useEffect(() => {
const shouldHighlight = selectionRanges.codeBasedSelections.some(
({ range }) => isOverlap(geoInfo.__geoMeta.sourceRange, range)
)
const shouldHighlightLine = selectionRanges.codeBasedSelections.some(
({ range, type }) =>
isOverlap(geoInfo.__geoMeta.sourceRange, range) && type === 'default'
)
setEditorCursor(shouldHighlight)
setEditorLineCursor(shouldHighlightLine)
}, [selectionRanges, geoInfo])
return (
<>
{geoInfo.__geoMeta.geos.map((meta, i) => {
if (meta.type === 'line')
return (
<LineRender
key={i}
geo={meta.geo}
sourceRange={geoInfo.__geoMeta.sourceRange}
forceHighlight={editorLineCursor}
rotation={rotation}
position={position}
/>
)
if (meta.type === 'lineEnd')
return (
<LineEnd
key={i}
geo={meta.geo}
from={geoInfo.from}
sourceRange={geoInfo.__geoMeta.sourceRange}
editorCursor={forceHighlight || editorCursor}
rotation={rotation}
position={position}
/>
)
if (meta.type === 'sketchBase')
return (
<LineRender
key={i}
geo={meta.geo}
sourceRange={geoInfo.__geoMeta.sourceRange}
forceHighlight={forceHighlight || editorLineCursor}
rotation={rotation}
position={position}
onClick={() => {
if (
!ast ||
!(guiMode.mode === 'sketch' && guiMode.sketchMode === 'line')
)
return
const path = getNodePathFromSourceRange(
ast,
geoInfo.__geoMeta.sourceRange
)
const getNode = getNodeFromPathCurry(ast, path)
const maybeStartSketchAt =
getNode<CallExpression>('CallExpression')
const pipe = getNode<PipeExpression>('PipeExpression')
if (
maybeStartSketchAt?.node.callee.name === 'startSketchAt' &&
pipe.node &&
pipe.node.body.length > 2
) {
const modifiedAst = JSON.parse(JSON.stringify(ast))
const _pipe = getNodeFromPath<PipeExpression>(
modifiedAst,
path,
'PipeExpression'
)
_pipe.node.body.push(
createCallExpression('close', [createPipeSubstitution()])
)
updateAstAsync(modifiedAst)
}
}}
/>
)
})}
</>
)
}
function LineRender({
geo,
sourceRange,
forceHighlight = false,
rotation,
position,
onClick: _onClick = () => {},
}: {
geo: BufferGeometry
sourceRange: [number, number]
forceHighlight?: boolean
rotation: Rotation
position: Position
onClick?: () => void
}) {
const { setHighlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
}))
const onClick = useSetCursor(sourceRange)
// This reference will give us direct access to the mesh
const ref = useRef<BufferGeometry | undefined>() as any
const [hovered, setHover] = useState(false)
const baseColor = useConstraintColors(sourceRange)
return (
<>
<mesh
quaternion={rotation}
position={position}
ref={ref}
onPointerOver={(e) => {
setHover(true)
setHighlightRange(sourceRange)
}}
onPointerOut={(e) => {
setHover(false)
setHighlightRange([0, 0])
}}
onClick={() => {
_onClick()
onClick()
}}
>
<primitive object={geo} />
<meshStandardMaterial
color={hovered ? 'hotpink' : forceHighlight ? 'skyblue' : baseColor}
/>
</mesh>
</>
)
}
type Artifact = ExtrudeGroup | SketchGroup
function useSetAppModeFromCursorLocation(artifacts: Artifact[]) {
const { selectionRanges, guiMode, setGuiMode, ast } = useStore(
({ selectionRanges, guiMode, setGuiMode, ast }) => ({
selectionRanges,
guiMode,
setGuiMode,
ast,
})
)
useEffect(() => {
const artifactsWithinCursorRange: (
| {
parentType: Artifact['type']
isParent: true
pathToNode: PathToNode
sourceRange: SourceRange
rotation: Rotation
position: Position
}
| {
parentType: Artifact['type']
isParent: false
pathToNode: PathToNode
sourceRange: SourceRange
rotation: Rotation
position: Position
}
)[] = []
artifacts?.forEach((artifact) => {
artifact.value.forEach((geo) => {
if (
isOverlap(
geo.__geoMeta.sourceRange,
selectionRanges.codeBasedSelections[0].range
)
) {
artifactsWithinCursorRange.push({
parentType: artifact.type,
isParent: false,
pathToNode: geo.__geoMeta.pathToNode,
sourceRange: geo.__geoMeta.sourceRange,
rotation: artifact.rotation,
position: artifact.position,
})
}
})
artifact.__meta.forEach((meta) => {
if (
isOverlap(
meta.sourceRange,
selectionRanges.codeBasedSelections[0].range
)
) {
artifactsWithinCursorRange.push({
parentType: artifact.type,
isParent: true,
pathToNode: meta.pathToNode,
sourceRange: meta.sourceRange,
rotation: artifact.rotation,
position: artifact.position,
})
}
})
})
const parentArtifacts = artifactsWithinCursorRange.filter((a) => a.isParent)
const hasSketchArtifact = artifactsWithinCursorRange.filter(
({ parentType }) => parentType === 'sketchGroup'
)
const hasExtrudeArtifact = artifactsWithinCursorRange.filter(
({ parentType }) => parentType === 'extrudeGroup'
)
const artifact = parentArtifacts[0]
const shouldHighlight = !!artifact || hasSketchArtifact.length
if (
(guiMode.mode === 'default' || guiMode.mode === 'canEditSketch') &&
ast &&
hasSketchArtifact.length
) {
const pathToNode = getNodePathFromSourceRange(
ast,
hasSketchArtifact[0].sourceRange
)
const { rotation, position } = hasSketchArtifact[0]
setGuiMode({ mode: 'canEditSketch', pathToNode, rotation, position })
} else if (
hasExtrudeArtifact.length &&
(guiMode.mode === 'default' || guiMode.mode === 'canEditExtrude') &&
ast
) {
const pathToNode = getNodePathFromSourceRange(
ast,
hasExtrudeArtifact[0].sourceRange
)
const { rotation, position } = hasExtrudeArtifact[0]
setGuiMode({ mode: 'canEditExtrude', pathToNode, rotation, position })
} else if (
!shouldHighlight &&
(guiMode.mode === 'canEditExtrude' || guiMode.mode === 'canEditSketch')
) {
setGuiMode({ mode: 'default' })
}
}, [artifacts, selectionRanges])
}
function useConstraintColors(sourceRange: [number, number]): string {
const { guiMode, ast } = useStore((s) => ({
guiMode: s.guiMode,
ast: s.ast,
}))
const [baseColor, setBaseColor] = useState('orange')
useEffect(() => {
if (!ast || guiMode.mode !== 'sketch') {
setBaseColor('orange')
return
}
try {
const level = getConstraintLevelFromSourceRange(sourceRange, ast)
if (level === 'free') {
setBaseColor('orange')
} else if (level === 'partial') {
setBaseColor('IndianRed')
} else if (level === 'full') {
setBaseColor('lightgreen')
}
} catch (e) {
setBaseColor('orange')
}
}, [guiMode, ast, sourceRange])
return baseColor
}

View File

@ -1,6 +1,6 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'
import { Value } from '../lang/abstractSyntaxTree'
import { Value } from '../lang/abstractSyntaxTreeTypes'
import {
AvailableVars,
addToInputHelper,

View File

@ -1,6 +1,6 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'
import { Value } from '../lang/abstractSyntaxTree'
import { Value } from '../lang/abstractSyntaxTreeTypes'
import {
AvailableVars,
addToInputHelper,

View File

@ -1,181 +0,0 @@
import { useState } from 'react'
import { Selections, useStore } from '../useStore'
import { DoubleSide, Vector3, Quaternion } from 'three'
import { Program } from '../lang/abstractSyntaxTree'
import { addNewSketchLn } from '../lang/std/sketch'
import { roundOff } from '../lib/utils'
export const SketchPlane = () => {
const {
ast,
guiMode,
programMemory,
updateAstAsync,
setSelectionRanges,
selectionRanges,
isShiftDown,
setCursor,
} = useStore((s) => ({
guiMode: s.guiMode,
ast: s.ast,
updateAstAsync: s.updateAstAsync,
programMemory: s.programMemory,
setSelectionRanges: s.setSelectionRanges,
selectionRanges: s.selectionRanges,
isShiftDown: s.isShiftDown,
setCursor: s.setCursor,
}))
const [xHover, setXHover] = useState(false)
const [yHover, setYHover] = useState(false)
if (guiMode.mode !== 'sketch') {
return null
}
if (!(guiMode.sketchMode === 'sketchEdit') && !('isTooltip' in guiMode)) {
return null
}
const sketchGridName = 'sketchGrid'
let clickDetectQuaternion = new Quaternion(...guiMode.rotation)
let temp = new Quaternion().setFromAxisAngle(
new Vector3(1, 0, 0),
Math.PI / 2
)
let position = guiMode.position
const gridQuaternion = new Quaternion().multiplyQuaternions(
new Quaternion(...guiMode.rotation),
temp
)
const onAxisClick = (name: 'y-axis' | 'x-axis') => () => {
const _selectionRanges: Selections = isShiftDown
? selectionRanges
: {
codeBasedSelections: [
{
range: [0, 0],
type: 'default',
},
],
otherSelections: [],
}
if (!isShiftDown) {
setCursor({
..._selectionRanges,
otherSelections: [name],
})
}
setTimeout(() => {
setSelectionRanges({
..._selectionRanges,
otherSelections: [name],
})
}, 100)
}
return (
<>
<mesh
quaternion={clickDetectQuaternion}
position={position}
name={sketchGridName}
onPointerDown={(e) => {
if (!('isTooltip' in guiMode)) {
return
}
const sketchGridIntersection = e.intersections.find(
({ object }) => object.name === sketchGridName
)
const inverseQuaternion = clickDetectQuaternion.clone().invert()
let transformedPoint = sketchGridIntersection?.point.clone()
if (transformedPoint) {
transformedPoint.applyQuaternion(inverseQuaternion)
transformedPoint?.sub(
new Vector3(...position).applyQuaternion(inverseQuaternion)
)
}
const point = roundy(transformedPoint)
let _ast: Program = ast
? ast
: {
type: 'Program',
start: 0,
end: 0,
body: [],
nonCodeMeta: {},
}
const { modifiedAst } = addNewSketchLn({
node: _ast,
programMemory,
to: [point.x, point.y],
fnName: guiMode.sketchMode,
pathToNode: guiMode.pathToNode,
})
updateAstAsync(modifiedAst)
}}
>
<planeGeometry args={[30, 40]} />
<meshStandardMaterial
color="blue"
side={DoubleSide}
opacity={0}
transparent
/>
</mesh>
<gridHelper
args={[50, 50, 'blue', 'hotpink']}
quaternion={gridQuaternion}
position={position}
onClick={() =>
!isShiftDown &&
setSelectionRanges({
...selectionRanges,
otherSelections: [],
})
}
/>
<mesh
onPointerOver={() => setXHover(true)}
onPointerOut={() => setXHover(false)}
onClick={onAxisClick('x-axis')}
>
<boxGeometry args={[50, 0.2, 0.05]} />
<meshStandardMaterial
color={
selectionRanges.otherSelections.includes('x-axis')
? 'skyblue'
: xHover
? '#FF5555'
: '#FF1111'
}
/>
</mesh>
<mesh
onPointerOver={() => setYHover(true)}
onPointerOut={() => setYHover(false)}
onClick={onAxisClick('y-axis')}
>
<boxGeometry args={[0.2, 50, 0.05]} />
<meshStandardMaterial
color={
selectionRanges.otherSelections.includes('y-axis')
? 'skyblue'
: yHover
? '#5555FF'
: '#1111FF'
}
/>
</mesh>
</>
)
}
function roundy({ x, y, z }: any) {
return {
x: roundOff(x, 2),
y: roundOff(y, 2),
z: roundOff(z, 2),
}
}

View File

@ -1,8 +1,43 @@
import { useEffect, useRef } from 'react'
import { PanelHeader } from '../components/PanelHeader'
import {
MouseEventHandler,
WheelEventHandler,
useEffect,
useRef,
useState,
} from 'react'
import { v4 as uuidv4 } from 'uuid'
import { useStore } from '../useStore'
import { throttle } from '../lib/utils'
import { EngineCommand } from '../lang/std/engineConnection'
import { getNormalisedCoordinates } from '../lib/utils'
import Loading from './Loading'
export const Stream = () => {
export const Stream = ({ className = '' }) => {
const [isLoading, setIsLoading] = useState(true)
const [zoom, setZoom] = useState(0)
const videoRef = useRef<HTMLVideoElement>(null)
const {
mediaStream,
engineCommandManager,
setIsMouseDownInStream,
fileId,
setFileId,
setCmdId,
didDragInStream,
setDidDragInStream,
streamDimensions,
} = useStore((s) => ({
mediaStream: s.mediaStream,
engineCommandManager: s.engineCommandManager,
isMouseDownInStream: s.isMouseDownInStream,
setIsMouseDownInStream: s.setIsMouseDownInStream,
fileId: s.fileId,
setFileId: s.setFileId,
setCmdId: s.setCmdId,
didDragInStream: s.didDragInStream,
setDidDragInStream: s.setDidDragInStream,
streamDimensions: s.streamDimensions,
}))
useEffect(() => {
if (
@ -10,127 +45,130 @@ export const Stream = () => {
typeof RTCPeerConnection === 'undefined'
)
return
const url = 'wss://dev.api.kittycad.io/ws/modeling/commands'
const [pc, socket] = [new RTCPeerConnection(), new WebSocket(url)]
// Connection opened
socket.addEventListener('open', (event) => {
console.log('Connected to websocket, waiting for ICE servers')
if (!videoRef.current) return
if (!mediaStream) return
videoRef.current.srcObject = mediaStream
setFileId(uuidv4())
setZoom(videoRef.current.getBoundingClientRect().height / 2)
}, [mediaStream, engineCommandManager, setFileId])
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = ({
clientX,
clientY,
ctrlKey,
}) => {
if (!videoRef.current) return
const { x, y } = getNormalisedCoordinates({
clientX,
clientY,
el: videoRef.current,
...streamDimensions,
})
console.log('click', x, y)
const newId = uuidv4()
setCmdId(newId)
const interaction = ctrlKey ? 'pan' : 'rotate'
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_start',
interaction,
window: { x, y },
},
cmd_id: newId,
file_id: fileId,
})
socket.addEventListener('close', (event) => {
console.log('websocket connection closed')
setIsMouseDownInStream(true)
}
// TODO: consolidate this with the same function in App.tsx
const debounceSocketSend = throttle<EngineCommand>((message) => {
engineCommandManager?.sendSceneCommand(message)
}, 16)
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
e.preventDefault()
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_move',
interaction: 'zoom',
window: { x: 0, y: zoom + e.deltaY },
},
cmd_id: uuidv4(),
file_id: uuidv4(),
})
socket.addEventListener('error', (event) => {
console.log('websocket connection error')
setZoom(zoom + e.deltaY)
}
const handleMouseUp: MouseEventHandler<HTMLVideoElement> = ({
clientX,
clientY,
ctrlKey,
}) => {
if (!videoRef.current) return
const { x, y } = getNormalisedCoordinates({
clientX,
clientY,
el: videoRef.current,
...streamDimensions,
})
// Listen for messages
socket.addEventListener('message', (event) => {
//console.log('Message from server ', event.data);
if (event.data instanceof Blob) {
const reader = new FileReader()
const newCmdId = uuidv4()
const interaction = ctrlKey ? 'pan' : 'rotate'
reader.onload = () => {
//console.log("Result: " + reader.result);
}
reader.readAsText(event.data)
} else {
const message = JSON.parse(event.data)
if (message.type === 'SDPAnswer') {
pc.setRemoteDescription(new RTCSessionDescription(message.answer))
} else if (message.type === 'IceServerInfo') {
console.log('received IceServerInfo')
pc.setConfiguration({
iceServers: message.ice_servers,
})
pc.ontrack = function (event) {
if (videoRef.current) {
videoRef.current.srcObject = event.streams[0]
videoRef.current.autoplay = true
videoRef.current.controls = false
}
}
pc.oniceconnectionstatechange = (e) =>
console.log(pc.iceConnectionState)
pc.onicecandidate = (event) => {
if (event.candidate === null) {
console.log('sent SDPOffer')
socket.send(
JSON.stringify({
type: 'SDPOffer',
offer: pc.localDescription,
})
)
}
}
// Offer to receive 1 video track
pc.addTransceiver('video', {
direction: 'sendrecv',
})
pc.createOffer()
.then((d) => pc.setLocalDescription(d))
.catch(console.log)
}
}
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_end',
interaction,
window: { x, y },
},
cmd_id: newCmdId,
file_id: fileId,
})
const debounceSocketSend = throttle((message) => {
socket.send(JSON.stringify(message))
}, 100)
const handleMouseMove = ({ clientX, clientY }: MouseEvent) => {
if (!videoRef.current) return
const { left, top } = videoRef.current.getBoundingClientRect()
const x = clientX - left
const y = clientY - top
debounceSocketSend({ type: 'MouseMove', x: x, y: y })
setIsMouseDownInStream(false)
if (!didDragInStream) {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'select_with_point',
selection_type: 'add',
selected_at_window: { x, y },
},
cmd_id: uuidv4(),
file_id: fileId,
})
}
if (videoRef.current) {
videoRef.current.addEventListener('mousemove', handleMouseMove)
}
return () => {
socket.close()
pc.close()
if (!videoRef.current) return
videoRef.current.removeEventListener('mousemove', handleMouseMove)
}
}, [])
setDidDragInStream(false)
}
return (
<div>
<PanelHeader title="Stream" />
<video ref={videoRef} />
<div id="stream" className={className}>
<video
ref={videoRef}
muted
autoPlay
controls={false}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onContextMenu={(e) => e.preventDefault()}
onContextMenuCapture={(e) => e.preventDefault()}
onWheelCapture={handleScroll}
onPlay={() => setIsLoading(false)}
className="w-full h-full"
/>
{isLoading && (
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<Loading>Loading stream...</Loading>
</div>
)}
</div>
)
}
function throttle(
func: (...args: any[]) => any,
wait: number
): (...args: any[]) => any {
let timeout: ReturnType<typeof setTimeout> | null
let latestArgs: any[]
let latestTimestamp: number
function later() {
timeout = null
func(...latestArgs)
}
function throttled(...args: any[]) {
const currentTimestamp = Date.now()
latestArgs = args
if (!latestTimestamp || currentTimestamp - latestTimestamp >= wait) {
latestTimestamp = currentTimestamp
func(...latestArgs)
} else if (!timeout) {
timeout = setTimeout(later, wait - (currentTimestamp - latestTimestamp))
}
}
return throttled
}

View File

@ -0,0 +1,40 @@
.toggle {
@apply flex items-center gap-2 w-fit;
--toggle-size: 1.25rem;
--padding: 0.25rem;
}
.toggle:focus-within > span {
@apply outline-none ring-2;
}
.toggle input {
@apply sr-only;
}
.toggle > span {
@apply relative rounded border border-chalkboard-110;
width: calc(2 * (var(--toggle-size) + var(--padding)));
height: calc(var(--toggle-size) + var(--padding));
}
:global(.dark) .toggle > span {
@apply border-chalkboard-40;
}
.toggle > span::after {
content: '';
@apply absolute w-4 h-4 rounded-sm bg-chalkboard-110;
top: 50%;
left: 50%;
translate: calc(-100% - var(--padding)) -50%;
transition: translate 0.08s ease-out;
}
:global(.dark) .toggle > span::after {
@apply bg-chalkboard-10;
}
.toggle input:checked + span::after {
translate: calc(50% - var(--padding)) -50%;
}

View File

@ -0,0 +1,34 @@
import styles from './Toggle.module.css'
interface ToggleProps {
className?: string
offLabel?: string
onLabel?: string
name: string
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
checked: boolean
}
export const Toggle = ({
className = '',
offLabel = 'Off',
onLabel = 'On',
name = '',
onChange,
checked,
}: ToggleProps) => {
return (
<label className={`${styles.toggle} ${className}`}>
{offLabel}
<input
type="checkbox"
name={name}
id={name}
checked={checked}
onChange={onChange}
/>
<span></span>
{onLabel}
</label>
)
}

View File

@ -53,9 +53,6 @@ export const ConvertToVariable = () => {
console.log('e', e)
}
}}
className={`border m-1 px-1 rounded text-xs ${
enableAngLen ? 'bg-gray-50 text-gray-800' : 'bg-gray-200 text-gray-400'
}`}
disabled={!enableAngLen}
>
ConvertToVariable

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { toolTips, useStore } from '../../useStore'
import { Value, VariableDeclarator } from '../../lang/abstractSyntaxTree'
import { Value, VariableDeclarator } from '../../lang/abstractSyntaxTreeTypes'
import {
getNodePathFromSourceRange,
getNodeFromPath,
@ -86,9 +86,6 @@ export const EqualAngle = () => {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}}
className={`border m-1 px-1 rounded text-xs ${
enableEqual ? 'bg-gray-50 text-gray-800' : 'bg-gray-200 text-gray-400'
}`}
disabled={!enableEqual}
title="yo dawg"
>

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { toolTips, useStore } from '../../useStore'
import { Value, VariableDeclarator } from '../../lang/abstractSyntaxTree'
import { Value, VariableDeclarator } from '../../lang/abstractSyntaxTreeTypes'
import {
getNodePathFromSourceRange,
getNodeFromPath,
@ -86,9 +86,6 @@ export const EqualLength = () => {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}}
className={`border m-1 px-1 rounded text-xs ${
enableEqual ? 'bg-gray-50 text-gray-800' : 'bg-gray-200 text-gray-400'
}`}
disabled={!enableEqual}
title="yo dawg"
>

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { toolTips, useStore } from '../../useStore'
import { Value } from '../../lang/abstractSyntaxTree'
import { Value } from '../../lang/abstractSyntaxTreeTypes'
import {
getNodePathFromSourceRange,
getNodeFromPath,
@ -65,9 +65,6 @@ export const HorzVert = ({
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}}
className={`border m-1 px-1 rounded text-xs ${
enableHorz ? 'bg-gray-50 text-gray-800' : 'bg-gray-200 text-gray-400'
}`}
disabled={!enableHorz}
title="yo dawg"
>

View File

@ -5,7 +5,7 @@ import {
BinaryPart,
Value,
VariableDeclarator,
} from '../../lang/abstractSyntaxTree'
} from '../../lang/abstractSyntaxTreeTypes'
import {
getNodePathFromSourceRange,
getNodeFromPath,
@ -58,7 +58,6 @@ export const Intersect = () => {
selectionRanges.codeBasedSelections?.[1]?.type !== 'line-end' &&
previousSegment &&
previousSegment.isParallelAndConstrained
console.log(shouldUsePreviousSegment)
const _forcedSelectionRanges: typeof selectionRanges = {
...selectionRanges,
@ -188,9 +187,6 @@ export const Intersect = () => {
})
}
}}
className={`border m-1 px-1 rounded text-xs ${
enable ? 'bg-gray-50 text-gray-800' : 'bg-gray-200 text-gray-400'
}`}
disabled={!enable}
>
perpendicularDistance

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { toolTips, useStore } from '../../useStore'
import { Value } from '../../lang/abstractSyntaxTree'
import { Value } from '../../lang/abstractSyntaxTreeTypes'
import {
getNodePathFromSourceRange,
getNodeFromPath,
@ -69,9 +69,6 @@ export const RemoveConstrainingValues = () => {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
})
}}
className={`border m-1 px-1 rounded text-xs ${
enableHorz ? 'bg-gray-50 text-gray-800' : 'bg-gray-200 text-gray-400'
}`}
disabled={!enableHorz}
title="yo dawg"
>

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'
import { create } from 'react-modal-promise'
import { toolTips, useStore } from '../../useStore'
import { Value } from '../../lang/abstractSyntaxTree'
import { Value } from '../../lang/abstractSyntaxTreeTypes'
import {
getNodePathFromSourceRange,
getNodeFromPath,
@ -131,9 +131,6 @@ export const SetAbsDistance = ({
console.log('e', e)
}
}}
className={`border m-1 px-1 rounded text-xs ${
enableAngLen ? 'bg-gray-50 text-gray-800' : 'bg-gray-200 text-gray-400'
}`}
disabled={!enableAngLen}
>
{buttonType}

View File

@ -5,7 +5,7 @@ import {
BinaryPart,
Value,
VariableDeclarator,
} from '../../lang/abstractSyntaxTree'
} from '../../lang/abstractSyntaxTreeTypes'
import {
getNodePathFromSourceRange,
getNodeFromPath,
@ -146,9 +146,6 @@ export const SetAngleBetween = () => {
})
}
}}
className={`border m-1 px-1 rounded text-xs ${
enable ? 'bg-gray-50 text-gray-800' : 'bg-gray-200 text-gray-400'
}`}
disabled={!enable}
>
angleBetween

View File

@ -5,7 +5,7 @@ import {
BinaryPart,
Value,
VariableDeclarator,
} from '../../lang/abstractSyntaxTree'
} from '../../lang/abstractSyntaxTreeTypes'
import {
getNodePathFromSourceRange,
getNodeFromPath,
@ -168,9 +168,6 @@ export const SetHorzVertDistance = ({
})
}
}}
className={`border m-1 px-1 rounded text-xs ${
enable ? 'bg-gray-50 text-gray-800' : 'bg-gray-200 text-gray-400'
}`}
disabled={!enable}
>
{buttonType}

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'
import { create } from 'react-modal-promise'
import { toolTips, useStore } from '../../useStore'
import { Value } from '../../lang/abstractSyntaxTree'
import { Value } from '../../lang/abstractSyntaxTreeTypes'
import {
getNodePathFromSourceRange,
getNodeFromPath,
@ -143,9 +143,6 @@ export const SetAngleLength = ({
console.log('e', e)
}
}}
className={`border m-1 px-1 rounded text-xs ${
enableAngLen ? 'bg-gray-50 text-gray-800' : 'bg-gray-200 text-gray-400'
}`}
disabled={!enableAngLen}
>
{angleOrLength}

View File

@ -0,0 +1,66 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { User } from '../useStore'
import UserSidebarMenu from './UserSidebarMenu'
import { BrowserRouter } from 'react-router-dom'
it("Renders user's name and email if available", () => {
const userWellFormed: User = {
id: '8675309',
name: 'Test User',
email: 'kittycad.sidebar.test@example.com',
image: 'https://placekitten.com/200/200',
created_at: 'yesteryear',
updated_at: 'today',
}
render(
<BrowserRouter>
<UserSidebarMenu user={userWellFormed} />
</BrowserRouter>
)
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
expect(screen.getByTestId('username')).toHaveTextContent(
userWellFormed.name || ''
)
expect(screen.getByTestId('email')).toHaveTextContent(userWellFormed.email)
})
it("Renders just the user's email if no name is available", () => {
const userNoName: User = {
id: '8675309',
email: 'kittycad.sidebar.test@example.com',
image: 'https://placekitten.com/200/200',
created_at: 'yesteryear',
updated_at: 'today',
}
render(
<BrowserRouter>
<UserSidebarMenu user={userNoName} />
</BrowserRouter>
)
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
expect(screen.getByTestId('username')).toHaveTextContent(userNoName.email)
})
it('Renders a menu button if no user avatar is available', () => {
const userNoAvatar: User = {
id: '8675309',
name: 'Test User',
email: 'kittycad.sidebar.test@example.com',
created_at: 'yesteryear',
updated_at: 'today',
}
render(
<BrowserRouter>
<UserSidebarMenu user={userNoAvatar} />
</BrowserRouter>
)
expect(screen.getByTestId('user-sidebar-toggle')).toHaveTextContent('Menu')
})

View File

@ -0,0 +1,133 @@
import { Popover } from '@headlessui/react'
import { User, useStore } from '../useStore'
import { ActionButton } from './ActionButton'
import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
import { useNavigate } from 'react-router-dom'
import { useState } from 'react'
import { paths } from '../Router'
const UserSidebarMenu = ({ user }: { user?: User }) => {
const displayedName = getDisplayName(user)
const [imageLoadFailed, setImageLoadFailed] = useState(false)
const navigate = useNavigate()
const { setToken } = useStore((s) => ({
setToken: s.setToken,
}))
// Fallback logic for displaying user's "name":
// 1. user.name
// 2. user.first_name + ' ' + user.last_name
// 3. user.first_name
// 4. user.email
function getDisplayName(user?: User) {
if (!user) return null
if (user.name) return user.name
if (user.first_name) {
if (user.last_name) return user.first_name + ' ' + user.last_name
return user.first_name
}
return user.email
}
return (
<Popover className="relative">
{user?.image && !imageLoadFailed ? (
<Popover.Button
className="border-0 rounded-full w-fit p-0"
data-testid="user-sidebar-toggle"
>
<div className="rounded-full border border-chalkboard-70/50 hover:border-liquid-50 overflow-hidden">
<img
src={user?.image || ''}
alt={user?.name || ''}
className="h-8 w-8"
referrerPolicy="no-referrer"
onError={() => setImageLoadFailed(true)}
/>
</div>
</Popover.Button>
) : (
<ActionButton
Element={Popover.Button}
icon={{ icon: faBars }}
className="border-transparent"
data-testid="user-sidebar-toggle"
>
Menu
</ActionButton>
)}
<Popover.Overlay className="fixed z-40 inset-0 bg-chalkboard-110/50" />
<Popover.Panel className="fixed inset-0 left-auto z-50 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-liquid-100 shadow-md rounded-l-lg">
{({ close }) => (
<>
{user && (
<div className="flex items-center gap-4 px-4 py-3 bg-liquid-100">
{user.image && !imageLoadFailed && (
<div className="rounded-full shadow-inner overflow-hidden">
<img
src={user.image}
alt={user.name || ''}
className="h-8 w-8"
referrerPolicy="no-referrer"
onError={() => setImageLoadFailed(true)}
/>
</div>
)}
<div>
<p
className="m-0 text-liquid-10 text-mono"
data-testid="username"
>
{displayedName || ''}
</p>
{displayedName !== user.email && (
<p
className="m-0 text-liquid-40 text-xs"
data-testid="email"
>
{user.email}
</p>
)}
</div>
</div>
)}
<div className="p-4 flex flex-col gap-2">
<ActionButton
icon={{ icon: faGear }}
className="border-transparent dark:border-transparent dark:hover:border-liquid-60"
onClick={() => {
// since /settings is a nested route the sidebar doesn't close
// automatically when navigating to it
close()
navigate(paths.SETTINGS)
}}
>
Settings
</ActionButton>
<ActionButton
Element="button"
onClick={() => {
setToken('')
navigate(paths.SIGN_IN)
}}
icon={{
icon: faSignOutAlt,
bgClassName: 'bg-destroy-80',
iconClassName:
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
}}
className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60"
>
Sign out
</ActionButton>
</div>
</>
)}
</Popover.Panel>
</Popover>
)
}
export default UserSidebarMenu

View File

@ -15,7 +15,7 @@ export const lineHighlightField = StateField.define({
for (let e of tr.effects) {
if (e.is(addLineHighlight)) {
lines = Decoration.none
const [from, to] = e.value
const [from, to] = e.value || [0, 0]
if (!(from === to && from === 0)) {
lines = lines.update({ add: [matchDeco.range(from, to)] })
deco.push(matchDeco.range(from, to))

11
src/env.ts Normal file
View File

@ -0,0 +1,11 @@
// all web app environment variables are defined here, jest doesn't like import.meta.env so centralising them here
// allows us to mock them in one place, see src/setupTests.ts, it pulls the variable names and valuse from .env.development
// note the exported variable name must match the env var name for the jest mocks to work
// i.e. const VITE_MY_VAR = import.meta.env.VITE_MY_VAR
// Maybe this file should be generated in a GHA from .env.development?
export const VITE_KC_API_WS_MODELING_URL = import.meta.env
.VITE_KC_API_WS_MODELING_URL
export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
export const TEST = import.meta.env.TEST

View File

@ -0,0 +1,57 @@
import useResizeObserver from '@react-hook/resize-observer'
import { useEffect, useState } from 'react'
interface Rect {
top: number
left: number
height: number
width: number
}
/**
* Takes an element id and uses React refs to create a CSS clip-path rule to apply to a backdrop element
* which excludes the element with the given id, creating a "highlight" effect.
* @param highlightId
*/
export function useBackdropHighlight(target: string): string {
const [clipPath, setClipPath] = useState('')
const [elem, setElem] = useState(document.getElementById(target))
// Build the actual clip path string, cutting out the target element
function buildClipPath({ top, left, height, width }: Rect) {
const windowWidth = window.innerWidth
const windowHeight = window.innerHeight
return `
path(evenodd, "M0 0 l${windowWidth} 0 l0 ${windowHeight} l-${windowWidth} 0 Z \
M${left} ${top} l${width} 0 l0 ${height} l-${width} 0 Z")
`
}
// initial setup of clip path
useEffect(() => {
if (!elem) {
const newElem = document.getElementById(target)
if (newElem === null) {
throw new Error(
`Could not find element with id "${target}" to highlight`
)
}
setElem(document.getElementById(target))
return
}
const { top, left, height, width } = elem.getBoundingClientRect()
setClipPath(buildClipPath({ top, left, height, width }))
}, [elem, target])
// update clip path on resize
useResizeObserver(elem, (entry) => {
const { height, width } = entry.contentRect
// the top and left are relative to the viewport, so we need to get the target's position
const { top, left } = entry.target.getBoundingClientRect()
setClipPath(buildClipPath({ top, left, height, width }))
})
return clipPath
}

View File

@ -1,21 +0,0 @@
import { useStore, Selection, Selections } from '../useStore'
export function useSetCursor(
sourceRange: Selection['range'],
type: Selection['type'] = 'default'
) {
const { setCursor, selectionRanges, isShiftDown } = useStore((s) => ({
setCursor: s.setCursor,
selectionRanges: s.selectionRanges,
isShiftDown: s.isShiftDown,
}))
return () => {
const selections: Selections = {
...selectionRanges,
codeBasedSelections: isShiftDown
? [...selectionRanges.codeBasedSelections, { range: sourceRange, type }]
: [{ range: sourceRange, type }],
}
setCursor(selections)
}
}

67
src/hooks/useTauriBoot.ts Normal file
View File

@ -0,0 +1,67 @@
import { useEffect } from 'react'
import { useStore } from '../useStore'
import { parse } from 'toml'
import {
createDir,
BaseDirectory,
readDir,
readTextFile,
} from '@tauri-apps/api/fs'
export const useTauriBoot = () => {
const { defaultDir, setDefaultDir, setHomeMenuItems } = useStore((s) => ({
defaultDir: s.defaultDir,
setDefaultDir: s.setDefaultDir,
setHomeMenuItems: s.setHomeMenuItems,
}))
useEffect(() => {
const isTauri = (window as any).__TAURI__
if (!isTauri) return
const run = async () => {
if (!defaultDir.base) {
createDir('puffin-projects/example', {
dir: BaseDirectory.Home,
recursive: true,
})
setDefaultDir({
base: BaseDirectory.Home,
dir: 'puffin-projects',
})
} else {
const directoryResult = await readDir(defaultDir.dir, {
dir: defaultDir.base,
recursive: true,
})
const puffinProjects = directoryResult.filter(
(file) =>
!file?.name?.startsWith('.') &&
file?.children?.find((child) => child?.name === 'wax.toml')
)
const tomlFiles = await Promise.all(
puffinProjects.map(async (file) => {
const parsedToml = parse(
await readTextFile(`${file.path}/wax.toml`, {
dir: defaultDir.base,
})
)
const mainPath = parsedToml?.package?.main
const projectName = parsedToml?.package?.name
return {
file,
mainPath,
projectName,
}
})
)
setHomeMenuItems(
tomlFiles.map(({ file, mainPath, projectName }) => ({
name: projectName,
path: mainPath ? `${file.path}/${mainPath}` : file.path,
}))
)
}
}
run()
}, [])
}

View File

@ -1,9 +1,9 @@
@import './colors.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@import '../node_modules/allotment/dist/style.css';
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
@ -11,6 +11,70 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@apply text-chalkboard-110;
overflow: hidden;
scrollbar-width: thin;
scrollbar-color: var(--color-chalkboard-20) var(--color-chalkboard-40);
}
.body-bg {
@apply bg-chalkboard-10;
}
.body-bg.dark,
.dark .body-bg {
@apply bg-chalkboard-100;
}
body.dark {
scrollbar-color: var(--color-chalkboard-70) var(--color-chalkboard-90);
@apply text-chalkboard-10;
}
::-webkit-scrollbar {
@apply w-2 rounded-sm;
@apply bg-chalkboard-20;
}
::-webkit-scrollbar-thumb {
@apply bg-chalkboard-40 rounded-sm;
}
.dark ::-webkit-scrollbar {
@apply bg-chalkboard-90;
}
.dark ::-webkit-scrollbar-thumb {
@apply bg-chalkboard-70;
}
button {
@apply border border-chalkboard-100 m-0.5 px-3 rounded text-xs;
}
.dark button {
@apply border-chalkboard-20 hover:border-chalkboard-10 hover:bg-chalkboard-90;
}
button:disabled {
@apply bg-chalkboard-20 text-chalkboard-60 border-chalkboard-20;
}
.dark button:disabled {
@apply bg-chalkboard-90 text-chalkboard-40 border-chalkboard-70;
}
a {
@apply text-liquid-80 hover:text-liquid-70;
}
.dark a {
@apply text-liquid-20 hover:text-liquid-10;
}
.mono {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
code {
@ -18,6 +82,17 @@ code {
monospace;
}
#code-mirror-override .cm-editor {
@apply bg-transparent;
}
#code-mirror-override .cm-gutters {
@apply bg-chalkboard-10/50;
}
.dark #code-mirror-override .cm-gutters {
@apply bg-chalkboard-110/50;
}
#code-mirror-override .cm-focused .cm-cursor {
width: 0px;
@ -25,5 +100,15 @@ code {
#code-mirror-override .cm-cursor {
display: block;
width: 200px;
background: linear-gradient(to right, rgb(0, 55, 94) 0%, #0084e2ff 2%, #0084e255 5%, transparent 100%);
background: linear-gradient(
to right,
rgb(0, 55, 94) 0%,
#0084e2ff 2%,
#0084e255 5%,
transparent 100%
);
}
.react-json-view {
@apply bg-transparent !important;
}

View File

@ -1,14 +1,36 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import { Auth } from './Auth'
import reportWebVitals from './reportWebVitals'
import { Toaster } from 'react-hot-toast'
import { Themes, useStore } from './useStore'
import { Router } from './Router'
import { HotkeysProvider } from 'react-hotkeys-hook'
import { getSystemTheme } from './lib/getSystemTheme'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
function setThemeClass(state: Partial<{ theme: Themes }>) {
const systemTheme = state.theme === Themes.System && getSystemTheme()
if (state.theme === Themes.Dark || systemTheme === Themes.Dark) {
document.body.classList.add('dark')
} else {
document.body.classList.remove('dark')
}
}
const { theme } = useStore.getState()
setThemeClass({ theme })
useStore.subscribe(setThemeClass)
root.render(
<React.StrictMode>
<Auth />
</React.StrictMode>
<HotkeysProvider>
<Router />
<Toaster
position="bottom-center"
toastOptions={{
className:
'bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-110 dark:text-chalkboard-10',
}}
/>
</HotkeysProvider>
)
// If you want to start measuring performance in your app, pass a function

View File

@ -29,7 +29,7 @@ describe('findClosingBrace', () => {
})
describe('testing AST', () => {
test('test 5 + 6', () => {
test('5 + 6', () => {
const tokens = lexer('5 +6')
const result = abstractSyntaxTree(tokens)
delete (result as any).nonCodeMeta
@ -66,7 +66,7 @@ describe('testing AST', () => {
],
})
})
test('test const myVar = 5', () => {
test('const myVar = 5', () => {
const tokens = lexer('const myVar = 5')
const { body } = abstractSyntaxTree(tokens)
expect(body).toEqual([
@ -98,7 +98,7 @@ describe('testing AST', () => {
},
])
})
test('test multi-line', () => {
test('multi-line', () => {
const code = `const myVar = 5
const newVar = myVar + 1
`
@ -171,7 +171,7 @@ const newVar = myVar + 1
},
])
})
test('test using std function "log"', () => {
test('using std function "log"', () => {
const code = `log(5, "hello", aIdentifier)`
const tokens = lexer(code)
const { body } = abstractSyntaxTree(tokens)
@ -1392,7 +1392,7 @@ describe('testing pipe operator special', () => {
})
describe('nests binary expressions correctly', () => {
it('it works with the simple case', () => {
it('works with the simple case', () => {
const code = `const yo = 1 + 2`
const { body } = abstractSyntaxTree(lexer(code))
expect(body[0]).toEqual({
@ -1435,7 +1435,7 @@ describe('nests binary expressions correctly', () => {
],
})
})
it('it should nest according to precedence with multiply first', () => {
it('should nest according to precedence with multiply first', () => {
// should be binExp { binExp { lit-1 * lit-2 } + lit}
const code = `const yo = 1 * 2 + 3`
const { body } = abstractSyntaxTree(lexer(code))
@ -1492,7 +1492,7 @@ describe('nests binary expressions correctly', () => {
],
})
})
it('it should nest according to precedence with sum first', () => {
it('should nest according to precedence with sum first', () => {
// should be binExp { lit-1 + binExp { lit-2 * lit-3 } }
const code = `const yo = 1 + 2 * 3`
const { body } = abstractSyntaxTree(lexer(code))
@ -1549,7 +1549,7 @@ describe('nests binary expressions correctly', () => {
],
})
})
it('it should nest properly with two opperators of equal precedence', () => {
it('should nest properly with two opperators of equal precedence', () => {
const code = `const yo = 1 + 2 - 3`
const { body } = abstractSyntaxTree(lexer(code))
expect((body[0] as any).declarations[0].init).toEqual({
@ -1586,7 +1586,7 @@ describe('nests binary expressions correctly', () => {
},
})
})
it('it should nest properly with two opperators of equal (but higher) precedence', () => {
it('should nest properly with two opperators of equal (but higher) precedence', () => {
const code = `const yo = 1 * 2 / 3`
const { body } = abstractSyntaxTree(lexer(code))
expect((body[0] as any).declarations[0].init).toEqual({
@ -1623,7 +1623,7 @@ describe('nests binary expressions correctly', () => {
},
})
})
it('it should nest properly with longer example', () => {
it('should nest properly with longer example', () => {
const code = `const yo = 1 + 2 * (3 - 4) / 5 + 6`
const { body } = abstractSyntaxTree(lexer(code))
const init = (body[0] as any).declarations[0].init
@ -1685,15 +1685,17 @@ const key = 'c'`
value: '\n// this is a comment\n',
}
const { nonCodeMeta } = abstractSyntaxTree(lexer(code))
expect(nonCodeMeta[0]).toEqual(nonCodeMetaInstance)
expect(nonCodeMeta.noneCodeNodes[0]).toEqual(nonCodeMetaInstance)
// extra whitespace won't change it's position (0) or value (NB the start end would have changed though)
const codeWithExtraStartWhitespace = '\n\n\n' + code
const { nonCodeMeta: nonCodeMeta2 } = abstractSyntaxTree(
lexer(codeWithExtraStartWhitespace)
)
expect(nonCodeMeta2[0].value).toBe(nonCodeMetaInstance.value)
expect(nonCodeMeta2[0].start).not.toBe(nonCodeMetaInstance.start)
expect(nonCodeMeta2.noneCodeNodes[0].value).toBe(nonCodeMetaInstance.value)
expect(nonCodeMeta2.noneCodeNodes[0].start).not.toBe(
nonCodeMetaInstance.start
)
})
it('comments nested within a block statement', () => {
const code = `const mySketch = startSketchAt([0,0])
@ -1708,6 +1710,7 @@ const key = 'c'`
const { body } = abstractSyntaxTree(lexer(code))
const indexOfSecondLineToExpression = 2
const sketchNonCodeMeta = (body as any)[0].declarations[0].init.nonCodeMeta
.noneCodeNodes
expect(sketchNonCodeMeta[indexOfSecondLineToExpression]).toEqual({
type: 'NoneCodeNode',
start: 106,
@ -1728,6 +1731,7 @@ const key = 'c'`
const { body } = abstractSyntaxTree(lexer(code))
const sketchNonCodeMeta = (body[0] as any).declarations[0].init.nonCodeMeta
.noneCodeNodes
expect(sketchNonCodeMeta[3]).toEqual({
type: 'NoneCodeNode',
start: 125,

View File

@ -1,107 +1,32 @@
import { Token } from './tokeniser'
import { parseExpression } from './astMathExpressions'
export type SyntaxType =
| 'Program'
| 'ExpressionStatement'
| 'BinaryExpression'
| 'CallExpression'
| 'Identifier'
| 'BlockStatement'
| 'ReturnStatement'
| 'VariableDeclaration'
| 'VariableDeclarator'
| 'MemberExpression'
| 'ArrayExpression'
| 'ObjectExpression'
| 'ObjectProperty'
| 'FunctionExpression'
| 'PipeExpression'
| 'PipeSubstitution'
| 'Literal'
| 'NoneCodeNode'
| 'UnaryExpression'
// | 'NumberLiteral'
// | 'StringLiteral'
// | 'IfStatement'
// | 'WhileStatement'
// | 'FunctionDeclaration'
// | 'AssignmentExpression'
// | 'Property'
// | 'LogicalExpression'
// | 'ConditionalExpression'
// | 'ForStatement'
// | 'ForInStatement'
// | 'ForOfStatement'
// | 'BreakStatement'
// | 'ContinueStatement'
// | 'SwitchStatement'
// | 'SwitchCase'
// | 'ThrowStatement'
// | 'TryStatement'
// | 'CatchClause'
// | 'ClassDeclaration'
// | 'ClassBody'
// | 'MethodDefinition'
// | 'NewExpression'
// | 'ThisExpression'
// | 'UpdateExpression'
// | 'YieldExpression'
// | 'AwaitExpression'
// | 'ImportDeclaration'
// | 'ImportSpecifier'
// | 'ImportDefaultSpecifier'
// | 'ImportNamespaceSpecifier'
// | 'ExportNamedDeclaration'
// | 'ExportDefaultDeclaration'
// | 'ExportAllDeclaration'
// | 'ExportSpecifier'
// | 'TaggedTemplateExpression'
// | 'TemplateLiteral'
// | 'TemplateElement'
// | 'SpreadElement'
// | 'RestElement'
// | 'SequenceExpression'
// | 'DebuggerStatement'
// | 'LabeledStatement'
// | 'DoWhileStatement'
// | 'WithStatement'
// | 'EmptyStatement'
// | 'ArrayPattern'
// | 'ObjectPattern'
// | 'AssignmentPattern'
// | 'MetaProperty'
// | 'Super'
// | 'Import'
// | 'RegExpLiteral'
// | 'BooleanLiteral'
// | 'NullLiteral'
// | 'TypeAnnotation'
export interface Program {
type: SyntaxType
start: number
end: number
body: BodyItem[]
nonCodeMeta: NoneCodeMeta
}
interface GeneralStatement {
type: SyntaxType
start: number
end: number
}
interface NoneCodeNode extends GeneralStatement {
type: 'NoneCodeNode'
value: string
}
interface NoneCodeMeta {
// Stores the whitespace/comments that go after the statement who's index we're using here
[statementIndex: number]: NoneCodeNode
// Which is why we also need `start` for and whitespace at the start of the file/block
start?: NoneCodeNode
}
import { KCLSyntaxError, KCLUnimplementedError } from './errors'
import {
BinaryPart,
BodyItem,
Identifier,
Literal,
NoneCodeMeta,
NoneCodeNode,
ObjectKeyInfo,
ObjectProperty,
PipeSubstitution,
Program,
Value,
VariableDeclaration,
VariableDeclarator,
ArrayExpression,
BinaryExpression,
CallExpression,
FunctionExpression,
MemberExpression,
ObjectExpression,
PipeExpression,
UnaryExpression,
BlockStatement,
ExpressionStatement,
ReturnStatement,
} from './abstractSyntaxTreeTypes'
function makeNoneCodeNode(
tokens: Token[],
@ -129,11 +54,6 @@ function findEndOfNonCodeNode(tokens: Token[], index: number): number {
return index
}
export interface ExpressionStatement extends GeneralStatement {
type: 'ExpressionStatement'
expression: Value
}
function makeExpressionStatement(
tokens: Token[],
index: number
@ -165,13 +85,6 @@ function makeExpressionStatement(
}
}
export interface CallExpression extends GeneralStatement {
type: 'CallExpression'
callee: Identifier
arguments: Value[]
optional: boolean
}
export function makeCallExpression(
tokens: Token[],
index: number
@ -216,6 +129,12 @@ function makeArguments(
}
}
const nextBraceOrCommaToken = nextMeaningfulToken(tokens, argumentToken.index)
if (nextBraceOrCommaToken.token == undefined) {
throw new KCLSyntaxError(
'Expected argument',
rangeOfToken(argumentToken.token)
)
}
const isIdentifierOrLiteral =
nextBraceOrCommaToken.token.type === 'comma' ||
nextBraceOrCommaToken.token.type === 'brace'
@ -370,13 +289,10 @@ function makeArguments(
) {
return makeArguments(tokens, argumentToken.index, previousArgs)
}
throw new Error('Expected a previous Argument if statement to match')
}
export interface VariableDeclaration extends GeneralStatement {
type: 'VariableDeclaration'
declarations: VariableDeclarator[]
kind: 'const' | 'unknown' | 'fn' //| "solid" | "surface" | "face"
throw new KCLSyntaxError(
'Expected a previous Argument if statement to match',
rangeOfToken(argumentToken.token)
)
}
function makeVariableDeclaration(
@ -407,19 +323,6 @@ function makeVariableDeclaration(
}
}
export type Value =
| Literal
| Identifier
| BinaryExpression
| FunctionExpression
| CallExpression
| PipeExpression
| PipeSubstitution
| ArrayExpression
| ObjectExpression
| MemberExpression
| UnaryExpression
function makeValue(
tokens: Token[],
index: number
@ -513,20 +416,20 @@ function makeValue(
lastIndex: arrowFunctionLastIndex,
}
} else {
throw new Error('TODO - handle expression with braces')
throw new KCLUnimplementedError(
'expression with braces',
rangeOfToken(currentToken)
)
}
}
if (currentToken.type === 'operator' && currentToken.value === '-') {
const { expression, lastIndex } = makeUnaryExpression(tokens, index)
return { value: expression, lastIndex }
}
throw new Error('Expected a previous Value if statement to match')
}
export interface VariableDeclarator extends GeneralStatement {
type: 'VariableDeclarator'
id: Identifier
init: Value
throw new KCLSyntaxError(
'Expected a previous Value if statement to match',
rangeOfToken(currentToken)
)
}
function makeVariableDeclarators(
@ -576,29 +479,6 @@ function makeVariableDeclarators(
}
}
export type BinaryPart =
| Literal
| Identifier
| BinaryExpression
| CallExpression
| UnaryExpression
// | MemberExpression
// | ArrayExpression
// | ObjectExpression
// | LogicalExpression
// | ConditionalExpression
export interface Literal extends GeneralStatement {
type: 'Literal'
value: string | number | boolean | null
raw: string
}
export interface Identifier extends GeneralStatement {
type: 'Identifier'
name: string
}
function makeIdentifier(token: Token[], index: number): Identifier {
const currentToken = token[index]
return {
@ -609,10 +489,6 @@ function makeIdentifier(token: Token[], index: number): Identifier {
}
}
export interface PipeSubstitution extends GeneralStatement {
type: 'PipeSubstitution'
}
function makeLiteral(tokens: Token[], index: number): Literal {
const token = tokens[index]
const value =
@ -626,11 +502,6 @@ function makeLiteral(tokens: Token[], index: number): Literal {
}
}
export interface ArrayExpression extends GeneralStatement {
type: 'ArrayExpression'
elements: Value[]
}
function makeArrayElements(
tokens: Token[],
index: number,
@ -650,7 +521,10 @@ function makeArrayElements(
nextToken.token.type === 'brace' && nextToken.token.value === ']'
const isComma = nextToken.token.type === 'comma'
if (!isClosingBrace && !isComma) {
throw new Error('Expected a comma or closing brace')
throw new KCLSyntaxError(
'Expected a comma or closing brace',
rangeOfToken(nextToken.token)
)
}
const nextCallIndex = isClosingBrace
? nextToken.index
@ -686,17 +560,6 @@ function makeArrayExpression(
}
}
export interface ObjectExpression extends GeneralStatement {
type: 'ObjectExpression'
properties: ObjectProperty[]
}
interface ObjectProperty extends GeneralStatement {
type: 'ObjectProperty'
key: Identifier
value: Value
}
function makeObjectExpression(
tokens: Token[],
index: number
@ -765,13 +628,6 @@ function makeObjectProperties(
])
}
export interface MemberExpression extends GeneralStatement {
type: 'MemberExpression'
object: MemberExpression | Identifier
property: Identifier | Literal
computed: boolean
}
function makeMemberExpression(
tokens: Token[],
index: number
@ -780,7 +636,8 @@ function makeMemberExpression(
const keysInfo = collectObjectKeys(tokens, index)
const lastKey = keysInfo[keysInfo.length - 1]
const firstKey = keysInfo.shift()
if (!firstKey) throw new Error('Expected a key')
if (!firstKey)
throw new KCLSyntaxError('Expected a key', rangeOfToken(currentToken))
const root = makeIdentifier(tokens, index)
let memberExpression: MemberExpression = {
type: 'MemberExpression',
@ -808,12 +665,6 @@ function makeMemberExpression(
}
}
interface ObjectKeyInfo {
key: Identifier | Literal
index: number
computed: boolean
}
function collectObjectKeys(
tokens: Token[],
index: number,
@ -859,13 +710,6 @@ function collectObjectKeys(
])
}
export interface BinaryExpression extends GeneralStatement {
type: 'BinaryExpression'
operator: string
left: BinaryPart
right: BinaryPart
}
export function findEndOfBinaryExpression(
tokens: Token[],
index: number
@ -922,12 +766,6 @@ function makeBinaryExpression(
}
}
export interface UnaryExpression extends GeneralStatement {
type: 'UnaryExpression'
operator: '-' | '!'
argument: BinaryPart
}
function makeUnaryExpression(
tokens: Token[],
index: number
@ -950,12 +788,6 @@ function makeUnaryExpression(
}
}
export interface PipeExpression extends GeneralStatement {
type: 'PipeExpression'
body: Value[]
nonCodeMeta: NoneCodeMeta
}
function makePipeExpression(
tokens: Token[],
index: number
@ -983,7 +815,7 @@ function makePipeBody(
tokens: Token[],
index: number,
previousValues: Value[] = [],
previousNonCodeMeta: NoneCodeMeta = {}
previousNonCodeMeta: NoneCodeMeta = { noneCodeNodes: {} }
): { body: Value[]; lastIndex: number; nonCodeMeta: NoneCodeMeta } {
const nonCodeMeta = { ...previousNonCodeMeta }
const currentToken = tokens[index]
@ -995,7 +827,10 @@ function makePipeBody(
value = val.value
lastIndex = val.lastIndex
} else {
throw new Error('Expected a previous PipeValue if statement to match')
throw new KCLSyntaxError(
'Expected a previous PipeValue if statement to match',
rangeOfToken(currentToken)
)
}
const nextPipeToken = hasPipeOperator(tokens, index)
@ -1007,7 +842,8 @@ function makePipeBody(
}
}
if (nextPipeToken.bonusNonCodeNode) {
nonCodeMeta[previousValues.length] = nextPipeToken.bonusNonCodeNode
nonCodeMeta.noneCodeNodes[previousValues.length] =
nextPipeToken.bonusNonCodeNode
}
return makePipeBody(
tokens,
@ -1017,13 +853,6 @@ function makePipeBody(
)
}
export interface FunctionExpression extends GeneralStatement {
type: 'FunctionExpression'
id: Identifier | null
params: Identifier[]
body: BlockStatement
}
function makeFunctionExpression(
tokens: Token[],
index: number
@ -1072,12 +901,6 @@ function makeParams(
])
}
export interface BlockStatement extends GeneralStatement {
type: 'BlockStatement'
body: BodyItem[]
nonCodeMeta: NoneCodeMeta
}
function makeBlockStatement(
tokens: Token[],
index: number
@ -1086,7 +909,11 @@ function makeBlockStatement(
const nextToken = { token: tokens[index + 1], index: index + 1 }
const { body, lastIndex, nonCodeMeta } =
nextToken.token.value === '}'
? { body: [], lastIndex: nextToken.index, nonCodeMeta: {} }
? {
body: [],
lastIndex: nextToken.index,
nonCodeMeta: { noneCodeNodes: {} },
}
: makeBody({ tokens, tokenIndex: nextToken.index })
return {
block: {
@ -1100,11 +927,6 @@ function makeBlockStatement(
}
}
export interface ReturnStatement extends GeneralStatement {
type: 'ReturnStatement'
argument: Value
}
function makeReturnStatement(
tokens: Token[],
index: number
@ -1123,8 +945,6 @@ function makeReturnStatement(
}
}
export type All = Program | ExpressionStatement[] | BinaryExpression | Literal
function nextMeaningfulToken(
tokens: Token[],
index: number,
@ -1163,8 +983,6 @@ function previousMeaningfulToken(
return { token, index: newIndex }
}
type BodyItem = ExpressionStatement | VariableDeclaration | ReturnStatement
function makeBody(
{
tokens,
@ -1174,7 +992,7 @@ function makeBody(
tokenIndex?: number
},
previousBody: BodyItem[] = [],
previousNonCodeMeta: NoneCodeMeta = {}
previousNonCodeMeta: NoneCodeMeta = { noneCodeNodes: {} }
): { body: BodyItem[]; lastIndex: number; nonCodeMeta: NoneCodeMeta } {
const nonCodeMeta = { ...previousNonCodeMeta }
if (tokenIndex >= tokens.length) {
@ -1191,7 +1009,8 @@ function makeBody(
if (previousBody.length === 0) {
nonCodeMeta.start = nextToken.bonusNonCodeNode
} else {
nonCodeMeta[previousBody.length] = nextToken.bonusNonCodeNode
nonCodeMeta.noneCodeNodes[previousBody.length] =
nextToken.bonusNonCodeNode
}
}
return makeBody(
@ -1202,7 +1021,8 @@ function makeBody(
}
const nextToken = nextMeaningfulToken(tokens, tokenIndex)
nextToken.bonusNonCodeNode &&
(nonCodeMeta[previousBody.length] = nextToken.bonusNonCodeNode)
(nonCodeMeta.noneCodeNodes[previousBody.length] =
nextToken.bonusNonCodeNode)
if (
token.type === 'word' &&
@ -1214,7 +1034,8 @@ function makeBody(
)
const nextThing = nextMeaningfulToken(tokens, lastIndex)
nextThing.bonusNonCodeNode &&
(nonCodeMeta[previousBody.length] = nextThing.bonusNonCodeNode)
(nonCodeMeta.noneCodeNodes[previousBody.length] =
nextThing.bonusNonCodeNode)
return makeBody(
{ tokens, tokenIndex: nextThing.index },
@ -1226,7 +1047,8 @@ function makeBody(
const { statement, lastIndex } = makeReturnStatement(tokens, tokenIndex)
const nextThing = nextMeaningfulToken(tokens, lastIndex)
nextThing.bonusNonCodeNode &&
(nonCodeMeta[previousBody.length] = nextThing.bonusNonCodeNode)
(nonCodeMeta.noneCodeNodes[previousBody.length] =
nextThing.bonusNonCodeNode)
return makeBody(
{ tokens, tokenIndex: nextThing.index },
@ -1245,7 +1067,8 @@ function makeBody(
)
const nextThing = nextMeaningfulToken(tokens, lastIndex)
if (nextThing.bonusNonCodeNode) {
nonCodeMeta[previousBody.length] = nextThing.bonusNonCodeNode
nonCodeMeta.noneCodeNodes[previousBody.length] =
nextThing.bonusNonCodeNode
}
return makeBody(
@ -1260,7 +1083,8 @@ function makeBody(
nextThing.token.type === 'operator'
) {
if (nextThing.bonusNonCodeNode) {
nonCodeMeta[previousBody.length] = nextThing.bonusNonCodeNode
nonCodeMeta.noneCodeNodes[previousBody.length] =
nextThing.bonusNonCodeNode
}
const { expression, lastIndex } = makeExpressionStatement(
tokens,
@ -1272,7 +1096,7 @@ function makeBody(
lastIndex,
}
}
throw new Error('Unexpected token')
throw new KCLSyntaxError('Unexpected token', rangeOfToken(token))
}
export const abstractSyntaxTree = (tokens: Token[]): Program => {
const { body, nonCodeMeta } = makeBody({ tokens })
@ -1426,15 +1250,27 @@ export function findClosingBrace(
if (isFirstCall) {
searchOpeningBrace = currentToken.value
if (!['(', '{', '['].includes(searchOpeningBrace)) {
throw new Error(
`expected to be started on a opening brace ( { [, instead found '${searchOpeningBrace}'`
throw new KCLSyntaxError(
`expected to be started on a opening brace ( { [, instead found '${searchOpeningBrace}'`,
rangeOfToken(currentToken)
)
}
}
const foundClosingBrace =
_braceCount === 1 &&
currentToken.value === closingBraceMap[searchOpeningBrace]
const foundClosingBrace = (() => {
try {
return (
_braceCount === 1 &&
currentToken.value === closingBraceMap[searchOpeningBrace]
)
} catch (e: any) {
throw new KCLSyntaxError(
'Missing a closing brace',
rangeOfToken(currentToken)
)
}
})()
const foundAnotherOpeningBrace = currentToken.value === searchOpeningBrace
const foundAnotherClosingBrace =
currentToken.value === closingBraceMap[searchOpeningBrace]
@ -1518,3 +1354,7 @@ export function isNotCodeToken(token: Token): boolean {
token?.type === 'blockcomment'
)
}
export function rangeOfToken(token: Token | undefined): [number, number][] {
return token === undefined ? [] : [[token.start, token.end]]
}

View File

@ -0,0 +1,177 @@
export type SyntaxType =
| 'Program'
| 'ExpressionStatement'
| 'BinaryExpression'
| 'CallExpression'
| 'Identifier'
| 'BlockStatement'
| 'ReturnStatement'
| 'VariableDeclaration'
| 'VariableDeclarator'
| 'MemberExpression'
| 'ArrayExpression'
| 'ObjectExpression'
| 'ObjectProperty'
| 'FunctionExpression'
| 'PipeExpression'
| 'PipeSubstitution'
| 'Literal'
| 'NoneCodeNode'
| 'UnaryExpression'
export interface Program {
type: SyntaxType
start: number
end: number
body: BodyItem[]
nonCodeMeta: NoneCodeMeta
}
interface GeneralStatement {
type: SyntaxType
start: number
end: number
}
export type BodyItem =
| ExpressionStatement
| VariableDeclaration
| ReturnStatement
export type Value =
| Literal
| Identifier
| BinaryExpression
| FunctionExpression
| CallExpression
| PipeExpression
| PipeSubstitution
| ArrayExpression
| ObjectExpression
| MemberExpression
| UnaryExpression
export type BinaryPart =
| Literal
| Identifier
| BinaryExpression
| CallExpression
| UnaryExpression
export interface NoneCodeNode extends GeneralStatement {
type: 'NoneCodeNode'
value: string
}
export interface NoneCodeMeta {
// Stores the whitespace/comments that go after the statement who's index we're using here
noneCodeNodes: { [statementIndex: number]: NoneCodeNode }
// Which is why we also need `start` for and whitespace at the start of the file/block
start?: NoneCodeNode
}
export interface ExpressionStatement extends GeneralStatement {
type: 'ExpressionStatement'
expression: Value
}
export interface CallExpression extends GeneralStatement {
type: 'CallExpression'
callee: Identifier
arguments: Value[]
optional: boolean
}
export interface VariableDeclaration extends GeneralStatement {
type: 'VariableDeclaration'
declarations: VariableDeclarator[]
kind: 'const' | 'unknown' | 'fn' //| "solid" | "surface" | "face"
}
export interface VariableDeclarator extends GeneralStatement {
type: 'VariableDeclarator'
id: Identifier
init: Value
}
export interface Literal extends GeneralStatement {
type: 'Literal'
value: string | number | boolean | null
raw: string
}
export interface Identifier extends GeneralStatement {
type: 'Identifier'
name: string
}
export interface PipeSubstitution extends GeneralStatement {
type: 'PipeSubstitution'
}
export interface ArrayExpression extends GeneralStatement {
type: 'ArrayExpression'
elements: Value[]
}
export interface ObjectExpression extends GeneralStatement {
type: 'ObjectExpression'
properties: ObjectProperty[]
}
export interface ObjectProperty extends GeneralStatement {
type: 'ObjectProperty'
key: Identifier
value: Value
}
export interface MemberExpression extends GeneralStatement {
type: 'MemberExpression'
object: MemberExpression | Identifier
property: Identifier | Literal
computed: boolean
}
export interface ObjectKeyInfo {
key: Identifier | Literal
index: number
computed: boolean
}
export interface BinaryExpression extends GeneralStatement {
type: 'BinaryExpression'
operator: string
left: BinaryPart
right: BinaryPart
}
export interface UnaryExpression extends GeneralStatement {
type: 'UnaryExpression'
operator: '-' | '!'
argument: BinaryPart
}
export interface PipeExpression extends GeneralStatement {
type: 'PipeExpression'
body: Value[]
nonCodeMeta: NoneCodeMeta
}
export interface FunctionExpression extends GeneralStatement {
type: 'FunctionExpression'
id: Identifier | null
params: Identifier[]
body: BlockStatement
}
export interface BlockStatement extends GeneralStatement {
type: 'BlockStatement'
body: BodyItem[]
nonCodeMeta: NoneCodeMeta
}
export interface ReturnStatement extends GeneralStatement {
type: 'ReturnStatement'
argument: Value
}
export type All = Program | ExpressionStatement[] | BinaryExpression | Literal

View File

@ -1,24 +1,27 @@
import { abstractSyntaxTree } from './abstractSyntaxTree'
import { lexer } from './tokeniser'
import { executor, SketchGroup, ExtrudeGroup } from './executor'
import { SketchGroup, ExtrudeGroup } from './executor'
import { initPromise } from './rust'
import { enginelessExecutor, executor } from '../lib/testHelpers'
beforeAll(() => initPromise)
describe('testing artifacts', () => {
test('sketch artifacts', () => {
// Enable rotations #152
test('sketch artifacts', async () => {
const code = `
const mySketch001 = startSketchAt([0, 0])
|> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)
|> rx(45, %)
// |> rx(45, %)
show(mySketch001)`
const programMemory = executor(abstractSyntaxTree(lexer(code)))
const geos = programMemory?.return?.map(
const programMemory = await enginelessExecutor(
abstractSyntaxTree(lexer(code))
)
const shown = programMemory?.return?.map(
(a) => programMemory?.root?.[a.name]
)
const artifactsWithoutGeos = removeGeo(geos as any)
expect(artifactsWithoutGeos).toEqual([
expect(shown).toEqual([
{
type: 'sketchGroup',
start: {
@ -26,9 +29,9 @@ show(mySketch001)`
to: [0, 0],
from: [0, 0],
__geoMeta: {
id: '66366561-6465-4734-a463-366330356563',
sourceRange: [21, 42],
pathToNode: [],
geos: ['sketchBase'],
},
},
value: [
@ -38,8 +41,8 @@ show(mySketch001)`
from: [0, 0],
__geoMeta: {
sourceRange: [48, 73],
id: '30366338-6462-4330-a364-303935626163',
pathToNode: [],
geos: ['line', 'lineEnd'],
},
},
{
@ -48,258 +51,101 @@ show(mySketch001)`
from: [-1.59, -1.54],
__geoMeta: {
sourceRange: [79, 103],
id: '32653334-6331-4231-b162-663334363535',
pathToNode: [],
geos: ['line', 'lineEnd'],
},
},
],
position: [0, 0, 0],
rotation: [0.3826834323650898, 0, 0, 0.9238795325112867],
__meta: [
{ sourceRange: [21, 42], pathToNode: [] },
{ sourceRange: [109, 118], pathToNode: [] },
],
rotation: [0, 0, 0, 1],
id: '39643164-6130-4734-b432-623638393262',
__meta: [{ sourceRange: [21, 42], pathToNode: [] }],
},
])
})
test('extrude artifacts', () => {
test('extrude artifacts', async () => {
// Enable rotations #152
const code = `
const mySketch001 = startSketchAt([0, 0])
|> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)
|> rx(45, %)
// |> rx(45, %)
|> extrude(2, %)
show(mySketch001)`
const programMemory = executor(abstractSyntaxTree(lexer(code)))
const geos = programMemory?.return?.map(
const programMemory = await enginelessExecutor(
abstractSyntaxTree(lexer(code))
)
const shown = programMemory?.return?.map(
(a) => programMemory?.root?.[a.name]
)
const artifactsWithoutGeos = removeGeo(geos as any)
expect(artifactsWithoutGeos).toEqual([
expect(shown).toEqual([
{
type: 'extrudeGroup',
value: [
{
type: 'extrudePlane',
position: [-0.795, -0.5444722215136415, -0.5444722215136416],
rotation: [
0.35471170441873584, 0.3467252481708758, -0.14361830020955396,
0.8563498075401887,
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [48, 73],
pathToNode: [],
},
},
{
type: 'extrudePlane',
position: [
-0.5650000000000001, -2.602152954766495, -2.602152954766495,
],
rotation: [
0.20394238048109659, 0.7817509623502217, -0.3238118510036805,
0.4923604609001174,
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [79, 103],
pathToNode: [],
},
},
],
id: '65383433-3839-4333-b836-343263636638',
value: [],
height: 2,
position: [0, 0, 0],
rotation: [0.3826834323650898, 0, 0, 0.9238795325112867],
rotation: [0, 0, 0, 1],
__meta: [
{ sourceRange: [124, 137], pathToNode: [] },
{ sourceRange: [127, 140], pathToNode: [] },
{ sourceRange: [21, 42], pathToNode: [] },
],
},
])
})
test('sketch extrude and sketch on one of the faces', () => {
test('sketch extrude and sketch on one of the faces', async () => {
// Enable rotations #152
// TODO #153 in order for getExtrudeWallTransform to work we need to query the engine for the location of a face.
const code = `
const sk1 = startSketchAt([0, 0])
|> lineTo([-2.5, 0], %)
|> lineTo({ to: [0, 10], tag: "p" }, %)
|> lineTo([2.5, 0], %)
|> rx(45, %)
|> translate([1,0,1], %)
|> ry(5, %)
// |> rx(45, %)
// |> translate([1,0,1], %)
// |> ry(5, %)
const theExtrude = extrude(2, sk1)
const theTransf = getExtrudeWallTransform('p', theExtrude)
// const theTransf = getExtrudeWallTransform('p', theExtrude)
const sk2 = startSketchAt([0, 0])
|> lineTo([-2.5, 0], %)
|> lineTo({ to: [0, 3], tag: "p" }, %)
|> lineTo([2.5, 0], %)
|> transform(theTransf, %)
// |> transform(theTransf, %)
|> extrude(2, %)
show(theExtrude, sk2)`
const programMemory = executor(abstractSyntaxTree(lexer(code)))
const geos = programMemory?.return?.map(
(a) => programMemory?.root?.[a.name]
const programMemory = await enginelessExecutor(
abstractSyntaxTree(lexer(code))
)
const artifactsWithoutGeos = removeGeo(geos as any)
expect(artifactsWithoutGeos).toEqual([
const geos = programMemory?.return?.map(
({ name }) => programMemory?.root?.[name]
)
expect(geos).toEqual([
{
type: 'extrudeGroup',
value: [
{
type: 'extrudePlane',
position: [-0.1618929317752782, 0, 1.01798363377866],
rotation: [
0.3823192025331841, -0.04029905920751535, -0.016692416874629204,
0.9230002039112793,
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [40, 60],
pathToNode: [],
},
},
{
type: 'extrudePlane',
position: [
0.14624915180581843, 3.5355339059327373, 4.540063765792454,
],
rotation: [
-0.24844095888221532, 0.7523143130765927, -0.2910733573455524,
-0.5362616571538269,
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [66, 102],
pathToNode: [],
},
name: 'p',
},
{
type: 'extrudePlane',
position: [
2.636735897035183, 3.5355339059327386, 4.322174408923308,
],
rotation: [
0.22212685137378593, 0.7027132469491032, -0.3116187916437232,
0.5997895323824204,
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [108, 127],
pathToNode: [],
},
},
],
id: '63333330-3631-4230-b664-623132643731',
value: [],
height: 2,
position: [1.083350440839404, 0, 0.9090389553440874],
rotation: [
0.38231920253318413, 0.04029905920751535, -0.01669241687462921,
0.9230002039112792,
],
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
__meta: [
{ sourceRange: [203, 218], pathToNode: [] },
{ sourceRange: [212, 227], pathToNode: [] },
{ sourceRange: [13, 34], pathToNode: [] },
],
},
{
type: 'extrudeGroup',
value: [
{
type: 'extrudePlane',
position: [
0.5230004643466108, 4.393026831645281, 5.367870706359959,
],
rotation: [
-0.5548685410139091, 0.7377864971619333, 0.3261466075583827,
-0.20351996751370383,
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [317, 337],
pathToNode: [],
},
},
{
type: 'extrudePlane',
position: [
0.43055783927228125, 5.453687003425103, 4.311246666755821,
],
rotation: [
0.5307054034531232, -0.4972416536396126, 0.3641462373475848,
-0.5818075544860157,
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [343, 378],
pathToNode: [],
},
name: 'p',
},
{
type: 'extrudePlane',
position: [
-0.3229447858093035, 3.7387011520000146, 2.6556327856208117,
],
rotation: [
0.06000443169260189, 0.12863059446321826, 0.6408199244764428,
-0.7544557394170275,
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [384, 403],
pathToNode: [],
},
},
],
id: '33316639-3438-4661-a334-663262383737',
value: [],
height: 2,
position: [0.14624915180581843, 3.5355339059327373, 4.540063765792454],
rotation: [
0.24844095888221532, -0.7523143130765927, 0.2910733573455524,
-0.5362616571538269,
],
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
__meta: [
{ sourceRange: [438, 451], pathToNode: [] },
{ sourceRange: [290, 311], pathToNode: [] },
{ sourceRange: [453, 466], pathToNode: [] },
{ sourceRange: [302, 323], pathToNode: [] },
],
},
])
})
})
function removeGeo(arts: (SketchGroup | ExtrudeGroup)[]): any {
return arts.map((art) => {
if (art.type === 'extrudeGroup') {
return {
...art,
value: art.value.map((v) => ({
...v,
__geoMeta: {
...v.__geoMeta,
geo: v.__geoMeta.geo.type,
},
})),
}
}
return {
...art,
start: art.start
? {
...art.start,
__geoMeta: {
...art.start.__geoMeta,
geos: art.start.__geoMeta.geos.map((g) => g.type),
},
}
: {},
value: art.value.map((v) => ({
...v,
__geoMeta: {
...v.__geoMeta,
geos: v.__geoMeta.geos.map((g) => g.type),
},
})),
}
})
}

View File

@ -53,7 +53,7 @@ describe('parseExpression', () => {
},
})
})
it('parses a more complex expression with parentheses with more ', () => {
it('parses a more complex expression with parentheses with more', () => {
const result = parseExpression(lexer('1 * ( 2 + 3 ) / 4'))
expect(result).toEqual({
type: 'BinaryExpression',
@ -78,7 +78,7 @@ describe('parseExpression', () => {
right: { type: 'Literal', value: 4, raw: '4', start: 16, end: 17 },
})
})
it('same as last one but with a 1 + at the start ', () => {
it('same as last one but with a 1 + at the start', () => {
const result = parseExpression(lexer('1 + ( 2 + 3 ) / 4'))
expect(result).toEqual({
type: 'BinaryExpression',
@ -103,7 +103,7 @@ describe('parseExpression', () => {
},
})
})
it('nested braces ', () => {
it('nested braces', () => {
const result = parseExpression(lexer('1 * (( 2 + 3 ) / 4 + 5 )'))
expect(result).toEqual({
type: 'BinaryExpression',
@ -141,7 +141,7 @@ describe('parseExpression', () => {
},
})
})
it('multiple braces around the same thing ', () => {
it('multiple braces around the same thing', () => {
const result = parseExpression(lexer('1 * ((( 2 + 3 )))'))
expect(result).toEqual({
type: 'BinaryExpression',

View File

@ -3,11 +3,14 @@ import {
Literal,
Identifier,
CallExpression,
} from './abstractSyntaxTreeTypes'
import {
findClosingBrace,
makeCallExpression,
isNotCodeToken,
} from './abstractSyntaxTree'
import { Token } from './tokeniser'
import { KCLSyntaxError } from './errors'
export function reversePolishNotation(
tokens: Token[],
@ -80,7 +83,9 @@ export function reversePolishNotation(
if (isNotCodeToken(currentToken)) {
return reversePolishNotation(tokens.slice(1), previousPostfix, operators)
}
throw new Error('Unknown token')
throw new KCLSyntaxError('Unknown token', [
[currentToken.start, currentToken.end],
])
}
interface ParenthesisToken {
@ -202,21 +207,27 @@ const buildTree = (
}
export function parseExpression(tokens: Token[]): BinaryExpression {
const treeWithMabyeBadTopLevelStartEnd = buildTree(
const treeWithMaybeBadTopLevelStartEnd = buildTree(
reversePolishNotation(tokens)
)
const left = treeWithMabyeBadTopLevelStartEnd?.left as any
const start = left?.startExtended || treeWithMabyeBadTopLevelStartEnd?.start
const left = treeWithMaybeBadTopLevelStartEnd?.left as any
const start = left?.startExtended || treeWithMaybeBadTopLevelStartEnd?.start
if (left == undefined || left == null) {
throw new KCLSyntaxError(
'syntax',
tokens.map((token) => [token.start, token.end])
) // Add text
}
delete left.startExtended
delete left.endExtended
const right = treeWithMabyeBadTopLevelStartEnd?.right as any
const end = right?.endExtended || treeWithMabyeBadTopLevelStartEnd?.end
const right = treeWithMaybeBadTopLevelStartEnd?.right as any
const end = right?.endExtended || treeWithMaybeBadTopLevelStartEnd?.end
delete right.startExtended
delete right.endExtended
const tree: BinaryExpression = {
...treeWithMabyeBadTopLevelStartEnd,
...treeWithMaybeBadTopLevelStartEnd,
start,
end,
left,
@ -230,7 +241,7 @@ function _precedence(operator: Token): number {
}
export function precedence(operator: string): number {
// might be useful for refenecne to make it match
// might be useful for reference to make it match
// another commonly used lang https://www.w3schools.com/js/js_precedence.asp
if (['+', '-'].includes(operator)) {
return 11

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