Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
9e2a94fcd9 | |||
8a3e8d331d | |||
1be9b2612c | |||
7c9aaeafa2 | |||
46c0078885 | |||
87ebf3b1d6 | |||
45238f8196 | |||
44f3a12fbe | |||
61acada2a0 | |||
c68fbbd89d | |||
97a0b6a543 | |||
3bccae492d | |||
0120a89d9c |
@ -3,5 +3,4 @@ VITE_KC_API_BASE_URL=https://api.dev.kittycad.io
|
|||||||
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
|
VITE_KC_SITE_BASE_URL=https://dev.kittycad.io
|
||||||
VITE_KC_SKIP_AUTH=false
|
VITE_KC_SKIP_AUTH=false
|
||||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||||
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=0
|
|
||||||
VITE_KC_SENTRY_DSN=
|
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_SITE_BASE_URL=https://kittycad.io
|
||||||
VITE_KC_SKIP_AUTH=false
|
VITE_KC_SKIP_AUTH=false
|
||||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||||
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS=30000
|
|
||||||
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
|
VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.9.0",
|
"@codemirror/autocomplete": "^6.9.0",
|
||||||
@ -10,7 +10,7 @@
|
|||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.7.13",
|
"@headlessui/react": "^1.7.13",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@kittycad/lib": "^0.0.36",
|
"@kittycad/lib": "^0.0.37",
|
||||||
"@lezer/javascript": "^1.4.7",
|
"@lezer/javascript": "^1.4.7",
|
||||||
"@open-rpc/client-js": "^1.8.1",
|
"@open-rpc/client-js": "^1.8.1",
|
||||||
"@react-hook/resize-observer": "^1.2.6",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
@ -70,7 +70,7 @@
|
|||||||
"fmt": "prettier --write ./src",
|
"fmt": "prettier --write ./src",
|
||||||
"fmt-check": "prettier --check ./src",
|
"fmt-check": "prettier --check ./src",
|
||||||
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt && yarn remove-importmeta",
|
"build:wasm": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -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",
|
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||||
"lint": "eslint --fix src",
|
"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"
|
"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": {
|
"package": {
|
||||||
"productName": "kittycad-modeling",
|
"productName": "kittycad-modeling",
|
||||||
"version": "0.4.0"
|
"version": "0.5.0"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
275
src/App.tsx
275
src/App.tsx
@ -2,7 +2,6 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useMemo,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
MouseEventHandler,
|
MouseEventHandler,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
@ -10,30 +9,20 @@ import { DebugPanel } from './components/DebugPanel'
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { asyncParser } from './lang/abstractSyntaxTree'
|
import { asyncParser } from './lang/abstractSyntaxTree'
|
||||||
import { _executor } from './lang/executor'
|
import { _executor } from './lang/executor'
|
||||||
import CodeMirror, { Extension } from '@uiw/react-codemirror'
|
import { PaneType, useStore } from './useStore'
|
||||||
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 { Logs, KCLErrors } from './components/Logs'
|
import { Logs, KCLErrors } from './components/Logs'
|
||||||
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
||||||
import { MemoryPanel } from './components/MemoryPanel'
|
import { MemoryPanel } from './components/MemoryPanel'
|
||||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||||
import { Stream } from './components/Stream'
|
import { Stream } from './components/Stream'
|
||||||
import ModalContainer from 'react-modal-promise'
|
import ModalContainer from 'react-modal-promise'
|
||||||
import { FromServer, IntoServer } from './editor/lsp/codec'
|
|
||||||
import {
|
import {
|
||||||
EngineCommand,
|
EngineCommand,
|
||||||
EngineCommandManager,
|
EngineCommandManager,
|
||||||
} from './lang/std/engineConnection'
|
} from './lang/std/engineConnection'
|
||||||
import { isOverlap, throttle } from './lib/utils'
|
import { throttle } from './lib/utils'
|
||||||
import { AppHeader } from './components/AppHeader'
|
import { AppHeader } from './components/AppHeader'
|
||||||
import { KCLError, kclErrToDiagnostic } from './lang/errors'
|
import { KCLError } from './lang/errors'
|
||||||
import { Resizable } from 're-resizable'
|
import { Resizable } from 're-resizable'
|
||||||
import {
|
import {
|
||||||
faCode,
|
faCode,
|
||||||
@ -41,97 +30,75 @@ import {
|
|||||||
faSquareRootVariable,
|
faSquareRootVariable,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { TEST } from './env'
|
|
||||||
import { getNormalisedCoordinates } from './lib/utils'
|
import { getNormalisedCoordinates } from './lib/utils'
|
||||||
import { Themes, getSystemTheme } from './lib/theme'
|
|
||||||
import { isTauri } from './lib/isTauri'
|
import { isTauri } from './lib/isTauri'
|
||||||
import { useLoaderData, useParams } from 'react-router-dom'
|
import { useLoaderData } from 'react-router-dom'
|
||||||
import { writeTextFile } from '@tauri-apps/api/fs'
|
|
||||||
import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
|
|
||||||
import { IndexLoaderData } from './Router'
|
import { IndexLoaderData } from './Router'
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import { onboardingPaths } from 'routes/Onboarding'
|
import { onboardingPaths } from 'routes/Onboarding'
|
||||||
import { LanguageServerClient } from 'editor/lsp'
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
import kclLanguage from 'editor/lsp/language'
|
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
import { CSSRuleObject } from 'tailwindcss/types/config'
|
import { CodeMenu } from 'components/CodeMenu'
|
||||||
|
import { TextEditor } from 'components/TextEditor'
|
||||||
|
import { Themes, getSystemTheme } from 'lib/theme'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
||||||
const pathParams = useParams()
|
|
||||||
const streamRef = useRef<HTMLDivElement>(null)
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
useHotKeyListener()
|
useHotKeyListener()
|
||||||
const {
|
const {
|
||||||
editorView,
|
|
||||||
setEditorView,
|
|
||||||
setSelectionRanges,
|
|
||||||
selectionRanges,
|
|
||||||
addLog,
|
addLog,
|
||||||
addKCLError,
|
addKCLError,
|
||||||
code,
|
|
||||||
setCode,
|
setCode,
|
||||||
setAst,
|
setAst,
|
||||||
setError,
|
setError,
|
||||||
setProgramMemory,
|
setProgramMemory,
|
||||||
resetLogs,
|
resetLogs,
|
||||||
resetKCLErrors,
|
resetKCLErrors,
|
||||||
selectionRangeTypeMap,
|
|
||||||
setArtifactMap,
|
setArtifactMap,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
setEngineCommandManager,
|
setEngineCommandManager,
|
||||||
highlightRange,
|
highlightRange,
|
||||||
setHighlightRange,
|
setHighlightRange,
|
||||||
setCursor2,
|
setCursor2,
|
||||||
sourceRangeMap,
|
|
||||||
setMediaStream,
|
setMediaStream,
|
||||||
setIsStreamReady,
|
setIsStreamReady,
|
||||||
isStreamReady,
|
isStreamReady,
|
||||||
isLSPServerReady,
|
buttonDownInStream,
|
||||||
setIsLSPServerReady,
|
|
||||||
isMouseDownInStream,
|
|
||||||
formatCode,
|
|
||||||
openPanes,
|
openPanes,
|
||||||
setOpenPanes,
|
setOpenPanes,
|
||||||
didDragInStream,
|
didDragInStream,
|
||||||
setStreamDimensions,
|
setStreamDimensions,
|
||||||
streamDimensions,
|
streamDimensions,
|
||||||
|
setIsExecuting,
|
||||||
|
defferedCode,
|
||||||
} = useStore((s) => ({
|
} = useStore((s) => ({
|
||||||
editorView: s.editorView,
|
|
||||||
setEditorView: s.setEditorView,
|
|
||||||
setSelectionRanges: s.setSelectionRanges,
|
|
||||||
selectionRanges: s.selectionRanges,
|
|
||||||
setGuiMode: s.setGuiMode,
|
|
||||||
addLog: s.addLog,
|
addLog: s.addLog,
|
||||||
code: s.code,
|
defferedCode: s.defferedCode,
|
||||||
setCode: s.setCode,
|
setCode: s.setCode,
|
||||||
setAst: s.setAst,
|
setAst: s.setAst,
|
||||||
setError: s.setError,
|
setError: s.setError,
|
||||||
setProgramMemory: s.setProgramMemory,
|
setProgramMemory: s.setProgramMemory,
|
||||||
resetLogs: s.resetLogs,
|
resetLogs: s.resetLogs,
|
||||||
resetKCLErrors: s.resetKCLErrors,
|
resetKCLErrors: s.resetKCLErrors,
|
||||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
|
||||||
setArtifactMap: s.setArtifactNSourceRangeMaps,
|
setArtifactMap: s.setArtifactNSourceRangeMaps,
|
||||||
engineCommandManager: s.engineCommandManager,
|
engineCommandManager: s.engineCommandManager,
|
||||||
setEngineCommandManager: s.setEngineCommandManager,
|
setEngineCommandManager: s.setEngineCommandManager,
|
||||||
highlightRange: s.highlightRange,
|
highlightRange: s.highlightRange,
|
||||||
setHighlightRange: s.setHighlightRange,
|
setHighlightRange: s.setHighlightRange,
|
||||||
isShiftDown: s.isShiftDown,
|
|
||||||
setCursor: s.setCursor,
|
|
||||||
setCursor2: s.setCursor2,
|
setCursor2: s.setCursor2,
|
||||||
sourceRangeMap: s.sourceRangeMap,
|
|
||||||
setMediaStream: s.setMediaStream,
|
setMediaStream: s.setMediaStream,
|
||||||
isStreamReady: s.isStreamReady,
|
isStreamReady: s.isStreamReady,
|
||||||
setIsStreamReady: s.setIsStreamReady,
|
setIsStreamReady: s.setIsStreamReady,
|
||||||
isLSPServerReady: s.isLSPServerReady,
|
buttonDownInStream: s.buttonDownInStream,
|
||||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
|
||||||
isMouseDownInStream: s.isMouseDownInStream,
|
|
||||||
formatCode: s.formatCode,
|
|
||||||
addKCLError: s.addKCLError,
|
addKCLError: s.addKCLError,
|
||||||
openPanes: s.openPanes,
|
openPanes: s.openPanes,
|
||||||
setOpenPanes: s.setOpenPanes,
|
setOpenPanes: s.setOpenPanes,
|
||||||
didDragInStream: s.didDragInStream,
|
didDragInStream: s.didDragInStream,
|
||||||
setStreamDimensions: s.setStreamDimensions,
|
setStreamDimensions: s.setStreamDimensions,
|
||||||
streamDimensions: s.streamDimensions,
|
streamDimensions: s.streamDimensions,
|
||||||
|
setIsExecuting: s.setIsExecuting,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -139,7 +106,7 @@ export function App() {
|
|||||||
context: { token },
|
context: { token },
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
context: { showDebugPanel, theme, onboardingStatus, textWrapping },
|
context: { showDebugPanel, onboardingStatus, cameraControls, theme },
|
||||||
},
|
},
|
||||||
} = useGlobalStateContext()
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
@ -180,80 +147,6 @@ export function App() {
|
|||||||
}
|
}
|
||||||
}, [loadedCode, setCode])
|
}, [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 streamWidth = streamRef?.current?.offsetWidth
|
||||||
const streamHeight = streamRef?.current?.offsetHeight
|
const streamHeight = streamRef?.current?.offsetHeight
|
||||||
|
|
||||||
@ -287,16 +180,17 @@ export function App() {
|
|||||||
let unsubFn: any[] = []
|
let unsubFn: any[] = []
|
||||||
const asyncWrap = async () => {
|
const asyncWrap = async () => {
|
||||||
try {
|
try {
|
||||||
if (!code) {
|
if (!defferedCode) {
|
||||||
setAst(null)
|
setAst(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const _ast = await asyncParser(code)
|
const _ast = await asyncParser(defferedCode)
|
||||||
setAst(_ast)
|
setAst(_ast)
|
||||||
resetLogs()
|
resetLogs()
|
||||||
resetKCLErrors()
|
resetKCLErrors()
|
||||||
engineCommandManager.endSession()
|
engineCommandManager.endSession()
|
||||||
engineCommandManager.startNewSession()
|
engineCommandManager.startNewSession()
|
||||||
|
setIsExecuting(true)
|
||||||
const programMemory = await _executor(
|
const programMemory = await _executor(
|
||||||
_ast,
|
_ast,
|
||||||
{
|
{
|
||||||
@ -328,6 +222,7 @@ export function App() {
|
|||||||
|
|
||||||
const { artifactMap, sourceRangeMap } =
|
const { artifactMap, sourceRangeMap } =
|
||||||
await engineCommandManager.waitForAllCommands()
|
await engineCommandManager.waitForAllCommands()
|
||||||
|
setIsExecuting(false)
|
||||||
|
|
||||||
setArtifactMap({ artifactMap, sourceRangeMap })
|
setArtifactMap({ artifactMap, sourceRangeMap })
|
||||||
const unSubHover = engineCommandManager.subscribeToUnreliable({
|
const unSubHover = engineCommandManager.subscribeToUnreliable({
|
||||||
@ -362,6 +257,7 @@ export function App() {
|
|||||||
|
|
||||||
setError()
|
setError()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
setIsExecuting(false)
|
||||||
if (e instanceof KCLError) {
|
if (e instanceof KCLError) {
|
||||||
addKCLError(e)
|
addKCLError(e)
|
||||||
} else {
|
} else {
|
||||||
@ -375,33 +271,38 @@ export function App() {
|
|||||||
return () => {
|
return () => {
|
||||||
unsubFn.forEach((fn) => fn())
|
unsubFn.forEach((fn) => fn())
|
||||||
}
|
}
|
||||||
}, [code, isStreamReady, engineCommandManager])
|
}, [defferedCode, isStreamReady, engineCommandManager])
|
||||||
|
|
||||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||||
engineCommandManager?.sendSceneCommand(message)
|
engineCommandManager?.sendSceneCommand(message)
|
||||||
}, 16)
|
}, 16)
|
||||||
const handleMouseMove: MouseEventHandler<HTMLDivElement> = ({
|
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
clientX,
|
e.nativeEvent.preventDefault()
|
||||||
clientY,
|
|
||||||
ctrlKey,
|
|
||||||
shiftKey,
|
|
||||||
currentTarget,
|
|
||||||
nativeEvent,
|
|
||||||
}) => {
|
|
||||||
nativeEvent.preventDefault()
|
|
||||||
|
|
||||||
const { x, y } = getNormalisedCoordinates({
|
const { x, y } = getNormalisedCoordinates({
|
||||||
clientX,
|
clientX: e.clientX,
|
||||||
clientY,
|
clientY: e.clientY,
|
||||||
el: currentTarget,
|
el: e.currentTarget,
|
||||||
...streamDimensions,
|
...streamDimensions,
|
||||||
})
|
})
|
||||||
|
|
||||||
const interaction = ctrlKey ? 'zoom' : shiftKey ? 'pan' : 'rotate'
|
|
||||||
|
|
||||||
const newCmdId = uuidv4()
|
const newCmdId = uuidv4()
|
||||||
|
|
||||||
if (isMouseDownInStream) {
|
if (buttonDownInStream) {
|
||||||
|
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 {
|
||||||
|
return
|
||||||
|
}
|
||||||
debounceSocketSend({
|
debounceSocketSend({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -423,64 +324,6 @@ export function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
|
||||||
// But the server happens async so we break this into two parts.
|
|
||||||
// Below is the client and server promise.
|
|
||||||
const { lspClient } = useMemo(() => {
|
|
||||||
const intoServer: IntoServer = new IntoServer()
|
|
||||||
const fromServer: FromServer = FromServer.create()
|
|
||||||
const client = new Client(fromServer, intoServer)
|
|
||||||
if (!TEST) {
|
|
||||||
Server.initialize(intoServer, fromServer).then((lspServer) => {
|
|
||||||
lspServer.start()
|
|
||||||
setIsLSPServerReady(true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const lspClient = new LanguageServerClient({ client })
|
|
||||||
return { lspClient }
|
|
||||||
}, [setIsLSPServerReady])
|
|
||||||
|
|
||||||
// Here we initialize the plugin which will start the client.
|
|
||||||
// When we have multi-file support the name of the file will be a dep of
|
|
||||||
// this use memo, as well as the directory structure, which I think is
|
|
||||||
// a good setup becuase it will restart the client but not the server :)
|
|
||||||
// We do not want to restart the server, its just wasteful.
|
|
||||||
const kclLSP = useMemo(() => {
|
|
||||||
let plugin = null
|
|
||||||
if (isLSPServerReady && !TEST) {
|
|
||||||
// Set up the lsp plugin.
|
|
||||||
const lsp = kclLanguage({
|
|
||||||
// When we have more than one file, we'll need to change this.
|
|
||||||
documentUri: `file:///we-just-have-one-file-for-now.kcl`,
|
|
||||||
workspaceFolders: null,
|
|
||||||
client: lspClient,
|
|
||||||
})
|
|
||||||
|
|
||||||
plugin = lsp
|
|
||||||
}
|
|
||||||
return plugin
|
|
||||||
}, [lspClient, isLSPServerReady])
|
|
||||||
|
|
||||||
const editorExtensions = useMemo(() => {
|
|
||||||
const extensions = [lineHighlightField] as Extension[]
|
|
||||||
|
|
||||||
if (kclLSP) extensions.push(kclLSP)
|
|
||||||
|
|
||||||
// These extensions have proven to mess with vitest
|
|
||||||
if (!TEST) {
|
|
||||||
extensions.push(
|
|
||||||
lintGutter(),
|
|
||||||
linter((_view) => {
|
|
||||||
return kclErrToDiagnostic(useStore.getState().kclErrors)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
if (textWrapping === 'On') extensions.push(EditorView.lineWrapping)
|
|
||||||
}
|
|
||||||
|
|
||||||
return extensions
|
|
||||||
}, [kclLSP, textWrapping])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-screen overflow-hidden relative flex flex-col cursor-pointer select-none"
|
className="h-screen overflow-hidden relative flex flex-col cursor-pointer select-none"
|
||||||
@ -491,7 +334,7 @@ export function App() {
|
|||||||
className={
|
className={
|
||||||
'transition-opacity transition-duration-75 ' +
|
'transition-opacity transition-duration-75 ' +
|
||||||
paneOpacity +
|
paneOpacity +
|
||||||
(isMouseDownInStream ? ' pointer-events-none' : '')
|
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||||
}
|
}
|
||||||
project={project}
|
project={project}
|
||||||
enableMenu={true}
|
enableMenu={true}
|
||||||
@ -500,7 +343,7 @@ export function App() {
|
|||||||
<Resizable
|
<Resizable
|
||||||
className={
|
className={
|
||||||
'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
|
'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 '
|
? ' pointer-events-none '
|
||||||
: ' ') +
|
: ' ') +
|
||||||
paneOpacity
|
paneOpacity
|
||||||
@ -524,31 +367,9 @@ export function App() {
|
|||||||
icon={faCode}
|
icon={faCode}
|
||||||
className="open:!mb-2"
|
className="open:!mb-2"
|
||||||
open={openPanes.includes('code')}
|
open={openPanes.includes('code')}
|
||||||
|
menu={<CodeMenu />}
|
||||||
>
|
>
|
||||||
<div className="px-2 py-1">
|
<TextEditor theme={editorTheme} />
|
||||||
<button
|
|
||||||
// disabled={!shouldFormat}
|
|
||||||
onClick={formatCode}
|
|
||||||
// className={`${!shouldFormat && 'text-gray-300'}`}
|
|
||||||
>
|
|
||||||
format
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
id="code-mirror-override"
|
|
||||||
className="full-height-subtract"
|
|
||||||
style={{ '--height-subtract': '4.25rem' } as CSSRuleObject}
|
|
||||||
>
|
|
||||||
<CodeMirror
|
|
||||||
className="h-full"
|
|
||||||
value={code}
|
|
||||||
extensions={editorExtensions}
|
|
||||||
onChange={onChange}
|
|
||||||
onUpdate={onUpdate}
|
|
||||||
theme={editorTheme}
|
|
||||||
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CollapsiblePanel>
|
</CollapsiblePanel>
|
||||||
<section className="flex flex-col">
|
<section className="flex flex-col">
|
||||||
<MemoryPanel
|
<MemoryPanel
|
||||||
@ -579,7 +400,7 @@ export function App() {
|
|||||||
className={
|
className={
|
||||||
'transition-opacity transition-duration-75 ' +
|
'transition-opacity transition-duration-75 ' +
|
||||||
paneOpacity +
|
paneOpacity +
|
||||||
(isMouseDownInStream ? ' pointer-events-none' : '')
|
(buttonDownInStream ? ' pointer-events-none' : '')
|
||||||
}
|
}
|
||||||
open={openPanes.includes('debug')}
|
open={openPanes.includes('debug')}
|
||||||
/>
|
/>
|
||||||
|
@ -8,7 +8,6 @@ import { EqualAngle } from './components/Toolbar/EqualAngle'
|
|||||||
import { Intersect } from './components/Toolbar/Intersect'
|
import { Intersect } from './components/Toolbar/Intersect'
|
||||||
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
|
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
|
||||||
import { SetAngleLength } from './components/Toolbar/setAngleLength'
|
import { SetAngleLength } from './components/Toolbar/setAngleLength'
|
||||||
import { ConvertToVariable } from './components/Toolbar/ConvertVariable'
|
|
||||||
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
||||||
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
||||||
import { Fragment, useEffect } from 'react'
|
import { Fragment, useEffect } from 'react'
|
||||||
@ -164,7 +163,6 @@ export const Toolbar = () => {
|
|||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<ConvertToVariable />
|
|
||||||
<HorzVert horOrVert="horizontal" />
|
<HorzVert horOrVert="horizontal" />
|
||||||
<HorzVert horOrVert="vertical" />
|
<HorzVert horOrVert="vertical" />
|
||||||
<EqualLength />
|
<EqualLength />
|
||||||
|
@ -198,29 +198,25 @@ export const CreateNewVariable = ({
|
|||||||
isNewVariableNameUnique,
|
isNewVariableNameUnique,
|
||||||
setNewVariableName,
|
setNewVariableName,
|
||||||
shouldCreateVariable,
|
shouldCreateVariable,
|
||||||
setShouldCreateVariable,
|
setShouldCreateVariable = () => {},
|
||||||
showCheckbox = true,
|
showCheckbox = true,
|
||||||
}: {
|
}: {
|
||||||
isNewVariableNameUnique: boolean
|
isNewVariableNameUnique: boolean
|
||||||
newVariableName: string
|
newVariableName: string
|
||||||
setNewVariableName: (a: string) => void
|
setNewVariableName: (a: string) => void
|
||||||
shouldCreateVariable: boolean
|
shouldCreateVariable?: boolean
|
||||||
setShouldCreateVariable: (a: boolean) => void
|
setShouldCreateVariable?: (a: boolean) => void
|
||||||
showCheckbox?: boolean
|
showCheckbox?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<label
|
<label htmlFor="create-new-variable" className="block mt-3 font-mono">
|
||||||
htmlFor="create-new-variable"
|
|
||||||
className="block text-sm font-medium text-gray-700 mt-3 font-mono"
|
|
||||||
>
|
|
||||||
Create new variable
|
Create new variable
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1 flex flex-1">
|
<div className="mt-1 flex gap-2 items-center">
|
||||||
{showCheckbox && (
|
{showCheckbox && (
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
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}
|
checked={shouldCreateVariable}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setShouldCreateVariable(e.target.checked)
|
setShouldCreateVariable(e.target.checked)
|
||||||
@ -232,7 +228,10 @@ export const CreateNewVariable = ({
|
|||||||
disabled={!shouldCreateVariable}
|
disabled={!shouldCreateVariable}
|
||||||
name="create-new-variable"
|
name="create-new-variable"
|
||||||
id="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' : ''
|
!shouldCreateVariable ? 'opacity-50' : ''
|
||||||
}`}
|
}`}
|
||||||
value={newVariableName}
|
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>
|
||||||
|
)
|
||||||
|
}
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
.header {
|
.header {
|
||||||
@apply sticky top-0 z-10 cursor-pointer;
|
@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 font-mono text-xs font-bold select-none text-chalkboard-90;
|
||||||
@apply bg-chalkboard-20;
|
@apply bg-chalkboard-20;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ export interface CollapsiblePanelProps
|
|||||||
title: string
|
title: string
|
||||||
icon?: IconDefinition
|
icon?: IconDefinition
|
||||||
open?: boolean
|
open?: boolean
|
||||||
|
menu?: React.ReactNode
|
||||||
iconClassNames?: {
|
iconClassNames?: {
|
||||||
bg?: string
|
bg?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
@ -18,9 +19,11 @@ export const PanelHeader = ({
|
|||||||
title,
|
title,
|
||||||
icon,
|
icon,
|
||||||
iconClassNames,
|
iconClassNames,
|
||||||
|
menu,
|
||||||
}: CollapsiblePanelProps) => {
|
}: CollapsiblePanelProps) => {
|
||||||
return (
|
return (
|
||||||
<summary className={styles.header}>
|
<summary className={styles.header}>
|
||||||
|
<div className="flex gap-2 align-center flex-1">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon={icon}
|
icon={icon}
|
||||||
bgClassName={
|
bgClassName={
|
||||||
@ -33,6 +36,10 @@ export const PanelHeader = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{title}
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className="group-open:opacity-100 opacity-0 group-open:pointer-events-auto pointer-events-none">
|
||||||
|
{menu}
|
||||||
|
</div>
|
||||||
</summary>
|
</summary>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -43,6 +50,7 @@ export const CollapsiblePanel = ({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
iconClassNames,
|
iconClassNames,
|
||||||
|
menu,
|
||||||
...props
|
...props
|
||||||
}: CollapsiblePanelProps) => {
|
}: CollapsiblePanelProps) => {
|
||||||
return (
|
return (
|
||||||
@ -50,7 +58,12 @@ export const CollapsiblePanel = ({
|
|||||||
{...props}
|
{...props}
|
||||||
className={styles.panel + ' group ' + (className || '')}
|
className={styles.panel + ' group ' + (className || '')}
|
||||||
>
|
>
|
||||||
<PanelHeader title={title} icon={icon} iconClassNames={iconClassNames} />
|
<PanelHeader
|
||||||
|
title={title}
|
||||||
|
icon={icon}
|
||||||
|
iconClassNames={iconClassNames}
|
||||||
|
menu={menu}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</details>
|
</details>
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { Dialog, Transition } from '@headlessui/react'
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
import { useCalc, CreateNewVariable } from './AvailableVarsHelpers'
|
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 = ({
|
export const SetVarNameModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
@ -19,21 +22,23 @@ export const SetVarNameModal = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<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
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0"
|
enterFrom="opacity-0 translate-y-4"
|
||||||
enterTo="opacity-100"
|
enterTo="opacity-100 translate-y-0"
|
||||||
leave="ease-in duration-200"
|
leave="ease-in duration-75"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
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>
|
</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
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
@ -43,43 +48,39 @@ export const SetVarNameModal = ({
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<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.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">
|
||||||
<Dialog.Title
|
<form
|
||||||
as="h3"
|
onSubmit={(e) => {
|
||||||
className="text-lg font-medium leading-6 text-gray-900 capitalize"
|
e.preventDefault()
|
||||||
|
onResolve({
|
||||||
|
variableName: newVariableName,
|
||||||
|
})
|
||||||
|
toast.success(`Added variable ${newVariableName}`)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Set {valueName}
|
|
||||||
</Dialog.Title>
|
|
||||||
|
|
||||||
<CreateNewVariable
|
<CreateNewVariable
|
||||||
setNewVariableName={setNewVariableName}
|
setNewVariableName={setNewVariableName}
|
||||||
newVariableName={newVariableName}
|
newVariableName={newVariableName}
|
||||||
isNewVariableNameUnique={isNewVariableNameUnique}
|
isNewVariableNameUnique={isNewVariableNameUnique}
|
||||||
shouldCreateVariable={true}
|
shouldCreateVariable={true}
|
||||||
setShouldCreateVariable={() => {}}
|
showCheckbox={false}
|
||||||
/>
|
/>
|
||||||
<div className="mt-4">
|
<div className="mt-8 flex justify-between">
|
||||||
<button
|
<ActionButton
|
||||||
type="button"
|
Element="button"
|
||||||
|
type="submit"
|
||||||
disabled={!isNewVariableNameUnique}
|
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 ${
|
icon={{ icon: faPlus }}
|
||||||
!isNewVariableNameUnique
|
|
||||||
? 'opacity-50 cursor-not-allowed'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
onClick={() =>
|
|
||||||
onResolve({
|
|
||||||
variableName: newVariableName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Add variable
|
Add variable
|
||||||
</button>
|
</ActionButton>
|
||||||
|
<ActionButton Element="button" onClick={() => onReject(false)}>
|
||||||
|
Cancel
|
||||||
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition>
|
</Transition>
|
||||||
)
|
)
|
||||||
|
@ -9,6 +9,9 @@ import { v4 as uuidv4 } from 'uuid'
|
|||||||
import { useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { getNormalisedCoordinates } from '../lib/utils'
|
import { getNormalisedCoordinates } from '../lib/utils'
|
||||||
import Loading from './Loading'
|
import Loading from './Loading'
|
||||||
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
|
|
||||||
export const Stream = ({ className = '' }) => {
|
export const Stream = ({ className = '' }) => {
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
@ -17,20 +20,26 @@ export const Stream = ({ className = '' }) => {
|
|||||||
const {
|
const {
|
||||||
mediaStream,
|
mediaStream,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
setIsMouseDownInStream,
|
setButtonDownInStream,
|
||||||
didDragInStream,
|
didDragInStream,
|
||||||
setDidDragInStream,
|
setDidDragInStream,
|
||||||
streamDimensions,
|
streamDimensions,
|
||||||
|
isExecuting,
|
||||||
} = useStore((s) => ({
|
} = useStore((s) => ({
|
||||||
mediaStream: s.mediaStream,
|
mediaStream: s.mediaStream,
|
||||||
engineCommandManager: s.engineCommandManager,
|
engineCommandManager: s.engineCommandManager,
|
||||||
isMouseDownInStream: s.isMouseDownInStream,
|
setButtonDownInStream: s.setButtonDownInStream,
|
||||||
setIsMouseDownInStream: s.setIsMouseDownInStream,
|
|
||||||
fileId: s.fileId,
|
fileId: s.fileId,
|
||||||
didDragInStream: s.didDragInStream,
|
didDragInStream: s.didDragInStream,
|
||||||
setDidDragInStream: s.setDidDragInStream,
|
setDidDragInStream: s.setDidDragInStream,
|
||||||
streamDimensions: s.streamDimensions,
|
streamDimensions: s.streamDimensions,
|
||||||
|
isExecuting: s.isExecuting,
|
||||||
}))
|
}))
|
||||||
|
const {
|
||||||
|
settings: {
|
||||||
|
context: { cameraControls },
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@ -43,23 +52,29 @@ export const Stream = ({ className = '' }) => {
|
|||||||
videoRef.current.srcObject = mediaStream
|
videoRef.current.srcObject = mediaStream
|
||||||
}, [mediaStream, engineCommandManager])
|
}, [mediaStream, engineCommandManager])
|
||||||
|
|
||||||
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = ({
|
const handleMouseDown: MouseEventHandler<HTMLVideoElement> = (e) => {
|
||||||
clientX,
|
|
||||||
clientY,
|
|
||||||
ctrlKey,
|
|
||||||
}) => {
|
|
||||||
if (!videoRef.current) return
|
if (!videoRef.current) return
|
||||||
const { x, y } = getNormalisedCoordinates({
|
const { x, y } = getNormalisedCoordinates({
|
||||||
clientX,
|
clientX: e.clientX,
|
||||||
clientY,
|
clientY: e.clientY,
|
||||||
el: videoRef.current,
|
el: videoRef.current,
|
||||||
...streamDimensions,
|
...streamDimensions,
|
||||||
})
|
})
|
||||||
console.log('click', x, y)
|
|
||||||
|
|
||||||
const newId = uuidv4()
|
const newId = uuidv4()
|
||||||
|
|
||||||
const interaction = ctrlKey ? 'pan' : 'rotate'
|
const interactionGuards = cameraMouseDragGuards[cameraControls]
|
||||||
|
let interaction: CameraDragInteractionType_type
|
||||||
|
|
||||||
|
if (interactionGuards.pan.callback(e)) {
|
||||||
|
interaction = 'pan'
|
||||||
|
} else if (interactionGuards.rotate.callback(e)) {
|
||||||
|
interaction = 'rotate'
|
||||||
|
} else if (interactionGuards.zoom.dragCallback(e)) {
|
||||||
|
interaction = 'zoom'
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
engineCommandManager?.sendSceneCommand({
|
engineCommandManager?.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -71,11 +86,13 @@ export const Stream = ({ className = '' }) => {
|
|||||||
cmd_id: newId,
|
cmd_id: newId,
|
||||||
})
|
})
|
||||||
|
|
||||||
setIsMouseDownInStream(true)
|
setButtonDownInStream(e.button)
|
||||||
setClickCoords({ x, y })
|
setClickCoords({ x, y })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
|
const handleScroll: WheelEventHandler<HTMLVideoElement> = (e) => {
|
||||||
|
if (!cameraMouseDragGuards[cameraControls].zoom.scrollCallback(e)) return
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
engineCommandManager?.sendSceneCommand({
|
engineCommandManager?.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -113,7 +130,7 @@ export const Stream = ({ className = '' }) => {
|
|||||||
cmd_id: newCmdId,
|
cmd_id: newCmdId,
|
||||||
})
|
})
|
||||||
|
|
||||||
setIsMouseDownInStream(false)
|
setButtonDownInStream(0)
|
||||||
if (!didDragInStream) {
|
if (!didDragInStream) {
|
||||||
engineCommandManager?.sendSceneCommand({
|
engineCommandManager?.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
@ -155,7 +172,8 @@ export const Stream = ({ className = '' }) => {
|
|||||||
onWheel={handleScroll}
|
onWheel={handleScroll}
|
||||||
onPlay={() => setIsLoading(false)}
|
onPlay={() => setIsLoading(false)}
|
||||||
onMouseMoveCapture={handleMouseMove}
|
onMouseMoveCapture={handleMouseMove}
|
||||||
className="w-full h-full"
|
className={`w-full h-full ${isExecuting && 'blur-md'}`}
|
||||||
|
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
||||||
/>
|
/>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||||
|
267
src/components/TextEditor.tsx
Normal file
267
src/components/TextEditor.tsx
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
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,
|
||||||
|
defferedCode: s.defferedCode,
|
||||||
|
defferedSetCode: s.defferedSetCode,
|
||||||
|
editorView: s.editorView,
|
||||||
|
engineCommandManager: s.engineCommandManager,
|
||||||
|
formatCode: s.formatCode,
|
||||||
|
isLSPServerReady: s.isLSPServerReady,
|
||||||
|
selectionRanges: s.selectionRanges,
|
||||||
|
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||||
|
setCode: s.setCode,
|
||||||
|
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -8,8 +8,6 @@ export const VITE_KC_API_WS_MODELING_URL = import.meta.env
|
|||||||
.VITE_KC_API_WS_MODELING_URL
|
.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_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_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
|
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
|
||||||
.VITE_KC_CONNECTION_TIMEOUT_MS
|
.VITE_KC_CONNECTION_TIMEOUT_MS
|
||||||
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
|
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
|
||||||
|
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 }
|
||||||
|
}
|
@ -1,20 +1,21 @@
|
|||||||
import { SourceRange } from 'lang/executor'
|
import { SourceRange } from 'lang/executor'
|
||||||
import { Selections } from 'useStore'
|
import { Selections } from 'useStore'
|
||||||
import {
|
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
|
||||||
VITE_KC_API_WS_MODELING_URL,
|
|
||||||
VITE_KC_CONNECTION_TIMEOUT_MS,
|
|
||||||
VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS,
|
|
||||||
} from 'env'
|
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { exportSave } from 'lib/exportSave'
|
import { exportSave } from 'lib/exportSave'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import * as Sentry from '@sentry/react'
|
import * as Sentry from '@sentry/react'
|
||||||
|
|
||||||
interface ResultCommand {
|
interface CommandInfo {
|
||||||
|
commandType: CommandTypes
|
||||||
|
range: SourceRange
|
||||||
|
parentId?: string
|
||||||
|
}
|
||||||
|
interface ResultCommand extends CommandInfo {
|
||||||
type: 'result'
|
type: 'result'
|
||||||
data: any
|
data: any
|
||||||
}
|
}
|
||||||
interface PendingCommand {
|
interface PendingCommand extends CommandInfo {
|
||||||
type: 'pending'
|
type: 'pending'
|
||||||
promise: Promise<any>
|
promise: Promise<any>
|
||||||
resolve: (val: any) => void
|
resolve: (val: any) => void
|
||||||
@ -34,6 +35,8 @@ interface NewTrackArgs {
|
|||||||
|
|
||||||
type WebSocketResponse = Models['OkWebSocketResponseData_type']
|
type WebSocketResponse = Models['OkWebSocketResponseData_type']
|
||||||
|
|
||||||
|
type ClientMetrics = Models['ClientMetrics_type']
|
||||||
|
|
||||||
// EngineConnection encapsulates the connection(s) to the Engine
|
// EngineConnection encapsulates the connection(s) to the Engine
|
||||||
// for the EngineCommandManager; namely, the underlying WebSocket
|
// for the EngineCommandManager; namely, the underlying WebSocket
|
||||||
// and WebRTC connections.
|
// and WebRTC connections.
|
||||||
@ -53,6 +56,9 @@ export class EngineConnection {
|
|||||||
private onClose: (engineConnection: EngineConnection) => void
|
private onClose: (engineConnection: EngineConnection) => void
|
||||||
private onNewTrack: (track: NewTrackArgs) => void
|
private onNewTrack: (track: NewTrackArgs) => void
|
||||||
|
|
||||||
|
// TODO: actual type is ClientMetrics
|
||||||
|
private webrtcStatsCollector?: () => Promise<ClientMetrics>
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
url,
|
url,
|
||||||
token,
|
token,
|
||||||
@ -188,6 +194,7 @@ export class EngineConnection {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.shouldTrace()) {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
handshakeSpan.promise,
|
handshakeSpan.promise,
|
||||||
iceSpan.promise,
|
iceSpan.promise,
|
||||||
@ -197,6 +204,7 @@ export class EngineConnection {
|
|||||||
console.log('All spans finished, reporting')
|
console.log('All spans finished, reporting')
|
||||||
webrtcMediaTransaction?.finish()
|
webrtcMediaTransaction?.finish()
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.onWebsocketOpen(this)
|
this.onWebsocketOpen(this)
|
||||||
})
|
})
|
||||||
@ -297,8 +305,10 @@ export class EngineConnection {
|
|||||||
|
|
||||||
this.pc.addEventListener('connectionstatechange', (event) => {
|
this.pc.addEventListener('connectionstatechange', (event) => {
|
||||||
if (this.pc?.iceConnectionState === 'connected') {
|
if (this.pc?.iceConnectionState === 'connected') {
|
||||||
|
if (this.shouldTrace()) {
|
||||||
iceSpan.resolve?.()
|
iceSpan.resolve?.()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.pc.addEventListener('icecandidate', (event) => {
|
this.pc.addEventListener('icecandidate', (event) => {
|
||||||
@ -330,6 +340,17 @@ export class EngineConnection {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(console.log)
|
.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
|
// TODO(paultag): This ought to be both controllable, as well as something
|
||||||
@ -361,127 +382,58 @@ export class EngineConnection {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the background thread to keep an eye on statistical
|
this.webrtcStatsCollector = (): Promise<ClientMetrics> => {
|
||||||
// information about the WebRTC media stream from the server to
|
return new Promise((resolve, reject) => {
|
||||||
// us. We'll also eventually want more global statistical information,
|
if (mediaStream.getVideoTracks().length !== 1) {
|
||||||
// but this will give us a baseline.
|
reject(new Error('too many video tracks to report'))
|
||||||
if (parseInt(VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS) !== 0) {
|
|
||||||
setInterval(() => {
|
|
||||||
if (this.pc === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!this.shouldTrace()) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the WebRTC Statistics API to collect statistical information
|
let videoTrack = mediaStream.getVideoTracks()[0]
|
||||||
// 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) => {
|
this.pc?.getStats(videoTrack).then((videoTrackStats) => {
|
||||||
// Sentry only allows 10 metrics per transaction. We're going
|
// TODO(paultag): this needs type information from the KittyCAD typescript
|
||||||
// to have to pick carefully here, eventually send like a prom
|
// library once it's updated
|
||||||
// file or something to the peer.
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
const transaction = Sentry.startTransaction({
|
|
||||||
name: 'webrtc-stats',
|
|
||||||
})
|
|
||||||
videoTrackStats.forEach((videoTrackReport) => {
|
videoTrackStats.forEach((videoTrackReport) => {
|
||||||
if (videoTrackReport.type === 'inbound-rtp') {
|
if (videoTrackReport.type === 'inbound-rtp') {
|
||||||
// RTC Stream Info
|
client_metrics.rtc_frames_decoded =
|
||||||
// transaction.setMeasurement(
|
videoTrackReport.framesDecoded
|
||||||
// 'mediaStreamTrack.framesDecoded',
|
client_metrics.rtc_frames_dropped =
|
||||||
// videoTrackReport.framesDecoded,
|
videoTrackReport.framesDropped
|
||||||
// 'frame'
|
client_metrics.rtc_frames_received =
|
||||||
// )
|
videoTrackReport.framesReceived
|
||||||
transaction.setMeasurement(
|
client_metrics.rtc_frames_per_second =
|
||||||
'rtcFramesDropped',
|
videoTrackReport.framesPerSecond || 0
|
||||||
videoTrackReport.framesDropped,
|
client_metrics.rtc_freeze_count = videoTrackReport.freezeCount
|
||||||
''
|
client_metrics.rtc_jitter_sec = videoTrackReport.jitter
|
||||||
)
|
client_metrics.rtc_keyframes_decoded =
|
||||||
// transaction.setMeasurement(
|
videoTrackReport.keyFramesDecoded
|
||||||
// 'mediaStreamTrack.framesReceived',
|
client_metrics.rtc_total_freezes_duration_sec =
|
||||||
// videoTrackReport.framesReceived,
|
videoTrackReport.totalFreezesDuration
|
||||||
// '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') {
|
} else if (videoTrackReport.type === 'transport') {
|
||||||
// // Bytes i/o
|
|
||||||
// transaction.setMeasurement(
|
|
||||||
// 'mediaStreamTrack.bytesReceived',
|
|
||||||
// videoTrackReport.bytesReceived,
|
// videoTrackReport.bytesReceived,
|
||||||
// 'byte'
|
|
||||||
// )
|
|
||||||
// transaction.setMeasurement(
|
|
||||||
// 'mediaStreamTrack.bytesSent',
|
|
||||||
// videoTrackReport.bytesSent,
|
// videoTrackReport.bytesSent,
|
||||||
// 'byte'
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
transaction?.finish()
|
resolve(client_metrics)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, VITE_KC_CONNECTION_WEBRTC_REPORT_STATS_MS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onNewTrack({
|
this.onNewTrack({
|
||||||
@ -490,10 +442,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.pc.addEventListener('datachannel', (event) => {
|
||||||
this.unreliableDataChannel = event.channel
|
this.unreliableDataChannel = event.channel
|
||||||
|
|
||||||
@ -537,6 +485,7 @@ export class EngineConnection {
|
|||||||
this.websocket = undefined
|
this.websocket = undefined
|
||||||
this.pc = undefined
|
this.pc = undefined
|
||||||
this.unreliableDataChannel = undefined
|
this.unreliableDataChannel = undefined
|
||||||
|
this.webrtcStatsCollector = undefined
|
||||||
|
|
||||||
this.onClose(this)
|
this.onClose(this)
|
||||||
this.ready = false
|
this.ready = false
|
||||||
@ -546,6 +495,8 @@ export class EngineConnection {
|
|||||||
export type EngineCommand = Models['WebSocketRequest_type']
|
export type EngineCommand = Models['WebSocketRequest_type']
|
||||||
type ModelTypes = Models['OkModelingCmdResponse_type']['type']
|
type ModelTypes = Models['OkModelingCmdResponse_type']['type']
|
||||||
|
|
||||||
|
type CommandTypes = Models['ModelingCmd_type']['type']
|
||||||
|
|
||||||
type UnreliableResponses = Extract<
|
type UnreliableResponses = Extract<
|
||||||
Models['OkModelingCmdResponse_type'],
|
Models['OkModelingCmdResponse_type'],
|
||||||
{ type: 'highlight_set_entity' }
|
{ type: 'highlight_set_entity' }
|
||||||
@ -687,15 +638,22 @@ export class EngineCommandManager {
|
|||||||
const resolve = command.resolve
|
const resolve = command.resolve
|
||||||
this.artifactMap[id] = {
|
this.artifactMap[id] = {
|
||||||
type: 'result',
|
type: 'result',
|
||||||
|
range: command.range,
|
||||||
|
commandType: command.commandType,
|
||||||
|
parentId: command.parentId ? command.parentId : undefined,
|
||||||
data: modelingResponse,
|
data: modelingResponse,
|
||||||
}
|
}
|
||||||
resolve({
|
resolve({
|
||||||
id,
|
id,
|
||||||
|
commandType: command.commandType,
|
||||||
|
range: command.range,
|
||||||
data: modelingResponse,
|
data: modelingResponse,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.artifactMap[id] = {
|
this.artifactMap[id] = {
|
||||||
type: 'result',
|
type: 'result',
|
||||||
|
commandType: command?.commandType,
|
||||||
|
range: command?.range,
|
||||||
data: modelingResponse,
|
data: modelingResponse,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -747,8 +705,29 @@ export class EngineCommandManager {
|
|||||||
delete this.unreliableSubscriptions[event][id]
|
delete this.unreliableSubscriptions[event][id]
|
||||||
}
|
}
|
||||||
endSession() {
|
endSession() {
|
||||||
// this.websocket?.close()
|
// TODO: instead of sending a single command with `object_ids: Object.keys(this.artifactMap)`
|
||||||
// socket.off('command')
|
// 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: {
|
cusorsSelected(selections: {
|
||||||
otherSelections: Selections['otherSelections']
|
otherSelections: Selections['otherSelections']
|
||||||
@ -801,11 +780,20 @@ export class EngineCommandManager {
|
|||||||
JSON.stringify(command)
|
JSON.stringify(command)
|
||||||
)
|
)
|
||||||
return Promise.resolve()
|
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
|
// since it's not mouse drag or highlighting send over TCP and keep track of the command
|
||||||
this.engineConnection?.send(command)
|
this.engineConnection?.send(command)
|
||||||
return this.handlePendingCommand(command.cmd_id)
|
return this.handlePendingCommand(command.cmd_id, command.cmd)
|
||||||
}
|
}
|
||||||
sendModelingCommand({
|
sendModelingCommand({
|
||||||
id,
|
id,
|
||||||
@ -823,15 +811,35 @@ export class EngineCommandManager {
|
|||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
this.engineConnection?.send(command)
|
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)
|
||||||
}
|
}
|
||||||
handlePendingCommand(id: string) {
|
throw 'shouldnt reach here'
|
||||||
|
}
|
||||||
|
handlePendingCommand(
|
||||||
|
id: string,
|
||||||
|
command: Models['ModelingCmd_type'],
|
||||||
|
range?: SourceRange
|
||||||
|
) {
|
||||||
let resolve: (val: any) => void = () => {}
|
let resolve: (val: any) => void = () => {}
|
||||||
const promise = new Promise((_resolve, reject) => {
|
const promise = new Promise((_resolve, reject) => {
|
||||||
resolve = _resolve
|
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] = {
|
this.artifactMap[id] = {
|
||||||
|
range: range || [0, 0],
|
||||||
type: 'pending',
|
type: 'pending',
|
||||||
|
commandType: command.type,
|
||||||
|
parentId: getParentId(),
|
||||||
promise,
|
promise,
|
||||||
resolve,
|
resolve,
|
||||||
}
|
}
|
||||||
|
133
src/lib/cameraControls.ts
Normal file
133
src/lib/cameraControls.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
const noModifiersPressed = (e: React.MouseEvent) =>
|
||||||
|
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
|
||||||
|
|
||||||
|
export type CADProgram =
|
||||||
|
| 'KittyCAD'
|
||||||
|
| 'OnShape'
|
||||||
|
| 'Solidworks'
|
||||||
|
| 'NX'
|
||||||
|
| 'Creo'
|
||||||
|
| 'AutoCAD'
|
||||||
|
|
||||||
|
export const cadPrograms: CADProgram[] = [
|
||||||
|
'KittyCAD',
|
||||||
|
'OnShape',
|
||||||
|
'Solidworks',
|
||||||
|
'NX',
|
||||||
|
'Creo',
|
||||||
|
'AutoCAD',
|
||||||
|
]
|
||||||
|
|
||||||
|
interface MouseGuardHandler {
|
||||||
|
description: string
|
||||||
|
callback: (e: React.MouseEvent) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MouseGuardZoomHandler {
|
||||||
|
description: string
|
||||||
|
dragCallback: (e: React.MouseEvent) => boolean
|
||||||
|
scrollCallback: (e: React.MouseEvent) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MouseGuard {
|
||||||
|
pan: MouseGuardHandler
|
||||||
|
zoom: MouseGuardZoomHandler
|
||||||
|
rotate: MouseGuardHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cameraMouseDragGuards: Record<CADProgram, MouseGuard> = {
|
||||||
|
KittyCAD: {
|
||||||
|
pan: {
|
||||||
|
description: 'Right click + Shift + drag or middle click + drag',
|
||||||
|
callback: (e) =>
|
||||||
|
(e.button === 3 && 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 === 3 && noModifiersPressed(e)),
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
description: 'Scroll wheel',
|
||||||
|
dragCallback: () => false,
|
||||||
|
scrollCallback: () => true,
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
description: 'Right click + drag',
|
||||||
|
callback: (e) => e.button === 2 && noModifiersPressed(e),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Solidworks: {
|
||||||
|
pan: {
|
||||||
|
description: 'Right click + Ctrl + drag',
|
||||||
|
callback: (e) => e.button === 2 && e.ctrlKey,
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
description: 'Scroll wheel or Middle click + Shift + drag',
|
||||||
|
dragCallback: (e) => e.button === 3 && e.shiftKey,
|
||||||
|
scrollCallback: () => true,
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
description: 'Middle click + drag',
|
||||||
|
callback: (e) => e.button === 3 && noModifiersPressed(e),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NX: {
|
||||||
|
pan: {
|
||||||
|
description: 'Middle click + Shift + drag',
|
||||||
|
callback: (e) => e.button === 3 && e.shiftKey,
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
description: 'Scroll wheel or Middle click + Ctrl + drag',
|
||||||
|
dragCallback: (e) => e.button === 3 && e.ctrlKey,
|
||||||
|
scrollCallback: () => true,
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
description: 'Middle click + drag',
|
||||||
|
callback: (e) => e.button === 3 && noModifiersPressed(e),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Creo: {
|
||||||
|
pan: {
|
||||||
|
description: 'Middle click + Shift + drag',
|
||||||
|
callback: (e) => e.button === 3 && e.shiftKey,
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
description: 'Scroll wheel or Middle click + Ctrl + drag',
|
||||||
|
dragCallback: (e) => e.button === 3 && e.ctrlKey,
|
||||||
|
scrollCallback: () => true,
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
description: 'Middle click + drag',
|
||||||
|
callback: (e) => e.button === 3 && noModifiersPressed(e),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AutoCAD: {
|
||||||
|
pan: {
|
||||||
|
description: 'Middle click + drag',
|
||||||
|
callback: (e) => e.button === 3 && noModifiersPressed(e),
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
description: 'Scroll wheel',
|
||||||
|
dragCallback: () => false,
|
||||||
|
scrollCallback: () => true,
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
description: 'Middle click + Shift + drag',
|
||||||
|
callback: (e) => e.button === 3 && e.shiftKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -56,6 +56,27 @@ export function throttle<T>(
|
|||||||
return throttled
|
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({
|
export function getNormalisedCoordinates({
|
||||||
clientX,
|
clientX,
|
||||||
clientY,
|
clientY,
|
||||||
|
@ -1,29 +1,54 @@
|
|||||||
import { assign, createMachine } from 'xstate'
|
import { assign, createMachine } from 'xstate'
|
||||||
import { BaseUnit, baseUnitsUnion } from '../useStore'
|
|
||||||
import { CommandBarMeta } from '../lib/commands'
|
import { CommandBarMeta } from '../lib/commands'
|
||||||
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
|
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
|
||||||
|
import { CADProgram, cadPrograms } from 'lib/cameraControls'
|
||||||
|
|
||||||
|
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
|
||||||
|
|
||||||
export enum UnitSystem {
|
export enum UnitSystem {
|
||||||
Imperial = 'imperial',
|
Imperial = 'imperial',
|
||||||
Metric = 'metric',
|
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 SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
|
||||||
|
|
||||||
export const settingsCommandBarMeta: CommandBarMeta = {
|
export const settingsCommandBarMeta: CommandBarMeta = {
|
||||||
'Set Theme': {
|
'Set Base Unit': {
|
||||||
displayValue: (args: string[]) => 'Change the app theme',
|
displayValue: (args: string[]) => 'Set your default base unit',
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: 'theme',
|
name: 'baseUnit',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
defaultValue: 'theme',
|
defaultValue: 'baseUnit',
|
||||||
options: Object.values(Themes).map((v) => ({ name: v })) as {
|
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
|
||||||
name: string
|
|
||||||
}[],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
'Set Camera Controls': {
|
||||||
|
displayValue: (args: string[]) => 'Set your camera controls',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'cameraControls',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'cameraControls',
|
||||||
|
options: Object.values(cadPrograms).map((v) => ({ name: v })),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Set Default Directory': {
|
||||||
|
hide: 'both',
|
||||||
|
},
|
||||||
'Set Default Project Name': {
|
'Set Default Project Name': {
|
||||||
displayValue: (args: string[]) => 'Set a new default project name',
|
displayValue: (args: string[]) => 'Set a new default project name',
|
||||||
hide: 'web',
|
hide: 'web',
|
||||||
@ -37,31 +62,9 @@ export const settingsCommandBarMeta: CommandBarMeta = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'Set Default Directory': {
|
'Set Onboarding Status': {
|
||||||
hide: 'both',
|
hide: 'both',
|
||||||
},
|
},
|
||||||
'Set Unit System': {
|
|
||||||
displayValue: (args: string[]) => 'Set your default unit system',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'unitSystem',
|
|
||||||
type: 'select',
|
|
||||||
defaultValue: 'unitSystem',
|
|
||||||
options: [{ name: UnitSystem.Imperial }, { name: UnitSystem.Metric }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Set Base Unit': {
|
|
||||||
displayValue: (args: string[]) => 'Set your default base unit',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'baseUnit',
|
|
||||||
type: 'select',
|
|
||||||
defaultValue: 'baseUnit',
|
|
||||||
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Set Text Wrapping': {
|
'Set Text Wrapping': {
|
||||||
displayValue: (args: string[]) => 'Set whether text in the editor wraps',
|
displayValue: (args: string[]) => 'Set whether text in the editor wraps',
|
||||||
args: [
|
args: [
|
||||||
@ -73,8 +76,29 @@ export const settingsCommandBarMeta: CommandBarMeta = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'Set Onboarding Status': {
|
'Set Theme': {
|
||||||
hide: 'both',
|
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: [
|
||||||
|
{
|
||||||
|
name: 'unitSystem',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'unitSystem',
|
||||||
|
options: [{ name: UnitSystem.Imperial }, { name: UnitSystem.Metric }],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,36 +108,34 @@ export const settingsMachine = createMachine(
|
|||||||
id: 'Settings',
|
id: 'Settings',
|
||||||
predictableActionArguments: true,
|
predictableActionArguments: true,
|
||||||
context: {
|
context: {
|
||||||
theme: Themes.System,
|
|
||||||
defaultProjectName: '',
|
|
||||||
unitSystem: UnitSystem.Imperial,
|
|
||||||
baseUnit: 'in' as BaseUnit,
|
baseUnit: 'in' as BaseUnit,
|
||||||
|
cameraControls: 'KittyCAD' as CADProgram,
|
||||||
defaultDirectory: '',
|
defaultDirectory: '',
|
||||||
textWrapping: 'On' as 'On' | 'Off',
|
defaultProjectName: DEFAULT_PROJECT_NAME,
|
||||||
showDebugPanel: false,
|
|
||||||
onboardingStatus: '',
|
onboardingStatus: '',
|
||||||
|
showDebugPanel: false,
|
||||||
|
textWrapping: 'On' as Toggle,
|
||||||
|
theme: Themes.System,
|
||||||
|
unitSystem: UnitSystem.Imperial,
|
||||||
},
|
},
|
||||||
initial: 'idle',
|
initial: 'idle',
|
||||||
states: {
|
states: {
|
||||||
idle: {
|
idle: {
|
||||||
entry: ['setThemeClass'],
|
entry: ['setThemeClass'],
|
||||||
on: {
|
on: {
|
||||||
'Set Theme': {
|
'Set Base Unit': {
|
||||||
actions: [
|
actions: [
|
||||||
assign({
|
assign({ baseUnit: (_, event) => event.data.baseUnit }),
|
||||||
theme: (_, event) => event.data.theme,
|
|
||||||
}),
|
|
||||||
'persistSettings',
|
'persistSettings',
|
||||||
'toastSuccess',
|
'toastSuccess',
|
||||||
'setThemeClass',
|
|
||||||
],
|
],
|
||||||
target: 'idle',
|
target: 'idle',
|
||||||
internal: true,
|
internal: true,
|
||||||
},
|
},
|
||||||
'Set Default Project Name': {
|
'Set Camera Controls': {
|
||||||
actions: [
|
actions: [
|
||||||
assign({
|
assign({
|
||||||
defaultProjectName: (_, event) => event.data.defaultProjectName,
|
cameraControls: (_, event) => event.data.cameraControls,
|
||||||
}),
|
}),
|
||||||
'persistSettings',
|
'persistSettings',
|
||||||
'toastSuccess',
|
'toastSuccess',
|
||||||
@ -132,12 +154,11 @@ export const settingsMachine = createMachine(
|
|||||||
target: 'idle',
|
target: 'idle',
|
||||||
internal: true,
|
internal: true,
|
||||||
},
|
},
|
||||||
'Set Unit System': {
|
'Set Default Project Name': {
|
||||||
actions: [
|
actions: [
|
||||||
assign({
|
assign({
|
||||||
unitSystem: (_, event) => event.data.unitSystem,
|
defaultProjectName: (_, event) =>
|
||||||
baseUnit: (_, event) =>
|
event.data.defaultProjectName.trim() || DEFAULT_PROJECT_NAME,
|
||||||
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
|
|
||||||
}),
|
}),
|
||||||
'persistSettings',
|
'persistSettings',
|
||||||
'toastSuccess',
|
'toastSuccess',
|
||||||
@ -145,11 +166,12 @@ export const settingsMachine = createMachine(
|
|||||||
target: 'idle',
|
target: 'idle',
|
||||||
internal: true,
|
internal: true,
|
||||||
},
|
},
|
||||||
'Set Base Unit': {
|
'Set Onboarding Status': {
|
||||||
actions: [
|
actions: [
|
||||||
assign({ baseUnit: (_, event) => event.data.baseUnit }),
|
assign({
|
||||||
|
onboardingStatus: (_, event) => event.data.onboardingStatus,
|
||||||
|
}),
|
||||||
'persistSettings',
|
'persistSettings',
|
||||||
'toastSuccess',
|
|
||||||
],
|
],
|
||||||
target: 'idle',
|
target: 'idle',
|
||||||
internal: true,
|
internal: true,
|
||||||
@ -165,6 +187,31 @@ export const settingsMachine = createMachine(
|
|||||||
target: 'idle',
|
target: 'idle',
|
||||||
internal: true,
|
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',
|
||||||
|
],
|
||||||
|
target: 'idle',
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
'Toggle Debug Panel': {
|
'Toggle Debug Panel': {
|
||||||
actions: [
|
actions: [
|
||||||
assign({
|
assign({
|
||||||
@ -178,35 +225,26 @@ export const settingsMachine = createMachine(
|
|||||||
target: 'idle',
|
target: 'idle',
|
||||||
internal: true,
|
internal: true,
|
||||||
},
|
},
|
||||||
'Set Onboarding Status': {
|
|
||||||
actions: [
|
|
||||||
assign({
|
|
||||||
onboardingStatus: (_, event) => event.data.onboardingStatus,
|
|
||||||
}),
|
|
||||||
'persistSettings',
|
|
||||||
],
|
|
||||||
target: 'idle',
|
|
||||||
internal: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
|
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
|
||||||
schema: {
|
schema: {
|
||||||
events: {} as
|
events: {} as
|
||||||
| { type: 'Set Theme'; data: { theme: Themes } }
|
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
|
||||||
|
| { type: 'Set Camera Controls'; data: { cameraControls: CADProgram } }
|
||||||
|
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
|
||||||
| {
|
| {
|
||||||
type: 'Set Default Project Name'
|
type: 'Set Default Project Name'
|
||||||
data: { defaultProjectName: string }
|
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'
|
type: 'Set Unit System'
|
||||||
data: { unitSystem: UnitSystem }
|
data: { unitSystem: UnitSystem }
|
||||||
}
|
}
|
||||||
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
|
|
||||||
| { type: 'Set Text Wrapping'; data: { textWrapping: 'On' | 'Off' } }
|
|
||||||
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
|
|
||||||
| { type: 'Toggle Debug Panel' },
|
| { type: 'Toggle Debug Panel' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -15,6 +15,7 @@ export interface Typegen0 {
|
|||||||
eventsCausingActions: {
|
eventsCausingActions: {
|
||||||
persistSettings:
|
persistSettings:
|
||||||
| 'Set Base Unit'
|
| 'Set Base Unit'
|
||||||
|
| 'Set Camera Controls'
|
||||||
| 'Set Default Directory'
|
| 'Set Default Directory'
|
||||||
| 'Set Default Project Name'
|
| 'Set Default Project Name'
|
||||||
| 'Set Onboarding Status'
|
| 'Set Onboarding Status'
|
||||||
@ -24,6 +25,7 @@ export interface Typegen0 {
|
|||||||
| 'Toggle Debug Panel'
|
| 'Toggle Debug Panel'
|
||||||
setThemeClass:
|
setThemeClass:
|
||||||
| 'Set Base Unit'
|
| 'Set Base Unit'
|
||||||
|
| 'Set Camera Controls'
|
||||||
| 'Set Default Directory'
|
| 'Set Default Directory'
|
||||||
| 'Set Default Project Name'
|
| 'Set Default Project Name'
|
||||||
| 'Set Onboarding Status'
|
| 'Set Onboarding Status'
|
||||||
@ -34,6 +36,7 @@ export interface Typegen0 {
|
|||||||
| 'xstate.init'
|
| 'xstate.init'
|
||||||
toastSuccess:
|
toastSuccess:
|
||||||
| 'Set Base Unit'
|
| 'Set Base Unit'
|
||||||
|
| 'Set Camera Controls'
|
||||||
| 'Set Default Directory'
|
| 'Set Default Directory'
|
||||||
| 'Set Default Project Name'
|
| 'Set Default Project Name'
|
||||||
| 'Set Text Wrapping'
|
| 'Set Text Wrapping'
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine'
|
||||||
|
|
||||||
// This route only opens in the Tauri desktop context for now,
|
// 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.
|
// as defined in Router.tsx, so we can use the Tauri APIs and types.
|
||||||
@ -38,6 +39,7 @@ const Home = () => {
|
|||||||
const {
|
const {
|
||||||
settings: {
|
settings: {
|
||||||
context: { defaultDirectory, defaultProjectName },
|
context: { defaultDirectory, defaultProjectName },
|
||||||
|
send: sendToSettings,
|
||||||
},
|
},
|
||||||
} = useGlobalStateContext()
|
} = useGlobalStateContext()
|
||||||
|
|
||||||
@ -71,16 +73,33 @@ const Home = () => {
|
|||||||
context: ContextFrom<typeof homeMachine>,
|
context: ContextFrom<typeof homeMachine>,
|
||||||
event: EventFrom<typeof homeMachine, 'Create project'>
|
event: EventFrom<typeof homeMachine, 'Create project'>
|
||||||
) => {
|
) => {
|
||||||
let name =
|
let name = (
|
||||||
event.data && 'name' in event.data
|
event.data && 'name' in event.data
|
||||||
? event.data.name
|
? event.data.name
|
||||||
: defaultProjectName
|
: 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)) {
|
if (doesProjectNameNeedInterpolated(name)) {
|
||||||
const nextIndex = await getNextProjectIndex(name, projects)
|
const nextIndex = await getNextProjectIndex(name, projects)
|
||||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
await createNewProject(context.defaultDirectory + '/' + name)
|
await createNewProject(context.defaultDirectory + '/' + name)
|
||||||
|
|
||||||
|
if (shouldUpdateDefaultProjectName) {
|
||||||
|
sendToSettings({
|
||||||
|
type: 'Set Default Project Name',
|
||||||
|
data: { defaultProjectName: DEFAULT_PROJECT_NAME },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return `Successfully created "${name}"`
|
return `Successfully created "${name}"`
|
||||||
},
|
},
|
||||||
renameProject: async (
|
renameProject: async (
|
||||||
|
@ -4,8 +4,8 @@ import { onboardingPaths, useDismiss, useNextClick } from '.'
|
|||||||
import { useStore } from '../../useStore'
|
import { useStore } from '../../useStore'
|
||||||
|
|
||||||
export default function Units() {
|
export default function Units() {
|
||||||
const { isMouseDownInStream } = useStore((s) => ({
|
const { buttonDownInStream } = useStore((s) => ({
|
||||||
isMouseDownInStream: s.isMouseDownInStream,
|
buttonDownInStream: s.buttonDownInStream,
|
||||||
}))
|
}))
|
||||||
const dismiss = useDismiss()
|
const dismiss = useDismiss()
|
||||||
const next = useNextClick(onboardingPaths.SKETCHING)
|
const next = useNextClick(onboardingPaths.SKETCHING)
|
||||||
@ -15,7 +15,7 @@ export default function Units() {
|
|||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'max-w-2xl flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded' +
|
'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>
|
<h1 className="text-2xl font-bold">Camera</h1>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
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 { ActionButton } from '../../components/ActionButton'
|
||||||
import { SettingsSection } from '../Settings'
|
import { SettingsSection } from '../Settings'
|
||||||
import { Toggle } from '../../components/Toggle/Toggle'
|
import { Toggle } from '../../components/Toggle/Toggle'
|
||||||
|
@ -6,13 +6,22 @@ import {
|
|||||||
import { ActionButton } from '../components/ActionButton'
|
import { ActionButton } from '../components/ActionButton'
|
||||||
import { AppHeader } from '../components/AppHeader'
|
import { AppHeader } from '../components/AppHeader'
|
||||||
import { open } from '@tauri-apps/api/dialog'
|
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 { Toggle } from '../components/Toggle/Toggle'
|
||||||
import { useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom'
|
import { useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { IndexLoaderData, paths } from '../Router'
|
import { IndexLoaderData, paths } from '../Router'
|
||||||
import { Themes } from '../lib/theme'
|
import { Themes } from '../lib/theme'
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||||
|
import {
|
||||||
|
CADProgram,
|
||||||
|
cadPrograms,
|
||||||
|
cameraMouseDragGuards,
|
||||||
|
} from 'lib/cameraControls'
|
||||||
import { UnitSystem } from 'machines/settingsMachine'
|
import { UnitSystem } from 'machines/settingsMachine'
|
||||||
|
|
||||||
export const Settings = () => {
|
export const Settings = () => {
|
||||||
@ -25,12 +34,13 @@ export const Settings = () => {
|
|||||||
send,
|
send,
|
||||||
state: {
|
state: {
|
||||||
context: {
|
context: {
|
||||||
|
baseUnit,
|
||||||
|
cameraControls,
|
||||||
|
defaultDirectory,
|
||||||
defaultProjectName,
|
defaultProjectName,
|
||||||
showDebugPanel,
|
showDebugPanel,
|
||||||
defaultDirectory,
|
|
||||||
unitSystem,
|
|
||||||
baseUnit,
|
|
||||||
theme,
|
theme,
|
||||||
|
unitSystem,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -82,6 +92,42 @@ export const Settings = () => {
|
|||||||
, and start a discussion if you don't see it! Your feedback will help
|
, and start a discussion if you don't see it! Your feedback will help
|
||||||
us prioritize what to build next.
|
us prioritize what to build next.
|
||||||
</p>
|
</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 CADProgram },
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cadPrograms.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__ && (
|
{(window as any).__TAURI__ && (
|
||||||
<>
|
<>
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
@ -118,10 +164,14 @@ export const Settings = () => {
|
|||||||
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
||||||
defaultValue={defaultProjectName}
|
defaultValue={defaultProjectName}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
|
const newValue = e.target.value.trim() || DEFAULT_PROJECT_NAME
|
||||||
send({
|
send({
|
||||||
type: 'Set Default Project Name',
|
type: 'Set Default Project Name',
|
||||||
data: { defaultProjectName: e.target.value },
|
data: {
|
||||||
|
defaultProjectName: newValue,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
e.target.value = newValue
|
||||||
}}
|
}}
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
EngineCommandManager,
|
EngineCommandManager,
|
||||||
} from './lang/std/engineConnection'
|
} from './lang/std/engineConnection'
|
||||||
import { KCLError } from './lang/errors'
|
import { KCLError } from './lang/errors'
|
||||||
|
import { defferExecution } from 'lib/utils'
|
||||||
|
|
||||||
export type Selection = {
|
export type Selection = {
|
||||||
type: 'default' | 'line-end' | 'line-mid'
|
type: 'default' | 'line-end' | 'line-mid'
|
||||||
@ -94,15 +95,6 @@ export type GuiModes =
|
|||||||
position: Position
|
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 =
|
export type PaneType =
|
||||||
| 'code'
|
| 'code'
|
||||||
| 'variables'
|
| 'variables'
|
||||||
@ -141,7 +133,9 @@ export interface StoreState {
|
|||||||
) => void
|
) => void
|
||||||
updateAstAsync: (ast: Program, focusPath?: PathToNode) => void
|
updateAstAsync: (ast: Program, focusPath?: PathToNode) => void
|
||||||
code: string
|
code: string
|
||||||
|
defferedCode: string
|
||||||
setCode: (code: string) => void
|
setCode: (code: string) => void
|
||||||
|
defferedSetCode: (code: string) => void
|
||||||
formatCode: () => void
|
formatCode: () => void
|
||||||
errorState: {
|
errorState: {
|
||||||
isError: boolean
|
isError: boolean
|
||||||
@ -166,8 +160,8 @@ export interface StoreState {
|
|||||||
setIsStreamReady: (isStreamReady: boolean) => void
|
setIsStreamReady: (isStreamReady: boolean) => void
|
||||||
isLSPServerReady: boolean
|
isLSPServerReady: boolean
|
||||||
setIsLSPServerReady: (isLSPServerReady: boolean) => void
|
setIsLSPServerReady: (isLSPServerReady: boolean) => void
|
||||||
isMouseDownInStream: boolean
|
buttonDownInStream: number
|
||||||
setIsMouseDownInStream: (isMouseDownInStream: boolean) => void
|
setButtonDownInStream: (buttonDownInStream: number) => void
|
||||||
didDragInStream: boolean
|
didDragInStream: boolean
|
||||||
setDidDragInStream: (didDragInStream: boolean) => void
|
setDidDragInStream: (didDragInStream: boolean) => void
|
||||||
fileId: string
|
fileId: string
|
||||||
@ -177,6 +171,8 @@ export interface StoreState {
|
|||||||
streamWidth: number
|
streamWidth: number
|
||||||
streamHeight: number
|
streamHeight: number
|
||||||
}) => void
|
}) => void
|
||||||
|
isExecuting: boolean
|
||||||
|
setIsExecuting: (isExecuting: boolean) => void
|
||||||
|
|
||||||
showHomeMenu: boolean
|
showHomeMenu: boolean
|
||||||
setHomeShowMenu: (showMenu: boolean) => void
|
setHomeShowMenu: (showMenu: boolean) => void
|
||||||
@ -195,7 +191,12 @@ let pendingAstUpdates: number[] = []
|
|||||||
|
|
||||||
export const useStore = create<StoreState>()(
|
export const useStore = create<StoreState>()(
|
||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => {
|
||||||
|
const setDefferedCode = defferExecution(
|
||||||
|
(code: string) => set({ defferedCode: code }),
|
||||||
|
600
|
||||||
|
)
|
||||||
|
return {
|
||||||
editorView: null,
|
editorView: null,
|
||||||
setEditorView: (editorView) => {
|
setEditorView: (editorView) => {
|
||||||
set({ editorView })
|
set({ editorView })
|
||||||
@ -291,7 +292,10 @@ export const useStore = create<StoreState>()(
|
|||||||
|
|
||||||
set({ ast: astWithUpdatedSource, code: newCode })
|
set({ ast: astWithUpdatedSource, code: newCode })
|
||||||
if (focusPath) {
|
if (focusPath) {
|
||||||
const { node } = getNodeFromPath<any>(astWithUpdatedSource, focusPath)
|
const { node } = getNodeFromPath<any>(
|
||||||
|
astWithUpdatedSource,
|
||||||
|
focusPath
|
||||||
|
)
|
||||||
const { start, end } = node
|
const { start, end } = node
|
||||||
if (!start || !end) return
|
if (!start || !end) return
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -319,8 +323,11 @@ export const useStore = create<StoreState>()(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
code: '',
|
code: '',
|
||||||
setCode: (code) => {
|
defferedCode: '',
|
||||||
|
setCode: (code) => set({ code, defferedCode: code }),
|
||||||
|
defferedSetCode: (code) => {
|
||||||
set({ code })
|
set({ code })
|
||||||
|
setDefferedCode(code)
|
||||||
},
|
},
|
||||||
formatCode: async () => {
|
formatCode: async () => {
|
||||||
const code = get().code
|
const code = get().code
|
||||||
@ -349,9 +356,9 @@ export const useStore = create<StoreState>()(
|
|||||||
setIsStreamReady: (isStreamReady) => set({ isStreamReady }),
|
setIsStreamReady: (isStreamReady) => set({ isStreamReady }),
|
||||||
isLSPServerReady: false,
|
isLSPServerReady: false,
|
||||||
setIsLSPServerReady: (isLSPServerReady) => set({ isLSPServerReady }),
|
setIsLSPServerReady: (isLSPServerReady) => set({ isLSPServerReady }),
|
||||||
isMouseDownInStream: false,
|
buttonDownInStream: 0,
|
||||||
setIsMouseDownInStream: (isMouseDownInStream) => {
|
setButtonDownInStream: (buttonDownInStream) => {
|
||||||
set({ isMouseDownInStream })
|
set({ buttonDownInStream })
|
||||||
},
|
},
|
||||||
didDragInStream: false,
|
didDragInStream: false,
|
||||||
setDidDragInStream: (didDragInStream) => {
|
setDidDragInStream: (didDragInStream) => {
|
||||||
@ -362,6 +369,8 @@ export const useStore = create<StoreState>()(
|
|||||||
setFileId: (fileId) => set({ fileId }),
|
setFileId: (fileId) => set({ fileId }),
|
||||||
streamDimensions: { streamWidth: 1280, streamHeight: 720 },
|
streamDimensions: { streamWidth: 1280, streamHeight: 720 },
|
||||||
setStreamDimensions: (streamDimensions) => set({ streamDimensions }),
|
setStreamDimensions: (streamDimensions) => set({ streamDimensions }),
|
||||||
|
isExecuting: false,
|
||||||
|
setIsExecuting: (isExecuting) => set({ isExecuting }),
|
||||||
|
|
||||||
// tauri specific app settings
|
// tauri specific app settings
|
||||||
defaultDir: {
|
defaultDir: {
|
||||||
@ -375,13 +384,14 @@ export const useStore = create<StoreState>()(
|
|||||||
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
|
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
|
||||||
homeMenuItems: [],
|
homeMenuItems: [],
|
||||||
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
|
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
|
||||||
}),
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'store',
|
name: 'store',
|
||||||
partialize: (state) =>
|
partialize: (state) =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
Object.entries(state).filter(([key]) =>
|
Object.entries(state).filter(([key]) =>
|
||||||
['code', 'openPanes'].includes(key)
|
['code', 'defferedCode', 'openPanes'].includes(key)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
6
src/wasm-lib/Cargo.lock
generated
6
src/wasm-lib/Cargo.lock
generated
@ -1094,7 +1094,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-lib"
|
name = "kcl-lib"
|
||||||
version = "0.1.24"
|
version = "0.1.26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bson",
|
"bson",
|
||||||
@ -1126,9 +1126,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kittycad"
|
name = "kittycad"
|
||||||
version = "0.2.23"
|
version = "0.2.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8b33e5df8f82b97e5f5af94ff1400ae37449d0f5f1bb79acedf17cf2193680f"
|
checksum = "d9cf962b1e81a0b4eb923a727e761b40672cbacc7f5f0b75e13579d346352bc7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.21.2",
|
"base64 0.21.2",
|
||||||
|
@ -11,7 +11,7 @@ crate-type = ["cdylib"]
|
|||||||
bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
|
bson = { version = "2.7.0", features = ["uuid-1", "chrono"] }
|
||||||
gloo-utils = "0.2.0"
|
gloo-utils = "0.2.0"
|
||||||
kcl-lib = { path = "kcl" }
|
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"
|
serde_json = "1.0.93"
|
||||||
wasm-bindgen = "0.2.87"
|
wasm-bindgen = "0.2.87"
|
||||||
wasm-bindgen-futures = "0.4.37"
|
wasm-bindgen-futures = "0.4.37"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-lib"
|
name = "kcl-lib"
|
||||||
description = "KittyCAD Language"
|
description = "KittyCAD Language"
|
||||||
version = "0.1.24"
|
version = "0.1.26"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ clap = { version = "4.4.2", features = ["cargo", "derive", "env", "unicode"] }
|
|||||||
dashmap = "5.5.3"
|
dashmap = "5.5.3"
|
||||||
derive-docs = { version = "0.1.3" }
|
derive-docs = { version = "0.1.3" }
|
||||||
#derive-docs = { path = "../derive-docs" }
|
#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"
|
lazy_static = "1.4.0"
|
||||||
parse-display = "0.8.2"
|
parse-display = "0.8.2"
|
||||||
regex = "1.7.1"
|
regex = "1.7.1"
|
||||||
|
@ -87,6 +87,9 @@ impl EngineConnection {
|
|||||||
|
|
||||||
if let Some(msg) = ws_resp.resp {
|
if let Some(msg) = ws_resp.resp {
|
||||||
match msg {
|
match msg {
|
||||||
|
OkWebSocketResponseData::MetricsRequest {} => {
|
||||||
|
// @paultag todo
|
||||||
|
}
|
||||||
OkWebSocketResponseData::IceServerInfo { ice_servers } => {
|
OkWebSocketResponseData::IceServerInfo { ice_servers } => {
|
||||||
println!("got ice server info: {:?}", ice_servers);
|
println!("got ice server info: {:?}", ice_servers);
|
||||||
}
|
}
|
||||||
|
@ -1530,10 +1530,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
|
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
|
||||||
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
||||||
|
|
||||||
"@kittycad/lib@^0.0.36":
|
"@kittycad/lib@^0.0.37":
|
||||||
version "0.0.36"
|
version "0.0.37"
|
||||||
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.36.tgz#7b9676c975bc629f227d41897b38e7d73280db71"
|
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.37.tgz#ec4f6c4fb5d06402a19339f3374036b6582d2265"
|
||||||
integrity sha512-4bVXTaIzpSRuJAuLbAD/CWWTns7H/IxogPj0827n8mwXDkj+65EBCNXhJGWRkMG2CeTVJVk1LSWKlaHE+ToxGA==
|
integrity sha512-P8p9FeLV79/0Lfd0RioBta1drzhmpROnU4YV38+zsAA4LhibQCTjeekRkxVvHztGumPxz9pPsAeeLJyuz2RWKQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
node-fetch "3.3.2"
|
node-fetch "3.3.2"
|
||||||
openapi-types "^12.0.0"
|
openapi-types "^12.0.0"
|
||||||
|
Reference in New Issue
Block a user