Compare commits
40 Commits
Author | SHA1 | Date | |
---|---|---|---|
33822b5a19 | |||
a2a4daebe3 | |||
a17ede50bd | |||
2d452f80d1 | |||
cf39c08428 | |||
2f25564fcc | |||
fd2ed8acbd | |||
5f3e1cfb6c | |||
ee767afc3f | |||
8071eb6f8a | |||
11f789e980 | |||
3f82522fe9 | |||
c5cb0e2fd4 | |||
9e2a94fcd9 | |||
8a3e8d331d | |||
1be9b2612c | |||
7c9aaeafa2 | |||
46c0078885 | |||
87ebf3b1d6 | |||
45238f8196 | |||
44f3a12fbe | |||
61acada2a0 | |||
c68fbbd89d | |||
97a0b6a543 | |||
3bccae492d | |||
0120a89d9c | |||
3da6fc3b7e | |||
34dd15ead7 | |||
b3d441e9d6 | |||
4b3dc3756c | |||
10027b98b5 | |||
da17dad63b | |||
fba6c422a8 | |||
0b4b93932d | |||
f42900ec46 | |||
eeca624ba6 | |||
84d08bad16 | |||
1181f33e9d | |||
797e200d08 | |||
d2f231066b |
@ -1,7 +1,6 @@
|
||||
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_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=5000
|
||||
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=0
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||
VITE_KC_SENTRY_DSN=
|
||||
|
@ -3,5 +3,4 @@ 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
|
||||
|
11
.github/workflows/cargo-clippy.yml
vendored
11
.github/workflows/cargo-clippy.yml
vendored
@ -40,6 +40,17 @@ jobs:
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.6.1
|
||||
|
||||
- name: Install ffmpeg
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install \
|
||||
ffmpeg \
|
||||
libavformat-dev \
|
||||
libavutil-dev \
|
||||
libclang-dev \
|
||||
libswscale-dev \
|
||||
--no-install-recommends
|
||||
|
||||
- name: Run clippy
|
||||
run: |
|
||||
cd "${{ matrix.dir }}"
|
||||
|
10
.github/workflows/cargo-test.yml
vendored
10
.github/workflows/cargo-test.yml
vendored
@ -41,6 +41,16 @@ jobs:
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.6.1
|
||||
- name: Install ffmpeg
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install \
|
||||
ffmpeg \
|
||||
libavformat-dev \
|
||||
libavutil-dev \
|
||||
libclang-dev \
|
||||
libswscale-dev \
|
||||
--no-install-recommends
|
||||
- name: cargo test
|
||||
shell: bash
|
||||
run: |-
|
||||
|
105
.github/workflows/ci.yml
vendored
105
.github/workflows/ci.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: CI
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@ -13,17 +13,31 @@ 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
|
||||
@ -36,12 +50,15 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- run: yarn build:wasm
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./src/wasm-lib"
|
||||
|
||||
- run: yarn tsc
|
||||
- run: yarn build:wasm
|
||||
|
||||
- run: yarn simpleserver:ci
|
||||
|
||||
@ -49,14 +66,12 @@ jobs:
|
||||
|
||||
- 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]
|
||||
needs: [check-format, build-test-web, check-types]
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@ -87,6 +102,10 @@ jobs:
|
||||
with:
|
||||
workspaces: './src-tauri -> target'
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./src/wasm-lib"
|
||||
|
||||
- name: wasm prep
|
||||
shell: bash
|
||||
run: |
|
||||
@ -110,15 +129,22 @@ 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: src-tauri/target/release/bundle/*/*
|
||||
path: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin/release/bundle/*/*' || 'src-tauri/target/release/bundle/*/*' }}
|
||||
|
||||
|
||||
publish-apps-release:
|
||||
@ -127,20 +153,23 @@ jobs:
|
||||
needs: [build-test-web, build-apps]
|
||||
env:
|
||||
VERSION_NO_V: ${{ needs.build-test-web.outputs.version }}
|
||||
PUB_DATE: ${{ github.event.release.created_at }}
|
||||
NOTES: ${{ github.event.release.body }}
|
||||
steps:
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
|
||||
- name: Generate the update static endpoint
|
||||
run: |
|
||||
ls -l artifact
|
||||
ls -l artifact/*
|
||||
ls -l artifact/*/*itty*
|
||||
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`
|
||||
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
|
||||
jq --null-input \
|
||||
--arg version "v${VERSION_NO_V}" \
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_sig "$DARWIN_SIG" \
|
||||
--arg darwin_url "$RELEASE_DIR/macos/KittyCAD%20Modeling.app.tar.gz" \
|
||||
--arg linux_sig "$LINUX_SIG" \
|
||||
@ -149,11 +178,17 @@ jobs:
|
||||
--arg windows_url "$RELEASE_DIR/nsis/KittyCAD%20Modeling_${VERSION_NO_V}_x64-setup.nsis.zip" \
|
||||
'{
|
||||
"version": $version,
|
||||
"pub_date": $pub_date,
|
||||
"notes": $notes,
|
||||
"platforms": {
|
||||
"darwin-x86_64": {
|
||||
"signature": $darwin_sig,
|
||||
"url": $darwin_url
|
||||
},
|
||||
"darwin-aarch64": {
|
||||
"signature": $darwin_sig,
|
||||
"url": $darwin_url
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"signature": $linux_sig,
|
||||
"url": $linux_url
|
||||
@ -166,6 +201,34 @@ jobs:
|
||||
}' > last_update.json
|
||||
cat last_update.json
|
||||
|
||||
- name: Generate the download static endpoint
|
||||
run: |
|
||||
RELEASE_DIR=https://dl.kittycad.io/releases/modeling-app/v${VERSION_NO_V}
|
||||
jq --null-input \
|
||||
--arg version "v${VERSION_NO_V}" \
|
||||
--arg pub_date "${PUB_DATE}" \
|
||||
--arg notes "${NOTES}" \
|
||||
--arg darwin_url "$RELEASE_DIR/dmg/KittyCAD%20Modeling_${VERSION_NO_V}_universal.dmg" \
|
||||
--arg linux_url "$RELEASE_DIR/appimage/kittycad-modeling_${VERSION_NO_V}_amd64.AppImage" \
|
||||
--arg windows_url "$RELEASE_DIR/msi/KittyCAD%20Modeling_${VERSION_NO_V}_x64_en-US.msi.zip" \
|
||||
'{
|
||||
"version": $version,
|
||||
"pub_date": $pub_date,
|
||||
"notes": $notes,
|
||||
"platforms": {
|
||||
"dmg-universal": {
|
||||
"url": $darwin_url
|
||||
},
|
||||
"appimage-x86_64": {
|
||||
"url": $linux_url
|
||||
},
|
||||
"msi-x86_64": {
|
||||
"url": $windows_url
|
||||
}
|
||||
}
|
||||
}' > last_download.json
|
||||
cat last_download.json
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: 'google-github-actions/auth@v1.1.1'
|
||||
with:
|
||||
@ -175,22 +238,28 @@ 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: '*/*'
|
||||
glob: '*/*itty*'
|
||||
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 download endpoint to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v1.0.3
|
||||
with:
|
||||
path: last_download.json
|
||||
destination: dl.kittycad.io/releases/modeling-app
|
||||
|
||||
- name: Upload release files to Github
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: artifact/*/*
|
||||
files: artifact/*/*itty*
|
||||
|
@ -5,3 +5,5 @@ coverage
|
||||
# Ignore Rust projects:
|
||||
*.rs
|
||||
target
|
||||
src/wasm-lib/pkg
|
||||
src/wasm-lib/kcl/bindings
|
||||
|
21
README.md
21
README.md
@ -86,3 +86,24 @@ 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).
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.3.1",
|
||||
"version": "0.6.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.9.0",
|
||||
@ -10,7 +10,7 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.13",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "^0.0.35",
|
||||
"@kittycad/lib": "^0.0.37",
|
||||
"@lezer/javascript": "^1.4.7",
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
@ -70,7 +70,7 @@
|
||||
"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",
|
||||
"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\"",
|
||||
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/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",
|
||||
"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"
|
||||
|
@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "kittycad-modeling",
|
||||
"version": "0.3.1"
|
||||
"version": "0.6.1"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
365
src/App.tsx
365
src/App.tsx
@ -2,7 +2,6 @@ import {
|
||||
useRef,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
MouseEventHandler,
|
||||
} from 'react'
|
||||
@ -10,30 +9,20 @@ import { DebugPanel } from './components/DebugPanel'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { asyncParser } from './lang/abstractSyntaxTree'
|
||||
import { _executor } from './lang/executor'
|
||||
import CodeMirror from '@uiw/react-codemirror'
|
||||
import { linter, lintGutter } from '@codemirror/lint'
|
||||
import { ViewUpdate, EditorView } 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, 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,
|
||||
} from './lang/std/engineConnection'
|
||||
import { isOverlap, throttle } from './lib/utils'
|
||||
import { throttle } from './lib/utils'
|
||||
import { AppHeader } from './components/AppHeader'
|
||||
import { KCLError, kclErrToDiagnostic } from './lang/errors'
|
||||
import { KCLError } from './lang/errors'
|
||||
import { Resizable } from 're-resizable'
|
||||
import {
|
||||
faCode,
|
||||
@ -41,96 +30,77 @@ import {
|
||||
faSquareRootVariable,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { TEST } from './env'
|
||||
import { getNormalisedCoordinates } from './lib/utils'
|
||||
import { Themes, getSystemTheme } from './lib/theme'
|
||||
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 { useLoaderData } from 'react-router-dom'
|
||||
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 { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { CodeMenu } from 'components/CodeMenu'
|
||||
import { TextEditor } from 'components/TextEditor'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
|
||||
export function App() {
|
||||
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
||||
const pathParams = useParams()
|
||||
|
||||
const streamRef = useRef<HTMLDivElement>(null)
|
||||
useHotKeyListener()
|
||||
const {
|
||||
editorView,
|
||||
setEditorView,
|
||||
setSelectionRanges,
|
||||
selectionRanges,
|
||||
addLog,
|
||||
addKCLError,
|
||||
code,
|
||||
setCode,
|
||||
setAst,
|
||||
setError,
|
||||
setProgramMemory,
|
||||
resetLogs,
|
||||
resetKCLErrors,
|
||||
selectionRangeTypeMap,
|
||||
setArtifactMap,
|
||||
engineCommandManager,
|
||||
setEngineCommandManager,
|
||||
highlightRange,
|
||||
setHighlightRange,
|
||||
setCursor2,
|
||||
sourceRangeMap,
|
||||
setMediaStream,
|
||||
setIsStreamReady,
|
||||
isStreamReady,
|
||||
isLSPServerReady,
|
||||
setIsLSPServerReady,
|
||||
isMouseDownInStream,
|
||||
formatCode,
|
||||
buttonDownInStream,
|
||||
openPanes,
|
||||
setOpenPanes,
|
||||
didDragInStream,
|
||||
setDidDragInStream,
|
||||
setStreamDimensions,
|
||||
streamDimensions,
|
||||
setIsExecuting,
|
||||
defferedCode,
|
||||
guiMode,
|
||||
} = useStore((s) => ({
|
||||
editorView: s.editorView,
|
||||
setEditorView: s.setEditorView,
|
||||
setSelectionRanges: s.setSelectionRanges,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setGuiMode: s.setGuiMode,
|
||||
guiMode: s.guiMode,
|
||||
addLog: s.addLog,
|
||||
code: s.code,
|
||||
defferedCode: s.defferedCode,
|
||||
setCode: s.setCode,
|
||||
setAst: s.setAst,
|
||||
setError: s.setError,
|
||||
setProgramMemory: s.setProgramMemory,
|
||||
resetLogs: s.resetLogs,
|
||||
resetKCLErrors: s.resetKCLErrors,
|
||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||
setArtifactMap: s.setArtifactNSourceRangeMaps,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
setEngineCommandManager: s.setEngineCommandManager,
|
||||
highlightRange: s.highlightRange,
|
||||
setHighlightRange: s.setHighlightRange,
|
||||
isShiftDown: s.isShiftDown,
|
||||
setCursor: s.setCursor,
|
||||
setCursor2: s.setCursor2,
|
||||
sourceRangeMap: s.sourceRangeMap,
|
||||
setMediaStream: s.setMediaStream,
|
||||
isStreamReady: s.isStreamReady,
|
||||
setIsStreamReady: s.setIsStreamReady,
|
||||
isLSPServerReady: s.isLSPServerReady,
|
||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
||||
isMouseDownInStream: s.isMouseDownInStream,
|
||||
formatCode: s.formatCode,
|
||||
buttonDownInStream: s.buttonDownInStream,
|
||||
addKCLError: s.addKCLError,
|
||||
openPanes: s.openPanes,
|
||||
setOpenPanes: s.setOpenPanes,
|
||||
didDragInStream: s.didDragInStream,
|
||||
setDidDragInStream: s.setDidDragInStream,
|
||||
setStreamDimensions: s.setStreamDimensions,
|
||||
streamDimensions: s.streamDimensions,
|
||||
setIsExecuting: s.setIsExecuting,
|
||||
}))
|
||||
|
||||
const {
|
||||
@ -138,7 +108,7 @@ export function App() {
|
||||
context: { token },
|
||||
},
|
||||
settings: {
|
||||
context: { showDebugPanel, theme, onboardingStatus },
|
||||
context: { showDebugPanel, onboardingStatus, cameraControls, theme },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
|
||||
@ -179,80 +149,6 @@ export function App() {
|
||||
}
|
||||
}, [loadedCode, setCode])
|
||||
|
||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
||||
setCode(value)
|
||||
if (isTauri() && pathParams.id) {
|
||||
// Save the file to disk
|
||||
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
|
||||
(err) => {
|
||||
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
||||
console.error('error saving file', err)
|
||||
toast.error('Error saving file, please check file permissions')
|
||||
}
|
||||
)
|
||||
}
|
||||
if (editorView) {
|
||||
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
|
||||
}
|
||||
} //, []);
|
||||
const onUpdate = (viewUpdate: ViewUpdate) => {
|
||||
if (!editorView) {
|
||||
setEditorView(viewUpdate.view)
|
||||
}
|
||||
const ranges = viewUpdate.state.selection.ranges
|
||||
|
||||
const isChange =
|
||||
ranges.length !== selectionRanges.codeBasedSelections.length ||
|
||||
ranges.some(({ from, to }, i) => {
|
||||
return (
|
||||
from !== selectionRanges.codeBasedSelections[i].range[0] ||
|
||||
to !== selectionRanges.codeBasedSelections[i].range[1]
|
||||
)
|
||||
})
|
||||
|
||||
if (!isChange) return
|
||||
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
|
||||
({ from, to }) => {
|
||||
if (selectionRangeTypeMap[to]) {
|
||||
return {
|
||||
type: selectionRangeTypeMap[to],
|
||||
range: [from, to],
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'default',
|
||||
range: [from, to],
|
||||
}
|
||||
}
|
||||
)
|
||||
const idBasedSelections = codeBasedSelections
|
||||
.map(({ type, range }) => {
|
||||
const hasOverlap = Object.entries(sourceRangeMap).filter(
|
||||
([_, sourceRange]) => {
|
||||
return isOverlap(sourceRange, range)
|
||||
}
|
||||
)
|
||||
if (hasOverlap.length) {
|
||||
return {
|
||||
type,
|
||||
id: hasOverlap[0][0],
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as any
|
||||
|
||||
engineCommandManager?.cusorsSelected({
|
||||
otherSelections: [],
|
||||
idBasedSelections,
|
||||
})
|
||||
|
||||
setSelectionRanges({
|
||||
otherSelections: [],
|
||||
codeBasedSelections,
|
||||
})
|
||||
}
|
||||
const streamWidth = streamRef?.current?.offsetWidth
|
||||
const streamHeight = streamRef?.current?.offsetHeight
|
||||
|
||||
@ -286,57 +182,77 @@ export function App() {
|
||||
let unsubFn: any[] = []
|
||||
const asyncWrap = async () => {
|
||||
try {
|
||||
if (!code) {
|
||||
setAst(null)
|
||||
if (!defferedCode) {
|
||||
setAst({
|
||||
start: 0,
|
||||
end: 0,
|
||||
body: [],
|
||||
nonCodeMeta: {
|
||||
noneCodeNodes: {},
|
||||
start: null,
|
||||
},
|
||||
})
|
||||
setProgramMemory({ root: {}, return: null })
|
||||
engineCommandManager.endSession()
|
||||
engineCommandManager.startNewSession()
|
||||
return
|
||||
}
|
||||
const _ast = await asyncParser(code)
|
||||
const _ast = await asyncParser(defferedCode)
|
||||
setAst(_ast)
|
||||
resetLogs()
|
||||
resetKCLErrors()
|
||||
engineCommandManager.endSession()
|
||||
engineCommandManager.startNewSession()
|
||||
setIsExecuting(true)
|
||||
const programMemory = await _executor(
|
||||
_ast,
|
||||
{
|
||||
root: {
|
||||
_0: {
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: 0,
|
||||
__meta: [],
|
||||
},
|
||||
_90: {
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: 90,
|
||||
__meta: [],
|
||||
},
|
||||
_180: {
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: 180,
|
||||
__meta: [],
|
||||
},
|
||||
_270: {
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: 270,
|
||||
__meta: [],
|
||||
},
|
||||
},
|
||||
return: null,
|
||||
},
|
||||
engineCommandManager
|
||||
)
|
||||
|
||||
const { artifactMap, sourceRangeMap } =
|
||||
await engineCommandManager.waitForAllCommands()
|
||||
setIsExecuting(false)
|
||||
if (programMemory !== undefined) {
|
||||
setProgramMemory(programMemory)
|
||||
}
|
||||
|
||||
setArtifactMap({ artifactMap, sourceRangeMap })
|
||||
const unSubHover = engineCommandManager.subscribeToUnreliable({
|
||||
event: 'highlight_set_entity',
|
||||
callback: ({ data }) => {
|
||||
if (!data?.entity_id) {
|
||||
setHighlightRange([0, 0])
|
||||
} else {
|
||||
if (data?.entity_id) {
|
||||
const sourceRange = sourceRangeMap[data.entity_id]
|
||||
setHighlightRange(sourceRange)
|
||||
} else if (
|
||||
!highlightRange ||
|
||||
(highlightRange[0] !== 0 && highlightRange[1] !== 0)
|
||||
) {
|
||||
setHighlightRange([0, 0])
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -352,12 +268,10 @@ export function App() {
|
||||
},
|
||||
})
|
||||
unsubFn.push(unSubHover, unSubClick)
|
||||
if (programMemory !== undefined) {
|
||||
setProgramMemory(programMemory)
|
||||
}
|
||||
|
||||
setError()
|
||||
} catch (e: any) {
|
||||
setIsExecuting(false)
|
||||
if (e instanceof KCLError) {
|
||||
addKCLError(e)
|
||||
} else {
|
||||
@ -371,36 +285,73 @@ export function App() {
|
||||
return () => {
|
||||
unsubFn.forEach((fn) => fn())
|
||||
}
|
||||
}, [code, isStreamReady, engineCommandManager])
|
||||
}, [defferedCode, isStreamReady, engineCommandManager])
|
||||
|
||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||
engineCommandManager?.sendSceneCommand(message)
|
||||
}, 16)
|
||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = ({
|
||||
clientX,
|
||||
clientY,
|
||||
ctrlKey,
|
||||
shiftKey,
|
||||
currentTarget,
|
||||
nativeEvent,
|
||||
}) => {
|
||||
nativeEvent.preventDefault()
|
||||
if (isMouseDownInStream) {
|
||||
setDidDragInStream(true)
|
||||
}
|
||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
e.nativeEvent.preventDefault()
|
||||
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX,
|
||||
clientY,
|
||||
el: currentTarget,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
el: e.currentTarget,
|
||||
...streamDimensions,
|
||||
})
|
||||
|
||||
const interaction = ctrlKey ? 'zoom' : shiftKey ? 'pan' : 'rotate'
|
||||
|
||||
const newCmdId = uuidv4()
|
||||
if (buttonDownInStream === undefined) {
|
||||
if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any)
|
||||
) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: newCmdId,
|
||||
cmd: {
|
||||
type: 'mouse_move',
|
||||
window: { x, y },
|
||||
},
|
||||
})
|
||||
} else {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'highlight_set_entity',
|
||||
selected_at_window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: newCmdId,
|
||||
cmd: {
|
||||
type: 'handle_mouse_drag_move',
|
||||
window: { x, y },
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
||||
let interaction: CameraDragInteractionType_type
|
||||
|
||||
const eWithButton = { ...e, button: buttonDownInStream }
|
||||
|
||||
if (interactionGuards.pan.callback(eWithButton)) {
|
||||
interaction = 'pan'
|
||||
} else if (interactionGuards.rotate.callback(eWithButton)) {
|
||||
interaction = 'rotate'
|
||||
} else if (interactionGuards.zoom.dragCallback(eWithButton)) {
|
||||
interaction = 'zoom'
|
||||
} else {
|
||||
console.log('none')
|
||||
return
|
||||
}
|
||||
|
||||
if (isMouseDownInStream) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
@ -410,68 +361,9 @@ export function App() {
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
} else {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'highlight_set_entity',
|
||||
selected_at_window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const extraExtensions = useMemo(() => {
|
||||
if (TEST) return []
|
||||
return [
|
||||
lintGutter(),
|
||||
linter((_view) => {
|
||||
return kclErrToDiagnostic(useStore.getState().kclErrors)
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
]
|
||||
}, [])
|
||||
|
||||
// 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])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-screen overflow-hidden relative flex flex-col cursor-pointer select-none"
|
||||
@ -482,7 +374,7 @@ export function App() {
|
||||
className={
|
||||
'transition-opacity transition-duration-75 ' +
|
||||
paneOpacity +
|
||||
(isMouseDownInStream ? ' pointer-events-none' : '')
|
||||
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||
}
|
||||
project={project}
|
||||
enableMenu={true}
|
||||
@ -491,17 +383,17 @@ export function App() {
|
||||
<Resizable
|
||||
className={
|
||||
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
|
||||
(isMouseDownInStream || onboardingStatus === 'camera'
|
||||
(buttonDownInStream || onboardingStatus === 'camera'
|
||||
? ' pointer-events-none '
|
||||
: ' ') +
|
||||
paneOpacity
|
||||
}
|
||||
defaultSize={{
|
||||
width: '400px',
|
||||
width: '550px',
|
||||
height: 'auto',
|
||||
}}
|
||||
minWidth={200}
|
||||
maxWidth={600}
|
||||
maxWidth={800}
|
||||
minHeight={'auto'}
|
||||
maxHeight={'auto'}
|
||||
handleClasses={{
|
||||
@ -513,36 +405,11 @@ export function App() {
|
||||
<CollapsiblePanel
|
||||
title="Code"
|
||||
icon={faCode}
|
||||
className="open:!mb-2 overflow-x-hidden"
|
||||
className="open:!mb-2"
|
||||
open={openPanes.includes('code')}
|
||||
menu={<CodeMenu />}
|
||||
>
|
||||
<div className="px-2 py-1">
|
||||
<button
|
||||
// disabled={!shouldFormat}
|
||||
onClick={formatCode}
|
||||
// className={`${!shouldFormat && 'text-gray-300'}`}
|
||||
>
|
||||
format
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="code-mirror-override"
|
||||
className="overflow-x-hidden h-full"
|
||||
>
|
||||
<CodeMirror
|
||||
className="h-full overflow-hidden-x"
|
||||
value={code}
|
||||
extensions={
|
||||
kclLSP
|
||||
? [kclLSP, lineHighlightField, ...extraExtensions]
|
||||
: [lineHighlightField, ...extraExtensions]
|
||||
}
|
||||
onChange={onChange}
|
||||
onUpdate={onUpdate}
|
||||
theme={editorTheme}
|
||||
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
||||
/>
|
||||
</div>
|
||||
<TextEditor theme={editorTheme} />
|
||||
</CollapsiblePanel>
|
||||
<section className="flex flex-col">
|
||||
<MemoryPanel
|
||||
@ -573,7 +440,7 @@ export function App() {
|
||||
className={
|
||||
'transition-opacity transition-duration-75 ' +
|
||||
paneOpacity +
|
||||
(isMouseDownInStream ? ' pointer-events-none' : '')
|
||||
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||
}
|
||||
open={openPanes.includes('debug')}
|
||||
/>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useStore, toolTips } from './useStore'
|
||||
import { useStore, toolTips, Selections } from './useStore'
|
||||
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
|
||||
import { getNodePathFromSourceRange } from './lang/queryAst'
|
||||
import { HorzVert } from './components/Toolbar/HorzVert'
|
||||
@ -8,7 +8,6 @@ import { EqualAngle } from './components/Toolbar/EqualAngle'
|
||||
import { Intersect } from './components/Toolbar/Intersect'
|
||||
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
|
||||
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'
|
||||
@ -16,6 +15,8 @@ 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'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useAppMode } from 'hooks/useAppMode'
|
||||
|
||||
export const Toolbar = () => {
|
||||
const {
|
||||
@ -25,6 +26,7 @@ export const Toolbar = () => {
|
||||
ast,
|
||||
updateAst,
|
||||
programMemory,
|
||||
engineCommandManager,
|
||||
} = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
setGuiMode: s.setGuiMode,
|
||||
@ -32,7 +34,9 @@ export const Toolbar = () => {
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
programMemory: s.programMemory,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
}))
|
||||
useAppMode()
|
||||
|
||||
useEffect(() => {
|
||||
console.log('guiMode', guiMode)
|
||||
@ -40,7 +44,7 @@ export const Toolbar = () => {
|
||||
|
||||
function ToolbarButtons() {
|
||||
return (
|
||||
<>
|
||||
<span className="overflow-x-auto">
|
||||
{guiMode.mode === 'default' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
@ -72,9 +76,18 @@ export const Toolbar = () => {
|
||||
SketchOnFace
|
||||
</button>
|
||||
)}
|
||||
{(guiMode.mode === 'canEditSketch' || false) && (
|
||||
{guiMode.mode === 'canEditSketch' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log('guiMode.pathId', guiMode.pathId)
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'edit_mode_enter',
|
||||
target: guiMode.pathId,
|
||||
},
|
||||
})
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
@ -126,14 +139,23 @@ export const Toolbar = () => {
|
||||
)}
|
||||
|
||||
{guiMode.mode === 'sketch' && (
|
||||
<button onClick={() => setGuiMode({ mode: 'default' })}>
|
||||
<button
|
||||
onClick={() => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'edit_mode_exit' },
|
||||
})
|
||||
setGuiMode({ mode: 'default' })
|
||||
}}
|
||||
>
|
||||
Exit sketch
|
||||
</button>
|
||||
)}
|
||||
{toolTips
|
||||
.filter(
|
||||
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
||||
(sketchFnName) => ['line'].includes(sketchFnName)
|
||||
(sketchFnName) => ['sketch_line', 'move'].includes(sketchFnName)
|
||||
)
|
||||
.map((sketchFnName) => {
|
||||
if (
|
||||
@ -144,7 +166,18 @@ export const Toolbar = () => {
|
||||
return (
|
||||
<button
|
||||
key={sketchFnName}
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_tool',
|
||||
tool:
|
||||
guiMode.sketchMode === sketchFnName
|
||||
? 'select'
|
||||
: (sketchFnName as any),
|
||||
},
|
||||
})
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
...(guiMode.sketchMode === sketchFnName
|
||||
@ -154,17 +187,17 @@ export const Toolbar = () => {
|
||||
}
|
||||
: {
|
||||
sketchMode: sketchFnName,
|
||||
waitingFirstClick: true,
|
||||
isTooltip: true,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{sketchFnName}
|
||||
{guiMode.sketchMode === sketchFnName && '✅'}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<ConvertToVariable />
|
||||
<HorzVert horOrVert="horizontal" />
|
||||
<HorzVert horOrVert="vertical" />
|
||||
<EqualLength />
|
||||
@ -182,7 +215,7 @@ export const Toolbar = () => {
|
||||
<Intersect />
|
||||
<RemoveConstrainingValues />
|
||||
<SetAngleBetween />
|
||||
</>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { ProjectWithEntryPointMetadata } from '../Router'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import styles from './AppHeader.module.css'
|
||||
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
|
||||
|
||||
interface AppHeaderProps extends React.PropsWithChildren {
|
||||
showToolbar?: boolean
|
||||
@ -43,7 +44,8 @@ export const AppHeader = ({
|
||||
)}
|
||||
{/* If there are children, show them, otherwise show User menu */}
|
||||
{children || (
|
||||
<div className="ml-auto">
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
<NetworkHealthIndicator />
|
||||
<UserSidebarMenu user={user} />
|
||||
</div>
|
||||
)}
|
||||
|
@ -144,7 +144,7 @@ export function useCalc({
|
||||
try {
|
||||
const code = `const __result__ = ${value}\nshow(__result__)`
|
||||
const ast = parser_wasm(code)
|
||||
const _programMem: any = { root: {} }
|
||||
const _programMem: any = { root: {}, return: null }
|
||||
availableVarInfo.variables.forEach(({ key, value }) => {
|
||||
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
||||
})
|
||||
@ -198,29 +198,25 @@ export const CreateNewVariable = ({
|
||||
isNewVariableNameUnique,
|
||||
setNewVariableName,
|
||||
shouldCreateVariable,
|
||||
setShouldCreateVariable,
|
||||
setShouldCreateVariable = () => {},
|
||||
showCheckbox = true,
|
||||
}: {
|
||||
isNewVariableNameUnique: boolean
|
||||
newVariableName: string
|
||||
setNewVariableName: (a: string) => void
|
||||
shouldCreateVariable: boolean
|
||||
setShouldCreateVariable: (a: boolean) => void
|
||||
shouldCreateVariable?: boolean
|
||||
setShouldCreateVariable?: (a: boolean) => void
|
||||
showCheckbox?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor="create-new-variable"
|
||||
className="block text-sm font-medium text-gray-700 mt-3 font-mono"
|
||||
>
|
||||
<label htmlFor="create-new-variable" className="block mt-3 font-mono">
|
||||
Create new variable
|
||||
</label>
|
||||
<div className="mt-1 flex flex-1">
|
||||
<div className="mt-1 flex gap-2 items-center">
|
||||
{showCheckbox && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink"
|
||||
checked={shouldCreateVariable}
|
||||
onChange={(e) => {
|
||||
setShouldCreateVariable(e.target.checked)
|
||||
@ -232,7 +228,10 @@ export const CreateNewVariable = ({
|
||||
disabled={!shouldCreateVariable}
|
||||
name="create-new-variable"
|
||||
id="create-new-variable"
|
||||
className={`shadow-sm font-[monospace] focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink-0 ${
|
||||
autoFocus={true}
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
className={`font-mono flex-1 sm:text-sm px-2 py-1 rounded-sm bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-90 dark:text-chalkboard-10 ${
|
||||
!shouldCreateVariable ? 'opacity-50' : ''
|
||||
}`}
|
||||
value={newVariableName}
|
||||
|
19
src/components/CodeMenu.module.css
Normal file
19
src/components/CodeMenu.module.css
Normal file
@ -0,0 +1,19 @@
|
||||
.button {
|
||||
@apply flex justify-between items-center gap-2 px-2 py-1 text-left border-none rounded-sm;
|
||||
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
|
||||
@apply ui-active:bg-liquid-10/50 ui-active:text-liquid-90;
|
||||
@apply transition-colors ease-out;
|
||||
}
|
||||
|
||||
:global(.dark) .button {
|
||||
@apply text-chalkboard-30;
|
||||
@apply ui-active:bg-chalkboard-80 ui-active:text-liquid-10;
|
||||
}
|
||||
|
||||
.button small {
|
||||
@apply text-chalkboard-60;
|
||||
}
|
||||
|
||||
:global(.dark) .button small {
|
||||
@apply text-chalkboard-40;
|
||||
}
|
59
src/components/CodeMenu.tsx
Normal file
59
src/components/CodeMenu.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { Menu } from '@headlessui/react'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { faEllipsis } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionIcon } from './ActionIcon'
|
||||
import { useStore } from 'useStore'
|
||||
import styles from './CodeMenu.module.css'
|
||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||
import { editorShortcutMeta } from './TextEditor'
|
||||
|
||||
export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||
const { formatCode } = useStore((s) => ({
|
||||
formatCode: s.formatCode,
|
||||
}))
|
||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||
useConvertToVariable()
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<div
|
||||
className="relative"
|
||||
onClick={(e) => {
|
||||
if (e.eventPhase === 3) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Menu.Button className="p-0 border-none relative">
|
||||
<ActionIcon
|
||||
icon={faEllipsis}
|
||||
bgClassName={
|
||||
'bg-chalkboard-20 dark:bg-chalkboard-110 hover:bg-liquid-10/50 hover:dark:bg-chalkboard-90 ui-active:bg-chalkboard-80 ui-active:dark:bg-chalkboard-90 rounded'
|
||||
}
|
||||
iconClassName={'text-chalkboard-90 dark:text-chalkboard-40'}
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50">
|
||||
<Menu.Item>
|
||||
<button onClick={() => formatCode()} className={styles.button}>
|
||||
<span>Format code</span>
|
||||
<small>{editorShortcutMeta.formatCode.display}</small>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
{convertToVarEnabled && (
|
||||
<Menu.Item>
|
||||
<button
|
||||
onClick={handleConvertToVarClick}
|
||||
className={styles.button}
|
||||
>
|
||||
<span>Convert to Variable</span>
|
||||
<small>{editorShortcutMeta.convertToVariable.display}</small>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Items>
|
||||
</div>
|
||||
</Menu>
|
||||
)
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
.panel {
|
||||
@apply relative overflow-auto z-0;
|
||||
@apply relative z-0;
|
||||
@apply bg-chalkboard-10/70 backdrop-blur-sm;
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
|
||||
.header {
|
||||
@apply sticky top-0 z-10 cursor-pointer;
|
||||
@apply flex items-center gap-2 w-full p-2;
|
||||
@apply flex items-center justify-between gap-2 w-full p-2;
|
||||
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
|
||||
@apply bg-chalkboard-20;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ export interface CollapsiblePanelProps
|
||||
title: string
|
||||
icon?: IconDefinition
|
||||
open?: boolean
|
||||
menu?: React.ReactNode
|
||||
iconClassNames?: {
|
||||
bg?: string
|
||||
icon?: string
|
||||
@ -18,21 +19,27 @@ export const PanelHeader = ({
|
||||
title,
|
||||
icon,
|
||||
iconClassNames,
|
||||
menu,
|
||||
}: CollapsiblePanelProps) => {
|
||||
return (
|
||||
<summary className={styles.header}>
|
||||
<ActionIcon
|
||||
icon={icon}
|
||||
bgClassName={
|
||||
'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
|
||||
(iconClassNames?.bg || '')
|
||||
}
|
||||
iconClassName={
|
||||
'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
|
||||
(iconClassNames?.icon || '')
|
||||
}
|
||||
/>
|
||||
{title}
|
||||
<div className="flex gap-2 align-center flex-1">
|
||||
<ActionIcon
|
||||
icon={icon}
|
||||
bgClassName={
|
||||
'bg-chalkboard-30 dark:bg-chalkboard-90 group-open:bg-chalkboard-80 rounded ' +
|
||||
(iconClassNames?.bg || '')
|
||||
}
|
||||
iconClassName={
|
||||
'text-chalkboard-90 dark:text-chalkboard-40 group-open:text-liquid-10 ' +
|
||||
(iconClassNames?.icon || '')
|
||||
}
|
||||
/>
|
||||
{title}
|
||||
</div>
|
||||
<div className="group-open:opacity-100 opacity-0 group-open:pointer-events-auto pointer-events-none">
|
||||
{menu}
|
||||
</div>
|
||||
</summary>
|
||||
)
|
||||
}
|
||||
@ -43,6 +50,7 @@ export const CollapsiblePanel = ({
|
||||
children,
|
||||
className,
|
||||
iconClassNames,
|
||||
menu,
|
||||
...props
|
||||
}: CollapsiblePanelProps) => {
|
||||
return (
|
||||
@ -50,7 +58,12 @@ export const CollapsiblePanel = ({
|
||||
{...props}
|
||||
className={styles.panel + ' group ' + (className || '')}
|
||||
>
|
||||
<PanelHeader title={title} icon={icon} iconClassNames={iconClassNames} />
|
||||
<PanelHeader
|
||||
title={title}
|
||||
icon={icon}
|
||||
iconClassNames={iconClassNames}
|
||||
menu={menu}
|
||||
/>
|
||||
{children}
|
||||
</details>
|
||||
)
|
||||
|
@ -196,7 +196,7 @@ const CommandBar = () => {
|
||||
setCommandBarOpen(false)
|
||||
clearState()
|
||||
}}
|
||||
className="fixed inset-0 overflow-y-auto p-4 pt-[25vh]"
|
||||
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
|
||||
>
|
||||
<Transition.Child
|
||||
enter="duration-100 ease-out"
|
||||
@ -207,7 +207,7 @@ const CommandBar = () => {
|
||||
leaveTo="opacity-0"
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog.Overlay className="fixed z-40 inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
||||
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
enter="duration-100 ease-out"
|
||||
@ -221,7 +221,7 @@ const CommandBar = () => {
|
||||
<Combobox
|
||||
value={selectedCommand}
|
||||
onChange={handleCommandSelection}
|
||||
className="rounded relative mx-auto z-40 p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
|
||||
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">
|
||||
|
@ -29,6 +29,7 @@ describe('processMemory', () => {
|
||||
const ast = parser_wasm(code)
|
||||
const programMemory = await enginelessExecutor(ast, {
|
||||
root: {},
|
||||
return: null,
|
||||
})
|
||||
const output = processMemory(programMemory)
|
||||
expect(output.myVar).toEqual(5)
|
||||
|
@ -2,7 +2,7 @@ import ReactJson from 'react-json-view'
|
||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { useStore } from '../useStore'
|
||||
import { useMemo } from 'react'
|
||||
import { ProgramMemory } from '../lang/executor'
|
||||
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/executor'
|
||||
import { Themes } from '../lib/theme'
|
||||
|
||||
interface MemoryPanelProps extends CollapsiblePanelProps {
|
||||
@ -49,8 +49,12 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
||||
Object.keys(programMemory.root).forEach((key) => {
|
||||
const val = programMemory.root[key]
|
||||
if (typeof val.value !== 'function') {
|
||||
if (val.type === 'sketchGroup' || val.type === 'extrudeGroup') {
|
||||
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }) => {
|
||||
if (val.type === 'SketchGroup') {
|
||||
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
|
||||
return rest
|
||||
})
|
||||
} else if (val.type === 'ExtrudeGroup') {
|
||||
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
||||
return rest
|
||||
})
|
||||
} else {
|
||||
|
51
src/components/NetworkHealthIndicator.test.tsx
Normal file
51
src/components/NetworkHealthIndicator.test.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import UserSidebarMenu from './UserSidebarMenu'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||
import CommandBarProvider from './CommandBar'
|
||||
import {
|
||||
NETWORK_CONTENT,
|
||||
NetworkHealthIndicator,
|
||||
} from './NetworkHealthIndicator'
|
||||
|
||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
// wrap in router and xState context
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<CommandBarProvider>
|
||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||
</CommandBarProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
describe('NetworkHealthIndicator tests', () => {
|
||||
test('Renders the network indicator', () => {
|
||||
render(
|
||||
<TestWrap>
|
||||
<NetworkHealthIndicator />
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||
|
||||
expect(screen.getByTestId('network-good')).toHaveTextContent(
|
||||
NETWORK_CONTENT.good
|
||||
)
|
||||
})
|
||||
|
||||
test('Responds to network changes', () => {
|
||||
render(
|
||||
<TestWrap>
|
||||
<NetworkHealthIndicator />
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
fireEvent.offline(window)
|
||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||
|
||||
expect(screen.getByTestId('network-bad')).toHaveTextContent(
|
||||
NETWORK_CONTENT.bad
|
||||
)
|
||||
})
|
||||
})
|
112
src/components/NetworkHealthIndicator.tsx
Normal file
112
src/components/NetworkHealthIndicator.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import {
|
||||
faCheck,
|
||||
faExclamation,
|
||||
faWifi,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ActionIcon } from './ActionIcon'
|
||||
|
||||
export const NETWORK_CONTENT = {
|
||||
good: 'Network health is good',
|
||||
bad: 'Network issue',
|
||||
}
|
||||
|
||||
const NETWORK_MESSAGES = {
|
||||
offline: 'You are offline',
|
||||
}
|
||||
|
||||
export const NetworkHealthIndicator = () => {
|
||||
const [networkIssues, setNetworkIssues] = useState<string[]>([])
|
||||
const hasIssues = [...networkIssues.values()].length > 0
|
||||
|
||||
useEffect(() => {
|
||||
const offlineListener = () =>
|
||||
setNetworkIssues((issues) => {
|
||||
return [
|
||||
...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline),
|
||||
NETWORK_MESSAGES.offline,
|
||||
]
|
||||
})
|
||||
window.addEventListener('offline', offlineListener)
|
||||
|
||||
const onlineListener = () =>
|
||||
setNetworkIssues((issues) => {
|
||||
return [...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline)]
|
||||
})
|
||||
window.addEventListener('online', onlineListener)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('offline', offlineListener)
|
||||
window.removeEventListener('online', onlineListener)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<Popover.Button
|
||||
className={
|
||||
'p-0 border-none relative ' +
|
||||
(hasIssues
|
||||
? 'focus-visible:outline-destroy-80'
|
||||
: 'focus-visible:outline-succeed-80')
|
||||
}
|
||||
data-testid="network-toggle"
|
||||
>
|
||||
<span className="sr-only">Network Health</span>
|
||||
<ActionIcon
|
||||
icon={faWifi}
|
||||
iconClassName={
|
||||
hasIssues
|
||||
? 'text-destroy-80 dark:text-destroy-30'
|
||||
: 'text-succeed-80 dark:text-succeed-30'
|
||||
}
|
||||
bgClassName={
|
||||
hasIssues
|
||||
? 'hover:bg-destroy-10/50 hover:dark:bg-destroy-80/50 rounded'
|
||||
: 'hover:bg-succeed-10/50 hover:dark:bg-succeed-80/50 rounded'
|
||||
}
|
||||
/>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="absolute right-0 left-auto top-full mt-1 w-56 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch py-2 bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm">
|
||||
{!hasIssues ? (
|
||||
<span
|
||||
className="flex items-center justify-center gap-1 px-4"
|
||||
data-testid="network-good"
|
||||
>
|
||||
<ActionIcon
|
||||
icon={faCheck}
|
||||
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded'}
|
||||
iconClassName={'text-succeed-80 dark:text-succeed-30'}
|
||||
/>
|
||||
{NETWORK_CONTENT.good}
|
||||
</span>
|
||||
) : (
|
||||
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
|
||||
<span
|
||||
className="font-bold text-xs uppercase text-destroy-60 dark:text-destroy-50 px-4"
|
||||
data-testid="network-bad"
|
||||
>
|
||||
{NETWORK_CONTENT.bad}
|
||||
{networkIssues.length > 1 ? 's' : ''}
|
||||
</span>
|
||||
{networkIssues.map((issue) => (
|
||||
<li
|
||||
key={issue}
|
||||
className="flex items-center gap-1 py-2 my-2 last:mb-0"
|
||||
>
|
||||
<ActionIcon
|
||||
icon={faExclamation}
|
||||
bgClassName={'bg-destroy-10/50 dark:bg-destroy-80/50 rounded'}
|
||||
iconClassName={'text-destroy-80 dark:text-destroy-30'}
|
||||
className="ml-4"
|
||||
/>
|
||||
<p className="flex-1 mr-4">{issue}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
)
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { Fragment } from 'react'
|
||||
import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
export const SetVarNameModal = ({
|
||||
isOpen,
|
||||
@ -19,67 +22,65 @@ export const SetVarNameModal = ({
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={onReject}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="fixed inset-0 z-40 overflow-y-auto p-4 pt-[25vh]"
|
||||
onClose={onReject}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
enterFrom="opacity-0 translate-y-4"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="ease-in duration-75"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
<Dialog.Overlay className="fixed inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="rounded relative mx-auto px-4 py-8 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
onResolve({
|
||||
variableName: newVariableName,
|
||||
})
|
||||
toast.success(`Added variable ${newVariableName}`)
|
||||
}}
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900 capitalize"
|
||||
<CreateNewVariable
|
||||
setNewVariableName={setNewVariableName}
|
||||
newVariableName={newVariableName}
|
||||
isNewVariableNameUnique={isNewVariableNameUnique}
|
||||
shouldCreateVariable={true}
|
||||
showCheckbox={false}
|
||||
/>
|
||||
<div className="mt-8 flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
type="submit"
|
||||
disabled={!isNewVariableNameUnique}
|
||||
icon={{ icon: faPlus }}
|
||||
>
|
||||
Set {valueName}
|
||||
</Dialog.Title>
|
||||
|
||||
<CreateNewVariable
|
||||
setNewVariableName={setNewVariableName}
|
||||
newVariableName={newVariableName}
|
||||
isNewVariableNameUnique={isNewVariableNameUnique}
|
||||
shouldCreateVariable={true}
|
||||
setShouldCreateVariable={() => {}}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!isNewVariableNameUnique}
|
||||
className={`inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
|
||||
!isNewVariableNameUnique
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() =>
|
||||
onResolve({
|
||||
variableName: newVariableName,
|
||||
})
|
||||
}
|
||||
>
|
||||
Add variable
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
Add variable
|
||||
</ActionButton>
|
||||
<ActionButton Element="button" onClick={() => onReject(false)}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
)
|
||||
|
@ -7,29 +7,52 @@ import {
|
||||
} from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useStore } from '../useStore'
|
||||
import { getNormalisedCoordinates } from '../lib/utils'
|
||||
import { getNormalisedCoordinates, roundOff } from '../lib/utils'
|
||||
import Loading from './Loading'
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { addStartSketch } from 'lang/modifyAst'
|
||||
import { addNewSketchLn } from 'lang/std/sketch'
|
||||
|
||||
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,
|
||||
setButtonDownInStream,
|
||||
didDragInStream,
|
||||
setDidDragInStream,
|
||||
streamDimensions,
|
||||
isExecuting,
|
||||
guiMode,
|
||||
ast,
|
||||
updateAst,
|
||||
setGuiMode,
|
||||
programMemory,
|
||||
} = useStore((s) => ({
|
||||
mediaStream: s.mediaStream,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
isMouseDownInStream: s.isMouseDownInStream,
|
||||
setIsMouseDownInStream: s.setIsMouseDownInStream,
|
||||
setButtonDownInStream: s.setButtonDownInStream,
|
||||
fileId: s.fileId,
|
||||
didDragInStream: s.didDragInStream,
|
||||
setDidDragInStream: s.setDidDragInStream,
|
||||
streamDimensions: s.streamDimensions,
|
||||
isExecuting: s.isExecuting,
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
setGuiMode: s.setGuiMode,
|
||||
programMemory: s.programMemory,
|
||||
}))
|
||||
const {
|
||||
settings: {
|
||||
context: { cameraControls },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -42,39 +65,70 @@ export const Stream = ({ className = '' }) => {
|
||||
videoRef.current.srcObject = mediaStream
|
||||
}, [mediaStream, engineCommandManager])
|
||||
|
||||
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = ({
|
||||
clientX,
|
||||
clientY,
|
||||
ctrlKey,
|
||||
}) => {
|
||||
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
|
||||
if (!videoRef.current) return
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX,
|
||||
clientY,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
el: videoRef.current,
|
||||
...streamDimensions,
|
||||
})
|
||||
console.log('click', x, y)
|
||||
|
||||
const newId = uuidv4()
|
||||
|
||||
const interaction = ctrlKey ? 'pan' : 'rotate'
|
||||
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
||||
let interaction: CameraDragInteractionType_type = 'rotate'
|
||||
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_start',
|
||||
interaction,
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
if (
|
||||
interactionGuards.pan.callback(e) ||
|
||||
interactionGuards.pan.lenientDragStartButton === e.button
|
||||
) {
|
||||
interaction = 'pan'
|
||||
} else if (
|
||||
interactionGuards.rotate.callback(e) ||
|
||||
interactionGuards.rotate.lenientDragStartButton === e.button
|
||||
) {
|
||||
interaction = 'rotate'
|
||||
} else if (
|
||||
interactionGuards.zoom.dragCallback(e) ||
|
||||
interactionGuards.zoom.lenientDragStartButton === e.button
|
||||
) {
|
||||
interaction = 'zoom'
|
||||
}
|
||||
|
||||
setIsMouseDownInStream(true)
|
||||
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'handle_mouse_drag_start',
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
} else if (
|
||||
!(
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any)
|
||||
)
|
||||
) {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_start',
|
||||
interaction,
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
}
|
||||
|
||||
setButtonDownInStream(e.button)
|
||||
setClickCoords({ x, y })
|
||||
}
|
||||
|
||||
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
|
||||
e.preventDefault()
|
||||
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
|
||||
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
@ -91,6 +145,7 @@ export const Stream = ({ className = '' }) => {
|
||||
ctrlKey,
|
||||
}) => {
|
||||
if (!videoRef.current) return
|
||||
setButtonDownInStream(undefined)
|
||||
const { x, y } = getNormalisedCoordinates({
|
||||
clientX,
|
||||
clientY,
|
||||
@ -101,7 +156,7 @@ export const Stream = ({ className = '' }) => {
|
||||
const newCmdId = uuidv4()
|
||||
const interaction = ctrlKey ? 'pan' : 'rotate'
|
||||
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
const command: Models['WebSocketRequest_type'] = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'camera_drag_end',
|
||||
@ -109,9 +164,8 @@ export const Stream = ({ className = '' }) => {
|
||||
window: { x, y },
|
||||
},
|
||||
cmd_id: newCmdId,
|
||||
})
|
||||
}
|
||||
|
||||
setIsMouseDownInStream(false)
|
||||
if (!didDragInStream) {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
@ -123,7 +177,109 @@ export const Stream = ({ className = '' }) => {
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
}
|
||||
|
||||
if (!didDragInStream && guiMode.mode === 'default') {
|
||||
command.cmd = {
|
||||
type: 'select_with_point',
|
||||
selection_type: 'add',
|
||||
selected_at_window: { x, y },
|
||||
}
|
||||
} else if (
|
||||
(!didDragInStream &&
|
||||
guiMode.mode === 'sketch' &&
|
||||
['move', 'select'].includes(guiMode.sketchMode)) ||
|
||||
(guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any))
|
||||
) {
|
||||
command.cmd = {
|
||||
type: 'mouse_click',
|
||||
window: { x, y },
|
||||
}
|
||||
} else if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('move' as any)
|
||||
) {
|
||||
command.cmd = {
|
||||
type: 'handle_mouse_drag_end',
|
||||
window: { x, y },
|
||||
}
|
||||
}
|
||||
engineCommandManager?.sendSceneCommand(command).then(async ({ data }) => {
|
||||
if (command.cmd.type !== 'mouse_click' || !ast) return
|
||||
if (
|
||||
!(
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any as 'line')
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if (data?.data?.entities_modified?.length && guiMode.waitingFirstClick) {
|
||||
const curve = await engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: data?.data?.entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
const _addStartSketch = addStartSketch(
|
||||
ast,
|
||||
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
||||
[
|
||||
roundOff(coords[1].x - coords[0].x),
|
||||
roundOff(coords[1].y - coords[0].y),
|
||||
]
|
||||
)
|
||||
const _modifiedAst = _addStartSketch.modifiedAst
|
||||
const _pathToNode = _addStartSketch.pathToNode
|
||||
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
pathToNode: _pathToNode,
|
||||
waitingFirstClick: false,
|
||||
})
|
||||
updateAst(_modifiedAst)
|
||||
} else if (
|
||||
data?.data?.entities_modified?.length &&
|
||||
!guiMode.waitingFirstClick
|
||||
) {
|
||||
const curve = await engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: data?.data?.entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
const _modifiedAst = addNewSketchLn({
|
||||
node: ast,
|
||||
programMemory,
|
||||
to: [coords[1].x, coords[1].y],
|
||||
fnName: 'line',
|
||||
pathToNode: guiMode.pathToNode,
|
||||
}).modifiedAst
|
||||
updateAst(_modifiedAst)
|
||||
}
|
||||
})
|
||||
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 (
|
||||
@ -139,7 +295,9 @@ export const Stream = ({ className = '' }) => {
|
||||
onContextMenuCapture={(e) => e.preventDefault()}
|
||||
onWheel={handleScroll}
|
||||
onPlay={() => setIsLoading(false)}
|
||||
className="w-full h-full"
|
||||
onMouseMoveCapture={handleMouseMove}
|
||||
className={`w-full h-full ${isExecuting && 'blur-md'}`}
|
||||
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
||||
/>
|
||||
{isLoading && (
|
||||
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
|
265
src/components/TextEditor.tsx
Normal file
265
src/components/TextEditor.tsx
Normal file
@ -0,0 +1,265 @@
|
||||
import ReactCodeMirror, {
|
||||
Extension,
|
||||
ViewUpdate,
|
||||
keymap,
|
||||
} from '@uiw/react-codemirror'
|
||||
import { FromServer, IntoServer } from 'editor/lsp/codec'
|
||||
import Server from '../editor/lsp/server'
|
||||
import Client from '../editor/lsp/client'
|
||||
import { TEST } from 'env'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { linter, lintGutter } from '@codemirror/lint'
|
||||
import { Selections, useStore } from 'useStore'
|
||||
import { LanguageServerClient } from 'editor/lsp'
|
||||
import kclLanguage from 'editor/lsp/language'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { writeTextFile } from '@tauri-apps/api/fs'
|
||||
import { PROJECT_ENTRYPOINT } from 'lib/tauriFS'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import {
|
||||
EditorView,
|
||||
addLineHighlight,
|
||||
lineHighlightField,
|
||||
} from 'editor/highlightextension'
|
||||
import { isOverlap } from 'lib/utils'
|
||||
import { kclErrToDiagnostic } from 'lang/errors'
|
||||
import { CSSRuleObject } from 'tailwindcss/types/config'
|
||||
|
||||
export const editorShortcutMeta = {
|
||||
formatCode: {
|
||||
codeMirror: 'Alt-Shift-f',
|
||||
display: 'Alt + Shift + F',
|
||||
},
|
||||
convertToVariable: {
|
||||
codeMirror: 'Ctrl-Shift-c',
|
||||
display: 'Ctrl + Shift + C',
|
||||
},
|
||||
}
|
||||
|
||||
export const TextEditor = ({
|
||||
theme,
|
||||
}: {
|
||||
theme: Themes.Light | Themes.Dark
|
||||
}) => {
|
||||
const pathParams = useParams()
|
||||
const {
|
||||
code,
|
||||
defferedSetCode,
|
||||
editorView,
|
||||
engineCommandManager,
|
||||
formatCode,
|
||||
isLSPServerReady,
|
||||
selectionRanges,
|
||||
selectionRangeTypeMap,
|
||||
setEditorView,
|
||||
setIsLSPServerReady,
|
||||
setSelectionRanges,
|
||||
sourceRangeMap,
|
||||
} = useStore((s) => ({
|
||||
code: s.code,
|
||||
defferedSetCode: s.defferedSetCode,
|
||||
editorView: s.editorView,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
formatCode: s.formatCode,
|
||||
isLSPServerReady: s.isLSPServerReady,
|
||||
selectionRanges: s.selectionRanges,
|
||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||
setEditorView: s.setEditorView,
|
||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
||||
setSelectionRanges: s.setSelectionRanges,
|
||||
sourceRangeMap: s.sourceRangeMap,
|
||||
}))
|
||||
|
||||
const {
|
||||
settings: {
|
||||
context: { textWrapping },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
const { setCommandBarOpen } = useCommandsContext()
|
||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||
useConvertToVariable()
|
||||
|
||||
// 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 onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
||||
defferedSetCode(value)
|
||||
if (isTauri() && pathParams.id) {
|
||||
// Save the file to disk
|
||||
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch(
|
||||
(err) => {
|
||||
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
||||
console.error('error saving file', err)
|
||||
toast.error('Error saving file, please check file permissions')
|
||||
}
|
||||
)
|
||||
}
|
||||
if (editorView) {
|
||||
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
|
||||
}
|
||||
} //, []);
|
||||
const onUpdate = (viewUpdate: ViewUpdate) => {
|
||||
if (!editorView) {
|
||||
setEditorView(viewUpdate.view)
|
||||
}
|
||||
const ranges = viewUpdate.state.selection.ranges
|
||||
|
||||
const isChange =
|
||||
ranges.length !== selectionRanges.codeBasedSelections.length ||
|
||||
ranges.some(({ from, to }, i) => {
|
||||
return (
|
||||
from !== selectionRanges.codeBasedSelections[i].range[0] ||
|
||||
to !== selectionRanges.codeBasedSelections[i].range[1]
|
||||
)
|
||||
})
|
||||
|
||||
if (!isChange) return
|
||||
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
|
||||
({ from, to }) => {
|
||||
if (selectionRangeTypeMap[to]) {
|
||||
return {
|
||||
type: selectionRangeTypeMap[to],
|
||||
range: [from, to],
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'default',
|
||||
range: [from, to],
|
||||
}
|
||||
}
|
||||
)
|
||||
const idBasedSelections = codeBasedSelections
|
||||
.map(({ type, range }) => {
|
||||
const hasOverlap = Object.entries(sourceRangeMap).filter(
|
||||
([_, sourceRange]) => {
|
||||
return isOverlap(sourceRange, range)
|
||||
}
|
||||
)
|
||||
if (hasOverlap.length) {
|
||||
return {
|
||||
type,
|
||||
id: hasOverlap[0][0],
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as any
|
||||
|
||||
engineCommandManager?.cusorsSelected({
|
||||
otherSelections: [],
|
||||
idBasedSelections,
|
||||
})
|
||||
|
||||
setSelectionRanges({
|
||||
otherSelections: [],
|
||||
codeBasedSelections,
|
||||
})
|
||||
}
|
||||
|
||||
const editorExtensions = useMemo(() => {
|
||||
const extensions = [
|
||||
lineHighlightField,
|
||||
keymap.of([
|
||||
{
|
||||
key: 'Meta-k',
|
||||
run: () => {
|
||||
setCommandBarOpen(true)
|
||||
return false
|
||||
},
|
||||
},
|
||||
{
|
||||
key: editorShortcutMeta.formatCode.codeMirror,
|
||||
run: () => {
|
||||
formatCode()
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
key: editorShortcutMeta.convertToVariable.codeMirror,
|
||||
run: () => {
|
||||
if (convertEnabled) {
|
||||
convertCallback()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
]),
|
||||
] 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])
|
||||
|
||||
return (
|
||||
<div
|
||||
id="code-mirror-override"
|
||||
className="full-height-subtract"
|
||||
style={{ '--height-subtract': '4.25rem' } as CSSRuleObject}
|
||||
>
|
||||
<ReactCodeMirror
|
||||
className="h-full"
|
||||
value={code}
|
||||
extensions={editorExtensions}
|
||||
onChange={onChange}
|
||||
onUpdate={onUpdate}
|
||||
theme={theme}
|
||||
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { create } from 'react-modal-promise'
|
||||
import { useStore } from '../../useStore'
|
||||
import { isNodeSafeToReplace } from '../../lang/queryAst'
|
||||
import { SetVarNameModal } from '../SetVarNameModal'
|
||||
import { moveValueIntoNewVariable } from '../../lang/modifyAst'
|
||||
|
||||
const getModalInfo = create(SetVarNameModal as any)
|
||||
|
||||
export const ConvertToVariable = () => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
|
||||
(s) => ({
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
selectionRanges: s.selectionRanges,
|
||||
programMemory: s.programMemory,
|
||||
})
|
||||
)
|
||||
const [enableAngLen, setEnableAngLen] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
|
||||
const { isSafe, value } = isNodeSafeToReplace(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections?.[0]?.range || []
|
||||
)
|
||||
const canReplace = isSafe && value.type !== 'Identifier'
|
||||
const isOnlyOneSelection = selectionRanges.codeBasedSelections.length === 1
|
||||
|
||||
const _enableHorz = canReplace && isOnlyOneSelection
|
||||
setEnableAngLen(_enableHorz)
|
||||
}, [guiMode, selectionRanges])
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!ast) return
|
||||
try {
|
||||
const { variableName } = await getModalInfo({
|
||||
valueName: 'var',
|
||||
} as any)
|
||||
|
||||
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
selectionRanges.codeBasedSelections[0].range,
|
||||
variableName
|
||||
)
|
||||
|
||||
updateAst(_modifiedAst)
|
||||
} catch (e) {
|
||||
console.log('e', e)
|
||||
}
|
||||
}}
|
||||
disabled={!enableAngLen}
|
||||
>
|
||||
ConvertToVariable
|
||||
</button>
|
||||
)
|
||||
}
|
@ -208,7 +208,13 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
filterText: filterText ?? label,
|
||||
}
|
||||
if (documentation) {
|
||||
completion.info = formatContents(documentation)
|
||||
completion.info = () => {
|
||||
const htmlString = formatContents(documentation)
|
||||
const htmlNode = document.createElement('div')
|
||||
htmlNode.style.display = 'contents'
|
||||
htmlNode.innerHTML = htmlString
|
||||
return { dom: htmlNode }
|
||||
}
|
||||
}
|
||||
|
||||
return completion
|
||||
|
@ -8,8 +8,6 @@ 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
|
||||
|
243
src/hooks/useAppMode.ts
Normal file
243
src/hooks/useAppMode.ts
Normal file
@ -0,0 +1,243 @@
|
||||
// needed somewhere to dump this logic,
|
||||
// Once we have xState this should be removed
|
||||
|
||||
import { useStore, Selections } from 'useStore'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ArtifactMap, EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import { Models } from '@kittycad/lib/dist/types/src'
|
||||
import { isReducedMotion } from 'lang/util'
|
||||
import { isOverlap } from 'lib/utils'
|
||||
|
||||
interface DefaultPlanes {
|
||||
xy: string
|
||||
yz: string
|
||||
xz: string
|
||||
}
|
||||
|
||||
export function useAppMode() {
|
||||
const {
|
||||
guiMode,
|
||||
setGuiMode,
|
||||
selectionRanges,
|
||||
engineCommandManager,
|
||||
selectionRangeTypeMap,
|
||||
} = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
setGuiMode: s.setGuiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
engineCommandManager: s.engineCommandManager,
|
||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||
}))
|
||||
const [defaultPlanes, setDefaultPlanes] = useState<DefaultPlanes | null>(null)
|
||||
useEffect(() => {
|
||||
if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === 'selectFace' &&
|
||||
engineCommandManager
|
||||
) {
|
||||
if (!defaultPlanes) {
|
||||
const xy = createPlane(engineCommandManager, {
|
||||
x_axis: { x: 1, y: 0, z: 0 },
|
||||
y_axis: { x: 0, y: 1, z: 0 },
|
||||
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
|
||||
})
|
||||
const yz = createPlane(engineCommandManager, {
|
||||
x_axis: { x: 0, y: 1, z: 0 },
|
||||
y_axis: { x: 0, y: 0, z: 1 },
|
||||
color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
|
||||
})
|
||||
const xz = createPlane(engineCommandManager, {
|
||||
x_axis: { x: 1, y: 0, z: 0 },
|
||||
y_axis: { x: 0, y: 0, z: 1 },
|
||||
color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
|
||||
})
|
||||
setDefaultPlanes({ xy, yz, xz })
|
||||
} else {
|
||||
hideDefaultPlanes(engineCommandManager, defaultPlanes)
|
||||
}
|
||||
}
|
||||
if (guiMode.mode !== 'sketch' && defaultPlanes) {
|
||||
Object.values(defaultPlanes).forEach((planeId) => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'object_visible',
|
||||
object_id: planeId,
|
||||
hidden: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
} else if (guiMode.mode === 'default') {
|
||||
const pathId =
|
||||
engineCommandManager &&
|
||||
isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
selectionRanges
|
||||
)
|
||||
if (pathId) {
|
||||
setGuiMode({
|
||||
mode: 'canEditSketch',
|
||||
rotation: [0, 0, 0, 1],
|
||||
position: [0, 0, 0],
|
||||
pathToNode: [],
|
||||
pathId,
|
||||
})
|
||||
}
|
||||
} else if (guiMode.mode === 'canEditSketch') {
|
||||
if (
|
||||
!engineCommandManager ||
|
||||
!isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
selectionRanges
|
||||
)
|
||||
) {
|
||||
setGuiMode({
|
||||
mode: 'default',
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [
|
||||
guiMode,
|
||||
guiMode.mode,
|
||||
engineCommandManager,
|
||||
selectionRanges,
|
||||
selectionRangeTypeMap,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const unSub = engineCommandManager?.subscribeTo({
|
||||
event: 'select_with_point',
|
||||
callback: async ({ data }) => {
|
||||
if (!data.entity_id) return
|
||||
if (!defaultPlanes) return
|
||||
if (!Object.values(defaultPlanes || {}).includes(data.entity_id)) {
|
||||
// user clicked something else in the scene
|
||||
return
|
||||
}
|
||||
const sketchModeResponse = await engineCommandManager?.sendSceneCommand(
|
||||
{
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'sketch_mode_enable',
|
||||
plane_id: data.entity_id,
|
||||
ortho: true,
|
||||
animated: !isReducedMotion(),
|
||||
},
|
||||
}
|
||||
)
|
||||
hideDefaultPlanes(engineCommandManager, defaultPlanes)
|
||||
const sketchUuid = uuidv4()
|
||||
const proms: any[] = []
|
||||
proms.push(
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: sketchUuid,
|
||||
cmd: {
|
||||
type: 'start_path',
|
||||
},
|
||||
})
|
||||
)
|
||||
proms.push(
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'edit_mode_enter',
|
||||
target: sketchUuid,
|
||||
},
|
||||
})
|
||||
)
|
||||
const res = await Promise.all(proms)
|
||||
console.log('res', res)
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
rotation: [0, 0, 0, 1],
|
||||
position: [0, 0, 0],
|
||||
pathToNode: [],
|
||||
})
|
||||
|
||||
console.log('sketchModeResponse', sketchModeResponse)
|
||||
},
|
||||
})
|
||||
return unSub
|
||||
}, [engineCommandManager, defaultPlanes])
|
||||
}
|
||||
|
||||
function createPlane(
|
||||
engineCommandManager: EngineCommandManager,
|
||||
{
|
||||
x_axis,
|
||||
y_axis,
|
||||
color,
|
||||
}: {
|
||||
x_axis: Models['Point3d_type']
|
||||
y_axis: Models['Point3d_type']
|
||||
color: Models['Color_type']
|
||||
}
|
||||
) {
|
||||
const planeId = uuidv4()
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'make_plane',
|
||||
size: 60,
|
||||
origin: { x: 0, y: 0, z: 0 },
|
||||
x_axis,
|
||||
y_axis,
|
||||
clobber: false,
|
||||
},
|
||||
cmd_id: planeId,
|
||||
})
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'plane_set_color',
|
||||
plane_id: planeId,
|
||||
color,
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
return planeId
|
||||
}
|
||||
|
||||
function hideDefaultPlanes(
|
||||
engineCommandManager: EngineCommandManager,
|
||||
defaultPlanes: DefaultPlanes
|
||||
) {
|
||||
Object.values(defaultPlanes).forEach((planeId) => {
|
||||
engineCommandManager?.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'object_visible',
|
||||
object_id: planeId,
|
||||
hidden: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function isCursorInSketchCommandRange(
|
||||
artifactMap: ArtifactMap,
|
||||
selectionRanges: Selections
|
||||
): string | false {
|
||||
const overlapingEntries = Object.entries(artifactMap || {}).filter(
|
||||
([id, artifact]) =>
|
||||
selectionRanges.codeBasedSelections.some(
|
||||
(selection) =>
|
||||
Array.isArray(selection.range) &&
|
||||
Array.isArray(artifact.range) &&
|
||||
isOverlap(selection.range, artifact.range) &&
|
||||
(artifact.commandType === 'start_path' ||
|
||||
artifact.commandType === 'extend_path' ||
|
||||
'close_path')
|
||||
)
|
||||
)
|
||||
return overlapingEntries.length === 1 && overlapingEntries[0][1].parentId
|
||||
? overlapingEntries[0][1].parentId
|
||||
: false
|
||||
}
|
56
src/hooks/useToolbarGuards.ts
Normal file
56
src/hooks/useToolbarGuards.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { SetVarNameModal } from 'components/SetVarNameModal'
|
||||
import { moveValueIntoNewVariable } from 'lang/modifyAst'
|
||||
import { isNodeSafeToReplace } from 'lang/queryAst'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { create } from 'react-modal-promise'
|
||||
import { useStore } from 'useStore'
|
||||
|
||||
const getModalInfo = create(SetVarNameModal as any)
|
||||
|
||||
export function useConvertToVariable() {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
|
||||
(s) => ({
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
selectionRanges: s.selectionRanges,
|
||||
programMemory: s.programMemory,
|
||||
})
|
||||
)
|
||||
const [enable, setEnabled] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
|
||||
const { isSafe, value } = isNodeSafeToReplace(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections?.[0]?.range || []
|
||||
)
|
||||
const canReplace = isSafe && value.type !== 'Identifier'
|
||||
const isOnlyOneSelection = selectionRanges.codeBasedSelections.length === 1
|
||||
|
||||
const _enableHorz = canReplace && isOnlyOneSelection
|
||||
setEnabled(_enableHorz)
|
||||
}, [guiMode, selectionRanges])
|
||||
|
||||
const handleClick = async () => {
|
||||
if (!ast) return
|
||||
try {
|
||||
const { variableName } = await getModalInfo({
|
||||
valueName: 'var',
|
||||
} as any)
|
||||
|
||||
const { modifiedAst: _modifiedAst } = moveValueIntoNewVariable(
|
||||
ast,
|
||||
programMemory,
|
||||
selectionRanges.codeBasedSelections[0].range,
|
||||
variableName
|
||||
)
|
||||
|
||||
updateAst(_modifiedAst)
|
||||
} catch (e) {
|
||||
console.log('e', e)
|
||||
}
|
||||
}
|
||||
|
||||
return { enable, handleClick }
|
||||
}
|
@ -82,11 +82,22 @@ code {
|
||||
monospace;
|
||||
}
|
||||
|
||||
.full-height-subtract {
|
||||
--height-subtract: 2.25rem;
|
||||
height: 100%;
|
||||
max-height: calc(100% - var(--height-subtract));
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-editor {
|
||||
@apply bg-transparent;
|
||||
@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,
|
||||
@ -137,14 +148,39 @@ code {
|
||||
}
|
||||
|
||||
#code-mirror-override .cm-tooltip {
|
||||
font-size: 80%;
|
||||
@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-tooltip-hover .documentation {
|
||||
padding: 5;
|
||||
#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 {
|
||||
|
@ -1564,7 +1564,7 @@ const key = 'c'`
|
||||
start: code.indexOf('\n// this is a comment'),
|
||||
end: code.indexOf('const key'),
|
||||
value: {
|
||||
type: 'block',
|
||||
type: 'blockComment',
|
||||
value: 'this is a comment',
|
||||
},
|
||||
}
|
||||
@ -1602,7 +1602,7 @@ const key = 'c'`
|
||||
start: 106,
|
||||
end: 166,
|
||||
value: {
|
||||
type: 'block',
|
||||
type: 'blockComment',
|
||||
value: 'this is\n a comment\n spanning a few lines',
|
||||
},
|
||||
})
|
||||
@ -1625,7 +1625,7 @@ const key = 'c'`
|
||||
start: 125,
|
||||
end: 141,
|
||||
value: {
|
||||
type: 'block',
|
||||
type: 'blockComment',
|
||||
value: 'a comment',
|
||||
},
|
||||
})
|
||||
|
@ -21,7 +21,7 @@ show(mySketch001)`
|
||||
)
|
||||
expect(shown).toEqual([
|
||||
{
|
||||
type: 'sketchGroup',
|
||||
type: 'SketchGroup',
|
||||
start: {
|
||||
to: [0, 0],
|
||||
from: [0, 0],
|
||||
@ -77,7 +77,7 @@ show(mySketch001)`
|
||||
)
|
||||
expect(shown).toEqual([
|
||||
{
|
||||
type: 'extrudeGroup',
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
@ -117,7 +117,7 @@ show(theExtrude, sk2)`
|
||||
)
|
||||
expect(geos).toEqual([
|
||||
{
|
||||
type: 'extrudeGroup',
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
@ -126,7 +126,7 @@ show(theExtrude, sk2)`
|
||||
__meta: [{ sourceRange: [13, 34] }],
|
||||
},
|
||||
{
|
||||
type: 'extrudeGroup',
|
||||
type: 'ExtrudeGroup',
|
||||
id: expect.any(String),
|
||||
value: [],
|
||||
height: 2,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { parser_wasm } from './abstractSyntaxTree'
|
||||
import { ProgramMemory } from './executor'
|
||||
import { ProgramMemory, SketchGroup } from './executor'
|
||||
import { initPromise } from './rust'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
import { vi } from 'vitest'
|
||||
@ -117,10 +117,10 @@ show(mySketch)
|
||||
// ].join('\n')
|
||||
// const { root } = await exe(code)
|
||||
// expect(root.mySk1.value).toHaveLength(3)
|
||||
// expect(root?.rotated?.type).toBe('sketchGroup')
|
||||
// expect(root?.rotated?.type).toBe('SketchGroup')
|
||||
// if (
|
||||
// root?.mySk1?.type !== 'sketchGroup' ||
|
||||
// root?.rotated?.type !== 'sketchGroup'
|
||||
// root?.mySk1?.type !== 'SketchGroup' ||
|
||||
// root?.rotated?.type !== 'SketchGroup'
|
||||
// )
|
||||
// throw new Error('not a sketch group')
|
||||
// expect(root.mySk1.rotation).toEqual([0, 0, 0, 1])
|
||||
@ -143,7 +143,7 @@ show(mySketch)
|
||||
].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.mySk1).toEqual({
|
||||
type: 'sketchGroup',
|
||||
type: 'SketchGroup',
|
||||
start: {
|
||||
to: [0, 0],
|
||||
from: [0, 0],
|
||||
@ -199,7 +199,7 @@ show(mySketch)
|
||||
// TODO path to node is probably wrong here, zero indexes are not correct
|
||||
expect(root).toEqual({
|
||||
three: {
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: 3,
|
||||
__meta: [
|
||||
{
|
||||
@ -208,7 +208,7 @@ show(mySketch)
|
||||
],
|
||||
},
|
||||
yo: {
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: [1, '2', 3, 9],
|
||||
__meta: [
|
||||
{
|
||||
@ -225,7 +225,7 @@ show(mySketch)
|
||||
].join('\n')
|
||||
const { root } = await exe(code)
|
||||
expect(root.yo).toEqual({
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9 },
|
||||
__meta: [
|
||||
{
|
||||
@ -240,7 +240,7 @@ show(mySketch)
|
||||
)
|
||||
const { root } = await exe(code)
|
||||
expect(root.myVar).toEqual({
|
||||
type: 'userVal',
|
||||
type: 'UserVal',
|
||||
value: '123',
|
||||
__meta: [
|
||||
{
|
||||
@ -338,7 +338,7 @@ describe('testing math operators', () => {
|
||||
const { root } = await exe(code)
|
||||
const sketch = root.part001
|
||||
// result of `-legLen(5, min(3, 999))` should be -4
|
||||
const yVal = sketch.value?.[0]?.to?.[1]
|
||||
const yVal = (sketch as SketchGroup).value?.[0]?.to?.[1]
|
||||
expect(yVal).toBe(-4)
|
||||
})
|
||||
it('test that % substitution feeds down CallExp->ArrExp->UnaryExp->CallExp', async () => {
|
||||
@ -356,8 +356,8 @@ describe('testing math operators', () => {
|
||||
const { root } = await exe(code)
|
||||
const sketch = root.part001
|
||||
// expect -legLen(segLen('seg01', %), myVar) to equal -4 setting the y value back to 0
|
||||
expect(sketch.value?.[1]?.from).toEqual([3, 4])
|
||||
expect(sketch.value?.[1]?.to).toEqual([6, 0])
|
||||
expect((sketch as SketchGroup).value?.[1]?.from).toEqual([3, 4])
|
||||
expect((sketch as SketchGroup).value?.[1]?.to).toEqual([6, 0])
|
||||
const removedUnaryExp = code.replace(
|
||||
`-legLen(segLen('seg01', %), myVar)`,
|
||||
`legLen(segLen('seg01', %), myVar)`
|
||||
@ -366,7 +366,9 @@ describe('testing math operators', () => {
|
||||
const removedUnaryExpRootSketch = removedUnaryExpRoot.part001
|
||||
|
||||
// without the minus sign, the y value should be 8
|
||||
expect(removedUnaryExpRootSketch.value?.[1]?.to).toEqual([6, 8])
|
||||
expect((removedUnaryExpRootSketch as SketchGroup).value?.[1]?.to).toEqual([
|
||||
6, 8,
|
||||
])
|
||||
})
|
||||
it('with nested callExpression and binaryExpression', async () => {
|
||||
const code = 'const myVar = 2 + min(100, -1 + legLen(5, 3))'
|
||||
@ -397,7 +399,10 @@ show(theExtrude)`
|
||||
|
||||
// helpers
|
||||
|
||||
async function exe(code: string, programMemory: ProgramMemory = { root: {} }) {
|
||||
async function exe(
|
||||
code: string,
|
||||
programMemory: ProgramMemory = { root: {}, return: null }
|
||||
) {
|
||||
const ast = parser_wasm(code)
|
||||
|
||||
const result = await enginelessExecutor(ast, programMemory)
|
||||
|
@ -5,96 +5,21 @@ import {
|
||||
SourceRangeMap,
|
||||
} from './std/engineConnection'
|
||||
import { ProgramReturn } from '../wasm-lib/kcl/bindings/ProgramReturn'
|
||||
import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
|
||||
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
|
||||
}
|
||||
export type Position = [number, number, number]
|
||||
export type Rotation = [number, number, number, number]
|
||||
export type { SourceRange } from '../wasm-lib/kcl/bindings/SourceRange'
|
||||
export type { Position } from '../wasm-lib/kcl/bindings/Position'
|
||||
export type { Rotation } from '../wasm-lib/kcl/bindings/Rotation'
|
||||
export type { Path } from '../wasm-lib/kcl/bindings/Path'
|
||||
export type { SketchGroup } from '../wasm-lib/kcl/bindings/SketchGroup'
|
||||
export type { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
|
||||
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
|
||||
|
||||
interface BasePath {
|
||||
from: [number, number]
|
||||
to: [number, number]
|
||||
name?: string
|
||||
__geoMeta: {
|
||||
id: string
|
||||
sourceRange: SourceRange
|
||||
}
|
||||
}
|
||||
|
||||
export interface ToPoint extends BasePath {
|
||||
type: 'toPoint'
|
||||
}
|
||||
|
||||
export interface Base extends BasePath {
|
||||
type: 'base'
|
||||
}
|
||||
|
||||
export interface HorizontalLineTo extends BasePath {
|
||||
type: 'horizontalLineTo'
|
||||
x: number
|
||||
}
|
||||
|
||||
export interface AngledLineTo extends BasePath {
|
||||
type: 'angledLineTo'
|
||||
angle: number
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
|
||||
interface GeoMeta {
|
||||
__geoMeta: {
|
||||
id: string
|
||||
sourceRange: SourceRange
|
||||
}
|
||||
}
|
||||
|
||||
export type Path = ToPoint | HorizontalLineTo | AngledLineTo | Base
|
||||
|
||||
export interface SketchGroup {
|
||||
type: 'sketchGroup'
|
||||
id: string
|
||||
value: Path[]
|
||||
start?: Base
|
||||
position: Position
|
||||
rotation: Rotation
|
||||
__meta: Metadata[]
|
||||
}
|
||||
|
||||
interface ExtrudePlane {
|
||||
type: 'extrudePlane'
|
||||
position: Position
|
||||
rotation: Rotation
|
||||
name?: string
|
||||
}
|
||||
|
||||
export type ExtrudeSurface = GeoMeta &
|
||||
ExtrudePlane /* | ExtrudeRadius | ExtrudeSpline */
|
||||
|
||||
export interface ExtrudeGroup {
|
||||
type: 'extrudeGroup'
|
||||
id: string
|
||||
value: ExtrudeSurface[]
|
||||
height: number
|
||||
position: Position
|
||||
rotation: Rotation
|
||||
__meta: Metadata[]
|
||||
}
|
||||
|
||||
/** UserVal not produced by one of our internal functions */
|
||||
export interface UserVal {
|
||||
type: 'userVal'
|
||||
value: any
|
||||
__meta: Metadata[]
|
||||
}
|
||||
|
||||
type MemoryItem = UserVal | SketchGroup | ExtrudeGroup
|
||||
export type PathToNode = [string | number, string][]
|
||||
|
||||
interface Memory {
|
||||
[key: string]: MemoryItem
|
||||
@ -102,12 +27,12 @@ interface Memory {
|
||||
|
||||
export interface ProgramMemory {
|
||||
root: Memory
|
||||
return?: ProgramReturn
|
||||
return: ProgramReturn | null
|
||||
}
|
||||
|
||||
export const executor = async (
|
||||
node: Program,
|
||||
programMemory: ProgramMemory = { root: {} },
|
||||
programMemory: ProgramMemory = { root: {}, return: null },
|
||||
engineCommandManager: EngineCommandManager,
|
||||
// work around while the gemotry is still be stored on the frontend
|
||||
// will be removed when the stream UI is added.
|
||||
@ -132,7 +57,7 @@ export const executor = async (
|
||||
|
||||
export const _executor = async (
|
||||
node: Program,
|
||||
programMemory: ProgramMemory = { root: {} },
|
||||
programMemory: ProgramMemory = { root: {}, return: null },
|
||||
engineCommandManager: EngineCommandManager
|
||||
): Promise<ProgramMemory> => {
|
||||
try {
|
||||
|
@ -114,7 +114,8 @@ describe('Testing addSketchTo', () => {
|
||||
expect(str).toBe(`const part001 = startSketchAt('default')
|
||||
|> ry(90, %)
|
||||
|> line('default', %)
|
||||
show(part001)`)
|
||||
show(part001)
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
@ -179,7 +180,10 @@ describe('Testing moveValueIntoNewVariable', () => {
|
||||
return x
|
||||
}
|
||||
`
|
||||
const code = `${fn('def')}${fn('ghi')}${fn('jkl')}${fn('hmm')}
|
||||
const code = `${fn('def')}${fn('jkl')}${fn('hmm')}
|
||||
fn ghi = (x) => {
|
||||
return 2
|
||||
}
|
||||
const abc = 3
|
||||
const identifierGuy = 5
|
||||
const yo = 5 + 6
|
||||
|
@ -28,6 +28,46 @@ import {
|
||||
createFirstArg,
|
||||
} from './std/sketch'
|
||||
|
||||
export function addStartSketch(
|
||||
node: Program,
|
||||
start: [number, number],
|
||||
end: [number, number]
|
||||
): { modifiedAst: Program; id: string; pathToNode: PathToNode } {
|
||||
const _node = { ...node }
|
||||
const _name = findUniqueName(node, 'part')
|
||||
|
||||
const startSketchAt = createCallExpression('startSketchAt', [
|
||||
createArrayExpression([createLiteral(start[0]), createLiteral(start[1])]),
|
||||
])
|
||||
const initialLineTo = createCallExpression('line', [
|
||||
createArrayExpression([createLiteral(end[0]), createLiteral(end[1])]),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
|
||||
const pipeBody = [startSketchAt, initialLineTo]
|
||||
|
||||
const variableDeclaration = createVariableDeclaration(
|
||||
_name,
|
||||
createPipeExpression(pipeBody)
|
||||
)
|
||||
|
||||
_node.body = [...node.body, variableDeclaration]
|
||||
|
||||
let pathToNode: PathToNode = [
|
||||
['body', ''],
|
||||
['0', 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
['0', 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
]
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
id: _name,
|
||||
pathToNode,
|
||||
}
|
||||
}
|
||||
|
||||
export function addSketchTo(
|
||||
node: Program,
|
||||
axis: 'xy' | 'xz' | 'yz',
|
||||
|
@ -11,26 +11,27 @@ describe('recast', () => {
|
||||
const code = '1 + 2'
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it('variable declaration', () => {
|
||||
const code = 'const myVar = 5'
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it("variable declaration that's binary with string", () => {
|
||||
const code = "const myVar = 5 + 'yo'"
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
const codeWithOtherQuotes = 'const myVar = 5 + "yo"'
|
||||
const { ast: ast2 } = code2ast(codeWithOtherQuotes)
|
||||
expect(recast(ast2)).toBe(codeWithOtherQuotes)
|
||||
expect(recast(ast2).trim()).toBe(codeWithOtherQuotes)
|
||||
})
|
||||
it('test assigning two variables, the second summing with the first', () => {
|
||||
const code = `const myVar = 5
|
||||
const newVar = myVar + 1`
|
||||
const newVar = myVar + 1
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -42,11 +43,12 @@ const newVar = myVar + 1`
|
||||
)
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('test with function call', () => {
|
||||
const code = `const myVar = "hello"
|
||||
log(5, myVar)`
|
||||
log(5, myVar)
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -61,7 +63,7 @@ log(5, myVar)`
|
||||
].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it('recast sketch declaration', () => {
|
||||
let code = `const mySketch = startSketchAt([0, 0])
|
||||
@ -70,10 +72,11 @@ 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())
|
||||
expect(recasted).toBe(code)
|
||||
})
|
||||
it('sketch piped into callExpression', () => {
|
||||
const code = [
|
||||
@ -85,7 +88,7 @@ show(mySketch)`
|
||||
].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('recast BinaryExpression piped into CallExpression', () => {
|
||||
const code = [
|
||||
@ -97,37 +100,37 @@ show(mySketch)`
|
||||
].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it('recast nested binary expression', () => {
|
||||
const code = ['const myVar = 1 + 2 * 5'].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('recast nested binary expression with parans', () => {
|
||||
const code = ['const myVar = 1 + (1 + 2) * 5'].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('unnecessary paran wrap will be remove', () => {
|
||||
const code = ['const myVar = 1 + (2 * 5)'].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.replace('(', '').replace(')', ''))
|
||||
expect(recasted.trim()).toBe(code.replace('(', '').replace(')', ''))
|
||||
})
|
||||
it('complex nested binary expression', () => {
|
||||
const code = ['1 * ((2 + 3) / 4 + 5)'].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('multiplied paren expressions', () => {
|
||||
const code = ['3 + (1 + 2) * (3 + 4)'].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('recast array declaration', () => {
|
||||
const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join(
|
||||
@ -135,7 +138,7 @@ show(mySketch)`
|
||||
)
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('recast long array declaration', () => {
|
||||
const code = [
|
||||
@ -150,7 +153,7 @@ show(mySketch)`
|
||||
].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted.trim()).toBe(code.trim())
|
||||
})
|
||||
it('recast long object exectution', () => {
|
||||
const code = `const three = 3
|
||||
@ -159,26 +162,29 @@ const yo = {
|
||||
anum: 2,
|
||||
identifier: three,
|
||||
binExp: 4 + 5
|
||||
}`
|
||||
}
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted).toBe(code)
|
||||
})
|
||||
it('recast short object exectution', () => {
|
||||
const code = `const yo = { key: 'val' }`
|
||||
const code = `const yo = { key: 'val' }
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted).toBe(code)
|
||||
})
|
||||
it('recast object execution with member expression', () => {
|
||||
const code = `const yo = { a: { b: { c: '123' } } }
|
||||
const key = 'c'
|
||||
const myVar = yo.a['b'][key]
|
||||
const key2 = 'b'
|
||||
const myVar2 = yo['a'][key2].c`
|
||||
const myVar2 = yo['a'][key2].c
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
expect(recasted).toBe(code)
|
||||
})
|
||||
})
|
||||
|
||||
@ -186,7 +192,8 @@ describe('testing recasting with comments and whitespace', () => {
|
||||
it('code with comments', () => {
|
||||
const code = `const yo = { a: { b: { c: '123' } } }
|
||||
// this is a comment
|
||||
const key = 'c'`
|
||||
const key = 'c'
|
||||
`
|
||||
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
@ -199,7 +206,8 @@ const key = 'c'`
|
||||
/* this is
|
||||
a
|
||||
comment */
|
||||
const yo = 'bing'`
|
||||
const yo = 'bing'
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -209,7 +217,8 @@ const yo = 'bing'`
|
||||
const yo = { a: { b: { c: '123' } } }
|
||||
const key = 'c'
|
||||
|
||||
// this is also a comment`
|
||||
// this is also a comment
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -223,7 +232,8 @@ const key = 'c'
|
||||
comment */
|
||||
const key = 'c'
|
||||
// this is also a comment
|
||||
}`
|
||||
}
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -239,7 +249,7 @@ const key = 'c'
|
||||
].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it('comments sprinkled in all over the place', () => {
|
||||
const code = `
|
||||
@ -261,7 +271,8 @@ const mySk1 = startSketchAt([0, 0])
|
||||
|> rx(45, %)
|
||||
/*
|
||||
one more for good measure
|
||||
*/`
|
||||
*/
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(`// comment at start
|
||||
@ -278,7 +289,8 @@ a comment between pipe expression statements */
|
||||
// and another with just white space between others below
|
||||
|> ry(45, %)
|
||||
|> rx(45, %)
|
||||
// one more for good measure`)
|
||||
// one more for good measure
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
@ -287,19 +299,19 @@ describe('testing call Expressions in BinaryExpressions and UnaryExpressions', (
|
||||
const code = 'const myVar = 2 + min(100, legLen(5, 3))'
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it('nested callExpression in unaryExpression', () => {
|
||||
const code = 'const myVar = -min(100, legLen(5, 3))'
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it('with unaryExpression in callExpression', () => {
|
||||
const code = 'const myVar = min(5, -legLen(5, 4))'
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
it('with unaryExpression in sketch situation', () => {
|
||||
const code = [
|
||||
@ -308,7 +320,7 @@ describe('testing call Expressions in BinaryExpressions and UnaryExpressions', (
|
||||
].join('\n')
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
expect(recasted.trim()).toBe(code)
|
||||
})
|
||||
})
|
||||
|
||||
@ -318,12 +330,13 @@ 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)`
|
||||
show(part001)
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -333,7 +346,8 @@ show(part001)`
|
||||
angle: 201,
|
||||
offset: -1.35,
|
||||
intersectTag: 'seg01'
|
||||
}, %)`
|
||||
}, %)
|
||||
`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code)
|
||||
@ -342,7 +356,8 @@ show(part001)`
|
||||
|
||||
describe('it recasts binary expression using brackets where needed', () => {
|
||||
it('when there are two minus in a row', () => {
|
||||
const code = `const part001 = 1 - (def - abc)`
|
||||
const code = `const part001 = 1 - (def - abc)
|
||||
`
|
||||
const recasted = recast(code2ast(code).ast)
|
||||
expect(recasted).toBe(code)
|
||||
})
|
||||
|
@ -1,20 +1,23 @@
|
||||
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 { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { exportSave } from 'lib/exportSave'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import * as Sentry from '@sentry/react'
|
||||
|
||||
interface ResultCommand {
|
||||
let lastMessage = ''
|
||||
|
||||
interface CommandInfo {
|
||||
commandType: CommandTypes
|
||||
range: SourceRange
|
||||
parentId?: string
|
||||
}
|
||||
interface ResultCommand extends CommandInfo {
|
||||
type: 'result'
|
||||
data: any
|
||||
}
|
||||
interface PendingCommand {
|
||||
interface PendingCommand extends CommandInfo {
|
||||
type: 'pending'
|
||||
promise: Promise<any>
|
||||
resolve: (val: any) => void
|
||||
@ -34,6 +37,8 @@ interface NewTrackArgs {
|
||||
|
||||
type WebSocketResponse = Models['OkWebSocketResponseData_type']
|
||||
|
||||
type ClientMetrics = Models['ClientMetrics_type']
|
||||
|
||||
// EngineConnection encapsulates the connection(s) to the Engine
|
||||
// for the EngineCommandManager; namely, the underlying WebSocket
|
||||
// and WebRTC connections.
|
||||
@ -53,6 +58,9 @@ export class EngineConnection {
|
||||
private onClose: (engineConnection: EngineConnection) => void
|
||||
private onNewTrack: (track: NewTrackArgs) => void
|
||||
|
||||
// TODO: actual type is ClientMetrics
|
||||
private webrtcStatsCollector?: () => Promise<ClientMetrics>
|
||||
|
||||
constructor({
|
||||
url,
|
||||
token,
|
||||
@ -188,15 +196,17 @@ export class EngineConnection {
|
||||
)
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
handshakeSpan.promise,
|
||||
iceSpan.promise,
|
||||
dataChannelSpan.promise,
|
||||
mediaTrackSpan.promise,
|
||||
]).then(() => {
|
||||
console.log('All spans finished, reporting')
|
||||
webrtcMediaTransaction?.finish()
|
||||
})
|
||||
if (this.shouldTrace()) {
|
||||
Promise.all([
|
||||
handshakeSpan.promise,
|
||||
iceSpan.promise,
|
||||
dataChannelSpan.promise,
|
||||
mediaTrackSpan.promise,
|
||||
]).then(() => {
|
||||
console.log('All spans finished, reporting')
|
||||
webrtcMediaTransaction?.finish()
|
||||
})
|
||||
}
|
||||
|
||||
this.onWebsocketOpen(this)
|
||||
})
|
||||
@ -297,7 +307,9 @@ export class EngineConnection {
|
||||
|
||||
this.pc.addEventListener('connectionstatechange', (event) => {
|
||||
if (this.pc?.iceConnectionState === 'connected') {
|
||||
iceSpan.resolve?.()
|
||||
if (this.shouldTrace()) {
|
||||
iceSpan.resolve?.()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -330,6 +342,17 @@ export class EngineConnection {
|
||||
})
|
||||
})
|
||||
.catch(console.log)
|
||||
} else if (resp.type === 'metrics_request') {
|
||||
if (this.webrtcStatsCollector === undefined) {
|
||||
// TODO: Error message here?
|
||||
return
|
||||
}
|
||||
this.webrtcStatsCollector().then((client_metrics) => {
|
||||
this.send({
|
||||
type: 'metrics_response',
|
||||
metrics: client_metrics,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(paultag): This ought to be both controllable, as well as something
|
||||
@ -361,127 +384,58 @@ export class EngineConnection {
|
||||
})
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (mediaStream.getVideoTracks().length !== 1) {
|
||||
reject(new Error('too many video tracks to report'))
|
||||
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.
|
||||
let videoTrack = mediaStream.getVideoTracks()[0]
|
||||
this.pc?.getStats(videoTrack).then((videoTrackStats) => {
|
||||
// TODO(paultag): this needs type information from the KittyCAD typescript
|
||||
// library once it's updated
|
||||
let client_metrics: ClientMetrics = {
|
||||
rtc_frames_decoded: 0,
|
||||
rtc_frames_dropped: 0,
|
||||
rtc_frames_received: 0,
|
||||
rtc_frames_per_second: 0,
|
||||
rtc_freeze_count: 0,
|
||||
rtc_jitter_sec: 0.0,
|
||||
rtc_keyframes_decoded: 0,
|
||||
rtc_total_freezes_duration_sec: 0.0,
|
||||
}
|
||||
|
||||
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()
|
||||
// TODO(paultag): Since we can technically have multiple WebRTC
|
||||
// video tracks (even if the Server doesn't at the moment), we
|
||||
// ought to send stats for every video track(?), and add the stream
|
||||
// ID into it. This raises the cardinality of collected metrics
|
||||
// when/if we do, but for now, just report the one stream.
|
||||
|
||||
videoTrackStats.forEach((videoTrackReport) => {
|
||||
if (videoTrackReport.type === 'inbound-rtp') {
|
||||
client_metrics.rtc_frames_decoded =
|
||||
videoTrackReport.framesDecoded
|
||||
client_metrics.rtc_frames_dropped =
|
||||
videoTrackReport.framesDropped
|
||||
client_metrics.rtc_frames_received =
|
||||
videoTrackReport.framesReceived
|
||||
client_metrics.rtc_frames_per_second =
|
||||
videoTrackReport.framesPerSecond || 0
|
||||
client_metrics.rtc_freeze_count = videoTrackReport.freezeCount
|
||||
client_metrics.rtc_jitter_sec = videoTrackReport.jitter
|
||||
client_metrics.rtc_keyframes_decoded =
|
||||
videoTrackReport.keyFramesDecoded
|
||||
client_metrics.rtc_total_freezes_duration_sec =
|
||||
videoTrackReport.totalFreezesDuration
|
||||
} else if (videoTrackReport.type === 'transport') {
|
||||
// videoTrackReport.bytesReceived,
|
||||
// videoTrackReport.bytesSent,
|
||||
}
|
||||
})
|
||||
resolve(client_metrics)
|
||||
})
|
||||
}, VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS)
|
||||
})
|
||||
}
|
||||
|
||||
this.onNewTrack({
|
||||
@ -490,10 +444,6 @@ export class EngineConnection {
|
||||
})
|
||||
})
|
||||
|
||||
// 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
|
||||
|
||||
@ -537,6 +487,7 @@ export class EngineConnection {
|
||||
this.websocket = undefined
|
||||
this.pc = undefined
|
||||
this.unreliableDataChannel = undefined
|
||||
this.webrtcStatsCollector = undefined
|
||||
|
||||
this.onClose(this)
|
||||
this.ready = false
|
||||
@ -546,6 +497,8 @@ export class EngineConnection {
|
||||
export type EngineCommand = Models['WebSocketRequest_type']
|
||||
type ModelTypes = Models['OkModelingCmdResponse_type']['type']
|
||||
|
||||
type CommandTypes = Models['ModelingCmd_type']['type']
|
||||
|
||||
type UnreliableResponses = Extract<
|
||||
Models['OkModelingCmdResponse_type'],
|
||||
{ type: 'highlight_set_entity' }
|
||||
@ -687,15 +640,22 @@ export class EngineCommandManager {
|
||||
const resolve = command.resolve
|
||||
this.artifactMap[id] = {
|
||||
type: 'result',
|
||||
range: command.range,
|
||||
commandType: command.commandType,
|
||||
parentId: command.parentId ? command.parentId : undefined,
|
||||
data: modelingResponse,
|
||||
}
|
||||
resolve({
|
||||
id,
|
||||
commandType: command.commandType,
|
||||
range: command.range,
|
||||
data: modelingResponse,
|
||||
})
|
||||
} else {
|
||||
this.artifactMap[id] = {
|
||||
type: 'result',
|
||||
commandType: command?.commandType,
|
||||
range: command?.range,
|
||||
data: modelingResponse,
|
||||
}
|
||||
}
|
||||
@ -747,8 +707,29 @@ export class EngineCommandManager {
|
||||
delete this.unreliableSubscriptions[event][id]
|
||||
}
|
||||
endSession() {
|
||||
// this.websocket?.close()
|
||||
// socket.off('command')
|
||||
// TODO: instead of sending a single command with `object_ids: Object.keys(this.artifactMap)`
|
||||
// we need to loop over them each individualy because if the engine doesn't recognise a single
|
||||
// id the whole command fails.
|
||||
Object.entries(this.artifactMap).forEach(([id, artifact]) => {
|
||||
const artifactTypesToDelete: ArtifactMap[string]['commandType'][] = [
|
||||
// 'start_path' creates a new scene object for the path, which is why it needs to be deleted,
|
||||
// however all of the segments in the path are its children so there don't need to be deleted.
|
||||
// this fact is very opaque in the api and docs (as to what should can be deleted).
|
||||
// Using an array is the list is likely to grow.
|
||||
'start_path',
|
||||
]
|
||||
if (!artifactTypesToDelete.includes(artifact.commandType)) return
|
||||
|
||||
const deletCmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'remove_scene_objects',
|
||||
object_ids: [id],
|
||||
},
|
||||
}
|
||||
this.engineConnection?.send(deletCmd)
|
||||
})
|
||||
}
|
||||
cusorsSelected(selections: {
|
||||
otherSelections: Selections['otherSelections']
|
||||
@ -775,6 +756,13 @@ export class EngineCommandManager {
|
||||
})
|
||||
}
|
||||
sendSceneCommand(command: EngineCommand): Promise<any> {
|
||||
if (
|
||||
command.type === 'modeling_cmd_req' &&
|
||||
command.cmd.type !== lastMessage
|
||||
) {
|
||||
console.log('sending command', command.cmd.type)
|
||||
lastMessage = command.cmd.type
|
||||
}
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
console.log('socket not ready')
|
||||
return Promise.resolve()
|
||||
@ -782,7 +770,8 @@ export class EngineCommandManager {
|
||||
if (command.type !== 'modeling_cmd_req') return Promise.resolve()
|
||||
const cmd = command.cmd
|
||||
if (
|
||||
cmd.type === 'camera_drag_move' &&
|
||||
(cmd.type === 'camera_drag_move' ||
|
||||
cmd.type === 'handle_mouse_drag_move') &&
|
||||
this.engineConnection?.unreliableDataChannel
|
||||
) {
|
||||
cmd.sequence = this.outSequence
|
||||
@ -801,11 +790,20 @@ export class EngineCommandManager {
|
||||
JSON.stringify(command)
|
||||
)
|
||||
return Promise.resolve()
|
||||
} else if (
|
||||
cmd.type === 'mouse_move' &&
|
||||
this.engineConnection.unreliableDataChannel
|
||||
) {
|
||||
cmd.sequence = this.outSequence
|
||||
this.outSequence++
|
||||
this.engineConnection?.unreliableDataChannel?.send(
|
||||
JSON.stringify(command)
|
||||
)
|
||||
return Promise.resolve()
|
||||
}
|
||||
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)
|
||||
return this.handlePendingCommand(command.cmd_id, command.cmd)
|
||||
}
|
||||
sendModelingCommand({
|
||||
id,
|
||||
@ -823,15 +821,35 @@ export class EngineCommandManager {
|
||||
return Promise.resolve()
|
||||
}
|
||||
this.engineConnection?.send(command)
|
||||
return this.handlePendingCommand(id)
|
||||
if (typeof command !== 'string' && command.type === 'modeling_cmd_req') {
|
||||
return this.handlePendingCommand(id, command?.cmd, range)
|
||||
} else if (typeof command === 'string') {
|
||||
const parseCommand: EngineCommand = JSON.parse(command)
|
||||
if (parseCommand.type === 'modeling_cmd_req')
|
||||
return this.handlePendingCommand(id, parseCommand?.cmd, range)
|
||||
}
|
||||
throw 'shouldnt reach here'
|
||||
}
|
||||
handlePendingCommand(id: string) {
|
||||
handlePendingCommand(
|
||||
id: string,
|
||||
command: Models['ModelingCmd_type'],
|
||||
range?: SourceRange
|
||||
) {
|
||||
let resolve: (val: any) => void = () => {}
|
||||
const promise = new Promise((_resolve, reject) => {
|
||||
resolve = _resolve
|
||||
})
|
||||
const getParentId = (): string | undefined => {
|
||||
if (command.type === 'extend_path') {
|
||||
return command.path
|
||||
}
|
||||
// TODO handle other commands that have a parent
|
||||
}
|
||||
this.artifactMap[id] = {
|
||||
range: range || [0, 0],
|
||||
type: 'pending',
|
||||
commandType: command.type,
|
||||
parentId: getParentId(),
|
||||
promise,
|
||||
resolve,
|
||||
}
|
||||
|
@ -101,7 +101,8 @@ describe('testing changeSketchArguments', () => {
|
||||
|> ${line}
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
// |> rx(45, %)
|
||||
show(mySketch001)`
|
||||
show(mySketch001)
|
||||
`
|
||||
const code = genCode(lineToChange)
|
||||
const expectedCode = genCode(lineAfterChange)
|
||||
const ast = parser_wasm(code)
|
||||
@ -164,7 +165,8 @@ show(mySketch001)`
|
||||
|> lineTo([-1.59, -1.54], %)
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
|> lineTo([2, 3], %)
|
||||
show(mySketch001)`
|
||||
show(mySketch001)
|
||||
`
|
||||
expect(recast(modifiedAst)).toBe(expectedCode)
|
||||
})
|
||||
})
|
||||
@ -177,7 +179,8 @@ describe('testing addTagForSketchOnFace', () => {
|
||||
// |> rx(45, %)
|
||||
|> ${line}
|
||||
|> lineTo([0.46, -5.82], %)
|
||||
show(mySketch001)`
|
||||
show(mySketch001)
|
||||
`
|
||||
const code = genCode(originalLine)
|
||||
const ast = parser_wasm(code)
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
SketchGroup,
|
||||
SourceRange,
|
||||
PathToNode,
|
||||
MemoryItem,
|
||||
} from '../executor'
|
||||
import {
|
||||
Program,
|
||||
@ -20,7 +21,7 @@ import {
|
||||
getNodePathFromSourceRange,
|
||||
} from '../queryAst'
|
||||
import { GuiModes, toolTips, TooTip } from '../../useStore'
|
||||
import { splitPathAtPipeExpression } from '../modifyAst'
|
||||
import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
|
||||
import { generateUuidFromHashSeed } from '../../lib/uuid'
|
||||
|
||||
import { SketchLineHelper, ModifyAstBase, TransformCallback } from './stdTypes'
|
||||
@ -185,7 +186,7 @@ export const line: SketchLineHelper = {
|
||||
createCallback,
|
||||
}) => {
|
||||
const _node = { ...node }
|
||||
const { node: pipe } = getNodeFromPath<PipeExpression>(
|
||||
const { node: pipe } = getNodeFromPath<PipeExpression | CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
@ -197,12 +198,12 @@ export const line: SketchLineHelper = {
|
||||
)
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.root?.[variableName]
|
||||
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
|
||||
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
|
||||
|
||||
const newXVal = createLiteral(roundOff(to[0] - from[0], 2))
|
||||
const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
|
||||
|
||||
if (replaceExisting && createCallback) {
|
||||
if (replaceExisting && createCallback && pipe.type !== 'CallExpression') {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
[newXVal, newYVal],
|
||||
@ -220,7 +221,11 @@ export const line: SketchLineHelper = {
|
||||
createArrayExpression([newXVal, newYVal]),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
pipe.body = [...pipe.body, callExp]
|
||||
if (pipe.type === 'PipeExpression') {
|
||||
pipe.body = [...pipe.body, callExp]
|
||||
} else {
|
||||
varDec.init = createPipeExpression([varDec.init, callExp])
|
||||
}
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
@ -238,22 +243,10 @@ export const line: SketchLineHelper = {
|
||||
createLiteral(roundOff(to[1] - from[1], 2)),
|
||||
])
|
||||
|
||||
if (
|
||||
callExpression.arguments?.[0].type === 'Literal' &&
|
||||
callExpression.arguments?.[0].value === 'default'
|
||||
) {
|
||||
callExpression.arguments[0] = toArrExp
|
||||
} else if (callExpression.arguments?.[0].type === 'ObjectExpression') {
|
||||
if (callExpression.arguments?.[0].type === 'ObjectExpression') {
|
||||
const toProp = callExpression.arguments?.[0].properties?.find(
|
||||
({ key }) => key.name === 'to'
|
||||
)
|
||||
if (
|
||||
toProp &&
|
||||
toProp.value.type === 'Literal' &&
|
||||
toProp.value.value === 'default'
|
||||
) {
|
||||
toProp.value = toArrExp
|
||||
}
|
||||
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
|
||||
} else {
|
||||
mutateArrExp(callExpression.arguments?.[0], toArrExp)
|
||||
@ -546,7 +539,7 @@ export const angledLineOfXLength: SketchLineHelper = {
|
||||
)
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.root?.[variableName]
|
||||
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
|
||||
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
|
||||
const angle = createLiteral(roundOff(getAngle(from, to), 0))
|
||||
const xLength = createLiteral(roundOff(Math.abs(from[0] - to[0]), 2) || 0.1)
|
||||
const newLine = createCallback
|
||||
@ -619,7 +612,7 @@ export const angledLineOfYLength: SketchLineHelper = {
|
||||
)
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.root?.[variableName]
|
||||
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
|
||||
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
|
||||
|
||||
const angle = createLiteral(roundOff(getAngle(from, to), 0))
|
||||
const yLength = createLiteral(roundOff(Math.abs(from[1] - to[1]), 2) || 0.1)
|
||||
@ -876,7 +869,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
|
||||
const varName = varDec.declarations[0].id.name
|
||||
const sketchGroup = previousProgramMemory.root[varName] as SketchGroup
|
||||
const intersectPath = sketchGroup.value.find(
|
||||
({ name }) => name === intersectTagName
|
||||
({ name }: Path) => name === intersectTagName
|
||||
)
|
||||
let offset = 0
|
||||
if (intersectPath) {
|
||||
@ -968,60 +961,14 @@ export function addNewSketchLn({
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
const { node: pipeExp, shallowPath: pipePath } =
|
||||
getNodeFromPath<PipeExpression>(node, pathToNode, 'PipeExpression')
|
||||
const maybeStartSketchAt = pipeExp.body.find(
|
||||
(exp) =>
|
||||
exp.type === 'CallExpression' &&
|
||||
exp.callee.name === 'startSketchAt' &&
|
||||
exp.arguments[0].type === 'Literal' &&
|
||||
exp.arguments[0].value === 'default'
|
||||
)
|
||||
const maybeDefaultLine = pipeExp.body.findIndex(
|
||||
(exp) =>
|
||||
exp.type === 'CallExpression' &&
|
||||
exp.callee.name === 'line' &&
|
||||
exp.arguments[0].type === 'Literal' &&
|
||||
exp.arguments[0].value === 'default'
|
||||
)
|
||||
const defaultLinePath: PathToNode = [
|
||||
...pipePath,
|
||||
['body', ''],
|
||||
[maybeDefaultLine, ''],
|
||||
]
|
||||
const { node: pipeExp, shallowPath: pipePath } = getNodeFromPath<
|
||||
PipeExpression | CallExpression
|
||||
>(node, pathToNode, 'PipeExpression')
|
||||
const variableName = varDec.id.name
|
||||
const sketch = previousProgramMemory?.root?.[variableName]
|
||||
if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup')
|
||||
if (sketch.type !== 'SketchGroup') throw new Error('not a SketchGroup')
|
||||
|
||||
if (maybeStartSketchAt) {
|
||||
const startSketchAt = maybeStartSketchAt as any
|
||||
startSketchAt.arguments[0] = createArrayExpression([
|
||||
createLiteral(to[0]),
|
||||
createLiteral(to[1]),
|
||||
])
|
||||
return {
|
||||
modifiedAst: node,
|
||||
}
|
||||
}
|
||||
if (maybeDefaultLine !== -1) {
|
||||
const defaultLine = getNodeFromPath<CallExpression>(
|
||||
node,
|
||||
defaultLinePath
|
||||
).node
|
||||
const { from } = getSketchSegmentFromSourceRange(sketch, [
|
||||
defaultLine.start,
|
||||
defaultLine.end,
|
||||
]).segment
|
||||
return updateArgs({
|
||||
node,
|
||||
previousProgramMemory,
|
||||
pathToNode: defaultLinePath,
|
||||
to,
|
||||
from,
|
||||
})
|
||||
}
|
||||
|
||||
const last = sketch.value[sketch.value.length - 1]
|
||||
const last = sketch.value[sketch.value.length - 1] || sketch.start
|
||||
const from = last.to
|
||||
|
||||
return add({
|
||||
@ -1198,14 +1145,6 @@ function getFirstArgValuesForXYFns(callExpression: CallExpression): {
|
||||
} {
|
||||
// used for lineTo, line
|
||||
const firstArg = callExpression.arguments[0]
|
||||
if (firstArg.type === 'Literal' && firstArg.value === 'default') {
|
||||
return {
|
||||
val:
|
||||
callExpression.callee.name === 'startSketchAt'
|
||||
? [createLiteral(0), createLiteral(0)]
|
||||
: [createLiteral(1), createLiteral(1)],
|
||||
}
|
||||
}
|
||||
if (firstArg.type === 'ArrayExpression') {
|
||||
return { val: [firstArg.elements[0], firstArg.elements[1]] }
|
||||
}
|
||||
@ -1215,8 +1154,6 @@ function getFirstArgValuesForXYFns(callExpression: CallExpression): {
|
||||
if (to?.type === 'ArrayExpression') {
|
||||
const [x, y] = to.elements
|
||||
return { val: [x, y], tag }
|
||||
} else if (to?.type === 'Literal' && to.value === 'default') {
|
||||
return { val: [createLiteral(0), createLiteral(0)], tag }
|
||||
}
|
||||
}
|
||||
throw new Error('expected ArrayExpression or ObjectExpression')
|
||||
|
@ -59,19 +59,19 @@ 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' }, %)`,
|
||||
@ -144,9 +144,9 @@ 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,9 +172,9 @@ 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,9 +201,9 @@ 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',
|
||||
@ -401,6 +401,11 @@ 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({
|
||||
to: [0, 0.04],
|
||||
from: [0, 0.04],
|
||||
name: '',
|
||||
type: 'base',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
VariableDeclarator,
|
||||
CallExpression,
|
||||
} from '../abstractSyntaxTreeTypes'
|
||||
import { SketchGroup, SourceRange } from '../executor'
|
||||
import { SketchGroup, SourceRange, Path } from '../executor'
|
||||
|
||||
export function getSketchSegmentFromSourceRange(
|
||||
sketchGroup: SketchGroup,
|
||||
@ -20,10 +20,10 @@ export function getSketchSegmentFromSourceRange(
|
||||
startSourceRange[1] >= rangeEnd &&
|
||||
sketchGroup.start
|
||||
)
|
||||
return { segment: sketchGroup.start, index: -1 }
|
||||
return { segment: { ...sketchGroup.start, type: 'base' }, index: -1 }
|
||||
|
||||
const lineIndex = sketchGroup.value.findIndex(
|
||||
({ __geoMeta: { sourceRange } }) =>
|
||||
({ __geoMeta: { sourceRange } }: Path) =>
|
||||
sourceRange[0] <= rangeStart && sourceRange[1] >= rangeEnd
|
||||
)
|
||||
const line = sketchGroup.value[lineIndex]
|
||||
|
@ -124,7 +124,8 @@ const part001 = startSketchAt([0, 0])
|
||||
|> yLine(1.04, %) // ln-yLine-free should sub in segLen
|
||||
|> xLineTo(30, %) // ln-xLineTo-free should convert to xLine
|
||||
|> yLineTo(20, %) // ln-yLineTo-free should convert to yLine
|
||||
show(part001)`
|
||||
show(part001)
|
||||
`
|
||||
const expectModifiedScript = `const myVar = 3
|
||||
const myVar2 = 5
|
||||
const myVar3 = 6
|
||||
@ -133,69 +134,70 @@ 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
|
||||
|> yLine(segLen('seg01', %), %) // ln-yLineTo-free should convert to yLine
|
||||
show(part001)`
|
||||
show(part001)
|
||||
`
|
||||
it('should transform the ast', async () => {
|
||||
const ast = parser_wasm(inputScript)
|
||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||
@ -254,7 +256,8 @@ const part001 = startSketchAt([0, 0])
|
||||
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
||||
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
||||
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
||||
show(part001)`
|
||||
show(part001)
|
||||
`
|
||||
it('should transform horizontal lines the ast', async () => {
|
||||
const expectModifiedScript = `const myVar = 2
|
||||
const myVar2 = 12
|
||||
@ -281,7 +284,8 @@ const part001 = startSketchAt([0, 0])
|
||||
|> angledLineToY([223, 7.68], %) // select for vertical constraint 9
|
||||
|> xLineTo(myVar3, %) // select for horizontal constraint 10
|
||||
|> angledLineToY([301, myVar], %) // select for vertical constraint 10
|
||||
show(part001)`
|
||||
show(part001)
|
||||
`
|
||||
const ast = parser_wasm(inputScript)
|
||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||
.split('\n')
|
||||
@ -338,7 +342,8 @@ const part001 = startSketchAt([0, 0])
|
||||
|> yLineTo(7.68, %) // select for vertical constraint 9
|
||||
|> angledLineToX([333, myVar3], %) // select for horizontal constraint 10
|
||||
|> yLineTo(myVar, %) // select for vertical constraint 10
|
||||
show(part001)`
|
||||
show(part001)
|
||||
`
|
||||
const ast = parser_wasm(inputScript)
|
||||
const selectionRanges: Selections['codeBasedSelections'] = inputScript
|
||||
.split('\n')
|
||||
@ -380,7 +385,8 @@ const part001 = startSketchAt([0, 0])
|
||||
|> line([0.45, 1.46], %) // free
|
||||
|> line([myVar, 0.01], %) // xRelative
|
||||
|> line([0.7, myVar], %) // yRelative
|
||||
show(part001)`
|
||||
show(part001)
|
||||
`
|
||||
it('testing for free to horizontal and vertical distance', async () => {
|
||||
const expectedHorizontalCode = await helperThing(
|
||||
inputScript,
|
||||
@ -406,9 +412,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 +423,9 @@ show(part001)`
|
||||
'setHorzDistance'
|
||||
)
|
||||
expect(expectedCode).toContain(`|> lineTo([
|
||||
segEndX('seg01', %) + 2.6,
|
||||
lastSegY(%) + myVar
|
||||
], %) // yRelative`)
|
||||
segEndX('seg01', %) + 2.6,
|
||||
lastSegY(%) + myVar
|
||||
], %) // yRelative`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -28,6 +28,7 @@ import { createFirstArg, getFirstArg, replaceSketchLine } from './sketch'
|
||||
import { PathToNode, ProgramMemory } from '../executor'
|
||||
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
||||
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
|
||||
import { MemoryItem } from 'wasm-lib/kcl/bindings/MemoryItem'
|
||||
|
||||
type LineInputsType =
|
||||
| 'xAbsolute'
|
||||
@ -1426,7 +1427,6 @@ export function transformAstSketchLines({
|
||||
selectionRanges.codeBasedSelections.forEach(({ range }, index) => {
|
||||
const callBack = transformInfos?.[index].createNode
|
||||
const transformTo = transformInfos?.[index].tooltip
|
||||
console.log('transformTo', transformInfos)
|
||||
if (!callBack || !transformTo) throw new Error('no callback helper')
|
||||
|
||||
const getNode = getNodeFromPathCurry(
|
||||
@ -1453,7 +1453,7 @@ export function transformAstSketchLines({
|
||||
|
||||
const varName = varDec.id.name
|
||||
const sketchGroup = programMemory.root?.[varName]
|
||||
if (!sketchGroup || sketchGroup.type !== 'sketchGroup')
|
||||
if (!sketchGroup || sketchGroup.type !== 'SketchGroup')
|
||||
throw new Error('not a sketch group')
|
||||
const seg = getSketchSegmentFromSourceRange(sketchGroup, range).segment
|
||||
const referencedSegment = referencedSegmentRange
|
||||
|
156
src/lib/cameraControls.ts
Normal file
156
src/lib/cameraControls.ts
Normal file
@ -0,0 +1,156 @@
|
||||
const noModifiersPressed = (e: React.MouseEvent) =>
|
||||
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
|
||||
|
||||
export type CameraSystem =
|
||||
| 'KittyCAD'
|
||||
| 'OnShape'
|
||||
| 'Trackpad Friendly'
|
||||
| 'Solidworks'
|
||||
| 'NX'
|
||||
| 'Creo'
|
||||
| 'AutoCAD'
|
||||
|
||||
export const cameraSystems: CameraSystem[] = [
|
||||
'KittyCAD',
|
||||
'OnShape',
|
||||
'Trackpad Friendly',
|
||||
'Solidworks',
|
||||
'NX',
|
||||
'Creo',
|
||||
'AutoCAD',
|
||||
]
|
||||
|
||||
interface MouseGuardHandler {
|
||||
description: string
|
||||
callback: (e: React.MouseEvent) => boolean
|
||||
lenientDragStartButton?: number
|
||||
}
|
||||
|
||||
interface MouseGuardZoomHandler {
|
||||
description: string
|
||||
dragCallback: (e: React.MouseEvent) => boolean
|
||||
scrollCallback: (e: React.MouseEvent) => boolean
|
||||
lenientDragStartButton?: number
|
||||
}
|
||||
|
||||
interface MouseGuard {
|
||||
pan: MouseGuardHandler
|
||||
zoom: MouseGuardZoomHandler
|
||||
rotate: MouseGuardHandler
|
||||
}
|
||||
|
||||
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
|
||||
KittyCAD: {
|
||||
pan: {
|
||||
description: 'Right click + Shift + drag or middle click + drag',
|
||||
callback: (e) =>
|
||||
(e.button === 1 && noModifiersPressed(e)) ||
|
||||
(e.button === 2 && e.shiftKey),
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Right click + Ctrl + drag',
|
||||
dragCallback: (e) => e.button === 2 && e.ctrlKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Right click + drag',
|
||||
callback: (e) => e.button === 2 && noModifiersPressed(e),
|
||||
},
|
||||
},
|
||||
OnShape: {
|
||||
pan: {
|
||||
description: 'Right click + Ctrl + drag or middle click + drag',
|
||||
callback: (e) =>
|
||||
(e.button === 2 && e.ctrlKey) ||
|
||||
(e.button === 1 && noModifiersPressed(e)),
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel',
|
||||
dragCallback: () => false,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Right click + drag',
|
||||
callback: (e) => e.button === 2 && noModifiersPressed(e),
|
||||
},
|
||||
},
|
||||
'Trackpad Friendly': {
|
||||
pan: {
|
||||
description: 'Left click + Alt + Shift + drag or middle click + drag',
|
||||
callback: (e) =>
|
||||
(e.button === 0 && e.altKey && e.shiftKey && !e.metaKey) ||
|
||||
(e.button === 1 && noModifiersPressed(e)),
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Left click + Alt + OS + drag',
|
||||
dragCallback: (e) => e.button === 0 && e.altKey && e.metaKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Left click + Alt + drag',
|
||||
callback: (e) => e.button === 0 && e.altKey && !e.shiftKey && !e.metaKey,
|
||||
lenientDragStartButton: 0,
|
||||
},
|
||||
},
|
||||
Solidworks: {
|
||||
pan: {
|
||||
description: 'Right click + Ctrl + drag',
|
||||
callback: (e) => e.button === 2 && e.ctrlKey,
|
||||
lenientDragStartButton: 2,
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Middle click + Shift + drag',
|
||||
dragCallback: (e) => e.button === 1 && e.shiftKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Middle click + drag',
|
||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||
},
|
||||
},
|
||||
NX: {
|
||||
pan: {
|
||||
description: 'Middle click + Shift + drag',
|
||||
callback: (e) => e.button === 1 && e.shiftKey,
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Middle click + Ctrl + drag',
|
||||
dragCallback: (e) => e.button === 1 && e.ctrlKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Middle click + drag',
|
||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||
},
|
||||
},
|
||||
Creo: {
|
||||
pan: {
|
||||
description: 'Middle click + Shift + drag',
|
||||
callback: (e) => e.button === 1 && e.shiftKey,
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel or Middle click + Ctrl + drag',
|
||||
dragCallback: (e) => e.button === 1 && e.ctrlKey,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Middle click + drag',
|
||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||
},
|
||||
},
|
||||
AutoCAD: {
|
||||
pan: {
|
||||
description: 'Middle click + drag',
|
||||
callback: (e) => e.button === 1 && noModifiersPressed(e),
|
||||
},
|
||||
zoom: {
|
||||
description: 'Scroll wheel',
|
||||
dragCallback: () => false,
|
||||
scrollCallback: () => true,
|
||||
},
|
||||
rotate: {
|
||||
description: 'Middle click + Shift + drag',
|
||||
callback: (e) => e.button === 1 && e.shiftKey,
|
||||
},
|
||||
},
|
||||
}
|
@ -5,7 +5,7 @@ import {
|
||||
readDir,
|
||||
writeTextFile,
|
||||
} from '@tauri-apps/api/fs'
|
||||
import { documentDir } from '@tauri-apps/api/path'
|
||||
import { documentDir, homeDir } from '@tauri-apps/api/path'
|
||||
import { isTauri } from './isTauri'
|
||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||
import { metadata } from 'tauri-plugin-fs-extra-api'
|
||||
@ -32,7 +32,13 @@ export async function initializeProjectDirectory(directory: string) {
|
||||
return directory
|
||||
}
|
||||
|
||||
const docDirectory = await documentDir()
|
||||
let docDirectory: string
|
||||
try {
|
||||
docDirectory = await documentDir()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
docDirectory = await homeDir() // seems to work better on Linux
|
||||
}
|
||||
|
||||
const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER
|
||||
|
||||
|
@ -49,7 +49,7 @@ class MockEngineCommandManager {
|
||||
|
||||
export async function enginelessExecutor(
|
||||
ast: Program,
|
||||
pm: ProgramMemory = { root: {} }
|
||||
pm: ProgramMemory = { root: {}, return: null }
|
||||
): Promise<ProgramMemory> {
|
||||
const mockEngineCommandManager = new MockEngineCommandManager({
|
||||
setIsStreamReady: () => {},
|
||||
@ -64,7 +64,7 @@ export async function enginelessExecutor(
|
||||
|
||||
export async function executor(
|
||||
ast: Program,
|
||||
pm: ProgramMemory = { root: {} }
|
||||
pm: ProgramMemory = { root: {}, return: null }
|
||||
): Promise<ProgramMemory> {
|
||||
const engineCommandManager = new EngineCommandManager({
|
||||
setIsStreamReady: () => {},
|
||||
|
@ -56,6 +56,27 @@ export function throttle<T>(
|
||||
return throttled
|
||||
}
|
||||
|
||||
// takes a function and executes it after the wait time, if the function is called again before the wait time is up, the timer is reset
|
||||
export function defferExecution<T>(func: (args: T) => any, wait: number) {
|
||||
let timeout: ReturnType<typeof setTimeout> | null
|
||||
let latestArgs: T
|
||||
|
||||
function later() {
|
||||
timeout = null
|
||||
func(latestArgs)
|
||||
}
|
||||
|
||||
function deffered(args: T) {
|
||||
latestArgs = args
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(later, wait)
|
||||
}
|
||||
|
||||
return deffered
|
||||
}
|
||||
|
||||
export function getNormalisedCoordinates({
|
||||
clientX,
|
||||
clientY,
|
||||
|
@ -118,16 +118,14 @@ 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,29 +1,54 @@
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { BaseUnit, baseUnitsUnion } from '../useStore'
|
||||
import { CommandBarMeta } from '../lib/commands'
|
||||
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
|
||||
import { CameraSystem, cameraSystems } from 'lib/cameraControls'
|
||||
|
||||
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
|
||||
|
||||
export enum UnitSystem {
|
||||
Imperial = 'imperial',
|
||||
Metric = 'metric',
|
||||
}
|
||||
|
||||
export const baseUnits = {
|
||||
imperial: ['in', 'ft'],
|
||||
metric: ['mm', 'cm', 'm'],
|
||||
} as const
|
||||
|
||||
export type BaseUnit = 'in' | 'ft' | 'mm' | 'cm' | 'm'
|
||||
|
||||
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
|
||||
|
||||
export type Toggle = 'On' | 'Off'
|
||||
|
||||
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
|
||||
|
||||
export const settingsCommandBarMeta: CommandBarMeta = {
|
||||
'Set Theme': {
|
||||
displayValue: (args: string[]) => 'Change the app theme',
|
||||
'Set Base Unit': {
|
||||
displayValue: (args: string[]) => 'Set your default base unit',
|
||||
args: [
|
||||
{
|
||||
name: 'theme',
|
||||
name: 'baseUnit',
|
||||
type: 'select',
|
||||
defaultValue: 'theme',
|
||||
options: Object.values(Themes).map((v) => ({ name: v })) as {
|
||||
name: string
|
||||
}[],
|
||||
defaultValue: 'baseUnit',
|
||||
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Camera Controls': {
|
||||
displayValue: (args: string[]) => 'Set your camera controls',
|
||||
args: [
|
||||
{
|
||||
name: 'cameraControls',
|
||||
type: 'select',
|
||||
defaultValue: 'cameraControls',
|
||||
options: Object.values(cameraSystems).map((v) => ({ name: v })),
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Default Directory': {
|
||||
hide: 'both',
|
||||
},
|
||||
'Set Default Project Name': {
|
||||
displayValue: (args: string[]) => 'Set a new default project name',
|
||||
hide: 'web',
|
||||
@ -37,9 +62,33 @@ export const settingsCommandBarMeta: CommandBarMeta = {
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Default Directory': {
|
||||
'Set Onboarding Status': {
|
||||
hide: 'both',
|
||||
},
|
||||
'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 Theme': {
|
||||
displayValue: (args: string[]) => 'Change the app theme',
|
||||
args: [
|
||||
{
|
||||
name: 'theme',
|
||||
type: 'select',
|
||||
defaultValue: 'theme',
|
||||
options: Object.values(Themes).map((v): { name: string } => ({
|
||||
name: v,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
'Set Unit System': {
|
||||
displayValue: (args: string[]) => 'Set your default unit system',
|
||||
args: [
|
||||
@ -51,20 +100,6 @@ export const settingsCommandBarMeta: CommandBarMeta = {
|
||||
},
|
||||
],
|
||||
},
|
||||
'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 Onboarding Status': {
|
||||
hide: 'both',
|
||||
},
|
||||
}
|
||||
|
||||
export const settingsMachine = createMachine(
|
||||
@ -73,35 +108,34 @@ export const settingsMachine = createMachine(
|
||||
id: 'Settings',
|
||||
predictableActionArguments: true,
|
||||
context: {
|
||||
theme: Themes.System,
|
||||
defaultProjectName: '',
|
||||
unitSystem: UnitSystem.Imperial,
|
||||
baseUnit: 'in' as BaseUnit,
|
||||
cameraControls: 'KittyCAD' as CameraSystem,
|
||||
defaultDirectory: '',
|
||||
showDebugPanel: false,
|
||||
defaultProjectName: DEFAULT_PROJECT_NAME,
|
||||
onboardingStatus: '',
|
||||
showDebugPanel: false,
|
||||
textWrapping: 'On' as Toggle,
|
||||
theme: Themes.System,
|
||||
unitSystem: UnitSystem.Imperial,
|
||||
},
|
||||
initial: 'idle',
|
||||
states: {
|
||||
idle: {
|
||||
entry: ['setThemeClass'],
|
||||
on: {
|
||||
'Set Theme': {
|
||||
'Set Base Unit': {
|
||||
actions: [
|
||||
assign({
|
||||
theme: (_, event) => event.data.theme,
|
||||
}),
|
||||
assign({ baseUnit: (_, event) => event.data.baseUnit }),
|
||||
'persistSettings',
|
||||
'toastSuccess',
|
||||
'setThemeClass',
|
||||
],
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
},
|
||||
'Set Default Project Name': {
|
||||
'Set Camera Controls': {
|
||||
actions: [
|
||||
assign({
|
||||
defaultProjectName: (_, event) => event.data.defaultProjectName,
|
||||
cameraControls: (_, event) => event.data.cameraControls,
|
||||
}),
|
||||
'persistSettings',
|
||||
'toastSuccess',
|
||||
@ -120,12 +154,11 @@ export const settingsMachine = createMachine(
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
},
|
||||
'Set Unit System': {
|
||||
'Set Default Project Name': {
|
||||
actions: [
|
||||
assign({
|
||||
unitSystem: (_, event) => event.data.unitSystem,
|
||||
baseUnit: (_, event) =>
|
||||
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
|
||||
defaultProjectName: (_, event) =>
|
||||
event.data.defaultProjectName.trim() || DEFAULT_PROJECT_NAME,
|
||||
}),
|
||||
'persistSettings',
|
||||
'toastSuccess',
|
||||
@ -133,9 +166,46 @@ export const settingsMachine = createMachine(
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
},
|
||||
'Set Base Unit': {
|
||||
'Set Onboarding Status': {
|
||||
actions: [
|
||||
assign({ baseUnit: (_, event) => event.data.baseUnit }),
|
||||
assign({
|
||||
onboardingStatus: (_, event) => event.data.onboardingStatus,
|
||||
}),
|
||||
'persistSettings',
|
||||
],
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
},
|
||||
'Set Text Wrapping': {
|
||||
actions: [
|
||||
assign({
|
||||
textWrapping: (_, event) => event.data.textWrapping,
|
||||
}),
|
||||
'persistSettings',
|
||||
'toastSuccess',
|
||||
],
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
},
|
||||
'Set Theme': {
|
||||
actions: [
|
||||
assign({
|
||||
theme: (_, event) => event.data.theme,
|
||||
}),
|
||||
'persistSettings',
|
||||
'toastSuccess',
|
||||
'setThemeClass',
|
||||
],
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
},
|
||||
'Set Unit System': {
|
||||
actions: [
|
||||
assign({
|
||||
unitSystem: (_, event) => event.data.unitSystem,
|
||||
baseUnit: (_, event) =>
|
||||
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
|
||||
}),
|
||||
'persistSettings',
|
||||
'toastSuccess',
|
||||
],
|
||||
@ -155,34 +225,29 @@ export const settingsMachine = createMachine(
|
||||
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 Base Unit'; data: { baseUnit: BaseUnit } }
|
||||
| {
|
||||
type: 'Set Camera Controls'
|
||||
data: { cameraControls: CameraSystem }
|
||||
}
|
||||
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
|
||||
| {
|
||||
type: 'Set Default Project Name'
|
||||
data: { defaultProjectName: string }
|
||||
}
|
||||
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
|
||||
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
|
||||
| { type: 'Set Text Wrapping'; data: { textWrapping: Toggle } }
|
||||
| { type: 'Set Theme'; data: { theme: Themes } }
|
||||
| {
|
||||
type: 'Set Unit System'
|
||||
data: { unitSystem: UnitSystem }
|
||||
}
|
||||
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
|
||||
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
|
||||
| { type: 'Toggle Debug Panel' },
|
||||
},
|
||||
},
|
||||
|
@ -15,25 +15,31 @@ export interface Typegen0 {
|
||||
eventsCausingActions: {
|
||||
persistSettings:
|
||||
| 'Set Base Unit'
|
||||
| 'Set Camera Controls'
|
||||
| '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 Camera Controls'
|
||||
| '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 Camera Controls'
|
||||
| 'Set Default Directory'
|
||||
| 'Set Default Project Name'
|
||||
| 'Set Text Wrapping'
|
||||
| 'Set Theme'
|
||||
| 'Set Unit System'
|
||||
| 'Toggle Debug Panel'
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine'
|
||||
|
||||
// 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.
|
||||
@ -38,6 +39,7 @@ const Home = () => {
|
||||
const {
|
||||
settings: {
|
||||
context: { defaultDirectory, defaultProjectName },
|
||||
send: sendToSettings,
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
|
||||
@ -71,16 +73,33 @@ const Home = () => {
|
||||
context: ContextFrom<typeof homeMachine>,
|
||||
event: EventFrom<typeof homeMachine, 'Create project'>
|
||||
) => {
|
||||
let name =
|
||||
let name = (
|
||||
event.data && 'name' in event.data
|
||||
? event.data.name
|
||||
: defaultProjectName
|
||||
).trim()
|
||||
let shouldUpdateDefaultProjectName = false
|
||||
|
||||
// If there is no default project name, flag it to be set to the default
|
||||
if (!name) {
|
||||
name = DEFAULT_PROJECT_NAME
|
||||
shouldUpdateDefaultProjectName = true
|
||||
}
|
||||
|
||||
if (doesProjectNameNeedInterpolated(name)) {
|
||||
const nextIndex = await getNextProjectIndex(name, projects)
|
||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||
}
|
||||
|
||||
await createNewProject(context.defaultDirectory + '/' + name)
|
||||
|
||||
if (shouldUpdateDefaultProjectName) {
|
||||
sendToSettings({
|
||||
type: 'Set Default Project Name',
|
||||
data: { defaultProjectName: DEFAULT_PROJECT_NAME },
|
||||
})
|
||||
}
|
||||
|
||||
return `Successfully created "${name}"`
|
||||
},
|
||||
renameProject: async (
|
||||
|
@ -4,8 +4,8 @@ import { onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { useStore } from '../../useStore'
|
||||
|
||||
export default function Units() {
|
||||
const { isMouseDownInStream } = useStore((s) => ({
|
||||
isMouseDownInStream: s.isMouseDownInStream,
|
||||
const { buttonDownInStream } = useStore((s) => ({
|
||||
buttonDownInStream: s.buttonDownInStream,
|
||||
}))
|
||||
const dismiss = useDismiss()
|
||||
const next = useNextClick(onboardingPaths.SKETCHING)
|
||||
@ -15,7 +15,7 @@ export default function Units() {
|
||||
<div
|
||||
className={
|
||||
'max-w-2xl flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' +
|
||||
(isMouseDownInStream ? '' : ' pointer-events-auto')
|
||||
(buttonDownInStream ? '' : ' pointer-events-auto')
|
||||
}
|
||||
>
|
||||
<h1 className="text-2xl font-bold">Camera</h1>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { BaseUnit, baseUnits } from '../../useStore'
|
||||
import { BaseUnit, baseUnits } from '../../machines/settingsMachine'
|
||||
import { ActionButton } from '../../components/ActionButton'
|
||||
import { SettingsSection } from '../Settings'
|
||||
import { Toggle } from '../../components/Toggle/Toggle'
|
||||
|
@ -6,13 +6,22 @@ import {
|
||||
import { ActionButton } from '../components/ActionButton'
|
||||
import { AppHeader } from '../components/AppHeader'
|
||||
import { open } from '@tauri-apps/api/dialog'
|
||||
import { BaseUnit, baseUnits } from '../useStore'
|
||||
import {
|
||||
BaseUnit,
|
||||
DEFAULT_PROJECT_NAME,
|
||||
baseUnits,
|
||||
} from '../machines/settingsMachine'
|
||||
import { Toggle } from '../components/Toggle/Toggle'
|
||||
import { useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { IndexLoaderData, paths } from '../Router'
|
||||
import { Themes } from '../lib/theme'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import {
|
||||
CameraSystem,
|
||||
cameraSystems,
|
||||
cameraMouseDragGuards,
|
||||
} from 'lib/cameraControls'
|
||||
import { UnitSystem } from 'machines/settingsMachine'
|
||||
|
||||
export const Settings = () => {
|
||||
@ -25,12 +34,13 @@ export const Settings = () => {
|
||||
send,
|
||||
state: {
|
||||
context: {
|
||||
baseUnit,
|
||||
cameraControls,
|
||||
defaultDirectory,
|
||||
defaultProjectName,
|
||||
showDebugPanel,
|
||||
defaultDirectory,
|
||||
unitSystem,
|
||||
baseUnit,
|
||||
theme,
|
||||
unitSystem,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -82,6 +92,42 @@ export const Settings = () => {
|
||||
, and start a discussion if you don't see it! Your feedback will help
|
||||
us prioritize what to build next.
|
||||
</p>
|
||||
<SettingsSection
|
||||
title="Camera Controls"
|
||||
description="How you want to control the camera in the 3D view"
|
||||
>
|
||||
<select
|
||||
id="camera-controls"
|
||||
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
||||
value={cameraControls}
|
||||
onChange={(e) => {
|
||||
send({
|
||||
type: 'Set Camera Controls',
|
||||
data: { cameraControls: e.target.value as CameraSystem },
|
||||
})
|
||||
}}
|
||||
>
|
||||
{cameraSystems.map((program) => (
|
||||
<option key={program} value={program}>
|
||||
{program}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ul className="text-sm my-2 mx-4 leading-relaxed">
|
||||
<li>
|
||||
<strong>Pan:</strong>{' '}
|
||||
{cameraMouseDragGuards[cameraControls].pan.description}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Zoom:</strong>{' '}
|
||||
{cameraMouseDragGuards[cameraControls].zoom.description}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Rotate:</strong>{' '}
|
||||
{cameraMouseDragGuards[cameraControls].rotate.description}
|
||||
</li>
|
||||
</ul>
|
||||
</SettingsSection>
|
||||
{(window as any).__TAURI__ && (
|
||||
<>
|
||||
<SettingsSection
|
||||
@ -118,10 +164,14 @@ export const Settings = () => {
|
||||
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
||||
defaultValue={defaultProjectName}
|
||||
onBlur={(e) => {
|
||||
const newValue = e.target.value.trim() || DEFAULT_PROJECT_NAME
|
||||
send({
|
||||
type: 'Set Default Project Name',
|
||||
data: { defaultProjectName: e.target.value },
|
||||
data: {
|
||||
defaultProjectName: newValue,
|
||||
},
|
||||
})
|
||||
e.target.value = newValue
|
||||
}}
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
|
408
src/useStore.ts
408
src/useStore.ts
@ -19,6 +19,7 @@ import {
|
||||
EngineCommandManager,
|
||||
} from './lang/std/engineConnection'
|
||||
import { KCLError } from './lang/errors'
|
||||
import { defferExecution } from 'lib/utils'
|
||||
|
||||
export type Selection = {
|
||||
type: 'default' | 'line-end' | 'line-mid'
|
||||
@ -42,9 +43,12 @@ export type TooTip =
|
||||
| 'yLineTo'
|
||||
| 'angledLineThatIntersects'
|
||||
|
||||
export const toolTips: TooTip[] = [
|
||||
'lineTo',
|
||||
export const toolTips = [
|
||||
'sketch_line',
|
||||
'move',
|
||||
// original tooltips
|
||||
'line',
|
||||
'lineTo',
|
||||
'angledLine',
|
||||
'angledLineOfXLength',
|
||||
'angledLineOfYLength',
|
||||
@ -55,7 +59,7 @@ export const toolTips: TooTip[] = [
|
||||
'xLineTo',
|
||||
'yLineTo',
|
||||
'angledLineThatIntersects',
|
||||
]
|
||||
] as any as TooTip[]
|
||||
|
||||
export type GuiModes =
|
||||
| {
|
||||
@ -65,6 +69,7 @@ export type GuiModes =
|
||||
mode: 'sketch'
|
||||
sketchMode: TooTip
|
||||
isTooltip: true
|
||||
waitingFirstClick: boolean
|
||||
rotation: Rotation
|
||||
position: Position
|
||||
id?: string
|
||||
@ -83,6 +88,7 @@ export type GuiModes =
|
||||
}
|
||||
| {
|
||||
mode: 'canEditSketch'
|
||||
pathId: string
|
||||
pathToNode: PathToNode
|
||||
rotation: Rotation
|
||||
position: Position
|
||||
@ -94,15 +100,6 @@ export type GuiModes =
|
||||
position: Position
|
||||
}
|
||||
|
||||
export const baseUnits = {
|
||||
imperial: ['in', 'ft'],
|
||||
metric: ['mm', 'cm', 'm'],
|
||||
} as const
|
||||
|
||||
export type BaseUnit = 'in' | 'ft' | 'mm' | 'cm' | 'm'
|
||||
|
||||
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
|
||||
|
||||
export type PaneType =
|
||||
| 'code'
|
||||
| 'variables'
|
||||
@ -130,8 +127,8 @@ export interface StoreState {
|
||||
kclErrors: KCLError[]
|
||||
addKCLError: (err: KCLError) => void
|
||||
resetKCLErrors: () => void
|
||||
ast: Program | null
|
||||
setAst: (ast: Program | null) => void
|
||||
ast: Program
|
||||
setAst: (ast: Program) => void
|
||||
updateAst: (
|
||||
ast: Program,
|
||||
optionalParams?: {
|
||||
@ -141,7 +138,9 @@ export interface StoreState {
|
||||
) => void
|
||||
updateAstAsync: (ast: Program, focusPath?: PathToNode) => void
|
||||
code: string
|
||||
defferedCode: string
|
||||
setCode: (code: string) => void
|
||||
defferedSetCode: (code: string) => void
|
||||
formatCode: () => void
|
||||
errorState: {
|
||||
isError: boolean
|
||||
@ -166,8 +165,8 @@ export interface StoreState {
|
||||
setIsStreamReady: (isStreamReady: boolean) => void
|
||||
isLSPServerReady: boolean
|
||||
setIsLSPServerReady: (isLSPServerReady: boolean) => void
|
||||
isMouseDownInStream: boolean
|
||||
setIsMouseDownInStream: (isMouseDownInStream: boolean) => void
|
||||
buttonDownInStream: number | undefined
|
||||
setButtonDownInStream: (buttonDownInStream: number | undefined) => void
|
||||
didDragInStream: boolean
|
||||
setDidDragInStream: (didDragInStream: boolean) => void
|
||||
fileId: string
|
||||
@ -177,6 +176,8 @@ export interface StoreState {
|
||||
streamWidth: number
|
||||
streamHeight: number
|
||||
}) => void
|
||||
isExecuting: boolean
|
||||
setIsExecuting: (isExecuting: boolean) => void
|
||||
|
||||
showHomeMenu: boolean
|
||||
setHomeShowMenu: (showMenu: boolean) => void
|
||||
@ -195,193 +196,220 @@ let pendingAstUpdates: number[] = []
|
||||
|
||||
export const useStore = create<StoreState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
editorView: null,
|
||||
setEditorView: (editorView) => {
|
||||
set({ editorView })
|
||||
},
|
||||
highlightRange: [0, 0],
|
||||
setHighlightRange: (selection) => {
|
||||
set({ highlightRange: selection })
|
||||
const editorView = get().editorView
|
||||
if (editorView) {
|
||||
editorView.dispatch({ effects: addLineHighlight.of(selection) })
|
||||
}
|
||||
},
|
||||
setCursor: (selections) => {
|
||||
const { editorView } = get()
|
||||
if (!editorView) return
|
||||
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
|
||||
const selectionRangeTypeMap: { [key: number]: Selection['type'] } = {}
|
||||
set({ selectionRangeTypeMap })
|
||||
selections.codeBasedSelections.forEach(({ range, type }) => {
|
||||
if (range?.[1]) {
|
||||
ranges.push(EditorSelection.cursor(range[1]))
|
||||
selectionRangeTypeMap[range[1]] = type
|
||||
(set, get) => {
|
||||
const setDefferedCode = defferExecution(
|
||||
(code: string) => set({ defferedCode: code }),
|
||||
600
|
||||
)
|
||||
return {
|
||||
editorView: null,
|
||||
setEditorView: (editorView) => {
|
||||
set({ editorView })
|
||||
},
|
||||
highlightRange: [0, 0],
|
||||
setHighlightRange: (selection) => {
|
||||
set({ highlightRange: selection })
|
||||
const editorView = get().editorView
|
||||
if (editorView) {
|
||||
editorView.dispatch({ effects: addLineHighlight.of(selection) })
|
||||
}
|
||||
})
|
||||
setTimeout(() => {
|
||||
editorView.dispatch({
|
||||
selection: EditorSelection.create(
|
||||
ranges,
|
||||
selections.codeBasedSelections.length - 1
|
||||
),
|
||||
},
|
||||
setCursor: (selections) => {
|
||||
const { editorView } = get()
|
||||
if (!editorView) return
|
||||
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
|
||||
const selectionRangeTypeMap: { [key: number]: Selection['type'] } = {}
|
||||
set({ selectionRangeTypeMap })
|
||||
selections.codeBasedSelections.forEach(({ range, type }) => {
|
||||
if (range?.[1]) {
|
||||
ranges.push(EditorSelection.cursor(range[1]))
|
||||
selectionRangeTypeMap[range[1]] = type
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
setCursor2: (codeSelections) => {
|
||||
const currestSelections = get().selectionRanges
|
||||
const code = get().code
|
||||
if (!codeSelections) {
|
||||
get().setCursor({
|
||||
otherSelections: currestSelections.otherSelections,
|
||||
codeBasedSelections: [
|
||||
{ range: [0, code.length - 1], type: 'default' },
|
||||
],
|
||||
})
|
||||
return
|
||||
}
|
||||
const selections: Selections = {
|
||||
...currestSelections,
|
||||
codeBasedSelections: get().isShiftDown
|
||||
? [...currestSelections.codeBasedSelections, codeSelections]
|
||||
: [codeSelections],
|
||||
}
|
||||
get().setCursor(selections)
|
||||
},
|
||||
selectionRangeTypeMap: {},
|
||||
selectionRanges: {
|
||||
otherSelections: [],
|
||||
codeBasedSelections: [],
|
||||
},
|
||||
setSelectionRanges: (selectionRanges) =>
|
||||
set({ selectionRanges, selectionRangeTypeMap: {} }),
|
||||
guiMode: { mode: 'default' },
|
||||
lastGuiMode: { mode: 'default' },
|
||||
setGuiMode: (guiMode) => {
|
||||
set({ guiMode })
|
||||
},
|
||||
logs: [],
|
||||
addLog: (log) => {
|
||||
if (Array.isArray(log)) {
|
||||
const cleanLog: any = log.map(({ __geoMeta, ...rest }) => rest)
|
||||
set((state) => ({ logs: [...state.logs, cleanLog] }))
|
||||
} else {
|
||||
set((state) => ({ logs: [...state.logs, log] }))
|
||||
}
|
||||
},
|
||||
resetLogs: () => {
|
||||
set({ logs: [] })
|
||||
},
|
||||
kclErrors: [],
|
||||
addKCLError: (e) => {
|
||||
set((state) => ({ kclErrors: [...state.kclErrors, e] }))
|
||||
},
|
||||
resetKCLErrors: () => {
|
||||
set({ kclErrors: [] })
|
||||
},
|
||||
ast: null,
|
||||
setAst: (ast) => {
|
||||
set({ ast })
|
||||
},
|
||||
updateAst: async (ast, { focusPath, callBack = () => {} } = {}) => {
|
||||
const newCode = recast(ast)
|
||||
const astWithUpdatedSource = parser_wasm(newCode)
|
||||
callBack(astWithUpdatedSource)
|
||||
|
||||
set({ ast: astWithUpdatedSource, code: newCode })
|
||||
if (focusPath) {
|
||||
const { node } = getNodeFromPath<any>(astWithUpdatedSource, focusPath)
|
||||
const { start, end } = node
|
||||
if (!start || !end) return
|
||||
setTimeout(() => {
|
||||
ranges.length &&
|
||||
editorView.dispatch({
|
||||
selection: EditorSelection.create(
|
||||
ranges,
|
||||
selections.codeBasedSelections.length - 1
|
||||
),
|
||||
})
|
||||
})
|
||||
},
|
||||
setCursor2: (codeSelections) => {
|
||||
const currestSelections = get().selectionRanges
|
||||
const code = get().code
|
||||
if (!codeSelections) {
|
||||
get().setCursor({
|
||||
otherSelections: currestSelections.otherSelections,
|
||||
codeBasedSelections: [
|
||||
{
|
||||
type: 'default',
|
||||
range: [start, end],
|
||||
},
|
||||
{ range: [0, code.length - 1], type: 'default' },
|
||||
],
|
||||
otherSelections: [],
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
updateAstAsync: async (ast, focusPath) => {
|
||||
// clear any pending updates
|
||||
pendingAstUpdates.forEach((id) => clearTimeout(id))
|
||||
pendingAstUpdates = []
|
||||
// setup a new update
|
||||
pendingAstUpdates.push(
|
||||
setTimeout(() => {
|
||||
get().updateAst(ast, { focusPath })
|
||||
}, 100) as unknown as number
|
||||
)
|
||||
},
|
||||
code: '',
|
||||
setCode: (code) => {
|
||||
set({ code })
|
||||
},
|
||||
formatCode: async () => {
|
||||
const code = get().code
|
||||
const ast = parser_wasm(code)
|
||||
const newCode = recast(ast)
|
||||
set({ code: newCode, ast })
|
||||
},
|
||||
errorState: {
|
||||
isError: false,
|
||||
error: '',
|
||||
},
|
||||
setError: (error = '') => {
|
||||
set({ errorState: { isError: !!error, error } })
|
||||
},
|
||||
programMemory: { root: {}, pendingMemory: {} },
|
||||
setProgramMemory: (programMemory) => set({ programMemory }),
|
||||
isShiftDown: false,
|
||||
setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
|
||||
artifactMap: {},
|
||||
sourceRangeMap: {},
|
||||
setArtifactNSourceRangeMaps: (maps) => set({ ...maps }),
|
||||
setEngineCommandManager: (engineCommandManager) =>
|
||||
set({ engineCommandManager }),
|
||||
setMediaStream: (mediaStream) => set({ mediaStream }),
|
||||
isStreamReady: false,
|
||||
setIsStreamReady: (isStreamReady) => set({ isStreamReady }),
|
||||
isLSPServerReady: false,
|
||||
setIsLSPServerReady: (isLSPServerReady) => set({ isLSPServerReady }),
|
||||
isMouseDownInStream: false,
|
||||
setIsMouseDownInStream: (isMouseDownInStream) => {
|
||||
set({ isMouseDownInStream })
|
||||
},
|
||||
didDragInStream: false,
|
||||
setDidDragInStream: (didDragInStream) => {
|
||||
set({ didDragInStream })
|
||||
},
|
||||
// For stream event handling
|
||||
fileId: '',
|
||||
setFileId: (fileId) => set({ fileId }),
|
||||
streamDimensions: { streamWidth: 1280, streamHeight: 720 },
|
||||
setStreamDimensions: (streamDimensions) => set({ streamDimensions }),
|
||||
return
|
||||
}
|
||||
const selections: Selections = {
|
||||
...currestSelections,
|
||||
codeBasedSelections: get().isShiftDown
|
||||
? [...currestSelections.codeBasedSelections, codeSelections]
|
||||
: [codeSelections],
|
||||
}
|
||||
get().setCursor(selections)
|
||||
},
|
||||
selectionRangeTypeMap: {},
|
||||
selectionRanges: {
|
||||
otherSelections: [],
|
||||
codeBasedSelections: [],
|
||||
},
|
||||
setSelectionRanges: (selectionRanges) =>
|
||||
set({ selectionRanges, selectionRangeTypeMap: {} }),
|
||||
guiMode: { mode: 'default' },
|
||||
lastGuiMode: { mode: 'default' },
|
||||
setGuiMode: (guiMode) => {
|
||||
set({ guiMode })
|
||||
},
|
||||
logs: [],
|
||||
addLog: (log) => {
|
||||
if (Array.isArray(log)) {
|
||||
const cleanLog: any = log.map(({ __geoMeta, ...rest }) => rest)
|
||||
set((state) => ({ logs: [...state.logs, cleanLog] }))
|
||||
} else {
|
||||
set((state) => ({ logs: [...state.logs, log] }))
|
||||
}
|
||||
},
|
||||
resetLogs: () => {
|
||||
set({ logs: [] })
|
||||
},
|
||||
kclErrors: [],
|
||||
addKCLError: (e) => {
|
||||
set((state) => ({ kclErrors: [...state.kclErrors, e] }))
|
||||
},
|
||||
resetKCLErrors: () => {
|
||||
set({ kclErrors: [] })
|
||||
},
|
||||
ast: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
body: [],
|
||||
nonCodeMeta: {
|
||||
noneCodeNodes: {},
|
||||
start: null,
|
||||
},
|
||||
},
|
||||
setAst: (ast) => {
|
||||
set({ ast })
|
||||
},
|
||||
updateAst: async (ast, { focusPath, callBack = () => {} } = {}) => {
|
||||
const newCode = recast(ast)
|
||||
const astWithUpdatedSource = parser_wasm(newCode)
|
||||
callBack(astWithUpdatedSource)
|
||||
|
||||
// tauri specific app settings
|
||||
defaultDir: {
|
||||
dir: '',
|
||||
},
|
||||
isBannerDismissed: false,
|
||||
setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }),
|
||||
openPanes: ['code'],
|
||||
setOpenPanes: (openPanes) => set({ openPanes }),
|
||||
showHomeMenu: true,
|
||||
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
|
||||
homeMenuItems: [],
|
||||
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
|
||||
}),
|
||||
set({
|
||||
ast: astWithUpdatedSource,
|
||||
code: newCode,
|
||||
defferedCode: newCode,
|
||||
})
|
||||
if (focusPath) {
|
||||
const { node } = getNodeFromPath<any>(
|
||||
astWithUpdatedSource,
|
||||
focusPath
|
||||
)
|
||||
const { start, end } = node
|
||||
if (!start || !end) return
|
||||
setTimeout(() => {
|
||||
get().setCursor({
|
||||
codeBasedSelections: [
|
||||
{
|
||||
type: 'default',
|
||||
range: [start, end],
|
||||
},
|
||||
],
|
||||
otherSelections: [],
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
updateAstAsync: async (ast, focusPath) => {
|
||||
// clear any pending updates
|
||||
pendingAstUpdates.forEach((id) => clearTimeout(id))
|
||||
pendingAstUpdates = []
|
||||
// setup a new update
|
||||
pendingAstUpdates.push(
|
||||
setTimeout(() => {
|
||||
get().updateAst(ast, { focusPath })
|
||||
}, 100) as unknown as number
|
||||
)
|
||||
},
|
||||
code: '',
|
||||
defferedCode: '',
|
||||
setCode: (code) => set({ code, defferedCode: code }),
|
||||
defferedSetCode: (code) => {
|
||||
set({ code })
|
||||
setDefferedCode(code)
|
||||
},
|
||||
formatCode: async () => {
|
||||
const code = get().code
|
||||
const ast = parser_wasm(code)
|
||||
const newCode = recast(ast)
|
||||
set({ code: newCode, ast })
|
||||
},
|
||||
errorState: {
|
||||
isError: false,
|
||||
error: '',
|
||||
},
|
||||
setError: (error = '') => {
|
||||
set({ errorState: { isError: !!error, error } })
|
||||
},
|
||||
programMemory: { root: {}, return: null },
|
||||
setProgramMemory: (programMemory) => set({ programMemory }),
|
||||
isShiftDown: false,
|
||||
setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
|
||||
artifactMap: {},
|
||||
sourceRangeMap: {},
|
||||
setArtifactNSourceRangeMaps: (maps) => set({ ...maps }),
|
||||
setEngineCommandManager: (engineCommandManager) =>
|
||||
set({ engineCommandManager }),
|
||||
setMediaStream: (mediaStream) => set({ mediaStream }),
|
||||
isStreamReady: false,
|
||||
setIsStreamReady: (isStreamReady) => set({ isStreamReady }),
|
||||
isLSPServerReady: false,
|
||||
setIsLSPServerReady: (isLSPServerReady) => set({ isLSPServerReady }),
|
||||
buttonDownInStream: undefined,
|
||||
setButtonDownInStream: (buttonDownInStream) => {
|
||||
set({ buttonDownInStream })
|
||||
},
|
||||
didDragInStream: false,
|
||||
setDidDragInStream: (didDragInStream) => {
|
||||
set({ didDragInStream })
|
||||
},
|
||||
// For stream event handling
|
||||
fileId: '',
|
||||
setFileId: (fileId) => set({ fileId }),
|
||||
streamDimensions: { streamWidth: 1280, streamHeight: 720 },
|
||||
setStreamDimensions: (streamDimensions) => set({ streamDimensions }),
|
||||
isExecuting: false,
|
||||
setIsExecuting: (isExecuting) => set({ isExecuting }),
|
||||
|
||||
// tauri specific app settings
|
||||
defaultDir: {
|
||||
dir: '',
|
||||
},
|
||||
isBannerDismissed: false,
|
||||
setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }),
|
||||
openPanes: ['code'],
|
||||
setOpenPanes: (openPanes) => set({ openPanes }),
|
||||
showHomeMenu: true,
|
||||
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
|
||||
homeMenuItems: [],
|
||||
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'store',
|
||||
partialize: (state) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(state).filter(([key]) =>
|
||||
['code', 'openPanes'].includes(key)
|
||||
['code', 'defferedCode', 'openPanes'].includes(key)
|
||||
)
|
||||
),
|
||||
}
|
||||
|
658
src/wasm-lib/Cargo.lock
generated
658
src/wasm-lib/Cargo.lock
generated
@ -229,6 +229,32 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.64.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"peeking_take_while",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@ -290,6 +316,12 @@ version = "3.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.4.3"
|
||||
@ -314,6 +346,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@ -336,6 +377,17 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.25"
|
||||
@ -418,6 +470,12 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.0"
|
||||
@ -482,6 +540,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.8"
|
||||
@ -492,6 +559,30 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"memoffset",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.16"
|
||||
@ -501,6 +592,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
@ -521,7 +618,7 @@ dependencies = [
|
||||
"hashbrown 0.14.0",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
"parking_lot_core 0.9.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -667,12 +764,85 @@ dependencies = [
|
||||
"similar",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exr"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1e481eb11a482815d3e9d618db8c42a93207134662873809335a92327440c18"
|
||||
dependencies = [
|
||||
"bit_field",
|
||||
"flume",
|
||||
"half",
|
||||
"lebe",
|
||||
"miniz_oxide",
|
||||
"rayon-core",
|
||||
"smallvec",
|
||||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ffmpeg-next"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8af03c47ad26832ab3aabc4cdbf210af3d3b878783edd5a7ba044ba33aab7a60"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"ffmpeg-sys-next",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ffmpeg-sys-next"
|
||||
version = "6.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf650f461ccf130f4eef4927affed703cc387b183bfc4a7dfee86a076c131127"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
"libc",
|
||||
"num_cpus",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.10.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"nanorand",
|
||||
"pin-project",
|
||||
"spin 0.9.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@ -819,12 +989,28 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "gloo-utils"
|
||||
version = "0.2.0"
|
||||
@ -857,6 +1043,15 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@ -1010,6 +1205,37 @@ dependencies = [
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"exr",
|
||||
"gif",
|
||||
"jpeg-decoder",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
"png",
|
||||
"qoi",
|
||||
"tiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-compare"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "419d59423b7202f6a2a95d3b9cf11a8cc9bf83a29cf7dda4d617a90e8c5ccfcf"
|
||||
dependencies = [
|
||||
"image",
|
||||
"itertools 0.10.5",
|
||||
"rayon",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@ -1031,6 +1257,18 @@ dependencies = [
|
||||
"hashbrown 0.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.8.0"
|
||||
@ -1072,6 +1310,15 @@ version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
||||
|
||||
[[package]]
|
||||
name = "jpeg-decoder"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
|
||||
dependencies = [
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.64"
|
||||
@ -1094,7 +1341,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.1.20"
|
||||
version = "0.1.26"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bson",
|
||||
@ -1126,23 +1373,36 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.2.23"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8b33e5df8f82b97e5f5af94ff1400ae37449d0f5f1bb79acedf17cf2193680f"
|
||||
checksum = "d9cf962b1e81a0b4eb923a727e761b40672cbacc7f5f0b75e13579d346352bc7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.21.2",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"data-encoding",
|
||||
"format_serde_error",
|
||||
"futures",
|
||||
"http",
|
||||
"itertools 0.10.5",
|
||||
"log",
|
||||
"parse-display",
|
||||
"phonenumber",
|
||||
"rand",
|
||||
"reqwest",
|
||||
"reqwest-conditional-middleware",
|
||||
"reqwest-middleware 0.2.3",
|
||||
"reqwest-retry",
|
||||
"reqwest-tracing",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
@ -1153,12 +1413,34 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.147"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
@ -1212,12 +1494,27 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@ -1247,6 +1544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
|
||||
dependencies = [
|
||||
"adler",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1260,6 +1558,15 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nanorand"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "newline-converter"
|
||||
version = "0.3.0"
|
||||
@ -1300,6 +1607,17 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.16"
|
||||
@ -1380,7 +1698,7 @@ dependencies = [
|
||||
"rand",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"reqwest-middleware",
|
||||
"reqwest-middleware 0.1.6",
|
||||
"rustfmt-wrapper",
|
||||
"schemars",
|
||||
"serde",
|
||||
@ -1415,12 +1733,42 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6105e89802af13fdf48c49d7646d3b533a70e536d818aae7e78ba0433d01acb8"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"crossbeam-channel",
|
||||
"futures-channel",
|
||||
"futures-executor",
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"lazy_static",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"rand",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"lock_api",
|
||||
"parking_lot_core 0.8.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.1"
|
||||
@ -1428,7 +1776,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
"parking_lot_core 0.9.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall 0.2.16",
|
||||
"smallvec",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1470,6 +1832,12 @@ dependencies = [
|
||||
"syn 2.0.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "peeking_take_while"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.0"
|
||||
@ -1538,6 +1906,25 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
@ -1587,6 +1974,15 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qoi"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.28.2"
|
||||
@ -1641,6 +2037,28 @@ dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
"num_cpus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
@ -1757,6 +2175,18 @@ dependencies = [
|
||||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-conditional-middleware"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59e50a2e70970896c99d1b8f20ddc30a70b30d3ac6e619a03a8353b64a49b277"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"reqwest",
|
||||
"reqwest-middleware 0.2.3",
|
||||
"task-local-extensions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-middleware"
|
||||
version = "0.1.6"
|
||||
@ -1773,6 +2203,73 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-middleware"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff44108c7925d082f2861e683a88618b68235ad9cdc60d64d9d1188efc951cdb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"http",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"task-local-extensions",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-retry"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c6a11c05102e5bec712c0619b8c7b7eda8b21a558a0bd981ceee15c38df8be4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"futures",
|
||||
"getrandom",
|
||||
"http",
|
||||
"hyper",
|
||||
"parking_lot 0.11.2",
|
||||
"reqwest",
|
||||
"reqwest-middleware 0.2.3",
|
||||
"retry-policies",
|
||||
"task-local-extensions",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"wasm-timer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-tracing"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14b1e66540e0cac90acadaf7109bf99c90d95abcc94b4c096bfa16a2d7aa7a71"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"getrandom",
|
||||
"matchit",
|
||||
"opentelemetry",
|
||||
"reqwest",
|
||||
"reqwest-middleware 0.2.3",
|
||||
"task-local-extensions",
|
||||
"tracing",
|
||||
"tracing-opentelemetry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "retry-policies"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e09bbcb5003282bcb688f0bae741b278e9c7e8f378f561522c9806c58e075d9b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.16.20"
|
||||
@ -1782,7 +2279,7 @@ dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"spin",
|
||||
"spin 0.5.2",
|
||||
"untrusted",
|
||||
"web-sys",
|
||||
"winapi",
|
||||
@ -1794,6 +2291,12 @@ version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustfmt-wrapper"
|
||||
version = "0.2.0"
|
||||
@ -2102,6 +2605,21 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.1"
|
||||
@ -2111,6 +2629,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.2.1"
|
||||
@ -2223,6 +2747,15 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
@ -2367,6 +2900,17 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"jpeg-decoder",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.45"
|
||||
@ -2434,7 +2978,7 @@ dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"parking_lot 0.12.1",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.5.3",
|
||||
@ -2605,6 +3149,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-opentelemetry"
|
||||
version = "0.17.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbbe89715c1dbbb790059e2565353978564924ee85017b5fff365c872ff6721f"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"opentelemetry",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
||||
dependencies = [
|
||||
"sharded-slab",
|
||||
"thread_local",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2668,6 +3249,19 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "twenty-twenty"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c77f4c2039b998d194da9717843bb8c634609b5ccacac513be56985569f6e732"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ffmpeg-next",
|
||||
"image",
|
||||
"image-compare",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.16.0"
|
||||
@ -2769,6 +3363,18 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
@ -2877,14 +3483,20 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
|
||||
name = "wasm-lib"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bson",
|
||||
"futures",
|
||||
"gloo-utils",
|
||||
"image",
|
||||
"js-sys",
|
||||
"kcl-lib",
|
||||
"kittycad",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tower-lsp",
|
||||
"twenty-twenty",
|
||||
"uuid",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
@ -2904,6 +3516,21 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-timer"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"js-sys",
|
||||
"parking_lot 0.11.2",
|
||||
"pin-utils",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.64"
|
||||
@ -2920,6 +3547,12 @@ version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc"
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@ -3125,3 +3758,12 @@ name = "yansi"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
|
||||
[[package]]
|
||||
name = "zune-inflate"
|
||||
version = "0.2.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
@ -11,11 +11,20 @@ crate-type = ["cdylib"]
|
||||
bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
|
||||
gloo-utils = "0.2.0"
|
||||
kcl-lib = { path = "kcl" }
|
||||
kittycad = { version = "0.2.23", default-features = false, features = ["js"] }
|
||||
kittycad = { version = "0.2.25", default-features = false, features = ["js"] }
|
||||
serde_json = "1.0.93"
|
||||
wasm-bindgen = "0.2.87"
|
||||
wasm-bindgen-futures = "0.4.37"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1"
|
||||
image = "0.24.7"
|
||||
kittycad = "0.2.25"
|
||||
reqwest = { version = "0.11.20", default-features = false }
|
||||
tokio = { version = "1.32.0", features = ["rt-multi-thread", "macros", "time"] }
|
||||
twenty-twenty = "0.6.1"
|
||||
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
futures = "0.3.28"
|
||||
js-sys = "0.3.64"
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language"
|
||||
version = "0.1.20"
|
||||
version = "0.1.26"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
@ -11,9 +11,9 @@ license = "MIT"
|
||||
anyhow = { version = "1.0.75", features = ["backtrace"] }
|
||||
clap = { version = "4.4.2", features = ["cargo", "derive", "env", "unicode"] }
|
||||
dashmap = "5.5.3"
|
||||
derive-docs = { version = "0.1.1" }
|
||||
derive-docs = { version = "0.1.3" }
|
||||
#derive-docs = { path = "../derive-docs" }
|
||||
kittycad = { version = "0.2.23", default-features = false, features = ["js"] }
|
||||
kittycad = { version = "0.2.25", default-features = false, features = ["js"] }
|
||||
lazy_static = "1.4.0"
|
||||
parse-display = "0.8.2"
|
||||
regex = "1.7.1"
|
||||
|
4
src/wasm-lib/kcl/fuzz/.gitignore
vendored
Normal file
4
src/wasm-lib/kcl/fuzz/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
target
|
||||
corpus
|
||||
artifacts
|
||||
coverage
|
2218
src/wasm-lib/kcl/fuzz/Cargo.lock
generated
Normal file
2218
src/wasm-lib/kcl/fuzz/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
src/wasm-lib/kcl/fuzz/Cargo.toml
Normal file
27
src/wasm-lib/kcl/fuzz/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "kcl-lib-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2021"
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.4"
|
||||
|
||||
[dependencies.kcl-lib]
|
||||
path = ".."
|
||||
|
||||
# Prevent this from interfering with workspaces
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
[profile.release]
|
||||
debug = 1
|
||||
|
||||
[[bin]]
|
||||
name = "parser"
|
||||
path = "fuzz_targets/parser.rs"
|
||||
test = false
|
||||
doc = false
|
14
src/wasm-lib/kcl/fuzz/fuzz_targets/parser.rs
Normal file
14
src/wasm-lib/kcl/fuzz/fuzz_targets/parser.rs
Normal file
@ -0,0 +1,14 @@
|
||||
#![no_main]
|
||||
#[macro_use]
|
||||
extern crate libfuzzer_sys;
|
||||
extern crate kcl_lib;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
if let Ok(s) = std::str::from_utf8(data) {
|
||||
let tokens = kcl_lib::tokeniser::lexer(s);
|
||||
let parser = kcl_lib::parser::Parser::new(tokens);
|
||||
if let Ok(_) = parser.ast() {
|
||||
println!("OK");
|
||||
}
|
||||
}
|
||||
});
|
File diff suppressed because it is too large
Load Diff
@ -87,6 +87,9 @@ impl EngineConnection {
|
||||
|
||||
if let Some(msg) = ws_resp.resp {
|
||||
match msg {
|
||||
OkWebSocketResponseData::MetricsRequest {} => {
|
||||
// @paultag todo
|
||||
}
|
||||
OkWebSocketResponseData::IceServerInfo { ice_servers } => {
|
||||
println!("got ice server info: {:?}", ice_servers);
|
||||
}
|
||||
|
@ -98,16 +98,14 @@ impl ProgramReturn {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum MemoryItem {
|
||||
UserVal {
|
||||
value: serde_json::Value,
|
||||
#[serde(rename = "__meta")]
|
||||
meta: Vec<Metadata>,
|
||||
},
|
||||
UserVal(UserVal),
|
||||
SketchGroup(SketchGroup),
|
||||
ExtrudeGroup(ExtrudeGroup),
|
||||
#[ts(skip)]
|
||||
ExtrudeTransform(ExtrudeTransform),
|
||||
#[ts(skip)]
|
||||
Function {
|
||||
#[serde(skip)]
|
||||
func: Option<MemoryFunction>,
|
||||
@ -119,7 +117,16 @@ pub enum MemoryItem {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub struct UserVal {
|
||||
pub value: serde_json::Value,
|
||||
#[serde(rename = "__meta")]
|
||||
pub meta: Vec<Metadata>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub struct ExtrudeTransform {
|
||||
pub position: Position,
|
||||
pub rotation: Rotation,
|
||||
@ -138,7 +145,7 @@ pub type MemoryFunction = fn(
|
||||
impl From<MemoryItem> for Vec<SourceRange> {
|
||||
fn from(item: MemoryItem) -> Self {
|
||||
match item {
|
||||
MemoryItem::UserVal { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
|
||||
MemoryItem::UserVal(u) => u.meta.iter().map(|m| m.source_range).collect(),
|
||||
MemoryItem::SketchGroup(s) => s.meta.iter().map(|m| m.source_range).collect(),
|
||||
MemoryItem::ExtrudeGroup(e) => e.meta.iter().map(|m| m.source_range).collect(),
|
||||
MemoryItem::ExtrudeTransform(e) => e.meta.iter().map(|m| m.source_range).collect(),
|
||||
@ -149,8 +156,8 @@ impl From<MemoryItem> for Vec<SourceRange> {
|
||||
|
||||
impl MemoryItem {
|
||||
pub fn get_json_value(&self) -> Result<serde_json::Value, KclError> {
|
||||
if let MemoryItem::UserVal { value, .. } = self {
|
||||
Ok(value.clone())
|
||||
if let MemoryItem::UserVal(user_val) = self {
|
||||
Ok(user_val.value.clone())
|
||||
} else {
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Not a user value: {:?}", self),
|
||||
@ -186,7 +193,7 @@ impl MemoryItem {
|
||||
/// A sketch group is a collection of paths.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub struct SketchGroup {
|
||||
/// The id of the sketch group.
|
||||
pub id: uuid::Uuid,
|
||||
@ -238,7 +245,7 @@ impl SketchGroup {
|
||||
/// An extrude group is a collection of extrude surfaces.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub struct ExtrudeGroup {
|
||||
/// The id of the extrude group.
|
||||
pub id: uuid::Uuid,
|
||||
@ -276,15 +283,15 @@ pub enum BodyType {
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct Position(pub [f64; 3]);
|
||||
pub struct Position(#[ts(type = "[number, number, number]")] pub [f64; 3]);
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
pub struct Rotation(pub [f64; 4]);
|
||||
pub struct Rotation(#[ts(type = "[number, number, number, number]")] pub [f64; 4]);
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Copy, Clone, ts_rs::TS, JsonSchema, Hash, Eq)]
|
||||
#[ts(export)]
|
||||
pub struct SourceRange(pub [usize; 2]);
|
||||
pub struct SourceRange(#[ts(type = "[number, number]")] pub [usize; 2]);
|
||||
|
||||
impl SourceRange {
|
||||
/// Create a new source range.
|
||||
@ -401,8 +408,10 @@ impl From<SourceRange> for Metadata {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BasePath {
|
||||
/// The from point.
|
||||
#[ts(type = "[number, number]")]
|
||||
pub from: [f64; 2],
|
||||
/// The to point.
|
||||
#[ts(type = "[number, number]")]
|
||||
pub to: [f64; 2],
|
||||
/// The name of the path.
|
||||
pub name: String,
|
||||
@ -595,7 +604,9 @@ pub fn execute(
|
||||
|
||||
memory.return_ = Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
|
||||
} else if let Some(func) = memory.clone().root.get(&fn_name) {
|
||||
func.call_fn(&args, memory, engine)?;
|
||||
let result = func.call_fn(&args, memory, engine)?;
|
||||
|
||||
memory.return_ = result;
|
||||
} else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("No such name {} defined", fn_name),
|
||||
@ -642,7 +653,7 @@ pub fn execute(
|
||||
for (index, param) in function_expression.params.iter().enumerate() {
|
||||
fn_memory.add(
|
||||
¶m.name,
|
||||
args.clone().get(index).unwrap().clone(),
|
||||
args.get(index).unwrap().clone(),
|
||||
param.into(),
|
||||
)?;
|
||||
}
|
||||
@ -696,11 +707,39 @@ pub fn execute(
|
||||
let result = bin_expr.get_result(memory, &mut pipe_info, engine)?;
|
||||
memory.return_ = Some(ProgramReturn::Value(result));
|
||||
}
|
||||
Value::UnaryExpression(unary_expr) => {
|
||||
let result = unary_expr.get_result(memory, &mut pipe_info, engine)?;
|
||||
memory.return_ = Some(ProgramReturn::Value(result));
|
||||
}
|
||||
Value::Identifier(identifier) => {
|
||||
let value = memory.get(&identifier.name, identifier.into())?.clone();
|
||||
memory.return_ = Some(ProgramReturn::Value(value));
|
||||
}
|
||||
_ => (),
|
||||
Value::Literal(literal) => {
|
||||
memory.return_ = Some(ProgramReturn::Value(literal.into()));
|
||||
}
|
||||
Value::ArrayExpression(array_expr) => {
|
||||
let result = array_expr.execute(memory, &mut pipe_info, engine)?;
|
||||
memory.return_ = Some(ProgramReturn::Value(result));
|
||||
}
|
||||
Value::ObjectExpression(obj_expr) => {
|
||||
let result = obj_expr.execute(memory, &mut pipe_info, engine)?;
|
||||
memory.return_ = Some(ProgramReturn::Value(result));
|
||||
}
|
||||
Value::CallExpression(call_expr) => {
|
||||
let result = call_expr.execute(memory, &mut pipe_info, engine)?;
|
||||
memory.return_ = Some(ProgramReturn::Value(result));
|
||||
}
|
||||
Value::MemberExpression(member_expr) => {
|
||||
let result = member_expr.get_result(memory)?;
|
||||
memory.return_ = Some(ProgramReturn::Value(result));
|
||||
}
|
||||
Value::PipeExpression(pipe_expr) => {
|
||||
let result = pipe_expr.get_result(memory, &mut pipe_info, engine)?;
|
||||
memory.return_ = Some(ProgramReturn::Value(result));
|
||||
}
|
||||
Value::PipeSubstitution(_) => {}
|
||||
Value::FunctionExpression(_) => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -856,4 +895,116 @@ show(part001)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_with_function_literal_in_pipe() {
|
||||
let ast = r#"const w = 20
|
||||
const l = 8
|
||||
const h = 10
|
||||
|
||||
fn thing = () => {
|
||||
return -8
|
||||
}
|
||||
|
||||
const firstExtrude = startSketchAt([0,0])
|
||||
|> line([0, l], %)
|
||||
|> line([w, 0], %)
|
||||
|> line([0, thing()], %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)
|
||||
|
||||
show(firstExtrude)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_with_function_unary_in_pipe() {
|
||||
let ast = r#"const w = 20
|
||||
const l = 8
|
||||
const h = 10
|
||||
|
||||
fn thing = (x) => {
|
||||
return -x
|
||||
}
|
||||
|
||||
const firstExtrude = startSketchAt([0,0])
|
||||
|> line([0, l], %)
|
||||
|> line([w, 0], %)
|
||||
|> line([0, thing(8)], %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)
|
||||
|
||||
show(firstExtrude)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_with_function_array_in_pipe() {
|
||||
let ast = r#"const w = 20
|
||||
const l = 8
|
||||
const h = 10
|
||||
|
||||
fn thing = (x) => {
|
||||
return [0, -x]
|
||||
}
|
||||
|
||||
const firstExtrude = startSketchAt([0,0])
|
||||
|> line([0, l], %)
|
||||
|> line([w, 0], %)
|
||||
|> line(thing(8), %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)
|
||||
|
||||
show(firstExtrude)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_with_function_call_in_pipe() {
|
||||
let ast = r#"const w = 20
|
||||
const l = 8
|
||||
const h = 10
|
||||
|
||||
fn other_thing = (y) => {
|
||||
return -y
|
||||
}
|
||||
|
||||
fn thing = (x) => {
|
||||
return other_thing(x)
|
||||
}
|
||||
|
||||
const firstExtrude = startSketchAt([0,0])
|
||||
|> line([0, l], %)
|
||||
|> line([w, 0], %)
|
||||
|> line([0, thing(8)], %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)
|
||||
|
||||
show(firstExtrude)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_with_function_sketch() {
|
||||
let ast = r#"const box = (h, l, w) => {
|
||||
const myBox = startSketchAt([0,0])
|
||||
|> line([0, l], %)
|
||||
|> line([w, 0], %)
|
||||
|> line([0, -l], %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)
|
||||
|
||||
return myBox
|
||||
}
|
||||
|
||||
const fnBox = box(3, 6, 10)
|
||||
|
||||
show(fnBox)"#;
|
||||
|
||||
parse_execute(ast).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
@ -228,8 +228,8 @@ impl ReversePolishNotation {
|
||||
.collect::<Vec<Token>>(),
|
||||
);
|
||||
return rpn.parse();
|
||||
} else if current_token.value == ")" {
|
||||
if !self.operators.is_empty() && self.operators[self.operators.len() - 1].value != "(" {
|
||||
} else if current_token.value == ")" && !self.operators.is_empty() {
|
||||
if self.operators[self.operators.len() - 1].value != "(" {
|
||||
// pop operators off the stack and push them to postFix until we find the matching '('
|
||||
let rpn = ReversePolishNotation::new(
|
||||
&self.parser.tokens,
|
||||
|
@ -336,17 +336,26 @@ impl Parser {
|
||||
value: if start_end_string.starts_with("\n\n") && is_new_line_comment {
|
||||
// Preserve if they want a whitespace line before the comment.
|
||||
// But let's just allow one.
|
||||
NoneCodeValue::NewLineBlock { value: full_string }
|
||||
NoneCodeValue::NewLineBlockComment { value: full_string }
|
||||
} else if is_new_line_comment {
|
||||
NoneCodeValue::Block { value: full_string }
|
||||
NoneCodeValue::BlockComment { value: full_string }
|
||||
} else {
|
||||
NoneCodeValue::Inline { value: full_string }
|
||||
NoneCodeValue::InlineComment { value: full_string }
|
||||
},
|
||||
};
|
||||
Ok((Some(node), end_index - 1))
|
||||
}
|
||||
|
||||
fn next_meaningful_token(&self, index: usize, offset: Option<usize>) -> Result<TokenReturnWithNonCode, KclError> {
|
||||
// There is no next meaningful token.
|
||||
if index >= self.tokens.len() - 1 {
|
||||
return Ok(TokenReturnWithNonCode {
|
||||
token: None,
|
||||
index: self.tokens.len() - 1,
|
||||
non_code_node: None,
|
||||
});
|
||||
}
|
||||
|
||||
let new_index = index + offset.unwrap_or(1);
|
||||
let Ok(token) = self.get_token(new_index) else {
|
||||
return Ok(TokenReturnWithNonCode {
|
||||
@ -405,7 +414,7 @@ impl Parser {
|
||||
if found_another_opening_brace {
|
||||
return self.find_closing_brace(index + 1, brace_count + 1, search_opening_brace);
|
||||
}
|
||||
if found_another_closing_brace {
|
||||
if found_another_closing_brace && brace_count > 0 {
|
||||
return self.find_closing_brace(index + 1, brace_count - 1, search_opening_brace);
|
||||
}
|
||||
// non-brace token, increment and continue
|
||||
@ -610,6 +619,12 @@ impl Parser {
|
||||
fn make_member_expression(&self, index: usize) -> Result<MemberExpressionReturn, KclError> {
|
||||
let current_token = self.get_token(index)?;
|
||||
let mut keys_info = self.collect_object_keys(index, None)?;
|
||||
if keys_info.is_empty() {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: "expected to be started on a identifier or literal".to_string(),
|
||||
}));
|
||||
}
|
||||
let last_key = keys_info[keys_info.len() - 1].clone();
|
||||
let first_key = keys_info.remove(0);
|
||||
let root = self.make_identifier(index)?;
|
||||
@ -679,7 +694,11 @@ impl Parser {
|
||||
return Ok(index);
|
||||
}
|
||||
let next_right = self.next_meaningful_token(maybe_operator.index, None)?;
|
||||
self.find_end_of_binary_expression(next_right.index)
|
||||
if next_right.index != index {
|
||||
self.find_end_of_binary_expression(next_right.index)
|
||||
} else {
|
||||
Ok(index)
|
||||
}
|
||||
} else {
|
||||
Ok(index)
|
||||
}
|
||||
@ -847,6 +866,8 @@ impl Parser {
|
||||
fn make_array_expression(&self, index: usize) -> Result<ArrayReturn, KclError> {
|
||||
let opening_brace_token = self.get_token(index)?;
|
||||
let first_element_token = self.next_meaningful_token(index, None)?;
|
||||
// Make sure there is a closing brace.
|
||||
let _closing_brace = self.find_closing_brace(index, 0, "")?;
|
||||
let array_elements = self.make_array_elements(first_element_token.index, Vec::new())?;
|
||||
Ok(ArrayReturn {
|
||||
expression: ArrayExpression {
|
||||
@ -1018,7 +1039,7 @@ impl Parser {
|
||||
} else {
|
||||
return Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![argument_token_token.clone().into()],
|
||||
message: format!("Unexpected token {} ", argument_token_token.value),
|
||||
message: format!("Unexpected token {}", argument_token_token.value),
|
||||
}));
|
||||
};
|
||||
}
|
||||
@ -1043,18 +1064,18 @@ impl Parser {
|
||||
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![argument_token_token.clone().into()],
|
||||
message: format!("Unexpected token {} ", argument_token_token.value),
|
||||
message: format!("Unexpected token {}", argument_token_token.value),
|
||||
}))
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![brace_or_comma_token.into()],
|
||||
message: format!("Unexpected token {} ", brace_or_comma_token.value),
|
||||
message: format!("Unexpected token {}", brace_or_comma_token.value),
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![brace_or_comma_token.into()],
|
||||
message: format!("Unexpected token {} ", brace_or_comma_token.value),
|
||||
message: format!("Unexpected token {}", brace_or_comma_token.value),
|
||||
}))
|
||||
}
|
||||
}
|
||||
@ -1063,6 +1084,8 @@ impl Parser {
|
||||
let current_token = self.get_token(index)?;
|
||||
let brace_token = self.next_meaningful_token(index, None)?;
|
||||
let callee = self.make_identifier(index)?;
|
||||
// Make sure there is a closing brace.
|
||||
let _closing_brace_token = self.find_closing_brace(brace_token.index, 0, "")?;
|
||||
let args = self.make_arguments(brace_token.index, vec![])?;
|
||||
let closing_brace_token = self.get_token(args.last_index)?;
|
||||
let function = if let Some(stdlib_fn) = self.stdlib.get(&callee.name) {
|
||||
@ -1105,42 +1128,42 @@ impl Parser {
|
||||
) -> Result<VariableDeclaratorsReturn, KclError> {
|
||||
let current_token = self.get_token(index)?;
|
||||
let assignment = self.next_meaningful_token(index, None)?;
|
||||
if let Some(assignment_token) = assignment.token {
|
||||
let contents_start_token = self.next_meaningful_token(assignment.index, None)?;
|
||||
let pipe_start_index = if assignment_token.token_type == TokenType::Operator {
|
||||
contents_start_token.index
|
||||
} else {
|
||||
assignment.index
|
||||
};
|
||||
let next_pipe_operator = self.has_pipe_operator(pipe_start_index, None)?;
|
||||
let init: Value;
|
||||
let last_index = if next_pipe_operator.token.is_some() {
|
||||
let pipe_expression_result = self.make_pipe_expression(assignment.index)?;
|
||||
init = Value::PipeExpression(Box::new(pipe_expression_result.expression));
|
||||
pipe_expression_result.last_index
|
||||
} else {
|
||||
let value_result = self.make_value(contents_start_token.index)?;
|
||||
init = value_result.value;
|
||||
value_result.last_index
|
||||
};
|
||||
let current_declarator = VariableDeclarator {
|
||||
start: current_token.start,
|
||||
end: self.get_token(last_index)?.end,
|
||||
id: self.make_identifier(index)?,
|
||||
init,
|
||||
};
|
||||
let mut declarations = previous_declarators;
|
||||
declarations.push(current_declarator);
|
||||
Ok(VariableDeclaratorsReturn {
|
||||
declarations,
|
||||
last_index,
|
||||
})
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
let Some(assignment_token) = assignment.token else {
|
||||
return Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![current_token.clone().into()],
|
||||
message: format!("Unexpected token {} ", current_token.value),
|
||||
}))
|
||||
}
|
||||
message: format!("Unexpected token {}", current_token.value),
|
||||
}));
|
||||
};
|
||||
|
||||
let contents_start_token = self.next_meaningful_token(assignment.index, None)?;
|
||||
let pipe_start_index = if assignment_token.token_type == TokenType::Operator {
|
||||
contents_start_token.index
|
||||
} else {
|
||||
assignment.index
|
||||
};
|
||||
let next_pipe_operator = self.has_pipe_operator(pipe_start_index, None)?;
|
||||
let init: Value;
|
||||
let last_index = if next_pipe_operator.token.is_some() {
|
||||
let pipe_expression_result = self.make_pipe_expression(assignment.index)?;
|
||||
init = Value::PipeExpression(Box::new(pipe_expression_result.expression));
|
||||
pipe_expression_result.last_index
|
||||
} else {
|
||||
let value_result = self.make_value(contents_start_token.index)?;
|
||||
init = value_result.value;
|
||||
value_result.last_index
|
||||
};
|
||||
let current_declarator = VariableDeclarator {
|
||||
start: current_token.start,
|
||||
end: self.get_token(last_index)?.end,
|
||||
id: self.make_identifier(index)?,
|
||||
init,
|
||||
};
|
||||
let mut declarations = previous_declarators;
|
||||
declarations.push(current_declarator);
|
||||
Ok(VariableDeclaratorsReturn {
|
||||
declarations,
|
||||
last_index,
|
||||
})
|
||||
}
|
||||
|
||||
fn make_variable_declaration(&self, index: usize) -> Result<VariableDeclarationResult, KclError> {
|
||||
@ -1184,7 +1207,7 @@ impl Parser {
|
||||
} else {
|
||||
Err(KclError::Unimplemented(KclErrorDetails {
|
||||
source_ranges: vec![brace_or_comma_token.into()],
|
||||
message: format!("Unexpected token {} ", brace_or_comma_token.value),
|
||||
message: format!("Unexpected token {}", brace_or_comma_token.value),
|
||||
}))
|
||||
}
|
||||
}
|
||||
@ -1192,6 +1215,12 @@ impl Parser {
|
||||
fn make_unary_expression(&self, index: usize) -> Result<UnaryExpressionResult, KclError> {
|
||||
let current_token = self.get_token(index)?;
|
||||
let next_token = self.next_meaningful_token(index, None)?;
|
||||
if next_token.token.is_none() {
|
||||
return Err(KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: vec![current_token.into()],
|
||||
message: "expected another token".to_string(),
|
||||
}));
|
||||
}
|
||||
let argument = self.make_value(next_token.index)?;
|
||||
let argument_token = self.get_token(argument.last_index)?;
|
||||
Ok(UnaryExpressionResult {
|
||||
@ -1232,7 +1261,6 @@ impl Parser {
|
||||
return Ok(ExpressionStatementResult {
|
||||
expression: ExpressionStatement {
|
||||
start: current_token.start,
|
||||
// end: call_expression.last_index,
|
||||
end,
|
||||
expression: Value::CallExpression(Box::new(call_expression.expression)),
|
||||
},
|
||||
@ -1314,6 +1342,8 @@ impl Parser {
|
||||
|
||||
fn make_object_expression(&self, index: usize) -> Result<ObjectExpressionResult, KclError> {
|
||||
let opening_brace_token = self.get_token(index)?;
|
||||
// Make sure there is a closing brace.
|
||||
let _closing_brace = self.find_closing_brace(index, 0, "")?;
|
||||
let first_property_token = self.next_meaningful_token(index, None)?;
|
||||
let object_properties = self.make_object_properties(first_property_token.index, vec![])?;
|
||||
Ok(ObjectExpressionResult {
|
||||
@ -1665,7 +1695,7 @@ const key = 'c'"#,
|
||||
Some(NoneCodeNode {
|
||||
start: 38,
|
||||
end: 60,
|
||||
value: NoneCodeValue::Block {
|
||||
value: NoneCodeValue::BlockComment {
|
||||
value: "this is a comment".to_string(),
|
||||
},
|
||||
}),
|
||||
@ -1687,7 +1717,7 @@ const key = 'c'"#,
|
||||
Some(NoneCodeNode {
|
||||
start: 106,
|
||||
end: 166,
|
||||
value: NoneCodeValue::Block {
|
||||
value: NoneCodeValue::BlockComment {
|
||||
value: "this is\n a comment\n spanning a few lines".to_string(),
|
||||
},
|
||||
}),
|
||||
@ -2716,4 +2746,139 @@ show(mySk1)"#;
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("file is empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_half_pipe_small() {
|
||||
let tokens = crate::tokeniser::lexer(
|
||||
"const secondExtrude = startSketchAt([0,0])
|
||||
|",
|
||||
);
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("Unexpected token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_half_pipe() {
|
||||
let tokens = crate::tokeniser::lexer(
|
||||
"const height = 10
|
||||
|
||||
const firstExtrude = startSketchAt([0,0])
|
||||
|> line([0, 8], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, -8], %)
|
||||
|> close(%)
|
||||
|> extrude(2, %)
|
||||
|
||||
show(firstExtrude)
|
||||
|
||||
const secondExtrude = startSketchAt([0,0])
|
||||
|",
|
||||
);
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("Unexpected token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_greater_bang() {
|
||||
let tokens = crate::tokeniser::lexer(">!");
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_z_percent_parens() {
|
||||
let tokens = crate::tokeniser::lexer("z%)");
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("Unexpected token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_parens_unicode() {
|
||||
let tokens = crate::tokeniser::lexer("(ޜ");
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_nested_open_brackets() {
|
||||
let tokens = crate::tokeniser::lexer(
|
||||
r#"
|
||||
z(-[["#,
|
||||
);
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("unexpected end"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_weird_new_line_function() {
|
||||
let tokens = crate::tokeniser::lexer(
|
||||
r#"z
|
||||
(--#"#,
|
||||
);
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("unexpected end"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_weird_lots_of_fancy_brackets() {
|
||||
let tokens = crate::tokeniser::lexer(r#"zz({{{{{{{{)iegAng{{{{{{{##"#);
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("unexpected end"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_weird_close_before_open() {
|
||||
let tokens = crate::tokeniser::lexer(
|
||||
r#"fn)n
|
||||
e
|
||||
["#,
|
||||
);
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("expected to be started on a identifier or literal"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_weird_close_before_nada() {
|
||||
let tokens = crate::tokeniser::lexer(r#"fn)n-"#);
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("expected another token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_weird_lots_of_slashes() {
|
||||
let tokens = crate::tokeniser::lexer(
|
||||
r#"J///////////o//+///////////P++++*++++++P///////˟
|
||||
++4"#,
|
||||
);
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("unexpected end of expression"));
|
||||
}
|
||||
}
|
||||
|
@ -233,6 +233,7 @@ impl LanguageServer for Backend {
|
||||
document_symbol_provider: Some(OneOf::Left(true)),
|
||||
hover_provider: Some(HoverProviderCapability::Simple(true)),
|
||||
inlay_hint_provider: Some(OneOf::Left(true)),
|
||||
rename_provider: Some(OneOf::Left(true)),
|
||||
semantic_tokens_provider: Some(SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(
|
||||
SemanticTokensRegistrationOptions {
|
||||
text_document_registration_options: {
|
||||
@ -552,19 +553,14 @@ impl LanguageServer for Backend {
|
||||
return Ok(None);
|
||||
};
|
||||
// Now recast it.
|
||||
// Make spaces for the tab size.
|
||||
/*let mut tab_size = String::new();
|
||||
for _ in 0..params.options.tab_size {
|
||||
tab_size.push(' ');
|
||||
}*/
|
||||
// TODO: use the tab size.
|
||||
let mut recast = ast.recast("", false).trim().to_string();
|
||||
if let Some(insert_final_newline) = params.options.insert_final_newline {
|
||||
if insert_final_newline {
|
||||
recast.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
let recast = ast.recast(
|
||||
&crate::abstract_syntax_tree_types::FormatOptions {
|
||||
tab_size: params.options.tab_size as usize,
|
||||
insert_final_newline: params.options.insert_final_newline.unwrap_or(false),
|
||||
use_tabs: !params.options.insert_spaces,
|
||||
},
|
||||
0,
|
||||
);
|
||||
let source_range = SourceRange([0, current_code.len() - 1]);
|
||||
let range = source_range.to_lsp_range(¤t_code);
|
||||
Ok(Some(vec![TextEdit {
|
||||
@ -572,6 +568,43 @@ impl LanguageServer for Backend {
|
||||
range,
|
||||
}]))
|
||||
}
|
||||
|
||||
async fn rename(&self, params: RenameParams) -> RpcResult<Option<WorkspaceEdit>> {
|
||||
let filename = params.text_document_position.text_document.uri.to_string();
|
||||
|
||||
let Some(current_code) = self.current_code_map.get(&filename) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Parse the ast.
|
||||
// I don't know if we need to do this again since it should be updated in the context.
|
||||
// But I figure better safe than sorry since this will write back out to the file.
|
||||
let tokens = crate::tokeniser::lexer(¤t_code);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let Ok(mut ast) = parser.ast() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Let's convert the position to a character index.
|
||||
let pos = position_to_char_index(params.text_document_position.position, ¤t_code);
|
||||
// Now let's perform the rename on the ast.
|
||||
ast.rename_symbol(¶ms.new_name, pos);
|
||||
// Now recast it.
|
||||
let recast = ast.recast(&Default::default(), 0);
|
||||
let source_range = SourceRange([0, current_code.len() - 1]);
|
||||
let range = source_range.to_lsp_range(¤t_code);
|
||||
Ok(Some(WorkspaceEdit {
|
||||
changes: Some(HashMap::from([(
|
||||
params.text_document_position.text_document.uri,
|
||||
vec![TextEdit {
|
||||
new_text: recast,
|
||||
range,
|
||||
}],
|
||||
)])),
|
||||
document_changes: None,
|
||||
change_annotations: None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get completions from our stdlib.
|
||||
|
@ -103,12 +103,12 @@ impl<'a> Args<'a> {
|
||||
}
|
||||
|
||||
fn make_user_val_from_json(&self, j: serde_json::Value) -> Result<MemoryItem, KclError> {
|
||||
Ok(MemoryItem::UserVal {
|
||||
Ok(MemoryItem::UserVal(crate::executor::UserVal {
|
||||
value: j,
|
||||
meta: vec![Metadata {
|
||||
source_range: self.source_range,
|
||||
}],
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn make_user_val_from_f64(&self, f: f64) -> Result<MemoryItem, KclError> {
|
||||
|
@ -382,6 +382,20 @@ fn inner_angled_line(
|
||||
},
|
||||
};
|
||||
|
||||
args.send_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::ExtendPath {
|
||||
path: sketch_group.id,
|
||||
segment: kittycad::types::PathSegment::Line {
|
||||
end: Point3D {
|
||||
x: to[0],
|
||||
y: to[1],
|
||||
z: 0.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut new_sketch_group = sketch_group.clone();
|
||||
new_sketch_group.value.push(current_path);
|
||||
Ok(new_sketch_group)
|
||||
|
@ -206,8 +206,8 @@ fn is_block_comment(character: &str) -> bool {
|
||||
BLOCKCOMMENT.is_match(character)
|
||||
}
|
||||
|
||||
fn match_first(str: &str, regex: &Regex) -> Option<String> {
|
||||
regex.find(str).map(|the_match| the_match.as_str().to_string())
|
||||
fn match_first(s: &str, regex: &Regex) -> Option<String> {
|
||||
regex.find(s).map(|the_match| the_match.as_str().to_string())
|
||||
}
|
||||
|
||||
fn make_token(token_type: TokenType, value: &str, start: usize) -> Token {
|
||||
@ -219,8 +219,8 @@ fn make_token(token_type: TokenType, value: &str, start: usize) -> Token {
|
||||
}
|
||||
}
|
||||
|
||||
fn return_token_at_index(str: &str, start_index: usize) -> Option<Token> {
|
||||
let str_from_index = &str[start_index..];
|
||||
fn return_token_at_index(s: &str, start_index: usize) -> Option<Token> {
|
||||
let str_from_index = &s.chars().skip(start_index).collect::<String>();
|
||||
if is_string(str_from_index) {
|
||||
return Some(make_token(
|
||||
TokenType::String,
|
||||
@ -348,21 +348,22 @@ fn return_token_at_index(str: &str, start_index: usize) -> Option<Token> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn lexer(str: &str) -> Vec<Token> {
|
||||
fn recursively_tokenise(str: &str, current_index: usize, previous_tokens: Vec<Token>) -> Vec<Token> {
|
||||
if current_index >= str.len() {
|
||||
return previous_tokens;
|
||||
}
|
||||
let token = return_token_at_index(str, current_index);
|
||||
let Some(token) = token else {
|
||||
return recursively_tokenise(str, current_index + 1, previous_tokens);
|
||||
};
|
||||
let mut new_tokens = previous_tokens;
|
||||
let token_length = token.value.len();
|
||||
new_tokens.push(token);
|
||||
recursively_tokenise(str, current_index + token_length, new_tokens)
|
||||
fn recursively_tokenise(s: &str, current_index: usize, previous_tokens: Vec<Token>) -> Vec<Token> {
|
||||
if current_index >= s.len() {
|
||||
return previous_tokens;
|
||||
}
|
||||
recursively_tokenise(str, 0, Vec::new())
|
||||
let token = return_token_at_index(s, current_index);
|
||||
let Some(token) = token else {
|
||||
return recursively_tokenise(s, current_index + 1, previous_tokens);
|
||||
};
|
||||
let mut new_tokens = previous_tokens;
|
||||
let token_length = token.value.len();
|
||||
new_tokens.push(token);
|
||||
recursively_tokenise(s, current_index + token_length, new_tokens)
|
||||
}
|
||||
|
||||
pub fn lexer(s: &str) -> Vec<Token> {
|
||||
recursively_tokenise(s, 0, Vec::new())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -76,7 +76,8 @@ pub fn recast_wasm(json_str: &str) -> Result<JsValue, JsError> {
|
||||
let program: kcl_lib::abstract_syntax_tree_types::Program =
|
||||
serde_json::from_str(json_str).map_err(JsError::from)?;
|
||||
|
||||
let result = program.recast("", false);
|
||||
// Use the default options until we integrate into the UI the ability to change them.
|
||||
let result = program.recast(&Default::default(), 0);
|
||||
Ok(JsValue::from_serde(&result)?)
|
||||
}
|
||||
|
||||
|
98
src/wasm-lib/tests/executor/main.rs
Normal file
98
src/wasm-lib/tests/executor/main.rs
Normal file
@ -0,0 +1,98 @@
|
||||
use anyhow::Result;
|
||||
|
||||
/// Executes a kcl program and takes a snapshot of the result.
|
||||
/// This returns the bytes of the snapshot.
|
||||
async fn execute_and_snapshot(code: &str) -> Result<image::DynamicImage> {
|
||||
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
|
||||
let http_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60));
|
||||
let ws_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60))
|
||||
.tcp_keepalive(std::time::Duration::from_secs(600))
|
||||
.http1_only();
|
||||
|
||||
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
|
||||
|
||||
// Create the client.
|
||||
let client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
|
||||
|
||||
let ws = client
|
||||
.modeling()
|
||||
.commands_ws(None, None, None, None, Some(false))
|
||||
.await?;
|
||||
|
||||
// Create a temporary file to write the output to.
|
||||
let output_file = std::env::temp_dir().join(format!("kcl_output_{}.png", uuid::Uuid::new_v4()));
|
||||
|
||||
let tokens = kcl_lib::tokeniser::lexer(code);
|
||||
let parser = kcl_lib::parser::Parser::new(tokens);
|
||||
let program = parser.ast()?;
|
||||
let mut mem: kcl_lib::executor::ProgramMemory = Default::default();
|
||||
let mut engine = kcl_lib::engine::EngineConnection::new(
|
||||
ws,
|
||||
std::env::temp_dir().display().to_string().as_str(),
|
||||
output_file.display().to_string().as_str(),
|
||||
)
|
||||
.await?;
|
||||
let _ = kcl_lib::executor::execute(program, &mut mem, kcl_lib::executor::BodyType::Root, &mut engine)?;
|
||||
|
||||
// Send a snapshot request to the engine.
|
||||
engine.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
kcl_lib::executor::SourceRange::default(),
|
||||
kittycad::types::ModelingCmd::TakeSnapshot {
|
||||
format: kittycad::types::ImageFormat::Png,
|
||||
},
|
||||
)?;
|
||||
|
||||
// Wait for the snapshot to be taken.
|
||||
engine.wait_for_snapshot().await;
|
||||
|
||||
// Read the output file.
|
||||
let actual = image::io::Reader::open(output_file).unwrap().decode().unwrap();
|
||||
Ok(actual)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_with_function_sketch() {
|
||||
let code = r#"const box = (h, l, w) => {
|
||||
const myBox = startSketchAt([0,0])
|
||||
|> line([0, l], %)
|
||||
|> line([w, 0], %)
|
||||
|> line([0, -l], %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)
|
||||
|
||||
return myBox
|
||||
}
|
||||
|
||||
const fnBox = box(3, 6, 10)
|
||||
|
||||
show(fnBox)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/function_sketch.png", &result, 1.0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_with_angled_line() {
|
||||
let code = r#"const part001 = startSketchAt([4.83, 12.56])
|
||||
|> line([15.1, 2.48], %)
|
||||
|> line({ to: [3.15, -9.85], tag: 'seg01' }, %)
|
||||
|> line([-15.17, -4.1], %)
|
||||
|> angledLine([segAng('seg01', %), 12.35], %)
|
||||
|> line([-13.02, 10.03], %)
|
||||
|> close(%)
|
||||
|> extrude(4, %)
|
||||
|
||||
show(part001)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/angled_line.png", &result, 1.0);
|
||||
}
|
BIN
src/wasm-lib/tests/executor/outputs/angled_line.png
Normal file
BIN
src/wasm-lib/tests/executor/outputs/angled_line.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
BIN
src/wasm-lib/tests/executor/outputs/function_sketch.png
Normal file
BIN
src/wasm-lib/tests/executor/outputs/function_sketch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
@ -1530,10 +1530,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
|
||||
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
||||
|
||||
"@kittycad/lib@^0.0.35":
|
||||
version "0.0.35"
|
||||
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.35.tgz#bde8868048f9fd53f8309e7308aeba622898b935"
|
||||
integrity sha512-qM8AyP2QUlDfPWNxb1Fs/Pq9AebGVDN1OHjByxbGomKCy0jFdN2TsyDdhQH/CAZGfBCgPEfr5bq6rkUBGSXcNw==
|
||||
"@kittycad/lib@^0.0.37":
|
||||
version "0.0.37"
|
||||
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.37.tgz#ec4f6c4fb5d06402a19339f3374036b6582d2265"
|
||||
integrity sha512-P8p9FeLV79/0Lfd0RioBta1drzhmpROnU4YV38+zsAA4LhibQCTjeekRkxVvHztGumPxz9pPsAeeLJyuz2RWKQ==
|
||||
dependencies:
|
||||
node-fetch "3.3.2"
|
||||
openapi-types "^12.0.0"
|
||||
|
Reference in New Issue
Block a user