Compare commits
	
		
			1 Commits
		
	
	
		
			v0.4.0
			...
			paultag/ad
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 73129b9f1c | 
| @ -1,7 +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 | ||||
| VITE_KC_SKIP_AUTH=false | ||||
| VITE_KC_CONNECTION_TIMEOUT_MS=5000 | ||||
| VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=0 | ||||
| VITE_KC_SENTRY_DSN= | ||||
|  | ||||
| @ -1,7 +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 | ||||
| VITE_KC_SKIP_AUTH=false | ||||
| VITE_KC_CONNECTION_TIMEOUT_MS=15000 | ||||
| VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=30000 | ||||
| VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224 | ||||
| VITE_KC_SITE_BASE_URL=https://kittycad.io | ||||
| @ -1 +0,0 @@ | ||||
| src/wasm-lib/* | ||||
							
								
								
									
										2
									
								
								.github/workflows/cargo-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/cargo-build.yml
									
									
									
									
										vendored
									
									
								
							| @ -22,7 +22,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         dir: ['src/wasm-lib'] | ||||
|         dir: ['src/wasm-lib', 'src-tauri'] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|  | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/cargo-clippy.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/cargo-clippy.yml
									
									
									
									
										vendored
									
									
								
							| @ -22,7 +22,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         dir: ['src/wasm-lib'] | ||||
|         dir: ['src/wasm-lib', 'src-tauri'] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Install latest rust | ||||
|  | ||||
							
								
								
									
										7
									
								
								.github/workflows/cargo-test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/cargo-test.yml
									
									
									
									
										vendored
									
									
								
							| @ -24,7 +24,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest-8-cores | ||||
|     strategy: | ||||
|       matrix: | ||||
|         dir: ['src/wasm-lib'] | ||||
|         dir: ['src/wasm-lib', 'src-tauri'] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Install latest rust | ||||
| @ -45,7 +45,4 @@ jobs: | ||||
|         shell: bash | ||||
|         run: |- | ||||
|           cd "${{ matrix.dir }}" | ||||
|           cargo test --all | ||||
|         env: | ||||
|           KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}} | ||||
|  | ||||
|           cargo llvm-cov nextest --lcov --output-path lcov.info --test-threads=1 --no-fail-fast | ||||
|  | ||||
							
								
								
									
										74
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										74
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,4 +1,4 @@ | ||||
| name: CI | ||||
| name: CI  | ||||
|  | ||||
| on: | ||||
|   pull_request: | ||||
| @ -13,31 +13,17 @@ jobs: | ||||
|   check-format: | ||||
|     runs-on: 'ubuntu-20.04' | ||||
|     steps: | ||||
|  | ||||
|       - uses: actions/checkout@v3 | ||||
|  | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version-file: '.nvmrc' | ||||
|           cache: 'yarn' | ||||
|  | ||||
|       - run: yarn install | ||||
|  | ||||
|       - run: yarn fmt-check | ||||
|  | ||||
|   check-types: | ||||
|     runs-on: ubuntu-20.04 | ||||
|  | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version-file: '.nvmrc' | ||||
|           cache: 'yarn' | ||||
|       - run: yarn install | ||||
|       - uses: Swatinem/rust-cache@v2 | ||||
|         with: | ||||
|           workspaces: "./src/wasm-lib" | ||||
|  | ||||
|       - run: yarn build:wasm | ||||
|       - run: yarn tsc | ||||
|  | ||||
|  | ||||
|   build-test-web: | ||||
|     runs-on: ubuntu-20.04 | ||||
| @ -50,28 +36,27 @@ jobs: | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version-file: '.nvmrc' | ||||
|           cache: 'yarn' | ||||
|  | ||||
|       - run: yarn install | ||||
|  | ||||
|       - uses: Swatinem/rust-cache@v2 | ||||
|         with: | ||||
|           workspaces: "./src/wasm-lib" | ||||
|  | ||||
|       - run: yarn build:wasm | ||||
|  | ||||
|       - run: yarn tsc | ||||
|  | ||||
|       - run: yarn simpleserver:ci | ||||
|  | ||||
|       - run: yarn test:nowatch | ||||
|  | ||||
|       - run: yarn test:cov | ||||
|  | ||||
|       - run: yarn test:rust | ||||
|        | ||||
|       - id: export_version | ||||
|         run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT" | ||||
|  | ||||
|  | ||||
|   build-apps: | ||||
|     needs: [check-format, build-test-web, check-types] | ||||
|     needs: [check-format, build-test-web] | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     strategy: | ||||
|       matrix: | ||||
| @ -102,10 +87,6 @@ jobs: | ||||
|         with: | ||||
|           workspaces: './src-tauri -> target' | ||||
|  | ||||
|       - uses: Swatinem/rust-cache@v2 | ||||
|         with: | ||||
|           workspaces: "./src/wasm-lib" | ||||
|  | ||||
|       - name: wasm prep | ||||
|         shell: bash | ||||
|         run: | | ||||
| @ -129,22 +110,15 @@ jobs: | ||||
|       - name: Fix format | ||||
|         run: yarn fmt | ||||
|  | ||||
|       - name: install apple silicon target mac | ||||
|         if: matrix.os == 'macos-latest' | ||||
|         run: | | ||||
|           rustup target add aarch64-apple-darwin | ||||
|  | ||||
|       - name: Build the app for the current platform (no upload) | ||||
|         uses: tauri-apps/tauri-action@v0 | ||||
|         env: | ||||
|           TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} | ||||
|           TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} | ||||
|         with: | ||||
|           args: ${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }} | ||||
|  | ||||
|       - uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           path: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin/release/bundle/*/*' || 'src-tauri/target/release/bundle/*/*' }} | ||||
|           path: src-tauri/target/release/bundle/*/* | ||||
|  | ||||
|  | ||||
|   publish-apps-release: | ||||
| @ -159,7 +133,8 @@ jobs: | ||||
|  | ||||
|       - name: Generate the update static endpoint | ||||
|         run: | | ||||
|           ls -l artifact/*/*itty* | ||||
|           ls -l artifact | ||||
|           ls -l artifact/* | ||||
|           DARWIN_SIG=`cat artifact/macos/*.app.tar.gz.sig` | ||||
|           LINUX_SIG=`cat artifact/appimage/*.AppImage.tar.gz.sig` | ||||
|           WINDOWS_SIG=`cat artifact/nsis/*.nsis.zip.sig` | ||||
| @ -167,11 +142,11 @@ jobs: | ||||
|           jq --null-input \ | ||||
|             --arg version "v${VERSION_NO_V}" \ | ||||
|             --arg darwin_sig "$DARWIN_SIG" \ | ||||
|             --arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \ | ||||
|             --arg darwin_url "$RELEASE_DIR/macos/kittycad-modeling-app.app.tar.gz" \ | ||||
|             --arg linux_sig "$LINUX_SIG" \ | ||||
|             --arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage.tar.gz" \ | ||||
|             --arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling-app_${VERSION_NO_V}_amd64.AppImage.tar.gz" \ | ||||
|             --arg windows_sig "$WINDOWS_SIG" \ | ||||
|             --arg windows_url "$RELEASE_DIR/nsis/KittyCAD%20Modeling_${VERSION_NO_V}_x64-setup.nsis.zip" \ | ||||
|             --arg windows_url "$RELEASE_DIR/nsis/kittycad-modeling-app_${VERSION_NO_V}_x64-setup.nsis.zip" \ | ||||
|             '{ | ||||
|               "version": $version, | ||||
|               "platforms": { | ||||
| @ -179,10 +154,6 @@ jobs: | ||||
|                   "signature": $darwin_sig, | ||||
|                   "url": $darwin_url | ||||
|                 }, | ||||
|                 "darwin-aarch64": { | ||||
|                   "signature": $darwin_sig, | ||||
|                   "url": $darwin_url | ||||
|                 }, | ||||
|                 "linux-x86_64": { | ||||
|                   "signature": $linux_sig, | ||||
|                   "url": $linux_url | ||||
| @ -204,22 +175,17 @@ jobs: | ||||
|         uses: google-github-actions/setup-gcloud@v1.1.1 | ||||
|         with: | ||||
|           project_id: kittycadapi | ||||
|  | ||||
|        | ||||
|       - name: Upload release files to public bucket | ||||
|         uses: google-github-actions/upload-cloud-storage@v1.0.3 | ||||
|         with: | ||||
|           path: artifact | ||||
|           glob: '*/*itty*' | ||||
|           glob: '*/kittycad-modeling-app*' | ||||
|           parent: false | ||||
|           destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }} | ||||
|  | ||||
|           destination: dl.kittycad.io/releases/modeling-app/v${{ env.VERSION_NO_V }}  | ||||
|        | ||||
|       - name: Upload update endpoint to public bucket | ||||
|         uses: google-github-actions/upload-cloud-storage@v1.0.3 | ||||
|         with: | ||||
|           path: last_update.json | ||||
|           destination: dl.kittycad.io/releases/modeling-app | ||||
|  | ||||
|       - name: Upload release files to Github | ||||
|         uses: softprops/action-gh-release@v1 | ||||
|         with: | ||||
|           files: artifact/*/*itty* | ||||
|  | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -25,6 +25,5 @@ yarn-error.log* | ||||
| # rust | ||||
| src/wasm-lib/target | ||||
| src/wasm-lib/bindings | ||||
| src/wasm-lib/kcl/bindings | ||||
| public/wasm_lib_bg.wasm | ||||
| src/wasm-lib/lcov.info | ||||
|  | ||||
| @ -5,5 +5,3 @@ coverage | ||||
| # Ignore Rust projects: | ||||
| *.rs | ||||
| target | ||||
| src/wasm-lib/pkg | ||||
| src/wasm-lib/kcl/bindings | ||||
|  | ||||
							
								
								
									
										21
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								README.md
									
									
									
									
									
								
							| @ -86,24 +86,3 @@ The PR may serve as a place to discuss the human-readable changelog and extra QA | ||||
| 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 | ||||
|  | ||||
| ## Fuzzing the parser | ||||
|  | ||||
| Make sure you install cargo fuzz: | ||||
|  | ||||
| ```bash | ||||
| $ cargo install cargo-fuzz | ||||
| ``` | ||||
|  | ||||
| ```bash | ||||
| $ cd src/wasm-lib/kcl | ||||
|  | ||||
| # list the fuzz targets | ||||
| $ cargo fuzz list | ||||
|  | ||||
| # run the parser fuzzer | ||||
| $ cargo +nightly fuzz run parser | ||||
| ``` | ||||
|  | ||||
| For more information on fuzzing you can check out  | ||||
| [this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html). | ||||
|  | ||||
							
								
								
									
										19458
									
								
								docs/kcl.json
									
									
									
									
									
								
							
							
						
						
									
										19458
									
								
								docs/kcl.json
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										3477
									
								
								docs/kcl.md
									
									
									
									
									
								
							
							
						
						
									
										3477
									
								
								docs/kcl.md
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -11,7 +11,6 @@ | ||||
|     /> | ||||
|     <link rel="apple-touch-icon" href="/logo192.png" /> | ||||
|     <link rel="manifest" href="/manifest.json" /> | ||||
|     <script defer data-domain="app.kittycad.io" src="https://plausible.corp.kittycad.io/js/script.js"></script> | ||||
|     <title>KittyCAD Modeling App</title> | ||||
|   </head> | ||||
|   <body class="body-bg"> | ||||
|  | ||||
							
								
								
									
										24
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								package.json
									
									
									
									
									
								
							| @ -1,35 +1,28 @@ | ||||
| { | ||||
|   "name": "untitled-app", | ||||
|   "version": "0.4.0", | ||||
|   "version": "0.0.4", | ||||
|   "private": true, | ||||
|   "dependencies": { | ||||
|     "@codemirror/autocomplete": "^6.9.0", | ||||
|     "@fortawesome/fontawesome-svg-core": "^6.4.2", | ||||
|     "@fortawesome/free-brands-svg-icons": "^6.4.2", | ||||
|     "@fortawesome/free-solid-svg-icons": "^6.4.2", | ||||
|     "@fortawesome/react-fontawesome": "^0.2.0", | ||||
|     "@headlessui/react": "^1.7.13", | ||||
|     "@headlessui/tailwindcss": "^0.2.0", | ||||
|     "@kittycad/lib": "^0.0.36", | ||||
|     "@lezer/javascript": "^1.4.7", | ||||
|     "@open-rpc/client-js": "^1.8.1", | ||||
|     "@kittycad/lib": "^0.0.29", | ||||
|     "@react-hook/resize-observer": "^1.2.6", | ||||
|     "@sentry/react": "^7.65.0", | ||||
|     "@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", | ||||
|     "@ts-stack/markdown": "^1.5.0", | ||||
|     "@types/node": "^16.7.13", | ||||
|     "@types/react": "^18.0.0", | ||||
|     "@types/react-dom": "^18.0.0", | ||||
|     "@uiw/react-codemirror": "^4.21.13", | ||||
|     "@uiw/codemirror-extensions-langs": "^4.21.9", | ||||
|     "@uiw/react-codemirror": "^4.15.1", | ||||
|     "@xstate/react": "^3.2.2", | ||||
|     "crypto-js": "^4.1.1", | ||||
|     "formik": "^2.4.3", | ||||
|     "fuse.js": "^6.6.2", | ||||
|     "http-server": "^14.1.1", | ||||
|     "json-rpc-2.0": "^1.6.0", | ||||
|     "re-resizable": "^6.9.9", | ||||
|     "react": "^18.2.0", | ||||
|     "react-dom": "^18.2.0", | ||||
| @ -47,8 +40,6 @@ | ||||
|     "typescript": "^4.4.2", | ||||
|     "uuid": "^9.0.0", | ||||
|     "vitest": "^0.34.1", | ||||
|     "vscode-jsonrpc": "^8.1.0", | ||||
|     "vscode-languageserver-protocol": "^3.17.3", | ||||
|     "wasm-pack": "^0.12.1", | ||||
|     "web-vitals": "^2.1.0", | ||||
|     "ws": "^8.13.0", | ||||
| @ -63,15 +54,15 @@ | ||||
|     "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 --all && cargo clippy --all --tests)", | ||||
|     "test:rust": "(cd src/wasm-lib && cargo test && cargo clippy)", | ||||
|     "test:cov": "vitest run --coverage --mode development", | ||||
|     "simpleserver:ci": "http-server ./public --cors -p 3000 &", | ||||
|     "simpleserver": "http-server ./public --cors -p 3000", | ||||
|     "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 && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta", | ||||
|     "build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test --all) && 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 && rm -rf src/wasm-lib/kcl/bindings", | ||||
|     "wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/bindings", | ||||
|     "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" | ||||
|   }, | ||||
| @ -98,7 +89,6 @@ | ||||
|     "@babel/preset-env": "^7.22.9", | ||||
|     "@tauri-apps/cli": "^1.3.1", | ||||
|     "@types/crypto-js": "^4.1.1", | ||||
|     "@types/debounce": "^1.2.1", | ||||
|     "@types/isomorphic-fetch": "^0.0.36", | ||||
|     "@types/react-modal": "^3.16.0", | ||||
|     "@types/uuid": "^9.0.1", | ||||
|  | ||||
							
								
								
									
										77
									
								
								src-tauri/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										77
									
								
								src-tauri/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -648,12 +648,6 @@ dependencies = [ | ||||
|  "cfg-if", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "equivalent" | ||||
| version = "1.0.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" | ||||
|  | ||||
| [[package]] | ||||
| name = "errno" | ||||
| version = "0.3.1" | ||||
| @ -1156,7 +1150,7 @@ dependencies = [ | ||||
|  "futures-sink", | ||||
|  "futures-util", | ||||
|  "http", | ||||
|  "indexmap 1.9.3", | ||||
|  "indexmap", | ||||
|  "slab", | ||||
|  "tokio", | ||||
|  "tokio-util", | ||||
| @ -1169,12 +1163,6 @@ version = "0.12.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" | ||||
|  | ||||
| [[package]] | ||||
| name = "hashbrown" | ||||
| version = "0.14.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" | ||||
|  | ||||
| [[package]] | ||||
| name = "heck" | ||||
| version = "0.3.3" | ||||
| @ -1390,18 +1378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" | ||||
| dependencies = [ | ||||
|  "autocfg", | ||||
|  "hashbrown 0.12.3", | ||||
|  "serde", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "indexmap" | ||||
| version = "2.0.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" | ||||
| dependencies = [ | ||||
|  "equivalent", | ||||
|  "hashbrown 0.14.0", | ||||
|  "hashbrown", | ||||
|  "serde", | ||||
| ] | ||||
|  | ||||
| @ -1651,12 +1628,6 @@ version = "0.3.17" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" | ||||
|  | ||||
| [[package]] | ||||
| name = "minisign-verify" | ||||
| version = "0.2.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881" | ||||
|  | ||||
| [[package]] | ||||
| name = "miniz_oxide" | ||||
| version = "0.6.2" | ||||
| @ -2151,7 +2122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590" | ||||
| dependencies = [ | ||||
|  "base64 0.21.2", | ||||
|  "indexmap 1.9.3", | ||||
|  "indexmap", | ||||
|  "line-wrap", | ||||
|  "quick-xml", | ||||
|  "serde", | ||||
| @ -2724,15 +2695,14 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "serde_with" | ||||
| version = "3.2.0" | ||||
| version = "2.3.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49" | ||||
| checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" | ||||
| dependencies = [ | ||||
|  "base64 0.21.2", | ||||
|  "base64 0.13.1", | ||||
|  "chrono", | ||||
|  "hex", | ||||
|  "indexmap 1.9.3", | ||||
|  "indexmap 2.0.0", | ||||
|  "indexmap", | ||||
|  "serde", | ||||
|  "serde_json", | ||||
|  "serde_with_macros", | ||||
| @ -2741,9 +2711,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "serde_with_macros" | ||||
| version = "3.2.0" | ||||
| version = "2.3.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "9197f1ad0e3c173a0222d3c4404fb04c3afe87e962bcb327af73e8301fa203c7" | ||||
| checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" | ||||
| dependencies = [ | ||||
|  "darling", | ||||
|  "proc-macro2", | ||||
| @ -3052,7 +3022,6 @@ checksum = "d42ba3a2e8556722f31336a0750c10dbb6a81396a1c452977f515da83f69f842" | ||||
| dependencies = [ | ||||
|  "anyhow", | ||||
|  "attohttpc", | ||||
|  "base64 0.21.2", | ||||
|  "cocoa", | ||||
|  "dirs-next", | ||||
|  "embed_plist", | ||||
| @ -3065,7 +3034,6 @@ dependencies = [ | ||||
|  "heck 0.4.1", | ||||
|  "http", | ||||
|  "ignore", | ||||
|  "minisign-verify", | ||||
|  "objc", | ||||
|  "once_cell", | ||||
|  "open", | ||||
| @ -3087,21 +3055,19 @@ dependencies = [ | ||||
|  "tauri-utils", | ||||
|  "tempfile", | ||||
|  "thiserror", | ||||
|  "time", | ||||
|  "tokio", | ||||
|  "url", | ||||
|  "uuid", | ||||
|  "webkit2gtk", | ||||
|  "webview2-com", | ||||
|  "windows 0.39.0", | ||||
|  "zip", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "tauri-build" | ||||
| version = "1.4.0" | ||||
| version = "1.3.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7d2edd6a259b5591c8efdeb9d5702cb53515b82a6affebd55c7fd6d3a27b7d1b" | ||||
| checksum = "929b3bd1248afc07b63e33a6a53c3f82c32d0b0a5e216e4530e94c467e019389" | ||||
| dependencies = [ | ||||
|  "anyhow", | ||||
|  "cargo_toml", | ||||
| @ -3112,6 +3078,7 @@ dependencies = [ | ||||
|  "serde_json", | ||||
|  "tauri-utils", | ||||
|  "tauri-winres", | ||||
|  "winnow", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @ -3209,13 +3176,12 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "tauri-utils" | ||||
| version = "1.4.0" | ||||
| version = "1.3.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "03fc02bb6072bb397e1d473c6f76c953cda48b4a2d0cce605df284aa74a12e84" | ||||
| checksum = "5a6f9c2dafef5cbcf52926af57ce9561bd33bb41d7394f8bb849c0330260d864" | ||||
| dependencies = [ | ||||
|  "brotli", | ||||
|  "ctor", | ||||
|  "dunce", | ||||
|  "glob", | ||||
|  "heck 0.4.1", | ||||
|  "html5ever", | ||||
| @ -3431,7 +3397,7 @@ version = "0.18.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b" | ||||
| dependencies = [ | ||||
|  "indexmap 1.9.3", | ||||
|  "indexmap", | ||||
|  "nom8", | ||||
|  "serde", | ||||
|  "serde_spanned", | ||||
| @ -3444,7 +3410,7 @@ version = "0.19.8" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" | ||||
| dependencies = [ | ||||
|  "indexmap 1.9.3", | ||||
|  "indexmap", | ||||
|  "serde", | ||||
|  "serde_spanned", | ||||
|  "toml_datetime 0.6.2", | ||||
| @ -4262,14 +4228,3 @@ checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "zip" | ||||
| version = "0.6.6" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" | ||||
| dependencies = [ | ||||
|  "byteorder", | ||||
|  "crc32fast", | ||||
|  "crossbeam-utils", | ||||
| ] | ||||
|  | ||||
| @ -12,14 +12,14 @@ rust-version = "1.60" | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
|  | ||||
| [build-dependencies] | ||||
| tauri-build = { version = "1.4.0", features = [] } | ||||
| tauri-build = { version = "1.3.0", features = [] } | ||||
|  | ||||
| [dependencies] | ||||
| anyhow = "1" | ||||
| oauth2 = "4.4.1" | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
| serde_json = "1.0" | ||||
| tauri = { version = "1.3.0", features = [ "updater", "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] } | ||||
| tauri = { version = "1.3.0", features = [ "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] } | ||||
| tokio = { version = "1.29.1", features = ["time"] } | ||||
| toml = "0.6.0" | ||||
| tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } | ||||
|  | ||||
| @ -7,8 +7,8 @@ | ||||
|     "distDir": "../build" | ||||
|   }, | ||||
|   "package": { | ||||
|     "productName": "kittycad-modeling", | ||||
|     "version": "0.4.0" | ||||
|     "productName": "kittycad-modeling-app", | ||||
|     "version": "0.0.4" | ||||
|   }, | ||||
|   "tauri": { | ||||
|     "allowlist": { | ||||
|  | ||||
| @ -1,7 +0,0 @@ | ||||
|  | ||||
| { | ||||
|   "$schema": "../node_modules/@tauri-apps/cli/schema.json", | ||||
|   "package": { | ||||
|     "productName": "KittyCAD Modeling" | ||||
|   } | ||||
| } | ||||
| @ -1,7 +0,0 @@ | ||||
|  | ||||
| { | ||||
|   "$schema": "../node_modules/@tauri-apps/cli/schema.json", | ||||
|   "package": { | ||||
|     "productName": "KittyCAD Modeling" | ||||
|   } | ||||
| } | ||||
| @ -2,8 +2,7 @@ import { render, screen } from '@testing-library/react' | ||||
| import { App } from './App' | ||||
| import { describe, test, vi } from 'vitest' | ||||
| import { BrowserRouter } from 'react-router-dom' | ||||
| import { GlobalStateProvider } from './components/GlobalStateProvider' | ||||
| import CommandBarProvider from 'components/CommandBar' | ||||
| import { GlobalStateProvider } from './hooks/useAuthMachine' | ||||
|  | ||||
| let listener: ((rect: any) => void) | undefined = undefined | ||||
| ;(global as any).ResizeObserver = class ResizeObserver { | ||||
| @ -44,9 +43,7 @@ function TestWrap({ children }: { children: React.ReactNode }) { | ||||
|   // wrap in router and xState context | ||||
|   return ( | ||||
|     <BrowserRouter> | ||||
|       <CommandBarProvider> | ||||
|         <GlobalStateProvider>{children}</GlobalStateProvider> | ||||
|       </CommandBarProvider> | ||||
|       <GlobalStateProvider>{children}</GlobalStateProvider> | ||||
|     </BrowserRouter> | ||||
|   ) | ||||
| } | ||||
|  | ||||
							
								
								
									
										208
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						
									
										208
									
								
								src/App.tsx
									
									
									
									
									
								
							| @ -10,23 +10,21 @@ import { DebugPanel } from './components/DebugPanel' | ||||
| import { v4 as uuidv4 } from 'uuid' | ||||
| import { asyncParser } from './lang/abstractSyntaxTree' | ||||
| import { _executor } from './lang/executor' | ||||
| import CodeMirror, { Extension } from '@uiw/react-codemirror' | ||||
| import CodeMirror from '@uiw/react-codemirror' | ||||
| import { langs } from '@uiw/codemirror-extensions-langs' | ||||
| import { linter, lintGutter } from '@codemirror/lint' | ||||
| import { ViewUpdate, EditorView } from '@codemirror/view' | ||||
| import { ViewUpdate } from '@codemirror/view' | ||||
| import { | ||||
|   lineHighlightField, | ||||
|   addLineHighlight, | ||||
| } from './editor/highlightextension' | ||||
| import { PaneType, Selections, useStore } from './useStore' | ||||
| import Server from './editor/lsp/server' | ||||
| import Client from './editor/lsp/client' | ||||
| 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 { FromServer, IntoServer } from './editor/lsp/codec' | ||||
| import { | ||||
|   EngineCommand, | ||||
|   EngineCommandManager, | ||||
| @ -43,18 +41,14 @@ import { | ||||
| import { useHotkeys } from 'react-hotkeys-hook' | ||||
| import { TEST } from './env' | ||||
| import { getNormalisedCoordinates } from './lib/utils' | ||||
| import { Themes, getSystemTheme } from './lib/theme' | ||||
| import { getSystemTheme } from './lib/getSystemTheme' | ||||
| import { isTauri } from './lib/isTauri' | ||||
| import { useLoaderData, useParams } from 'react-router-dom' | ||||
| import { writeTextFile } from '@tauri-apps/api/fs' | ||||
| import { PROJECT_ENTRYPOINT } from './lib/tauriFS' | ||||
| import { IndexLoaderData } from './Router' | ||||
| import { toast } from 'react-hot-toast' | ||||
| import { useGlobalStateContext } from 'hooks/useGlobalStateContext' | ||||
| import { onboardingPaths } from 'routes/Onboarding' | ||||
| import { LanguageServerClient } from 'editor/lsp' | ||||
| import kclLanguage from 'editor/lsp/language' | ||||
| import { CSSRuleObject } from 'tailwindcss/types/config' | ||||
| import { useAuthMachine } from './hooks/useAuthMachine' | ||||
|  | ||||
| export function App() { | ||||
|   const { code: loadedCode, project } = useLoaderData() as IndexLoaderData | ||||
| @ -79,20 +73,23 @@ export function App() { | ||||
|     setArtifactMap, | ||||
|     engineCommandManager, | ||||
|     setEngineCommandManager, | ||||
|     highlightRange, | ||||
|     setHighlightRange, | ||||
|     setCursor2, | ||||
|     sourceRangeMap, | ||||
|     setMediaStream, | ||||
|     setIsStreamReady, | ||||
|     isStreamReady, | ||||
|     isLSPServerReady, | ||||
|     setIsLSPServerReady, | ||||
|     isMouseDownInStream, | ||||
|     cmdId, | ||||
|     setCmdId, | ||||
|     formatCode, | ||||
|     debugPanel, | ||||
|     theme, | ||||
|     openPanes, | ||||
|     setOpenPanes, | ||||
|     onboardingStatus, | ||||
|     didDragInStream, | ||||
|     setDidDragInStream, | ||||
|     setStreamDimensions, | ||||
|     streamDimensions, | ||||
|   } = useStore((s) => ({ | ||||
| @ -113,7 +110,6 @@ export function App() { | ||||
|     setArtifactMap: s.setArtifactNSourceRangeMaps, | ||||
|     engineCommandManager: s.engineCommandManager, | ||||
|     setEngineCommandManager: s.setEngineCommandManager, | ||||
|     highlightRange: s.highlightRange, | ||||
|     setHighlightRange: s.setHighlightRange, | ||||
|     isShiftDown: s.isShiftDown, | ||||
|     setCursor: s.setCursor, | ||||
| @ -122,26 +118,22 @@ export function App() { | ||||
|     setMediaStream: s.setMediaStream, | ||||
|     isStreamReady: s.isStreamReady, | ||||
|     setIsStreamReady: s.setIsStreamReady, | ||||
|     isLSPServerReady: s.isLSPServerReady, | ||||
|     setIsLSPServerReady: s.setIsLSPServerReady, | ||||
|     isMouseDownInStream: s.isMouseDownInStream, | ||||
|     cmdId: s.cmdId, | ||||
|     setCmdId: s.setCmdId, | ||||
|     formatCode: s.formatCode, | ||||
|     debugPanel: s.debugPanel, | ||||
|     addKCLError: s.addKCLError, | ||||
|     theme: s.theme, | ||||
|     openPanes: s.openPanes, | ||||
|     setOpenPanes: s.setOpenPanes, | ||||
|     onboardingStatus: s.onboardingStatus, | ||||
|     didDragInStream: s.didDragInStream, | ||||
|     setDidDragInStream: s.setDidDragInStream, | ||||
|     setStreamDimensions: s.setStreamDimensions, | ||||
|     streamDimensions: s.streamDimensions, | ||||
|   })) | ||||
|  | ||||
|   const { | ||||
|     auth: { | ||||
|       context: { token }, | ||||
|     }, | ||||
|     settings: { | ||||
|       context: { showDebugPanel, theme, onboardingStatus, textWrapping }, | ||||
|     }, | ||||
|   } = useGlobalStateContext() | ||||
|   const [token] = useAuthMachine((s) => s?.context?.token) | ||||
|  | ||||
|   const editorTheme = theme === Themes.System ? getSystemTheme() : theme | ||||
|  | ||||
| @ -160,7 +152,7 @@ export function App() { | ||||
|   useHotkeys('shift + d', () => togglePane('debug')) | ||||
|  | ||||
|   const paneOpacity = | ||||
|     onboardingStatus === onboardingPaths.CAMERA | ||||
|     onboardingStatus === 'camera' | ||||
|       ? 'opacity-20' | ||||
|       : didDragInStream | ||||
|       ? 'opacity-40' | ||||
| @ -254,12 +246,13 @@ export function App() { | ||||
|       codeBasedSelections, | ||||
|     }) | ||||
|   } | ||||
|   const pixelDensity = window.devicePixelRatio | ||||
|   const streamWidth = streamRef?.current?.offsetWidth | ||||
|   const streamHeight = streamRef?.current?.offsetHeight | ||||
|  | ||||
|   const width = streamWidth ? streamWidth : 0 | ||||
|   const width = streamWidth ? streamWidth * pixelDensity : 0 | ||||
|   const quadWidth = Math.round(width / 4) * 4 | ||||
|   const height = streamHeight ? streamHeight : 0 | ||||
|   const height = streamHeight ? streamHeight * pixelDensity : 0 | ||||
|   const quadHeight = Math.round(height / 4) * 4 | ||||
|  | ||||
|   useLayoutEffect(() => { | ||||
| @ -283,8 +276,6 @@ export function App() { | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (!isStreamReady) return | ||||
|     if (!engineCommandManager) return | ||||
|     let unsubFn: any[] = [] | ||||
|     const asyncWrap = async () => { | ||||
|       try { | ||||
|         if (!code) { | ||||
| @ -295,12 +286,27 @@ export function App() { | ||||
|         setAst(_ast) | ||||
|         resetLogs() | ||||
|         resetKCLErrors() | ||||
|         engineCommandManager.endSession() | ||||
|         engineCommandManager.startNewSession() | ||||
|         if (engineCommandManager) { | ||||
|           engineCommandManager.endSession() | ||||
|           engineCommandManager.startNewSession() | ||||
|         } | ||||
|         if (!engineCommandManager) return | ||||
|         const programMemory = await _executor( | ||||
|           _ast, | ||||
|           { | ||||
|             root: { | ||||
|               log: { | ||||
|                 type: 'userVal', | ||||
|                 value: (a: any) => { | ||||
|                   addLog(a) | ||||
|                 }, | ||||
|                 __meta: [ | ||||
|                   { | ||||
|                     pathToNode: [], | ||||
|                     sourceRange: [0, 0], | ||||
|                   }, | ||||
|                 ], | ||||
|               }, | ||||
|               _0: { | ||||
|                 type: 'userVal', | ||||
|                 value: 0, | ||||
| @ -322,40 +328,33 @@ export function App() { | ||||
|                 __meta: [], | ||||
|               }, | ||||
|             }, | ||||
|             pendingMemory: {}, | ||||
|           }, | ||||
|           engineCommandManager | ||||
|           engineCommandManager, | ||||
|           { bodyType: 'root' }, | ||||
|           [] | ||||
|         ) | ||||
|  | ||||
|         const { artifactMap, sourceRangeMap } = | ||||
|           await engineCommandManager.waitForAllCommands() | ||||
|  | ||||
|         setArtifactMap({ artifactMap, sourceRangeMap }) | ||||
|         const unSubHover = engineCommandManager.subscribeToUnreliable({ | ||||
|           event: 'highlight_set_entity', | ||||
|           callback: ({ data }) => { | ||||
|             if (data?.entity_id) { | ||||
|               const sourceRange = sourceRangeMap[data.entity_id] | ||||
|               setHighlightRange(sourceRange) | ||||
|             } else if ( | ||||
|               !highlightRange || | ||||
|               (highlightRange[0] !== 0 && highlightRange[1] !== 0) | ||||
|             ) { | ||||
|               setHighlightRange([0, 0]) | ||||
|             } | ||||
|           }, | ||||
|         engineCommandManager.onHover((id) => { | ||||
|           if (!id) { | ||||
|             setHighlightRange([0, 0]) | ||||
|           } else { | ||||
|             const sourceRange = sourceRangeMap[id] | ||||
|             setHighlightRange(sourceRange) | ||||
|           } | ||||
|         }) | ||||
|         const unSubClick = engineCommandManager.subscribeTo({ | ||||
|           event: 'select_with_point', | ||||
|           callback: ({ data }) => { | ||||
|             if (!data?.entity_id) { | ||||
|               setCursor2() | ||||
|               return | ||||
|             } | ||||
|             const sourceRange = sourceRangeMap[data.entity_id] | ||||
|             setCursor2({ range: sourceRange, type: 'default' }) | ||||
|           }, | ||||
|         engineCommandManager.onClick((selections) => { | ||||
|           if (!selections) { | ||||
|             setCursor2() | ||||
|             return | ||||
|           } | ||||
|           const { id, type } = selections | ||||
|           setCursor2({ range: sourceRangeMap[id], type }) | ||||
|         }) | ||||
|         unsubFn.push(unSubHover, unSubClick) | ||||
|         if (programMemory !== undefined) { | ||||
|           setProgramMemory(programMemory) | ||||
|         } | ||||
| @ -372,10 +371,7 @@ export function App() { | ||||
|       } | ||||
|     } | ||||
|     asyncWrap() | ||||
|     return () => { | ||||
|       unsubFn.forEach((fn) => fn()) | ||||
|     } | ||||
|   }, [code, isStreamReady, engineCommandManager]) | ||||
|   }, [code, isStreamReady]) | ||||
|  | ||||
|   const debounceSocketSend = throttle<EngineCommand>((message) => { | ||||
|     engineCommandManager?.sendSceneCommand(message) | ||||
| @ -389,6 +385,9 @@ export function App() { | ||||
|     nativeEvent, | ||||
|   }) => { | ||||
|     nativeEvent.preventDefault() | ||||
|     if (isMouseDownInStream) { | ||||
|       setDidDragInStream(true) | ||||
|     } | ||||
|  | ||||
|     const { x, y } = getNormalisedCoordinates({ | ||||
|       clientX, | ||||
| @ -400,8 +399,9 @@ export function App() { | ||||
|     const interaction = ctrlKey ? 'zoom' : shiftKey ? 'pan' : 'rotate' | ||||
|  | ||||
|     const newCmdId = uuidv4() | ||||
|     setCmdId(newCmdId) | ||||
|  | ||||
|     if (isMouseDownInStream) { | ||||
|     if (cmdId && isMouseDownInStream) { | ||||
|       debounceSocketSend({ | ||||
|         type: 'modeling_cmd_req', | ||||
|         cmd: { | ||||
| @ -423,63 +423,15 @@ export function App() { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // So this is a bit weird, we need to initialize the lsp server and client. | ||||
|   // But the server happens async so we break this into two parts. | ||||
|   // Below is the client and server promise. | ||||
|   const { lspClient } = useMemo(() => { | ||||
|     const intoServer: IntoServer = new IntoServer() | ||||
|     const fromServer: FromServer = FromServer.create() | ||||
|     const client = new Client(fromServer, intoServer) | ||||
|     if (!TEST) { | ||||
|       Server.initialize(intoServer, fromServer).then((lspServer) => { | ||||
|         lspServer.start() | ||||
|         setIsLSPServerReady(true) | ||||
|       }) | ||||
|     } | ||||
|  | ||||
|     const lspClient = new LanguageServerClient({ client }) | ||||
|     return { lspClient } | ||||
|   }, [setIsLSPServerReady]) | ||||
|  | ||||
|   // Here we initialize the plugin which will start the client. | ||||
|   // When we have multi-file support the name of the file will be a dep of | ||||
|   // this use memo, as well as the directory structure, which I think is | ||||
|   // a good setup becuase it will restart the client but not the server :) | ||||
|   // We do not want to restart the server, its just wasteful. | ||||
|   const kclLSP = useMemo(() => { | ||||
|     let plugin = null | ||||
|     if (isLSPServerReady && !TEST) { | ||||
|       // Set up the lsp plugin. | ||||
|       const lsp = kclLanguage({ | ||||
|         // When we have more than one file, we'll need to change this. | ||||
|         documentUri: `file:///we-just-have-one-file-for-now.kcl`, | ||||
|         workspaceFolders: null, | ||||
|         client: lspClient, | ||||
|       }) | ||||
|  | ||||
|       plugin = lsp | ||||
|     } | ||||
|     return plugin | ||||
|   }, [lspClient, isLSPServerReady]) | ||||
|  | ||||
|   const editorExtensions = useMemo(() => { | ||||
|     const extensions = [lineHighlightField] as Extension[] | ||||
|  | ||||
|     if (kclLSP) extensions.push(kclLSP) | ||||
|  | ||||
|     // These extensions have proven to mess with vitest | ||||
|     if (!TEST) { | ||||
|       extensions.push( | ||||
|         lintGutter(), | ||||
|         linter((_view) => { | ||||
|           return kclErrToDiagnostic(useStore.getState().kclErrors) | ||||
|         }) | ||||
|       ) | ||||
|       if (textWrapping === 'On') extensions.push(EditorView.lineWrapping) | ||||
|     } | ||||
|  | ||||
|     return extensions | ||||
|   }, [kclLSP, textWrapping]) | ||||
|   const extraExtensions = useMemo(() => { | ||||
|     if (TEST) return [] | ||||
|     return [ | ||||
|       lintGutter(), | ||||
|       linter((_view) => { | ||||
|         return kclErrToDiagnostic(useStore.getState().kclErrors) | ||||
|       }), | ||||
|     ] | ||||
|   }, []) | ||||
|  | ||||
|   return ( | ||||
|     <div | ||||
| @ -534,15 +486,15 @@ export function App() { | ||||
|                 format | ||||
|               </button> | ||||
|             </div> | ||||
|             <div | ||||
|               id="code-mirror-override" | ||||
|               className="full-height-subtract" | ||||
|               style={{ '--height-subtract': '4.25rem' } as CSSRuleObject} | ||||
|             > | ||||
|             <div id="code-mirror-override"> | ||||
|               <CodeMirror | ||||
|                 className="h-full" | ||||
|                 value={code} | ||||
|                 extensions={editorExtensions} | ||||
|                 extensions={[ | ||||
|                   langs.javascript({ jsx: true }), | ||||
|                   lineHighlightField, | ||||
|                   ...extraExtensions, | ||||
|                 ]} | ||||
|                 onChange={onChange} | ||||
|                 onUpdate={onUpdate} | ||||
|                 theme={editorTheme} | ||||
| @ -573,7 +525,7 @@ export function App() { | ||||
|         </div> | ||||
|       </Resizable> | ||||
|       <Stream className="absolute inset-0 z-0" /> | ||||
|       {showDebugPanel && ( | ||||
|       {debugPanel && ( | ||||
|         <DebugPanel | ||||
|           title="Debug" | ||||
|           className={ | ||||
|  | ||||
| @ -1,12 +1,9 @@ | ||||
| import Loading from './components/Loading' | ||||
| import { useGlobalStateContext } from 'hooks/useGlobalStateContext' | ||||
| import { useAuthMachine } from './hooks/useAuthMachine' | ||||
|  | ||||
| // Wrapper around protected routes, used in src/Router.tsx | ||||
| export const Auth = ({ children }: React.PropsWithChildren) => { | ||||
|   const { | ||||
|     auth: { state }, | ||||
|   } = useGlobalStateContext() | ||||
|   const isLoggedIn = state.matches('checkIfLoggedIn') | ||||
|   const [isLoggedIn] = useAuthMachine((s) => s.matches('checkIfLoggedIn')) | ||||
|  | ||||
|   return isLoggedIn ? ( | ||||
|     <Loading>Loading KittyCAD Modeling App...</Loading> | ||||
|  | ||||
							
								
								
									
										112
									
								
								src/Router.tsx
									
									
									
									
									
								
							
							
						
						
									
										112
									
								
								src/Router.tsx
									
									
									
									
									
								
							| @ -3,15 +3,8 @@ import { | ||||
|   createBrowserRouter, | ||||
|   Outlet, | ||||
|   redirect, | ||||
|   useLocation, | ||||
|   RouterProvider, | ||||
| } from 'react-router-dom' | ||||
| import { | ||||
|   matchRoutes, | ||||
|   createRoutesFromChildren, | ||||
|   useNavigationType, | ||||
| } from 'react-router' | ||||
| import { useEffect } from 'react' | ||||
| import { ErrorPage } from './components/ErrorPage' | ||||
| import { Settings } from './routes/Settings' | ||||
| import Onboarding, { | ||||
| @ -31,47 +24,7 @@ import { | ||||
| } from './lib/tauriFS' | ||||
| import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api' | ||||
| import DownloadAppBanner from './components/DownloadAppBanner' | ||||
| import { GlobalStateProvider } from './components/GlobalStateProvider' | ||||
| import { | ||||
|   SETTINGS_PERSIST_KEY, | ||||
|   settingsMachine, | ||||
| } from './machines/settingsMachine' | ||||
| import { ContextFrom } from 'xstate' | ||||
| import CommandBarProvider from 'components/CommandBar' | ||||
| import { TEST, VITE_KC_SENTRY_DSN } from './env' | ||||
| import * as Sentry from '@sentry/react' | ||||
|  | ||||
| if (VITE_KC_SENTRY_DSN && !TEST) { | ||||
|   Sentry.init({ | ||||
|     dsn: VITE_KC_SENTRY_DSN, | ||||
|     // TODO(paultag): pass in the right env here. | ||||
|     // environment: "production", | ||||
|     integrations: [ | ||||
|       new Sentry.BrowserTracing({ | ||||
|         routingInstrumentation: Sentry.reactRouterV6Instrumentation( | ||||
|           useEffect, | ||||
|           useLocation, | ||||
|           useNavigationType, | ||||
|           createRoutesFromChildren, | ||||
|           matchRoutes | ||||
|         ), | ||||
|       }), | ||||
|       new Sentry.Replay(), | ||||
|     ], | ||||
|  | ||||
|     // Set tracesSampleRate to 1.0 to capture 100% | ||||
|     // of transactions for performance monitoring. | ||||
|     tracesSampleRate: 1.0, | ||||
|  | ||||
|     // TODO: Add in kittycad.io endpoints | ||||
|     tracePropagationTargets: ['localhost'], | ||||
|  | ||||
|     // Capture Replay for 10% of all sessions, | ||||
|     // plus for 100% of sessions with an error | ||||
|     replaysSessionSampleRate: 0.1, | ||||
|     replaysOnErrorSampleRate: 1.0, | ||||
|   }) | ||||
| } | ||||
| import { GlobalStateProvider } from './hooks/useAuthMachine' | ||||
|  | ||||
| const prependRoutes = | ||||
|   (routesObject: Record<string, string>) => (prepend: string) => { | ||||
| @ -115,11 +68,7 @@ const addGlobalContextToElements = ( | ||||
|     'element' in route | ||||
|       ? { | ||||
|           ...route, | ||||
|           element: ( | ||||
|             <CommandBarProvider> | ||||
|               <GlobalStateProvider>{route.element}</GlobalStateProvider> | ||||
|             </CommandBarProvider> | ||||
|           ), | ||||
|           element: <GlobalStateProvider>{route.element}</GlobalStateProvider>, | ||||
|         } | ||||
|       : route | ||||
|   ) | ||||
| @ -146,25 +95,26 @@ const router = createBrowserRouter( | ||||
|         request, | ||||
|         params, | ||||
|       }): Promise<IndexLoaderData | Response> => { | ||||
|         const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY) | ||||
|         const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial< | ||||
|           ContextFrom<typeof settingsMachine> | ||||
|         > | ||||
|         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 | ||||
|  | ||||
|         const status = persistedSettings.onboardingStatus || '' | ||||
|         const notEnRouteToOnboarding = !request.url.includes( | ||||
|           paths.ONBOARDING.INDEX | ||||
|         ) | ||||
|         // '' is the initial state, 'done' and 'dismissed' are the final states | ||||
|         const hasValidOnboardingStatus = | ||||
|           status.length === 0 || !(status === 'done' || status === 'dismissed') | ||||
|         const shouldRedirectToOnboarding = | ||||
|           notEnRouteToOnboarding && hasValidOnboardingStatus | ||||
|  | ||||
|         if (shouldRedirectToOnboarding) { | ||||
|           return redirect( | ||||
|             makeUrlPathRelative(paths.ONBOARDING.INDEX) + status.slice(1) | ||||
|           ) | ||||
|           if (shouldRedirectToOnboarding) { | ||||
|             return redirect( | ||||
|               makeUrlPathRelative(paths.ONBOARDING.INDEX) + status | ||||
|             ) | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         if (params.id && params.id !== 'new') { | ||||
| @ -214,23 +164,9 @@ const router = createBrowserRouter( | ||||
|         if (!isTauri()) { | ||||
|           return redirect(paths.FILE + '/new') | ||||
|         } | ||||
|         const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY) | ||||
|         const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial< | ||||
|           ContextFrom<typeof settingsMachine> | ||||
|         > | ||||
|         const projectDir = await initializeProjectDirectory( | ||||
|           persistedSettings.defaultDirectory || '' | ||||
|         ) | ||||
|         if (projectDir !== persistedSettings.defaultDirectory) { | ||||
|           localStorage.setItem( | ||||
|             SETTINGS_PERSIST_KEY, | ||||
|             JSON.stringify({ | ||||
|               ...persistedSettings, | ||||
|               defaultDirectory: projectDir, | ||||
|             }) | ||||
|           ) | ||||
|         } | ||||
|         const projectsNoMeta = (await readDir(projectDir)).filter( | ||||
|  | ||||
|         const projectDir = await initializeProjectDirectory() | ||||
|         const projectsNoMeta = (await readDir(projectDir.dir)).filter( | ||||
|           isProjectDirectory | ||||
|         ) | ||||
|         const projects = await Promise.all( | ||||
|  | ||||
| @ -1,60 +0,0 @@ | ||||
| .toolbarWrapper { | ||||
|   @apply relative; | ||||
| } | ||||
|  | ||||
| .toolbar { | ||||
|   @apply flex gap-4 items-center rounded-full; | ||||
|   @apply border border-cool-20/30 bg-cool-10/50; | ||||
| } | ||||
|  | ||||
| :global(.dark) .toolbar { | ||||
|   @apply border-cool-100/50 bg-cool-120/50; | ||||
| } | ||||
|  | ||||
| :global(.sketch) .toolbar { | ||||
|   @apply border-fern-20/20 bg-fern-10/20; | ||||
| } | ||||
|  | ||||
| :global(.dark .sketch) .toolbar { | ||||
|   @apply border-fern-120/50 bg-fern-100/30; | ||||
| } | ||||
|  | ||||
| .toolbarCap { | ||||
|   @apply text-sm font-bold; | ||||
|   @apply bg-cool-20/50 text-cool-100; | ||||
| } | ||||
|  | ||||
| :global(.dark) .toolbarCap { | ||||
|   @apply bg-cool-90/50 text-cool-30; | ||||
| } | ||||
|  | ||||
| :global(.sketch) .toolbarCap { | ||||
|   @apply bg-fern-20/50 text-fern-100; | ||||
| } | ||||
|  | ||||
| :global(.dark .sketch) .toolbarCap { | ||||
|   @apply bg-fern-90/50 text-fern-30; | ||||
| } | ||||
|  | ||||
| .label { | ||||
|   @apply self-stretch flex items-center px-4 py-1; | ||||
|   @apply rounded-l-full; | ||||
| } | ||||
|  | ||||
| .popoverToggle { | ||||
|   @apply self-stretch m-0 flex items-center px-4 py-1; | ||||
|   @apply rounded-r-full border-none; | ||||
|   @apply hover:bg-cool-20; | ||||
| } | ||||
|  | ||||
| :global(.dark) .popoverToggle { | ||||
|   @apply hover:bg-cool-90; | ||||
| } | ||||
|  | ||||
| :global(.sketch) .popoverToggle { | ||||
|   @apply hover:bg-fern-20; | ||||
| } | ||||
|  | ||||
| :global(.dark .sketch) .popoverToggle { | ||||
|   @apply hover:bg-fern-90; | ||||
| } | ||||
							
								
								
									
										314
									
								
								src/Toolbar.tsx
									
									
									
									
									
								
							
							
						
						
									
										314
									
								
								src/Toolbar.tsx
									
									
									
									
									
								
							| @ -11,11 +11,6 @@ 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 { Fragment, useEffect } from 'react' | ||||
| import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' | ||||
| import { faSearch, faX } from '@fortawesome/free-solid-svg-icons' | ||||
| import { Popover, Transition } from '@headlessui/react' | ||||
| import styles from './Toolbar.module.css' | ||||
|  | ||||
| export const Toolbar = () => { | ||||
|   const { | ||||
| @ -34,26 +29,56 @@ export const Toolbar = () => { | ||||
|     programMemory: s.programMemory, | ||||
|   })) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     console.log('guiMode', guiMode) | ||||
|   }, [guiMode]) | ||||
|  | ||||
|   function ToolbarButtons() { | ||||
|     return ( | ||||
|       <> | ||||
|         {guiMode.mode === 'default' && ( | ||||
|           <button | ||||
|             onClick={() => { | ||||
|               setGuiMode({ | ||||
|                 mode: 'sketch', | ||||
|                 sketchMode: 'selectFace', | ||||
|               }) | ||||
|             }} | ||||
|           > | ||||
|             Start Sketch | ||||
|           </button> | ||||
|         )} | ||||
|         {guiMode.mode === 'canEditExtrude' && ( | ||||
|   return ( | ||||
|     <div> | ||||
|       {guiMode.mode === 'default' && ( | ||||
|         <button | ||||
|           onClick={() => { | ||||
|             setGuiMode({ | ||||
|               mode: 'sketch', | ||||
|               sketchMode: 'selectFace', | ||||
|             }) | ||||
|           }} | ||||
|         > | ||||
|           Start Sketch | ||||
|         </button> | ||||
|       )} | ||||
|       {guiMode.mode === 'canEditExtrude' && ( | ||||
|         <button | ||||
|           onClick={() => { | ||||
|             if (!ast) return | ||||
|             const pathToNode = getNodePathFromSourceRange( | ||||
|               ast, | ||||
|               selectionRanges.codeBasedSelections[0].range | ||||
|             ) | ||||
|             const { modifiedAst } = sketchOnExtrudedFace( | ||||
|               ast, | ||||
|               pathToNode, | ||||
|               programMemory | ||||
|             ) | ||||
|             updateAst(modifiedAst) | ||||
|           }} | ||||
|         > | ||||
|           SketchOnFace | ||||
|         </button> | ||||
|       )} | ||||
|       {(guiMode.mode === 'canEditSketch' || false) && ( | ||||
|         <button | ||||
|           onClick={() => { | ||||
|             setGuiMode({ | ||||
|               mode: 'sketch', | ||||
|               sketchMode: 'sketchEdit', | ||||
|               pathToNode: guiMode.pathToNode, | ||||
|               rotation: guiMode.rotation, | ||||
|               position: guiMode.position, | ||||
|             }) | ||||
|           }} | ||||
|         > | ||||
|           Edit Sketch | ||||
|         </button> | ||||
|       )} | ||||
|       {guiMode.mode === 'canEditSketch' && ( | ||||
|         <> | ||||
|           <button | ||||
|             onClick={() => { | ||||
|               if (!ast) return | ||||
| @ -61,182 +86,93 @@ export const Toolbar = () => { | ||||
|                 ast, | ||||
|                 selectionRanges.codeBasedSelections[0].range | ||||
|               ) | ||||
|               const { modifiedAst } = sketchOnExtrudedFace( | ||||
|               const { modifiedAst, pathToExtrudeArg } = extrudeSketch( | ||||
|                 ast, | ||||
|                 pathToNode, | ||||
|                 programMemory | ||||
|                 pathToNode | ||||
|               ) | ||||
|               updateAst(modifiedAst) | ||||
|               updateAst(modifiedAst, { focusPath: pathToExtrudeArg }) | ||||
|             }} | ||||
|           > | ||||
|             SketchOnFace | ||||
|             ExtrudeSketch | ||||
|           </button> | ||||
|         )} | ||||
|         {(guiMode.mode === 'canEditSketch' || false) && ( | ||||
|           <button | ||||
|             onClick={() => { | ||||
|               setGuiMode({ | ||||
|                 mode: 'sketch', | ||||
|                 sketchMode: 'sketchEdit', | ||||
|                 pathToNode: guiMode.pathToNode, | ||||
|                 rotation: guiMode.rotation, | ||||
|                 position: guiMode.position, | ||||
|               }) | ||||
|               if (!ast) return | ||||
|               const pathToNode = getNodePathFromSourceRange( | ||||
|                 ast, | ||||
|                 selectionRanges.codeBasedSelections[0].range | ||||
|               ) | ||||
|               const { modifiedAst, pathToExtrudeArg } = extrudeSketch( | ||||
|                 ast, | ||||
|                 pathToNode, | ||||
|                 false | ||||
|               ) | ||||
|               updateAst(modifiedAst, { focusPath: pathToExtrudeArg }) | ||||
|             }} | ||||
|           > | ||||
|             Edit Sketch | ||||
|             ExtrudeSketch (w/o pipe) | ||||
|           </button> | ||||
|         )} | ||||
|         {guiMode.mode === 'canEditSketch' && ( | ||||
|           <> | ||||
|             <button | ||||
|               onClick={() => { | ||||
|                 if (!ast) return | ||||
|                 const pathToNode = getNodePathFromSourceRange( | ||||
|                   ast, | ||||
|                   selectionRanges.codeBasedSelections[0].range | ||||
|                 ) | ||||
|                 const { modifiedAst, pathToExtrudeArg } = extrudeSketch( | ||||
|                   ast, | ||||
|                   pathToNode | ||||
|                 ) | ||||
|                 updateAst(modifiedAst, { focusPath: pathToExtrudeArg }) | ||||
|               }} | ||||
|             > | ||||
|               ExtrudeSketch | ||||
|             </button> | ||||
|             <button | ||||
|               onClick={() => { | ||||
|                 if (!ast) return | ||||
|                 const pathToNode = getNodePathFromSourceRange( | ||||
|                   ast, | ||||
|                   selectionRanges.codeBasedSelections[0].range | ||||
|                 ) | ||||
|                 const { modifiedAst, pathToExtrudeArg } = extrudeSketch( | ||||
|                   ast, | ||||
|                   pathToNode, | ||||
|                   false | ||||
|                 ) | ||||
|                 updateAst(modifiedAst, { focusPath: pathToExtrudeArg }) | ||||
|               }} | ||||
|             > | ||||
|               ExtrudeSketch (w/o pipe) | ||||
|             </button> | ||||
|           </> | ||||
|         )} | ||||
|         </> | ||||
|       )} | ||||
|  | ||||
|         {guiMode.mode === 'sketch' && ( | ||||
|           <button onClick={() => setGuiMode({ mode: 'default' })}> | ||||
|             Exit sketch | ||||
|           </button> | ||||
|         )} | ||||
|         {toolTips | ||||
|           .filter( | ||||
|             // (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName) | ||||
|             (sketchFnName) => ['line'].includes(sketchFnName) | ||||
|       {guiMode.mode === 'sketch' && ( | ||||
|         <button onClick={() => setGuiMode({ mode: 'default' })}> | ||||
|           Exit sketch | ||||
|         </button> | ||||
|       )} | ||||
|       {toolTips | ||||
|         .filter( | ||||
|           // (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName) | ||||
|           (sketchFnName) => ['line'].includes(sketchFnName) | ||||
|         ) | ||||
|         .map((sketchFnName) => { | ||||
|           if ( | ||||
|             guiMode.mode !== 'sketch' || | ||||
|             !('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit') | ||||
|           ) | ||||
|           .map((sketchFnName) => { | ||||
|             if ( | ||||
|               guiMode.mode !== 'sketch' || | ||||
|               !('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit') | ||||
|             ) | ||||
|               return null | ||||
|             return ( | ||||
|               <button | ||||
|                 key={sketchFnName} | ||||
|                 onClick={() => | ||||
|                   setGuiMode({ | ||||
|                     ...guiMode, | ||||
|                     ...(guiMode.sketchMode === sketchFnName | ||||
|                       ? { | ||||
|                           sketchMode: 'sketchEdit', | ||||
|                           // todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion | ||||
|                         } | ||||
|                       : { | ||||
|                           sketchMode: sketchFnName, | ||||
|                           isTooltip: true, | ||||
|                         }), | ||||
|                   }) | ||||
|                 } | ||||
|               > | ||||
|                 {sketchFnName} | ||||
|                 {guiMode.sketchMode === sketchFnName && '✅'} | ||||
|               </button> | ||||
|             ) | ||||
|           })} | ||||
|         <ConvertToVariable /> | ||||
|         <HorzVert horOrVert="horizontal" /> | ||||
|         <HorzVert horOrVert="vertical" /> | ||||
|         <EqualLength /> | ||||
|         <EqualAngle /> | ||||
|         <SetHorzVertDistance buttonType="alignEndsVertically" /> | ||||
|         <SetHorzVertDistance buttonType="setHorzDistance" /> | ||||
|         <SetAbsDistance buttonType="snapToYAxis" /> | ||||
|         <SetAbsDistance buttonType="xAbs" /> | ||||
|         <SetHorzVertDistance buttonType="alignEndsHorizontally" /> | ||||
|         <SetAbsDistance buttonType="snapToXAxis" /> | ||||
|         <SetHorzVertDistance buttonType="setVertDistance" /> | ||||
|         <SetAbsDistance buttonType="yAbs" /> | ||||
|         <SetAngleLength angleOrLength="setAngle" /> | ||||
|         <SetAngleLength angleOrLength="setLength" /> | ||||
|         <Intersect /> | ||||
|         <RemoveConstrainingValues /> | ||||
|         <SetAngleBetween /> | ||||
|       </> | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Popover className={styles.toolbarWrapper + ' ' + guiMode.mode}> | ||||
|       <div className={styles.toolbar}> | ||||
|         <span className={styles.toolbarCap + ' ' + styles.label}> | ||||
|           {guiMode.mode === 'sketch' ? '2D' : '3D'} | ||||
|         </span> | ||||
|         <menu className="flex flex-1 gap-2 py-0.5 overflow-hidden whitespace-nowrap"> | ||||
|           <ToolbarButtons /> | ||||
|         </menu> | ||||
|         <Popover.Button | ||||
|           className={styles.toolbarCap + ' ' + styles.popoverToggle} | ||||
|         > | ||||
|           <FontAwesomeIcon icon={faSearch} /> | ||||
|         </Popover.Button> | ||||
|       </div> | ||||
|       <Transition | ||||
|         as={Fragment} | ||||
|         enter="transition ease-out duration-200" | ||||
|         enterFrom="opacity-0" | ||||
|         enterTo="opacity-100" | ||||
|         leave="transition ease-out duration-100" | ||||
|         leaveFrom="opacity-100" | ||||
|         leaveTo="opacity-0" | ||||
|       > | ||||
|         <Popover.Overlay className="fixed inset-0 bg-chalkboard-110/20 dark:bg-chalkboard-110/50" /> | ||||
|       </Transition> | ||||
|       <Transition | ||||
|         as={Fragment} | ||||
|         enter="transition ease-out duration-100" | ||||
|         enterFrom="opacity-0 translate-y-1 scale-95" | ||||
|         enterTo="opacity-100 translate-y-0 scale-100" | ||||
|         leave="transition ease-out duration-75" | ||||
|         leaveFrom="opacity-100 translate-y-0" | ||||
|         leaveTo="opacity-0 translate-y-2" | ||||
|       > | ||||
|         <Popover.Panel className="absolute top-0 w-screen max-w-xl left-1/2 -translate-x-1/2 flex flex-col gap-8 bg-chalkboard-10 dark:bg-chalkboard-100 p-5 rounded border border-chalkboard-20/30 dark:border-chalkboard-70/50"> | ||||
|           <section className="flex justify-between items-center"> | ||||
|             <p | ||||
|               className={`${styles.toolbarCap} ${styles.label} !self-center rounded-r-full w-fit`} | ||||
|             return null | ||||
|           return ( | ||||
|             <button | ||||
|               key={sketchFnName} | ||||
|               onClick={() => | ||||
|                 setGuiMode({ | ||||
|                   ...guiMode, | ||||
|                   ...(guiMode.sketchMode === sketchFnName | ||||
|                     ? { | ||||
|                         sketchMode: 'sketchEdit', | ||||
|                         // todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion | ||||
|                       } | ||||
|                     : { | ||||
|                         sketchMode: sketchFnName, | ||||
|                         isTooltip: true, | ||||
|                       }), | ||||
|                 }) | ||||
|               } | ||||
|             > | ||||
|               You're in {guiMode.mode === 'sketch' ? '2D' : '3D'} | ||||
|             </p> | ||||
|             <Popover.Button className="p-2 flex items-center justify-center rounded-sm bg-chalkboard-20 text-chalkboard-110 dark:bg-chalkboard-70 dark:text-chalkboard-20 border-none hover:bg-chalkboard-30 dark:hover:bg-chalkboard-60"> | ||||
|               <FontAwesomeIcon icon={faX} className="w-4 h-4" /> | ||||
|             </Popover.Button> | ||||
|           </section> | ||||
|           <section> | ||||
|             <ToolbarButtons /> | ||||
|           </section> | ||||
|         </Popover.Panel> | ||||
|       </Transition> | ||||
|     </Popover> | ||||
|               {sketchFnName} | ||||
|               {guiMode.sketchMode === sketchFnName && '✅'} | ||||
|             </button> | ||||
|           ) | ||||
|         })} | ||||
|       <br></br> | ||||
|       <ConvertToVariable /> | ||||
|       <HorzVert horOrVert="horizontal" /> | ||||
|       <HorzVert horOrVert="vertical" /> | ||||
|       <EqualLength /> | ||||
|       <EqualAngle /> | ||||
|       <SetHorzVertDistance buttonType="alignEndsVertically" /> | ||||
|       <SetHorzVertDistance buttonType="setHorzDistance" /> | ||||
|       <SetAbsDistance buttonType="snapToYAxis" /> | ||||
|       <SetAbsDistance buttonType="xAbs" /> | ||||
|       <SetHorzVertDistance buttonType="alignEndsHorizontally" /> | ||||
|       <SetAbsDistance buttonType="snapToXAxis" /> | ||||
|       <SetHorzVertDistance buttonType="setVertDistance" /> | ||||
|       <SetAbsDistance buttonType="yAbs" /> | ||||
|       <SetAngleLength angleOrLength="setAngle" /> | ||||
|       <SetAngleLength angleOrLength="setLength" /> | ||||
|       <Intersect /> | ||||
|       <RemoveConstrainingValues /> | ||||
|       <SetAngleBetween /> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -8,13 +8,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' | ||||
| const iconSizes = { | ||||
|   sm: 12, | ||||
|   md: 14.4, | ||||
|   lg: 20, | ||||
|   xl: 28, | ||||
|   lg: 18, | ||||
| } | ||||
|  | ||||
| export interface ActionIconProps extends React.PropsWithChildren { | ||||
|   icon?: SolidIconDefinition | BrandIconDefinition | ||||
|   className?: string | ||||
|   bgClassName?: string | ||||
|   iconClassName?: string | ||||
|   size?: keyof typeof iconSizes | ||||
| @ -22,7 +20,6 @@ export interface ActionIconProps extends React.PropsWithChildren { | ||||
|  | ||||
| export const ActionIcon = ({ | ||||
|   icon = faCircleExclamation, | ||||
|   className, | ||||
|   bgClassName, | ||||
|   iconClassName, | ||||
|   size = 'md', | ||||
| @ -31,9 +28,7 @@ export const ActionIcon = ({ | ||||
|   return ( | ||||
|     <div | ||||
|       className={ | ||||
|         `p-${ | ||||
|           size === 'xl' ? '2' : '1' | ||||
|         } w-fit inline-grid place-content-center ${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') | ||||
|       } | ||||
| @ -45,7 +40,7 @@ export const ActionIcon = ({ | ||||
|           height={iconSizes[size]} | ||||
|           className={ | ||||
|             iconClassName || | ||||
|             'text-liquid-20 h-auto group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100' | ||||
|             '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' | ||||
|           } | ||||
|         /> | ||||
|       )} | ||||
|  | ||||
| @ -1,7 +0,0 @@ | ||||
| /* | ||||
|   Some CSS cannot be represented | ||||
|   in Tailwind, such as complex grid layouts. | ||||
|  */ | ||||
| .header { | ||||
|   grid-template-columns: 1fr auto 1fr; | ||||
| } | ||||
| @ -2,8 +2,7 @@ import { Toolbar } from '../Toolbar' | ||||
| import UserSidebarMenu from './UserSidebarMenu' | ||||
| import { ProjectWithEntryPointMetadata } from '../Router' | ||||
| import ProjectSidebarMenu from './ProjectSidebarMenu' | ||||
| import { useGlobalStateContext } from 'hooks/useGlobalStateContext' | ||||
| import styles from './AppHeader.module.css' | ||||
| import { useAuthMachine } from '../hooks/useAuthMachine' | ||||
|  | ||||
| interface AppHeaderProps extends React.PropsWithChildren { | ||||
|   showToolbar?: boolean | ||||
| @ -19,18 +18,12 @@ export const AppHeader = ({ | ||||
|   className = '', | ||||
|   enableMenu = false, | ||||
| }: AppHeaderProps) => { | ||||
|   const { | ||||
|     auth: { | ||||
|       context: { user }, | ||||
|     }, | ||||
|   } = useGlobalStateContext() | ||||
|   const [user] = useAuthMachine((s) => s?.context?.user) | ||||
|  | ||||
|   return ( | ||||
|     <header | ||||
|       className={ | ||||
|         (showToolbar ? 'grid ' : 'flex justify-between ') + | ||||
|         styles.header + | ||||
|         ' overlaid-panes sticky top-0 z-20 py-1 px-5 bg-chalkboard-10/70 dark:bg-chalkboard-100/50 border-b dark:border-b-2 border-chalkboard-30 dark:border-chalkboard-90 items-center ' + | ||||
|         'overlaid-panes sticky top-0 z-20 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 | ||||
|       } | ||||
|     > | ||||
| @ -42,11 +35,7 @@ export const AppHeader = ({ | ||||
|         </div> | ||||
|       )} | ||||
|       {/* If there are children, show them, otherwise show User menu */} | ||||
|       {children || ( | ||||
|         <div className="ml-auto"> | ||||
|           <UserSidebarMenu user={user} /> | ||||
|         </div> | ||||
|       )} | ||||
|       {children || <UserSidebarMenu user={user} />} | ||||
|     </header> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| .panel { | ||||
|   @apply relative z-0; | ||||
|   @apply bg-chalkboard-10/70 backdrop-blur-sm; | ||||
|   @apply relative overflow-auto z-0; | ||||
|   @apply bg-chalkboard-20/40; | ||||
| } | ||||
|  | ||||
| :global(.dark) .panel { | ||||
|   @apply bg-chalkboard-110/50 backdrop-blur-0; | ||||
|   @apply bg-chalkboard-110/50; | ||||
| } | ||||
|  | ||||
| .header { | ||||
|  | ||||
| @ -1,290 +0,0 @@ | ||||
| import { Combobox, Dialog, Transition } from '@headlessui/react' | ||||
| import { | ||||
|   Dispatch, | ||||
|   Fragment, | ||||
|   SetStateAction, | ||||
|   createContext, | ||||
|   useState, | ||||
| } from 'react' | ||||
| import { useHotkeys } from 'react-hotkeys-hook' | ||||
| import { ActionIcon } from './ActionIcon' | ||||
| import { faSearch } from '@fortawesome/free-solid-svg-icons' | ||||
| import Fuse from 'fuse.js' | ||||
| import { Command, SubCommand } from '../lib/commands' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
|  | ||||
| export type SortedCommand = { | ||||
|   item: Partial<Command | SubCommand> & { name: string } | ||||
| } | ||||
|  | ||||
| export const CommandsContext = createContext( | ||||
|   {} as { | ||||
|     commands: Command[] | ||||
|     addCommands: (commands: Command[]) => void | ||||
|     removeCommands: (commands: Command[]) => void | ||||
|     commandBarOpen: boolean | ||||
|     setCommandBarOpen: Dispatch<SetStateAction<boolean>> | ||||
|   } | ||||
| ) | ||||
|  | ||||
| export const CommandBarProvider = ({ | ||||
|   children, | ||||
| }: { | ||||
|   children: React.ReactNode | ||||
| }) => { | ||||
|   const [commands, internalSetCommands] = useState([] as Command[]) | ||||
|   const [commandBarOpen, setCommandBarOpen] = useState(false) | ||||
|  | ||||
|   const addCommands = (newCommands: Command[]) => { | ||||
|     internalSetCommands((prevCommands) => [...newCommands, ...prevCommands]) | ||||
|   } | ||||
|   const removeCommands = (newCommands: Command[]) => { | ||||
|     internalSetCommands((prevCommands) => | ||||
|       prevCommands.filter((command) => !newCommands.includes(command)) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <CommandsContext.Provider | ||||
|       value={{ | ||||
|         commands, | ||||
|         addCommands, | ||||
|         removeCommands, | ||||
|         commandBarOpen, | ||||
|         setCommandBarOpen, | ||||
|       }} | ||||
|     > | ||||
|       {children} | ||||
|       <CommandBar /> | ||||
|     </CommandsContext.Provider> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const CommandBar = () => { | ||||
|   const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext() | ||||
|   useHotkeys('meta+k', () => { | ||||
|     if (commands.length === 0) return | ||||
|     setCommandBarOpen(!commandBarOpen) | ||||
|   }) | ||||
|  | ||||
|   const [selectedCommand, setSelectedCommand] = useState<SortedCommand | null>( | ||||
|     null | ||||
|   ) | ||||
|   // keep track of the current subcommand index | ||||
|   const [subCommandIndex, setSubCommandIndex] = useState<number>() | ||||
|   const [subCommandData, setSubCommandData] = useState<{ | ||||
|     [key: string]: string | ||||
|   }>({}) | ||||
|  | ||||
|   // if the subcommand index is null, we're not in a subcommand | ||||
|   const inSubCommand = | ||||
|     selectedCommand && | ||||
|     'meta' in selectedCommand.item && | ||||
|     selectedCommand.item.meta?.args !== undefined && | ||||
|     subCommandIndex !== undefined | ||||
|   const currentSubCommand = | ||||
|     inSubCommand && 'meta' in selectedCommand.item | ||||
|       ? selectedCommand.item.meta?.args[subCommandIndex] | ||||
|       : undefined | ||||
|  | ||||
|   const [query, setQuery] = useState('') | ||||
|  | ||||
|   const availableCommands = | ||||
|     inSubCommand && currentSubCommand | ||||
|       ? currentSubCommand.type === 'string' | ||||
|         ? query | ||||
|           ? [{ name: query }] | ||||
|           : currentSubCommand.options | ||||
|         : currentSubCommand.options | ||||
|       : commands | ||||
|  | ||||
|   const fuse = new Fuse(availableCommands || [], { | ||||
|     keys: ['name', 'description'], | ||||
|   }) | ||||
|  | ||||
|   const filteredCommands = query | ||||
|     ? fuse.search(query) | ||||
|     : availableCommands?.map((c) => ({ item: c } as SortedCommand)) | ||||
|  | ||||
|   function clearState() { | ||||
|     setQuery('') | ||||
|     setCommandBarOpen(false) | ||||
|     setSelectedCommand(null) | ||||
|     setSubCommandIndex(undefined) | ||||
|     setSubCommandData({}) | ||||
|   } | ||||
|  | ||||
|   function handleCommandSelection(entry: SortedCommand) { | ||||
|     // If we have subcommands and have not yet gathered all the | ||||
|     // data required from them, set the selected command to the | ||||
|     // current command and increment the subcommand index | ||||
|     if (selectedCommand === null && 'meta' in entry.item && entry.item.meta) { | ||||
|       setSelectedCommand(entry) | ||||
|       setSubCommandIndex(0) | ||||
|       setQuery('') | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     const { item } = entry | ||||
|     // If we have just selected a command with no subcommands, run it | ||||
|     const isCommandWithoutSubcommands = | ||||
|       'callback' in item && !('meta' in item && item.meta) | ||||
|     if (isCommandWithoutSubcommands) { | ||||
|       if (item.callback === undefined) return | ||||
|       item.callback() | ||||
|       setCommandBarOpen(false) | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // If we have subcommands and have not yet gathered all the | ||||
|     // data required from them, set the selected command to the | ||||
|     // current command and increment the subcommand index | ||||
|     if ( | ||||
|       selectedCommand && | ||||
|       subCommandIndex !== undefined && | ||||
|       'meta' in selectedCommand.item | ||||
|     ) { | ||||
|       const subCommand = selectedCommand.item.meta?.args[subCommandIndex] | ||||
|  | ||||
|       if (subCommand) { | ||||
|         const newSubCommandData = { | ||||
|           ...subCommandData, | ||||
|           [subCommand.name]: item.name, | ||||
|         } | ||||
|         const newSubCommandIndex = subCommandIndex + 1 | ||||
|  | ||||
|         // If we have subcommands and have gathered all the data required | ||||
|         // from them, run the command with the gathered data | ||||
|         if ( | ||||
|           selectedCommand.item.callback && | ||||
|           selectedCommand.item.meta?.args.length === newSubCommandIndex | ||||
|         ) { | ||||
|           selectedCommand.item.callback(newSubCommandData) | ||||
|           setCommandBarOpen(false) | ||||
|         } else { | ||||
|           // Otherwise, set the subcommand data and increment the subcommand index | ||||
|           setSubCommandData(newSubCommandData) | ||||
|           setSubCommandIndex(newSubCommandIndex) | ||||
|           setQuery('') | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function getDisplayValue(command: Command) { | ||||
|     if (command.meta?.displayValue === undefined || !command.meta.args) | ||||
|       return command.name | ||||
|     return command.meta?.displayValue( | ||||
|       command.meta.args.map((c) => | ||||
|         subCommandData[c.name] ? subCommandData[c.name] : `<${c.name}>` | ||||
|       ) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Transition.Root | ||||
|       show={ | ||||
|         commandBarOpen && | ||||
|         availableCommands?.length !== undefined && | ||||
|         availableCommands.length > 0 | ||||
|       } | ||||
|       as={Fragment} | ||||
|       afterLeave={() => clearState()} | ||||
|     > | ||||
|       <Dialog | ||||
|         onClose={() => { | ||||
|           setCommandBarOpen(false) | ||||
|           clearState() | ||||
|         }} | ||||
|         className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]" | ||||
|       > | ||||
|         <Transition.Child | ||||
|           enter="duration-100 ease-out" | ||||
|           enterFrom="opacity-0" | ||||
|           enterTo="opacity-100" | ||||
|           leave="duration-75 ease-in" | ||||
|           leaveFrom="opacity-100" | ||||
|           leaveTo="opacity-0" | ||||
|           as={Fragment} | ||||
|         > | ||||
|           <Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" /> | ||||
|         </Transition.Child> | ||||
|         <Transition.Child | ||||
|           enter="duration-100 ease-out" | ||||
|           enterFrom="opacity-0 scale-95" | ||||
|           enterTo="opacity-100 scale-100" | ||||
|           leave="duration-75 ease-in" | ||||
|           leaveFrom="opacity-100 scale-100" | ||||
|           leaveTo="opacity-0 scale-95" | ||||
|           as={Fragment} | ||||
|         > | ||||
|           <Combobox | ||||
|             value={selectedCommand} | ||||
|             onChange={handleCommandSelection} | ||||
|             className="rounded relative mx-auto p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg" | ||||
|             as="div" | ||||
|           > | ||||
|             <div className="flex gap-2 items-center"> | ||||
|               <ActionIcon icon={faSearch} size="xl" className="rounded-sm" /> | ||||
|               <div> | ||||
|                 {inSubCommand && ( | ||||
|                   <p className="text-liquid-70 dark:text-liquid-30"> | ||||
|                     {selectedCommand.item && | ||||
|                       getDisplayValue(selectedCommand.item as Command)} | ||||
|                   </p> | ||||
|                 )} | ||||
|                 <Combobox.Input | ||||
|                   onChange={(event) => setQuery(event.target.value)} | ||||
|                   className="bg-transparent focus:outline-none w-full" | ||||
|                   onKeyDown={(event) => { | ||||
|                     if (event.metaKey && event.key === 'k') | ||||
|                       setCommandBarOpen(false) | ||||
|                     if ( | ||||
|                       inSubCommand && | ||||
|                       event.key === 'Backspace' && | ||||
|                       !event.currentTarget.value | ||||
|                     ) { | ||||
|                       setSubCommandIndex(subCommandIndex - 1) | ||||
|                       setSelectedCommand(null) | ||||
|                     } | ||||
|                   }} | ||||
|                   displayValue={(command: SortedCommand) => | ||||
|                     command !== null ? command.item.name : '' | ||||
|                   } | ||||
|                   placeholder={ | ||||
|                     inSubCommand | ||||
|                       ? `Enter <${currentSubCommand?.name}>` | ||||
|                       : 'Search for a command' | ||||
|                   } | ||||
|                   value={query} | ||||
|                   autoCapitalize="off" | ||||
|                   autoComplete="off" | ||||
|                   autoCorrect="off" | ||||
|                   spellCheck="false" | ||||
|                 /> | ||||
|               </div> | ||||
|             </div> | ||||
|             <Combobox.Options static className="max-h-96 overflow-y-auto"> | ||||
|               {filteredCommands?.map((commandResult) => ( | ||||
|                 <Combobox.Option | ||||
|                   key={commandResult.item.name} | ||||
|                   value={commandResult} | ||||
|                   className="my-2 first:mt-4 last:mb-4 ui-active:bg-liquid-10 dark:ui-active:bg-liquid-90 py-1 px-2" | ||||
|                 > | ||||
|                   <p>{commandResult.item.name}</p> | ||||
|                   {(commandResult.item as SubCommand).description && ( | ||||
|                     <p className="mt-0.5 text-liquid-70 dark:text-liquid-30 text-sm"> | ||||
|                       {(commandResult.item as SubCommand).description} | ||||
|                     </p> | ||||
|                   )} | ||||
|                 </Combobox.Option> | ||||
|               ))} | ||||
|             </Combobox.Options> | ||||
|           </Combobox> | ||||
|         </Transition.Child> | ||||
|       </Dialog> | ||||
|     </Transition.Root> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default CommandBarProvider | ||||
| @ -5,7 +5,6 @@ import { EngineCommand } from '../lang/std/engineConnection' | ||||
| import { useState } from 'react' | ||||
| import { ActionButton } from '../components/ActionButton' | ||||
| import { faCheck } from '@fortawesome/free-solid-svg-icons' | ||||
| import { isReducedMotion } from 'lang/util' | ||||
|  | ||||
| type SketchModeCmd = Extract< | ||||
|   Extract<EngineCommand, { type: 'modeling_cmd_req' }>['cmd'], | ||||
| @ -23,7 +22,7 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => { | ||||
|     y_axis: { x: 0, y: 1, z: 0 }, | ||||
|     distance_to_plane: 100, | ||||
|     ortho: true, | ||||
|     animated: !isReducedMotion(), | ||||
|     animated: true, // TODO #273 get prefers reduced motion from CSS | ||||
|   }) | ||||
|   if (!sketchModeCmd) return null | ||||
|   return ( | ||||
|  | ||||
| @ -39,7 +39,6 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => { | ||||
|   const initialValues: OutputFormat = { | ||||
|     type: defaultType, | ||||
|     storage: 'embedded', | ||||
|     presentation: 'pretty', | ||||
|   } | ||||
|   const formik = useFormik({ | ||||
|     initialValues, | ||||
| @ -128,9 +127,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => { | ||||
|                   id="storage" | ||||
|                   name="storage" | ||||
|                   onChange={formik.handleChange} | ||||
|                   value={ | ||||
|                     'storage' in formik.values ? formik.values.storage : '' | ||||
|                   } | ||||
|                   value={formik.values.storage} | ||||
|                   className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full" | ||||
|                 > | ||||
|                   {type === 'gltf' && ( | ||||
|  | ||||
| @ -1,158 +0,0 @@ | ||||
| import { useMachine } from '@xstate/react' | ||||
| import { useNavigate } from 'react-router-dom' | ||||
| import { paths } from '../Router' | ||||
| import { | ||||
|   authCommandBarMeta, | ||||
|   authMachine, | ||||
|   TOKEN_PERSIST_KEY, | ||||
| } from '../machines/authMachine' | ||||
| import withBaseUrl from '../lib/withBaseURL' | ||||
| import React, { createContext, useEffect, useRef } from 'react' | ||||
| import useStateMachineCommands from '../hooks/useStateMachineCommands' | ||||
| import { | ||||
|   SETTINGS_PERSIST_KEY, | ||||
|   settingsCommandBarMeta, | ||||
|   settingsMachine, | ||||
| } from 'machines/settingsMachine' | ||||
| import { toast } from 'react-hot-toast' | ||||
| import { setThemeClass, Themes } from 'lib/theme' | ||||
| import { | ||||
|   AnyStateMachine, | ||||
|   ContextFrom, | ||||
|   InterpreterFrom, | ||||
|   Prop, | ||||
|   StateFrom, | ||||
| } from 'xstate' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
|  | ||||
| type MachineContext<T extends AnyStateMachine> = { | ||||
|   state: StateFrom<T> | ||||
|   context: ContextFrom<T> | ||||
|   send: Prop<InterpreterFrom<T>, 'send'> | ||||
| } | ||||
|  | ||||
| type GlobalContext = { | ||||
|   auth: MachineContext<typeof authMachine> | ||||
|   settings: MachineContext<typeof settingsMachine> | ||||
| } | ||||
|  | ||||
| export const GlobalStateContext = createContext({} as GlobalContext) | ||||
|  | ||||
| export const GlobalStateProvider = ({ | ||||
|   children, | ||||
| }: { | ||||
|   children: React.ReactNode | ||||
| }) => { | ||||
|   const navigate = useNavigate() | ||||
|   const { commands } = useCommandsContext() | ||||
|  | ||||
|   // Settings machine setup | ||||
|   const retrievedSettings = useRef( | ||||
|     localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}' | ||||
|   ) | ||||
|   const persistedSettings = Object.assign( | ||||
|     settingsMachine.initialState.context, | ||||
|     JSON.parse(retrievedSettings.current) as Partial< | ||||
|       (typeof settingsMachine)['context'] | ||||
|     > | ||||
|   ) | ||||
|  | ||||
|   const [settingsState, settingsSend] = useMachine(settingsMachine, { | ||||
|     context: persistedSettings, | ||||
|     actions: { | ||||
|       toastSuccess: (context, event) => { | ||||
|         const truncatedNewValue = | ||||
|           'data' in event && event.data instanceof Object | ||||
|             ? (context[Object.keys(event.data)[0] as keyof typeof context] | ||||
|                 .toString() | ||||
|                 .substring(0, 28) as any) | ||||
|             : undefined | ||||
|         toast.success( | ||||
|           event.type + | ||||
|             (truncatedNewValue | ||||
|               ? ` to "${truncatedNewValue}${ | ||||
|                   truncatedNewValue.length === 28 ? '...' : '' | ||||
|                 }"` | ||||
|               : '') | ||||
|         ) | ||||
|       }, | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   useStateMachineCommands({ | ||||
|     state: settingsState, | ||||
|     send: settingsSend, | ||||
|     commands, | ||||
|     owner: 'settings', | ||||
|     commandBarMeta: settingsCommandBarMeta, | ||||
|   }) | ||||
|  | ||||
|   // Listen for changes to the system theme and update the app theme accordingly | ||||
|   // This is only done if the theme setting is set to 'system'. | ||||
|   // It can't be done in XState (in an invoked callback, for example) | ||||
|   // because there doesn't seem to be a good way to listen to | ||||
|   // events outside of the machine that also depend on the machine's context | ||||
|   useEffect(() => { | ||||
|     const matcher = window.matchMedia('(prefers-color-scheme: dark)') | ||||
|     const listener = (e: MediaQueryListEvent) => { | ||||
|       if (settingsState.context.theme !== 'system') return | ||||
|       setThemeClass(e.matches ? Themes.Dark : Themes.Light) | ||||
|     } | ||||
|  | ||||
|     matcher.addEventListener('change', listener) | ||||
|     return () => matcher.removeEventListener('change', listener) | ||||
|   }, [settingsState.context]) | ||||
|  | ||||
|   // Auth machine setup | ||||
|   const [authState, authSend] = useMachine(authMachine, { | ||||
|     actions: { | ||||
|       goToSignInPage: () => { | ||||
|         navigate(paths.SIGN_IN) | ||||
|         logout() | ||||
|       }, | ||||
|       goToIndexPage: () => { | ||||
|         if (window.location.pathname.includes(paths.SIGN_IN)) { | ||||
|           navigate(paths.INDEX) | ||||
|         } | ||||
|       }, | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   useStateMachineCommands({ | ||||
|     state: authState, | ||||
|     send: authSend, | ||||
|     commands, | ||||
|     commandBarMeta: authCommandBarMeta, | ||||
|     owner: 'auth', | ||||
|   }) | ||||
|  | ||||
|   return ( | ||||
|     <GlobalStateContext.Provider | ||||
|       value={{ | ||||
|         auth: { | ||||
|           state: authState, | ||||
|           context: authState.context, | ||||
|           send: authSend, | ||||
|         }, | ||||
|         settings: { | ||||
|           state: settingsState, | ||||
|           context: settingsState.context, | ||||
|           send: settingsSend, | ||||
|         }, | ||||
|       }} | ||||
|     > | ||||
|       {children} | ||||
|     </GlobalStateContext.Provider> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default GlobalStateProvider | ||||
|  | ||||
| export function logout() { | ||||
|   const url = withBaseUrl('/logout') | ||||
|   localStorage.removeItem(TOKEN_PERSIST_KEY) | ||||
|   return fetch(url, { | ||||
|     method: 'POST', | ||||
|     credentials: 'include', | ||||
|   }) | ||||
| } | ||||
| @ -1,8 +1,7 @@ | ||||
| import ReactJson from 'react-json-view' | ||||
| import { useEffect } from 'react' | ||||
| import { useStore } from '../useStore' | ||||
| import { Themes, useStore } from '../useStore' | ||||
| import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel' | ||||
| import { Themes } from '../lib/theme' | ||||
|  | ||||
| const ReactJsonTypeHack = ReactJson as any | ||||
|  | ||||
|  | ||||
| @ -14,12 +14,12 @@ describe('processMemory', () => { | ||||
|     return a - 2 | ||||
|   } | ||||
|   const otherVar = myFn(5) | ||||
|  | ||||
|   const theExtrude = startSketchAt([0, 0]) | ||||
|    | ||||
|   const theExtrude = startSketchAt([0, 0])  | ||||
|     |> lineTo([-2.4, myVar], %) | ||||
|     |> lineTo([-0.76, otherVar], %) | ||||
|     |> extrude(4, %) | ||||
|  | ||||
|    | ||||
|   const theSketch = startSketchAt([0, 0]) | ||||
|     |> lineTo([-3.35, 0.17], %) | ||||
|     |> lineTo([0.98, 5.16], %) | ||||
| @ -28,20 +28,30 @@ describe('processMemory', () => { | ||||
|   show(theExtrude, theSketch)` | ||||
|     const ast = parser_wasm(code) | ||||
|     const programMemory = await enginelessExecutor(ast, { | ||||
|       root: {}, | ||||
|       root: { | ||||
|         log: { | ||||
|           type: 'userVal', | ||||
|           value: (a: any) => { | ||||
|             console.log('raw log', a) | ||||
|           }, | ||||
|           __meta: [], | ||||
|         }, | ||||
|       }, | ||||
|       pendingMemory: {}, | ||||
|     }) | ||||
|     const output = processMemory(programMemory) | ||||
|     expect(output.myVar).toEqual(5) | ||||
|     expect(output.myFn).toEqual('__function__') | ||||
|     expect(output.otherVar).toEqual(3) | ||||
|     expect(output).toEqual({ | ||||
|       myVar: 5, | ||||
|       myFn: undefined, | ||||
|       myFn: '__function__', | ||||
|       otherVar: 3, | ||||
|       theExtrude: [], | ||||
|       theSketch: [ | ||||
|         { type: 'toPoint', to: [-3.35, 0.17], from: [0, 0], name: '' }, | ||||
|         { type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17], name: '' }, | ||||
|         { type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16], name: '' }, | ||||
|         { type: 'toPoint', to: [-3.35, 0.17], from: [0, 0] }, | ||||
|         { type: 'toPoint', to: [0.98, 5.16], from: [-3.35, 0.17] }, | ||||
|         { type: 'toPoint', to: [2.15, 4.32], from: [0.98, 5.16] }, | ||||
|       ], | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
| @ -1,9 +1,8 @@ | ||||
| import ReactJson from 'react-json-view' | ||||
| import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel' | ||||
| import { useStore } from '../useStore' | ||||
| import { Themes, useStore } from '../useStore' | ||||
| import { useMemo } from 'react' | ||||
| import { ProgramMemory } from '../lang/executor' | ||||
| import { Themes } from '../lib/theme' | ||||
|  | ||||
| interface MemoryPanelProps extends CollapsiblePanelProps { | ||||
|   theme?: Exclude<Themes, Themes.System> | ||||
|  | ||||
| @ -1,11 +1,10 @@ | ||||
| import { Popover, Transition } from '@headlessui/react' | ||||
| import { Popover } from '@headlessui/react' | ||||
| import { ActionButton } from './ActionButton' | ||||
| import { faHome } from '@fortawesome/free-solid-svg-icons' | ||||
| import { ProjectWithEntryPointMetadata, paths } from '../Router' | ||||
| import { isTauri } from '../lib/isTauri' | ||||
| import { Link } from 'react-router-dom' | ||||
| import { ExportButton } from './ExportButton' | ||||
| import { Fragment } from 'react' | ||||
|  | ||||
| const ProjectSidebarMenu = ({ | ||||
|   project, | ||||
| @ -35,7 +34,7 @@ const ProjectSidebarMenu = ({ | ||||
|   ) : ( | ||||
|     <Popover className="relative"> | ||||
|       <Popover.Button | ||||
|         className="border-0 p-0.5 pr-2 flex items-center gap-4 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-energy-50" | ||||
|         className="border-0 px-1 pr-2 pl-0 flex items-center gap-4 focus:outline-none focus:ring-2 focus:ring-energy-50" | ||||
|         data-testid="project-sidebar-toggle" | ||||
|       > | ||||
|         <img | ||||
| @ -47,77 +46,54 @@ const ProjectSidebarMenu = ({ | ||||
|           {isTauri() && project?.name ? project.name : 'KittyCAD Modeling App'} | ||||
|         </span> | ||||
|       </Popover.Button> | ||||
|       <Transition | ||||
|         enter="duration-200 ease-out" | ||||
|         enterFrom="opacity-0" | ||||
|         enterTo="opacity-100" | ||||
|         leave="duration-100 ease-in" | ||||
|         leaveFrom="opacity-100" | ||||
|         leaveTo="opacity-0" | ||||
|         as={Fragment} | ||||
|       > | ||||
|         <Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" /> | ||||
|       </Transition> | ||||
|       <Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" /> | ||||
|  | ||||
|       <Transition | ||||
|         enter="duration-100 ease-out" | ||||
|         enterFrom="opacity-0 -translate-x-1/4" | ||||
|         enterTo="opacity-100 translate-x-0" | ||||
|         leave="duration-75 ease-in" | ||||
|         leaveFrom="opacity-100 translate-x-0" | ||||
|         leaveTo="opacity-0 -translate-x-4" | ||||
|         as={Fragment} | ||||
|       > | ||||
|         <Popover.Panel className="fixed inset-0 right-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-energy-100 dark:border-energy-100/50 shadow-md rounded-r-lg overflow-hidden"> | ||||
|           <div className="flex items-center gap-4 px-4 py-3 bg-energy-100"> | ||||
|             <img | ||||
|               src="/kitt-8bit-winking.svg" | ||||
|               alt="KittyCAD App" | ||||
|               className="h-9 w-auto" | ||||
|             /> | ||||
|       <Popover.Panel className="fixed inset-0 right-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-energy-100 shadow-md rounded-r-lg overflow-hidden"> | ||||
|         <div className="flex items-center gap-4 px-4 py-3 bg-energy-100"> | ||||
|           <img | ||||
|             src="/kitt-8bit-winking.svg" | ||||
|             alt="KittyCAD App" | ||||
|             className="h-9 w-auto" | ||||
|           /> | ||||
|  | ||||
|             <div> | ||||
|               <p | ||||
|                 className="m-0 text-energy-10 text-mono" | ||||
|                 data-testid="projectName" | ||||
|               > | ||||
|                 {project?.name ? project.name : 'KittyCAD Modeling App'} | ||||
|               </p> | ||||
|               {project?.entrypoint_metadata && ( | ||||
|                 <p | ||||
|                   className="m-0 text-energy-40 text-xs" | ||||
|                   data-testid="createdAt" | ||||
|                 > | ||||
|                   Created{' '} | ||||
|                   {project?.entrypoint_metadata.createdAt.toLocaleDateString()} | ||||
|                 </p> | ||||
|               )} | ||||
|             </div> | ||||
|           </div> | ||||
|           <div className="p-4 flex flex-col gap-2"> | ||||
|             <ExportButton | ||||
|               className={{ | ||||
|                 button: | ||||
|                   'border-transparent dark:border-transparent dark:hover:border-energy-60', | ||||
|               }} | ||||
|           <div> | ||||
|             <p | ||||
|               className="m-0 text-energy-10 text-mono" | ||||
|               data-testid="projectName" | ||||
|             > | ||||
|               Export Model | ||||
|             </ExportButton> | ||||
|             {isTauri() && ( | ||||
|               <ActionButton | ||||
|                 Element="link" | ||||
|                 to={paths.HOME} | ||||
|                 icon={{ | ||||
|                   icon: faHome, | ||||
|                 }} | ||||
|                 className="border-transparent dark:border-transparent dark:hover:border-energy-60" | ||||
|               > | ||||
|                 Go to Home | ||||
|               </ActionButton> | ||||
|               {project?.name ? project.name : 'KittyCAD Modeling App'} | ||||
|             </p> | ||||
|             {project?.entrypoint_metadata && ( | ||||
|               <p className="m-0 text-energy-40 text-xs" data-testid="createdAt"> | ||||
|                 Created{' '} | ||||
|                 {project?.entrypoint_metadata.createdAt.toLocaleDateString()} | ||||
|               </p> | ||||
|             )} | ||||
|           </div> | ||||
|         </Popover.Panel> | ||||
|       </Transition> | ||||
|         </div> | ||||
|         <div className="p-4 flex flex-col gap-2"> | ||||
|           <ExportButton | ||||
|             className={{ | ||||
|               button: | ||||
|                 'border-transparent dark:border-transparent dark:hover:border-energy-60', | ||||
|             }} | ||||
|           > | ||||
|             Export Model | ||||
|           </ExportButton> | ||||
|           {isTauri() && ( | ||||
|             <ActionButton | ||||
|               Element="link" | ||||
|               to={paths.HOME} | ||||
|               icon={{ | ||||
|                 icon: faHome, | ||||
|               }} | ||||
|               className="border-transparent dark:border-transparent dark:hover:border-energy-60" | ||||
|             > | ||||
|               Go to Home | ||||
|             </ActionButton> | ||||
|           )} | ||||
|         </div> | ||||
|       </Popover.Panel> | ||||
|     </Popover> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -12,12 +12,12 @@ import Loading from './Loading' | ||||
|  | ||||
| export const Stream = ({ className = '' }) => { | ||||
|   const [isLoading, setIsLoading] = useState(true) | ||||
|   const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>() | ||||
|   const videoRef = useRef<HTMLVideoElement>(null) | ||||
|   const { | ||||
|     mediaStream, | ||||
|     engineCommandManager, | ||||
|     setIsMouseDownInStream, | ||||
|     setCmdId, | ||||
|     didDragInStream, | ||||
|     setDidDragInStream, | ||||
|     streamDimensions, | ||||
| @ -27,6 +27,7 @@ export const Stream = ({ className = '' }) => { | ||||
|     isMouseDownInStream: s.isMouseDownInStream, | ||||
|     setIsMouseDownInStream: s.setIsMouseDownInStream, | ||||
|     fileId: s.fileId, | ||||
|     setCmdId: s.setCmdId, | ||||
|     didDragInStream: s.didDragInStream, | ||||
|     setDidDragInStream: s.setDidDragInStream, | ||||
|     streamDimensions: s.streamDimensions, | ||||
| @ -40,7 +41,9 @@ export const Stream = ({ className = '' }) => { | ||||
|       return | ||||
|     if (!videoRef.current) return | ||||
|     if (!mediaStream) return | ||||
|     console.log('setting video ref') | ||||
|     videoRef.current.srcObject = mediaStream | ||||
|     videoRef.current.play() | ||||
|   }, [mediaStream, engineCommandManager]) | ||||
|  | ||||
|   const handleMouseDown: MouseEventHandler<HTMLVideoElement> = ({ | ||||
| @ -58,6 +61,7 @@ export const Stream = ({ className = '' }) => { | ||||
|     console.log('click', x, y) | ||||
|  | ||||
|     const newId = uuidv4() | ||||
|     setCmdId(newId) | ||||
|  | ||||
|     const interaction = ctrlKey ? 'pan' : 'rotate' | ||||
|  | ||||
| @ -72,7 +76,6 @@ export const Stream = ({ className = '' }) => { | ||||
|     }) | ||||
|  | ||||
|     setIsMouseDownInStream(true) | ||||
|     setClickCoords({ x, y }) | ||||
|   } | ||||
|  | ||||
|   const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => { | ||||
| @ -126,19 +129,6 @@ export const Stream = ({ className = '' }) => { | ||||
|       }) | ||||
|     } | ||||
|     setDidDragInStream(false) | ||||
|     setClickCoords(undefined) | ||||
|   } | ||||
|  | ||||
|   const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => { | ||||
|     if (!clickCoords) return | ||||
|  | ||||
|     const delta = | ||||
|       ((clickCoords.x - e.clientX) ** 2 + (clickCoords.y - e.clientY) ** 2) ** | ||||
|       0.5 | ||||
|  | ||||
|     if (delta > 5 && !didDragInStream) { | ||||
|       setDidDragInStream(true) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
| @ -154,7 +144,6 @@ export const Stream = ({ className = '' }) => { | ||||
|         onContextMenuCapture={(e) => e.preventDefault()} | ||||
|         onWheel={handleScroll} | ||||
|         onPlay={() => setIsLoading(false)} | ||||
|         onMouseMoveCapture={handleMouseMove} | ||||
|         className="w-full h-full" | ||||
|       /> | ||||
|       {isLoading && ( | ||||
|  | ||||
| @ -2,8 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react' | ||||
| import UserSidebarMenu from './UserSidebarMenu' | ||||
| import { BrowserRouter } from 'react-router-dom' | ||||
| import { Models } from '@kittycad/lib' | ||||
| import { GlobalStateProvider } from './GlobalStateProvider' | ||||
| import CommandBarProvider from './CommandBar' | ||||
| import { GlobalStateProvider } from '../hooks/useAuthMachine' | ||||
|  | ||||
| type User = Models['User_type'] | ||||
|  | ||||
| @ -95,9 +94,7 @@ function TestWrap({ children }: { children: React.ReactNode }) { | ||||
|   // wrap in router and xState context | ||||
|   return ( | ||||
|     <BrowserRouter> | ||||
|       <CommandBarProvider> | ||||
|         <GlobalStateProvider>{children}</GlobalStateProvider> | ||||
|       </CommandBarProvider> | ||||
|       <GlobalStateProvider>{children}</GlobalStateProvider> | ||||
|     </BrowserRouter> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -1,13 +1,13 @@ | ||||
| import { Popover, Transition } from '@headlessui/react' | ||||
| import { Popover } from '@headlessui/react' | ||||
| import { ActionButton } from './ActionButton' | ||||
| import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons' | ||||
| import { faGithub } from '@fortawesome/free-brands-svg-icons' | ||||
| import { useNavigate } from 'react-router-dom' | ||||
| import { Fragment, useState } from 'react' | ||||
| import { useState } from 'react' | ||||
| import { paths } from '../Router' | ||||
| import makeUrlPathRelative from '../lib/makeUrlPathRelative' | ||||
| import { useAuthMachine } from '../hooks/useAuthMachine' | ||||
| import { Models } from '@kittycad/lib' | ||||
| import { useGlobalStateContext } from 'hooks/useGlobalStateContext' | ||||
|  | ||||
| type User = Models['User_type'] | ||||
|  | ||||
| @ -15,9 +15,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => { | ||||
|   const displayedName = getDisplayName(user) | ||||
|   const [imageLoadFailed, setImageLoadFailed] = useState(false) | ||||
|   const navigate = useNavigate() | ||||
|   const { | ||||
|     auth: { send }, | ||||
|   } = useGlobalStateContext() | ||||
|   const [_, send] = useAuthMachine() | ||||
|  | ||||
|   // Fallback logic for displaying user's "name": | ||||
|   // 1. user.name | ||||
| @ -61,102 +59,82 @@ const UserSidebarMenu = ({ user }: { user?: User }) => { | ||||
|           Menu | ||||
|         </ActionButton> | ||||
|       )} | ||||
|       <Transition | ||||
|         enter="duration-200 ease-out" | ||||
|         enterFrom="opacity-0" | ||||
|         enterTo="opacity-100" | ||||
|         leave="duration-100 ease-in" | ||||
|         leaveFrom="opacity-100" | ||||
|         leaveTo="opacity-0" | ||||
|         as={Fragment} | ||||
|       > | ||||
|         <Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" /> | ||||
|       </Transition> | ||||
|       <Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" /> | ||||
|  | ||||
|       <Transition | ||||
|         enter="duration-100 ease-out" | ||||
|         enterFrom="opacity-0 translate-x-1/4" | ||||
|         enterTo="opacity-100 translate-x-0" | ||||
|         leave="duration-75 ease-in" | ||||
|         leaveFrom="opacity-100 translate-x-0" | ||||
|         leaveTo="opacity-0 translate-x-4" | ||||
|         as={Fragment} | ||||
|       > | ||||
|         <Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-liquid-100 dark:border-liquid-100/50 shadow-md rounded-l-lg overflow-hidden"> | ||||
|           {({ 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> | ||||
|                     )} | ||||
|       <Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-100 border border-liquid-100 shadow-md rounded-l-lg overflow-hidden"> | ||||
|         {({ 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 className="p-4 flex flex-col gap-2"> | ||||
|                 <ActionButton | ||||
|                   Element="button" | ||||
|                   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(makeUrlPathRelative(paths.SETTINGS)) | ||||
|                   }} | ||||
|                 > | ||||
|                   Settings | ||||
|                 </ActionButton> | ||||
|                 <ActionButton | ||||
|                   Element="link" | ||||
|                   to="https://github.com/KittyCAD/modeling-app/discussions" | ||||
|                   icon={{ icon: faGithub }} | ||||
|                   className="border-transparent dark:border-transparent dark:hover:border-liquid-60" | ||||
|                 > | ||||
|                   Request a feature | ||||
|                 </ActionButton> | ||||
|                 <ActionButton | ||||
|                   Element="button" | ||||
|                   onClick={() => send('Log out')} | ||||
|                   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> | ||||
|       </Transition> | ||||
|             )} | ||||
|             <div className="p-4 flex flex-col gap-2"> | ||||
|               <ActionButton | ||||
|                 Element="button" | ||||
|                 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(makeUrlPathRelative(paths.SETTINGS)) | ||||
|                 }} | ||||
|               > | ||||
|                 Settings | ||||
|               </ActionButton> | ||||
|               <ActionButton | ||||
|                 Element="link" | ||||
|                 to="https://github.com/KittyCAD/modeling-app/discussions" | ||||
|                 icon={{ icon: faGithub }} | ||||
|                 className="border-transparent dark:border-transparent dark:hover:border-liquid-60" | ||||
|               > | ||||
|                 Request a feature | ||||
|               </ActionButton> | ||||
|               <ActionButton | ||||
|                 Element="button" | ||||
|                 onClick={() => send('logout')} | ||||
|                 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> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -1,185 +0,0 @@ | ||||
| import * as jsrpc from 'json-rpc-2.0' | ||||
| import * as LSP from 'vscode-languageserver-protocol' | ||||
|  | ||||
| import { | ||||
|   registerServerCapability, | ||||
|   unregisterServerCapability, | ||||
| } from './server-capability-registration' | ||||
| import { Codec, FromServer, IntoServer } from './codec' | ||||
|  | ||||
| const client_capabilities: LSP.ClientCapabilities = { | ||||
|   textDocument: { | ||||
|     hover: { | ||||
|       dynamicRegistration: true, | ||||
|       contentFormat: ['plaintext', 'markdown'], | ||||
|     }, | ||||
|     moniker: {}, | ||||
|     synchronization: { | ||||
|       dynamicRegistration: true, | ||||
|       willSave: false, | ||||
|       didSave: false, | ||||
|       willSaveWaitUntil: false, | ||||
|     }, | ||||
|     completion: { | ||||
|       dynamicRegistration: true, | ||||
|       completionItem: { | ||||
|         snippetSupport: false, | ||||
|         commitCharactersSupport: true, | ||||
|         documentationFormat: ['plaintext', 'markdown'], | ||||
|         deprecatedSupport: false, | ||||
|         preselectSupport: false, | ||||
|       }, | ||||
|       contextSupport: false, | ||||
|     }, | ||||
|     signatureHelp: { | ||||
|       dynamicRegistration: true, | ||||
|       signatureInformation: { | ||||
|         documentationFormat: ['plaintext', 'markdown'], | ||||
|       }, | ||||
|     }, | ||||
|     declaration: { | ||||
|       dynamicRegistration: true, | ||||
|       linkSupport: true, | ||||
|     }, | ||||
|     definition: { | ||||
|       dynamicRegistration: true, | ||||
|       linkSupport: true, | ||||
|     }, | ||||
|     typeDefinition: { | ||||
|       dynamicRegistration: true, | ||||
|       linkSupport: true, | ||||
|     }, | ||||
|     implementation: { | ||||
|       dynamicRegistration: true, | ||||
|       linkSupport: true, | ||||
|     }, | ||||
|   }, | ||||
|   workspace: { | ||||
|     didChangeConfiguration: { | ||||
|       dynamicRegistration: true, | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
|  | ||||
| export default class Client extends jsrpc.JSONRPCServerAndClient { | ||||
|   afterInitializedHooks: (() => Promise<void>)[] = [] | ||||
|   #fromServer: FromServer | ||||
|   private serverCapabilities: LSP.ServerCapabilities<any> = {} | ||||
|  | ||||
|   constructor(fromServer: FromServer, intoServer: IntoServer) { | ||||
|     super( | ||||
|       new jsrpc.JSONRPCServer(), | ||||
|       new jsrpc.JSONRPCClient(async (json: jsrpc.JSONRPCRequest) => { | ||||
|         const encoded = Codec.encode(json) | ||||
|         intoServer.enqueue(encoded) | ||||
|         if (null != json.id) { | ||||
|           // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||||
|           const response = await fromServer.responses.get(json.id)! | ||||
|           this.client.receive(response as jsrpc.JSONRPCResponse) | ||||
|         } | ||||
|       }) | ||||
|     ) | ||||
|     this.#fromServer = fromServer | ||||
|   } | ||||
|  | ||||
|   async start(): Promise<void> { | ||||
|     // process "window/logMessage": client <- server | ||||
|     this.addMethod(LSP.LogMessageNotification.type.method, (params) => { | ||||
|       const { type, message } = params as { | ||||
|         type: LSP.MessageType | ||||
|         message: string | ||||
|       } | ||||
|       let messageString = '' | ||||
|       switch (type) { | ||||
|         case LSP.MessageType.Error: { | ||||
|           messageString += '[error] ' | ||||
|           break | ||||
|         } | ||||
|         case LSP.MessageType.Warning: { | ||||
|           messageString += ' [warn] ' | ||||
|           break | ||||
|         } | ||||
|         case LSP.MessageType.Info: { | ||||
|           messageString += ' [info] ' | ||||
|           break | ||||
|         } | ||||
|         case LSP.MessageType.Log: { | ||||
|           messageString += '  [log] ' | ||||
|           break | ||||
|         } | ||||
|       } | ||||
|       messageString += message | ||||
|       // console.log(messageString) | ||||
|       return | ||||
|     }) | ||||
|  | ||||
|     // process "client/registerCapability": client <- server | ||||
|     this.addMethod(LSP.RegistrationRequest.type.method, (params) => { | ||||
|       // Register a server capability. | ||||
|       params.registrations.forEach( | ||||
|         (capabilityRegistration: LSP.Registration) => { | ||||
|           this.serverCapabilities = registerServerCapability( | ||||
|             this.serverCapabilities, | ||||
|             capabilityRegistration | ||||
|           ) | ||||
|         } | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     // process "client/unregisterCapability": client <- server | ||||
|     this.addMethod(LSP.UnregistrationRequest.type.method, (params) => { | ||||
|       // Unregister a server capability. | ||||
|       params.unregisterations.forEach( | ||||
|         (capabilityUnregistration: LSP.Unregistration) => { | ||||
|           this.serverCapabilities = unregisterServerCapability( | ||||
|             this.serverCapabilities, | ||||
|             capabilityUnregistration | ||||
|           ) | ||||
|         } | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     // request "initialize": client <-> server | ||||
|     const { capabilities } = await this.request( | ||||
|       LSP.InitializeRequest.type.method, | ||||
|       { | ||||
|         processId: null, | ||||
|         clientInfo: { | ||||
|           name: 'kcl-language-client', | ||||
|         }, | ||||
|         capabilities: client_capabilities, | ||||
|         rootUri: null, | ||||
|       } as LSP.InitializeParams | ||||
|     ) | ||||
|  | ||||
|     this.serverCapabilities = capabilities | ||||
|  | ||||
|     // notify "initialized": client --> server | ||||
|     this.notify(LSP.InitializedNotification.type.method, {}) | ||||
|  | ||||
|     await Promise.all( | ||||
|       this.afterInitializedHooks.map((f: () => Promise<void>) => f()) | ||||
|     ) | ||||
|     await Promise.all([this.processNotifications(), this.processRequests()]) | ||||
|   } | ||||
|  | ||||
|   getServerCapabilities(): LSP.ServerCapabilities<any> { | ||||
|     return this.serverCapabilities | ||||
|   } | ||||
|  | ||||
|   async processNotifications(): Promise<void> { | ||||
|     for await (const notification of this.#fromServer.notifications) { | ||||
|       await this.receiveAndSend(notification) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async processRequests(): Promise<void> { | ||||
|     for await (const request of this.#fromServer.requests) { | ||||
|       await this.receiveAndSend(request) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pushAfterInitializeHook(...hooks: (() => Promise<void>)[]): void { | ||||
|     this.afterInitializedHooks.push(...hooks) | ||||
|   } | ||||
| } | ||||
| @ -1,53 +0,0 @@ | ||||
| import * as jsrpc from 'json-rpc-2.0' | ||||
| import * as vsrpc from 'vscode-jsonrpc' | ||||
|  | ||||
| import Bytes from './codec/bytes' | ||||
| import StreamDemuxer from './codec/demuxer' | ||||
| import Headers from './codec/headers' | ||||
| import Queue from './codec/queue' | ||||
| import Tracer from './tracer' | ||||
|  | ||||
| export const encoder = new TextEncoder() | ||||
| export const decoder = new TextDecoder() | ||||
|  | ||||
| export class Codec { | ||||
|   static encode( | ||||
|     json: jsrpc.JSONRPCRequest | jsrpc.JSONRPCResponse | ||||
|   ): Uint8Array { | ||||
|     const message = JSON.stringify(json) | ||||
|     const delimited = Headers.add(message) | ||||
|     return Bytes.encode(delimited) | ||||
|   } | ||||
|  | ||||
|   static decode<T>(data: Uint8Array): T { | ||||
|     const delimited = Bytes.decode(data) | ||||
|     const message = Headers.remove(delimited) | ||||
|     return JSON.parse(message) as T | ||||
|   } | ||||
| } | ||||
|  | ||||
| // FIXME: tracing effiency | ||||
| export class IntoServer | ||||
|   extends Queue<Uint8Array> | ||||
|   implements AsyncGenerator<Uint8Array, never, void> | ||||
| { | ||||
|   enqueue(item: Uint8Array): void { | ||||
|     Tracer.client(Headers.remove(decoder.decode(item))) | ||||
|     super.enqueue(item) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export interface FromServer extends WritableStream<Uint8Array> { | ||||
|   readonly responses: { | ||||
|     get(key: number | string): null | Promise<vsrpc.ResponseMessage> | ||||
|   } | ||||
|   readonly notifications: AsyncGenerator<vsrpc.NotificationMessage, never, void> | ||||
|   readonly requests: AsyncGenerator<vsrpc.RequestMessage, never, void> | ||||
| } | ||||
|  | ||||
| // eslint-disable-next-line @typescript-eslint/no-namespace | ||||
| export namespace FromServer { | ||||
|   export function create(): FromServer { | ||||
|     return new StreamDemuxer() | ||||
|   } | ||||
| } | ||||
| @ -1,27 +0,0 @@ | ||||
| import { encoder, decoder } from '../codec' | ||||
|  | ||||
| export default class Bytes { | ||||
|   static encode(input: string): Uint8Array { | ||||
|     return encoder.encode(input) | ||||
|   } | ||||
|  | ||||
|   static decode(input: Uint8Array): string { | ||||
|     return decoder.decode(input) | ||||
|   } | ||||
|  | ||||
|   static append< | ||||
|     T extends { length: number; set(arr: T, offset: number): void } | ||||
|   >(constructor: { new (length: number): T }, ...arrays: T[]) { | ||||
|     let totalLength = 0 | ||||
|     for (const arr of arrays) { | ||||
|       totalLength += arr.length | ||||
|     } | ||||
|     const result = new constructor(totalLength) | ||||
|     let offset = 0 | ||||
|     for (const arr of arrays) { | ||||
|       result.set(arr, offset) | ||||
|       offset += arr.length | ||||
|     } | ||||
|     return result | ||||
|   } | ||||
| } | ||||
| @ -1,82 +0,0 @@ | ||||
| import * as vsrpc from 'vscode-jsonrpc' | ||||
|  | ||||
| import Bytes from './bytes' | ||||
| import PromiseMap from './map' | ||||
| import Queue from './queue' | ||||
| import Tracer from '../tracer' | ||||
|  | ||||
| export default class StreamDemuxer extends Queue<Uint8Array> { | ||||
|   readonly responses: PromiseMap<number | string, vsrpc.ResponseMessage> = | ||||
|     new PromiseMap() | ||||
|   readonly notifications: Queue<vsrpc.NotificationMessage> = | ||||
|     new Queue<vsrpc.NotificationMessage>() | ||||
|   readonly requests: Queue<vsrpc.RequestMessage> = | ||||
|     new Queue<vsrpc.RequestMessage>() | ||||
|  | ||||
|   readonly #start: Promise<void> | ||||
|  | ||||
|   constructor() { | ||||
|     super() | ||||
|     this.#start = this.start() | ||||
|   } | ||||
|  | ||||
|   private async start(): Promise<void> { | ||||
|     let contentLength: null | number = null | ||||
|     let buffer = new Uint8Array() | ||||
|  | ||||
|     for await (const bytes of this) { | ||||
|       buffer = Bytes.append(Uint8Array, buffer, bytes) | ||||
|       while (buffer.length > 0) { | ||||
|         // check if the content length is known | ||||
|         if (null == contentLength) { | ||||
|           // if not, try to match the prefixed headers | ||||
|           const match = Bytes.decode(buffer).match( | ||||
|             /^Content-Length:\s*(\d+)\s*/ | ||||
|           ) | ||||
|           if (null == match) continue | ||||
|  | ||||
|           // try to parse the content-length from the headers | ||||
|           const length = parseInt(match[1]) | ||||
|           if (isNaN(length)) throw new Error('invalid content length') | ||||
|  | ||||
|           // slice the headers since we now have the content length | ||||
|           buffer = buffer.slice(match[0].length) | ||||
|  | ||||
|           // set the content length | ||||
|           contentLength = length | ||||
|         } | ||||
|  | ||||
|         // if the buffer doesn't contain a full message; await another iteration | ||||
|         if (buffer.length < contentLength) continue | ||||
|  | ||||
|         // Get just the slice of the buffer that is our content length. | ||||
|         const slice = buffer.slice(0, contentLength) | ||||
|  | ||||
|         // decode buffer to a string | ||||
|         const delimited = Bytes.decode(slice) | ||||
|  | ||||
|         // reset the buffer | ||||
|         buffer = buffer.slice(contentLength) | ||||
|         // reset the contentLength | ||||
|         contentLength = null | ||||
|  | ||||
|         const message = JSON.parse(delimited) as vsrpc.Message | ||||
|         Tracer.server(message) | ||||
|  | ||||
|         // demux the message stream | ||||
|         if (vsrpc.Message.isResponse(message) && null != message.id) { | ||||
|           this.responses.set(message.id, message) | ||||
|           continue | ||||
|         } | ||||
|         if (vsrpc.Message.isNotification(message)) { | ||||
|           this.notifications.enqueue(message) | ||||
|           continue | ||||
|         } | ||||
|         if (vsrpc.Message.isRequest(message)) { | ||||
|           this.requests.enqueue(message) | ||||
|           continue | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,9 +0,0 @@ | ||||
| export default class Headers { | ||||
|   static add(message: string): string { | ||||
|     return `Content-Length: ${message.length}\r\n\r\n${message}` | ||||
|   } | ||||
|  | ||||
|   static remove(delimited: string): string { | ||||
|     return delimited.replace(/^Content-Length:\s*\d+\s*/, '') | ||||
|   } | ||||
| } | ||||
| @ -1,72 +0,0 @@ | ||||
| export default class PromiseMap<K, V extends { toString(): string }> { | ||||
|   #map: Map<K, PromiseMap.Entry<V>> = new Map() | ||||
|  | ||||
|   get(key: K & { toString(): string }): null | Promise<V> { | ||||
|     let initialized: PromiseMap.Entry<V> | ||||
|     // if the entry doesn't exist, set it | ||||
|     if (!this.#map.has(key)) { | ||||
|       initialized = this.#set(key) | ||||
|     } else { | ||||
|       // otherwise return the entry | ||||
|       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||||
|       initialized = this.#map.get(key)! | ||||
|     } | ||||
|     // if the entry is a pending promise, return it | ||||
|     if (initialized.status === 'pending') { | ||||
|       return initialized.promise | ||||
|     } else { | ||||
|       // otherwise return null | ||||
|       return null | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   #set(key: K, value?: V): PromiseMap.Entry<V> { | ||||
|     if (this.#map.has(key)) { | ||||
|       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||||
|       return this.#map.get(key)! | ||||
|     } | ||||
|     // placeholder resolver for entry | ||||
|     let resolve = (item: V) => { | ||||
|       void item | ||||
|     } | ||||
|     // promise for entry (which assigns the resolver | ||||
|     const promise = new Promise<V>((resolver) => { | ||||
|       resolve = resolver | ||||
|     }) | ||||
|     // the initialized entry | ||||
|     const initialized: PromiseMap.Entry<V> = { | ||||
|       status: 'pending', | ||||
|       resolve, | ||||
|       promise, | ||||
|     } | ||||
|     if (null != value) { | ||||
|       initialized.resolve(value) | ||||
|     } | ||||
|     // set the entry | ||||
|     this.#map.set(key, initialized) | ||||
|     return initialized | ||||
|   } | ||||
|  | ||||
|   set(key: K & { toString(): string }, value: V): this { | ||||
|     const initialized = this.#set(key, value) | ||||
|     // if the promise is pending ... | ||||
|     if (initialized.status === 'pending') { | ||||
|       // ... set the entry status to resolved to free the promise | ||||
|       this.#map.set(key, { status: 'resolved' }) | ||||
|       // ... and resolve the promise with the given value | ||||
|       initialized.resolve(value) | ||||
|     } | ||||
|     return this | ||||
|   } | ||||
|  | ||||
|   get size(): number { | ||||
|     return this.#map.size | ||||
|   } | ||||
| } | ||||
|  | ||||
| // eslint-disable-next-line @typescript-eslint/no-namespace | ||||
| export namespace PromiseMap { | ||||
|   export type Entry<V> = | ||||
|     | { status: 'pending'; resolve: (item: V) => void; promise: Promise<V> } | ||||
|     | { status: 'resolved' } | ||||
| } | ||||
| @ -1,113 +0,0 @@ | ||||
| export default class Queue<T> | ||||
|   implements WritableStream<T>, AsyncGenerator<T, never, void> | ||||
| { | ||||
|   readonly #promises: Promise<T>[] = [] | ||||
|   readonly #resolvers: ((item: T) => void)[] = [] | ||||
|   readonly #observers: ((item: T) => void)[] = [] | ||||
|  | ||||
|   #closed = false | ||||
|   #locked = false | ||||
|   readonly #stream: WritableStream<T> | ||||
|  | ||||
|   static #__add<X>( | ||||
|     promises: Promise<X>[], | ||||
|     resolvers: ((item: X) => void)[] | ||||
|   ): void { | ||||
|     promises.push( | ||||
|       new Promise((resolve) => { | ||||
|         resolvers.push(resolve) | ||||
|       }) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   static #__enqueue<X>( | ||||
|     closed: boolean, | ||||
|     promises: Promise<X>[], | ||||
|     resolvers: ((item: X) => void)[], | ||||
|     item: X | ||||
|   ): void { | ||||
|     if (!closed) { | ||||
|       if (!resolvers.length) Queue.#__add(promises, resolvers) | ||||
|       const resolve = resolvers.shift()! // eslint-disable-line @typescript-eslint/no-non-null-assertion | ||||
|       resolve(item) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   constructor() { | ||||
|     const closed = this.#closed | ||||
|     const promises = this.#promises | ||||
|     const resolvers = this.#resolvers | ||||
|     this.#stream = new WritableStream({ | ||||
|       write(item: T): void { | ||||
|         Queue.#__enqueue(closed, promises, resolvers, item) | ||||
|       }, | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   #add(): void { | ||||
|     return Queue.#__add(this.#promises, this.#resolvers) | ||||
|   } | ||||
|  | ||||
|   enqueue(item: T): void { | ||||
|     return Queue.#__enqueue(this.#closed, this.#promises, this.#resolvers, item) | ||||
|   } | ||||
|  | ||||
|   dequeue(): Promise<T> { | ||||
|     if (!this.#promises.length) this.#add() | ||||
|     const item = this.#promises.shift()! // eslint-disable-line @typescript-eslint/no-non-null-assertion | ||||
|     return item | ||||
|   } | ||||
|  | ||||
|   isEmpty(): boolean { | ||||
|     return !this.#promises.length | ||||
|   } | ||||
|  | ||||
|   isBlocked(): boolean { | ||||
|     return !!this.#resolvers.length | ||||
|   } | ||||
|  | ||||
|   get length(): number { | ||||
|     return this.#promises.length - this.#resolvers.length | ||||
|   } | ||||
|  | ||||
|   async next(): Promise<IteratorResult<T, never>> { | ||||
|     const done = false | ||||
|     const value = await this.dequeue() | ||||
|     for (const observer of this.#observers) { | ||||
|       observer(value) | ||||
|     } | ||||
|     return { done, value } | ||||
|   } | ||||
|  | ||||
|   return(): Promise<IteratorResult<T, never>> { | ||||
|     return new Promise(() => { | ||||
|       // empty | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   throw(err: Error): Promise<IteratorResult<T, never>> { | ||||
|     return new Promise((_resolve, reject) => { | ||||
|       reject(err) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   [Symbol.asyncIterator](): AsyncGenerator<T, never, void> { | ||||
|     return this | ||||
|   } | ||||
|  | ||||
|   get locked(): boolean { | ||||
|     return this.#stream.locked | ||||
|   } | ||||
|  | ||||
|   abort(reason?: Error): Promise<void> { | ||||
|     return this.#stream.abort(reason) | ||||
|   } | ||||
|  | ||||
|   close(): Promise<void> { | ||||
|     return this.#stream.close() | ||||
|   } | ||||
|  | ||||
|   getWriter(): WritableStreamDefaultWriter<T> { | ||||
|     return this.#stream.getWriter() | ||||
|   } | ||||
| } | ||||
| @ -1,151 +0,0 @@ | ||||
| import type * as LSP from 'vscode-languageserver-protocol' | ||||
| import Client from './client' | ||||
| import { LanguageServerPlugin } from './plugin' | ||||
| import { SemanticToken, deserializeTokens } from './semantic_tokens' | ||||
|  | ||||
| // https://microsoft.github.io/language-server-protocol/specifications/specification-current/ | ||||
|  | ||||
| // Client to server then server to client | ||||
| interface LSPRequestMap { | ||||
|   initialize: [LSP.InitializeParams, LSP.InitializeResult] | ||||
|   'textDocument/hover': [LSP.HoverParams, LSP.Hover] | ||||
|   'textDocument/completion': [ | ||||
|     LSP.CompletionParams, | ||||
|     LSP.CompletionItem[] | LSP.CompletionList | null | ||||
|   ] | ||||
|   'textDocument/semanticTokens/full': [ | ||||
|     LSP.SemanticTokensParams, | ||||
|     LSP.SemanticTokens | ||||
|   ] | ||||
| } | ||||
|  | ||||
| // Client to server | ||||
| interface LSPNotifyMap { | ||||
|   initialized: LSP.InitializedParams | ||||
|   'textDocument/didChange': LSP.DidChangeTextDocumentParams | ||||
|   'textDocument/didOpen': LSP.DidOpenTextDocumentParams | ||||
| } | ||||
|  | ||||
| // Server to client | ||||
| interface LSPEventMap { | ||||
|   'textDocument/publishDiagnostics': LSP.PublishDiagnosticsParams | ||||
| } | ||||
|  | ||||
| export type Notification = { | ||||
|   [key in keyof LSPEventMap]: { | ||||
|     jsonrpc: '2.0' | ||||
|     id?: null | undefined | ||||
|     method: key | ||||
|     params: LSPEventMap[key] | ||||
|   } | ||||
| }[keyof LSPEventMap] | ||||
|  | ||||
| export interface LanguageServerClientOptions { | ||||
|   client: Client | ||||
| } | ||||
|  | ||||
| export class LanguageServerClient { | ||||
|   private client: Client | ||||
|  | ||||
|   public ready: boolean | ||||
|  | ||||
|   private plugins: LanguageServerPlugin[] | ||||
|  | ||||
|   public initializePromise: Promise<void> | ||||
|  | ||||
|   private isUpdatingSemanticTokens: boolean = false | ||||
|   private semanticTokens: SemanticToken[] = [] | ||||
|  | ||||
|   constructor(options: LanguageServerClientOptions) { | ||||
|     this.plugins = [] | ||||
|     this.client = options.client | ||||
|  | ||||
|     this.ready = false | ||||
|  | ||||
|     this.initializePromise = this.initialize() | ||||
|   } | ||||
|  | ||||
|   async initialize() { | ||||
|     // Start the client in the background. | ||||
|     this.client.start() | ||||
|  | ||||
|     this.ready = true | ||||
|   } | ||||
|  | ||||
|   getServerCapabilities(): LSP.ServerCapabilities<any> { | ||||
|     return this.client.getServerCapabilities() | ||||
|   } | ||||
|  | ||||
|   close() {} | ||||
|  | ||||
|   textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) { | ||||
|     this.notify('textDocument/didOpen', params) | ||||
|  | ||||
|     this.updateSemanticTokens(params.textDocument.uri) | ||||
|   } | ||||
|  | ||||
|   textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) { | ||||
|     this.notify('textDocument/didChange', params) | ||||
|     this.updateSemanticTokens(params.textDocument.uri) | ||||
|   } | ||||
|  | ||||
|   async updateSemanticTokens(uri: string) { | ||||
|     // Make sure we can only run, if we aren't already running. | ||||
|     if (!this.isUpdatingSemanticTokens) { | ||||
|       this.isUpdatingSemanticTokens = true | ||||
|  | ||||
|       const result = await this.request('textDocument/semanticTokens/full', { | ||||
|         textDocument: { | ||||
|           uri, | ||||
|         }, | ||||
|       }) | ||||
|  | ||||
|       this.semanticTokens = deserializeTokens( | ||||
|         result.data, | ||||
|         this.getServerCapabilities().semanticTokensProvider | ||||
|       ) | ||||
|  | ||||
|       this.isUpdatingSemanticTokens = false | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   getSemanticTokens(): SemanticToken[] { | ||||
|     return this.semanticTokens | ||||
|   } | ||||
|  | ||||
|   async textDocumentHover(params: LSP.HoverParams) { | ||||
|     return await this.request('textDocument/hover', params) | ||||
|   } | ||||
|  | ||||
|   async textDocumentCompletion(params: LSP.CompletionParams) { | ||||
|     return await this.request('textDocument/completion', params) | ||||
|   } | ||||
|  | ||||
|   attachPlugin(plugin: LanguageServerPlugin) { | ||||
|     this.plugins.push(plugin) | ||||
|   } | ||||
|  | ||||
|   detachPlugin(plugin: LanguageServerPlugin) { | ||||
|     const i = this.plugins.indexOf(plugin) | ||||
|     if (i === -1) return | ||||
|     this.plugins.splice(i, 1) | ||||
|   } | ||||
|  | ||||
|   private request<K extends keyof LSPRequestMap>( | ||||
|     method: K, | ||||
|     params: LSPRequestMap[K][0] | ||||
|   ): Promise<LSPRequestMap[K][1]> { | ||||
|     return this.client.request(method, params) as Promise<LSPRequestMap[K][1]> | ||||
|   } | ||||
|  | ||||
|   private notify<K extends keyof LSPNotifyMap>( | ||||
|     method: K, | ||||
|     params: LSPNotifyMap[K] | ||||
|   ): void { | ||||
|     return this.client.notify(method, params) | ||||
|   } | ||||
|  | ||||
|   private processNotification(notification: Notification) { | ||||
|     for (const plugin of this.plugins) plugin.processNotification(notification) | ||||
|   } | ||||
| } | ||||
| @ -1,36 +0,0 @@ | ||||
| // Code mirror language implementation for kcl. | ||||
|  | ||||
| import { | ||||
|   Language, | ||||
|   defineLanguageFacet, | ||||
|   LanguageSupport, | ||||
| } from '@codemirror/language' | ||||
| import { LanguageServerClient } from '.' | ||||
| import { kclPlugin } from './plugin' | ||||
| import type * as LSP from 'vscode-languageserver-protocol' | ||||
| import { parser as jsParser } from '@lezer/javascript' | ||||
|  | ||||
| const data = defineLanguageFacet({}) | ||||
|  | ||||
| export interface LanguageOptions { | ||||
|   workspaceFolders: LSP.WorkspaceFolder[] | null | ||||
|   documentUri: string | ||||
|   client: LanguageServerClient | ||||
| } | ||||
|  | ||||
| export default function kclLanguage(options: LanguageOptions): LanguageSupport { | ||||
|   // For now let's use the javascript parser. | ||||
|   // It works really well and has good syntax highlighting. | ||||
|   // We can use our lsp for the rest. | ||||
|   const lang = new Language(data, jsParser, [], 'kcl') | ||||
|  | ||||
|   // Create our supporting extension. | ||||
|   const kclLsp = kclPlugin({ | ||||
|     documentUri: options.documentUri, | ||||
|     workspaceFolders: options.workspaceFolders, | ||||
|     allowHTMLContent: true, | ||||
|     client: options.client, | ||||
|   }) | ||||
|  | ||||
|   return new LanguageSupport(lang, [kclLsp]) | ||||
| } | ||||
| @ -1,168 +0,0 @@ | ||||
| // Extends the codemirror Parser for kcl. | ||||
|  | ||||
| import { | ||||
|   Parser, | ||||
|   Input, | ||||
|   TreeFragment, | ||||
|   PartialParse, | ||||
|   Tree, | ||||
|   NodeType, | ||||
|   NodeSet, | ||||
| } from '@lezer/common' | ||||
| import { LanguageServerClient } from '.' | ||||
| import { posToOffset } from './plugin' | ||||
| import { SemanticToken } from './semantic_tokens' | ||||
| import { DocInput } from '@codemirror/language' | ||||
| import { tags, styleTags } from '@lezer/highlight' | ||||
|  | ||||
| export default class KclParser extends Parser { | ||||
|   private client: LanguageServerClient | ||||
|  | ||||
|   constructor(client: LanguageServerClient) { | ||||
|     super() | ||||
|     this.client = client | ||||
|   } | ||||
|  | ||||
|   createParse( | ||||
|     input: Input, | ||||
|     fragments: readonly TreeFragment[], | ||||
|     ranges: readonly { from: number; to: number }[] | ||||
|   ): PartialParse { | ||||
|     let parse: PartialParse = new Context(this, input, fragments, ranges) | ||||
|     return parse | ||||
|   } | ||||
|  | ||||
|   getTokenTypes(): string[] { | ||||
|     return this.client.getServerCapabilities().semanticTokensProvider!.legend | ||||
|       .tokenTypes | ||||
|   } | ||||
|  | ||||
|   getSemanticTokens(): SemanticToken[] { | ||||
|     return this.client.getSemanticTokens() | ||||
|   } | ||||
| } | ||||
|  | ||||
| class Context implements PartialParse { | ||||
|   private parser: KclParser | ||||
|   private input: DocInput | ||||
|   private fragments: readonly TreeFragment[] | ||||
|   private ranges: readonly { from: number; to: number }[] | ||||
|  | ||||
|   private nodeTypes: { [key: string]: NodeType } | ||||
|   stoppedAt: number = 0 | ||||
|  | ||||
|   private semanticTokens: SemanticToken[] = [] | ||||
|   private currentLine: number = 0 | ||||
|   private currentColumn: number = 0 | ||||
|   private nodeSet: NodeSet | ||||
|  | ||||
|   constructor( | ||||
|     /// The parser configuration used. | ||||
|     parser: KclParser, | ||||
|     input: Input, | ||||
|     fragments: readonly TreeFragment[], | ||||
|     ranges: readonly { from: number; to: number }[] | ||||
|   ) { | ||||
|     this.parser = parser | ||||
|     this.input = input as DocInput | ||||
|     this.fragments = fragments | ||||
|     this.ranges = ranges | ||||
|  | ||||
|     // Iterate over the semantic token types and create a node type for each. | ||||
|     this.nodeTypes = {} | ||||
|     let nodeArray: NodeType[] = [] | ||||
|     this.parser.getTokenTypes().forEach((tokenType, index) => { | ||||
|       const nodeType = NodeType.define({ | ||||
|         id: index, | ||||
|         name: tokenType, | ||||
|         // props: [this.styleTags], | ||||
|       }) | ||||
|       this.nodeTypes[tokenType] = nodeType | ||||
|       nodeArray.push(nodeType) | ||||
|     }) | ||||
|  | ||||
|     this.semanticTokens = this.parser.getSemanticTokens() | ||||
|     const styles = styleTags({ | ||||
|       number: tags.number, | ||||
|       variable: tags.variableName, | ||||
|       operator: tags.operator, | ||||
|       keyword: tags.keyword, | ||||
|       string: tags.string, | ||||
|       comment: tags.comment, | ||||
|       function: tags.function(tags.variableName), | ||||
|     }) | ||||
|     this.nodeSet = new NodeSet(nodeArray).extend(styles) | ||||
|   } | ||||
|  | ||||
|   get parsedPos(): number { | ||||
|     return 0 | ||||
|   } | ||||
|  | ||||
|   advance(): Tree | null { | ||||
|     if (this.semanticTokens.length === 0) { | ||||
|       return new Tree(NodeType.none, [], [], 0) | ||||
|     } | ||||
|     const tree = this.createTree(this.semanticTokens[0], 0) | ||||
|     this.stoppedAt = this.input.doc.length | ||||
|     return tree | ||||
|   } | ||||
|  | ||||
|   createTree(token: SemanticToken, index: number): Tree { | ||||
|     const changedLine = token.delta_line !== 0 | ||||
|     this.currentLine += token.delta_line | ||||
|     if (changedLine) { | ||||
|       this.currentColumn = 0 | ||||
|     } | ||||
|     this.currentColumn += token.delta_start | ||||
|  | ||||
|     // Let's get our position relative to the start of the file. | ||||
|     let currentPosition = posToOffset(this.input.doc, { | ||||
|       line: this.currentLine, | ||||
|       character: this.currentColumn, | ||||
|     }) | ||||
|  | ||||
|     const nodeType = this.nodeSet.types[this.nodeTypes[token.token_type].id] | ||||
|  | ||||
|     if (currentPosition === undefined) { | ||||
|       // This is bad and weird. | ||||
|       return new Tree(nodeType, [], [], token.length) | ||||
|     } | ||||
|  | ||||
|     if (index >= this.semanticTokens.length - 1) { | ||||
|       // We have no children. | ||||
|       return new Tree(nodeType, [], [], token.length) | ||||
|     } | ||||
|  | ||||
|     const nextIndex = index + 1 | ||||
|     const nextToken = this.semanticTokens[nextIndex] | ||||
|     const changedLineNext = nextToken.delta_line !== 0 | ||||
|     const nextLine = this.currentLine + nextToken.delta_line | ||||
|     const nextColumn = changedLineNext | ||||
|       ? nextToken.delta_start | ||||
|       : this.currentColumn + nextToken.delta_start | ||||
|     const nextPosition = posToOffset(this.input.doc, { | ||||
|       line: nextLine, | ||||
|       character: nextColumn, | ||||
|     }) | ||||
|  | ||||
|     if (nextPosition === undefined) { | ||||
|       // This is bad and weird. | ||||
|       return new Tree(nodeType, [], [], token.length) | ||||
|     } | ||||
|  | ||||
|     // Let's get the | ||||
|  | ||||
|     return new Tree( | ||||
|       nodeType, | ||||
|       [this.createTree(nextToken, nextIndex)], | ||||
|  | ||||
|       // The positions (offsets relative to the start of this tree) of the children. | ||||
|       [nextPosition - currentPosition], | ||||
|       token.length | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   stopAt(pos: number) { | ||||
|     this.stoppedAt = pos | ||||
|   } | ||||
| } | ||||
| @ -1,360 +0,0 @@ | ||||
| import { autocompletion, completeFromList } from '@codemirror/autocomplete' | ||||
| import { setDiagnostics } from '@codemirror/lint' | ||||
| import { Facet } from '@codemirror/state' | ||||
| import { | ||||
|   EditorView, | ||||
|   ViewPlugin, | ||||
|   Tooltip, | ||||
|   hoverTooltip, | ||||
|   tooltips, | ||||
| } from '@codemirror/view' | ||||
| import { | ||||
|   DiagnosticSeverity, | ||||
|   CompletionItemKind, | ||||
|   CompletionTriggerKind, | ||||
| } from 'vscode-languageserver-protocol' | ||||
|  | ||||
| import type { | ||||
|   Completion, | ||||
|   CompletionContext, | ||||
|   CompletionResult, | ||||
| } from '@codemirror/autocomplete' | ||||
| import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol' | ||||
| import type { ViewUpdate, PluginValue } from '@codemirror/view' | ||||
| import type { Text } from '@codemirror/state' | ||||
| import type * as LSP from 'vscode-languageserver-protocol' | ||||
| import { LanguageServerClient, Notification } from '.' | ||||
| import { Marked } from '@ts-stack/markdown' | ||||
|  | ||||
| const changesDelay = 500 | ||||
|  | ||||
| const CompletionItemKindMap = Object.fromEntries( | ||||
|   Object.entries(CompletionItemKind).map(([key, value]) => [value, key]) | ||||
| ) as Record<CompletionItemKind, string> | ||||
|  | ||||
| const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '') | ||||
| const documentUri = Facet.define<string, string>({ combine: useLast }) | ||||
| const languageId = Facet.define<string, string>({ combine: useLast }) | ||||
| const client = Facet.define<LanguageServerClient, LanguageServerClient>({ | ||||
|   combine: useLast, | ||||
| }) | ||||
|  | ||||
| export interface LanguageServerOptions { | ||||
|   workspaceFolders: LSP.WorkspaceFolder[] | null | ||||
|   documentUri: string | ||||
|   allowHTMLContent: boolean | ||||
|   client: LanguageServerClient | ||||
| } | ||||
|  | ||||
| export class LanguageServerPlugin implements PluginValue { | ||||
|   public client: LanguageServerClient | ||||
|  | ||||
|   private documentUri: string | ||||
|   private languageId: string | ||||
|   private documentVersion: number | ||||
|  | ||||
|   private changesTimeout: number | ||||
|  | ||||
|   constructor(private view: EditorView, private allowHTMLContent: boolean) { | ||||
|     this.client = this.view.state.facet(client) | ||||
|     this.documentUri = this.view.state.facet(documentUri) | ||||
|     this.languageId = this.view.state.facet(languageId) | ||||
|     this.documentVersion = 0 | ||||
|     this.changesTimeout = 0 | ||||
|  | ||||
|     this.client.attachPlugin(this) | ||||
|  | ||||
|     this.initialize({ | ||||
|       documentText: this.view.state.doc.toString(), | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   update({ docChanged }: ViewUpdate) { | ||||
|     if (!docChanged) return | ||||
|     if (this.changesTimeout) clearTimeout(this.changesTimeout) | ||||
|     this.changesTimeout = window.setTimeout(() => { | ||||
|       this.sendChange({ | ||||
|         documentText: this.view.state.doc.toString(), | ||||
|       }) | ||||
|     }, changesDelay) | ||||
|   } | ||||
|  | ||||
|   destroy() { | ||||
|     this.client.detachPlugin(this) | ||||
|   } | ||||
|  | ||||
|   async initialize({ documentText }: { documentText: string }) { | ||||
|     if (this.client.initializePromise) { | ||||
|       await this.client.initializePromise | ||||
|     } | ||||
|     this.client.textDocumentDidOpen({ | ||||
|       textDocument: { | ||||
|         uri: this.documentUri, | ||||
|         languageId: this.languageId, | ||||
|         text: documentText, | ||||
|         version: this.documentVersion, | ||||
|       }, | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   async sendChange({ documentText }: { documentText: string }) { | ||||
|     if (!this.client.ready) return | ||||
|     try { | ||||
|       await this.client.textDocumentDidChange({ | ||||
|         textDocument: { | ||||
|           uri: this.documentUri, | ||||
|           version: this.documentVersion++, | ||||
|         }, | ||||
|         contentChanges: [{ text: documentText }], | ||||
|       }) | ||||
|     } catch (e) { | ||||
|       console.error(e) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   requestDiagnostics(view: EditorView) { | ||||
|     this.sendChange({ documentText: view.state.doc.toString() }) | ||||
|   } | ||||
|  | ||||
|   async requestHoverTooltip( | ||||
|     view: EditorView, | ||||
|     { line, character }: { line: number; character: number } | ||||
|   ): Promise<Tooltip | null> { | ||||
|     if ( | ||||
|       !this.client.ready || | ||||
|       !this.client.getServerCapabilities().hoverProvider | ||||
|     ) | ||||
|       return null | ||||
|  | ||||
|     this.sendChange({ documentText: view.state.doc.toString() }) | ||||
|     const result = await this.client.textDocumentHover({ | ||||
|       textDocument: { uri: this.documentUri }, | ||||
|       position: { line, character }, | ||||
|     }) | ||||
|     if (!result) return null | ||||
|     const { contents, range } = result | ||||
|     let pos = posToOffset(view.state.doc, { line, character })! | ||||
|     let end: number | undefined | ||||
|     if (range) { | ||||
|       pos = posToOffset(view.state.doc, range.start)! | ||||
|       end = posToOffset(view.state.doc, range.end) | ||||
|     } | ||||
|     if (pos === null) return null | ||||
|     const dom = document.createElement('div') | ||||
|     dom.classList.add('documentation') | ||||
|     if (this.allowHTMLContent) dom.innerHTML = formatContents(contents) | ||||
|     else dom.textContent = formatContents(contents) | ||||
|     return { pos, end, create: (view) => ({ dom }), above: true } | ||||
|   } | ||||
|  | ||||
|   async requestCompletion( | ||||
|     context: CompletionContext, | ||||
|     { line, character }: { line: number; character: number }, | ||||
|     { | ||||
|       triggerKind, | ||||
|       triggerCharacter, | ||||
|     }: { | ||||
|       triggerKind: CompletionTriggerKind | ||||
|       triggerCharacter: string | undefined | ||||
|     } | ||||
|   ): Promise<CompletionResult | null> { | ||||
|     if ( | ||||
|       !this.client.ready || | ||||
|       !this.client.getServerCapabilities().completionProvider | ||||
|     ) | ||||
|       return null | ||||
|  | ||||
|     this.sendChange({ | ||||
|       documentText: context.state.doc.toString(), | ||||
|     }) | ||||
|  | ||||
|     const result = await this.client.textDocumentCompletion({ | ||||
|       textDocument: { uri: this.documentUri }, | ||||
|       position: { line, character }, | ||||
|       context: { | ||||
|         triggerKind, | ||||
|         triggerCharacter, | ||||
|       }, | ||||
|     }) | ||||
|  | ||||
|     if (!result) return null | ||||
|  | ||||
|     const items = 'items' in result ? result.items : result | ||||
|  | ||||
|     let options = items.map( | ||||
|       ({ | ||||
|         detail, | ||||
|         label, | ||||
|         labelDetails, | ||||
|         kind, | ||||
|         textEdit, | ||||
|         documentation, | ||||
|         deprecated, | ||||
|         insertText, | ||||
|         insertTextFormat, | ||||
|         sortText, | ||||
|         filterText, | ||||
|       }) => { | ||||
|         const completion: Completion & { | ||||
|           filterText: string | ||||
|           sortText?: string | ||||
|           apply: string | ||||
|         } = { | ||||
|           label, | ||||
|           detail: labelDetails ? labelDetails.detail : detail, | ||||
|           apply: label, | ||||
|           type: kind && CompletionItemKindMap[kind].toLowerCase(), | ||||
|           sortText: sortText ?? label, | ||||
|           filterText: filterText ?? label, | ||||
|         } | ||||
|         if (documentation) { | ||||
|           completion.info = () => { | ||||
|             const htmlString = formatContents(documentation) | ||||
|             const htmlNode = document.createElement('div') | ||||
|             htmlNode.style.display = 'contents' | ||||
|             htmlNode.innerHTML = htmlString | ||||
|             return { dom: htmlNode } | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         return completion | ||||
|       } | ||||
|     ) | ||||
|  | ||||
|     return completeFromList(options)(context) | ||||
|   } | ||||
|  | ||||
|   processNotification(notification: Notification) { | ||||
|     try { | ||||
|       switch (notification.method) { | ||||
|         case 'textDocument/publishDiagnostics': | ||||
|           this.processDiagnostics(notification.params) | ||||
|       } | ||||
|     } catch (error) { | ||||
|       console.error(error) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   processDiagnostics(params: PublishDiagnosticsParams) { | ||||
|     if (params.uri !== this.documentUri) return | ||||
|  | ||||
|     const diagnostics = params.diagnostics | ||||
|       .map(({ range, message, severity }) => ({ | ||||
|         from: posToOffset(this.view.state.doc, range.start)!, | ||||
|         to: posToOffset(this.view.state.doc, range.end)!, | ||||
|         severity: ( | ||||
|           { | ||||
|             [DiagnosticSeverity.Error]: 'error', | ||||
|             [DiagnosticSeverity.Warning]: 'warning', | ||||
|             [DiagnosticSeverity.Information]: 'info', | ||||
|             [DiagnosticSeverity.Hint]: 'info', | ||||
|           } as const | ||||
|         )[severity!], | ||||
|         message, | ||||
|       })) | ||||
|       .filter( | ||||
|         ({ from, to }) => | ||||
|           from !== null && to !== null && from !== undefined && to !== undefined | ||||
|       ) | ||||
|       .sort((a, b) => { | ||||
|         switch (true) { | ||||
|           case a.from < b.from: | ||||
|             return -1 | ||||
|           case a.from > b.from: | ||||
|             return 1 | ||||
|         } | ||||
|         return 0 | ||||
|       }) | ||||
|  | ||||
|     this.view.dispatch(setDiagnostics(this.view.state, diagnostics)) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function kclPlugin(options: LanguageServerOptions) { | ||||
|   let plugin: LanguageServerPlugin | null = null | ||||
|  | ||||
|   return [ | ||||
|     client.of(options.client), | ||||
|     documentUri.of(options.documentUri), | ||||
|     languageId.of('kcl'), | ||||
|     ViewPlugin.define( | ||||
|       (view) => | ||||
|         (plugin = new LanguageServerPlugin(view, options.allowHTMLContent)) | ||||
|     ), | ||||
|     hoverTooltip( | ||||
|       (view, pos) => | ||||
|         plugin?.requestHoverTooltip(view, offsetToPos(view.state.doc, pos)) ?? | ||||
|         null | ||||
|     ), | ||||
|     tooltips({ | ||||
|       position: 'absolute', | ||||
|     }), | ||||
|     autocompletion({ | ||||
|       override: [ | ||||
|         async (context) => { | ||||
|           if (plugin == null) return null | ||||
|  | ||||
|           const { state, pos, explicit } = context | ||||
|           const line = state.doc.lineAt(pos) | ||||
|           let trigKind: CompletionTriggerKind = CompletionTriggerKind.Invoked | ||||
|           let trigChar: string | undefined | ||||
|           if ( | ||||
|             !explicit && | ||||
|             plugin.client | ||||
|               .getServerCapabilities() | ||||
|               .completionProvider?.triggerCharacters?.includes( | ||||
|                 line.text[pos - line.from - 1] | ||||
|               ) | ||||
|           ) { | ||||
|             trigKind = CompletionTriggerKind.TriggerCharacter | ||||
|             trigChar = line.text[pos - line.from - 1] | ||||
|           } | ||||
|           if ( | ||||
|             trigKind === CompletionTriggerKind.Invoked && | ||||
|             !context.matchBefore(/\w+$/) | ||||
|           ) { | ||||
|             return null | ||||
|           } | ||||
|           return await plugin.requestCompletion( | ||||
|             context, | ||||
|             offsetToPos(state.doc, pos), | ||||
|             { | ||||
|               triggerKind: trigKind, | ||||
|               triggerCharacter: trigChar, | ||||
|             } | ||||
|           ) | ||||
|         }, | ||||
|       ], | ||||
|     }), | ||||
|   ] | ||||
| } | ||||
|  | ||||
| export function posToOffset( | ||||
|   doc: Text, | ||||
|   pos: { line: number; character: number } | ||||
| ): number | undefined { | ||||
|   if (pos.line >= doc.lines) return | ||||
|   const offset = doc.line(pos.line + 1).from + pos.character | ||||
|   if (offset > doc.length) return | ||||
|   return offset | ||||
| } | ||||
|  | ||||
| function offsetToPos(doc: Text, offset: number) { | ||||
|   const line = doc.lineAt(offset) | ||||
|   return { | ||||
|     line: line.number - 1, | ||||
|     character: offset - line.from, | ||||
|   } | ||||
| } | ||||
|  | ||||
| function formatContents( | ||||
|   contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[] | ||||
| ): string { | ||||
|   if (Array.isArray(contents)) { | ||||
|     return contents.map((c) => formatContents(c) + '\n\n').join('') | ||||
|   } else if (typeof contents === 'string') { | ||||
|     return Marked.parse(contents) | ||||
|   } else { | ||||
|     return Marked.parse(contents.value) | ||||
|   } | ||||
| } | ||||
| @ -1,51 +0,0 @@ | ||||
| import type * as LSP from 'vscode-languageserver-protocol' | ||||
|  | ||||
| export class SemanticToken { | ||||
|   delta_line: number | ||||
|   delta_start: number | ||||
|   length: number | ||||
|   token_type: string | ||||
|   token_modifiers_bitset: string | ||||
|  | ||||
|   constructor( | ||||
|     delta_line = 0, | ||||
|     delta_start = 0, | ||||
|     length = 0, | ||||
|     token_type = '', | ||||
|     token_modifiers_bitset = '' | ||||
|   ) { | ||||
|     this.delta_line = delta_line | ||||
|     this.delta_start = delta_start | ||||
|     this.length = length | ||||
|     this.token_type = token_type | ||||
|     this.token_modifiers_bitset = token_modifiers_bitset | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function deserializeTokens( | ||||
|   data: number[], | ||||
|   semanticTokensProvider?: LSP.SemanticTokensOptions | ||||
| ): SemanticToken[] { | ||||
|   if (!semanticTokensProvider) { | ||||
|     return [] | ||||
|   } | ||||
|   // Check if data length is divisible by 5 | ||||
|   if (data.length % 5 !== 0) { | ||||
|     throw new Error('Length is not divisible by 5') | ||||
|   } | ||||
|  | ||||
|   const tokens = [] | ||||
|   for (let i = 0; i < data.length; i += 5) { | ||||
|     tokens.push( | ||||
|       new SemanticToken( | ||||
|         data[i], | ||||
|         data[i + 1], | ||||
|         data[i + 2], | ||||
|         semanticTokensProvider.legend.tokenTypes[data[i + 3]], | ||||
|         semanticTokensProvider.legend.tokenModifiers[data[i + 4]] | ||||
|       ) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   return tokens | ||||
| } | ||||
| @ -1,80 +0,0 @@ | ||||
| import { | ||||
|   Registration, | ||||
|   ServerCapabilities, | ||||
|   Unregistration, | ||||
| } from 'vscode-languageserver-protocol' | ||||
|  | ||||
| interface IFlexibleServerCapabilities extends ServerCapabilities { | ||||
|   [key: string]: any | ||||
| } | ||||
|  | ||||
| interface IMethodServerCapabilityProviderDictionary { | ||||
|   [key: string]: string | ||||
| } | ||||
|  | ||||
| const ServerCapabilitiesProviders: IMethodServerCapabilityProviderDictionary = { | ||||
|   'textDocument/hover': 'hoverProvider', | ||||
|   'textDocument/completion': 'completionProvider', | ||||
|   'textDocument/signatureHelp': 'signatureHelpProvider', | ||||
|   'textDocument/definition': 'definitionProvider', | ||||
|   'textDocument/typeDefinition': 'typeDefinitionProvider', | ||||
|   'textDocument/implementation': 'implementationProvider', | ||||
|   'textDocument/references': 'referencesProvider', | ||||
|   'textDocument/documentHighlight': 'documentHighlightProvider', | ||||
|   'textDocument/documentSymbol': 'documentSymbolProvider', | ||||
|   'textDocument/workspaceSymbol': 'workspaceSymbolProvider', | ||||
|   'textDocument/codeAction': 'codeActionProvider', | ||||
|   'textDocument/codeLens': 'codeLensProvider', | ||||
|   'textDocument/documentFormatting': 'documentFormattingProvider', | ||||
|   'textDocument/documentRangeFormatting': 'documentRangeFormattingProvider', | ||||
|   'textDocument/documentOnTypeFormatting': 'documentOnTypeFormattingProvider', | ||||
|   'textDocument/rename': 'renameProvider', | ||||
|   'textDocument/documentLink': 'documentLinkProvider', | ||||
|   'textDocument/color': 'colorProvider', | ||||
|   'textDocument/foldingRange': 'foldingRangeProvider', | ||||
|   'textDocument/declaration': 'declarationProvider', | ||||
|   'textDocument/executeCommand': 'executeCommandProvider', | ||||
| } | ||||
|  | ||||
| function registerServerCapability( | ||||
|   serverCapabilities: ServerCapabilities, | ||||
|   registration: Registration | ||||
| ): ServerCapabilities { | ||||
|   const serverCapabilitiesCopy = JSON.parse( | ||||
|     JSON.stringify(serverCapabilities) | ||||
|   ) as IFlexibleServerCapabilities | ||||
|   const { method, registerOptions } = registration | ||||
|   const providerName = ServerCapabilitiesProviders[method] | ||||
|  | ||||
|   if (providerName) { | ||||
|     if (!registerOptions) { | ||||
|       serverCapabilitiesCopy[providerName] = true | ||||
|     } else { | ||||
|       serverCapabilitiesCopy[providerName] = Object.assign( | ||||
|         {}, | ||||
|         JSON.parse(JSON.stringify(registerOptions)) | ||||
|       ) | ||||
|     } | ||||
|   } else { | ||||
|     throw new Error('Could not register server capability.') | ||||
|   } | ||||
|  | ||||
|   return serverCapabilitiesCopy | ||||
| } | ||||
|  | ||||
| function unregisterServerCapability( | ||||
|   serverCapabilities: ServerCapabilities, | ||||
|   unregistration: Unregistration | ||||
| ): ServerCapabilities { | ||||
|   const serverCapabilitiesCopy = JSON.parse( | ||||
|     JSON.stringify(serverCapabilities) | ||||
|   ) as IFlexibleServerCapabilities | ||||
|   const { method } = unregistration | ||||
|   const providerName = ServerCapabilitiesProviders[method] | ||||
|  | ||||
|   delete serverCapabilitiesCopy[providerName] | ||||
|  | ||||
|   return serverCapabilitiesCopy | ||||
| } | ||||
|  | ||||
| export { registerServerCapability, unregisterServerCapability } | ||||
| @ -1,42 +0,0 @@ | ||||
| import init, { | ||||
|   InitOutput, | ||||
|   lsp_run, | ||||
|   ServerConfig, | ||||
| } from '../../wasm-lib/pkg/wasm_lib' | ||||
| import { FromServer, IntoServer } from './codec' | ||||
|  | ||||
| let server: null | Server | ||||
|  | ||||
| export default class Server { | ||||
|   readonly initOutput: InitOutput | ||||
|   readonly #intoServer: IntoServer | ||||
|   readonly #fromServer: FromServer | ||||
|  | ||||
|   private constructor( | ||||
|     initOutput: InitOutput, | ||||
|     intoServer: IntoServer, | ||||
|     fromServer: FromServer | ||||
|   ) { | ||||
|     this.initOutput = initOutput | ||||
|     this.#intoServer = intoServer | ||||
|     this.#fromServer = fromServer | ||||
|   } | ||||
|  | ||||
|   static async initialize( | ||||
|     intoServer: IntoServer, | ||||
|     fromServer: FromServer | ||||
|   ): Promise<Server> { | ||||
|     if (null == server) { | ||||
|       const initOutput = await init() | ||||
|       server = new Server(initOutput, intoServer, fromServer) | ||||
|     } else { | ||||
|       console.warn('Server already initialized; ignoring') | ||||
|     } | ||||
|     return server | ||||
|   } | ||||
|  | ||||
|   async start(): Promise<void> { | ||||
|     const config = new ServerConfig(this.#intoServer, this.#fromServer) | ||||
|     await lsp_run(config) | ||||
|   } | ||||
| } | ||||
| @ -1,21 +0,0 @@ | ||||
| import { Message } from 'vscode-languageserver-protocol' | ||||
|  | ||||
| const env = import.meta.env.MODE | ||||
|  | ||||
| export default class Tracer { | ||||
|   static client(message: string): void { | ||||
|     // These are really noisy, so we have a special env var for them. | ||||
|     if (env === 'lsp_tracing') { | ||||
|       console.log('lsp client message', message) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   static server(input: string | Message): void { | ||||
|     // These are really noisy, so we have a special env var for them. | ||||
|     if (env === 'lsp_tracing') { | ||||
|       const message: string = | ||||
|         typeof input === 'string' ? input : JSON.stringify(input) | ||||
|       console.log('lsp server message', message) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -8,9 +8,4 @@ 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 VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS = import.meta.env | ||||
|   .VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS | ||||
| export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env | ||||
|   .VITE_KC_CONNECTION_TIMEOUT_MS | ||||
| export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN | ||||
| export const TEST = import.meta.env.TEST | ||||
|  | ||||
							
								
								
									
										54
									
								
								src/hooks/useAuthMachine.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/hooks/useAuthMachine.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | ||||
| import { createActorContext } from '@xstate/react' | ||||
| import { useNavigate } from 'react-router-dom' | ||||
| import { paths } from '../Router' | ||||
| import { authMachine, TOKEN_PERSIST_KEY } from '../lib/authMachine' | ||||
| import withBaseUrl from '../lib/withBaseURL' | ||||
|  | ||||
| export const AuthMachineContext = createActorContext(authMachine) | ||||
|  | ||||
| export const GlobalStateProvider = ({ | ||||
|   children, | ||||
| }: { | ||||
|   children: React.ReactNode | ||||
| }) => { | ||||
|   const navigate = useNavigate() | ||||
|   return ( | ||||
|     <AuthMachineContext.Provider | ||||
|       machine={() => | ||||
|         authMachine.withConfig({ | ||||
|           actions: { | ||||
|             goToSignInPage: () => { | ||||
|               navigate(paths.SIGN_IN) | ||||
|               logout() | ||||
|             }, | ||||
|             goToIndexPage: () => navigate(paths.INDEX), | ||||
|           }, | ||||
|         }) | ||||
|       } | ||||
|     > | ||||
|       {children} | ||||
|     </AuthMachineContext.Provider> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export function useAuthMachine<T>( | ||||
|   selector: ( | ||||
|     state: Parameters<Parameters<typeof AuthMachineContext.useSelector>[0]>[0] | ||||
|   ) => T = () => null as T | ||||
| ): [T, ReturnType<typeof AuthMachineContext.useActor>[1]] { | ||||
|   // useActor api normally `[state, send] = useActor` | ||||
|   // we're only interested in send because of the selector | ||||
|   const send = AuthMachineContext.useActor()[1] | ||||
|  | ||||
|   const selection = AuthMachineContext.useSelector(selector) | ||||
|   return [selection, send] | ||||
| } | ||||
|  | ||||
| export function logout() { | ||||
|   const url = withBaseUrl('/logout') | ||||
|   localStorage.removeItem(TOKEN_PERSIST_KEY) | ||||
|   return fetch(url, { | ||||
|     method: 'POST', | ||||
|     credentials: 'include', | ||||
|   }) | ||||
| } | ||||
| @ -1,6 +0,0 @@ | ||||
| import { CommandsContext } from 'components/CommandBar' | ||||
| import { useContext } from 'react' | ||||
|  | ||||
| export const useCommandsContext = () => { | ||||
|   return useContext(CommandsContext) | ||||
| } | ||||
| @ -1,6 +0,0 @@ | ||||
| import { GlobalStateContext } from 'components/GlobalStateProvider' | ||||
| import { useContext } from 'react' | ||||
|  | ||||
| export const useGlobalStateContext = () => { | ||||
|   return useContext(GlobalStateContext) | ||||
| } | ||||
| @ -1,42 +0,0 @@ | ||||
| import { useEffect } from 'react' | ||||
| import { AnyStateMachine, StateFrom } from 'xstate' | ||||
| import { Command, CommandBarMeta, createMachineCommand } from '../lib/commands' | ||||
| import { useCommandsContext } from './useCommandsContext' | ||||
|  | ||||
| interface UseStateMachineCommandsArgs<T extends AnyStateMachine> { | ||||
|   state: StateFrom<T> | ||||
|   send: Function | ||||
|   commandBarMeta?: CommandBarMeta | ||||
|   commands: Command[] | ||||
|   owner: string | ||||
| } | ||||
|  | ||||
| export default function useStateMachineCommands<T extends AnyStateMachine>({ | ||||
|   state, | ||||
|   send, | ||||
|   commandBarMeta, | ||||
|   owner, | ||||
| }: UseStateMachineCommandsArgs<T>) { | ||||
|   const { addCommands, removeCommands } = useCommandsContext() | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const newCommands = state.nextEvents | ||||
|       .filter((e) => !['done.', 'error.'].some((n) => e.includes(n))) | ||||
|       .map((type) => | ||||
|         createMachineCommand<T>({ | ||||
|           type, | ||||
|           state, | ||||
|           send, | ||||
|           commandBarMeta, | ||||
|           owner, | ||||
|         }) | ||||
|       ) | ||||
|       .filter((c) => c !== null) as Command[] | ||||
|  | ||||
|     addCommands(newCommands) | ||||
|  | ||||
|     return () => { | ||||
|       removeCommands(newCommands) | ||||
|     } | ||||
|   }, [state]) | ||||
| } | ||||
							
								
								
									
										67
									
								
								src/hooks/useTauriBoot.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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() | ||||
|   }, []) | ||||
| } | ||||
| @ -82,36 +82,12 @@ code { | ||||
|     monospace; | ||||
| } | ||||
|  | ||||
| .full-height-subtract { | ||||
|   --height-subtract: 2.25rem; | ||||
|   height: 100%; | ||||
|   max-height: calc(100% - var(--height-subtract)); | ||||
| } | ||||
|  | ||||
| #code-mirror-override .cm-editor { | ||||
|   @apply h-full bg-transparent; | ||||
| } | ||||
|  | ||||
| #code-mirror-override .cm-scroller { | ||||
|   @apply h-full; | ||||
| } | ||||
|  | ||||
| #code-mirror-override .cm-scroller::-webkit-scrollbar { | ||||
|   @apply h-0; | ||||
| } | ||||
|  | ||||
| #code-mirror-override .cm-activeLine, | ||||
| #code-mirror-override .cm-activeLineGutter { | ||||
|   @apply bg-liquid-10/50; | ||||
| } | ||||
|  | ||||
| .dark #code-mirror-override .cm-activeLine, | ||||
| .dark #code-mirror-override .cm-activeLineGutter { | ||||
|   @apply bg-liquid-80/50; | ||||
|   @apply bg-transparent; | ||||
| } | ||||
|  | ||||
| #code-mirror-override .cm-gutters { | ||||
|   @apply bg-chalkboard-10/30; | ||||
|   @apply bg-chalkboard-10/50; | ||||
| } | ||||
|  | ||||
| .dark #code-mirror-override .cm-gutters { | ||||
| @ -123,68 +99,16 @@ code { | ||||
| } | ||||
| #code-mirror-override .cm-cursor { | ||||
|   display: block; | ||||
|   width: 1ch; | ||||
|   @apply bg-liquid-40 mix-blend-multiply; | ||||
|  | ||||
|   animation: blink 2s ease-out infinite; | ||||
| } | ||||
|  | ||||
| .dark #code-mirror-override .cm-cursor { | ||||
|   @apply bg-liquid-50; | ||||
| } | ||||
|  | ||||
| @keyframes blink { | ||||
|   0%, | ||||
|   100% { | ||||
|     opacity: 0; | ||||
|   } | ||||
|   15% { | ||||
|     opacity: 0.75; | ||||
|   } | ||||
|   width: 200px; | ||||
|   background: linear-gradient( | ||||
|     to right, | ||||
|     rgb(0, 55, 94) 0%, | ||||
|     #0084e2ff 2%, | ||||
|     #0084e255 5%, | ||||
|     transparent 100% | ||||
|   ); | ||||
| } | ||||
|  | ||||
| .react-json-view { | ||||
|   @apply bg-transparent !important; | ||||
| } | ||||
|  | ||||
| #code-mirror-override .cm-tooltip { | ||||
|   @apply text-xs shadow-md; | ||||
|   @apply bg-chalkboard-10 text-chalkboard-80; | ||||
|   @apply rounded-sm border-solid border border-chalkboard-40/30 border-l-liquid-10; | ||||
| } | ||||
|  | ||||
| .dark #code-mirror-override .cm-tooltip { | ||||
|   @apply bg-chalkboard-110 text-chalkboard-40; | ||||
|   @apply border-chalkboard-70/20 border-l-liquid-70; | ||||
| } | ||||
|  | ||||
| #code-mirror-override .cm-tooltip-hover { | ||||
|   @apply py-1 px-2 w-max max-w-md; | ||||
| } | ||||
|  | ||||
| #code-mirror-override .cm-completionInfo { | ||||
|   @apply px-4 rounded-l-none; | ||||
|   @apply bg-chalkboard-10 text-liquid-90; | ||||
|   @apply border-liquid-40/30; | ||||
| } | ||||
|  | ||||
| .dark #code-mirror-override .cm-completionInfo { | ||||
|   @apply bg-liquid-120 text-liquid-50; | ||||
|   @apply border-liquid-90/60; | ||||
| } | ||||
|  | ||||
| #code-mirror-override .cm-tooltip-autocomplete li { | ||||
|   @apply px-2 py-1; | ||||
| } | ||||
| #code-mirror-override .cm-tooltip-autocomplete li[aria-selected='true'] { | ||||
|   @apply bg-liquid-10 text-liquid-110; | ||||
| } | ||||
| .dark #code-mirror-override .cm-tooltip-autocomplete li[aria-selected='true'] { | ||||
|   @apply bg-liquid-100 text-liquid-20; | ||||
| } | ||||
|  | ||||
| #code-mirror-override .cm-content { | ||||
|   white-space: pre-wrap; | ||||
|   word-break: normal; | ||||
|   word-wrap: break-word; | ||||
| } | ||||
|  | ||||
| @ -2,10 +2,23 @@ import ReactDOM from 'react-dom/client' | ||||
| import './index.css' | ||||
| 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( | ||||
|   <HotkeysProvider> | ||||
|  | ||||
| @ -179,9 +179,6 @@ const newVar = myVar + 1 | ||||
|               name: 'aIdentifier', | ||||
|             }, | ||||
|           ], | ||||
|           function: { | ||||
|             type: 'InMemory', | ||||
|           }, | ||||
|           optional: false, | ||||
|         }, | ||||
|       }, | ||||
| @ -214,8 +211,10 @@ describe('testing function declaration', () => { | ||||
|               type: 'FunctionExpression', | ||||
|               start: 11, | ||||
|               end: 19, | ||||
|               id: null, | ||||
|               params: [], | ||||
|               body: { | ||||
|                 type: 'BlockStatement', | ||||
|                 start: 17, | ||||
|                 end: 19, | ||||
|                 body: [], | ||||
| @ -252,6 +251,7 @@ describe('testing function declaration', () => { | ||||
|               type: 'FunctionExpression', | ||||
|               start: 11, | ||||
|               end: 39, | ||||
|               id: null, | ||||
|               params: [ | ||||
|                 { | ||||
|                   type: 'Identifier', | ||||
| @ -267,6 +267,7 @@ describe('testing function declaration', () => { | ||||
|                 }, | ||||
|               ], | ||||
|               body: { | ||||
|                 type: 'BlockStatement', | ||||
|                 start: 21, | ||||
|                 end: 39, | ||||
|                 body: [ | ||||
| @ -327,6 +328,7 @@ const myVar = funcN(1, 2)` | ||||
|               type: 'FunctionExpression', | ||||
|               start: 11, | ||||
|               end: 37, | ||||
|               id: null, | ||||
|               params: [ | ||||
|                 { | ||||
|                   type: 'Identifier', | ||||
| @ -342,6 +344,7 @@ const myVar = funcN(1, 2)` | ||||
|                 }, | ||||
|               ], | ||||
|               body: { | ||||
|                 type: 'BlockStatement', | ||||
|                 start: 21, | ||||
|                 end: 37, | ||||
|                 body: [ | ||||
| @ -416,9 +419,6 @@ const myVar = funcN(1, 2)` | ||||
|                   raw: '2', | ||||
|                 }, | ||||
|               ], | ||||
|               function: { | ||||
|                 type: 'InMemory', | ||||
|               }, | ||||
|               optional: false, | ||||
|             }, | ||||
|           }, | ||||
| @ -488,7 +488,6 @@ describe('testing pipe operator special', () => { | ||||
|                       ], | ||||
|                     }, | ||||
|                   ], | ||||
|                   function: expect.any(Object), | ||||
|                   optional: false, | ||||
|                 }, | ||||
|                 { | ||||
| @ -525,7 +524,6 @@ describe('testing pipe operator special', () => { | ||||
|                     }, | ||||
|                     { type: 'PipeSubstitution', start: 59, end: 60 }, | ||||
|                   ], | ||||
|                   function: expect.any(Object), | ||||
|                   optional: false, | ||||
|                 }, | ||||
|                 { | ||||
| @ -598,7 +596,6 @@ describe('testing pipe operator special', () => { | ||||
|                     }, | ||||
|                     { type: 'PipeSubstitution', start: 105, end: 106 }, | ||||
|                   ], | ||||
|                   function: expect.any(Object), | ||||
|                   optional: false, | ||||
|                 }, | ||||
|                 { | ||||
| @ -635,7 +632,6 @@ describe('testing pipe operator special', () => { | ||||
|                     }, | ||||
|                     { type: 'PipeSubstitution', start: 128, end: 129 }, | ||||
|                   ], | ||||
|                   function: expect.any(Object), | ||||
|                   optional: false, | ||||
|                 }, | ||||
|                 { | ||||
| @ -658,9 +654,6 @@ describe('testing pipe operator special', () => { | ||||
|                     }, | ||||
|                     { type: 'PipeSubstitution', start: 143, end: 144 }, | ||||
|                   ], | ||||
|                   function: { | ||||
|                     type: 'InMemory', | ||||
|                   }, | ||||
|                   optional: false, | ||||
|                 }, | ||||
|               ], | ||||
| @ -740,9 +733,6 @@ describe('testing pipe operator special', () => { | ||||
|                       end: 35, | ||||
|                     }, | ||||
|                   ], | ||||
|                   function: { | ||||
|                     type: 'InMemory', | ||||
|                   }, | ||||
|                   optional: false, | ||||
|                 }, | ||||
|               ], | ||||
| @ -1563,10 +1553,7 @@ const key = 'c'` | ||||
|       type: 'NoneCodeNode', | ||||
|       start: code.indexOf('\n// this is a comment'), | ||||
|       end: code.indexOf('const key'), | ||||
|       value: { | ||||
|         type: 'blockComment', | ||||
|         value: 'this is a comment', | ||||
|       }, | ||||
|       value: '\n// this is a comment\n', | ||||
|     } | ||||
|     const { nonCodeMeta } = parser_wasm(code) | ||||
|     expect(nonCodeMeta.noneCodeNodes[0]).toEqual(nonCodeMetaInstance) | ||||
| @ -1576,9 +1563,7 @@ const key = 'c'` | ||||
|     const { nonCodeMeta: nonCodeMeta2 } = parser_wasm( | ||||
|       codeWithExtraStartWhitespace | ||||
|     ) | ||||
|     expect(nonCodeMeta2.noneCodeNodes[0].value).toStrictEqual( | ||||
|       nonCodeMetaInstance.value | ||||
|     ) | ||||
|     expect(nonCodeMeta2.noneCodeNodes[0].value).toBe(nonCodeMetaInstance.value) | ||||
|     expect(nonCodeMeta2.noneCodeNodes[0].start).not.toBe( | ||||
|       nonCodeMetaInstance.start | ||||
|     ) | ||||
| @ -1586,8 +1571,8 @@ const key = 'c'` | ||||
|   it('comments nested within a block statement', () => { | ||||
|     const code = `const mySketch = startSketchAt([0,0]) | ||||
|   |> lineTo({ to: [0, 1], tag: 'myPath' }, %) | ||||
|   |> lineTo([1, 1], %) /* this is | ||||
|       a comment | ||||
|   |> lineTo([1, 1], %) /* this is  | ||||
|       a comment  | ||||
|       spanning a few lines */ | ||||
|   |> lineTo({ to: [1,0], tag: "rightPath" }, %) | ||||
|   |> close(%) | ||||
| @ -1600,11 +1585,9 @@ const key = 'c'` | ||||
|     expect(sketchNonCodeMeta[indexOfSecondLineToExpression]).toEqual({ | ||||
|       type: 'NoneCodeNode', | ||||
|       start: 106, | ||||
|       end: 166, | ||||
|       value: { | ||||
|         type: 'blockComment', | ||||
|         value: 'this is\n      a comment\n      spanning a few lines', | ||||
|       }, | ||||
|       end: 168, | ||||
|       value: | ||||
|         ' /* this is \n      a comment \n      spanning a few lines */\n  ', | ||||
|     }) | ||||
|   }) | ||||
|   it('comments in a pipe expression', () => { | ||||
| @ -1624,10 +1607,7 @@ const key = 'c'` | ||||
|       type: 'NoneCodeNode', | ||||
|       start: 125, | ||||
|       end: 141, | ||||
|       value: { | ||||
|         type: 'blockComment', | ||||
|         value: 'a comment', | ||||
|       }, | ||||
|       value: '\n// a comment\n  ', | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
| @ -1651,7 +1631,6 @@ describe('test UnaryExpression', () => { | ||||
|           { type: 'Literal', start: 19, end: 20, value: 4, raw: '4' }, | ||||
|           { type: 'Literal', start: 22, end: 25, value: 100, raw: '100' }, | ||||
|         ], | ||||
|         function: expect.any(Object), | ||||
|         optional: false, | ||||
|       }, | ||||
|     }) | ||||
| @ -1685,12 +1664,10 @@ describe('testing nested call expressions', () => { | ||||
|               { type: 'Literal', start: 34, end: 35, value: 5, raw: '5' }, | ||||
|               { type: 'Literal', start: 37, end: 38, value: 3, raw: '3' }, | ||||
|             ], | ||||
|             function: expect.any(Object), | ||||
|             optional: false, | ||||
|           }, | ||||
|         }, | ||||
|       ], | ||||
|       function: expect.any(Object), | ||||
|       optional: false, | ||||
|     }) | ||||
|   }) | ||||
| @ -1722,7 +1699,6 @@ describe('should recognise callExpresions in binaryExpressions', () => { | ||||
|             }, | ||||
|             { type: 'PipeSubstitution', start: 25, end: 26 }, | ||||
|           ], | ||||
|           function: expect.any(Object), | ||||
|           optional: false, | ||||
|         }, | ||||
|         right: { type: 'Literal', value: 1, raw: '1', start: 30, end: 31 }, | ||||
|  | ||||
| @ -3,9 +3,9 @@ import { parse_js } from '../wasm-lib/pkg/wasm_lib' | ||||
| import { initPromise } from './rust' | ||||
| import { Token } from './tokeniser' | ||||
| import { KCLError } from './errors' | ||||
| import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' | ||||
| import { KclError as RustKclError } from '../wasm-lib/bindings/KclError' | ||||
|  | ||||
| export const rangeTypeFix = (ranges: number[][]): [number, number][] => | ||||
| const rangeTypeFix = (ranges: number[][]): [number, number][] => | ||||
|   ranges.map(([start, end]) => [start, end]) | ||||
|  | ||||
| export const parser_wasm = (code: string): Program => { | ||||
| @ -16,8 +16,10 @@ export const parser_wasm = (code: string): Program => { | ||||
|     const parsed: RustKclError = JSON.parse(e.toString()) | ||||
|     const kclError = new KCLError( | ||||
|       parsed.kind, | ||||
|       parsed.msg, | ||||
|       rangeTypeFix(parsed.sourceRanges) | ||||
|       parsed.kind === 'invalid_expression' ? parsed.kind : parsed.msg, | ||||
|       parsed.kind === 'invalid_expression' | ||||
|         ? [[parsed.start, parsed.end]] | ||||
|         : rangeTypeFix(parsed.sourceRanges) | ||||
|     ) | ||||
|  | ||||
|     console.log(kclError) | ||||
| @ -34,8 +36,10 @@ export async function asyncParser(code: string): Promise<Program> { | ||||
|     const parsed: RustKclError = JSON.parse(e.toString()) | ||||
|     const kclError = new KCLError( | ||||
|       parsed.kind, | ||||
|       parsed.msg, | ||||
|       rangeTypeFix(parsed.sourceRanges) | ||||
|       parsed.kind === 'invalid_expression' ? parsed.kind : parsed.msg, | ||||
|       parsed.kind === 'invalid_expression' | ||||
|         ? [[parsed.start, parsed.end]] | ||||
|         : rangeTypeFix(parsed.sourceRanges) | ||||
|     ) | ||||
|  | ||||
|     console.log(kclError) | ||||
|  | ||||
| @ -1,20 +1,20 @@ | ||||
| export type { Program } from '../wasm-lib/kcl/bindings/Program' | ||||
| export type { Value } from '../wasm-lib/kcl/bindings/Value' | ||||
| export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression' | ||||
| export type { MemberExpression } from '../wasm-lib/kcl/bindings/MemberExpression' | ||||
| export type { PipeExpression } from '../wasm-lib/kcl/bindings/PipeExpression' | ||||
| export type { VariableDeclaration } from '../wasm-lib/kcl/bindings/VariableDeclaration' | ||||
| export type { PipeSubstitution } from '../wasm-lib/kcl/bindings/PipeSubstitution' | ||||
| export type { Identifier } from '../wasm-lib/kcl/bindings/Identifier' | ||||
| export type { UnaryExpression } from '../wasm-lib/kcl/bindings/UnaryExpression' | ||||
| export type { BinaryExpression } from '../wasm-lib/kcl/bindings/BinaryExpression' | ||||
| export type { ReturnStatement } from '../wasm-lib/kcl/bindings/ReturnStatement' | ||||
| export type { ExpressionStatement } from '../wasm-lib/kcl/bindings/ExpressionStatement' | ||||
| export type { CallExpression } from '../wasm-lib/kcl/bindings/CallExpression' | ||||
| export type { VariableDeclarator } from '../wasm-lib/kcl/bindings/VariableDeclarator' | ||||
| export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart' | ||||
| export type { Literal } from '../wasm-lib/kcl/bindings/Literal' | ||||
| export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression' | ||||
| export type { Program } from '../wasm-lib/bindings/Program' | ||||
| export type { Value } from '../wasm-lib/bindings/Value' | ||||
| export type { ObjectExpression } from '../wasm-lib/bindings/ObjectExpression' | ||||
| export type { MemberExpression } from '../wasm-lib/bindings/MemberExpression' | ||||
| export type { PipeExpression } from '../wasm-lib/bindings/PipeExpression' | ||||
| export type { VariableDeclaration } from '../wasm-lib/bindings/VariableDeclaration' | ||||
| export type { PipeSubstitution } from '../wasm-lib/bindings/PipeSubstitution' | ||||
| export type { Identifier } from '../wasm-lib/bindings/Identifier' | ||||
| export type { UnaryExpression } from '../wasm-lib/bindings/UnaryExpression' | ||||
| export type { BinaryExpression } from '../wasm-lib/bindings/BinaryExpression' | ||||
| export type { ReturnStatement } from '../wasm-lib/bindings/ReturnStatement' | ||||
| export type { ExpressionStatement } from '../wasm-lib/bindings/ExpressionStatement' | ||||
| export type { CallExpression } from '../wasm-lib/bindings/CallExpression' | ||||
| export type { VariableDeclarator } from '../wasm-lib/bindings/VariableDeclarator' | ||||
| export type { BinaryPart } from '../wasm-lib/bindings/BinaryPart' | ||||
| export type { Literal } from '../wasm-lib/bindings/Literal' | ||||
| export type { ArrayExpression } from '../wasm-lib/bindings/ArrayExpression' | ||||
|  | ||||
| export type SyntaxType = | ||||
|   | 'Program' | ||||
| @ -22,6 +22,7 @@ export type SyntaxType = | ||||
|   | 'BinaryExpression' | ||||
|   | 'CallExpression' | ||||
|   | 'Identifier' | ||||
|   | 'BlockStatement' | ||||
|   | 'ReturnStatement' | ||||
|   | 'VariableDeclaration' | ||||
|   | 'VariableDeclarator' | ||||
|  | ||||
| @ -11,52 +11,51 @@ describe('testing artifacts', () => { | ||||
| const mySketch001 = startSketchAt([0, 0]) | ||||
|   |> lineTo([-1.59, -1.54], %) | ||||
|   |> lineTo([0.46, -5.82], %) | ||||
|   // |> rx(45, %) | ||||
|   // |> rx(45, %)  | ||||
| show(mySketch001)` | ||||
|     const programMemory = await enginelessExecutor(parser_wasm(code)) | ||||
|     // @ts-ignore | ||||
|     const shown = programMemory?.return?.map( | ||||
|       // @ts-ignore | ||||
|       (a) => programMemory?.root?.[a.name] | ||||
|     ) | ||||
|     expect(shown).toEqual([ | ||||
|       { | ||||
|         type: 'sketchGroup', | ||||
|         start: { | ||||
|           type: 'base', | ||||
|           to: [0, 0], | ||||
|           from: [0, 0], | ||||
|           name: '', | ||||
|           __geoMeta: { | ||||
|             id: expect.any(String), | ||||
|             id: '66366561-6465-4734-a463-366330356563', | ||||
|             sourceRange: [21, 42], | ||||
|             pathToNode: [], | ||||
|           }, | ||||
|         }, | ||||
|         value: [ | ||||
|           { | ||||
|             type: 'toPoint', | ||||
|             name: '', | ||||
|             to: [-1.59, -1.54], | ||||
|             from: [0, 0], | ||||
|             __geoMeta: { | ||||
|               sourceRange: [48, 73], | ||||
|               id: expect.any(String), | ||||
|               id: '30366338-6462-4330-a364-303935626163', | ||||
|               pathToNode: [], | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             type: 'toPoint', | ||||
|             to: [0.46, -5.82], | ||||
|             from: [-1.59, -1.54], | ||||
|             name: '', | ||||
|             __geoMeta: { | ||||
|               sourceRange: [79, 103], | ||||
|               id: expect.any(String), | ||||
|               id: '32653334-6331-4231-b162-663334363535', | ||||
|               pathToNode: [], | ||||
|             }, | ||||
|           }, | ||||
|         ], | ||||
|         position: [0, 0, 0], | ||||
|         rotation: [0, 0, 0, 1], | ||||
|         id: expect.any(String), | ||||
|         __meta: [{ sourceRange: [21, 42] }], | ||||
|         id: '39643164-6130-4734-b432-623638393262', | ||||
|         __meta: [{ sourceRange: [21, 42], pathToNode: [] }], | ||||
|       }, | ||||
|     ]) | ||||
|   }) | ||||
| @ -70,20 +69,21 @@ const mySketch001 = startSketchAt([0, 0]) | ||||
|   |> extrude(2, %) | ||||
| show(mySketch001)` | ||||
|     const programMemory = await enginelessExecutor(parser_wasm(code)) | ||||
|     // @ts-ignore | ||||
|     const shown = programMemory?.return?.map( | ||||
|       // @ts-ignore | ||||
|       (a) => programMemory?.root?.[a.name] | ||||
|     ) | ||||
|     expect(shown).toEqual([ | ||||
|       { | ||||
|         type: 'extrudeGroup', | ||||
|         id: expect.any(String), | ||||
|         id: '65383433-3839-4333-b836-343263636638', | ||||
|         value: [], | ||||
|         height: 2, | ||||
|         position: [0, 0, 0], | ||||
|         rotation: [0, 0, 0, 1], | ||||
|         __meta: [{ sourceRange: [21, 42] }], | ||||
|         __meta: [ | ||||
|           { sourceRange: [127, 140], pathToNode: [] }, | ||||
|           { sourceRange: [21, 42], pathToNode: [] }, | ||||
|         ], | ||||
|       }, | ||||
|     ]) | ||||
|   }) | ||||
| @ -106,33 +106,37 @@ const sk2 = startSketchAt([0, 0]) | ||||
|   |> lineTo([2.5, 0], %) | ||||
|   // |> transform(theTransf, %) | ||||
|   |> extrude(2, %) | ||||
|  | ||||
|    | ||||
|  | ||||
| show(theExtrude, sk2)` | ||||
|     const programMemory = await enginelessExecutor(parser_wasm(code)) | ||||
|     // @ts-ignore | ||||
|     const geos = programMemory?.return?.map( | ||||
|       // @ts-ignore | ||||
|       ({ name }) => programMemory?.root?.[name] | ||||
|     ) | ||||
|     expect(geos).toEqual([ | ||||
|       { | ||||
|         type: 'extrudeGroup', | ||||
|         id: expect.any(String), | ||||
|         id: '63333330-3631-4230-b664-623132643731', | ||||
|         value: [], | ||||
|         height: 2, | ||||
|         position: [0, 0, 0], | ||||
|         rotation: [0, 0, 0, 1], | ||||
|         __meta: [{ sourceRange: [13, 34] }], | ||||
|         __meta: [ | ||||
|           { sourceRange: [212, 227], pathToNode: [] }, | ||||
|           { sourceRange: [13, 34], pathToNode: [] }, | ||||
|         ], | ||||
|       }, | ||||
|       { | ||||
|         type: 'extrudeGroup', | ||||
|         id: expect.any(String), | ||||
|         id: '33316639-3438-4661-a334-663262383737', | ||||
|         value: [], | ||||
|         height: 2, | ||||
|         position: [0, 0, 0], | ||||
|         rotation: [0, 0, 0, 1], | ||||
|         __meta: [{ sourceRange: [302, 323] }], | ||||
|         __meta: [ | ||||
|           { sourceRange: [453, 466], pathToNode: [] }, | ||||
|           { sourceRange: [302, 323], pathToNode: [] }, | ||||
|         ], | ||||
|       }, | ||||
|     ]) | ||||
|   }) | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { Diagnostic } from '@codemirror/lint' | ||||
| import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' | ||||
| import { KclError as RustKclError } from '../wasm-lib/bindings/KclError' | ||||
|  | ||||
| type ExtractKind<T> = T extends { kind: infer K } ? K : never | ||||
| export class KCLError { | ||||
|  | ||||
| @ -5,7 +5,7 @@ import { ProgramMemory } from './executor' | ||||
| import { initPromise } from './rust' | ||||
| import { enginelessExecutor } from '../lib/testHelpers' | ||||
| import { vi } from 'vitest' | ||||
| import { KCLError } from './errors' | ||||
| import { KCLUndefinedValueError } from './errors' | ||||
|  | ||||
| beforeAll(() => initPromise) | ||||
|  | ||||
| @ -30,6 +30,29 @@ const newVar = myVar + 1` | ||||
|     const { root } = await exe(code) | ||||
|     expect(root.myVar.value).toBe('a str another str') | ||||
|   }) | ||||
|   it('test with function call', async () => { | ||||
|     const code = ` | ||||
| const myVar = "hello" | ||||
| log(5, myVar)` | ||||
|     const programMemoryOverride: ProgramMemory['root'] = { | ||||
|       log: { | ||||
|         type: 'userVal', | ||||
|         value: vi.fn(), | ||||
|         __meta: [ | ||||
|           { | ||||
|             sourceRange: [0, 0], | ||||
|             pathToNode: [], | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|     } | ||||
|     const { root } = await enginelessExecutor(parser_wasm(code), { | ||||
|       root: programMemoryOverride, | ||||
|       pendingMemory: {}, | ||||
|     }) | ||||
|     expect(root.myVar.value).toBe('hello') | ||||
|     expect(programMemoryOverride.log.value).toHaveBeenCalledWith(5, 'hello') | ||||
|   }) | ||||
|   it('fn funcN = () => {} execute', async () => { | ||||
|     const { root } = await exe( | ||||
|       [ | ||||
| @ -61,7 +84,8 @@ show(mySketch) | ||||
|         from: [0, 0], | ||||
|         __geoMeta: { | ||||
|           sourceRange: [43, 80], | ||||
|           id: expect.any(String), | ||||
|           id: '37333036-3033-4432-b530-643030303837', | ||||
|           pathToNode: [], | ||||
|         }, | ||||
|         name: 'myPath', | ||||
|       }, | ||||
| @ -69,10 +93,10 @@ show(mySketch) | ||||
|         type: 'toPoint', | ||||
|         to: [2, 3], | ||||
|         from: [0, 2], | ||||
|         name: '', | ||||
|         __geoMeta: { | ||||
|           sourceRange: [86, 102], | ||||
|           id: expect.any(String), | ||||
|           id: '32343136-3330-4134-a462-376437386365', | ||||
|           pathToNode: [], | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
| @ -81,7 +105,8 @@ show(mySketch) | ||||
|         from: [2, 3], | ||||
|         __geoMeta: { | ||||
|           sourceRange: [108, 151], | ||||
|           id: expect.any(String), | ||||
|           id: '32306132-6130-4138-b832-636363326330', | ||||
|           pathToNode: [], | ||||
|         }, | ||||
|         name: 'rightPath', | ||||
|       }, | ||||
| @ -145,12 +170,13 @@ show(mySketch) | ||||
|     expect(root.mySk1).toEqual({ | ||||
|       type: 'sketchGroup', | ||||
|       start: { | ||||
|         type: 'base', | ||||
|         to: [0, 0], | ||||
|         from: [0, 0], | ||||
|         name: '', | ||||
|         __geoMeta: { | ||||
|           id: expect.any(String), | ||||
|           id: '37663863-3664-4366-a637-623739336334', | ||||
|           sourceRange: [14, 34], | ||||
|           pathToNode: [], | ||||
|         }, | ||||
|       }, | ||||
|       value: [ | ||||
| @ -158,10 +184,10 @@ show(mySketch) | ||||
|           type: 'toPoint', | ||||
|           to: [1, 1], | ||||
|           from: [0, 0], | ||||
|           name: '', | ||||
|           __geoMeta: { | ||||
|             sourceRange: [40, 56], | ||||
|             id: expect.any(String), | ||||
|             id: '34356231-3362-4363-b935-393033353034', | ||||
|             pathToNode: [], | ||||
|           }, | ||||
|         }, | ||||
|         { | ||||
| @ -170,7 +196,8 @@ show(mySketch) | ||||
|           from: [1, 1], | ||||
|           __geoMeta: { | ||||
|             sourceRange: [62, 100], | ||||
|             id: expect.any(String), | ||||
|             id: '39623339-3538-4366-b633-356630326639', | ||||
|             pathToNode: [], | ||||
|           }, | ||||
|           name: 'myPath', | ||||
|         }, | ||||
| @ -178,17 +205,17 @@ show(mySketch) | ||||
|           type: 'toPoint', | ||||
|           to: [1, 1], | ||||
|           from: [0, 1], | ||||
|           name: '', | ||||
|           __geoMeta: { | ||||
|             sourceRange: [106, 122], | ||||
|             id: expect.any(String), | ||||
|             id: '30636135-6232-4335-b665-366562303161', | ||||
|             pathToNode: [], | ||||
|           }, | ||||
|         }, | ||||
|       ], | ||||
|       position: [0, 0, 0], | ||||
|       rotation: [0, 0, 0, 1], | ||||
|       id: expect.any(String), | ||||
|       __meta: [{ sourceRange: [14, 34] }], | ||||
|       id: '30376661-3039-4965-b532-653665313731', | ||||
|       __meta: [{ sourceRange: [14, 34], pathToNode: [] }], | ||||
|     }) | ||||
|   }) | ||||
|   it('execute array expression', async () => { | ||||
| @ -203,6 +230,13 @@ show(mySketch) | ||||
|         value: 3, | ||||
|         __meta: [ | ||||
|           { | ||||
|             pathToNode: [ | ||||
|               ['body', ''], | ||||
|               [0, 'index'], | ||||
|               ['declarations', 'VariableDeclaration'], | ||||
|               [0, 'index'], | ||||
|               ['init', 'VariableDeclaration'], | ||||
|             ], | ||||
|             sourceRange: [14, 15], | ||||
|           }, | ||||
|         ], | ||||
| @ -212,8 +246,25 @@ show(mySketch) | ||||
|         value: [1, '2', 3, 9], | ||||
|         __meta: [ | ||||
|           { | ||||
|             pathToNode: [ | ||||
|               ['body', ''], | ||||
|               [1, 'index'], | ||||
|               ['declarations', 'VariableDeclaration'], | ||||
|               [0, 'index'], | ||||
|               ['init', 'VariableDeclaration'], | ||||
|             ], | ||||
|             sourceRange: [27, 49], | ||||
|           }, | ||||
|           { | ||||
|             pathToNode: [ | ||||
|               ['body', ''], | ||||
|               [0, 'index'], | ||||
|               ['declarations', 'VariableDeclaration'], | ||||
|               [0, 'index'], | ||||
|               ['init', 'VariableDeclaration'], | ||||
|             ], | ||||
|             sourceRange: [14, 15], | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|     }) | ||||
| @ -229,6 +280,13 @@ show(mySketch) | ||||
|       value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 }, | ||||
|       __meta: [ | ||||
|         { | ||||
|           pathToNode: [ | ||||
|             ['body', ''], | ||||
|             [1, 'index'], | ||||
|             ['declarations', 'VariableDeclaration'], | ||||
|             [0, 'index'], | ||||
|             ['init', 'VariableDeclaration'], | ||||
|           ], | ||||
|           sourceRange: [27, 83], | ||||
|         }, | ||||
|       ], | ||||
| @ -244,6 +302,13 @@ show(mySketch) | ||||
|       value: '123', | ||||
|       __meta: [ | ||||
|         { | ||||
|           pathToNode: [ | ||||
|             ['body', ''], | ||||
|             [1, 'index'], | ||||
|             ['declarations', 'VariableDeclaration'], | ||||
|             [0, 'index'], | ||||
|             ['init', 'VariableDeclaration'], | ||||
|           ], | ||||
|           sourceRange: [41, 50], | ||||
|         }, | ||||
|       ], | ||||
| @ -386,18 +451,17 @@ const theExtrude = startSketchAt([0, 0]) | ||||
|   |> extrude(4, %) | ||||
| show(theExtrude)` | ||||
|     await expect(exe(code)).rejects.toEqual( | ||||
|       new KCLError( | ||||
|         'undefined_value', | ||||
|         'memory item key `myVarZ` is not defined', | ||||
|         [[100, 106]] | ||||
|       ) | ||||
|       new KCLUndefinedValueError('Memory item myVarZ not found', [[100, 106]]) | ||||
|     ) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| // helpers | ||||
|  | ||||
| async function exe(code: string, programMemory: ProgramMemory = { root: {} }) { | ||||
| async function exe( | ||||
|   code: string, | ||||
|   programMemory: ProgramMemory = { root: {}, pendingMemory: {} } | ||||
| ) { | ||||
|   const ast = parser_wasm(code) | ||||
|  | ||||
|   const result = await enginelessExecutor(ast, programMemory) | ||||
|  | ||||
| @ -1,19 +1,35 @@ | ||||
| import { Program } from './abstractSyntaxTreeTypes' | ||||
| import { | ||||
|   Program, | ||||
|   BinaryPart, | ||||
|   BinaryExpression, | ||||
|   PipeExpression, | ||||
|   ObjectExpression, | ||||
|   MemberExpression, | ||||
|   Identifier, | ||||
|   CallExpression, | ||||
|   ArrayExpression, | ||||
|   UnaryExpression, | ||||
| } from './abstractSyntaxTreeTypes' | ||||
| import { InternalFnNames } from './std/stdTypes' | ||||
| import { internalFns } from './std/std' | ||||
| import { | ||||
|   KCLUndefinedValueError, | ||||
|   KCLValueAlreadyDefined, | ||||
|   KCLSyntaxError, | ||||
|   KCLSemanticError, | ||||
|   KCLTypeError, | ||||
| } from './errors' | ||||
| import { | ||||
|   EngineCommandManager, | ||||
|   ArtifactMap, | ||||
|   SourceRangeMap, | ||||
| } from './std/engineConnection' | ||||
| import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn' | ||||
| import { execute_wasm } from '../wasm-lib/pkg/wasm_lib' | ||||
| import { KCLError } from './errors' | ||||
| import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' | ||||
| import { rangeTypeFix } from './abstractSyntaxTree' | ||||
|  | ||||
| export type SourceRange = [number, number] | ||||
| export type PathToNode = [string | number, string][] // [pathKey, nodeType][] | ||||
| export type Metadata = { | ||||
|   sourceRange: SourceRange | ||||
|   pathToNode: PathToNode | ||||
| } | ||||
| export type Position = [number, number, number] | ||||
| export type Rotation = [number, number, number, number] | ||||
| @ -25,6 +41,7 @@ interface BasePath { | ||||
|   __geoMeta: { | ||||
|     id: string | ||||
|     sourceRange: SourceRange | ||||
|     pathToNode: PathToNode | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -51,7 +68,9 @@ export interface AngledLineTo extends BasePath { | ||||
| interface GeoMeta { | ||||
|   __geoMeta: { | ||||
|     id: string | ||||
|     refId?: string | ||||
|     sourceRange: SourceRange | ||||
|     pathToNode: PathToNode | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -99,16 +118,69 @@ type MemoryItem = UserVal | SketchGroup | ExtrudeGroup | ||||
| interface Memory { | ||||
|   [key: string]: MemoryItem | ||||
| } | ||||
| interface PendingMemory { | ||||
|   [key: string]: Promise<MemoryItem> | ||||
| } | ||||
|  | ||||
| export interface ProgramMemory { | ||||
|   root: Memory | ||||
|   return?: ProgramReturn | ||||
|   pendingMemory: Partial<PendingMemory> | ||||
|   return?: Identifier[] | ||||
| } | ||||
|  | ||||
| const addItemToMemory = ( | ||||
|   programMemory: ProgramMemory, | ||||
|   key: string, | ||||
|   sourceRange: [[number, number]], | ||||
|   value: MemoryItem | Promise<MemoryItem> | ||||
| ) => { | ||||
|   const _programMemory = programMemory | ||||
|   if (_programMemory.root[key] || _programMemory.pendingMemory[key]) { | ||||
|     throw new KCLValueAlreadyDefined(key, sourceRange) | ||||
|   } | ||||
|   if (value instanceof Promise) { | ||||
|     _programMemory.pendingMemory[key] = value | ||||
|     value.then((resolvedValue) => { | ||||
|       _programMemory.root[key] = resolvedValue | ||||
|       delete _programMemory.pendingMemory[key] | ||||
|     }) | ||||
|   } else { | ||||
|     _programMemory.root[key] = value | ||||
|   } | ||||
|   return _programMemory | ||||
| } | ||||
|  | ||||
| const promisifyMemoryItem = async (obj: MemoryItem) => { | ||||
|   if (obj.value instanceof Promise) { | ||||
|     const resolvedGuy = await obj.value | ||||
|     return { | ||||
|       ...obj, | ||||
|       value: resolvedGuy, | ||||
|     } | ||||
|   } | ||||
|   return obj | ||||
| } | ||||
|  | ||||
| const getMemoryItem = async ( | ||||
|   programMemory: ProgramMemory, | ||||
|   key: string, | ||||
|   sourceRanges: [number, number][] | ||||
| ): Promise<MemoryItem> => { | ||||
|   if (programMemory.root[key]) { | ||||
|     return programMemory.root[key] | ||||
|   } | ||||
|   if (programMemory.pendingMemory[key]) { | ||||
|     return programMemory.pendingMemory[key] as Promise<MemoryItem> | ||||
|   } | ||||
|   throw new KCLUndefinedValueError(`Memory item ${key} not found`, sourceRanges) | ||||
| } | ||||
|  | ||||
| export const executor = async ( | ||||
|   node: Program, | ||||
|   programMemory: ProgramMemory = { root: {} }, | ||||
|   programMemory: ProgramMemory = { root: {}, pendingMemory: {} }, | ||||
|   engineCommandManager: EngineCommandManager, | ||||
|   options: { bodyType: 'root' | 'sketch' | 'block' } = { bodyType: 'root' }, | ||||
|   previousPathToNode: PathToNode = [], | ||||
|   // work around while the gemotry is still be stored on the frontend | ||||
|   // will be removed when the stream UI is added. | ||||
|   tempMapCallback: (a: { | ||||
| @ -120,7 +192,9 @@ export const executor = async ( | ||||
|   const _programMemory = await _executor( | ||||
|     node, | ||||
|     programMemory, | ||||
|     engineCommandManager | ||||
|     engineCommandManager, | ||||
|     options, | ||||
|     previousPathToNode | ||||
|   ) | ||||
|   const { artifactMap, sourceRangeMap } = | ||||
|     await engineCommandManager.waitForAllCommands() | ||||
| @ -132,25 +206,840 @@ export const executor = async ( | ||||
|  | ||||
| export const _executor = async ( | ||||
|   node: Program, | ||||
|   programMemory: ProgramMemory = { root: {} }, | ||||
|   engineCommandManager: EngineCommandManager | ||||
|   programMemory: ProgramMemory = { root: {}, pendingMemory: {} }, | ||||
|   engineCommandManager: EngineCommandManager, | ||||
|   options: { bodyType: 'root' | 'sketch' | 'block' } = { bodyType: 'root' }, | ||||
|   previousPathToNode: PathToNode = [] | ||||
| ): Promise<ProgramMemory> => { | ||||
|   try { | ||||
|     const memory: ProgramMemory = await execute_wasm( | ||||
|       JSON.stringify(node), | ||||
|       JSON.stringify(programMemory), | ||||
|       engineCommandManager | ||||
|     ) | ||||
|     return memory | ||||
|   } catch (e: any) { | ||||
|     const parsed: RustKclError = JSON.parse(e.toString()) | ||||
|     const kclError = new KCLError( | ||||
|       parsed.kind, | ||||
|       parsed.msg, | ||||
|       rangeTypeFix(parsed.sourceRanges) | ||||
|     ) | ||||
|   let _programMemory: ProgramMemory = { | ||||
|     root: { | ||||
|       ...programMemory.root, | ||||
|     }, | ||||
|     pendingMemory: { | ||||
|       ...programMemory.pendingMemory, | ||||
|     }, | ||||
|     return: programMemory.return, | ||||
|   } | ||||
|   const { body } = node | ||||
|   const proms: Promise<any>[] = [] | ||||
|   for (let bodyIndex = 0; bodyIndex < body.length; bodyIndex++) { | ||||
|     const statement = body[bodyIndex] | ||||
|     if (statement.type === 'VariableDeclaration') { | ||||
|       for (let index = 0; index < statement.declarations.length; index++) { | ||||
|         const declaration = statement.declarations[index] | ||||
|         const variableName = declaration.id.name | ||||
|         const pathToNode: PathToNode = [ | ||||
|           ...previousPathToNode, | ||||
|           ['body', ''], | ||||
|           [bodyIndex, 'index'], | ||||
|           ['declarations', 'VariableDeclaration'], | ||||
|           [index, 'index'], | ||||
|           ['init', 'VariableDeclaration'], | ||||
|         ] | ||||
|         const sourceRange: SourceRange = [ | ||||
|           declaration.init.start, | ||||
|           declaration.init.end, | ||||
|         ] | ||||
|         const __meta: Metadata[] = [ | ||||
|           { | ||||
|             pathToNode, | ||||
|             sourceRange, | ||||
|           }, | ||||
|         ] | ||||
|  | ||||
|     console.log(kclError) | ||||
|     throw kclError | ||||
|         if (declaration.init.type === 'PipeExpression') { | ||||
|           const prom = getPipeExpressionResult( | ||||
|             declaration.init, | ||||
|             _programMemory, | ||||
|             engineCommandManager, | ||||
|             pathToNode | ||||
|           ) | ||||
|           proms.push(prom) | ||||
|           const value = await prom | ||||
|           if (value?.type === 'sketchGroup' || value?.type === 'extrudeGroup') { | ||||
|             _programMemory = addItemToMemory( | ||||
|               _programMemory, | ||||
|               variableName, | ||||
|               [sourceRange], | ||||
|               value | ||||
|             ) | ||||
|           } else { | ||||
|             _programMemory = addItemToMemory( | ||||
|               _programMemory, | ||||
|               variableName, | ||||
|               [sourceRange], | ||||
|               { | ||||
|                 type: 'userVal', | ||||
|                 value, | ||||
|                 __meta, | ||||
|               } | ||||
|             ) | ||||
|           } | ||||
|         } else if (declaration.init.type === 'Identifier') { | ||||
|           _programMemory = addItemToMemory( | ||||
|             _programMemory, | ||||
|             variableName, | ||||
|             [sourceRange], | ||||
|             { | ||||
|               type: 'userVal', | ||||
|               value: _programMemory.root[declaration.init.name].value, | ||||
|               __meta, | ||||
|             } | ||||
|           ) | ||||
|         } else if (declaration.init.type === 'Literal') { | ||||
|           _programMemory = addItemToMemory( | ||||
|             _programMemory, | ||||
|             variableName, | ||||
|             [sourceRange], | ||||
|             { | ||||
|               type: 'userVal', | ||||
|               value: declaration.init.value, | ||||
|               __meta, | ||||
|             } | ||||
|           ) | ||||
|         } else if (declaration.init.type === 'BinaryExpression') { | ||||
|           const prom = getBinaryExpressionResult( | ||||
|             declaration.init, | ||||
|             _programMemory, | ||||
|             engineCommandManager | ||||
|           ) | ||||
|           proms.push(prom) | ||||
|           _programMemory = addItemToMemory( | ||||
|             _programMemory, | ||||
|             variableName, | ||||
|             [sourceRange], | ||||
|             promisifyMemoryItem({ | ||||
|               type: 'userVal', | ||||
|               value: prom, | ||||
|               __meta, | ||||
|             }) | ||||
|           ) | ||||
|         } else if (declaration.init.type === 'UnaryExpression') { | ||||
|           const prom = getUnaryExpressionResult( | ||||
|             declaration.init, | ||||
|             _programMemory, | ||||
|             engineCommandManager | ||||
|           ) | ||||
|           proms.push(prom) | ||||
|           _programMemory = addItemToMemory( | ||||
|             _programMemory, | ||||
|             variableName, | ||||
|             [sourceRange], | ||||
|             promisifyMemoryItem({ | ||||
|               type: 'userVal', | ||||
|               value: prom, | ||||
|               __meta, | ||||
|             }) | ||||
|           ) | ||||
|         } else if (declaration.init.type === 'ArrayExpression') { | ||||
|           const valueInfo: Promise<{ value: any; __meta?: Metadata }>[] = | ||||
|             declaration.init.elements.map( | ||||
|               async (element): Promise<{ value: any; __meta?: Metadata }> => { | ||||
|                 if (element.type === 'Literal') { | ||||
|                   return { | ||||
|                     value: element.value, | ||||
|                   } | ||||
|                 } else if (element.type === 'BinaryExpression') { | ||||
|                   const prom = getBinaryExpressionResult( | ||||
|                     element, | ||||
|                     _programMemory, | ||||
|                     engineCommandManager | ||||
|                   ) | ||||
|                   proms.push(prom) | ||||
|                   return { | ||||
|                     value: await prom, | ||||
|                   } | ||||
|                 } else if (element.type === 'PipeExpression') { | ||||
|                   const prom = getPipeExpressionResult( | ||||
|                     element, | ||||
|                     _programMemory, | ||||
|                     engineCommandManager, | ||||
|                     pathToNode | ||||
|                   ) | ||||
|                   proms.push(prom) | ||||
|                   return { | ||||
|                     value: await prom, | ||||
|                   } | ||||
|                 } else if (element.type === 'Identifier') { | ||||
|                   const node = await getMemoryItem( | ||||
|                     _programMemory, | ||||
|                     element.name, | ||||
|                     [[element.start, element.end]] | ||||
|                   ) | ||||
|                   return { | ||||
|                     value: node.value, | ||||
|                     __meta: node.__meta[node.__meta.length - 1], | ||||
|                   } | ||||
|                 } else if (element.type === 'UnaryExpression') { | ||||
|                   const prom = getUnaryExpressionResult( | ||||
|                     element, | ||||
|                     _programMemory, | ||||
|                     engineCommandManager | ||||
|                   ) | ||||
|                   proms.push(prom) | ||||
|                   return { | ||||
|                     value: await prom, | ||||
|                   } | ||||
|                 } else { | ||||
|                   throw new KCLSyntaxError( | ||||
|                     `Unexpected element type ${element.type} in array expression`, | ||||
|                     // TODO: Refactor this whole block into a `switch` so that we have a specific | ||||
|                     // type here and can put a sourceRange. | ||||
|                     [] | ||||
|                   ) | ||||
|                 } | ||||
|               } | ||||
|             ) | ||||
|           const awaitedValueInfo = await Promise.all(valueInfo) | ||||
|           const meta = awaitedValueInfo | ||||
|             .filter(({ __meta }) => __meta) | ||||
|             .map(({ __meta }) => __meta) as Metadata[] | ||||
|           _programMemory = addItemToMemory( | ||||
|             _programMemory, | ||||
|             variableName, | ||||
|             [sourceRange], | ||||
|             { | ||||
|               type: 'userVal', | ||||
|               value: awaitedValueInfo.map(({ value }) => value), | ||||
|               __meta: [...__meta, ...meta], | ||||
|             } | ||||
|           ) | ||||
|         } else if (declaration.init.type === 'ObjectExpression') { | ||||
|           const prom = executeObjectExpression( | ||||
|             _programMemory, | ||||
|             declaration.init, | ||||
|             engineCommandManager | ||||
|           ) | ||||
|           proms.push(prom) | ||||
|           _programMemory = addItemToMemory( | ||||
|             _programMemory, | ||||
|             variableName, | ||||
|             [sourceRange], | ||||
|             promisifyMemoryItem({ | ||||
|               type: 'userVal', | ||||
|               value: prom, | ||||
|               __meta, | ||||
|             }) | ||||
|           ) | ||||
|         } else if (declaration.init.type === 'FunctionExpression') { | ||||
|           const fnInit = declaration.init | ||||
|  | ||||
|           _programMemory = addItemToMemory( | ||||
|             _programMemory, | ||||
|             declaration.id.name, | ||||
|             [sourceRange], | ||||
|             { | ||||
|               type: 'userVal', | ||||
|               value: async (...args: any[]) => { | ||||
|                 let fnMemory: ProgramMemory = { | ||||
|                   root: { | ||||
|                     ..._programMemory.root, | ||||
|                   }, | ||||
|                   pendingMemory: { | ||||
|                     ..._programMemory.pendingMemory, | ||||
|                   }, | ||||
|                 } | ||||
|                 if (args.length > fnInit.params.length) { | ||||
|                   throw new KCLSyntaxError( | ||||
|                     `Too many arguments passed to function ${declaration.id.name}`, | ||||
|                     [[declaration.start, declaration.end]] | ||||
|                   ) | ||||
|                 } else if (args.length < fnInit.params.length) { | ||||
|                   throw new KCLSyntaxError( | ||||
|                     `Too few arguments passed to function ${declaration.id.name}`, | ||||
|                     [[declaration.start, declaration.end]] | ||||
|                   ) | ||||
|                 } | ||||
|                 fnInit.params.forEach((param, index) => { | ||||
|                   fnMemory = addItemToMemory( | ||||
|                     fnMemory, | ||||
|                     param.name, | ||||
|                     [sourceRange], | ||||
|                     { | ||||
|                       type: 'userVal', | ||||
|                       value: args[index], | ||||
|                       __meta, | ||||
|                     } | ||||
|                   ) | ||||
|                 }) | ||||
|                 const prom = _executor( | ||||
|                   fnInit.body, | ||||
|                   fnMemory, | ||||
|                   engineCommandManager, | ||||
|                   { | ||||
|                     bodyType: 'block', | ||||
|                   } | ||||
|                 ) | ||||
|                 proms.push(prom) | ||||
|                 const result = (await prom).return | ||||
|                 return result | ||||
|               }, | ||||
|               __meta, | ||||
|             } | ||||
|           ) | ||||
|         } else if (declaration.init.type === 'MemberExpression') { | ||||
|           await Promise.all([...proms]) // TODO wait for previous promises, does that makes sense? | ||||
|           const prom = getMemberExpressionResult( | ||||
|             declaration.init, | ||||
|             _programMemory | ||||
|           ) | ||||
|           proms.push(prom) | ||||
|           _programMemory = addItemToMemory( | ||||
|             _programMemory, | ||||
|             variableName, | ||||
|             [sourceRange], | ||||
|             promisifyMemoryItem({ | ||||
|               type: 'userVal', | ||||
|               value: prom, | ||||
|               __meta, | ||||
|             }) | ||||
|           ) | ||||
|         } else if (declaration.init.type === 'CallExpression') { | ||||
|           const prom = executeCallExpression( | ||||
|             _programMemory, | ||||
|             declaration.init, | ||||
|             engineCommandManager, | ||||
|             previousPathToNode | ||||
|           ) | ||||
|           proms.push(prom) | ||||
|           _programMemory = addItemToMemory( | ||||
|             _programMemory, | ||||
|             variableName, | ||||
|             [sourceRange], | ||||
|             prom.then((a) => { | ||||
|               return a?.type === 'sketchGroup' || a?.type === 'extrudeGroup' | ||||
|                 ? a | ||||
|                 : { | ||||
|                     type: 'userVal', | ||||
|                     value: a, | ||||
|                     __meta, | ||||
|                   } | ||||
|             }) | ||||
|           ) | ||||
|         } else { | ||||
|           throw new KCLSyntaxError( | ||||
|             'Unsupported declaration type: ' + declaration.init.type, | ||||
|             [[declaration.start, declaration.end]] | ||||
|           ) | ||||
|         } | ||||
|       } | ||||
|     } else if (statement.type === 'ExpressionStatement') { | ||||
|       const expression = statement.expression | ||||
|       if (expression.type === 'CallExpression') { | ||||
|         const functionName = expression.callee.name | ||||
|         const args = expression.arguments.map((arg) => { | ||||
|           if (arg.type === 'Literal') { | ||||
|             return arg.value | ||||
|           } else if (arg.type === 'Identifier') { | ||||
|             return _programMemory.root[arg.name]?.value | ||||
|           } | ||||
|         }) | ||||
|         if ('show' === functionName) { | ||||
|           if (options.bodyType !== 'root') { | ||||
|             throw new KCLSemanticError( | ||||
|               `Cannot call ${functionName} outside of a root`, | ||||
|               [[statement.start, statement.end]] | ||||
|             ) | ||||
|           } | ||||
|           _programMemory.return = expression.arguments as any // todo memory redo | ||||
|         } else { | ||||
|           if (_programMemory.root[functionName] === undefined) { | ||||
|             throw new KCLSemanticError(`No such name ${functionName} defined`, [ | ||||
|               [statement.start, statement.end], | ||||
|             ]) | ||||
|           } | ||||
|           _programMemory.root[functionName].value(...args) | ||||
|         } | ||||
|       } | ||||
|     } else if (statement.type === 'ReturnStatement') { | ||||
|       if (statement.argument.type === 'BinaryExpression') { | ||||
|         const prom = getBinaryExpressionResult( | ||||
|           statement.argument, | ||||
|           _programMemory, | ||||
|           engineCommandManager | ||||
|         ) | ||||
|         proms.push(prom) | ||||
|         _programMemory.return = await prom | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   await Promise.all(proms) | ||||
|   return _programMemory | ||||
| } | ||||
|  | ||||
| function getMemberExpressionResult( | ||||
|   expression: MemberExpression, | ||||
|   programMemory: ProgramMemory | ||||
| ) { | ||||
|   const propertyName = ( | ||||
|     expression.property.type === 'Identifier' | ||||
|       ? expression.property.name | ||||
|       : expression.property.value | ||||
|   ) as any | ||||
|   const object: any = | ||||
|     expression.object.type === 'MemberExpression' | ||||
|       ? getMemberExpressionResult(expression.object, programMemory) | ||||
|       : programMemory.root[expression.object.name]?.value | ||||
|   return object?.[propertyName] | ||||
| } | ||||
|  | ||||
| async function getBinaryExpressionResult( | ||||
|   expression: BinaryExpression, | ||||
|   programMemory: ProgramMemory, | ||||
|   engineCommandManager: EngineCommandManager, | ||||
|   pipeInfo: { | ||||
|     isInPipe: boolean | ||||
|     previousResults: any[] | ||||
|     expressionIndex: number | ||||
|     body: PipeExpression['body'] | ||||
|     sourceRangeOverride?: SourceRange | ||||
|   } = { | ||||
|     isInPipe: false, | ||||
|     previousResults: [], | ||||
|     expressionIndex: 0, | ||||
|     body: [], | ||||
|   } | ||||
| ) { | ||||
|   const _pipeInfo = { | ||||
|     ...pipeInfo, | ||||
|     isInPipe: false, | ||||
|   } | ||||
|   const left = await getBinaryPartResult( | ||||
|     expression.left, | ||||
|     programMemory, | ||||
|     engineCommandManager, | ||||
|     _pipeInfo | ||||
|   ) | ||||
|   const right = await getBinaryPartResult( | ||||
|     expression.right, | ||||
|     programMemory, | ||||
|     engineCommandManager, | ||||
|     _pipeInfo | ||||
|   ) | ||||
|   if (expression.operator === '+') return left + right | ||||
|   if (expression.operator === '-') return left - right | ||||
|   if (expression.operator === '*') return left * right | ||||
|   if (expression.operator === '/') return left / right | ||||
|   if (expression.operator === '%') return left % right | ||||
| } | ||||
|  | ||||
| async function getBinaryPartResult( | ||||
|   part: BinaryPart, | ||||
|   programMemory: ProgramMemory, | ||||
|   engineCommandManager: EngineCommandManager, | ||||
|   pipeInfo: { | ||||
|     isInPipe: boolean | ||||
|     previousResults: any[] | ||||
|     expressionIndex: number | ||||
|     body: PipeExpression['body'] | ||||
|     sourceRangeOverride?: SourceRange | ||||
|   } = { | ||||
|     isInPipe: false, | ||||
|     previousResults: [], | ||||
|     expressionIndex: 0, | ||||
|     body: [], | ||||
|   } | ||||
| ): Promise<any> { | ||||
|   const _pipeInfo = { | ||||
|     ...pipeInfo, | ||||
|     isInPipe: false, | ||||
|   } | ||||
|   if (part.type === 'Literal') { | ||||
|     return part.value | ||||
|   } else if (part.type === 'Identifier') { | ||||
|     return programMemory.root[part.name].value | ||||
|   } else if (part.type === 'BinaryExpression') { | ||||
|     const prom = getBinaryExpressionResult( | ||||
|       part, | ||||
|       programMemory, | ||||
|       engineCommandManager, | ||||
|       _pipeInfo | ||||
|     ) | ||||
|     const result = await prom | ||||
|     return result | ||||
|   } else if (part.type === 'CallExpression') { | ||||
|     const result = await executeCallExpression( | ||||
|       programMemory, | ||||
|       part, | ||||
|       engineCommandManager, | ||||
|       [], | ||||
|       _pipeInfo | ||||
|     ) | ||||
|     return result | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function getUnaryExpressionResult( | ||||
|   expression: UnaryExpression, | ||||
|   programMemory: ProgramMemory, | ||||
|   engineCommandManager: EngineCommandManager, | ||||
|   pipeInfo: { | ||||
|     isInPipe: boolean | ||||
|     previousResults: any[] | ||||
|     expressionIndex: number | ||||
|     body: PipeExpression['body'] | ||||
|     sourceRangeOverride?: SourceRange | ||||
|   } = { | ||||
|     isInPipe: false, | ||||
|     previousResults: [], | ||||
|     expressionIndex: 0, | ||||
|     body: [], | ||||
|   } | ||||
| ) { | ||||
|   return -(await getBinaryPartResult( | ||||
|     expression.argument, | ||||
|     programMemory, | ||||
|     engineCommandManager, | ||||
|     { | ||||
|       ...pipeInfo, | ||||
|       isInPipe: false, | ||||
|     } | ||||
|   )) | ||||
| } | ||||
|  | ||||
| async function getPipeExpressionResult( | ||||
|   expression: PipeExpression, | ||||
|   programMemory: ProgramMemory, | ||||
|   engineCommandManager: EngineCommandManager, | ||||
|   previousPathToNode: PathToNode = [] | ||||
| ) { | ||||
|   const executedBody = await executePipeBody( | ||||
|     expression.body, | ||||
|     programMemory, | ||||
|     engineCommandManager, | ||||
|     previousPathToNode | ||||
|   ) | ||||
|   const result = executedBody[executedBody.length - 1] | ||||
|   return result | ||||
| } | ||||
|  | ||||
| async function executePipeBody( | ||||
|   body: PipeExpression['body'], | ||||
|   programMemory: ProgramMemory, | ||||
|   engineCommandManager: EngineCommandManager, | ||||
|   previousPathToNode: PathToNode = [], | ||||
|   expressionIndex = 0, | ||||
|   previousResults: any[] = [] | ||||
| ): Promise<any[]> { | ||||
|   if (expressionIndex === body.length) { | ||||
|     return previousResults | ||||
|   } | ||||
|   const expression = body[expressionIndex] | ||||
|   if (expression.type === 'BinaryExpression') { | ||||
|     const result = await getBinaryExpressionResult( | ||||
|       expression, | ||||
|       programMemory, | ||||
|       engineCommandManager | ||||
|     ) | ||||
|     return executePipeBody( | ||||
|       body, | ||||
|       programMemory, | ||||
|       engineCommandManager, | ||||
|       previousPathToNode, | ||||
|       expressionIndex + 1, | ||||
|       [...previousResults, result] | ||||
|     ) | ||||
|   } else if (expression.type === 'CallExpression') { | ||||
|     return await executeCallExpression( | ||||
|       programMemory, | ||||
|       expression, | ||||
|       engineCommandManager, | ||||
|       previousPathToNode, | ||||
|       { | ||||
|         isInPipe: true, | ||||
|         previousResults, | ||||
|         expressionIndex, | ||||
|         body, | ||||
|       } | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   throw new KCLSyntaxError('Invalid pipe expression', [ | ||||
|     [expression.start, expression.end], | ||||
|   ]) | ||||
| } | ||||
|  | ||||
| async function executeObjectExpression( | ||||
|   _programMemory: ProgramMemory, | ||||
|   objExp: ObjectExpression, | ||||
|   engineCommandManager: EngineCommandManager, | ||||
|   pipeInfo: { | ||||
|     isInPipe: boolean | ||||
|     previousResults: any[] | ||||
|     expressionIndex: number | ||||
|     body: PipeExpression['body'] | ||||
|     sourceRangeOverride?: SourceRange | ||||
|   } = { | ||||
|     isInPipe: false, | ||||
|     previousResults: [], | ||||
|     expressionIndex: 0, | ||||
|     body: [], | ||||
|   } | ||||
| ) { | ||||
|   const _pipeInfo = { | ||||
|     ...pipeInfo, | ||||
|     isInPipe: false, | ||||
|   } | ||||
|   const obj: { [key: string]: any } = {} | ||||
|   const proms: Promise<any>[] = [] | ||||
|   objExp.properties.forEach(async (property) => { | ||||
|     if (property.type === 'ObjectProperty') { | ||||
|       if (property.value.type === 'Literal') { | ||||
|         obj[property.key.name] = property.value.value | ||||
|       } else if (property.value.type === 'BinaryExpression') { | ||||
|         const prom = getBinaryExpressionResult( | ||||
|           property.value, | ||||
|           _programMemory, | ||||
|           engineCommandManager, | ||||
|           _pipeInfo | ||||
|         ) | ||||
|         proms.push(prom) | ||||
|         obj[property.key.name] = await prom | ||||
|       } else if (property.value.type === 'PipeExpression') { | ||||
|         const prom = getPipeExpressionResult( | ||||
|           property.value, | ||||
|           _programMemory, | ||||
|           engineCommandManager | ||||
|         ) | ||||
|         proms.push(prom) | ||||
|         obj[property.key.name] = await prom | ||||
|       } else if (property.value.type === 'Identifier') { | ||||
|         obj[property.key.name] = ( | ||||
|           await getMemoryItem(_programMemory, property.value.name, [ | ||||
|             [property.value.start, property.value.end], | ||||
|           ]) | ||||
|         ).value | ||||
|       } else if (property.value.type === 'ObjectExpression') { | ||||
|         const prom = executeObjectExpression( | ||||
|           _programMemory, | ||||
|           property.value, | ||||
|           engineCommandManager | ||||
|         ) | ||||
|         proms.push(prom) | ||||
|         obj[property.key.name] = await prom | ||||
|       } else if (property.value.type === 'ArrayExpression') { | ||||
|         const prom = executeArrayExpression( | ||||
|           _programMemory, | ||||
|           property.value, | ||||
|           engineCommandManager | ||||
|         ) | ||||
|         proms.push(prom) | ||||
|         obj[property.key.name] = await prom | ||||
|       } else if (property.value.type === 'CallExpression') { | ||||
|         const prom = executeCallExpression( | ||||
|           _programMemory, | ||||
|           property.value, | ||||
|           engineCommandManager, | ||||
|           [], | ||||
|           _pipeInfo | ||||
|         ) | ||||
|         proms.push(prom) | ||||
|         const result = await prom | ||||
|         obj[property.key.name] = result | ||||
|       } else if (property.value.type === 'UnaryExpression') { | ||||
|         const prom = getUnaryExpressionResult( | ||||
|           property.value, | ||||
|           _programMemory, | ||||
|           engineCommandManager | ||||
|         ) | ||||
|         proms.push(prom) | ||||
|         obj[property.key.name] = await prom | ||||
|       } else { | ||||
|         throw new KCLSyntaxError( | ||||
|           `Unexpected property type ${property.value.type} in object expression`, | ||||
|           [[property.value.start, property.value.end]] | ||||
|         ) | ||||
|       } | ||||
|     } else { | ||||
|       throw new KCLSyntaxError( | ||||
|         `Unexpected property type ${property.type} in object expression`, | ||||
|         [[property.value.start, property.value.end]] | ||||
|       ) | ||||
|     } | ||||
|   }) | ||||
|   await Promise.all(proms) | ||||
|   return obj | ||||
| } | ||||
|  | ||||
| async function executeArrayExpression( | ||||
|   _programMemory: ProgramMemory, | ||||
|   arrExp: ArrayExpression, | ||||
|   engineCommandManager: EngineCommandManager, | ||||
|   pipeInfo: { | ||||
|     isInPipe: boolean | ||||
|     previousResults: any[] | ||||
|     expressionIndex: number | ||||
|     body: PipeExpression['body'] | ||||
|     sourceRangeOverride?: SourceRange | ||||
|   } = { | ||||
|     isInPipe: false, | ||||
|     previousResults: [], | ||||
|     expressionIndex: 0, | ||||
|     body: [], | ||||
|   } | ||||
| ) { | ||||
|   const _pipeInfo = { | ||||
|     ...pipeInfo, | ||||
|     isInPipe: false, | ||||
|   } | ||||
|   return await Promise.all( | ||||
|     arrExp.elements.map((el) => { | ||||
|       if (el.type === 'Literal') { | ||||
|         return el.value | ||||
|       } else if (el.type === 'Identifier') { | ||||
|         return _programMemory.root?.[el.name]?.value | ||||
|       } else if (el.type === 'BinaryExpression') { | ||||
|         return getBinaryExpressionResult( | ||||
|           el, | ||||
|           _programMemory, | ||||
|           engineCommandManager, | ||||
|           _pipeInfo | ||||
|         ) | ||||
|       } else if (el.type === 'ObjectExpression') { | ||||
|         return executeObjectExpression(_programMemory, el, engineCommandManager) | ||||
|       } else if (el.type === 'CallExpression') { | ||||
|         const result: any = executeCallExpression( | ||||
|           _programMemory, | ||||
|           el, | ||||
|           engineCommandManager, | ||||
|           [], | ||||
|           _pipeInfo | ||||
|         ) | ||||
|         return result | ||||
|       } else if (el.type === 'UnaryExpression') { | ||||
|         return getUnaryExpressionResult( | ||||
|           el, | ||||
|           _programMemory, | ||||
|           engineCommandManager, | ||||
|           { | ||||
|             ...pipeInfo, | ||||
|             isInPipe: false, | ||||
|           } | ||||
|         ) | ||||
|       } | ||||
|       throw new KCLTypeError('Invalid argument type', [[el.start, el.end]]) | ||||
|     }) | ||||
|   ) | ||||
| } | ||||
|  | ||||
| async function executeCallExpression( | ||||
|   programMemory: ProgramMemory, | ||||
|   expression: CallExpression, | ||||
|   engineCommandManager: EngineCommandManager, | ||||
|   previousPathToNode: PathToNode = [], | ||||
|   pipeInfo: { | ||||
|     isInPipe: boolean | ||||
|     previousResults: any[] | ||||
|     expressionIndex: number | ||||
|     body: PipeExpression['body'] | ||||
|     sourceRangeOverride?: SourceRange | ||||
|   } = { | ||||
|     isInPipe: false, | ||||
|     previousResults: [], | ||||
|     expressionIndex: 0, | ||||
|     body: [], | ||||
|   } | ||||
| ) { | ||||
|   const { | ||||
|     isInPipe, | ||||
|     previousResults, | ||||
|     expressionIndex, | ||||
|     body, | ||||
|     sourceRangeOverride, | ||||
|   } = pipeInfo | ||||
|   const functionName = expression?.callee?.name | ||||
|   const _pipeInfo = { | ||||
|     ...pipeInfo, | ||||
|     isInPipe: false, | ||||
|   } | ||||
|   const fnArgs = await Promise.all( | ||||
|     expression?.arguments?.map(async (arg) => { | ||||
|       if (arg.type === 'Literal') { | ||||
|         return arg.value | ||||
|       } else if (arg.type === 'Identifier') { | ||||
|         await new Promise((r) => setTimeout(r)) // push into next even loop, but also probably should fix this | ||||
|         const temp = await getMemoryItem(programMemory, arg.name, [ | ||||
|           [arg.start, arg.end], | ||||
|         ]) | ||||
|         return temp?.type === 'userVal' ? temp.value : temp | ||||
|       } else if (arg.type === 'PipeSubstitution') { | ||||
|         return previousResults[expressionIndex - 1] | ||||
|       } else if (arg.type === 'ArrayExpression') { | ||||
|         return await executeArrayExpression( | ||||
|           programMemory, | ||||
|           arg, | ||||
|           engineCommandManager, | ||||
|           pipeInfo | ||||
|         ) | ||||
|       } else if (arg.type === 'CallExpression') { | ||||
|         const result: any = await executeCallExpression( | ||||
|           programMemory, | ||||
|           arg, | ||||
|           engineCommandManager, | ||||
|           previousPathToNode, | ||||
|           _pipeInfo | ||||
|         ) | ||||
|         return result | ||||
|       } else if (arg.type === 'ObjectExpression') { | ||||
|         return await executeObjectExpression( | ||||
|           programMemory, | ||||
|           arg, | ||||
|           engineCommandManager, | ||||
|           _pipeInfo | ||||
|         ) | ||||
|       } else if (arg.type === 'UnaryExpression') { | ||||
|         return getUnaryExpressionResult( | ||||
|           arg, | ||||
|           programMemory, | ||||
|           engineCommandManager, | ||||
|           _pipeInfo | ||||
|         ) | ||||
|       } else if (arg.type === 'BinaryExpression') { | ||||
|         return getBinaryExpressionResult( | ||||
|           arg, | ||||
|           programMemory, | ||||
|           engineCommandManager, | ||||
|           _pipeInfo | ||||
|         ) | ||||
|       } | ||||
|       throw new KCLSyntaxError('Invalid argument type in function call', [ | ||||
|         [arg.start, arg.end], | ||||
|       ]) | ||||
|     }) | ||||
|   ) | ||||
|   if (functionName in internalFns) { | ||||
|     const fnNameWithSketchOrExtrude = functionName as InternalFnNames | ||||
|     const result = await internalFns[fnNameWithSketchOrExtrude]( | ||||
|       { | ||||
|         programMemory, | ||||
|         sourceRange: sourceRangeOverride || [expression.start, expression.end], | ||||
|         engineCommandManager, | ||||
|         code: JSON.stringify(expression), | ||||
|       }, | ||||
|       fnArgs[0], | ||||
|       fnArgs[1], | ||||
|       fnArgs[2] | ||||
|     ) | ||||
|     return isInPipe | ||||
|       ? await executePipeBody( | ||||
|           body, | ||||
|           programMemory, | ||||
|           engineCommandManager, | ||||
|           previousPathToNode, | ||||
|           expressionIndex + 1, | ||||
|           [...previousResults, result] | ||||
|         ) | ||||
|       : result | ||||
|   } | ||||
|   const result = await programMemory.root[functionName].value(...fnArgs) | ||||
|   return isInPipe | ||||
|     ? await executePipeBody( | ||||
|         body, | ||||
|         programMemory, | ||||
|         engineCommandManager, | ||||
|         previousPathToNode, | ||||
|         expressionIndex + 1, | ||||
|         [...previousResults, result] | ||||
|       ) | ||||
|     : result | ||||
| } | ||||
|  | ||||
| @ -182,14 +182,14 @@ describe('Testing moveValueIntoNewVariable', () => { | ||||
|   const code = `${fn('def')}${fn('ghi')}${fn('jkl')}${fn('hmm')} | ||||
| const abc = 3 | ||||
| const identifierGuy = 5 | ||||
| const yo = 5 + 6 | ||||
| const part001 = startSketchAt([-1.2, 4.83]) | ||||
| |> line([2.8, 0], %) | ||||
| |> angledLine([100 + 100, 3.09], %) | ||||
| |> angledLine([abc, 3.09], %) | ||||
| |> angledLine([def(yo), 3.09], %) | ||||
| |> angledLine([def('yo'), 3.09], %) | ||||
| |> angledLine([ghi(%), 3.09], %) | ||||
| |> angledLine([jkl(yo) + 2, 3.09], %) | ||||
| |> angledLine([jkl('yo') + 2, 3.09], %) | ||||
| const yo = 5 + 6 | ||||
| const yo2 = hmm([identifierGuy + 5]) | ||||
| show(part001)` | ||||
|   it('should move a binary expression into a new variable', async () => { | ||||
| @ -231,7 +231,7 @@ show(part001)` | ||||
|       'newVar' | ||||
|     ) | ||||
|     const newCode = recast(modifiedAst) | ||||
|     expect(newCode).toContain(`const newVar = def(yo)`) | ||||
|     expect(newCode).toContain(`const newVar = def('yo')`) | ||||
|     expect(newCode).toContain(`angledLine([newVar, 3.09], %)`) | ||||
|   }) | ||||
|   it('should move a binary expression with call expression into a new variable', async () => { | ||||
| @ -245,7 +245,7 @@ show(part001)` | ||||
|       'newVar' | ||||
|     ) | ||||
|     const newCode = recast(modifiedAst) | ||||
|     expect(newCode).toContain(`const newVar = jkl(yo) + 2`) | ||||
|     expect(newCode).toContain(`const newVar = jkl('yo') + 2`) | ||||
|     expect(newCode).toContain(`angledLine([newVar, 3.09], %)`) | ||||
|   }) | ||||
|   it('should move a identifier into a new variable', async () => { | ||||
|  | ||||
| @ -36,14 +36,14 @@ export function addSketchTo( | ||||
|   const _node = { ...node } | ||||
|   const _name = name || findUniqueName(node, 'part') | ||||
|  | ||||
|   const startSketchAt = createCallExpressionStdLib('startSketchAt', [ | ||||
|   const startSketchAt = createCallExpression('startSketchAt', [ | ||||
|     createLiteral('default'), | ||||
|   ]) | ||||
|   const rotate = createCallExpression(axis === 'xz' ? 'rx' : 'ry', [ | ||||
|     createLiteral(90), | ||||
|     createPipeSubstitution(), | ||||
|   ]) | ||||
|   const initialLineTo = createCallExpressionStdLib('line', [ | ||||
|   const initialLineTo = createCallExpression('line', [ | ||||
|     createLiteral('default'), | ||||
|     createPipeSubstitution(), | ||||
|   ]) | ||||
| @ -112,9 +112,7 @@ function addToShow(node: Program, name: string): Program { | ||||
|   const dumbyStartend = { start: 0, end: 0 } | ||||
|   const showCallIndex = getShowIndex(_node) | ||||
|   if (showCallIndex === -1) { | ||||
|     const showCall = createCallExpressionStdLib('show', [ | ||||
|       createIdentifier(name), | ||||
|     ]) | ||||
|     const showCall = createCallExpression('show', [createIdentifier(name)]) | ||||
|     const showExpressionStatement: ExpressionStatement = { | ||||
|       type: 'ExpressionStatement', | ||||
|       ...dumbyStartend, | ||||
| @ -126,7 +124,7 @@ function addToShow(node: Program, name: string): Program { | ||||
|   const showCall = { ..._node.body[showCallIndex] } as ExpressionStatement | ||||
|   const showCallArgs = (showCall.expression as CallExpression).arguments | ||||
|   const newShowCallArgs: Value[] = [...showCallArgs, createIdentifier(name)] | ||||
|   const newShowExpression = createCallExpressionStdLib('show', newShowCallArgs) | ||||
|   const newShowExpression = createCallExpression('show', newShowCallArgs) | ||||
|  | ||||
|   _node.body[showCallIndex] = { | ||||
|     ...showCall, | ||||
| @ -227,7 +225,7 @@ export function extrudeSketch( | ||||
|   const { node: variableDeclorator, shallowPath: pathToDecleration } = | ||||
|     getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator') | ||||
|  | ||||
|   const extrudeCall = createCallExpressionStdLib('extrude', [ | ||||
|   const extrudeCall = createCallExpression('extrude', [ | ||||
|     createLiteral(4), | ||||
|     shouldPipe | ||||
|       ? createPipeSubstitution() | ||||
| @ -315,15 +313,15 @@ export function sketchOnExtrudedFace( | ||||
|   const newSketch = createVariableDeclaration( | ||||
|     newSketchName, | ||||
|     createPipeExpression([ | ||||
|       createCallExpressionStdLib('startSketchAt', [ | ||||
|       createCallExpression('startSketchAt', [ | ||||
|         createArrayExpression([createLiteral(0), createLiteral(0)]), | ||||
|       ]), | ||||
|       createCallExpressionStdLib('lineTo', [ | ||||
|       createCallExpression('lineTo', [ | ||||
|         createArrayExpression([createLiteral(1), createLiteral(1)]), | ||||
|         createPipeSubstitution(), | ||||
|       ]), | ||||
|       createCallExpression('transform', [ | ||||
|         createCallExpressionStdLib('getExtrudeWallTransform', [ | ||||
|         createCallExpression('getExtrudeWallTransform', [ | ||||
|           createLiteral(tag), | ||||
|           createIdentifier(oldSketchName), | ||||
|         ]), | ||||
| @ -416,40 +414,6 @@ export function createPipeSubstitution(): PipeSubstitution { | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function createCallExpressionStdLib( | ||||
|   name: string, | ||||
|   args: CallExpression['arguments'] | ||||
| ): CallExpression { | ||||
|   return { | ||||
|     type: 'CallExpression', | ||||
|     start: 0, | ||||
|     end: 0, | ||||
|     callee: { | ||||
|       type: 'Identifier', | ||||
|       start: 0, | ||||
|       end: 0, | ||||
|       name, | ||||
|     }, | ||||
|     function: { | ||||
|       type: 'StdLib', | ||||
|       func: { | ||||
|         // We only need the name here to map it back when it serializes | ||||
|         // to rust, don't worry about the rest. | ||||
|         name, | ||||
|         summary: '', | ||||
|         description: '', | ||||
|         tags: [], | ||||
|         returnValue: { type: '', required: false, name: '', schema: {} }, | ||||
|         args: [], | ||||
|         unpublished: false, | ||||
|         deprecated: false, | ||||
|       }, | ||||
|     }, | ||||
|     optional: false, | ||||
|     arguments: args, | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function createCallExpression( | ||||
|   name: string, | ||||
|   args: CallExpression['arguments'] | ||||
| @ -464,9 +428,6 @@ export function createCallExpression( | ||||
|       end: 0, | ||||
|       name, | ||||
|     }, | ||||
|     function: { | ||||
|       type: 'InMemory', | ||||
|     }, | ||||
|     optional: false, | ||||
|     arguments: args, | ||||
|   } | ||||
|  | ||||
| @ -45,7 +45,8 @@ const newVar = myVar + 1` | ||||
|     expect(recasted).toBe(code.trim()) | ||||
|   }) | ||||
|   it('test with function call', () => { | ||||
|     const code = `const myVar = "hello" | ||||
|     const code = ` | ||||
| const myVar = "hello" | ||||
| log(5, myVar)` | ||||
|     const { ast } = code2ast(code) | ||||
|     const recasted = recast(ast) | ||||
| @ -70,7 +71,8 @@ log(5, myVar)` | ||||
|   |> lineTo({ to: [1, 0], tag: "rightPath" }, %) | ||||
|   |> close(%) | ||||
|  | ||||
| show(mySketch)` | ||||
| show(mySketch) | ||||
| ` | ||||
|     const { ast } = code2ast(code) | ||||
|     const recasted = recast(ast) | ||||
|     expect(recasted).toBe(code.trim()) | ||||
| @ -184,7 +186,8 @@ const myVar2 = yo['a'][key2].c` | ||||
|  | ||||
| describe('testing recasting with comments and whitespace', () => { | ||||
|   it('code with comments', () => { | ||||
|     const code = `const yo = { a: { b: { c: '123' } } } | ||||
|     const code = ` | ||||
| const yo = { a: { b: { c: '123' } } } | ||||
| // this is a comment | ||||
| const key = 'c'` | ||||
|  | ||||
| @ -194,18 +197,20 @@ const key = 'c'` | ||||
|     expect(recasted).toBe(code) | ||||
|   }) | ||||
|   it('code with comment and extra lines', () => { | ||||
|     const code = `const yo = 'c' | ||||
|  | ||||
| /* this is | ||||
|     const code = ` | ||||
| const yo = 'c' /* this is | ||||
| a | ||||
| comment */ | ||||
|  | ||||
| const yo = 'bing'` | ||||
|     const { ast } = code2ast(code) | ||||
|     const recasted = recast(ast) | ||||
|     expect(recasted).toBe(code) | ||||
|   }) | ||||
|   it('comments at the start and end', () => { | ||||
|     const code = `// this is a comment | ||||
|     const code = ` | ||||
| // this is a comment | ||||
|  | ||||
| const yo = { a: { b: { c: '123' } } } | ||||
| const key = 'c' | ||||
|  | ||||
| @ -215,12 +220,12 @@ const key = 'c' | ||||
|     expect(recasted).toBe(code) | ||||
|   }) | ||||
|   it('comments in a fn block', () => { | ||||
|     const code = `const myFn = () => { | ||||
|     const code = ` | ||||
| const myFn = () => { | ||||
|   // this is a comment | ||||
|   const yo = { a: { b: { c: '123' } } } | ||||
|  | ||||
|   /* block | ||||
|   const yo = { a: { b: { c: '123' } } } /* block | ||||
|   comment */ | ||||
|  | ||||
|   const key = 'c' | ||||
|   // this is also a comment | ||||
| }` | ||||
| @ -250,7 +255,7 @@ const mySk1 = startSketchAt([0, 0]) | ||||
|   // comment here | ||||
|   |> lineTo({ to: [0, 1], tag: 'myTag' }, %) | ||||
|   |> lineTo([1, 1], %) /* and | ||||
|   here | ||||
|   here  | ||||
|   */ | ||||
|   // a comment between pipe expression statements | ||||
|   |> rx(90, %) | ||||
| @ -264,21 +269,7 @@ const mySk1 = startSketchAt([0, 0]) | ||||
|   */` | ||||
|     const { ast } = code2ast(code) | ||||
|     const recasted = recast(ast) | ||||
|     expect(recasted).toBe(`// comment at start | ||||
| const mySk1 = startSketchAt([0, 0]) | ||||
|   |> lineTo([1, 1], %) | ||||
|   // comment here | ||||
|   |> lineTo({ to: [0, 1], tag: 'myTag' }, %) | ||||
|   |> lineTo([1, 1], %) | ||||
|   /* and | ||||
|   here | ||||
|  | ||||
| a comment between pipe expression statements */ | ||||
|   |> rx(90, %) | ||||
|   // and another with just white space between others below | ||||
|   |> ry(45, %) | ||||
|   |> rx(45, %) | ||||
| // one more for good measure`) | ||||
|     expect(recasted).toBe(code) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -304,7 +295,7 @@ describe('testing call Expressions in BinaryExpressions and UnaryExpressions', ( | ||||
|   it('with unaryExpression in sketch situation', () => { | ||||
|     const code = [ | ||||
|       'const part001 = startSketchAt([0, 0])', | ||||
|       '  |> line([-2.21, -legLen(5, min(3, 999))], %)', | ||||
|       '|> line([-2.21, -legLen(5, min(3, 999))], %)', | ||||
|     ].join('\n') | ||||
|     const { ast } = code2ast(code) | ||||
|     const recasted = recast(ast) | ||||
| @ -318,10 +309,10 @@ describe('it recasts wrapped object expressions in pipe bodies with correct inde | ||||
|   |> line({ to: [0.62, 4.15], tag: 'seg01' }, %) | ||||
|   |> line([2.77, -1.24], %) | ||||
|   |> angledLineThatIntersects({ | ||||
|        angle: 201, | ||||
|        offset: -1.35, | ||||
|        intersectTag: 'seg01' | ||||
|      }, %) | ||||
|       angle: 201, | ||||
|       offset: -1.35, | ||||
|       intersectTag: 'seg01' | ||||
|     }, %) | ||||
|   |> line([-0.42, -1.72], %) | ||||
| show(part001)` | ||||
|     const { ast } = code2ast(code) | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| import { Program } from './abstractSyntaxTreeTypes' | ||||
| import { recast_wasm } from '../wasm-lib/pkg/wasm_lib' | ||||
| import { recast_js } from '../wasm-lib/pkg/wasm_lib' | ||||
|  | ||||
| export const recast = (ast: Program): string => { | ||||
|   try { | ||||
|     const s: string = recast_wasm(JSON.stringify(ast)) | ||||
|     const s: string = recast_js(JSON.stringify(ast)) | ||||
|     return s | ||||
|   } catch (e) { | ||||
|     // TODO: do something real with the error. | ||||
|  | ||||
| @ -1,14 +1,9 @@ | ||||
| import { SourceRange } from 'lang/executor' | ||||
| import { Selections } from 'useStore' | ||||
| import { | ||||
|   VITE_KC_API_WS_MODELING_URL, | ||||
|   VITE_KC_CONNECTION_TIMEOUT_MS, | ||||
|   VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS, | ||||
| } from 'env' | ||||
| import { SourceRange } from '../executor' | ||||
| import { Selections } from '../../useStore' | ||||
| import { VITE_KC_API_WS_MODELING_URL } from '../../env' | ||||
| import { Models } from '@kittycad/lib' | ||||
| import { exportSave } from 'lib/exportSave' | ||||
| import { exportSave } from '../../lib/exportSave' | ||||
| import { v4 as uuidv4 } from 'uuid' | ||||
| import * as Sentry from '@sentry/react' | ||||
|  | ||||
| interface ResultCommand { | ||||
|   type: 'result' | ||||
| @ -27,60 +22,61 @@ export interface SourceRangeMap { | ||||
|   [key: string]: SourceRange | ||||
| } | ||||
|  | ||||
| interface SelectionsArgs { | ||||
|   id: string | ||||
|   type: Selections['codeBasedSelections'][number]['type'] | ||||
| } | ||||
|  | ||||
| interface CursorSelectionsArgs { | ||||
|   otherSelections: Selections['otherSelections'] | ||||
|   idBasedSelections: { type: string; id: string }[] | ||||
| } | ||||
|  | ||||
| interface NewTrackArgs { | ||||
|   conn: EngineConnection | ||||
|   mediaStream: MediaStream | ||||
| } | ||||
|  | ||||
| type WebSocketResponse = Models['OkWebSocketResponseData_type'] | ||||
| export type EngineCommand = Models['WebSocketMessages_type'] | ||||
|  | ||||
| type OkResponse = Models['OkModelingCmdResponse_type'] | ||||
|  | ||||
| type WebSocketResponse = Models['WebSocketResponses_type'] | ||||
|  | ||||
| enum EngineConnectionEvents { | ||||
|   ConnectionStarted = 'connectionStarted', | ||||
|   WebsocketOpen = 'websocketOpen', | ||||
|   NewTrack = 'newTrack', | ||||
|   DataChannelOpen = 'dataChannelOpen', | ||||
|   Open = 'open', | ||||
|   Close = 'close', | ||||
| } | ||||
|  | ||||
| // EngineConnection encapsulates the connection(s) to the Engine | ||||
| // for the EngineCommandManager; namely, the underlying WebSocket | ||||
| // and WebRTC connections. | ||||
| export class EngineConnection { | ||||
| export class EngineConnection extends EventTarget { | ||||
|   websocket?: WebSocket | ||||
|   pc?: RTCPeerConnection | ||||
|   unreliableDataChannel?: RTCDataChannel | ||||
|   lossyDataChannel?: RTCDataChannel | ||||
|  | ||||
|   private ready: boolean | ||||
|  | ||||
|   readonly url: string | ||||
|   private readonly token?: string | ||||
|   private onWebsocketOpen: (engineConnection: EngineConnection) => void | ||||
|   private onDataChannelOpen: (engineConnection: EngineConnection) => void | ||||
|   private onEngineConnectionOpen: (engineConnection: EngineConnection) => void | ||||
|   private onConnectionStarted: (engineConnection: EngineConnection) => void | ||||
|   private onClose: (engineConnection: EngineConnection) => void | ||||
|   private onNewTrack: (track: NewTrackArgs) => void | ||||
|  | ||||
|   constructor({ | ||||
|     url, | ||||
|     token, | ||||
|     onWebsocketOpen = () => {}, | ||||
|     onNewTrack = () => {}, | ||||
|     onEngineConnectionOpen = () => {}, | ||||
|     onConnectionStarted = () => {}, | ||||
|     onClose = () => {}, | ||||
|     onDataChannelOpen = () => {}, | ||||
|   }: { | ||||
|     url: string | ||||
|     token?: string | ||||
|     onWebsocketOpen?: (engineConnection: EngineConnection) => void | ||||
|     onDataChannelOpen?: (engineConnection: EngineConnection) => void | ||||
|     onEngineConnectionOpen?: (engineConnection: EngineConnection) => void | ||||
|     onConnectionStarted?: (engineConnection: EngineConnection) => void | ||||
|     onClose?: (engineConnection: EngineConnection) => void | ||||
|     onNewTrack?: (track: NewTrackArgs) => void | ||||
|   }) { | ||||
|   constructor({ url, token }: { url: string; token?: string }) { | ||||
|     super() | ||||
|     this.url = url | ||||
|     this.token = token | ||||
|     this.ready = false | ||||
|     this.onWebsocketOpen = onWebsocketOpen | ||||
|     this.onDataChannelOpen = onDataChannelOpen | ||||
|     this.onEngineConnectionOpen = onEngineConnectionOpen | ||||
|     this.onConnectionStarted = onConnectionStarted | ||||
|     this.onClose = onClose | ||||
|     this.onNewTrack = onNewTrack | ||||
|  | ||||
|     this.addEventListener(EngineConnectionEvents.Open, () => { | ||||
|       this.ready = true | ||||
|     }) | ||||
|     this.addEventListener(EngineConnectionEvents.Close, () => { | ||||
|       this.ready = false | ||||
|     }) | ||||
|  | ||||
|     // TODO(paultag): This ought to be tweakable. | ||||
|     const pingIntervalMs = 10000 | ||||
| @ -102,11 +98,6 @@ export class EngineConnection { | ||||
|   isReady() { | ||||
|     return this.ready | ||||
|   } | ||||
|   // shouldTrace will return true when Sentry should be used to instrument | ||||
|   // the Engine. | ||||
|   shouldTrace() { | ||||
|     return Sentry.getCurrentHub()?.getClient()?.getOptions()?.sendClientReports | ||||
|   } | ||||
|   // connect will attempt to connect to the Engine over a WebSocket, and | ||||
|   // establish the WebRTC connections. | ||||
|   // | ||||
| @ -116,44 +107,6 @@ export class EngineConnection { | ||||
|     // TODO(paultag): make this safe to call multiple times, and figure out | ||||
|     // when a connection is in progress (state: connecting or something). | ||||
|  | ||||
|     // Information on the connect transaction | ||||
|  | ||||
|     class SpanPromise { | ||||
|       span: Sentry.Span | ||||
|       promise: Promise<void> | ||||
|       resolve?: (v: void) => void | ||||
|  | ||||
|       constructor(span: Sentry.Span) { | ||||
|         this.span = span | ||||
|         this.promise = new Promise((resolve) => { | ||||
|           this.resolve = (v: void) => { | ||||
|             // here we're going to invoke finish before resolving the | ||||
|             // promise so that a `.then()` will order strictly after | ||||
|             // all spans have -- for sure -- been resolved, rather than | ||||
|             // doing a `then` on this promise. | ||||
|             this.span.finish() | ||||
|             resolve(v) | ||||
|           } | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     let webrtcMediaTransaction: Sentry.Transaction | ||||
|     let websocketSpan: SpanPromise | ||||
|     let mediaTrackSpan: SpanPromise | ||||
|     let dataChannelSpan: SpanPromise | ||||
|     let handshakeSpan: SpanPromise | ||||
|     let iceSpan: SpanPromise | ||||
|  | ||||
|     if (this.shouldTrace()) { | ||||
|       webrtcMediaTransaction = Sentry.startTransaction({ | ||||
|         name: 'webrtc-media', | ||||
|       }) | ||||
|       websocketSpan = new SpanPromise( | ||||
|         webrtcMediaTransaction.startChild({ op: 'websocket' }) | ||||
|       ) | ||||
|     } | ||||
|  | ||||
|     this.websocket = new WebSocket(this.url, []) | ||||
|     this.websocket.binaryType = 'arraybuffer' | ||||
|  | ||||
| @ -167,38 +120,11 @@ export class EngineConnection { | ||||
|     }) | ||||
|  | ||||
|     this.websocket.addEventListener('open', (event) => { | ||||
|       if (this.shouldTrace()) { | ||||
|         websocketSpan.resolve?.() | ||||
|  | ||||
|         handshakeSpan = new SpanPromise( | ||||
|           webrtcMediaTransaction.startChild({ op: 'handshake' }) | ||||
|         ) | ||||
|         iceSpan = new SpanPromise( | ||||
|           webrtcMediaTransaction.startChild({ op: 'ice' }) | ||||
|         ) | ||||
|         dataChannelSpan = new SpanPromise( | ||||
|           webrtcMediaTransaction.startChild({ | ||||
|             op: 'data-channel', | ||||
|           }) | ||||
|         ) | ||||
|         mediaTrackSpan = new SpanPromise( | ||||
|           webrtcMediaTransaction.startChild({ | ||||
|             op: 'media-track', | ||||
|           }) | ||||
|         ) | ||||
|       } | ||||
|  | ||||
|       Promise.all([ | ||||
|         handshakeSpan.promise, | ||||
|         iceSpan.promise, | ||||
|         dataChannelSpan.promise, | ||||
|         mediaTrackSpan.promise, | ||||
|       ]).then(() => { | ||||
|         console.log('All spans finished, reporting') | ||||
|         webrtcMediaTransaction?.finish() | ||||
|       }) | ||||
|  | ||||
|       this.onWebsocketOpen(this) | ||||
|       this.dispatchEvent( | ||||
|         new CustomEvent(EngineConnectionEvents.WebsocketOpen, { | ||||
|           detail: this, | ||||
|         }) | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     this.websocket.addEventListener('close', (event) => { | ||||
| @ -223,32 +149,17 @@ export class EngineConnection { | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       const message: Models['WebSocketResponse_type'] = JSON.parse(event.data) | ||||
|  | ||||
|       if (!message.success) { | ||||
|         if (message.request_id) { | ||||
|           console.error(`Error in response to request ${message.request_id}:`) | ||||
|         } else { | ||||
|           console.error(`Error from server:`) | ||||
|         } | ||||
|         message?.errors?.forEach((error) => { | ||||
|           console.error(` - ${error.error_code}: ${error.message}`) | ||||
|         }) | ||||
|       if (event.data.toLocaleLowerCase().startsWith('error')) { | ||||
|         console.error('something went wrong: ', event.data) | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       let resp = message.resp | ||||
|       if (!resp) { | ||||
|         // If there's no body to the response, we can bail here. | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       if (resp.type === 'sdp_answer') { | ||||
|         let answer = resp.data?.answer | ||||
|         if (!answer || answer.type === 'unspecified') { | ||||
|           return | ||||
|         } | ||||
|       const message: WebSocketResponse = JSON.parse(event.data) | ||||
|  | ||||
|       if ( | ||||
|         message.type === 'sdp_answer' && | ||||
|         message.answer.type !== 'unspecified' | ||||
|       ) { | ||||
|         if (this.pc?.signalingState !== 'stable') { | ||||
|           // If the connection is stable, we shouldn't bother updating the | ||||
|           // SDP, since we have a stable connection to the backend. If we | ||||
| @ -256,33 +167,24 @@ export class EngineConnection { | ||||
|           // tore down. | ||||
|           this.pc?.setRemoteDescription( | ||||
|             new RTCSessionDescription({ | ||||
|               type: answer.type, | ||||
|               sdp: answer.sdp, | ||||
|               type: message.answer.type, | ||||
|               sdp: message.answer.sdp, | ||||
|             }) | ||||
|           ) | ||||
|  | ||||
|           if (this.shouldTrace()) { | ||||
|             // When both ends have a local and remote SDP, we've been able to | ||||
|             // set up successfully. We'll still need to find the right ICE | ||||
|             // servers, but this is hand-shook. | ||||
|             handshakeSpan.resolve?.() | ||||
|           } | ||||
|         } | ||||
|       } else if (resp.type === 'trickle_ice') { | ||||
|         let candidate = resp.data?.candidate | ||||
|         this.pc?.addIceCandidate(candidate as RTCIceCandidateInit) | ||||
|       } else if (resp.type === 'ice_server_info' && this.pc) { | ||||
|       } else if (message.type === 'trickle_ice') { | ||||
|         this.pc?.addIceCandidate(message.candidate as RTCIceCandidateInit) | ||||
|       } else if (message.type === 'ice_server_info' && this.pc) { | ||||
|         console.log('received ice_server_info') | ||||
|         let ice_servers = resp.data?.ice_servers | ||||
|  | ||||
|         if (ice_servers?.length > 0) { | ||||
|         if (message.ice_servers.length > 0) { | ||||
|           // When we set the Configuration, we want to always force | ||||
|           // iceTransportPolicy to 'relay', since we know the topology | ||||
|           // of the ICE/STUN/TUN server and the engine. We don't wish to | ||||
|           // talk to the engine in any configuration /other/ than relay | ||||
|           // from a infra POV. | ||||
|           this.pc.setConfiguration({ | ||||
|             iceServers: ice_servers, | ||||
|             iceServers: message.ice_servers, | ||||
|             iceTransportPolicy: 'relay', | ||||
|           }) | ||||
|         } else { | ||||
| @ -296,14 +198,20 @@ export class EngineConnection { | ||||
|         // PeerConnection and waiting for events to fire our callbacks. | ||||
|  | ||||
|         this.pc.addEventListener('connectionstatechange', (event) => { | ||||
|           if (this.pc?.iceConnectionState === 'connected') { | ||||
|             iceSpan.resolve?.() | ||||
|           } | ||||
|           // if (this.pc?.iceConnectionState === 'disconnected') { | ||||
|           //   this.close() | ||||
|           // } | ||||
|         }) | ||||
|  | ||||
|         this.pc.addEventListener('icecandidate', (event) => { | ||||
|           if (!this.pc || !this.websocket) return | ||||
|           if (event.candidate !== null) { | ||||
|           if (event.candidate === null) { | ||||
|             console.log('sent sdp_offer') | ||||
|             this.send({ | ||||
|               type: 'sdp_offer', | ||||
|               offer: this.pc.localDescription, | ||||
|             }) | ||||
|           } else { | ||||
|             console.log('sending trickle ice candidate') | ||||
|             const { candidate } = event | ||||
|             this.send({ | ||||
| @ -335,7 +243,7 @@ export class EngineConnection { | ||||
|       // TODO(paultag): This ought to be both controllable, as well as something | ||||
|       // like exponential backoff to have some grace on the backend, as well as | ||||
|       // fix responsiveness for clients that had a weird network hiccup. | ||||
|       const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS | ||||
|       const connectionTimeoutMs = 5000 | ||||
|  | ||||
|       setTimeout(() => { | ||||
|         if (this.isReady()) { | ||||
| @ -348,220 +256,73 @@ export class EngineConnection { | ||||
|     }) | ||||
|  | ||||
|     this.pc.addEventListener('track', (event) => { | ||||
|       console.log('received track', event) | ||||
|       const mediaStream = event.streams[0] | ||||
|  | ||||
|       if (this.shouldTrace()) { | ||||
|         let mediaStreamTrack = mediaStream.getVideoTracks()[0] | ||||
|         mediaStreamTrack.addEventListener('unmute', () => { | ||||
|           // let settings = mediaStreamTrack.getSettings() | ||||
|           // mediaTrackSpan.span.setTag("fps", settings.frameRate) | ||||
|           // mediaTrackSpan.span.setTag("width", settings.width) | ||||
|           // mediaTrackSpan.span.setTag("height", settings.height) | ||||
|           mediaTrackSpan.resolve?.() | ||||
|       this.dispatchEvent( | ||||
|         new CustomEvent(EngineConnectionEvents.NewTrack, { | ||||
|           detail: { | ||||
|             conn: this, | ||||
|             mediaStream: mediaStream, | ||||
|           }, | ||||
|         }) | ||||
|       } | ||||
|  | ||||
|       // Set up the background thread to keep an eye on statistical | ||||
|       // information about the WebRTC media stream from the server to | ||||
|       // us. We'll also eventually want more global statistical information, | ||||
|       // but this will give us a baseline. | ||||
|       if (parseInt(VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS) !== 0) { | ||||
|         setInterval(() => { | ||||
|           if (this.pc === undefined) { | ||||
|             return | ||||
|           } | ||||
|           if (!this.shouldTrace()) { | ||||
|             return | ||||
|           } | ||||
|  | ||||
|           // Use the WebRTC Statistics API to collect statistical information | ||||
|           // about the WebRTC connection we're using to report to Sentry. | ||||
|           mediaStream.getVideoTracks().forEach((videoTrack) => { | ||||
|             let trackStats = new Map<string, any>() | ||||
|             this.pc?.getStats(videoTrack).then((videoTrackStats) => { | ||||
|               // Sentry only allows 10 metrics per transaction. We're going | ||||
|               // to have to pick carefully here, eventually send like a prom | ||||
|               // file or something to the peer. | ||||
|  | ||||
|               const transaction = Sentry.startTransaction({ | ||||
|                 name: 'webrtc-stats', | ||||
|               }) | ||||
|               videoTrackStats.forEach((videoTrackReport) => { | ||||
|                 if (videoTrackReport.type === 'inbound-rtp') { | ||||
|                   // RTC Stream Info | ||||
|                   // transaction.setMeasurement( | ||||
|                   //   'mediaStreamTrack.framesDecoded', | ||||
|                   //   videoTrackReport.framesDecoded, | ||||
|                   //   'frame' | ||||
|                   // ) | ||||
|                   transaction.setMeasurement( | ||||
|                     'rtcFramesDropped', | ||||
|                     videoTrackReport.framesDropped, | ||||
|                     '' | ||||
|                   ) | ||||
|                   // transaction.setMeasurement( | ||||
|                   //   'mediaStreamTrack.framesReceived', | ||||
|                   //   videoTrackReport.framesReceived, | ||||
|                   //   'frame' | ||||
|                   // ) | ||||
|                   transaction.setMeasurement( | ||||
|                     'rtcFramesPerSecond', | ||||
|                     videoTrackReport.framesPerSecond, | ||||
|                     'fps' | ||||
|                   ) | ||||
|                   transaction.setMeasurement( | ||||
|                     'rtcFreezeCount', | ||||
|                     videoTrackReport.freezeCount, | ||||
|                     '' | ||||
|                   ) | ||||
|                   transaction.setMeasurement( | ||||
|                     'rtcJitter', | ||||
|                     videoTrackReport.jitter, | ||||
|                     'second' | ||||
|                   ) | ||||
|                   // transaction.setMeasurement( | ||||
|                   //   'mediaStreamTrack.jitterBufferDelay', | ||||
|                   //   videoTrackReport.jitterBufferDelay, | ||||
|                   //   '' | ||||
|                   // ) | ||||
|                   // transaction.setMeasurement( | ||||
|                   //   'mediaStreamTrack.jitterBufferEmittedCount', | ||||
|                   //   videoTrackReport.jitterBufferEmittedCount, | ||||
|                   //   '' | ||||
|                   // ) | ||||
|                   // transaction.setMeasurement( | ||||
|                   //   'mediaStreamTrack.jitterBufferMinimumDelay', | ||||
|                   //   videoTrackReport.jitterBufferMinimumDelay, | ||||
|                   //   '' | ||||
|                   // ) | ||||
|                   // transaction.setMeasurement( | ||||
|                   //   'mediaStreamTrack.jitterBufferTargetDelay', | ||||
|                   //   videoTrackReport.jitterBufferTargetDelay, | ||||
|                   //   '' | ||||
|                   // ) | ||||
|                   transaction.setMeasurement( | ||||
|                     'rtcKeyFramesDecoded', | ||||
|                     videoTrackReport.keyFramesDecoded, | ||||
|                     '' | ||||
|                   ) | ||||
|                   transaction.setMeasurement( | ||||
|                     'rtcTotalFreezesDuration', | ||||
|                     videoTrackReport.totalFreezesDuration, | ||||
|                     'second' | ||||
|                   ) | ||||
|                   // transaction.setMeasurement( | ||||
|                   //   'mediaStreamTrack.totalInterFrameDelay', | ||||
|                   //   videoTrackReport.totalInterFrameDelay, | ||||
|                   //   '' | ||||
|                   // ) | ||||
|                   transaction.setMeasurement( | ||||
|                     'rtcTotalPausesDuration', | ||||
|                     videoTrackReport.totalPausesDuration, | ||||
|                     'second' | ||||
|                   ) | ||||
|                   // transaction.setMeasurement( | ||||
|                   //   'mediaStreamTrack.totalProcessingDelay', | ||||
|                   //   videoTrackReport.totalProcessingDelay, | ||||
|                   //   'second' | ||||
|                   // ) | ||||
|                 } else if (videoTrackReport.type === 'transport') { | ||||
|                   // // Bytes i/o | ||||
|                   // transaction.setMeasurement( | ||||
|                   //   'mediaStreamTrack.bytesReceived', | ||||
|                   //   videoTrackReport.bytesReceived, | ||||
|                   //   'byte' | ||||
|                   // ) | ||||
|                   // transaction.setMeasurement( | ||||
|                   //   'mediaStreamTrack.bytesSent', | ||||
|                   //   videoTrackReport.bytesSent, | ||||
|                   //   'byte' | ||||
|                   // ) | ||||
|                 } | ||||
|               }) | ||||
|               transaction?.finish() | ||||
|             }) | ||||
|           }) | ||||
|         }, VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS) | ||||
|       } | ||||
|  | ||||
|       this.onNewTrack({ | ||||
|         conn: this, | ||||
|         mediaStream: mediaStream, | ||||
|       }) | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
|     // During startup, we'll track the time from `connect` being called | ||||
|     // until the 'done' event fires. | ||||
|     let connectionStarted = new Date() | ||||
|  | ||||
|     this.pc.addEventListener('datachannel', (event) => { | ||||
|       this.unreliableDataChannel = event.channel | ||||
|       this.lossyDataChannel = event.channel | ||||
|  | ||||
|       console.log('accepted unreliable data channel', event.channel.label) | ||||
|       this.unreliableDataChannel.addEventListener('open', (event) => { | ||||
|         console.log('unreliable data channel opened', event) | ||||
|         if (this.shouldTrace()) { | ||||
|           dataChannelSpan.resolve?.() | ||||
|         } | ||||
|       console.log('accepted lossy data channel', event.channel.label) | ||||
|       this.lossyDataChannel.addEventListener('open', (event) => { | ||||
|         console.log('lossy data channel opened', event) | ||||
|  | ||||
|         this.onDataChannelOpen(this) | ||||
|         this.dispatchEvent( | ||||
|           new CustomEvent(EngineConnectionEvents.DataChannelOpen, { | ||||
|             detail: this, | ||||
|           }) | ||||
|         ) | ||||
|  | ||||
|         this.onEngineConnectionOpen(this) | ||||
|         this.ready = true | ||||
|         this.dispatchEvent( | ||||
|           new CustomEvent(EngineConnectionEvents.Open, { | ||||
|             detail: this, | ||||
|           }) | ||||
|         ) | ||||
|       }) | ||||
|  | ||||
|       this.unreliableDataChannel.addEventListener('close', (event) => { | ||||
|         console.log('unreliable data channel closed') | ||||
|       this.lossyDataChannel.addEventListener('close', (event) => { | ||||
|         console.log('lossy data channel closed') | ||||
|         this.close() | ||||
|       }) | ||||
|  | ||||
|       this.unreliableDataChannel.addEventListener('error', (event) => { | ||||
|         console.log('unreliable data channel error') | ||||
|       this.lossyDataChannel.addEventListener('error', (event) => { | ||||
|         console.log('lossy data channel error') | ||||
|         this.close() | ||||
|       }) | ||||
|     }) | ||||
|  | ||||
|     this.onConnectionStarted(this) | ||||
|     this.dispatchEvent( | ||||
|       new CustomEvent(EngineConnectionEvents.ConnectionStarted, { | ||||
|         detail: this, | ||||
|       }) | ||||
|     ) | ||||
|   } | ||||
|   send(message: object | string) { | ||||
|   send(message: object) { | ||||
|     // TODO(paultag): Add in logic to determine the connection state and | ||||
|     // take actions if needed? | ||||
|     this.websocket?.send( | ||||
|       typeof message === 'string' ? message : JSON.stringify(message) | ||||
|     ) | ||||
|     this.websocket?.send(JSON.stringify(message)) | ||||
|   } | ||||
|   close() { | ||||
|     this.websocket?.close() | ||||
|     this.pc?.close() | ||||
|     this.unreliableDataChannel?.close() | ||||
|     this.websocket = undefined | ||||
|     this.pc = undefined | ||||
|     this.unreliableDataChannel = undefined | ||||
|     this.lossyDataChannel?.close() | ||||
|  | ||||
|     this.onClose(this) | ||||
|     this.ready = false | ||||
|     this.dispatchEvent( | ||||
|       new CustomEvent(EngineConnectionEvents.Close, { | ||||
|         detail: this, | ||||
|       }) | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export type EngineCommand = Models['WebSocketRequest_type'] | ||||
| type ModelTypes = Models['OkModelingCmdResponse_type']['type'] | ||||
|  | ||||
| type UnreliableResponses = Extract< | ||||
|   Models['OkModelingCmdResponse_type'], | ||||
|   { type: 'highlight_set_entity' } | ||||
| > | ||||
| interface UnreliableSubscription<T extends UnreliableResponses['type']> { | ||||
|   event: T | ||||
|   callback: (data: Extract<UnreliableResponses, { type: T }>) => void | ||||
| } | ||||
|  | ||||
| interface Subscription<T extends ModelTypes> { | ||||
|   event: T | ||||
|   callback: ( | ||||
|     data: Extract<Models['OkModelingCmdResponse_type'], { type: T }> | ||||
|   ) => void | ||||
| } | ||||
|  | ||||
| export class EngineCommandManager { | ||||
|   artifactMap: ArtifactMap = {} | ||||
|   sourceRangeMap: SourceRangeMap = {} | ||||
| @ -570,17 +331,10 @@ export class EngineCommandManager { | ||||
|   engineConnection?: EngineConnection | ||||
|   waitForReady: Promise<void> = new Promise(() => {}) | ||||
|   private resolveReady = () => {} | ||||
|  | ||||
|   subscriptions: { | ||||
|     [event: string]: { | ||||
|       [localUnsubscribeId: string]: (a: any) => void | ||||
|     } | ||||
|   } = {} as any | ||||
|   unreliableSubscriptions: { | ||||
|     [event: string]: { | ||||
|       [localUnsubscribeId: string]: (a: any) => void | ||||
|     } | ||||
|   } = {} as any | ||||
|   onHoverCallback: (id?: string) => void = () => {} | ||||
|   onClickCallback: (selection?: SelectionsArgs) => void = () => {} | ||||
|   onCursorsSelectedCallback: (selections: CursorSelectionsArgs) => void = | ||||
|     () => {} | ||||
|   constructor({ | ||||
|     setMediaStream, | ||||
|     setIsStreamReady, | ||||
| @ -601,43 +355,48 @@ export class EngineCommandManager { | ||||
|     this.engineConnection = new EngineConnection({ | ||||
|       url, | ||||
|       token, | ||||
|       onEngineConnectionOpen: () => { | ||||
|     }) | ||||
|  | ||||
|     this.engineConnection.addEventListener( | ||||
|       EngineConnectionEvents.Open, | ||||
|       (event) => { | ||||
|         this.resolveReady() | ||||
|         setIsStreamReady(true) | ||||
|       }, | ||||
|       onClose: () => { | ||||
|         setIsStreamReady(false) | ||||
|       }, | ||||
|       onConnectionStarted: (engineConnection) => { | ||||
|         engineConnection?.pc?.addEventListener('datachannel', (event) => { | ||||
|           let unreliableDataChannel = event.channel | ||||
|       } | ||||
|     ) | ||||
|  | ||||
|           unreliableDataChannel.addEventListener('message', (event) => { | ||||
|             const result: UnreliableResponses = JSON.parse(event.data) | ||||
|             Object.values( | ||||
|               this.unreliableSubscriptions[result.type] || {} | ||||
|             ).forEach( | ||||
|               // TODO: There is only one response that uses the unreliable channel atm, | ||||
|               // highlight_set_entity, if there are more it's likely they will all have the same | ||||
|               // sequence logic, but I'm not sure if we use a single global sequence or a sequence | ||||
|               // per unreliable subscription. | ||||
|               (callback) => { | ||||
|                 if ( | ||||
|                   result?.data?.sequence && | ||||
|                   result?.data.sequence > this.inSequence && | ||||
|                   result.type === 'highlight_set_entity' | ||||
|                 ) { | ||||
|                   this.inSequence = result.data.sequence | ||||
|                   callback(result) | ||||
|                 } | ||||
|               } | ||||
|             ) | ||||
|     this.engineConnection.addEventListener( | ||||
|       EngineConnectionEvents.Close, | ||||
|       (event) => { | ||||
|         setIsStreamReady(false) | ||||
|       } | ||||
|     ) | ||||
|  | ||||
|     this.engineConnection.addEventListener( | ||||
|       EngineConnectionEvents.ConnectionStarted, | ||||
|       (event: Event) => { | ||||
|         let customEvent = <CustomEvent<EngineConnection>>event | ||||
|         let conn = customEvent.detail | ||||
|  | ||||
|         this.engineConnection?.pc?.addEventListener('datachannel', (event) => { | ||||
|           let lossyDataChannel = event.channel | ||||
|  | ||||
|           lossyDataChannel.addEventListener('message', (event) => { | ||||
|             const result: OkResponse = JSON.parse(event.data) | ||||
|             if ( | ||||
|               result.type === 'highlight_set_entity' && | ||||
|               result.sequence && | ||||
|               result.sequence > this.inSequence | ||||
|             ) { | ||||
|               this.onHoverCallback(result.entity_id) | ||||
|               this.inSequence = result.sequence | ||||
|             } | ||||
|           }) | ||||
|         }) | ||||
|  | ||||
|         // When the EngineConnection starts a connection, we want to register | ||||
|         // callbacks into the WebSocket/PeerConnection. | ||||
|         engineConnection.websocket?.addEventListener('message', (event) => { | ||||
|         conn.websocket?.addEventListener('message', (event) => { | ||||
|           if (event.data instanceof ArrayBuffer) { | ||||
|             // If the data is an ArrayBuffer, it's  the result of an export command, | ||||
|             // because in all other cases we send JSON strings. But in the case of | ||||
| @ -645,58 +404,65 @@ export class EngineCommandManager { | ||||
|             // Pass this to our export function. | ||||
|             exportSave(event.data) | ||||
|           } else { | ||||
|             const message: Models['WebSocketResponse_type'] = JSON.parse( | ||||
|               event.data | ||||
|             ) | ||||
|             if ( | ||||
|               message.success && | ||||
|               message.resp.type === 'modeling' && | ||||
|               message.request_id | ||||
|             ) { | ||||
|               this.handleModelingCommand(message.resp, message.request_id) | ||||
|             if (event.data.toLocaleLowerCase().startsWith('error')) { | ||||
|               // Errors are not JSON encoded; if we have an error we can bail | ||||
|               // here; debugging the error to the console happens in the core | ||||
|               // engine code. | ||||
|               return | ||||
|             } | ||||
|             const message: WebSocketResponse = JSON.parse(event.data) | ||||
|             if (message.type === 'modeling') { | ||||
|               this.handleModelingCommand(message) | ||||
|             } | ||||
|           } | ||||
|         }) | ||||
|       }, | ||||
|       onNewTrack: ({ mediaStream }) => { | ||||
|       } | ||||
|     ) | ||||
|     this.engineConnection.addEventListener( | ||||
|       EngineConnectionEvents.NewTrack, | ||||
|       (event: Event) => { | ||||
|         let customEvent = <CustomEvent<NewTrackArgs>>event | ||||
|  | ||||
|         let mediaStream = customEvent.detail.mediaStream | ||||
|         console.log('received track', mediaStream) | ||||
|  | ||||
|         mediaStream.getVideoTracks()[0].addEventListener('mute', () => { | ||||
|           console.log('peer is not sending video to us') | ||||
|           // this.engineConnection?.close() | ||||
|           // this.engineConnection?.connect() | ||||
|         }) | ||||
|  | ||||
|         setMediaStream(mediaStream) | ||||
|       }, | ||||
|     }) | ||||
|  | ||||
|       } | ||||
|     ) | ||||
|     this.engineConnection?.connect() | ||||
|   } | ||||
|   handleModelingCommand(message: WebSocketResponse, id: string) { | ||||
|   handleModelingCommand(message: WebSocketResponse) { | ||||
|     if (message.type !== 'modeling') { | ||||
|       return | ||||
|     } | ||||
|     const modelingResponse = message.data.modeling_response | ||||
|     Object.values(this.subscriptions[modelingResponse.type] || {}).forEach( | ||||
|       (callback) => callback(modelingResponse) | ||||
|     ) | ||||
|  | ||||
|     const id = message.cmd_id | ||||
|     const command = this.artifactMap[id] | ||||
|     if ('ok' in message.result) { | ||||
|       const result: OkResponse = message.result.ok | ||||
|       if (result.type === 'select_with_point') { | ||||
|         if (result.entity_id) { | ||||
|           this.onClickCallback({ | ||||
|             id: result.entity_id, | ||||
|             type: 'default', | ||||
|           }) | ||||
|         } else { | ||||
|           this.onClickCallback() | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (command && command.type === 'pending') { | ||||
|       const resolve = command.resolve | ||||
|       this.artifactMap[id] = { | ||||
|         type: 'result', | ||||
|         data: modelingResponse, | ||||
|         data: message.result, | ||||
|       } | ||||
|       resolve({ | ||||
|         id, | ||||
|         data: modelingResponse, | ||||
|       }) | ||||
|     } else { | ||||
|       this.artifactMap[id] = { | ||||
|         type: 'result', | ||||
|         data: modelingResponse, | ||||
|         data: message.result, | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @ -707,49 +473,21 @@ export class EngineCommandManager { | ||||
|     this.artifactMap = {} | ||||
|     this.sourceRangeMap = {} | ||||
|   } | ||||
|   subscribeTo<T extends ModelTypes>({ | ||||
|     event, | ||||
|     callback, | ||||
|   }: Subscription<T>): () => void { | ||||
|     const localUnsubscribeId = uuidv4() | ||||
|     const otherEventCallbacks = this.subscriptions[event] | ||||
|     if (otherEventCallbacks) { | ||||
|       otherEventCallbacks[localUnsubscribeId] = callback | ||||
|     } else { | ||||
|       this.subscriptions[event] = { | ||||
|         [localUnsubscribeId]: callback, | ||||
|       } | ||||
|     } | ||||
|     return () => this.unSubscribeTo(event, localUnsubscribeId) | ||||
|   } | ||||
|   private unSubscribeTo(event: ModelTypes, id: string) { | ||||
|     delete this.subscriptions[event][id] | ||||
|   } | ||||
|   subscribeToUnreliable<T extends UnreliableResponses['type']>({ | ||||
|     event, | ||||
|     callback, | ||||
|   }: UnreliableSubscription<T>): () => void { | ||||
|     const localUnsubscribeId = uuidv4() | ||||
|     const otherEventCallbacks = this.unreliableSubscriptions[event] | ||||
|     if (otherEventCallbacks) { | ||||
|       otherEventCallbacks[localUnsubscribeId] = callback | ||||
|     } else { | ||||
|       this.unreliableSubscriptions[event] = { | ||||
|         [localUnsubscribeId]: callback, | ||||
|       } | ||||
|     } | ||||
|     return () => this.unSubscribeToUnreliable(event, localUnsubscribeId) | ||||
|   } | ||||
|   private unSubscribeToUnreliable( | ||||
|     event: UnreliableResponses['type'], | ||||
|     id: string | ||||
|   ) { | ||||
|     delete this.unreliableSubscriptions[event][id] | ||||
|   } | ||||
|   endSession() { | ||||
|     // this.websocket?.close() | ||||
|     // socket.off('command') | ||||
|   } | ||||
|   onHover(callback: (id?: string) => void) { | ||||
|     // It's when the user hovers over a part in the 3d scene, and so the engine should tell the | ||||
|     // frontend about that (with it's id) so that the FE can highlight code associated with that id | ||||
|     this.onHoverCallback = callback | ||||
|   } | ||||
|   onClick(callback: (selection?: SelectionsArgs) => void) { | ||||
|     // It's when the user clicks on a part in the 3d scene, and so the engine should tell the | ||||
|     // frontend about that (with it's id) so that the FE can put the user's cursor on the right | ||||
|     // line of code | ||||
|     this.onClickCallback = callback | ||||
|   } | ||||
|   cusorsSelected(selections: { | ||||
|     otherSelections: Selections['otherSelections'] | ||||
|     idBasedSelections: { type: string; id: string }[] | ||||
| @ -774,58 +512,51 @@ export class EngineCommandManager { | ||||
|       cmd_id: uuidv4(), | ||||
|     }) | ||||
|   } | ||||
|   sendSceneCommand(command: EngineCommand): Promise<any> { | ||||
|   sendSceneCommand(command: EngineCommand) { | ||||
|     if (!this.engineConnection?.isReady()) { | ||||
|       console.log('socket not ready') | ||||
|       return Promise.resolve() | ||||
|       return | ||||
|     } | ||||
|     if (command.type !== 'modeling_cmd_req') return Promise.resolve() | ||||
|     if (command.type !== 'modeling_cmd_req') return | ||||
|     const cmd = command.cmd | ||||
|     if ( | ||||
|       cmd.type === 'camera_drag_move' && | ||||
|       this.engineConnection?.unreliableDataChannel | ||||
|       this.engineConnection?.lossyDataChannel | ||||
|     ) { | ||||
|       cmd.sequence = this.outSequence | ||||
|       this.outSequence++ | ||||
|       this.engineConnection?.unreliableDataChannel?.send( | ||||
|         JSON.stringify(command) | ||||
|       ) | ||||
|       return Promise.resolve() | ||||
|       this.engineConnection?.lossyDataChannel?.send(JSON.stringify(command)) | ||||
|       return | ||||
|     } else if ( | ||||
|       cmd.type === 'highlight_set_entity' && | ||||
|       this.engineConnection?.unreliableDataChannel | ||||
|       this.engineConnection?.lossyDataChannel | ||||
|     ) { | ||||
|       cmd.sequence = this.outSequence | ||||
|       this.outSequence++ | ||||
|       this.engineConnection?.unreliableDataChannel?.send( | ||||
|         JSON.stringify(command) | ||||
|       ) | ||||
|       return Promise.resolve() | ||||
|       this.engineConnection?.lossyDataChannel?.send(JSON.stringify(command)) | ||||
|       return | ||||
|     } | ||||
|     console.log('sending command', command) | ||||
|     // since it's not mouse drag or highlighting send over TCP and keep track of the command | ||||
|     this.engineConnection?.send(command) | ||||
|     return this.handlePendingCommand(command.cmd_id) | ||||
|   } | ||||
|   sendModelingCommand({ | ||||
|   sendModellingCommand({ | ||||
|     id, | ||||
|     params, | ||||
|     range, | ||||
|     command, | ||||
|   }: { | ||||
|     id: string | ||||
|     params: any | ||||
|     range: SourceRange | ||||
|     command: EngineCommand | string | ||||
|     command: EngineCommand | ||||
|   }): Promise<any> { | ||||
|     this.sourceRangeMap[id] = range | ||||
|  | ||||
|     if (!this.engineConnection?.isReady()) { | ||||
|       console.log('socket not ready') | ||||
|       return Promise.resolve() | ||||
|       return new Promise(() => {}) | ||||
|     } | ||||
|     this.engineConnection?.send(command) | ||||
|     return this.handlePendingCommand(id) | ||||
|   } | ||||
|   handlePendingCommand(id: string) { | ||||
|     let resolve: (val: any) => void = () => {} | ||||
|     const promise = new Promise((_resolve, reject) => { | ||||
|       resolve = _resolve | ||||
| @ -837,24 +568,6 @@ export class EngineCommandManager { | ||||
|     } | ||||
|     return promise | ||||
|   } | ||||
|   sendModelingCommandFromWasm( | ||||
|     id: string, | ||||
|     rangeStr: string, | ||||
|     commandStr: string | ||||
|   ): Promise<any> { | ||||
|     if (id === undefined) { | ||||
|       throw new Error('id is undefined') | ||||
|     } | ||||
|     if (rangeStr === undefined) { | ||||
|       throw new Error('rangeStr is undefined') | ||||
|     } | ||||
|     if (commandStr === undefined) { | ||||
|       throw new Error('commandStr is undefined') | ||||
|     } | ||||
|     const range: SourceRange = JSON.parse(rangeStr) | ||||
|  | ||||
|     return this.sendModelingCommand({ id, range, command: commandStr }) | ||||
|   } | ||||
|   commandResult(id: string): Promise<any> { | ||||
|     const command = this.artifactMap[id] | ||||
|     if (!command) { | ||||
|  | ||||
							
								
								
									
										88
									
								
								src/lang/std/extrude.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/lang/std/extrude.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,88 @@ | ||||
| import { InternalFn } from './stdTypes' | ||||
| import { | ||||
|   ExtrudeGroup, | ||||
|   ExtrudeSurface, | ||||
|   SketchGroup, | ||||
|   Position, | ||||
|   Rotation, | ||||
| } from '../executor' | ||||
| import { clockwiseSign } from './std' | ||||
| import { generateUuidFromHashSeed } from '../../lib/uuid' | ||||
|  | ||||
| export const extrude: InternalFn = ( | ||||
|   { sourceRange, engineCommandManager, code }, | ||||
|   length: number, | ||||
|   sketchVal: SketchGroup | ||||
| ): ExtrudeGroup => { | ||||
|   const sketch = sketchVal | ||||
|   const { position, rotation } = sketchVal | ||||
|  | ||||
|   const id = generateUuidFromHashSeed( | ||||
|     JSON.stringify({ | ||||
|       code, | ||||
|       sourceRange, | ||||
|       data: { | ||||
|         length, | ||||
|         sketchVal, | ||||
|       }, | ||||
|     }) | ||||
|   ) | ||||
|  | ||||
|   const extrudeSurfaces: ExtrudeSurface[] = [] | ||||
|   const extrusionDirection = clockwiseSign(sketch.value.map((line) => line.to)) | ||||
|   engineCommandManager.sendModellingCommand({ | ||||
|     id, | ||||
|     params: [ | ||||
|       { | ||||
|         length, | ||||
|         extrusionDirection: extrusionDirection, | ||||
|       }, | ||||
|     ], | ||||
|     range: sourceRange, | ||||
|     command: { | ||||
|       type: 'modeling_cmd_req', | ||||
|       cmd: { | ||||
|         type: 'extrude', | ||||
|         target: sketch.id, | ||||
|         distance: length, | ||||
|         cap: true, | ||||
|       }, | ||||
|       cmd_id: id, | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   return { | ||||
|     type: 'extrudeGroup', | ||||
|     id, | ||||
|     value: extrudeSurfaces, // TODO, this is just an empty array now, should be deleted. | ||||
|     height: length, | ||||
|     position, | ||||
|     rotation, | ||||
|     __meta: [ | ||||
|       { | ||||
|         sourceRange, | ||||
|         pathToNode: [], // TODO | ||||
|       }, | ||||
|       { | ||||
|         sourceRange: sketchVal.__meta[0].sourceRange, | ||||
|         pathToNode: sketchVal.__meta[0].pathToNode, | ||||
|       }, | ||||
|     ], | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const getExtrudeWallTransform: InternalFn = ( | ||||
|   _, | ||||
|   pathName: string, | ||||
|   extrudeGroup: ExtrudeGroup | ||||
| ): { | ||||
|   position: Position | ||||
|   quaternion: Rotation | ||||
| } => { | ||||
|   const path = extrudeGroup?.value.find((path) => path.name === pathName) | ||||
|   if (!path) throw new Error(`Could not find path with name ${pathName}`) | ||||
|   return { | ||||
|     position: path.position, | ||||
|     quaternion: path.rotation, | ||||
|   } | ||||
| } | ||||
| @ -97,10 +97,11 @@ describe('testing changeSketchArguments', () => { | ||||
|   const lineAfterChange = 'lineTo([2, 3], %)' | ||||
|   test('changeSketchArguments', async () => { | ||||
|     // Enable rotations #152 | ||||
|     const genCode = (line: string) => `const mySketch001 = startSketchAt([0, 0]) | ||||
|   |> ${line} | ||||
|   |> lineTo([0.46, -5.82], %) | ||||
| // |> rx(45, %) | ||||
|     const genCode = (line: string) => ` | ||||
| const mySketch001 = startSketchAt([0, 0]) | ||||
|     |> ${line} | ||||
|     |> lineTo([0.46, -5.82], %) | ||||
|     // |> rx(45, %) | ||||
| show(mySketch001)` | ||||
|     const code = genCode(lineToChange) | ||||
|     const expectedCode = genCode(lineAfterChange) | ||||
| @ -159,7 +160,8 @@ show(mySketch001)` | ||||
|       ], | ||||
|     }) | ||||
|     // Enable rotations #152 | ||||
|     const expectedCode = `const mySketch001 = startSketchAt([0, 0]) | ||||
|     const expectedCode = ` | ||||
| const mySketch001 = startSketchAt([0, 0]) | ||||
|   // |> rx(45, %) | ||||
|   |> lineTo([-1.59, -1.54], %) | ||||
|   |> lineTo([0.46, -5.82], %) | ||||
| @ -173,11 +175,12 @@ describe('testing addTagForSketchOnFace', () => { | ||||
|   it('needs to be in it', async () => { | ||||
|     const originalLine = 'lineTo([-1.59, -1.54], %)' | ||||
|     // Enable rotations #152 | ||||
|     const genCode = (line: string) => `const mySketch001 = startSketchAt([0, 0]) | ||||
|   // |> rx(45, %) | ||||
|   |> ${line} | ||||
|   |> lineTo([0.46, -5.82], %) | ||||
| show(mySketch001)` | ||||
|     const genCode = (line: string) => ` | ||||
|   const mySketch001 = startSketchAt([0, 0]) | ||||
|     // |> rx(45, %) | ||||
|     |> ${line} | ||||
|     |> lineTo([0.46, -5.82], %) | ||||
|   show(mySketch001)` | ||||
|     const code = genCode(originalLine) | ||||
|     const ast = parser_wasm(code) | ||||
|     const programMemory = await enginelessExecutor(ast) | ||||
|  | ||||
| @ -23,7 +23,12 @@ import { GuiModes, toolTips, TooTip } from '../../useStore' | ||||
| import { splitPathAtPipeExpression } from '../modifyAst' | ||||
| import { generateUuidFromHashSeed } from '../../lib/uuid' | ||||
|  | ||||
| import { SketchLineHelper, ModifyAstBase, TransformCallback } from './stdTypes' | ||||
| import { | ||||
|   SketchLineHelper, | ||||
|   ModifyAstBase, | ||||
|   InternalFn, | ||||
|   TransformCallback, | ||||
| } from './stdTypes' | ||||
|  | ||||
| import { | ||||
|   createLiteral, | ||||
| @ -37,7 +42,10 @@ import { | ||||
| } from '../modifyAst' | ||||
| import { roundOff, getLength, getAngle } from '../../lib/utils' | ||||
| import { getSketchSegmentFromSourceRange } from './sketchConstraints' | ||||
| import { perpendicularDistance } from 'sketch-helpers' | ||||
| import { | ||||
|   intersectionWithParallelLine, | ||||
|   perpendicularDistance, | ||||
| } from 'sketch-helpers' | ||||
|  | ||||
| export type Coords2d = [number, number] | ||||
|  | ||||
| @ -107,6 +115,45 @@ function makeId(seed: string | any) { | ||||
| } | ||||
|  | ||||
| export const lineTo: SketchLineHelper = { | ||||
|   fn: ( | ||||
|     { sourceRange, code }, | ||||
|     data: | ||||
|       | [number, number] | ||||
|       | { | ||||
|           to: [number, number] | ||||
|           tag?: string | ||||
|         }, | ||||
|     previousSketch: SketchGroup | ||||
|   ): SketchGroup => { | ||||
|     if (!previousSketch) | ||||
|       throw new Error('lineTo must be called after startSketchAt') | ||||
|     const sketchGroup = { ...previousSketch } | ||||
|     const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1) | ||||
|     const to = 'to' in data ? data.to : data | ||||
|  | ||||
|     const id = makeId({ | ||||
|       code, | ||||
|       sourceRange, | ||||
|       data, | ||||
|     }) | ||||
|     const currentPath: Path = { | ||||
|       type: 'toPoint', | ||||
|       to, | ||||
|       from, | ||||
|       __geoMeta: { | ||||
|         sourceRange, | ||||
|         id, | ||||
|         pathToNode: [], // TODO | ||||
|       }, | ||||
|     } | ||||
|     if ('tag' in data) { | ||||
|       currentPath.name = data.tag | ||||
|     } | ||||
|     return { | ||||
|       ...sketchGroup, | ||||
|       value: [...sketchGroup.value, currentPath], | ||||
|     } | ||||
|   }, | ||||
|   add: ({ | ||||
|     node, | ||||
|     pathToNode, | ||||
| @ -174,6 +221,77 @@ export const lineTo: SketchLineHelper = { | ||||
| } | ||||
|  | ||||
| export const line: SketchLineHelper = { | ||||
|   fn: ( | ||||
|     { sourceRange, engineCommandManager, code }, | ||||
|     data: | ||||
|       | [number, number] | ||||
|       | 'default' | ||||
|       | { | ||||
|           to: [number, number] | 'default' | ||||
|           // name?: string | ||||
|           tag?: string | ||||
|         }, | ||||
|     previousSketch: SketchGroup | ||||
|   ): SketchGroup => { | ||||
|     if (!previousSketch) throw new Error('lineTo must be called after lineTo') | ||||
|     const sketchGroup = { ...previousSketch } | ||||
|     const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1) | ||||
|     let args: [number, number] = [0.2, 1] | ||||
|     if (data !== 'default' && 'to' in data && data.to !== 'default') { | ||||
|       args = data.to | ||||
|     } else if (data !== 'default' && !('to' in data)) { | ||||
|       args = data | ||||
|     } | ||||
|  | ||||
|     const to: [number, number] = [from[0] + args[0], from[1] + args[1]] | ||||
|     const lineData: LineData = { | ||||
|       from: [...from, 0], | ||||
|       to: [...to, 0], | ||||
|     } | ||||
|     const id = makeId({ | ||||
|       code, | ||||
|       sourceRange, | ||||
|       data, | ||||
|     }) | ||||
|     engineCommandManager.sendModellingCommand({ | ||||
|       id, | ||||
|       params: [lineData, previousSketch], | ||||
|       range: sourceRange, | ||||
|       command: { | ||||
|         type: 'modeling_cmd_req', | ||||
|         cmd: { | ||||
|           type: 'extend_path', | ||||
|           path: sketchGroup.id, | ||||
|           segment: { | ||||
|             type: 'line', | ||||
|             end: { | ||||
|               x: lineData.to[0], | ||||
|               y: lineData.to[1], | ||||
|               z: 0, | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|         cmd_id: id, | ||||
|       }, | ||||
|     }) | ||||
|     const currentPath: Path = { | ||||
|       type: 'toPoint', | ||||
|       to, | ||||
|       from, | ||||
|       __geoMeta: { | ||||
|         id, | ||||
|         sourceRange, | ||||
|         pathToNode: [], // TODO | ||||
|       }, | ||||
|     } | ||||
|     if (data !== 'default' && 'tag' in data) { | ||||
|       currentPath.name = data.tag | ||||
|     } | ||||
|     return { | ||||
|       ...sketchGroup, | ||||
|       value: [...sketchGroup.value, currentPath], | ||||
|     } | ||||
|   }, | ||||
|   add: ({ | ||||
|     node, | ||||
|     previousProgramMemory, | ||||
| @ -267,6 +385,25 @@ export const line: SketchLineHelper = { | ||||
| } | ||||
|  | ||||
| export const xLineTo: SketchLineHelper = { | ||||
|   fn: ( | ||||
|     meta, | ||||
|     data: | ||||
|       | number | ||||
|       | { | ||||
|           to: number | ||||
|           // name?: string | ||||
|           tag?: string | ||||
|         }, | ||||
|     previousSketch: SketchGroup | ||||
|   ) => { | ||||
|     if (!previousSketch) throw new Error('bad bad bad') | ||||
|     const from = getCoordsFromPaths( | ||||
|       previousSketch, | ||||
|       previousSketch.value.length - 1 | ||||
|     ) | ||||
|     const [xVal, tag] = typeof data !== 'number' ? [data.to, data.tag] : [data] | ||||
|     return lineTo.fn(meta, { to: [xVal, from[1]], tag }, previousSketch) | ||||
|   }, | ||||
|   add: ({ node, pathToNode, to, replaceExisting, createCallback }) => { | ||||
|     const _node = { ...node } | ||||
|     const getNode = getNodeFromPathCurry(_node, pathToNode) | ||||
| @ -315,6 +452,25 @@ export const xLineTo: SketchLineHelper = { | ||||
| } | ||||
|  | ||||
| export const yLineTo: SketchLineHelper = { | ||||
|   fn: ( | ||||
|     meta, | ||||
|     data: | ||||
|       | number | ||||
|       | { | ||||
|           to: number | ||||
|           // name?: string | ||||
|           tag?: string | ||||
|         }, | ||||
|     previousSketch: SketchGroup | ||||
|   ) => { | ||||
|     if (!previousSketch) throw new Error('bad bad bad') | ||||
|     const from = getCoordsFromPaths( | ||||
|       previousSketch, | ||||
|       previousSketch.value.length - 1 | ||||
|     ) | ||||
|     const [yVal, tag] = typeof data !== 'number' ? [data.to, data.tag] : [data] | ||||
|     return lineTo.fn(meta, { to: [from[0], yVal], tag }, previousSketch) | ||||
|   }, | ||||
|   add: ({ node, pathToNode, to, replaceExisting, createCallback }) => { | ||||
|     const _node = { ...node } | ||||
|     const getNode = getNodeFromPathCurry(_node, pathToNode) | ||||
| @ -363,6 +519,21 @@ export const yLineTo: SketchLineHelper = { | ||||
| } | ||||
|  | ||||
| export const xLine: SketchLineHelper = { | ||||
|   fn: ( | ||||
|     meta, | ||||
|     data: | ||||
|       | number | ||||
|       | { | ||||
|           length: number | ||||
|           tag?: string | ||||
|         }, | ||||
|     previousSketch: SketchGroup | ||||
|   ) => { | ||||
|     if (!previousSketch) throw new Error('bad bad bad') | ||||
|     const [xVal, tag] = | ||||
|       typeof data !== 'number' ? [data.length, data.tag] : [data] | ||||
|     return line.fn(meta, { to: [xVal, 0], tag }, previousSketch) | ||||
|   }, | ||||
|   add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => { | ||||
|     const _node = { ...node } | ||||
|     const getNode = getNodeFromPathCurry(_node, pathToNode) | ||||
| @ -413,6 +584,22 @@ export const xLine: SketchLineHelper = { | ||||
| } | ||||
|  | ||||
| export const yLine: SketchLineHelper = { | ||||
|   fn: ( | ||||
|     meta, | ||||
|     data: | ||||
|       | number | ||||
|       | { | ||||
|           length: number | ||||
|           // name?: string | ||||
|           tag?: string | ||||
|         }, | ||||
|     previousSketch: SketchGroup | ||||
|   ) => { | ||||
|     if (!previousSketch) throw new Error('bad bad bad') | ||||
|     const [yVal, tag] = | ||||
|       typeof data !== 'number' ? [data.length, data.tag] : [data] | ||||
|     return line.fn(meta, { to: [0, yVal], tag }, previousSketch) | ||||
|   }, | ||||
|   add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => { | ||||
|     const _node = { ...node } | ||||
|     const getNode = getNodeFromPathCurry(_node, pathToNode) | ||||
| @ -457,6 +644,48 @@ export const yLine: SketchLineHelper = { | ||||
| } | ||||
|  | ||||
| export const angledLine: SketchLineHelper = { | ||||
|   fn: ( | ||||
|     { sourceRange, engineCommandManager, code }, | ||||
|     data: | ||||
|       | [number, number] | ||||
|       | { | ||||
|           angle: number | ||||
|           length: number | ||||
|           tag?: string | ||||
|         }, | ||||
|     previousSketch: SketchGroup | ||||
|   ) => { | ||||
|     if (!previousSketch) throw new Error('lineTo must be called after lineTo') | ||||
|     const sketchGroup = { ...previousSketch } | ||||
|     const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1) | ||||
|     const [angle, length] = 'angle' in data ? [data.angle, data.length] : data | ||||
|     const to: [number, number] = [ | ||||
|       from[0] + length * Math.cos((angle * Math.PI) / 180), | ||||
|       from[1] + length * Math.sin((angle * Math.PI) / 180), | ||||
|     ] | ||||
|     const id = makeId({ | ||||
|       code, | ||||
|       sourceRange, | ||||
|       data, | ||||
|     }) | ||||
|     const currentPath: Path = { | ||||
|       type: 'toPoint', | ||||
|       to, | ||||
|       from, | ||||
|       __geoMeta: { | ||||
|         id, | ||||
|         sourceRange, | ||||
|         pathToNode: [], // TODO | ||||
|       }, | ||||
|     } | ||||
|     if ('tag' in data) { | ||||
|       currentPath.name = data.tag | ||||
|     } | ||||
|     return { | ||||
|       ...sketchGroup, | ||||
|       value: [...sketchGroup.value, currentPath], | ||||
|     } | ||||
|   }, | ||||
|   add: ({ | ||||
|     node, | ||||
|     pathToNode, | ||||
| @ -524,6 +753,26 @@ export const angledLine: SketchLineHelper = { | ||||
| } | ||||
|  | ||||
| export const angledLineOfXLength: SketchLineHelper = { | ||||
|   fn: ( | ||||
|     { sourceRange, programMemory, engineCommandManager, code }, | ||||
|     data: | ||||
|       | [number, number] | ||||
|       | { | ||||
|           angle: number | ||||
|           length: number | ||||
|           tag?: string | ||||
|         }, | ||||
|     previousSketch: SketchGroup | ||||
|   ) => { | ||||
|     if (!previousSketch) throw new Error('lineTo must be called after lineTo') | ||||
|     const [angle, length, tag] = | ||||
|       'angle' in data ? [data.angle, data.length, data.tag] : data | ||||
|     return line.fn( | ||||
|       { sourceRange, programMemory, engineCommandManager, code }, | ||||
|       { to: getYComponent(angle, length), tag }, | ||||
|       previousSketch | ||||
|     ) | ||||
|   }, | ||||
|   add: ({ | ||||
|     node, | ||||
|     previousProgramMemory, | ||||
| @ -597,6 +846,26 @@ export const angledLineOfXLength: SketchLineHelper = { | ||||
| } | ||||
|  | ||||
| export const angledLineOfYLength: SketchLineHelper = { | ||||
|   fn: ( | ||||
|     { sourceRange, programMemory, engineCommandManager, code }, | ||||
|     data: | ||||
|       | [number, number] | ||||
|       | { | ||||
|           angle: number | ||||
|           length: number | ||||
|           tag?: string | ||||
|         }, | ||||
|     previousSketch: SketchGroup | ||||
|   ) => { | ||||
|     if (!previousSketch) throw new Error('lineTo must be called after lineTo') | ||||
|     const [angle, length, tag] = | ||||
|       'angle' in data ? [data.angle, data.length, data.tag] : data | ||||
|     return line.fn( | ||||
|       { sourceRange, programMemory, engineCommandManager, code }, | ||||
|       { to: getXComponent(angle, length), tag }, | ||||
|       previousSketch | ||||
|     ) | ||||
|   }, | ||||
|   add: ({ | ||||
|     node, | ||||
|     previousProgramMemory, | ||||
| @ -671,6 +940,33 @@ export const angledLineOfYLength: SketchLineHelper = { | ||||
| } | ||||
|  | ||||
| export const angledLineToX: SketchLineHelper = { | ||||
|   fn: ( | ||||
|     { sourceRange, programMemory, engineCommandManager, code }, | ||||
|     data: | ||||
|       | [number, number] | ||||
|       | { | ||||
|           angle: number | ||||
|           to: number | ||||
|           tag?: string | ||||
|         }, | ||||
|     previousSketch: SketchGroup | ||||
|   ) => { | ||||
|     if (!previousSketch) throw new Error('lineTo must be called after lineTo') | ||||
|     const from = getCoordsFromPaths( | ||||
|       previousSketch, | ||||
|       previousSketch.value.length - 1 | ||||
|     ) | ||||
|     const [angle, xTo, tag] = | ||||
|       'angle' in data ? [data.angle, data.to, data.tag] : data | ||||
|     const xComponent = xTo - from[0] | ||||
|     const yComponent = xComponent * Math.tan((angle * Math.PI) / 180) | ||||
|     const yTo = from[1] + yComponent | ||||
|     return lineTo.fn( | ||||
|       { sourceRange, programMemory, engineCommandManager, code }, | ||||
|       { to: [xTo, yTo], tag }, | ||||
|       previousSketch | ||||
|     ) | ||||
|   }, | ||||
|   add: ({ | ||||
|     node, | ||||
|     pathToNode, | ||||
| @ -740,6 +1036,33 @@ export const angledLineToX: SketchLineHelper = { | ||||
| } | ||||
|  | ||||
| export const angledLineToY: SketchLineHelper = { | ||||
|   fn: ( | ||||
|     { sourceRange, programMemory, engineCommandManager, code }, | ||||
|     data: | ||||
|       | [number, number] | ||||
|       | { | ||||
|           angle: number | ||||
|           to: number | ||||
|           tag?: string | ||||
|         }, | ||||
|     previousSketch: SketchGroup | ||||
|   ) => { | ||||
|     if (!previousSketch) throw new Error('lineTo must be called after lineTo') | ||||
|     const from = getCoordsFromPaths( | ||||
|       previousSketch, | ||||
|       previousSketch.value.length - 1 | ||||
|     ) | ||||
|     const [angle, yTo, tag] = | ||||
|       'angle' in data ? [data.angle, data.to, data.tag] : data | ||||
|     const yComponent = yTo - from[1] | ||||
|     const xComponent = yComponent / Math.tan((angle * Math.PI) / 180) | ||||
|     const xTo = from[0] + xComponent | ||||
|     return lineTo.fn( | ||||
|       { sourceRange, programMemory, engineCommandManager, code }, | ||||
|       { to: [xTo, yTo], tag }, | ||||
|       previousSketch | ||||
|     ) | ||||
|   }, | ||||
|   add: ({ | ||||
|     node, | ||||
|     pathToNode, | ||||
| @ -810,6 +1133,37 @@ export const angledLineToY: SketchLineHelper = { | ||||
| } | ||||
|  | ||||
| export const angledLineThatIntersects: SketchLineHelper = { | ||||
|   fn: ( | ||||
|     { sourceRange, programMemory, engineCommandManager, code }, | ||||
|     data: { | ||||
|       angle: number | ||||
|       intersectTag: string | ||||
|       offset?: number | ||||
|       tag?: string | ||||
|     }, | ||||
|     previousSketch: SketchGroup | ||||
|   ) => { | ||||
|     if (!previousSketch) throw new Error('lineTo must be called after lineTo') | ||||
|     const intersectPath = previousSketch.value.find( | ||||
|       ({ name }) => name === data.intersectTag | ||||
|     ) | ||||
|     if (!intersectPath) throw new Error('intersectTag must match a line') | ||||
|     const from = getCoordsFromPaths( | ||||
|       previousSketch, | ||||
|       previousSketch.value.length - 1 | ||||
|     ) | ||||
|     const to = intersectionWithParallelLine({ | ||||
|       line1: [intersectPath.from, intersectPath.to], | ||||
|       line1Offset: data.offset || 0, | ||||
|       line2Point: from, | ||||
|       line2Angle: data.angle, | ||||
|     }) | ||||
|     return lineTo.fn( | ||||
|       { sourceRange, programMemory, engineCommandManager, code }, | ||||
|       { to, tag: data.tag }, | ||||
|       previousSketch | ||||
|     ) | ||||
|   }, | ||||
|   add: ({ | ||||
|     node, | ||||
|     pathToNode, | ||||
| @ -1172,6 +1526,142 @@ function addTagWithTo( | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const close: InternalFn = ( | ||||
|   { sourceRange, engineCommandManager, code }, | ||||
|   sketchGroup: SketchGroup | ||||
| ): SketchGroup => { | ||||
|   const from = getCoordsFromPaths(sketchGroup, sketchGroup.value.length - 1) | ||||
|   const to = sketchGroup.start | ||||
|     ? sketchGroup.start.from | ||||
|     : getCoordsFromPaths(sketchGroup, 0) | ||||
|  | ||||
|   const lineData: LineData = { | ||||
|     from: [...from, 0], | ||||
|     to: [...to, 0], | ||||
|   } | ||||
|   const id = makeId({ | ||||
|     code, | ||||
|     sourceRange, | ||||
|     data: sketchGroup, | ||||
|   }) | ||||
|   engineCommandManager.sendModellingCommand({ | ||||
|     id, | ||||
|     params: [lineData], | ||||
|     range: sourceRange, | ||||
|     command: { | ||||
|       type: 'modeling_cmd_req', | ||||
|       cmd: { | ||||
|         type: 'close_path', | ||||
|         path_id: sketchGroup.id, | ||||
|       }, | ||||
|       cmd_id: id, | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   const currentPath: Path = { | ||||
|     type: 'toPoint', | ||||
|     to, | ||||
|     from, | ||||
|     __geoMeta: { | ||||
|       id, | ||||
|       sourceRange, | ||||
|       pathToNode: [], // TODO | ||||
|     }, | ||||
|   } | ||||
|   const newValue = [...sketchGroup.value] | ||||
|   newValue.push(currentPath) | ||||
|   return { | ||||
|     ...sketchGroup, | ||||
|     value: newValue, | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const startSketchAt: InternalFn = ( | ||||
|   { sourceRange, programMemory, engineCommandManager, code }, | ||||
|   data: | ||||
|     | [number, number] | ||||
|     | 'default' | ||||
|     | { | ||||
|         to: [number, number] | 'default' | ||||
|         // name?: string | ||||
|         tag?: string | ||||
|       } | ||||
| ): SketchGroup => { | ||||
|   let to: [number, number] = [0, 0] | ||||
|   if (data !== 'default' && 'to' in data && data.to !== 'default') { | ||||
|     to = data.to | ||||
|   } else if (data !== 'default' && !('to' in data)) { | ||||
|     to = data | ||||
|   } | ||||
|  | ||||
|   const lineData: { to: [number, number, number] } = { | ||||
|     to: [...to, 0], | ||||
|   } | ||||
|   const id = makeId({ | ||||
|     code, | ||||
|     sourceRange, | ||||
|     data, | ||||
|   }) | ||||
|   const pathId = makeId({ | ||||
|     code, | ||||
|     sourceRange, | ||||
|     data, | ||||
|     isPath: true, | ||||
|   }) | ||||
|   engineCommandManager.sendModellingCommand({ | ||||
|     id: pathId, | ||||
|     params: [lineData], | ||||
|     range: sourceRange, | ||||
|     command: { | ||||
|       type: 'modeling_cmd_req', | ||||
|       cmd: { | ||||
|         type: 'start_path', | ||||
|       }, | ||||
|       cmd_id: pathId, | ||||
|     }, | ||||
|   }) | ||||
|   engineCommandManager.sendSceneCommand({ | ||||
|     type: 'modeling_cmd_req', | ||||
|     cmd: { | ||||
|       type: 'move_path_pen', | ||||
|       path: pathId, | ||||
|       to: { | ||||
|         x: lineData.to[0], | ||||
|         y: lineData.to[1], | ||||
|         z: 0, | ||||
|       }, | ||||
|     }, | ||||
|     cmd_id: id, | ||||
|   }) | ||||
|   const currentPath: Path = { | ||||
|     type: 'base', | ||||
|     to, | ||||
|     from: to, | ||||
|     __geoMeta: { | ||||
|       id, | ||||
|       sourceRange, | ||||
|       pathToNode: [], // TODO | ||||
|     }, | ||||
|   } | ||||
|   if (data !== 'default' && 'tag' in data) { | ||||
|     currentPath.name = data.tag | ||||
|   } | ||||
|   return { | ||||
|     type: 'sketchGroup', | ||||
|     start: currentPath, | ||||
|     value: [], | ||||
|     position: [0, 0, 0], | ||||
|     rotation: [0, 0, 0, 1], | ||||
|     id: pathId, | ||||
|     __meta: [ | ||||
|       { | ||||
|         sourceRange, | ||||
|         pathToNode: [], // TODO | ||||
|       }, | ||||
|     ], | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function getYComponent( | ||||
|   angleDegree: number, | ||||
|   xComponent: number | ||||
|  | ||||
| @ -59,20 +59,20 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => { | ||||
|     `  |> lineTo({ to: [1, 1], tag: 'abc1' }, %)`, | ||||
|     `  |> line({ to: [-2.04, -0.7], tag: 'abc2' }, %)`, | ||||
|     `  |> angledLine({`, | ||||
|     `       angle: 157,`, | ||||
|     `       length: 1.69,`, | ||||
|     `       tag: 'abc3'`, | ||||
|     `     }, %)`, | ||||
|     `      angle: 157,`, | ||||
|     `      length: 1.69,`, | ||||
|     `      tag: 'abc3'`, | ||||
|     `    }, %)`, | ||||
|     `  |> angledLineOfXLength({`, | ||||
|     `       angle: 217,`, | ||||
|     `       length: 0.86,`, | ||||
|     `       tag: 'abc4'`, | ||||
|     `     }, %)`, | ||||
|     `      angle: 217,`, | ||||
|     `      length: 0.86,`, | ||||
|     `      tag: 'abc4'`, | ||||
|     `    }, %)`, | ||||
|     `  |> angledLineOfYLength({`, | ||||
|     `       angle: 104,`, | ||||
|     `       length: 1.58,`, | ||||
|     `       tag: 'abc5'`, | ||||
|     `     }, %)`, | ||||
|     `      angle: 104,`, | ||||
|     `      length: 1.58,`, | ||||
|     `      tag: 'abc5'`, | ||||
|     `    }, %)`, | ||||
|     `  |> angledLineToX({ angle: 55, to: -2.89, tag: 'abc6' }, %)`, | ||||
|     `  |> angledLineToY({ angle: 330, to: 2.53, tag: 'abc7' }, %)`, | ||||
|     `  |> xLine({ length: 1.47, tag: 'abc8' }, %)`, | ||||
| @ -144,10 +144,10 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => { | ||||
|       inputCode: bigExample, | ||||
|       callToSwap: [ | ||||
|         `angledLine({`, | ||||
|         `       angle: 157,`, | ||||
|         `       length: 1.69,`, | ||||
|         `       tag: 'abc3'`, | ||||
|         `     }, %)`, | ||||
|         `      angle: 157,`, | ||||
|         `      length: 1.69,`, | ||||
|         `      tag: 'abc3'`, | ||||
|         `    }, %)`, | ||||
|       ].join('\n'), | ||||
|       constraintType: 'horizontal', | ||||
|     }) | ||||
| @ -172,10 +172,10 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => { | ||||
|       inputCode: bigExample, | ||||
|       callToSwap: [ | ||||
|         `angledLineOfXLength({`, | ||||
|         `       angle: 217,`, | ||||
|         `       length: 0.86,`, | ||||
|         `       tag: 'abc4'`, | ||||
|         `     }, %)`, | ||||
|         `      angle: 217,`, | ||||
|         `      length: 0.86,`, | ||||
|         `      tag: 'abc4'`, | ||||
|         `    }, %)`, | ||||
|       ].join('\n'), | ||||
|       constraintType: 'horizontal', | ||||
|     }) | ||||
| @ -201,10 +201,10 @@ describe('testing swaping out sketch calls with xLine/xLineTo', () => { | ||||
|       inputCode: bigExample, | ||||
|       callToSwap: [ | ||||
|         `angledLineOfYLength({`, | ||||
|         `       angle: 104,`, | ||||
|         `       length: 1.58,`, | ||||
|         `       tag: 'abc5'`, | ||||
|         `     }, %)`, | ||||
|         `      angle: 104,`, | ||||
|         `      length: 1.58,`, | ||||
|         `      tag: 'abc5'`, | ||||
|         `    }, %)`, | ||||
|       ].join('\n'), | ||||
|       constraintType: 'vertical', | ||||
|     }) | ||||
| @ -391,7 +391,6 @@ show(part001)` | ||||
|       type: 'toPoint', | ||||
|       to: [5.62, 1.79], | ||||
|       from: [3.48, 0.44], | ||||
|       name: '', | ||||
|     }) | ||||
|   }) | ||||
|   it('verify it works when the segment is in the `start` property', async () => { | ||||
| @ -401,6 +400,6 @@ show(part001)` | ||||
|       programMemory.root['part001'] as SketchGroup, | ||||
|       [index, index] | ||||
|     ).segment | ||||
|     expect(segment).toEqual({ to: [0, 0.04], from: [0, 0.04], name: '' }) | ||||
|     expect(segment).toEqual({ type: 'base', to: [0, 0.04], from: [0, 0.04] }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { getAngle } from '../../lib/utils' | ||||
| import { TooTip, toolTips } from '../../useStore' | ||||
| import { | ||||
|   Program, | ||||
| @ -5,6 +6,7 @@ import { | ||||
|   CallExpression, | ||||
| } from '../abstractSyntaxTreeTypes' | ||||
| import { SketchGroup, SourceRange } from '../executor' | ||||
| import { InternalFn } from './stdTypes' | ||||
|  | ||||
| export function getSketchSegmentFromSourceRange( | ||||
|   sketchGroup: SketchGroup, | ||||
| @ -34,6 +36,79 @@ export function getSketchSegmentFromSourceRange( | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const segLen: InternalFn = ( | ||||
|   _, | ||||
|   segName: string, | ||||
|   sketchGroup: SketchGroup | ||||
| ): number => { | ||||
|   const line = sketchGroup?.value.find((seg) => seg?.name === segName) | ||||
|   // maybe this should throw, but the language doesn't have a way to handle errors yet | ||||
|   if (!line) return 0 | ||||
|  | ||||
|   return Math.sqrt( | ||||
|     (line.from[1] - line.to[1]) ** 2 + (line.from[0] - line.to[0]) ** 2 | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export const segAng: InternalFn = ( | ||||
|   _, | ||||
|   segName: string, | ||||
|   sketchGroup: SketchGroup | ||||
| ): number => { | ||||
|   const line = sketchGroup?.value.find((seg) => seg.name === segName) | ||||
|   // maybe this should throw, but the language doesn't have a way to handle errors yet | ||||
|   if (!line) return 0 | ||||
|   return getAngle(line.from, line.to) | ||||
| } | ||||
|  | ||||
| function segEndFactory(which: 'x' | 'y'): InternalFn { | ||||
|   return (_, segName: string, sketchGroup: SketchGroup): number => { | ||||
|     const line = | ||||
|       sketchGroup?.start?.name === segName | ||||
|         ? sketchGroup?.start | ||||
|         : sketchGroup?.value.find((seg) => seg.name === segName) | ||||
|     // maybe this should throw, but the language doesn't have a way to handle errors yet | ||||
|     if (!line) return 0 | ||||
|     return which === 'x' ? line.to[0] : line.to[1] | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const segEndX: InternalFn = segEndFactory('x') | ||||
| export const segEndY: InternalFn = segEndFactory('y') | ||||
|  | ||||
| function lastSegFactory(which: 'x' | 'y'): InternalFn { | ||||
|   return (_, sketchGroup: SketchGroup): number => { | ||||
|     const lastLine = sketchGroup?.value[sketchGroup.value.length - 1] | ||||
|     return which === 'x' ? lastLine.to[0] : lastLine.to[1] | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const lastSegX: InternalFn = lastSegFactory('x') | ||||
| export const lastSegY: InternalFn = lastSegFactory('y') | ||||
|  | ||||
| function angleToMatchLengthFactory(which: 'x' | 'y'): InternalFn { | ||||
|   return (_, segName: string, to: number, sketchGroup: SketchGroup): number => { | ||||
|     const isX = which === 'x' | ||||
|     const lineToMatch = sketchGroup?.value.find((seg) => seg.name === segName) | ||||
|     // maybe this should throw, but the language doesn't have a way to handle errors yet | ||||
|     if (!lineToMatch) return 0 | ||||
|     const lengthToMatch = Math.sqrt( | ||||
|       (lineToMatch.from[1] - lineToMatch.to[1]) ** 2 + | ||||
|         (lineToMatch.from[0] - lineToMatch.to[0]) ** 2 | ||||
|     ) | ||||
|  | ||||
|     const lastLine = sketchGroup?.value[sketchGroup.value.length - 1] | ||||
|     const diff = Math.abs(to - (isX ? lastLine.to[0] : lastLine.to[1])) | ||||
|  | ||||
|     const angleR = Math[isX ? 'acos' : 'asin'](diff / lengthToMatch) | ||||
|  | ||||
|     return diff > lengthToMatch ? 0 : (angleR * 180) / Math.PI | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const angleToMatchLengthX: InternalFn = angleToMatchLengthFactory('x') | ||||
| export const angleToMatchLengthY: InternalFn = angleToMatchLengthFactory('y') | ||||
|  | ||||
| export function isSketchVariablesLinked( | ||||
|   secondaryVarDec: VariableDeclarator, | ||||
|   primaryVarDec: VariableDeclarator, | ||||
|  | ||||
| @ -133,64 +133,64 @@ const myAng2 = 134 | ||||
| const part001 = startSketchAt([0, 0]) | ||||
|   |> line({ to: [1, 3.82], tag: 'seg01' }, %) // ln-should-get-tag | ||||
|   |> angledLineToX([ | ||||
|        -angleToMatchLengthX('seg01', myVar, %), | ||||
|        myVar | ||||
|      ], %) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper | ||||
|       -angleToMatchLengthX('seg01', myVar, %), | ||||
|       myVar | ||||
|     ], %) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper | ||||
|   |> angledLineToY([ | ||||
|        -angleToMatchLengthY('seg01', myVar, %), | ||||
|        myVar | ||||
|      ], %) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper | ||||
|       -angleToMatchLengthY('seg01', myVar, %), | ||||
|       myVar | ||||
|     ], %) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper | ||||
|   |> angledLine([45, segLen('seg01', %)], %) // ln-lineTo-free should become angledLine | ||||
|   |> angledLine([45, segLen('seg01', %)], %) // ln-angledLineToX-free should become angledLine | ||||
|   |> angledLine([myAng, segLen('seg01', %)], %) // ln-angledLineToX-angle should become angledLine | ||||
|   |> angledLineToX([ | ||||
|        angleToMatchLengthX('seg01', myVar2, %), | ||||
|        myVar2 | ||||
|      ], %) // ln-angledLineToX-xAbsolute should use angleToMatchLengthX to get angle | ||||
|       angleToMatchLengthX('seg01', myVar2, %), | ||||
|       myVar2 | ||||
|     ], %) // ln-angledLineToX-xAbsolute should use angleToMatchLengthX to get angle | ||||
|   |> angledLine([-45, segLen('seg01', %)], %) // ln-angledLineToY-free should become angledLine | ||||
|   |> angledLine([myAng2, segLen('seg01', %)], %) // ln-angledLineToY-angle should become angledLine | ||||
|   |> angledLineToY([ | ||||
|        angleToMatchLengthY('seg01', myVar3, %), | ||||
|        myVar3 | ||||
|      ], %) // ln-angledLineToY-yAbsolute should use angleToMatchLengthY to get angle | ||||
|       angleToMatchLengthY('seg01', myVar3, %), | ||||
|       myVar3 | ||||
|     ], %) // ln-angledLineToY-yAbsolute should use angleToMatchLengthY to get angle | ||||
|   |> line([ | ||||
|        min(segLen('seg01', %), myVar), | ||||
|        legLen(segLen('seg01', %), myVar) | ||||
|      ], %) // ln-should use legLen for y | ||||
|       min(segLen('seg01', %), myVar), | ||||
|       legLen(segLen('seg01', %), myVar) | ||||
|     ], %) // ln-should use legLen for y | ||||
|   |> line([ | ||||
|        min(segLen('seg01', %), myVar), | ||||
|        -legLen(segLen('seg01', %), myVar) | ||||
|      ], %) // ln-legLen but negative | ||||
|       min(segLen('seg01', %), myVar), | ||||
|       -legLen(segLen('seg01', %), myVar) | ||||
|     ], %) // ln-legLen but negative | ||||
|   |> angledLine([-112, segLen('seg01', %)], %) // ln-should become angledLine | ||||
|   |> angledLine([myVar, segLen('seg01', %)], %) // ln-use segLen for secound arg | ||||
|   |> angledLine([45, segLen('seg01', %)], %) // ln-segLen again | ||||
|   |> angledLine([54, segLen('seg01', %)], %) // ln-should be transformed to angledLine | ||||
|   |> angledLineOfXLength([ | ||||
|        legAngX(segLen('seg01', %), myVar), | ||||
|        min(segLen('seg01', %), myVar) | ||||
|      ], %) // ln-should use legAngX to calculate angle | ||||
|       legAngX(segLen('seg01', %), myVar), | ||||
|       min(segLen('seg01', %), myVar) | ||||
|     ], %) // ln-should use legAngX to calculate angle | ||||
|   |> angledLineOfXLength([ | ||||
|        180 + legAngX(segLen('seg01', %), myVar), | ||||
|        min(segLen('seg01', %), myVar) | ||||
|      ], %) // ln-same as above but should have + 180 to match original quadrant | ||||
|       180 + legAngX(segLen('seg01', %), myVar), | ||||
|       min(segLen('seg01', %), myVar) | ||||
|     ], %) // ln-same as above but should have + 180 to match original quadrant | ||||
|   |> line([ | ||||
|        legLen(segLen('seg01', %), myVar), | ||||
|        min(segLen('seg01', %), myVar) | ||||
|      ], %) // ln-legLen again but yRelative | ||||
|       legLen(segLen('seg01', %), myVar), | ||||
|       min(segLen('seg01', %), myVar) | ||||
|     ], %) // ln-legLen again but yRelative | ||||
|   |> line([ | ||||
|        -legLen(segLen('seg01', %), myVar), | ||||
|        min(segLen('seg01', %), myVar) | ||||
|      ], %) // ln-negative legLen yRelative | ||||
|       -legLen(segLen('seg01', %), myVar), | ||||
|       min(segLen('seg01', %), myVar) | ||||
|     ], %) // ln-negative legLen yRelative | ||||
|   |> angledLine([58, segLen('seg01', %)], %) // ln-angledLineOfYLength-free should become angledLine | ||||
|   |> angledLine([myAng, segLen('seg01', %)], %) // ln-angledLineOfYLength-angle should become angledLine | ||||
|   |> angledLineOfXLength([ | ||||
|        legAngY(segLen('seg01', %), myVar), | ||||
|        min(segLen('seg01', %), myVar) | ||||
|      ], %) // ln-angledLineOfYLength-yRelative use legAngY | ||||
|       legAngY(segLen('seg01', %), myVar), | ||||
|       min(segLen('seg01', %), myVar) | ||||
|     ], %) // ln-angledLineOfYLength-yRelative use legAngY | ||||
|   |> angledLineOfXLength([ | ||||
|        270 + legAngY(segLen('seg01', %), myVar), | ||||
|        min(segLen('seg01', %), myVar) | ||||
|      ], %) // ln-angledLineOfYLength-yRelative with angle > 90 use binExp | ||||
|       270 + legAngY(segLen('seg01', %), myVar), | ||||
|       min(segLen('seg01', %), myVar) | ||||
|     ], %) // ln-angledLineOfYLength-yRelative with angle > 90 use binExp | ||||
|   |> xLine(segLen('seg01', %), %) // ln-xLine-free should sub in segLen | ||||
|   |> yLine(segLen('seg01', %), %) // ln-yLine-free should sub in segLen | ||||
|   |> xLine(segLen('seg01', %), %) // ln-xLineTo-free should convert to xLine | ||||
| @ -406,9 +406,9 @@ show(part001)` | ||||
|         'setVertDistance' | ||||
|       ) | ||||
|       expect(expectedCode).toContain(`|> lineTo([ | ||||
|        lastSegX(%) + myVar, | ||||
|        segEndY('seg01', %) + 2.93 | ||||
|      ], %) // xRelative`) | ||||
|       lastSegX(%) + myVar, | ||||
|       segEndY('seg01', %) + 2.93 | ||||
|     ], %) // xRelative`) | ||||
|     }) | ||||
|     it('testing for yRelative to horizontal distance', async () => { | ||||
|       const expectedCode = await helperThing( | ||||
| @ -417,9 +417,9 @@ show(part001)` | ||||
|         'setHorzDistance' | ||||
|       ) | ||||
|       expect(expectedCode).toContain(`|> lineTo([ | ||||
|        segEndX('seg01', %) + 2.6, | ||||
|        lastSegY(%) + myVar | ||||
|      ], %) // yRelative`) | ||||
|       segEndX('seg01', %) + 2.6, | ||||
|       lastSegY(%) + myVar | ||||
|     ], %) // yRelative`) | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
							
								
								
									
										170
									
								
								src/lang/std/std.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								src/lang/std/std.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,170 @@ | ||||
| import { | ||||
|   lineTo, | ||||
|   xLineTo, | ||||
|   yLineTo, | ||||
|   line, | ||||
|   xLine, | ||||
|   yLine, | ||||
|   angledLine, | ||||
|   angledLineOfXLength, | ||||
|   angledLineToX, | ||||
|   angledLineOfYLength, | ||||
|   angledLineToY, | ||||
|   close, | ||||
|   startSketchAt, | ||||
|   angledLineThatIntersects, | ||||
| } from './sketch' | ||||
| import { | ||||
|   segLen, | ||||
|   segAng, | ||||
|   angleToMatchLengthX, | ||||
|   angleToMatchLengthY, | ||||
|   segEndX, | ||||
|   segEndY, | ||||
|   lastSegX, | ||||
|   lastSegY, | ||||
| } from './sketchConstraints' | ||||
| import { getExtrudeWallTransform, extrude } from './extrude' | ||||
|  | ||||
| import { InternalFn, InternalFnNames } from './stdTypes' | ||||
|  | ||||
| // const transform: InternalFn = <T extends SketchGroup | ExtrudeGroup>( | ||||
| //   { sourceRange }: InternalFirstArg, | ||||
| //   transformInfo: { | ||||
| //     position: Position | ||||
| //     quaternion: Rotation | ||||
| //   }, | ||||
| //   sketch: T | ||||
| // ): T => { | ||||
| //   const quaternionToApply = new Quaternion(...transformInfo?.quaternion) | ||||
| //   const newQuaternion = new Quaternion(...sketch.rotation).multiply( | ||||
| //     quaternionToApply.invert() | ||||
| //   ) | ||||
|  | ||||
| //   const oldPosition = new Vector3(...sketch?.position) | ||||
| //   const newPosition = oldPosition | ||||
| //     .applyQuaternion(quaternionToApply) | ||||
| //     .add(new Vector3(...transformInfo?.position)) | ||||
| //   return { | ||||
| //     ...sketch, | ||||
| //     position: newPosition.toArray(), | ||||
| //     rotation: newQuaternion.toArray(), | ||||
| //     __meta: [ | ||||
| //       ...sketch.__meta, | ||||
| //       { | ||||
| //         sourceRange, | ||||
| //         pathToNode: [], // TODO | ||||
| //       }, | ||||
| //     ], | ||||
| //   } | ||||
| // } | ||||
|  | ||||
| // const translate: InternalFn = <T extends SketchGroup | ExtrudeGroup>( | ||||
| //   { sourceRange }: InternalFirstArg, | ||||
| //   vec3: [number, number, number], | ||||
| //   sketch: T | ||||
| // ): T => { | ||||
| //   const oldPosition = new Vector3(...sketch.position) | ||||
| //   const newPosition = oldPosition.add(new Vector3(...vec3)) | ||||
| //   return { | ||||
| //     ...sketch, | ||||
| //     position: newPosition.toArray(), | ||||
| //     __meta: [ | ||||
| //       ...sketch.__meta, | ||||
| //       { | ||||
| //         sourceRange, | ||||
| //         pathToNode: [], // TODO | ||||
| //       }, | ||||
| //     ], | ||||
| //   } | ||||
| // } | ||||
|  | ||||
| const min: InternalFn = (_, a: number, b: number): number => Math.min(a, b) | ||||
|  | ||||
| const legLen: InternalFn = (_, hypotenuse: number, leg: number): number => | ||||
|   Math.sqrt( | ||||
|     hypotenuse ** 2 - Math.min(Math.abs(leg), Math.abs(hypotenuse)) ** 2 | ||||
|   ) | ||||
|  | ||||
| const legAngX: InternalFn = (_, hypotenuse: number, leg: number): number => | ||||
|   (Math.acos(Math.min(leg, hypotenuse) / hypotenuse) * 180) / Math.PI | ||||
|  | ||||
| const legAngY: InternalFn = (_, hypotenuse: number, leg: number): number => | ||||
|   (Math.asin(Math.min(leg, hypotenuse) / hypotenuse) * 180) / Math.PI | ||||
|  | ||||
| export const internalFns: { [key in InternalFnNames]: InternalFn } = { | ||||
|   // TODO - re-enable these | ||||
|   // rx: rotateOnAxis([1, 0, 0]), // Enable rotations #152 | ||||
|   // ry: rotateOnAxis([0, 1, 0]), | ||||
|   // rz: rotateOnAxis([0, 0, 1]), | ||||
|   extrude, | ||||
|   // translate, | ||||
|   // transform, | ||||
|   getExtrudeWallTransform, | ||||
|   min, | ||||
|   legLen, | ||||
|   legAngX, | ||||
|   legAngY, | ||||
|   segEndX, | ||||
|   segEndY, | ||||
|   lastSegX, | ||||
|   lastSegY, | ||||
|   segLen, | ||||
|   segAng, | ||||
|   angleToMatchLengthX, | ||||
|   angleToMatchLengthY, | ||||
|   lineTo: lineTo.fn, | ||||
|   xLineTo: xLineTo.fn, | ||||
|   yLineTo: yLineTo.fn, | ||||
|   line: line.fn, | ||||
|   xLine: xLine.fn, | ||||
|   yLine: yLine.fn, | ||||
|   angledLine: angledLine.fn, | ||||
|   angledLineOfXLength: angledLineOfXLength.fn, | ||||
|   angledLineToX: angledLineToX.fn, | ||||
|   angledLineOfYLength: angledLineOfYLength.fn, | ||||
|   angledLineToY: angledLineToY.fn, | ||||
|   angledLineThatIntersects: angledLineThatIntersects.fn, | ||||
|   startSketchAt, | ||||
|   close, | ||||
| } | ||||
|  | ||||
| // function rotateOnAxis<T extends SketchGroup | ExtrudeGroup>( | ||||
| //   axisMultiplier: [number, number, number] | ||||
| // ): InternalFn { | ||||
| //   return ({ sourceRange }, rotationD: number, sketch: T): T => { | ||||
| //     const rotationR = rotationD * (Math.PI / 180) | ||||
| //     const rotateVec = new Vector3(...axisMultiplier) | ||||
| //     const quaternion = new Quaternion() | ||||
| //     quaternion.setFromAxisAngle(rotateVec, rotationR) | ||||
|  | ||||
| //     const position = new Vector3(...sketch.position) | ||||
| //       .applyQuaternion(quaternion) | ||||
| //       .toArray() | ||||
|  | ||||
| //     const existingQuat = new Quaternion(...sketch.rotation) | ||||
| //     const rotation = quaternion.multiply(existingQuat).toArray() | ||||
| //     return { | ||||
| //       ...sketch, | ||||
| //       rotation, | ||||
| //       position, | ||||
| //       __meta: [ | ||||
| //         ...sketch.__meta, | ||||
| //         { | ||||
| //           sourceRange, | ||||
| //           pathToNode: [], // TODO | ||||
| //         }, | ||||
| //       ], | ||||
| //     } | ||||
| //   } | ||||
| // } | ||||
|  | ||||
| export function clockwiseSign(points: [number, number][]): number { | ||||
|   let sum = 0 | ||||
|   for (let i = 0; i < points.length; i++) { | ||||
|     const currentPoint = points[i] | ||||
|     const nextPoint = points[(i + 1) % points.length] | ||||
|     sum += (nextPoint[0] - currentPoint[0]) * (nextPoint[1] + currentPoint[1]) | ||||
|   } | ||||
|   return sum >= 0 ? 1 : -1 | ||||
| } | ||||
| @ -17,6 +17,44 @@ export interface PathReturn { | ||||
|   currentPath: Path | ||||
| } | ||||
|  | ||||
| export type InternalFn = (internals: InternalFirstArg, ...args: any[]) => any | ||||
|  | ||||
| export type InternalFnNames = | ||||
|   // TODO re-enable these | ||||
|   // | 'translate' | ||||
|   // | 'transform' | ||||
|   // | 'rx' // Enable rotations #152 | ||||
|   // | 'ry' | ||||
|   // | 'rz' | ||||
|   | 'extrude' | ||||
|   | 'getExtrudeWallTransform' | ||||
|   | 'min' | ||||
|   | 'legLen' | ||||
|   | 'legAngX' | ||||
|   | 'legAngY' | ||||
|   | 'segEndX' | ||||
|   | 'segEndY' | ||||
|   | 'lastSegX' | ||||
|   | 'lastSegY' | ||||
|   | 'segLen' | ||||
|   | 'segAng' | ||||
|   | 'angleToMatchLengthX' | ||||
|   | 'angleToMatchLengthY' | ||||
|   | 'lineTo' | ||||
|   | 'yLineTo' | ||||
|   | 'xLineTo' | ||||
|   | 'line' | ||||
|   | 'yLine' | ||||
|   | 'xLine' | ||||
|   | 'angledLine' | ||||
|   | 'angledLineOfXLength' | ||||
|   | 'angledLineToX' | ||||
|   | 'angledLineOfYLength' | ||||
|   | 'angledLineToY' | ||||
|   | 'startSketchAt' | ||||
|   | 'close' | ||||
|   | 'angledLineThatIntersects' | ||||
|  | ||||
| export interface ModifyAstBase { | ||||
|   node: Program | ||||
|   previousProgramMemory: ProgramMemory | ||||
| @ -49,6 +87,7 @@ export type SketchCallTransfromMap = { | ||||
| } | ||||
|  | ||||
| export interface SketchLineHelper { | ||||
|   fn: InternalFn | ||||
|   add: (a: addCall) => { | ||||
|     modifiedAst: Program | ||||
|     pathToNode: PathToNode | ||||
|  | ||||
| @ -110,7 +110,7 @@ const yi=45` | ||||
|       "brace        ')'        from 17  to 18", | ||||
|     ]) | ||||
|     expect(stringSummaryLexer('fn funcName = (param1, param2) => {}')).toEqual([ | ||||
|       "keyword      'fn'       from 0   to 2", | ||||
|       "word         'fn'       from 0   to 2", | ||||
|       "whitespace   ' '        from 2   to 3", | ||||
|       "word         'funcName' from 3   to 11", | ||||
|       "whitespace   ' '        from 11  to 12", | ||||
| @ -203,7 +203,7 @@ const yi=45` | ||||
|   it('testing array declaration', () => { | ||||
|     const result = stringSummaryLexer(`const yo = [1, 2]`) | ||||
|     expect(result).toEqual([ | ||||
|       "keyword      'const'    from 0   to 5", | ||||
|       "word         'const'    from 0   to 5", | ||||
|       "whitespace   ' '        from 5   to 6", | ||||
|       "word         'yo'       from 6   to 8", | ||||
|       "whitespace   ' '        from 8   to 9", | ||||
| @ -220,7 +220,7 @@ const yi=45` | ||||
|   it('testing object declaration', () => { | ||||
|     const result = stringSummaryLexer(`const yo = {key: 'value'}`) | ||||
|     expect(result).toEqual([ | ||||
|       "keyword      'const'    from 0   to 5", | ||||
|       "word         'const'    from 0   to 5", | ||||
|       "whitespace   ' '        from 5   to 6", | ||||
|       "word         'yo'       from 6   to 8", | ||||
|       "whitespace   ' '        from 8   to 9", | ||||
| @ -241,7 +241,7 @@ const prop2 = yo['key'] | ||||
| const key = 'key' | ||||
| const prop3 = yo[key]`) | ||||
|     expect(result).toEqual([ | ||||
|       "keyword      'const'    from 0   to 5", | ||||
|       "word         'const'    from 0   to 5", | ||||
|       "whitespace   ' '        from 5   to 6", | ||||
|       "word         'yo'       from 6   to 8", | ||||
|       "whitespace   ' '        from 8   to 9", | ||||
| @ -254,7 +254,7 @@ const prop3 = yo[key]`) | ||||
|       "string       ''value''  from 17  to 24", | ||||
|       "brace        '}'        from 24  to 25", | ||||
|       "whitespace   '\n'        from 25  to 26", | ||||
|       "keyword      'const'    from 26  to 31", | ||||
|       "word         'const'    from 26  to 31", | ||||
|       "whitespace   ' '        from 31  to 32", | ||||
|       "word         'prop'     from 32  to 36", | ||||
|       "whitespace   ' '        from 36  to 37", | ||||
| @ -264,7 +264,7 @@ const prop3 = yo[key]`) | ||||
|       "period       '.'        from 41  to 42", | ||||
|       "word         'key'      from 42  to 45", | ||||
|       "whitespace   '\n'        from 45  to 46", | ||||
|       "keyword      'const'    from 46  to 51", | ||||
|       "word         'const'    from 46  to 51", | ||||
|       "whitespace   ' '        from 51  to 52", | ||||
|       "word         'prop2'    from 52  to 57", | ||||
|       "whitespace   ' '        from 57  to 58", | ||||
| @ -275,7 +275,7 @@ const prop3 = yo[key]`) | ||||
|       "string       ''key''    from 63  to 68", | ||||
|       "brace        ']'        from 68  to 69", | ||||
|       "whitespace   '\n'        from 69  to 70", | ||||
|       "keyword      'const'    from 70  to 75", | ||||
|       "word         'const'    from 70  to 75", | ||||
|       "whitespace   ' '        from 75  to 76", | ||||
|       "word         'key'      from 76  to 79", | ||||
|       "whitespace   ' '        from 79  to 80", | ||||
| @ -283,7 +283,7 @@ const prop3 = yo[key]`) | ||||
|       "whitespace   ' '        from 81  to 82", | ||||
|       "string       ''key''    from 82  to 87", | ||||
|       "whitespace   '\n'        from 87  to 88", | ||||
|       "keyword      'const'    from 88  to 93", | ||||
|       "word         'const'    from 88  to 93", | ||||
|       "whitespace   ' '        from 93  to 94", | ||||
|       "word         'prop3'    from 94  to 99", | ||||
|       "whitespace   ' '        from 99  to 100", | ||||
| @ -299,7 +299,7 @@ const prop3 = yo[key]`) | ||||
|     const result = stringSummaryLexer(`const yo = 45 // this is a comment | ||||
| const yo = 6`) | ||||
|     expect(result).toEqual([ | ||||
|       "keyword      'const'    from 0   to 5", | ||||
|       "word         'const'    from 0   to 5", | ||||
|       "whitespace   ' '        from 5   to 6", | ||||
|       "word         'yo'       from 6   to 8", | ||||
|       "whitespace   ' '        from 8   to 9", | ||||
| @ -307,9 +307,9 @@ const yo = 6`) | ||||
|       "whitespace   ' '        from 10  to 11", | ||||
|       "number       '45'       from 11  to 13", | ||||
|       "whitespace   ' '        from 13  to 14", | ||||
|       "lineComment  '// this is a comment' from 14  to 34", | ||||
|       "linecomment  '// this is a comment' from 14  to 34", | ||||
|       "whitespace   '\n'        from 34  to 35", | ||||
|       "keyword      'const'    from 35  to 40", | ||||
|       "word         'const'    from 35  to 40", | ||||
|       "whitespace   ' '        from 40  to 41", | ||||
|       "word         'yo'       from 41  to 43", | ||||
|       "whitespace   ' '        from 43  to 44", | ||||
| @ -328,9 +328,9 @@ const yo=45`) | ||||
|       "string       ''hi''     from 4   to 8", | ||||
|       "brace        ')'        from 8   to 9", | ||||
|       "whitespace   '\n'        from 9   to 10", | ||||
|       "lineComment  '// comment on a line by itself' from 10  to 40", | ||||
|       "linecomment  '// comment on a line by itself' from 10  to 40", | ||||
|       "whitespace   '\n'        from 40  to 41", | ||||
|       "keyword      'const'    from 41  to 46", | ||||
|       "word         'const'    from 41  to 46", | ||||
|       "whitespace   ' '        from 46  to 47", | ||||
|       "word         'yo'       from 47  to 49", | ||||
|       "operator     '='        from 49  to 50", | ||||
| @ -342,7 +342,7 @@ const yo=45`) | ||||
| const ya = 6 */ | ||||
| const yi=45`) | ||||
|     expect(result).toEqual([ | ||||
|       "keyword      'const'    from 0   to 5", | ||||
|       "word         'const'    from 0   to 5", | ||||
|       "whitespace   ' '        from 5   to 6", | ||||
|       "word         'yo'       from 6   to 8", | ||||
|       "whitespace   ' '        from 8   to 9", | ||||
| @ -350,10 +350,10 @@ const yi=45`) | ||||
|       "whitespace   ' '        from 10  to 11", | ||||
|       "number       '45'       from 11  to 13", | ||||
|       "whitespace   ' '        from 13  to 14", | ||||
|       `blockComment '/* this is a comment | ||||
|       `blockcomment '/* this is a comment | ||||
| const ya = 6 */' from 14  to 50`, | ||||
|       "whitespace   '\n'        from 50  to 51", | ||||
|       "keyword      'const'    from 51  to 56", | ||||
|       "word         'const'    from 51  to 56", | ||||
|       "whitespace   ' '        from 56  to 57", | ||||
|       "word         'yi'       from 57  to 59", | ||||
|       "operator     '='        from 59  to 60", | ||||
|  | ||||
| @ -1,8 +1,8 @@ | ||||
| import { lexer_js } from '../wasm-lib/pkg/wasm_lib' | ||||
| import { initPromise } from './rust' | ||||
| import { Token } from '../wasm-lib/kcl/bindings/Token' | ||||
| import { Token } from '../wasm-lib/bindings/Token' | ||||
|  | ||||
| export type { Token } from '../wasm-lib/kcl/bindings/Token' | ||||
| export type { Token } from '../wasm-lib/bindings/Token' | ||||
|  | ||||
| export async function asyncLexer(str: string): Promise<Token[]> { | ||||
|   await initPromise | ||||
|  | ||||
| @ -26,12 +26,3 @@ export function updateCursors( | ||||
|     setCursor(newSelections) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function isReducedMotion(): boolean { | ||||
|   return ( | ||||
|     typeof window !== 'undefined' && | ||||
|     window.matchMedia && | ||||
|     // TODO/Note I (Kurt) think '(prefers-reduced-motion: reduce)' and '(prefers-reduced-motion)' are equivalent, but not 100% sure | ||||
|     window.matchMedia('(prefers-reduced-motion)').matches | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -1,24 +1,6 @@ | ||||
| import { createMachine, assign } from 'xstate' | ||||
| import { Models } from '@kittycad/lib' | ||||
| import withBaseURL from '../lib/withBaseURL' | ||||
| import { CommandBarMeta } from '../lib/commands' | ||||
| 
 | ||||
| const SKIP_AUTH = | ||||
|   import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV | ||||
| const LOCAL_USER: Models['User_type'] = { | ||||
|   id: '8675309', | ||||
|   name: 'Test User', | ||||
|   email: 'kittycad.sidebar.test@example.com', | ||||
|   image: 'https://placekitten.com/200/200', | ||||
|   created_at: 'yesteryear', | ||||
|   updated_at: 'today', | ||||
|   company: 'Test Company', | ||||
|   discord: 'Test User#1234', | ||||
|   github: 'testuser', | ||||
|   phone: '555-555-5555', | ||||
|   first_name: 'Test', | ||||
|   last_name: 'User', | ||||
| } | ||||
| 
 | ||||
| export interface UserContext { | ||||
|   user?: Models['User_type'] | ||||
| @ -27,22 +9,16 @@ export interface UserContext { | ||||
| 
 | ||||
| export type Events = | ||||
|   | { | ||||
|       type: 'Log out' | ||||
|       type: 'logout' | ||||
|     } | ||||
|   | { | ||||
|       type: 'Log in' | ||||
|       type: 'tryLogin' | ||||
|       token?: string | ||||
|     } | ||||
| 
 | ||||
| export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY' | ||||
| const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || '' | ||||
| 
 | ||||
| export const authCommandBarMeta: CommandBarMeta = { | ||||
|   'Log in': { | ||||
|     hide: 'both', | ||||
|   }, | ||||
| } | ||||
| 
 | ||||
| export const authMachine = createMachine<UserContext, Events>( | ||||
|   { | ||||
|     id: 'Auth', | ||||
| @ -74,7 +50,7 @@ export const authMachine = createMachine<UserContext, Events>( | ||||
|       loggedIn: { | ||||
|         entry: ['goToIndexPage'], | ||||
|         on: { | ||||
|           'Log out': { | ||||
|           logout: { | ||||
|             target: 'loggedOut', | ||||
|           }, | ||||
|         }, | ||||
| @ -82,10 +58,10 @@ export const authMachine = createMachine<UserContext, Events>( | ||||
|       loggedOut: { | ||||
|         entry: ['goToSignInPage'], | ||||
|         on: { | ||||
|           'Log in': { | ||||
|           tryLogin: { | ||||
|             target: 'checkIfLoggedIn', | ||||
|             actions: assign({ | ||||
|               token: (_, event) => { | ||||
|               token: (context, event) => { | ||||
|                 const token = event.token || '' | ||||
|                 localStorage.setItem(TOKEN_PERSIST_KEY, token) | ||||
|                 return token | ||||
| @ -95,12 +71,10 @@ export const authMachine = createMachine<UserContext, Events>( | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     schema: { events: {} as { type: 'Log out' } | { type: 'Log in' } }, | ||||
|     schema: { events: {} as { type: 'logout' } | { type: 'tryLogin' } }, | ||||
|     predictableActionArguments: true, | ||||
|     preserveActionOrder: true, | ||||
|     context: { | ||||
|       token: persistedToken, | ||||
|     }, | ||||
|     context: { token: persistedToken }, | ||||
|   }, | ||||
|   { | ||||
|     actions: {}, | ||||
| @ -117,17 +91,12 @@ async function getUser(context: UserContext) { | ||||
|   } | ||||
|   if (!context.token && '__TAURI__' in window) throw 'not log in' | ||||
|   if (context.token) headers['Authorization'] = `Bearer ${context.token}` | ||||
|   if (SKIP_AUTH) return LOCAL_USER | ||||
|   try { | ||||
|     const response = await fetch(url, { | ||||
|       method: 'GET', | ||||
|       credentials: 'include', | ||||
|       headers, | ||||
|     }) | ||||
|     const user = await response.json() | ||||
|     if ('error_code' in user) throw new Error(user.message) | ||||
|     return user | ||||
|   } catch (e) { | ||||
|     console.error(e) | ||||
|   } | ||||
|   const response = await fetch(url, { | ||||
|     method: 'GET', | ||||
|     credentials: 'include', | ||||
|     headers, | ||||
|   }) | ||||
|   const user = await response.json() | ||||
|   if ('error_code' in user) throw new Error(user.message) | ||||
|   return user | ||||
| } | ||||
| @ -1,124 +0,0 @@ | ||||
| import { AnyStateMachine, EventFrom, StateFrom } from 'xstate' | ||||
| import { isTauri } from './isTauri' | ||||
|  | ||||
| type InitialCommandBarMetaArg = { | ||||
|   name: string | ||||
|   type: 'string' | 'select' | ||||
|   description?: string | ||||
|   defaultValue?: string | ||||
|   options: string | Array<{ name: string }> | ||||
| } | ||||
|  | ||||
| type Platform = 'both' | 'web' | 'desktop' | ||||
|  | ||||
| export type CommandBarMeta = { | ||||
|   [key: string]: | ||||
|     | { | ||||
|         displayValue: (args: string[]) => string | ||||
|         args: InitialCommandBarMetaArg[] | ||||
|         hide?: Platform | ||||
|       } | ||||
|     | { | ||||
|         hide?: Platform | ||||
|       } | ||||
| } | ||||
|  | ||||
| export type Command = { | ||||
|   owner: string | ||||
|   name: string | ||||
|   callback: Function | ||||
|   meta?: { | ||||
|     displayValue(args: string[]): string | string | ||||
|     args: SubCommand[] | ||||
|   } | ||||
| } | ||||
|  | ||||
| export type SubCommand = { | ||||
|   name: string | ||||
|   type: 'select' | 'string' | ||||
|   description?: string | ||||
|   options?: Partial<{ name: string }>[] | ||||
| } | ||||
|  | ||||
| interface CommandBarArgs<T extends AnyStateMachine> { | ||||
|   type: EventFrom<T>['type'] | ||||
|   state: StateFrom<T> | ||||
|   commandBarMeta?: CommandBarMeta | ||||
|   send: Function | ||||
|   owner: string | ||||
| } | ||||
|  | ||||
| export function createMachineCommand<T extends AnyStateMachine>({ | ||||
|   type, | ||||
|   state, | ||||
|   commandBarMeta, | ||||
|   send, | ||||
|   owner, | ||||
| }: CommandBarArgs<T>): Command | null { | ||||
|   const lookedUpMeta = commandBarMeta && commandBarMeta[type] | ||||
|   if (lookedUpMeta && 'hide' in lookedUpMeta) { | ||||
|     const { hide } = lookedUpMeta | ||||
|     if (hide === 'both') return null | ||||
|     else if (hide === 'desktop' && isTauri()) return null | ||||
|     else if (hide === 'web' && !isTauri()) return null | ||||
|   } | ||||
|   let replacedArgs | ||||
|  | ||||
|   if (lookedUpMeta && 'args' in lookedUpMeta) { | ||||
|     replacedArgs = lookedUpMeta.args.map((arg) => { | ||||
|       const optionsFromContext = state.context[ | ||||
|         arg.options as keyof typeof state.context | ||||
|       ] as { name: string }[] | string | undefined | ||||
|       const defaultValueFromContext = state.context[ | ||||
|         arg.defaultValue as keyof typeof state.context | ||||
|       ] as string | undefined | ||||
|  | ||||
|       const options = | ||||
|         arg.options instanceof Array | ||||
|           ? arg.options.map((o) => ({ | ||||
|               ...o, | ||||
|               description: | ||||
|                 defaultValueFromContext === o.name ? '(current)' : '', | ||||
|             })) | ||||
|           : !optionsFromContext || typeof optionsFromContext === 'string' | ||||
|           ? [ | ||||
|               { | ||||
|                 name: optionsFromContext, | ||||
|                 description: arg.description || '', | ||||
|               }, | ||||
|             ] | ||||
|           : optionsFromContext.map((o) => ({ | ||||
|               name: o.name || '', | ||||
|               description: arg.description || '', | ||||
|             })) | ||||
|  | ||||
|       return { | ||||
|         ...arg, | ||||
|         options, | ||||
|       } | ||||
|     }) as any[] | ||||
|   } | ||||
|  | ||||
|   // We have to recreate this object every time, | ||||
|   // otherwise we'll have stale state in the CommandBar | ||||
|   // after completing our first action | ||||
|   const meta = lookedUpMeta | ||||
|     ? { | ||||
|         ...lookedUpMeta, | ||||
|         args: replacedArgs, | ||||
|       } | ||||
|     : undefined | ||||
|  | ||||
|   return { | ||||
|     name: type, | ||||
|     owner, | ||||
|     callback: (data: EventFrom<T, typeof type>) => { | ||||
|       if (data !== undefined && data !== null) { | ||||
|         send(type, { data }) | ||||
|       } else { | ||||
|         send(type) | ||||
|       } | ||||
|     }, | ||||
|     meta: meta as any, | ||||
|   } | ||||
| } | ||||
| @ -1,13 +1,16 @@ | ||||
| export default function fetcher(input: RequestInfo, init: RequestInit = {}) { | ||||
|   const fetcherWithToken = async (token?: string): Promise<JSON> => { | ||||
|     const headers = { ...init.headers } as Record<string, string> | ||||
|     if (token) { | ||||
|       headers.Authorization = `Bearer ${token}` | ||||
|     } | ||||
| import { useAuthMachine } from '../hooks/useAuthMachine' | ||||
|  | ||||
|     const credentials = 'include' as RequestCredentials | ||||
|     const res = await fetch(input, { ...init, credentials, headers }) | ||||
|     return res.json() | ||||
| export default async function fetcher<JSON = any>( | ||||
|   input: RequestInfo, | ||||
|   init: RequestInit = {} | ||||
| ): Promise<JSON> { | ||||
|   const [token] = useAuthMachine((s) => s?.context?.token) | ||||
|   const headers = { ...init.headers } as Record<string, string> | ||||
|   if (token) { | ||||
|     headers.Authorization = `Bearer ${token}` | ||||
|   } | ||||
|   return fetcherWithToken | ||||
|  | ||||
|   const credentials = 'include' as RequestCredentials | ||||
|   const res = await fetch(input, { ...init, credentials, headers }) | ||||
|   return res.json() | ||||
| } | ||||
|  | ||||
							
								
								
									
										9
									
								
								src/lib/getSystemTheme.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/lib/getSystemTheme.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| import { Themes } from '../useStore' | ||||
|  | ||||
| export function getSystemTheme(): Exclude<Themes, 'system'> { | ||||
|   return typeof window !== 'undefined' && | ||||
|     'matchMedia' in window && | ||||
|     window.matchMedia('(prefers-color-scheme: dark)').matches | ||||
|     ? Themes.Dark | ||||
|     : Themes.Light | ||||
| } | ||||
| @ -1,64 +0,0 @@ | ||||
| import { | ||||
|   faArrowDown, | ||||
|   faArrowUp, | ||||
|   faCircleDot, | ||||
| } from '@fortawesome/free-solid-svg-icons' | ||||
| import { ProjectWithEntryPointMetadata } from '../Router' | ||||
|  | ||||
| const DESC = ':desc' | ||||
|  | ||||
| export function getSortIcon(currentSort: string, newSort: string) { | ||||
|   if (currentSort === newSort) { | ||||
|     return faArrowUp | ||||
|   } else if (currentSort === newSort + DESC) { | ||||
|     return faArrowDown | ||||
|   } | ||||
|   return faCircleDot | ||||
| } | ||||
|  | ||||
| export function getNextSearchParams(currentSort: string, newSort: string) { | ||||
|   if (currentSort === null || !currentSort) | ||||
|     return { sort_by: newSort + (newSort !== 'modified' ? DESC : '') } | ||||
|   if (currentSort.includes(newSort) && !currentSort.includes(DESC)) | ||||
|     return { sort_by: '' } | ||||
|   return { | ||||
|     sort_by: newSort + (currentSort.includes(DESC) ? '' : DESC), | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function getSortFunction(sortBy: string) { | ||||
|   const sortByName = ( | ||||
|     a: ProjectWithEntryPointMetadata, | ||||
|     b: ProjectWithEntryPointMetadata | ||||
|   ) => { | ||||
|     if (a.name && b.name) { | ||||
|       return sortBy.includes('desc') | ||||
|         ? a.name.localeCompare(b.name) | ||||
|         : b.name.localeCompare(a.name) | ||||
|     } | ||||
|     return 0 | ||||
|   } | ||||
|  | ||||
|   const sortByModified = ( | ||||
|     a: ProjectWithEntryPointMetadata, | ||||
|     b: ProjectWithEntryPointMetadata | ||||
|   ) => { | ||||
|     if ( | ||||
|       a.entrypoint_metadata?.modifiedAt && | ||||
|       b.entrypoint_metadata?.modifiedAt | ||||
|     ) { | ||||
|       return !sortBy || sortBy.includes('desc') | ||||
|         ? b.entrypoint_metadata.modifiedAt.getTime() - | ||||
|             a.entrypoint_metadata.modifiedAt.getTime() | ||||
|         : a.entrypoint_metadata.modifiedAt.getTime() - | ||||
|             b.entrypoint_metadata.modifiedAt.getTime() | ||||
|     } | ||||
|     return 0 | ||||
|   } | ||||
|  | ||||
|   if (sortBy?.includes('name')) { | ||||
|     return sortByName | ||||
|   } else { | ||||
|     return sortByModified | ||||
|   } | ||||
| } | ||||
| @ -1,11 +1,6 @@ | ||||
| import { | ||||
|   FileEntry, | ||||
|   createDir, | ||||
|   exists, | ||||
|   readDir, | ||||
|   writeTextFile, | ||||
| } from '@tauri-apps/api/fs' | ||||
| import { FileEntry, createDir, exists, writeTextFile } from '@tauri-apps/api/fs' | ||||
| import { documentDir } from '@tauri-apps/api/path' | ||||
| import { useStore } from '../useStore' | ||||
| import { isTauri } from './isTauri' | ||||
| import { ProjectWithEntryPointMetadata } from '../Router' | ||||
| import { metadata } from 'tauri-plugin-fs-extra-api' | ||||
| @ -17,31 +12,35 @@ const INDEX_IDENTIFIER = '$n' // $nn.. will pad the number with 0s | ||||
| export const MAX_PADDING = 7 | ||||
|  | ||||
| // Initializes the project directory and returns the path | ||||
| export async function initializeProjectDirectory(directory: string) { | ||||
| export async function initializeProjectDirectory() { | ||||
|   if (!isTauri()) { | ||||
|     throw new Error( | ||||
|       'initializeProjectDirectory() can only be called from a Tauri app' | ||||
|     ) | ||||
|   } | ||||
|   const { defaultDir: projectDir, setDefaultDir } = useStore.getState() | ||||
|  | ||||
|   if (directory) { | ||||
|     const dirExists = await exists(directory) | ||||
|   if (projectDir && projectDir.dir.length > 0) { | ||||
|     const dirExists = await exists(projectDir.dir) | ||||
|     if (!dirExists) { | ||||
|       await createDir(directory, { recursive: true }) | ||||
|       await createDir(projectDir.dir, { recursive: true }) | ||||
|     } | ||||
|     return directory | ||||
|     return projectDir | ||||
|   } | ||||
|  | ||||
|   const docDirectory = await documentDir() | ||||
|   const appData = await documentDir() | ||||
|  | ||||
|   const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER | ||||
|   const INITIAL_DEFAULT_DIR = { | ||||
|     dir: appData + PROJECT_FOLDER, | ||||
|   } | ||||
|  | ||||
|   const defaultDirExists = await exists(INITIAL_DEFAULT_DIR) | ||||
|   const defaultDirExists = await exists(INITIAL_DEFAULT_DIR.dir) | ||||
|  | ||||
|   if (!defaultDirExists) { | ||||
|     await createDir(INITIAL_DEFAULT_DIR, { recursive: true }) | ||||
|     await createDir(INITIAL_DEFAULT_DIR.dir, { recursive: true }) | ||||
|   } | ||||
|  | ||||
|   setDefaultDir(INITIAL_DEFAULT_DIR) | ||||
|   return INITIAL_DEFAULT_DIR | ||||
| } | ||||
|  | ||||
| @ -52,25 +51,6 @@ export function isProjectDirectory(fileOrDir: Partial<FileEntry>) { | ||||
|   ) | ||||
| } | ||||
|  | ||||
| // Read the contents of a directory | ||||
| // and return the valid projects | ||||
| export async function getProjectsInDir(projectDir: string) { | ||||
|   const readProjects = ( | ||||
|     await readDir(projectDir, { | ||||
|       recursive: true, | ||||
|     }) | ||||
|   ).filter(isProjectDirectory) | ||||
|  | ||||
|   const projectsWithMetadata = await Promise.all( | ||||
|     readProjects.map(async (p) => ({ | ||||
|       entrypoint_metadata: await metadata(p.path + '/' + PROJECT_ENTRYPOINT), | ||||
|       ...p, | ||||
|     })) | ||||
|   ) | ||||
|  | ||||
|   return projectsWithMetadata | ||||
| } | ||||
|  | ||||
| // Creates a new file in the default directory with the default project name | ||||
| // Returns the path to the new file | ||||
| export async function createNewProject( | ||||
|  | ||||
| @ -1,10 +1,6 @@ | ||||
| import { Program } from '../lang/abstractSyntaxTreeTypes' | ||||
| import { ProgramMemory, _executor } from '../lang/executor' | ||||
| import { | ||||
|   EngineCommandManager, | ||||
|   EngineCommand, | ||||
| } from '../lang/std/engineConnection' | ||||
| import { SourceRange } from 'lang/executor' | ||||
| import { EngineCommandManager } from '../lang/std/engineConnection' | ||||
|  | ||||
| class MockEngineCommandManager { | ||||
|   constructor(mockParams: { | ||||
| @ -14,42 +10,13 @@ class MockEngineCommandManager { | ||||
|   startNewSession() {} | ||||
|   waitForAllCommands() {} | ||||
|   waitForReady = new Promise<void>((resolve) => resolve()) | ||||
|   sendModelingCommand({ | ||||
|     id, | ||||
|     range, | ||||
|     command, | ||||
|   }: { | ||||
|     id: string | ||||
|     range: SourceRange | ||||
|     command: EngineCommand | ||||
|   }): Promise<any> { | ||||
|     return Promise.resolve() | ||||
|   } | ||||
|   sendModelingCommandFromWasm( | ||||
|     id: string, | ||||
|     rangeStr: string, | ||||
|     commandStr: string | ||||
|   ): Promise<any> { | ||||
|     if (id === undefined) { | ||||
|       throw new Error('id is undefined') | ||||
|     } | ||||
|     if (rangeStr === undefined) { | ||||
|       throw new Error('rangeStr is undefined') | ||||
|     } | ||||
|     if (commandStr === undefined) { | ||||
|       throw new Error('commandStr is undefined') | ||||
|     } | ||||
|     const command: EngineCommand = JSON.parse(commandStr) | ||||
|     const range: SourceRange = JSON.parse(rangeStr) | ||||
|  | ||||
|     return this.sendModelingCommand({ id, range, command }) | ||||
|   } | ||||
|   sendModellingCommand() {} | ||||
|   sendSceneCommand() {} | ||||
| } | ||||
|  | ||||
| export async function enginelessExecutor( | ||||
|   ast: Program, | ||||
|   pm: ProgramMemory = { root: {} } | ||||
|   pm: ProgramMemory = { root: {}, pendingMemory: {} } | ||||
| ): Promise<ProgramMemory> { | ||||
|   const mockEngineCommandManager = new MockEngineCommandManager({ | ||||
|     setIsStreamReady: () => {}, | ||||
| @ -64,7 +31,7 @@ export async function enginelessExecutor( | ||||
|  | ||||
| export async function executor( | ||||
|   ast: Program, | ||||
|   pm: ProgramMemory = { root: {} } | ||||
|   pm: ProgramMemory = { root: {}, pendingMemory: {} } | ||||
| ): Promise<ProgramMemory> { | ||||
|   const engineCommandManager = new EngineCommandManager({ | ||||
|     setIsStreamReady: () => {}, | ||||
|  | ||||
| @ -1,23 +0,0 @@ | ||||
| export enum Themes { | ||||
|   Light = 'light', | ||||
|   Dark = 'dark', | ||||
|   System = 'system', | ||||
| } | ||||
|  | ||||
| // Get the theme from the system settings manually | ||||
| export function getSystemTheme(): Exclude<Themes, 'system'> { | ||||
|   return typeof window !== 'undefined' && 'matchMedia' in window | ||||
|     ? window.matchMedia('(prefers-color-scheme: dark)').matches | ||||
|       ? Themes.Dark | ||||
|       : Themes.Light | ||||
|     : Themes.Light | ||||
| } | ||||
|  | ||||
| // Set the theme class on the body element | ||||
| export function setThemeClass(theme: Themes) { | ||||
|   if (theme === Themes.Dark) { | ||||
|     document.body.classList.add('dark') | ||||
|   } else { | ||||
|     document.body.classList.remove('dark') | ||||
|   } | ||||
| } | ||||
| @ -1,218 +0,0 @@ | ||||
| import { assign, createMachine } from 'xstate' | ||||
| import { ProjectWithEntryPointMetadata } from '../Router' | ||||
| import { CommandBarMeta } from '../lib/commands' | ||||
|  | ||||
| export const homeCommandMeta: CommandBarMeta = { | ||||
|   'Create project': { | ||||
|     displayValue: (args: string[]) => `Create project "${args[0]}"`, | ||||
|     args: [ | ||||
|       { | ||||
|         name: 'name', | ||||
|         type: 'string', | ||||
|         description: '(default)', | ||||
|         options: 'defaultProjectName', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   'Open project': { | ||||
|     displayValue: (args: string[]) => `Open project "${args[0]}"`, | ||||
|     args: [ | ||||
|       { | ||||
|         name: 'name', | ||||
|         type: 'select', | ||||
|         options: 'projects', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   'Delete project': { | ||||
|     displayValue: (args: string[]) => `Delete project "${args[0]}"`, | ||||
|     args: [ | ||||
|       { | ||||
|         name: 'name', | ||||
|         type: 'select', | ||||
|         options: 'projects', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   'Rename project': { | ||||
|     displayValue: (args: string[]) => | ||||
|       `Rename project "${args[0]}" to "${args[1]}"`, | ||||
|     args: [ | ||||
|       { | ||||
|         name: 'oldName', | ||||
|         type: 'select', | ||||
|         options: 'projects', | ||||
|       }, | ||||
|       { | ||||
|         name: 'newName', | ||||
|         type: 'string', | ||||
|         description: '(default)', | ||||
|         options: 'defaultProjectName', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   assign: { | ||||
|     hide: 'both', | ||||
|   }, | ||||
| } | ||||
|  | ||||
| export const homeMachine = createMachine( | ||||
|   { | ||||
|     /** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAHTK6xampYAOATqgFZj4AusAxAMLMwuLthbtOXANoAGALqJQjVLGJdiqUopAAPRAHYAbPooAWABwBGUwE5zAJgeGArM-MAaEAE9EN0wGYKGX97GX1nGVNDS0MbfwBfeM80TBwCEnIqGiZWDm4+ACUwUlxU8TzpeW1lVXVNbT0EcJNg02d-fzt7fU77Tx8EQ0iKCPtnfUsjGRtLGXtE5IxsPCIySmpacsk+QWFRHIluWQUkEBq1DS1TxqN7ChjzOxtXf0t7a37EcwsRibH-ZzRezA8wLEApZbpNZZTa5ba8AAiYAANmB9lsjlVTuc6ldQDdDOYKP5bm0os5TDJDJ8mlEzPpzIZHA4bO9umCIWlVpkNgcKnwAPKMYp8yTHaoqC71a6IEmBUz6BkWZzWDq2Uw0qzOIJAwz+PXWfSmeZJcFLLkZSi7ERkKCi7i8CCaShkABuqAA1pR8EIRGAALQYyonJSS3ENRDA2wUeyvd6dPVhGw0-RhGOp8IA8xGFkc80rS0Ua3qUh2oO8MDMVjMCiMZEiABmqGY6AoPr2AaD4uxYcuEYQoQpQWNNjsMnMgLGKbT3TC7TcOfsNjzqQL0KKJXQtvtXEdzoobs9lCEm87cMxIbOvel+MQqtMQRmS5ks31sZpAUsZkcIX+cQZJIrpC3KUBupTbuWlbVrW9ZcE2LYUCepRnocwYSrUfYyggbzvBQ+jMq49imLYwTUt4iCft+5i-u0-7UfoQEWtCSKoiWZbnruTqZIeXoUBAKJoihFTdqGGE3rod7UdqsQTI8hiGAqrIauRA7RvYeoqhO1jtAqjFrpkLFohBHEVlWzYwY2zatvxrFCWKWKiVKeISdh4yBJE-jGs4fhhA4zg0kRNgxhplhaW0nn4XpUKZEUuAQMZqF8FxLqkO6vG+hAgYcbAIlXmJzmNERdy0RYNiKgpthxDSEU6q8MSTJYjWGFFIEULF8WljuSX7jxx7CJlQY5ZYl44pht4IP61gyPc8njt0lIuH51UKrVVITEyMy2C1hbtQl-KmdBdaWQhGVZYluWjeJjSTf402shMEyuEyljPAFL0UNmMiuN86lWHMiSmvQ-HwKcnL6WA6FOf2k3mESMRDA4RpUm4U4qf6gSEt0QIvvqfjOCaiyrtF6zZPQXWQ+GWFlUEsbmNMf1TV9NLeXDcqRIySnNaaYPEzC5M9vl-b+IyFCjupryPF9jKWP5Kks-cbMWLERHRNt0LFntkgU2NLk4dqsz43YsTK++Kk2C+MbTOOcxzOMrhqzFxTgZ1Qba1dd6BUE1jGsLMxxK9KlDNqm3tMLUQvqYlgO5QhlsTubsFXesTTUuPTfHExshDS0RftRftGgEnTZtHbX9Zr+QJ-2S4Y3qnmTC+4tMyp1EfeOnmeQqdOhyXQrFOXXCV1hCkmLDOnBJYvRRDSsyRzGjiKj0lKdAkANAA */ | ||||
|     id: 'Home machine', | ||||
|  | ||||
|     initial: 'Reading projects', | ||||
|  | ||||
|     context: { | ||||
|       projects: [] as ProjectWithEntryPointMetadata[], | ||||
|       defaultProjectName: '', | ||||
|       defaultDirectory: '', | ||||
|     }, | ||||
|  | ||||
|     on: { | ||||
|       assign: { | ||||
|         actions: assign((_, event) => ({ | ||||
|           ...event.data, | ||||
|         })), | ||||
|         target: '.Reading projects', | ||||
|       }, | ||||
|     }, | ||||
|     states: { | ||||
|       'Has no projects': { | ||||
|         on: { | ||||
|           'Create project': { | ||||
|             target: 'Creating project', | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|  | ||||
|       'Has projects': { | ||||
|         on: { | ||||
|           'Rename project': { | ||||
|             target: 'Renaming project', | ||||
|           }, | ||||
|  | ||||
|           'Create project': { | ||||
|             target: 'Creating project', | ||||
|           }, | ||||
|  | ||||
|           'Delete project': { | ||||
|             target: 'Deleting project', | ||||
|           }, | ||||
|  | ||||
|           'Open project': { | ||||
|             target: 'Opening project', | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|  | ||||
|       'Creating project': { | ||||
|         invoke: { | ||||
|           id: 'create-project', | ||||
|           src: 'createProject', | ||||
|           onDone: [ | ||||
|             { | ||||
|               target: 'Reading projects', | ||||
|               actions: ['toastSuccess'], | ||||
|             }, | ||||
|           ], | ||||
|           onError: [ | ||||
|             { | ||||
|               target: 'Reading projects', | ||||
|               actions: ['toastError'], | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|       }, | ||||
|  | ||||
|       'Renaming project': { | ||||
|         invoke: { | ||||
|           id: 'rename-project', | ||||
|           src: 'renameProject', | ||||
|           onDone: [ | ||||
|             { | ||||
|               target: '#Home machine.Reading projects', | ||||
|               actions: ['toastSuccess'], | ||||
|             }, | ||||
|           ], | ||||
|           onError: [ | ||||
|             { | ||||
|               target: '#Home machine.Reading projects', | ||||
|               actions: ['toastError'], | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|       }, | ||||
|  | ||||
|       'Deleting project': { | ||||
|         invoke: { | ||||
|           id: 'delete-project', | ||||
|           src: 'deleteProject', | ||||
|           onDone: [ | ||||
|             { | ||||
|               actions: ['toastSuccess'], | ||||
|               target: '#Home machine.Reading projects', | ||||
|             }, | ||||
|           ], | ||||
|           onError: { | ||||
|             actions: ['toastError'], | ||||
|             target: '#Home machine.Has projects', | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|  | ||||
|       'Reading projects': { | ||||
|         invoke: { | ||||
|           id: 'read-projects', | ||||
|           src: 'readProjects', | ||||
|           onDone: [ | ||||
|             { | ||||
|               cond: 'Has at least 1 project', | ||||
|               target: 'Has projects', | ||||
|               actions: ['setProjects'], | ||||
|             }, | ||||
|             { | ||||
|               target: 'Has no projects', | ||||
|               actions: ['setProjects'], | ||||
|             }, | ||||
|           ], | ||||
|           onError: [ | ||||
|             { | ||||
|               target: 'Has no projects', | ||||
|               actions: ['toastError'], | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|       }, | ||||
|  | ||||
|       'Opening project': { | ||||
|         entry: ['navigateToProject'], | ||||
|       }, | ||||
|     }, | ||||
|  | ||||
|     schema: { | ||||
|       events: {} as | ||||
|         | { type: 'Open project'; data: { name: string } } | ||||
|         | { type: 'Rename project'; data: { oldName: string; newName: string } } | ||||
|         | { type: 'Create project'; data: { name: string } } | ||||
|         | { type: 'Delete project'; data: { name: string } } | ||||
|         | { type: 'navigate'; data: { name: string } } | ||||
|         | { | ||||
|             type: 'done.invoke.read-projects' | ||||
|             data: ProjectWithEntryPointMetadata[] | ||||
|           } | ||||
|         | { type: 'assign'; data: { [key: string]: any } }, | ||||
|     }, | ||||
|  | ||||
|     predictableActionArguments: true, | ||||
|     preserveActionOrder: true, | ||||
|     tsTypes: {} as import('./homeMachine.typegen').Typegen0, | ||||
|   }, | ||||
|   { | ||||
|     actions: { | ||||
|       setProjects: assign((_, event) => { | ||||
|         return { projects: event.data as ProjectWithEntryPointMetadata[] } | ||||
|       }), | ||||
|     }, | ||||
|   } | ||||
| ) | ||||
| @ -1,99 +0,0 @@ | ||||
| // This file was automatically generated. Edits will be overwritten | ||||
|  | ||||
| export interface Typegen0 { | ||||
|   '@@xstate/typegen': true | ||||
|   internalEvents: { | ||||
|     'done.invoke.create-project': { | ||||
|       type: 'done.invoke.create-project' | ||||
|       data: unknown | ||||
|       __tip: 'See the XState TS docs to learn how to strongly type this.' | ||||
|     } | ||||
|     'done.invoke.delete-project': { | ||||
|       type: 'done.invoke.delete-project' | ||||
|       data: unknown | ||||
|       __tip: 'See the XState TS docs to learn how to strongly type this.' | ||||
|     } | ||||
|     'done.invoke.read-projects': { | ||||
|       type: 'done.invoke.read-projects' | ||||
|       data: unknown | ||||
|       __tip: 'See the XState TS docs to learn how to strongly type this.' | ||||
|     } | ||||
|     'done.invoke.rename-project': { | ||||
|       type: 'done.invoke.rename-project' | ||||
|       data: unknown | ||||
|       __tip: 'See the XState TS docs to learn how to strongly type this.' | ||||
|     } | ||||
|     'error.platform.create-project': { | ||||
|       type: 'error.platform.create-project' | ||||
|       data: unknown | ||||
|     } | ||||
|     'error.platform.delete-project': { | ||||
|       type: 'error.platform.delete-project' | ||||
|       data: unknown | ||||
|     } | ||||
|     'error.platform.read-projects': { | ||||
|       type: 'error.platform.read-projects' | ||||
|       data: unknown | ||||
|     } | ||||
|     'error.platform.rename-project': { | ||||
|       type: 'error.platform.rename-project' | ||||
|       data: unknown | ||||
|     } | ||||
|     'xstate.init': { type: 'xstate.init' } | ||||
|   } | ||||
|   invokeSrcNameMap: { | ||||
|     createProject: 'done.invoke.create-project' | ||||
|     deleteProject: 'done.invoke.delete-project' | ||||
|     readProjects: 'done.invoke.read-projects' | ||||
|     renameProject: 'done.invoke.rename-project' | ||||
|   } | ||||
|   missingImplementations: { | ||||
|     actions: 'navigateToProject' | 'toastError' | 'toastSuccess' | ||||
|     delays: never | ||||
|     guards: 'Has at least 1 project' | ||||
|     services: | ||||
|       | 'createProject' | ||||
|       | 'deleteProject' | ||||
|       | 'readProjects' | ||||
|       | 'renameProject' | ||||
|   } | ||||
|   eventsCausingActions: { | ||||
|     navigateToProject: 'Open project' | ||||
|     setProjects: 'done.invoke.read-projects' | ||||
|     toastError: | ||||
|       | 'error.platform.create-project' | ||||
|       | 'error.platform.delete-project' | ||||
|       | 'error.platform.read-projects' | ||||
|       | 'error.platform.rename-project' | ||||
|     toastSuccess: | ||||
|       | 'done.invoke.create-project' | ||||
|       | 'done.invoke.delete-project' | ||||
|       | 'done.invoke.rename-project' | ||||
|   } | ||||
|   eventsCausingDelays: {} | ||||
|   eventsCausingGuards: { | ||||
|     'Has at least 1 project': 'done.invoke.read-projects' | ||||
|   } | ||||
|   eventsCausingServices: { | ||||
|     createProject: 'Create project' | ||||
|     deleteProject: 'Delete project' | ||||
|     readProjects: | ||||
|       | 'assign' | ||||
|       | 'done.invoke.create-project' | ||||
|       | 'done.invoke.delete-project' | ||||
|       | 'done.invoke.rename-project' | ||||
|       | 'error.platform.create-project' | ||||
|       | 'error.platform.rename-project' | ||||
|       | 'xstate.init' | ||||
|     renameProject: 'Rename project' | ||||
|   } | ||||
|   matchesStates: | ||||
|     | 'Creating project' | ||||
|     | 'Deleting project' | ||||
|     | 'Has no projects' | ||||
|     | 'Has projects' | ||||
|     | 'Opening project' | ||||
|     | 'Reading projects' | ||||
|     | 'Renaming project' | ||||
|   tags: never | ||||
| } | ||||
| @ -1,231 +0,0 @@ | ||||
| import { assign, createMachine } from 'xstate' | ||||
| import { BaseUnit, baseUnitsUnion } from '../useStore' | ||||
| import { CommandBarMeta } from '../lib/commands' | ||||
| import { Themes, getSystemTheme, setThemeClass } from '../lib/theme' | ||||
|  | ||||
| export enum UnitSystem { | ||||
|   Imperial = 'imperial', | ||||
|   Metric = 'metric', | ||||
| } | ||||
|  | ||||
| export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY' | ||||
|  | ||||
| export const settingsCommandBarMeta: CommandBarMeta = { | ||||
|   'Set Theme': { | ||||
|     displayValue: (args: string[]) => 'Change the app theme', | ||||
|     args: [ | ||||
|       { | ||||
|         name: 'theme', | ||||
|         type: 'select', | ||||
|         defaultValue: 'theme', | ||||
|         options: Object.values(Themes).map((v) => ({ name: v })) as { | ||||
|           name: string | ||||
|         }[], | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   'Set Default Project Name': { | ||||
|     displayValue: (args: string[]) => 'Set a new default project name', | ||||
|     hide: 'web', | ||||
|     args: [ | ||||
|       { | ||||
|         name: 'defaultProjectName', | ||||
|         type: 'string', | ||||
|         description: '(default)', | ||||
|         defaultValue: 'defaultProjectName', | ||||
|         options: 'defaultProjectName', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   'Set Default Directory': { | ||||
|     hide: 'both', | ||||
|   }, | ||||
|   'Set Unit System': { | ||||
|     displayValue: (args: string[]) => 'Set your default unit system', | ||||
|     args: [ | ||||
|       { | ||||
|         name: 'unitSystem', | ||||
|         type: 'select', | ||||
|         defaultValue: 'unitSystem', | ||||
|         options: [{ name: UnitSystem.Imperial }, { name: UnitSystem.Metric }], | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   'Set Base Unit': { | ||||
|     displayValue: (args: string[]) => 'Set your default base unit', | ||||
|     args: [ | ||||
|       { | ||||
|         name: 'baseUnit', | ||||
|         type: 'select', | ||||
|         defaultValue: 'baseUnit', | ||||
|         options: Object.values(baseUnitsUnion).map((v) => ({ name: v })), | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   'Set Text Wrapping': { | ||||
|     displayValue: (args: string[]) => 'Set whether text in the editor wraps', | ||||
|     args: [ | ||||
|       { | ||||
|         name: 'textWrapping', | ||||
|         type: 'select', | ||||
|         defaultValue: 'textWrapping', | ||||
|         options: [{ name: 'On' }, { name: 'Off' }], | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   'Set Onboarding Status': { | ||||
|     hide: 'both', | ||||
|   }, | ||||
| } | ||||
|  | ||||
| export const settingsMachine = createMachine( | ||||
|   { | ||||
|     /** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIAVACzAFswBtABgF1FQAHAe1iYsfbNxAAPRAA42+AEwB2KQFYAzGznKAnADZli1QBoQAT2kBGKfm37lOned3nzqgL6vjlLLgJFSFdCoAETAAMwBDAFdiagAFACc+ACswAGNqADlw5nYuJBB+QWFRfMkEABY5fDYa2rra83LjMwQdLWV8BXLyuxlVLU1Ld090bzxCEnJKYLComODMeLS0PniTXLFCoUwRMTK7fC1zNql7NgUjtnKjU0RlBSqpLVUVPVUda60tYZAvHHG-FNAgBVbBCKjIEywNBMDb5LbFPaILqdfRSORsS4qcxXZqIHqyK6qY4XOxsGTKco-P4+Cb+aYAIXCsDAVFBQjhvAE212pWkskUKnUml0+gUNxaqkU+EccnKF1UCnucnMcjcHl+o3+vkmZBofCgUFIMwARpEoFRYuFsGBiJyCtzEXzWrJlGxlKdVFKvfY1XiEBjyvhVOVzBdzu13pYFNStbTAQFqAB5bAmvjheIQf4QtDhNCRWD2hE7EqgfayHTEh7lHQNSxSf1Scz4cpHHFyFVujTKczuDXYPgQOBiGl4TaOktIhAAWg6X3nC4Xp39050sYw2rpYHHRUnztVhPJqmUlIGbEriv9WhrLZ6uibHcqUr7riAA */ | ||||
|     id: 'Settings', | ||||
|     predictableActionArguments: true, | ||||
|     context: { | ||||
|       theme: Themes.System, | ||||
|       defaultProjectName: '', | ||||
|       unitSystem: UnitSystem.Imperial, | ||||
|       baseUnit: 'in' as BaseUnit, | ||||
|       defaultDirectory: '', | ||||
|       textWrapping: 'On' as 'On' | 'Off', | ||||
|       showDebugPanel: false, | ||||
|       onboardingStatus: '', | ||||
|     }, | ||||
|     initial: 'idle', | ||||
|     states: { | ||||
|       idle: { | ||||
|         entry: ['setThemeClass'], | ||||
|         on: { | ||||
|           'Set Theme': { | ||||
|             actions: [ | ||||
|               assign({ | ||||
|                 theme: (_, event) => event.data.theme, | ||||
|               }), | ||||
|               'persistSettings', | ||||
|               'toastSuccess', | ||||
|               'setThemeClass', | ||||
|             ], | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|           'Set Default Project Name': { | ||||
|             actions: [ | ||||
|               assign({ | ||||
|                 defaultProjectName: (_, event) => event.data.defaultProjectName, | ||||
|               }), | ||||
|               'persistSettings', | ||||
|               'toastSuccess', | ||||
|             ], | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|           'Set Default Directory': { | ||||
|             actions: [ | ||||
|               assign({ | ||||
|                 defaultDirectory: (_, event) => event.data.defaultDirectory, | ||||
|               }), | ||||
|               'persistSettings', | ||||
|               'toastSuccess', | ||||
|             ], | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|           'Set Unit System': { | ||||
|             actions: [ | ||||
|               assign({ | ||||
|                 unitSystem: (_, event) => event.data.unitSystem, | ||||
|                 baseUnit: (_, event) => | ||||
|                   event.data.unitSystem === 'imperial' ? 'in' : 'mm', | ||||
|               }), | ||||
|               'persistSettings', | ||||
|               'toastSuccess', | ||||
|             ], | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|           'Set Base Unit': { | ||||
|             actions: [ | ||||
|               assign({ baseUnit: (_, event) => event.data.baseUnit }), | ||||
|               'persistSettings', | ||||
|               'toastSuccess', | ||||
|             ], | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|           'Set Text Wrapping': { | ||||
|             actions: [ | ||||
|               assign({ | ||||
|                 textWrapping: (_, event) => event.data.textWrapping, | ||||
|               }), | ||||
|               'persistSettings', | ||||
|               'toastSuccess', | ||||
|             ], | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|           'Toggle Debug Panel': { | ||||
|             actions: [ | ||||
|               assign({ | ||||
|                 showDebugPanel: (context) => { | ||||
|                   return !context.showDebugPanel | ||||
|                 }, | ||||
|               }), | ||||
|               'persistSettings', | ||||
|               'toastSuccess', | ||||
|             ], | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|           'Set Onboarding Status': { | ||||
|             actions: [ | ||||
|               assign({ | ||||
|                 onboardingStatus: (_, event) => event.data.onboardingStatus, | ||||
|               }), | ||||
|               'persistSettings', | ||||
|             ], | ||||
|             target: 'idle', | ||||
|             internal: true, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     tsTypes: {} as import('./settingsMachine.typegen').Typegen0, | ||||
|     schema: { | ||||
|       events: {} as | ||||
|         | { type: 'Set Theme'; data: { theme: Themes } } | ||||
|         | { | ||||
|             type: 'Set Default Project Name' | ||||
|             data: { defaultProjectName: string } | ||||
|           } | ||||
|         | { type: 'Set Default Directory'; data: { defaultDirectory: string } } | ||||
|         | { | ||||
|             type: 'Set Unit System' | ||||
|             data: { unitSystem: UnitSystem } | ||||
|           } | ||||
|         | { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } } | ||||
|         | { type: 'Set Text Wrapping'; data: { textWrapping: 'On' | 'Off' } } | ||||
|         | { type: 'Set Onboarding Status'; data: { onboardingStatus: string } } | ||||
|         | { type: 'Toggle Debug Panel' }, | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     actions: { | ||||
|       persistSettings: (context) => { | ||||
|         try { | ||||
|           localStorage.setItem(SETTINGS_PERSIST_KEY, JSON.stringify(context)) | ||||
|         } catch (e) { | ||||
|           console.error(e) | ||||
|         } | ||||
|       }, | ||||
|       setThemeClass: (context, event) => { | ||||
|         const currentTheme = | ||||
|           event.type === 'Set Theme' ? event.data.theme : context.theme | ||||
|         setThemeClass( | ||||
|           currentTheme === Themes.System ? getSystemTheme() : currentTheme | ||||
|         ) | ||||
|       }, | ||||
|     }, | ||||
|   } | ||||
| ) | ||||
| @ -1,49 +0,0 @@ | ||||
| // This file was automatically generated. Edits will be overwritten | ||||
|  | ||||
| export interface Typegen0 { | ||||
|   '@@xstate/typegen': true | ||||
|   internalEvents: { | ||||
|     'xstate.init': { type: 'xstate.init' } | ||||
|   } | ||||
|   invokeSrcNameMap: {} | ||||
|   missingImplementations: { | ||||
|     actions: 'toastSuccess' | ||||
|     delays: never | ||||
|     guards: never | ||||
|     services: never | ||||
|   } | ||||
|   eventsCausingActions: { | ||||
|     persistSettings: | ||||
|       | 'Set Base Unit' | ||||
|       | 'Set Default Directory' | ||||
|       | 'Set Default Project Name' | ||||
|       | 'Set Onboarding Status' | ||||
|       | 'Set Text Wrapping' | ||||
|       | 'Set Theme' | ||||
|       | 'Set Unit System' | ||||
|       | 'Toggle Debug Panel' | ||||
|     setThemeClass: | ||||
|       | 'Set Base Unit' | ||||
|       | 'Set Default Directory' | ||||
|       | 'Set Default Project Name' | ||||
|       | 'Set Onboarding Status' | ||||
|       | 'Set Text Wrapping' | ||||
|       | 'Set Theme' | ||||
|       | 'Set Unit System' | ||||
|       | 'Toggle Debug Panel' | ||||
|       | 'xstate.init' | ||||
|     toastSuccess: | ||||
|       | 'Set Base Unit' | ||||
|       | 'Set Default Directory' | ||||
|       | 'Set Default Project Name' | ||||
|       | 'Set Text Wrapping' | ||||
|       | 'Set Theme' | ||||
|       | 'Set Unit System' | ||||
|       | 'Toggle Debug Panel' | ||||
|   } | ||||
|   eventsCausingDelays: {} | ||||
|   eventsCausingGuards: {} | ||||
|   eventsCausingServices: {} | ||||
|   matchesStates: 'idle' | ||||
|   tags: never | ||||
| } | ||||
| @ -1,139 +1,93 @@ | ||||
| import { FormEvent, useEffect } from 'react' | ||||
| import { removeDir, renameFile } from '@tauri-apps/api/fs' | ||||
| import { FormEvent, useCallback, useEffect, useState } from 'react' | ||||
| import { readDir, removeDir, renameFile } from '@tauri-apps/api/fs' | ||||
| import { | ||||
|   createNewProject, | ||||
|   getNextProjectIndex, | ||||
|   interpolateProjectNameWithIndex, | ||||
|   doesProjectNameNeedInterpolated, | ||||
|   getProjectsInDir, | ||||
|   isProjectDirectory, | ||||
|   PROJECT_ENTRYPOINT, | ||||
| } from '../lib/tauriFS' | ||||
| import { ActionButton } from '../components/ActionButton' | ||||
| import { faArrowDown, faPlus } from '@fortawesome/free-solid-svg-icons' | ||||
| import { | ||||
|   faArrowDown, | ||||
|   faArrowUp, | ||||
|   faCircleDot, | ||||
|   faPlus, | ||||
| } from '@fortawesome/free-solid-svg-icons' | ||||
| import { useStore } from '../useStore' | ||||
| import { toast } from 'react-hot-toast' | ||||
| import { AppHeader } from '../components/AppHeader' | ||||
| import ProjectCard from '../components/ProjectCard' | ||||
| import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' | ||||
| import { useLoaderData, useSearchParams } from 'react-router-dom' | ||||
| import { Link } from 'react-router-dom' | ||||
| import { ProjectWithEntryPointMetadata, HomeLoaderData } from '../Router' | ||||
| import Loading from '../components/Loading' | ||||
| import { useMachine } from '@xstate/react' | ||||
| import { homeCommandMeta, homeMachine } from '../machines/homeMachine' | ||||
| import { ContextFrom, EventFrom } from 'xstate' | ||||
| import { paths } from '../Router' | ||||
| import { | ||||
|   getNextSearchParams, | ||||
|   getSortFunction, | ||||
|   getSortIcon, | ||||
| } from '../lib/sorting' | ||||
| import useStateMachineCommands from '../hooks/useStateMachineCommands' | ||||
| import { useGlobalStateContext } from 'hooks/useGlobalStateContext' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { metadata } from 'tauri-plugin-fs-extra-api' | ||||
|  | ||||
| const DESC = ':desc' | ||||
|  | ||||
| // This route only opens in the Tauri desktop context for now, | ||||
| // as defined in Router.tsx, so we can use the Tauri APIs and types. | ||||
| const Home = () => { | ||||
|   const { commands, setCommandBarOpen } = useCommandsContext() | ||||
|   const navigate = useNavigate() | ||||
|   const { projects: loadedProjects } = useLoaderData() as HomeLoaderData | ||||
|   const { | ||||
|     settings: { | ||||
|       context: { defaultDirectory, defaultProjectName }, | ||||
|     }, | ||||
|   } = useGlobalStateContext() | ||||
|  | ||||
|   const [state, send] = useMachine(homeMachine, { | ||||
|     context: { | ||||
|       projects: loadedProjects, | ||||
|       defaultProjectName, | ||||
|       defaultDirectory, | ||||
|     }, | ||||
|     actions: { | ||||
|       navigateToProject: ( | ||||
|         context: ContextFrom<typeof homeMachine>, | ||||
|         event: EventFrom<typeof homeMachine> | ||||
|       ) => { | ||||
|         if (event.data && 'name' in event.data) { | ||||
|           setCommandBarOpen(false) | ||||
|           navigate( | ||||
|             `${paths.FILE}/${encodeURIComponent( | ||||
|               context.defaultDirectory + '/' + event.data.name | ||||
|             )}` | ||||
|           ) | ||||
|         } | ||||
|       }, | ||||
|       toastSuccess: (_, event) => toast.success((event.data || '') + ''), | ||||
|       toastError: (_, event) => toast.error((event.data || '') + ''), | ||||
|     }, | ||||
|     services: { | ||||
|       readProjects: async (context: ContextFrom<typeof homeMachine>) => | ||||
|         getProjectsInDir(context.defaultDirectory), | ||||
|       createProject: async ( | ||||
|         context: ContextFrom<typeof homeMachine>, | ||||
|         event: EventFrom<typeof homeMachine, 'Create project'> | ||||
|       ) => { | ||||
|         let name = | ||||
|           event.data && 'name' in event.data | ||||
|             ? event.data.name | ||||
|             : defaultProjectName | ||||
|         if (doesProjectNameNeedInterpolated(name)) { | ||||
|           const nextIndex = await getNextProjectIndex(name, projects) | ||||
|           name = interpolateProjectNameWithIndex(name, nextIndex) | ||||
|         } | ||||
|  | ||||
|         await createNewProject(context.defaultDirectory + '/' + name) | ||||
|         return `Successfully created "${name}"` | ||||
|       }, | ||||
|       renameProject: async ( | ||||
|         context: ContextFrom<typeof homeMachine>, | ||||
|         event: EventFrom<typeof homeMachine, 'Rename project'> | ||||
|       ) => { | ||||
|         const { oldName, newName } = event.data | ||||
|         let name = newName ? newName : context.defaultProjectName | ||||
|         if (doesProjectNameNeedInterpolated(name)) { | ||||
|           const nextIndex = await getNextProjectIndex(name, projects) | ||||
|           name = interpolateProjectNameWithIndex(name, nextIndex) | ||||
|         } | ||||
|  | ||||
|         await renameFile( | ||||
|           context.defaultDirectory + '/' + oldName, | ||||
|           context.defaultDirectory + '/' + name | ||||
|         ) | ||||
|         return `Successfully renamed "${oldName}" to "${name}"` | ||||
|       }, | ||||
|       deleteProject: async ( | ||||
|         context: ContextFrom<typeof homeMachine>, | ||||
|         event: EventFrom<typeof homeMachine, 'Delete project'> | ||||
|       ) => { | ||||
|         await removeDir(context.defaultDirectory + '/' + event.data.name, { | ||||
|           recursive: true, | ||||
|         }) | ||||
|         return `Successfully deleted "${event.data.name}"` | ||||
|       }, | ||||
|     }, | ||||
|     guards: { | ||||
|       'Has at least 1 project': (_, event: EventFrom<typeof homeMachine>) => { | ||||
|         if (event.type !== 'done.invoke.read-projects') return false | ||||
|         return event?.data?.length ? event.data?.length >= 1 : false | ||||
|       }, | ||||
|     }, | ||||
|   }) | ||||
|   const { projects } = state.context | ||||
|   const [searchParams, setSearchParams] = useSearchParams() | ||||
|   const sort = searchParams.get('sort_by') ?? 'modified:desc' | ||||
|   const { projects: loadedProjects } = useLoaderData() as HomeLoaderData | ||||
|   const [isLoading, setIsLoading] = useState(true) | ||||
|   const [projects, setProjects] = useState(loadedProjects || []) | ||||
|   const { defaultDir, defaultProjectName } = useStore((s) => ({ | ||||
|     defaultDir: s.defaultDir, | ||||
|     defaultProjectName: s.defaultProjectName, | ||||
|   })) | ||||
|  | ||||
|   const isSortByModified = sort?.includes('modified') || !sort || sort === null | ||||
|   const modifiedSelected = sort?.includes('modified') || !sort || sort === null | ||||
|  | ||||
|   useStateMachineCommands<typeof homeMachine>({ | ||||
|     commands, | ||||
|     send, | ||||
|     state, | ||||
|     commandBarMeta: homeCommandMeta, | ||||
|     owner: 'home', | ||||
|   }) | ||||
|   const refreshProjects = useCallback( | ||||
|     async (projectDir = defaultDir) => { | ||||
|       const readProjects = ( | ||||
|         await readDir(projectDir.dir, { | ||||
|           recursive: true, | ||||
|         }) | ||||
|       ).filter(isProjectDirectory) | ||||
|  | ||||
|       const projectsWithMetadata = await Promise.all( | ||||
|         readProjects.map(async (p) => ({ | ||||
|           entrypoint_metadata: await metadata( | ||||
|             p.path + '/' + PROJECT_ENTRYPOINT | ||||
|           ), | ||||
|           ...p, | ||||
|         })) | ||||
|       ) | ||||
|  | ||||
|       setProjects(projectsWithMetadata) | ||||
|     }, | ||||
|     [defaultDir, setProjects] | ||||
|   ) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     send({ type: 'assign', data: { defaultProjectName, defaultDirectory } }) | ||||
|   }, [defaultDirectory, defaultProjectName, send]) | ||||
|     refreshProjects(defaultDir).then(() => { | ||||
|       setIsLoading(false) | ||||
|     }) | ||||
|   }, [setIsLoading, refreshProjects, defaultDir]) | ||||
|  | ||||
|   async function handleNewProject() { | ||||
|     let projectName = defaultProjectName | ||||
|     if (doesProjectNameNeedInterpolated(projectName)) { | ||||
|       const nextIndex = await getNextProjectIndex(defaultProjectName, projects) | ||||
|       projectName = interpolateProjectNameWithIndex( | ||||
|         defaultProjectName, | ||||
|         nextIndex | ||||
|       ) | ||||
|     } | ||||
|  | ||||
|     await createNewProject(defaultDir.dir + '/' + projectName).catch((err) => { | ||||
|       console.error('Error creating project:', err) | ||||
|       toast.error('Error creating project') | ||||
|     }) | ||||
|  | ||||
|     await refreshProjects() | ||||
|     toast.success('Project created') | ||||
|   } | ||||
|  | ||||
|   async function handleRenameProject( | ||||
|     e: FormEvent<HTMLFormElement>, | ||||
| @ -142,14 +96,85 @@ const Home = () => { | ||||
|     const { newProjectName } = Object.fromEntries( | ||||
|       new FormData(e.target as HTMLFormElement) | ||||
|     ) | ||||
|     if (newProjectName && project.name && newProjectName !== project.name) { | ||||
|       const dir = project.path?.slice(0, project.path?.lastIndexOf('/')) | ||||
|       await renameFile(project.path, dir + '/' + newProjectName).catch( | ||||
|         (err) => { | ||||
|           console.error('Error renaming project:', err) | ||||
|           toast.error('Error renaming project') | ||||
|         } | ||||
|       ) | ||||
|  | ||||
|     send('Rename project', { | ||||
|       data: { oldName: project.name, newName: newProjectName }, | ||||
|     }) | ||||
|       await refreshProjects() | ||||
|       toast.success('Project renamed') | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async function handleDeleteProject(project: ProjectWithEntryPointMetadata) { | ||||
|     send('Delete project', { data: { name: project.name || '' } }) | ||||
|     if (project.path) { | ||||
|       await removeDir(project.path, { recursive: true }).catch((err) => { | ||||
|         console.error('Error deleting project:', err) | ||||
|         toast.error('Error deleting project') | ||||
|       }) | ||||
|  | ||||
|       await refreshProjects() | ||||
|       toast.success('Project deleted') | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function getSortIcon(sortBy: string) { | ||||
|     if (sort === sortBy) { | ||||
|       return faArrowUp | ||||
|     } else if (sort === sortBy + DESC) { | ||||
|       return faArrowDown | ||||
|     } | ||||
|     return faCircleDot | ||||
|   } | ||||
|  | ||||
|   function getNextSearchParams(sortBy: string) { | ||||
|     if (sort === null || !sort) | ||||
|       return { sort_by: sortBy + (sortBy !== 'modified' ? DESC : '') } | ||||
|     if (sort.includes(sortBy) && !sort.includes(DESC)) return { sort_by: '' } | ||||
|     return { | ||||
|       sort_by: sortBy + (sort.includes(DESC) ? '' : DESC), | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function getSortFunction(sortBy: string) { | ||||
|     const sortByName = ( | ||||
|       a: ProjectWithEntryPointMetadata, | ||||
|       b: ProjectWithEntryPointMetadata | ||||
|     ) => { | ||||
|       if (a.name && b.name) { | ||||
|         return sortBy.includes('desc') | ||||
|           ? a.name.localeCompare(b.name) | ||||
|           : b.name.localeCompare(a.name) | ||||
|       } | ||||
|       return 0 | ||||
|     } | ||||
|  | ||||
|     const sortByModified = ( | ||||
|       a: ProjectWithEntryPointMetadata, | ||||
|       b: ProjectWithEntryPointMetadata | ||||
|     ) => { | ||||
|       if ( | ||||
|         a.entrypoint_metadata?.modifiedAt && | ||||
|         b.entrypoint_metadata?.modifiedAt | ||||
|       ) { | ||||
|         return !sortBy || sortBy.includes('desc') | ||||
|           ? b.entrypoint_metadata.modifiedAt.getTime() - | ||||
|               a.entrypoint_metadata.modifiedAt.getTime() | ||||
|           : a.entrypoint_metadata.modifiedAt.getTime() - | ||||
|               b.entrypoint_metadata.modifiedAt.getTime() | ||||
|       } | ||||
|       return 0 | ||||
|     } | ||||
|  | ||||
|     if (sortBy?.includes('name')) { | ||||
|       return sortByName | ||||
|     } else { | ||||
|       return sortByModified | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
| @ -166,9 +191,9 @@ const Home = () => { | ||||
|                   ? 'text-chalkboard-80 dark:text-chalkboard-40' | ||||
|                   : '' | ||||
|               } | ||||
|               onClick={() => setSearchParams(getNextSearchParams(sort, 'name'))} | ||||
|               onClick={() => setSearchParams(getNextSearchParams('name'))} | ||||
|               icon={{ | ||||
|                 icon: getSortIcon(sort, 'name'), | ||||
|                 icon: getSortIcon('name'), | ||||
|                 bgClassName: !sort?.includes('name') | ||||
|                   ? 'bg-liquid-50 dark:bg-liquid-70' | ||||
|                   : '', | ||||
| @ -182,19 +207,17 @@ const Home = () => { | ||||
|             <ActionButton | ||||
|               Element="button" | ||||
|               className={ | ||||
|                 !isSortByModified | ||||
|                 !modifiedSelected | ||||
|                   ? 'text-chalkboard-80 dark:text-chalkboard-40' | ||||
|                   : '' | ||||
|               } | ||||
|               onClick={() => | ||||
|                 setSearchParams(getNextSearchParams(sort, 'modified')) | ||||
|               } | ||||
|               onClick={() => setSearchParams(getNextSearchParams('modified'))} | ||||
|               icon={{ | ||||
|                 icon: sort ? getSortIcon(sort, 'modified') : faArrowDown, | ||||
|                 bgClassName: !isSortByModified | ||||
|                 icon: sort ? getSortIcon('modified') : faArrowDown, | ||||
|                 bgClassName: !modifiedSelected | ||||
|                   ? 'bg-liquid-50 dark:bg-liquid-70' | ||||
|                   : '', | ||||
|                 iconClassName: !isSortByModified | ||||
|                 iconClassName: !modifiedSelected | ||||
|                   ? 'text-liquid-80 dark:text-liquid-30' | ||||
|                   : '', | ||||
|               }} | ||||
| @ -207,11 +230,11 @@ const Home = () => { | ||||
|           <p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30"> | ||||
|             Are being saved at{' '} | ||||
|             <code className="text-liquid-80 dark:text-liquid-30"> | ||||
|               {defaultDirectory} | ||||
|               {defaultDir.dir} | ||||
|             </code> | ||||
|             , which you can change in your <Link to="settings">Settings</Link>. | ||||
|           </p> | ||||
|           {state.matches('Reading projects') ? ( | ||||
|           {isLoading ? ( | ||||
|             <Loading>Loading your Projects...</Loading> | ||||
|           ) : ( | ||||
|             <> | ||||
| @ -233,7 +256,7 @@ const Home = () => { | ||||
|               )} | ||||
|               <ActionButton | ||||
|                 Element="button" | ||||
|                 onClick={() => send('Create project')} | ||||
|                 onClick={handleNewProject} | ||||
|                 icon={{ icon: faPlus }} | ||||
|               > | ||||
|                 New file | ||||
|  | ||||
| @ -20,25 +20,9 @@ export default function Units() { | ||||
|       > | ||||
|         <h1 className="text-2xl font-bold">Camera</h1> | ||||
|         <p className="mt-6"> | ||||
|           Moving the camera is easy! The controls are as you might expect: | ||||
|         </p> | ||||
|         <ul className="list-disc list-outside ms-8 mb-4"> | ||||
|           <li>Click and drag anywhere in the scene to rotate the camera</li> | ||||
|           <li> | ||||
|             Hold down the <kbd>Shift</kbd> key while clicking and dragging to | ||||
|             pan the camera | ||||
|           </li> | ||||
|           <li> | ||||
|             Hold down the <kbd>Ctrl</kbd> key while dragging to zoom. You can | ||||
|             also use the scroll wheel to zoom in and out. | ||||
|           </li> | ||||
|         </ul> | ||||
|         <p> | ||||
|           What you're seeing here is just a video, and your interactions are | ||||
|           being sent to our Geometry Engine API, which sends back video frames | ||||
|           in real time. How cool is that? It means that you can use KittyCAD | ||||
|           Modeling App (or whatever you want to build) on any device, even a | ||||
|           cheap laptop with no graphics card! | ||||
|           Moving the camera is easy. Just click and drag anywhere in the scene | ||||
|           to rotate the camera, or hold down the <kbd>Ctrl</kbd> key and drag to | ||||
|           pan the camera. | ||||
|         </p> | ||||
|         <div className="flex justify-between mt-6"> | ||||
|           <ActionButton | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	