Compare commits
69 Commits
Author | SHA1 | Date | |
---|---|---|---|
826ad267b4 | |||
2476c12480 | |||
ae460ed02f | |||
3a93839a2d | |||
dbb94d7e95 | |||
968a67e654 | |||
8ebb8b8b94 | |||
f9259aa869 | |||
a6f92e358b | |||
35e4727856 | |||
a986f76e70 | |||
7a537eea8e | |||
ca985dd1a8 | |||
1cba48f513 | |||
e7a70a9735 | |||
6e14dbaf77 | |||
62dc07e117 | |||
391f4ba206 | |||
4c5178ea5c | |||
8c5d7bf648 | |||
231371fb16 | |||
cd4672c98d | |||
c80dd44c59 | |||
c190122240 | |||
36ea343860 | |||
1962af760c | |||
8be7805ac2 | |||
64772f5c98 | |||
5419039fae | |||
aabb88ee45 | |||
7408ba50dd | |||
7181ff0c33 | |||
7ae1b66855 | |||
c31f1ad98b | |||
894bddb369 | |||
94918ccb2e | |||
be7605cdef | |||
aca9b9226c | |||
7312035818 | |||
956e4c46c1 | |||
0d010b60e5 | |||
6838e96723 | |||
c2210835ea | |||
d1e7cb23a1 | |||
9cd3845975 | |||
ca2634d523 | |||
48f1d5e623 | |||
87aecf7f50 | |||
b89faa4a28 | |||
1666e17ca5 | |||
5d90c0488f | |||
11cc85a9e8 | |||
3383becc0f | |||
4c65d5b2ef | |||
59fa51d75a | |||
3fc4d71a1e | |||
317dc6d0b2 | |||
4f8fe2d155 | |||
cda301997e | |||
a70399bacf | |||
3510abfcb9 | |||
fb3c34d5f3 | |||
7289965916 | |||
2d3c73d46a | |||
dd3117cf03 | |||
efa3bc7ac6 | |||
0858d32c1e | |||
e7c1554129 | |||
6eaa0e0852 |
3
.env.development
Normal 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
@ -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
@ -0,0 +1,16 @@
|
||||
{
|
||||
"plugins": [
|
||||
"css-modules"
|
||||
],
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest",
|
||||
"plugin:css-modules/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
]
|
||||
}
|
||||
}
|
68
.github/workflows/build.yml
vendored
@ -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
@ -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
|
9
.github/workflows/test.yml
vendored
@ -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
@ -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
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
yarn fmt-check
|
7
.prettierignore
Normal file
@ -0,0 +1,7 @@
|
||||
# Ignore artifacts:
|
||||
build
|
||||
coverage
|
||||
|
||||
# Ignore Rust projects:
|
||||
*.rs
|
||||
target
|
4
.vscode/settings.json
vendored
@ -1,5 +1,7 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"geos"
|
||||
]
|
||||
],
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true,
|
||||
}
|
31
README.md
@ -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 😉
|
||||

|
||||
|
||||
|
||||
<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
After Width: | Height: | Size: 195 KiB |
3
babel.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: ["@babel/preset-env"],
|
||||
}
|
21
index.html
Normal 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>
|
97
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 83 KiB |
@ -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>
|
37
public/kitt-8bit-winking.svg
Normal 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 |
18
public/kitt-arcade-winking.svg
Normal 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 |
26
public/kittycad-logomark-light.svg
Normal 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 |
26
public/kittycad-logomark.svg
Normal 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 |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 28 KiB |
@ -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
@ -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"]
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 112 KiB |
@ -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");
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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()
|
||||
})
|
||||
|
561
src/App.tsx
@ -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
|
||||
|
57
src/Auth.tsx
@ -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
@ -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} />
|
||||
}
|
@ -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
@ -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;
|
||||
}
|
52
src/components/ActionButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
48
src/components/ActionIcon.tsx
Normal 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>
|
||||
)
|
||||
}
|
46
src/components/AppHeader.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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)
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
@ -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>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
52
src/components/CollapsiblePanel.module.css
Normal 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;
|
||||
}
|
57
src/components/CollapsiblePanel.tsx
Normal 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>
|
||||
)
|
||||
}
|
136
src/components/DebugPanel.tsx
Normal 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>
|
||||
)
|
||||
}
|
8
src/components/ErrorPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
185
src/components/ExportButton.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
41
src/components/Loading.tsx
Normal 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
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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] },
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
42
src/components/OpenFileButton.tsx
Normal 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>
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
40
src/components/Toggle/Toggle.module.css
Normal 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%;
|
||||
}
|
34
src/components/Toggle/Toggle.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
66
src/components/UserSidebarMenu.test.tsx
Normal 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')
|
||||
})
|
133
src/components/UserSidebarMenu.tsx
Normal 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
|
@ -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
@ -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
|
57
src/hooks/useBackdropHighlight.ts
Normal 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
|
||||
}
|
@ -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
@ -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()
|
||||
}, [])
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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]]
|
||||
}
|
||||
|
177
src/lang/abstractSyntaxTreeTypes.ts
Normal 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
|
@ -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),
|
||||
},
|
||||
})),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|