Compare commits
14 Commits
v0.10.0
...
achalmers/
Author | SHA1 | Date | |
---|---|---|---|
f3cb24cad7 | |||
4631b1e74d | |||
06d0fa1da5 | |||
6427b9da48 | |||
6c15a743a2 | |||
d0930477ad | |||
e5e30d231b | |||
9822576077 | |||
629f326f4c | |||
89b880d9ae | |||
f6de0de1bf | |||
65ebb86b67 | |||
cce8274902 | |||
c515bef8e4 |
@ -7,3 +7,6 @@ coverage
|
||||
target
|
||||
src/wasm-lib/pkg
|
||||
src/wasm-lib/kcl/bindings
|
||||
|
||||
# XState generated files
|
||||
src/machines/modelingMachine.typegen.ts
|
||||
|
@ -88,6 +88,13 @@ yarn test
|
||||
|
||||
Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro/) tests, in interactive mode by default.
|
||||
|
||||
For running the rust (not tauri rust though) only, you can
|
||||
```bash
|
||||
cd src/wasm-lib
|
||||
cargo test
|
||||
```
|
||||
but you will need to have install ffmpeg prior to.
|
||||
|
||||
## Tauri
|
||||
|
||||
To spin up up tauri dev, `yarn install` and `yarn build:wasm` need to have been done before hand then
|
||||
|
@ -25,6 +25,7 @@
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@uiw/react-codemirror": "^4.21.13",
|
||||
"@xstate/inspect": "^0.8.0",
|
||||
"@xstate/react": "^3.2.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"debounce-promise": "^3.1.2",
|
||||
|
6
src-tauri/Cargo.lock
generated
6
src-tauri/Cargo.lock
generated
@ -1658,9 +1658,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.2.28"
|
||||
version = "0.2.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35b2f9302648dbb06fd7121687f9505fc3179eba84111a06d76b246e3158f5dc"
|
||||
checksum = "539b323537b877fc8dd130362b8f1af9af8051c19208bb8bfd816ab7c330f2bb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -3828,7 +3828,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs-extra"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#fa32d1afa97f52f74d814c5619b8d95da3268e3e"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#9f27e6e4415ddf6c40f846d50c0d95c768cded77"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
|
@ -16,7 +16,7 @@ tauri-build = { version = "1.5.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
kittycad = "0.2.28"
|
||||
kittycad = "0.2.31"
|
||||
oauth2 = "4.4.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
} from 'react-router-dom'
|
||||
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||
import CommandBarProvider from 'components/CommandBar'
|
||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||
import { BROWSER_FILE_NAME } from 'Router'
|
||||
|
||||
let listener: ((rect: any) => void) | undefined = undefined
|
||||
@ -56,7 +57,9 @@ function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
path="/file/:id"
|
||||
element={
|
||||
<CommandBarProvider>
|
||||
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||
<GlobalStateProvider>
|
||||
<ModelingMachineProvider>{children}</ModelingMachineProvider>
|
||||
</GlobalStateProvider>
|
||||
</CommandBarProvider>
|
||||
}
|
||||
/>
|
||||
|
88
src/App.tsx
88
src/App.tsx
@ -1,4 +1,4 @@
|
||||
import { useRef, useEffect, useCallback, MouseEventHandler } from 'react'
|
||||
import { useEffect, useCallback, MouseEventHandler } from 'react'
|
||||
import { DebugPanel } from './components/DebugPanel'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { PaneType, useStore } from './useStore'
|
||||
@ -29,45 +29,33 @@ import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/mod
|
||||
import { CodeMenu } from 'components/CodeMenu'
|
||||
import { TextEditor } from 'components/TextEditor'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||
import { engineCommandManager } from './lang/std/engineConnection'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
|
||||
export function App() {
|
||||
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
||||
|
||||
const streamRef = useRef<HTMLDivElement>(null)
|
||||
useHotKeyListener()
|
||||
const {
|
||||
setCode,
|
||||
buttonDownInStream,
|
||||
openPanes,
|
||||
setOpenPanes,
|
||||
didDragInStream,
|
||||
streamDimensions,
|
||||
guiMode,
|
||||
setGuiMode,
|
||||
executeAst,
|
||||
} = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
setGuiMode: s.setGuiMode,
|
||||
setCode: s.setCode,
|
||||
buttonDownInStream: s.buttonDownInStream,
|
||||
openPanes: s.openPanes,
|
||||
setOpenPanes: s.setOpenPanes,
|
||||
didDragInStream: s.didDragInStream,
|
||||
streamDimensions: s.streamDimensions,
|
||||
executeAst: s.executeAst,
|
||||
}))
|
||||
|
||||
const {
|
||||
auth: {
|
||||
context: { token },
|
||||
},
|
||||
settings: {
|
||||
context: { showDebugPanel, onboardingStatus, cameraControls, theme },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
const { settings } = useGlobalStateContext()
|
||||
const { showDebugPanel, onboardingStatus, cameraControls, theme } =
|
||||
settings?.context || {}
|
||||
const { state, send } = useModelingContext()
|
||||
|
||||
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||
|
||||
@ -84,50 +72,7 @@ export function App() {
|
||||
useHotkeys('shift + l', () => togglePane('logs'))
|
||||
useHotkeys('shift + e', () => togglePane('kclErrors'))
|
||||
useHotkeys('shift + d', () => togglePane('debug'))
|
||||
useHotkeys('esc', () => {
|
||||
if (guiMode.mode === 'sketch') {
|
||||
if (guiMode.sketchMode === 'selectFace') return
|
||||
if (guiMode.sketchMode === 'sketchEdit') {
|
||||
// TODO: share this with Toolbar's "Exit sketch" button
|
||||
// exiting sketch should be done consistently across all exits
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'edit_mode_exit' },
|
||||
})
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||
})
|
||||
setGuiMode({ mode: 'default' })
|
||||
// this is necessary to get the UI back into a consistent
|
||||
// state right now, hopefully won't need to rerender
|
||||
// when exiting sketch mode in the future
|
||||
executeAst()
|
||||
} else {
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_tool',
|
||||
tool: 'select',
|
||||
},
|
||||
})
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
rotation: guiMode.rotation,
|
||||
position: guiMode.position,
|
||||
pathToNode: guiMode.pathToNode,
|
||||
pathId: guiMode.pathId,
|
||||
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
||||
})
|
||||
}
|
||||
} else {
|
||||
setGuiMode({ mode: 'default' })
|
||||
}
|
||||
})
|
||||
useHotkeys('esc', () => send('Cancel'))
|
||||
|
||||
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
|
||||
(p) => p === onboardingStatus
|
||||
@ -141,17 +86,16 @@ export function App() {
|
||||
// on mount, and overwrite any locally-stored code
|
||||
useEffect(() => {
|
||||
if (isTauri() && loadedCode !== null) {
|
||||
setCode(loadedCode)
|
||||
kclManager.setCode(loadedCode)
|
||||
}
|
||||
return () => {
|
||||
// Clear code on unmount if in desktop app
|
||||
if (isTauri()) {
|
||||
setCode('')
|
||||
kclManager.setCode('')
|
||||
}
|
||||
}
|
||||
}, [loadedCode, setCode])
|
||||
}, [loadedCode])
|
||||
|
||||
useSetupEngineManager(streamRef, token)
|
||||
useEngineConnectionSubscriptions()
|
||||
|
||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||
@ -169,10 +113,7 @@ export function App() {
|
||||
|
||||
const newCmdId = uuidv4()
|
||||
if (buttonDownInStream === undefined) {
|
||||
if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any)
|
||||
) {
|
||||
if (state.matches('Sketch.Line Tool')) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: newCmdId,
|
||||
@ -192,7 +133,7 @@ export function App() {
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||
if (state.matches('Sketch.Move Tool')) {
|
||||
debounceSocketSend({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: newCmdId,
|
||||
@ -232,9 +173,8 @@ export function App() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-screen overflow-hidden relative flex flex-col cursor-pointer select-none"
|
||||
className="relative h-full flex flex-col"
|
||||
onMouseMove={handleMouseMove}
|
||||
ref={streamRef}
|
||||
>
|
||||
<AppHeader
|
||||
className={
|
||||
|
@ -3,10 +3,8 @@ import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
|
||||
// Wrapper around protected routes, used in src/Router.tsx
|
||||
export const Auth = ({ children }: React.PropsWithChildren) => {
|
||||
const {
|
||||
auth: { state },
|
||||
} = useGlobalStateContext()
|
||||
const isLoggingIn = state.matches('checkIfLoggedIn')
|
||||
const { auth } = useGlobalStateContext()
|
||||
const isLoggingIn = auth?.state.matches('checkIfLoggedIn')
|
||||
|
||||
return isLoggingIn ? (
|
||||
<Loading>Loading KittyCAD Modeling App...</Loading>
|
||||
|
@ -40,6 +40,8 @@ import { ContextFrom } from 'xstate'
|
||||
import CommandBarProvider from 'components/CommandBar'
|
||||
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||
import { KclContextProvider } from 'lang/KclSinglton'
|
||||
|
||||
if (VITE_KC_SENTRY_DSN && !TEST) {
|
||||
Sentry.init({
|
||||
@ -141,7 +143,11 @@ const router = createBrowserRouter(
|
||||
element: (
|
||||
<Auth>
|
||||
<Outlet />
|
||||
<App />
|
||||
<KclContextProvider>
|
||||
<ModelingMachineProvider>
|
||||
<App />
|
||||
</ModelingMachineProvider>
|
||||
</KclContextProvider>
|
||||
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
||||
</Auth>
|
||||
),
|
||||
|
306
src/Toolbar.tsx
306
src/Toolbar.tsx
@ -1,24 +1,26 @@
|
||||
import { useStore, toolTips, ToolTip } from './useStore'
|
||||
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
|
||||
import { getNodePathFromSourceRange } from './lang/queryAst'
|
||||
import { HorzVert } from './components/Toolbar/HorzVert'
|
||||
import { RemoveConstrainingValues } from './components/Toolbar/RemoveConstrainingValues'
|
||||
import { EqualLength } from './components/Toolbar/EqualLength'
|
||||
import { EqualAngle } from './components/Toolbar/EqualAngle'
|
||||
import { Intersect } from './components/Toolbar/Intersect'
|
||||
import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
|
||||
import { SetAngleLength } from './components/Toolbar/setAngleLength'
|
||||
import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
||||
import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
||||
import { Fragment, WheelEvent, useRef } from 'react'
|
||||
// import { HorzVert } from './components/Toolbar/HorzVert'
|
||||
// import { RemoveConstrainingValues } from './components/Toolbar/RemoveConstrainingValues'
|
||||
// import { EqualLength } from './components/Toolbar/EqualLength'
|
||||
// import { EqualAngle } from './components/Toolbar/EqualAngle'
|
||||
// import { Intersect } from './components/Toolbar/Intersect'
|
||||
// import { SetHorzVertDistance } from './components/Toolbar/SetHorzVertDistance'
|
||||
// import { SetAngleLength } from './components/Toolbar/setAngleLength'
|
||||
// import { SetAbsDistance } from './components/Toolbar/SetAbsDistance'
|
||||
// import { SetAngleBetween } from './components/Toolbar/SetAngleBetween'
|
||||
import { Fragment, WheelEvent, useRef, useMemo } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSearch, faX } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import styles from './Toolbar.module.css'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useAppMode } from 'hooks/useAppMode'
|
||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import { engineCommandManager } from './lang/std/engineConnection'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
export const sketchButtonClassnames = {
|
||||
background:
|
||||
@ -44,25 +46,16 @@ const sketchFnLabels: Record<ToolTip | 'sketch_line' | 'move', string> = {
|
||||
}
|
||||
|
||||
export const Toolbar = () => {
|
||||
const {
|
||||
setGuiMode,
|
||||
guiMode,
|
||||
selectionRanges,
|
||||
ast,
|
||||
updateAst,
|
||||
programMemory,
|
||||
executeAst,
|
||||
} = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
setGuiMode: s.setGuiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
programMemory: s.programMemory,
|
||||
executeAst: s.executeAst,
|
||||
}))
|
||||
useAppMode()
|
||||
const { state, send, context } = useModelingContext()
|
||||
const toolbarButtonsRef = useRef<HTMLSpanElement>(null)
|
||||
const pathId = useMemo(
|
||||
() =>
|
||||
isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
context.selectionRanges
|
||||
),
|
||||
[engineCommandManager.artifactMap, context.selectionRanges]
|
||||
)
|
||||
|
||||
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
|
||||
const span = toolbarButtonsRef.current
|
||||
@ -80,194 +73,113 @@ export const Toolbar = () => {
|
||||
onWheel={handleToolbarButtonsWheelEvent}
|
||||
className={styles.toolbarButtons + ' ' + className}
|
||||
>
|
||||
{guiMode.mode === 'default' && (
|
||||
{state.nextEvents.includes('Enter sketch') && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'selectFace',
|
||||
})
|
||||
}}
|
||||
onClick={() => send({ type: 'Enter sketch' })}
|
||||
className="group"
|
||||
>
|
||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||
Start Sketch
|
||||
</button>
|
||||
)}
|
||||
{guiMode.mode === 'canEditExtrude' && (
|
||||
{state.nextEvents.includes('Enter sketch') && pathId && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!ast) return
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
const { modifiedAst } = sketchOnExtrudedFace(
|
||||
ast,
|
||||
pathToNode,
|
||||
programMemory
|
||||
)
|
||||
updateAst(modifiedAst, true)
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||
Sketch on Face
|
||||
</button>
|
||||
)}
|
||||
{guiMode.mode === 'canEditSketch' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'enterSketchEdit',
|
||||
pathToNode: pathToNode,
|
||||
rotation: [0, 0, 0, 1],
|
||||
position: [0, 0, 0],
|
||||
pathId: guiMode.pathId,
|
||||
})
|
||||
}}
|
||||
onClick={() => send({ type: 'Enter sketch' })}
|
||||
className="group"
|
||||
>
|
||||
<ActionIcon icon="sketch" className="!p-0.5" size="md" />
|
||||
Edit Sketch
|
||||
</button>
|
||||
)}
|
||||
{guiMode.mode === 'canEditSketch' && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!ast) return
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||
ast,
|
||||
pathToNode
|
||||
)
|
||||
updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
<ActionIcon icon="extrude" className="!p-0.5" size="md" />
|
||||
Extrude
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!ast) return
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||
ast,
|
||||
pathToNode,
|
||||
false
|
||||
)
|
||||
updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
<ActionIcon icon="extrude" className="!p-0.5" size="md" />
|
||||
Extrude as new
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{guiMode.mode === 'sketch' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'edit_mode_exit' },
|
||||
})
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||
})
|
||||
|
||||
setGuiMode({ mode: 'default' })
|
||||
executeAst()
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
<ActionIcon
|
||||
icon="exit"
|
||||
className="!p-0.5"
|
||||
bgClassName={sketchButtonClassnames.background}
|
||||
iconClassName={sketchButtonClassnames.icon}
|
||||
size="md"
|
||||
/>
|
||||
Exit sketch
|
||||
{state.nextEvents.includes('Cancel') && !state.matches('idle') && (
|
||||
<button onClick={() => send({ type: 'Cancel' })} className="group">
|
||||
<ActionIcon icon="exit" className="!p-0.5" size="md" />
|
||||
Exit Sketch
|
||||
</button>
|
||||
)}
|
||||
{toolTips
|
||||
.filter(
|
||||
// (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName)
|
||||
(sketchFnName) => ['sketch_line', 'move'].includes(sketchFnName)
|
||||
)
|
||||
.map((sketchFnName) => {
|
||||
if (
|
||||
guiMode.mode !== 'sketch' ||
|
||||
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
|
||||
{state.matches('Sketch') && !state.matches('idle') && (
|
||||
<button
|
||||
onClick={() =>
|
||||
state.matches('Sketch.Line Tool')
|
||||
? send('CancelSketch')
|
||||
: send('Equip tool')
|
||||
}
|
||||
className={
|
||||
'group ' +
|
||||
(state.matches('Sketch.Line Tool')
|
||||
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
<ActionIcon icon="line" className="!p-0.5" size="md" />
|
||||
Line
|
||||
</button>
|
||||
)}
|
||||
{state.matches('Sketch') && (
|
||||
<button
|
||||
onClick={() =>
|
||||
state.matches('Sketch.Move Tool')
|
||||
? send('CancelSketch')
|
||||
: send('Equip move tool')
|
||||
}
|
||||
className={
|
||||
'group ' +
|
||||
(state.matches('Sketch.Move Tool')
|
||||
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
|
||||
: '')
|
||||
}
|
||||
>
|
||||
<ActionIcon icon="move" className="!p-0.5" size="md" />
|
||||
Move
|
||||
</button>
|
||||
)}
|
||||
{state.matches('Sketch.SketchIdle') &&
|
||||
state.nextEvents
|
||||
.filter(
|
||||
(eventName) =>
|
||||
eventName.includes('Make segment') ||
|
||||
eventName.includes('Constrain')
|
||||
)
|
||||
return null
|
||||
return (
|
||||
.map((eventName) => (
|
||||
<button
|
||||
key={sketchFnName}
|
||||
onClick={() => {
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_tool',
|
||||
tool:
|
||||
guiMode.sketchMode === sketchFnName
|
||||
? 'select'
|
||||
: (sketchFnName as any),
|
||||
},
|
||||
})
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
...(guiMode.sketchMode === sketchFnName
|
||||
? {
|
||||
sketchMode: 'sketchEdit',
|
||||
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
|
||||
}
|
||||
: {
|
||||
sketchMode: sketchFnName,
|
||||
waitingFirstClick: true,
|
||||
isTooltip: true,
|
||||
pathId: guiMode.pathId,
|
||||
}),
|
||||
})
|
||||
}}
|
||||
className={
|
||||
'group ' +
|
||||
(guiMode.sketchMode === sketchFnName
|
||||
? '!text-fern-70 !bg-fern-10 !dark:text-fern-20 !border-fern-50'
|
||||
: '')
|
||||
key={eventName}
|
||||
onClick={() => send(eventName)}
|
||||
className="group"
|
||||
disabled={
|
||||
!state.nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(eventName)
|
||||
}
|
||||
title={eventName}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={sketchFnName.includes('line') ? 'line' : 'move'}
|
||||
className="!p-0.5"
|
||||
icon={'line'} // TODO
|
||||
bgClassName={sketchButtonClassnames.background}
|
||||
iconClassName={sketchButtonClassnames.icon}
|
||||
size="md"
|
||||
/>
|
||||
{sketchFnLabels[sketchFnName]}
|
||||
{eventName
|
||||
.replace('Make segment ', '')
|
||||
.replace('Constrain ', '')}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<HorzVert horOrVert="horizontal" />
|
||||
))}
|
||||
{state.matches('idle') && (
|
||||
<button
|
||||
onClick={() => send('extrude intent')}
|
||||
disabled={!state.can('extrude intent')}
|
||||
className="group"
|
||||
title={
|
||||
state.can('extrude intent')
|
||||
? 'extrude'
|
||||
: 'sketches need to be closed, or not already extruded'
|
||||
}
|
||||
>
|
||||
<ActionIcon icon="extrude" className="!p-0.5" size="md" />
|
||||
Extrude
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* <HorzVert horOrVert="horizontal" />
|
||||
<HorzVert horOrVert="vertical" />
|
||||
<EqualLength />
|
||||
<EqualAngle />
|
||||
@ -283,16 +195,20 @@ export const Toolbar = () => {
|
||||
<SetAngleLength angleOrLength="setLength" />
|
||||
<Intersect />
|
||||
<RemoveConstrainingValues />
|
||||
<SetAngleBetween />
|
||||
<SetAngleBetween /> */}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover className={styles.toolbarWrapper + ' ' + guiMode.mode}>
|
||||
<Popover
|
||||
className={
|
||||
styles.toolbarWrapper + state.matches('Sketch') ? ' sketch' : ''
|
||||
}
|
||||
>
|
||||
<div className={styles.toolbar}>
|
||||
<span className={styles.toolbarCap + ' ' + styles.label}>
|
||||
{guiMode.mode === 'sketch' ? '2D' : '3D'}
|
||||
{state.matches('Sketch') ? '2D' : '3D'}
|
||||
</span>
|
||||
<menu className="flex-1 gap-2 py-0.5 overflow-hidden whitespace-nowrap">
|
||||
<ToolbarButtons />
|
||||
@ -328,7 +244,7 @@ export const Toolbar = () => {
|
||||
<p
|
||||
className={`${styles.toolbarCap} ${styles.label} !self-center rounded-r-full w-fit`}
|
||||
>
|
||||
You're in {guiMode.mode === 'sketch' ? '2D' : '3D'}
|
||||
You're in {state.matches('Sketch') ? '2D' : '3D'}
|
||||
</p>
|
||||
<Popover.Button className="p-2 flex items-center justify-center rounded-sm bg-chalkboard-20 text-chalkboard-110 dark:bg-chalkboard-70 dark:text-chalkboard-20 border-none hover:bg-chalkboard-30 dark:hover:bg-chalkboard-60">
|
||||
<FontAwesomeIcon icon={faX} className="w-4 h-4" />
|
||||
|
@ -20,11 +20,8 @@ export const AppHeader = ({
|
||||
className = '',
|
||||
enableMenu = false,
|
||||
}: AppHeaderProps) => {
|
||||
const {
|
||||
auth: {
|
||||
context: { user },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
const { auth } = useGlobalStateContext()
|
||||
const user = auth?.context?.user
|
||||
|
||||
return (
|
||||
<header
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useStore } from 'useStore'
|
||||
|
||||
export function AstExplorer() {
|
||||
const { ast, setHighlightRange, selectionRanges } = useStore((s) => ({
|
||||
ast: s.ast,
|
||||
setHighlightRange: s.setHighlightRange,
|
||||
selectionRanges: s.selectionRanges,
|
||||
}))
|
||||
const setHighlightRange = useStore((s) => s.setHighlightRange)
|
||||
const { context } = useModelingContext()
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections?.[0]?.range
|
||||
// TODO maybe need to have callback to make sure it stays in sync
|
||||
kclManager.ast,
|
||||
context.selectionRanges.codeBasedSelections?.[0]?.range
|
||||
)
|
||||
const node = getNodeFromPath(ast, pathToNode).node
|
||||
const node = getNodeFromPath(kclManager.ast, pathToNode).node
|
||||
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
|
||||
|
||||
return (
|
||||
@ -46,7 +46,11 @@ export function AstExplorer() {
|
||||
}}
|
||||
>
|
||||
<pre className=" text-xs overflow-y-auto" style={{ width: '300px' }}>
|
||||
<DisplayObj obj={ast} filterKeys={filterKeys} node={node} />
|
||||
<DisplayObj
|
||||
obj={kclManager.ast}
|
||||
filterKeys={filterKeys}
|
||||
node={node}
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
@ -84,10 +88,8 @@ function DisplayObj({
|
||||
filterKeys: string[]
|
||||
node: any
|
||||
}) {
|
||||
const { setHighlightRange, setCursor2 } = useStore((s) => ({
|
||||
setHighlightRange: s.setHighlightRange,
|
||||
setCursor2: s.setCursor2,
|
||||
}))
|
||||
const setHighlightRange = useStore((s) => s.setHighlightRange)
|
||||
const { send } = useModelingContext()
|
||||
const ref = useRef<HTMLPreElement>(null)
|
||||
const [hasCursor, setHasCursor] = useState(false)
|
||||
const [isCollapsed, setIsCollapsed] = useState(false)
|
||||
@ -118,7 +120,16 @@ function DisplayObj({
|
||||
setHighlightRange([obj?.start || 0, obj.end])
|
||||
}}
|
||||
onClick={(e) => {
|
||||
setCursor2({ type: 'default', range: [obj?.start || 0, obj.end || 0] })
|
||||
send({
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'singleCodeCursor',
|
||||
selection: {
|
||||
type: 'default',
|
||||
range: [obj?.start || 0, obj.end || 0],
|
||||
},
|
||||
},
|
||||
})
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
|
@ -7,8 +7,9 @@ import {
|
||||
findUniqueName,
|
||||
} from '../lang/modifyAst'
|
||||
import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
|
||||
import { useStore } from '../useStore'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
|
||||
export const AvailableVars = ({
|
||||
onVarClick,
|
||||
@ -91,14 +92,9 @@ export function useCalc({
|
||||
newVariableInsertIndex: number
|
||||
setNewVariableName: (a: string) => void
|
||||
} {
|
||||
const { ast, programMemory, selectionRange, defaultPlanes } = useStore(
|
||||
(s) => ({
|
||||
ast: s.ast,
|
||||
programMemory: s.programMemory,
|
||||
selectionRange: s.selectionRanges.codeBasedSelections[0].range,
|
||||
defaultPlanes: s.defaultPlanes,
|
||||
})
|
||||
)
|
||||
const { programMemory } = useKclContext()
|
||||
const { context } = useModelingContext()
|
||||
const selectionRange = context.selectionRanges.codeBasedSelections[0].range
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [availableVarInfo, setAvailableVarInfo] = useState<
|
||||
ReturnType<typeof findAllPreviousVariables>
|
||||
@ -118,9 +114,7 @@ export function useCalc({
|
||||
inputRef.current &&
|
||||
inputRef.current.setSelectionRange(0, String(value).length)
|
||||
}, 100)
|
||||
if (ast) {
|
||||
setNewVariableName(findUniqueName(ast, valueName))
|
||||
}
|
||||
setNewVariableName(findUniqueName(kclManager.ast, valueName))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@ -133,10 +127,14 @@ export function useCalc({
|
||||
}, [newVariableName])
|
||||
|
||||
useEffect(() => {
|
||||
if (!ast || !programMemory || !selectionRange) return
|
||||
const varInfo = findAllPreviousVariables(ast, programMemory, selectionRange)
|
||||
if (!programMemory || !selectionRange) return
|
||||
const varInfo = findAllPreviousVariables(
|
||||
kclManager.ast,
|
||||
programMemory,
|
||||
selectionRange
|
||||
)
|
||||
setAvailableVarInfo(varInfo)
|
||||
}, [ast, programMemory, selectionRange])
|
||||
}, [kclManager.ast, programMemory, selectionRange])
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
@ -146,22 +144,25 @@ export function useCalc({
|
||||
availableVarInfo.variables.forEach(({ key, value }) => {
|
||||
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
||||
})
|
||||
if (!defaultPlanes) return
|
||||
executor(ast, _programMem, engineCommandManager, defaultPlanes!).then(
|
||||
(programMemory) => {
|
||||
const resultDeclaration = ast.body.find(
|
||||
(a) =>
|
||||
a.type === 'VariableDeclaration' &&
|
||||
a.declarations?.[0]?.id?.name === '__result__'
|
||||
)
|
||||
const init =
|
||||
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||
resultDeclaration?.declarations?.[0]?.init
|
||||
const result = programMemory?.root?.__result__?.value
|
||||
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||
init && setValueNode(init)
|
||||
}
|
||||
)
|
||||
|
||||
executor(
|
||||
ast,
|
||||
_programMem,
|
||||
engineCommandManager,
|
||||
kclManager.defaultPlanes
|
||||
).then((programMemory) => {
|
||||
const resultDeclaration = ast.body.find(
|
||||
(a) =>
|
||||
a.type === 'VariableDeclaration' &&
|
||||
a.declarations?.[0]?.id?.name === '__result__'
|
||||
)
|
||||
const init =
|
||||
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||
resultDeclaration?.declarations?.[0]?.init
|
||||
const result = programMemory?.root?.__result__?.value
|
||||
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||
init && setValueNode(init)
|
||||
})
|
||||
} catch (e) {
|
||||
setCalcResult('NAN')
|
||||
setValueNode(null)
|
||||
|
@ -5,16 +5,13 @@ 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'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||
const { formatCode } = useStore((s) => ({
|
||||
formatCode: s.formatCode,
|
||||
}))
|
||||
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
|
||||
useConvertToVariable()
|
||||
|
||||
@ -41,7 +38,10 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
|
||||
</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}>
|
||||
<button
|
||||
onClick={() => kclManager.format()}
|
||||
className={styles.button}
|
||||
>
|
||||
<span>Format code</span>
|
||||
<small>{editorShortcutMeta.formatCode.display}</small>
|
||||
</button>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import ReactJson from 'react-json-view'
|
||||
import { useEffect } from 'react'
|
||||
import { useStore } from '../useStore'
|
||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { Themes } from '../lib/theme'
|
||||
import { useKclContext } from 'lang/KclSinglton'
|
||||
|
||||
const ReactJsonTypeHack = ReactJson as any
|
||||
|
||||
@ -11,9 +11,7 @@ interface LogPanelProps extends CollapsiblePanelProps {
|
||||
}
|
||||
|
||||
export const Logs = ({ theme = Themes.Light, ...props }: LogPanelProps) => {
|
||||
const { logs } = useStore(({ logs }) => ({
|
||||
logs,
|
||||
}))
|
||||
const { logs } = useKclContext()
|
||||
useEffect(() => {
|
||||
const element = document.querySelector('.console-tile')
|
||||
if (element) {
|
||||
@ -47,21 +45,19 @@ export const KCLErrors = ({
|
||||
theme = Themes.Light,
|
||||
...props
|
||||
}: LogPanelProps) => {
|
||||
const { kclErrors } = useStore(({ kclErrors }) => ({
|
||||
kclErrors,
|
||||
}))
|
||||
const { errors } = useKclContext()
|
||||
useEffect(() => {
|
||||
const element = document.querySelector('.console-tile')
|
||||
if (element) {
|
||||
element.scrollTop = element.scrollHeight - element.clientHeight
|
||||
}
|
||||
}, [kclErrors])
|
||||
}, [errors])
|
||||
return (
|
||||
<CollapsiblePanel {...props}>
|
||||
<div className="h-full relative">
|
||||
<div className="absolute inset-0 flex flex-col">
|
||||
<ReactJsonTypeHack
|
||||
src={kclErrors}
|
||||
src={errors}
|
||||
collapsed={1}
|
||||
collapseStringsAfterLength={60}
|
||||
enableClipboard={false}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import ReactJson from 'react-json-view'
|
||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||
import { useStore } from '../useStore'
|
||||
import { useMemo } from 'react'
|
||||
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/wasm'
|
||||
import { Themes } from '../lib/theme'
|
||||
import { useKclContext } from 'lang/KclSinglton'
|
||||
|
||||
interface MemoryPanelProps extends CollapsiblePanelProps {
|
||||
theme?: Exclude<Themes, Themes.System>
|
||||
@ -13,9 +13,7 @@ export const MemoryPanel = ({
|
||||
theme = Themes.Light,
|
||||
...props
|
||||
}: MemoryPanelProps) => {
|
||||
const { programMemory } = useStore((s) => ({
|
||||
programMemory: s.programMemory,
|
||||
}))
|
||||
const { programMemory } = useKclContext()
|
||||
const ProcessedMemory = useMemo(
|
||||
() => processMemory(programMemory),
|
||||
[programMemory]
|
||||
|
355
src/components/ModelingMachineProvider.tsx
Normal file
355
src/components/ModelingMachineProvider.tsx
Normal file
@ -0,0 +1,355 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
import React, { createContext, useEffect, useRef } from 'react'
|
||||
import {
|
||||
AnyStateMachine,
|
||||
ContextFrom,
|
||||
InterpreterFrom,
|
||||
Prop,
|
||||
StateFrom,
|
||||
assign,
|
||||
} from 'xstate'
|
||||
import { SetSelections, modelingMachine } from 'machines/modelingMachine'
|
||||
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||
import { engineCommandManager } from 'lang/std/engineConnection'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { addStartSketch } from 'lang/modifyAst'
|
||||
import { roundOff } from 'lib/utils'
|
||||
import { recast, parse, Program, VariableDeclarator } from 'lang/wasm'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import {
|
||||
addCloseToPipe,
|
||||
addNewSketchLn,
|
||||
compareVec2Epsilon,
|
||||
} from 'lang/std/sketch'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
||||
import { applyConstraintAngleBetween } from './Toolbar/SetAngleBetween'
|
||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { pathMapToSelections } from 'lang/util'
|
||||
import {
|
||||
dispatchCodeMirrorCursor,
|
||||
setCodeMirrorCursor,
|
||||
useStore,
|
||||
} from 'useStore'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
context: ContextFrom<T>
|
||||
send: Prop<InterpreterFrom<T>, 'send'>
|
||||
}
|
||||
|
||||
export const ModelingMachineContext = createContext(
|
||||
{} as MachineContext<typeof modelingMachine>
|
||||
)
|
||||
|
||||
export const ModelingMachineProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const { auth } = useGlobalStateContext()
|
||||
const token = auth?.context?.token
|
||||
const streamRef = useRef<HTMLDivElement>(null)
|
||||
useSetupEngineManager(streamRef, token)
|
||||
|
||||
const { isShiftDown, editorView } = useStore((s) => ({
|
||||
isShiftDown: s.isShiftDown,
|
||||
editorView: s.editorView,
|
||||
}))
|
||||
|
||||
// const { commands } = useCommandsContext()
|
||||
|
||||
// Settings machine setup
|
||||
// const retrievedSettings = useRef(
|
||||
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
||||
// )
|
||||
|
||||
// What should we persist from modeling state? Nothing?
|
||||
// const persistedSettings = Object.assign(
|
||||
// settingsMachine.initialState.context,
|
||||
// JSON.parse(retrievedSettings.current) as Partial<
|
||||
// (typeof settingsMachine)['context']
|
||||
// >
|
||||
// )
|
||||
|
||||
const [modelingState, modelingSend] = useMachine(modelingMachine, {
|
||||
// context: persistedSettings,
|
||||
actions: {
|
||||
'Modify AST': () => {},
|
||||
'Update code selection cursors': () => {},
|
||||
'show default planes': () => {
|
||||
kclManager.showPlanes()
|
||||
},
|
||||
'create path': async () => {
|
||||
const sketchUuid = uuidv4()
|
||||
const proms = [
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: sketchUuid,
|
||||
cmd: {
|
||||
type: 'start_path',
|
||||
},
|
||||
}),
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'edit_mode_enter',
|
||||
target: sketchUuid,
|
||||
},
|
||||
}),
|
||||
]
|
||||
await Promise.all(proms)
|
||||
},
|
||||
'AST start new sketch': assign((_, { data: { coords, axis } }) => {
|
||||
// Something really weird must have happened for this to happen.
|
||||
if (!axis) {
|
||||
console.error('axis is undefined for starting a new sketch')
|
||||
return {}
|
||||
}
|
||||
|
||||
const _addStartSketch = addStartSketch(
|
||||
kclManager.ast,
|
||||
axis,
|
||||
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
||||
[
|
||||
roundOff(coords[1].x - coords[0].x),
|
||||
roundOff(coords[1].y - coords[0].y),
|
||||
]
|
||||
)
|
||||
const _modifiedAst = _addStartSketch.modifiedAst
|
||||
const _pathToNode = _addStartSketch.pathToNode
|
||||
const newCode = recast(_modifiedAst)
|
||||
const astWithUpdatedSource = parse(newCode)
|
||||
|
||||
kclManager.executeAstMock(astWithUpdatedSource, true)
|
||||
|
||||
return {
|
||||
sketchPathToNode: _pathToNode,
|
||||
}
|
||||
}),
|
||||
'AST add line segment': ({ sketchPathToNode }, { data: { coords } }) => {
|
||||
if (!sketchPathToNode) return
|
||||
const lastCoord = coords[coords.length - 1]
|
||||
|
||||
const { node: varDec } = getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
sketchPathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
const variableName = varDec.id.name
|
||||
const sketchGroup = kclManager.programMemory.root[variableName]
|
||||
if (!sketchGroup || sketchGroup.type !== 'SketchGroup') return
|
||||
const initialCoords = sketchGroup.value[0].from
|
||||
|
||||
const isClose = compareVec2Epsilon(initialCoords, [
|
||||
lastCoord.x,
|
||||
lastCoord.y,
|
||||
])
|
||||
|
||||
let _modifiedAst: Program
|
||||
if (!isClose) {
|
||||
_modifiedAst = addNewSketchLn({
|
||||
node: kclManager.ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
to: [lastCoord.x, lastCoord.y],
|
||||
fnName: 'line',
|
||||
pathToNode: sketchPathToNode,
|
||||
}).modifiedAst
|
||||
kclManager.executeAstMock(_modifiedAst, true)
|
||||
// kclManager.updateAst(_modifiedAst, false)
|
||||
} else {
|
||||
_modifiedAst = addCloseToPipe({
|
||||
node: kclManager.ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
pathToNode: sketchPathToNode,
|
||||
})
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'edit_mode_exit' },
|
||||
})
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||
})
|
||||
kclManager.executeAstMock(_modifiedAst, true)
|
||||
// updateAst(_modifiedAst, true)
|
||||
}
|
||||
},
|
||||
'sketch exit execute': () => {
|
||||
kclManager.executeAst()
|
||||
},
|
||||
'set tool': () => {}, // TODO
|
||||
'toast extrude failed': () => {
|
||||
toast.error(
|
||||
'Extrude failed, sketches need to be closed, or not already extruded'
|
||||
)
|
||||
},
|
||||
'Set selection': assign(({ selectionRanges }, event) => {
|
||||
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
|
||||
const setSelections = event.data
|
||||
if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
|
||||
return { selectionRanges: setSelections.selection }
|
||||
else if (setSelections.selectionType === 'otherSelection')
|
||||
return {
|
||||
selectionRanges: {
|
||||
...selectionRanges,
|
||||
otherSelections: [setSelections.selection],
|
||||
},
|
||||
}
|
||||
else if (!editorView) return {}
|
||||
else if (setSelections.selectionType === 'singleCodeCursor') {
|
||||
// This DOES NOT set the `selectionRanges` in xstate context
|
||||
// instead it updates/dispatches to the editor, which in turn updates the xstate context
|
||||
// I've found this the best way to deal with the editor without causing an infinite loop
|
||||
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
|
||||
// because we want to respect the user manually placing the cursor too.
|
||||
const selectionRangeTypeMap = setCodeMirrorCursor({
|
||||
codeSelection: setSelections.selection,
|
||||
currestSelections: selectionRanges,
|
||||
editorView,
|
||||
isShiftDown,
|
||||
})
|
||||
return {
|
||||
selectionRangeTypeMap,
|
||||
}
|
||||
}
|
||||
// This DOES NOT set the `selectionRanges` in xstate context
|
||||
// same as comment above
|
||||
const selectionRangeTypeMap = dispatchCodeMirrorCursor({
|
||||
selections: setSelections.selection,
|
||||
editorView,
|
||||
})
|
||||
return {
|
||||
selectionRangeTypeMap,
|
||||
}
|
||||
}),
|
||||
},
|
||||
guards: {
|
||||
'Selection contains axis': () => true,
|
||||
'Selection contains edge': () => true,
|
||||
'Selection contains face': () => true,
|
||||
'Selection contains line': () => true,
|
||||
'Selection contains point': () => true,
|
||||
'Selection is not empty': () => true,
|
||||
'Selection is one face': ({ selectionRanges }) => {
|
||||
return !!isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
selectionRanges
|
||||
)
|
||||
},
|
||||
},
|
||||
services: {
|
||||
'Get horizontal info': async ({
|
||||
selectionRanges,
|
||||
}): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintHorzVertDistance({
|
||||
constraint: 'setHorzDistance',
|
||||
selectionRanges,
|
||||
})
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection: pathMapToSelections(
|
||||
kclManager.ast,
|
||||
selectionRanges,
|
||||
pathToNodeMap
|
||||
),
|
||||
}
|
||||
},
|
||||
'Get vertical info': async ({
|
||||
selectionRanges,
|
||||
}): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintHorzVertDistance({
|
||||
constraint: 'setVertDistance',
|
||||
selectionRanges,
|
||||
})
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection: pathMapToSelections(
|
||||
kclManager.ast,
|
||||
selectionRanges,
|
||||
pathToNodeMap
|
||||
),
|
||||
}
|
||||
},
|
||||
'Get angle info': async ({ selectionRanges }): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintAngleBetween({
|
||||
selectionRanges,
|
||||
})
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection: pathMapToSelections(
|
||||
kclManager.ast,
|
||||
selectionRanges,
|
||||
pathToNodeMap
|
||||
),
|
||||
}
|
||||
},
|
||||
'Get length info': async ({
|
||||
selectionRanges,
|
||||
}): Promise<SetSelections> => {
|
||||
const { modifiedAst, pathToNodeMap } = await applyConstraintAngleLength(
|
||||
{ selectionRanges }
|
||||
)
|
||||
await kclManager.updateAst(modifiedAst, true)
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection: pathMapToSelections(
|
||||
kclManager.ast,
|
||||
selectionRanges,
|
||||
pathToNodeMap
|
||||
),
|
||||
}
|
||||
},
|
||||
},
|
||||
devTools: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
engineCommandManager.onPlaneSelected((plane_id: string) => {
|
||||
if (modelingState.nextEvents.includes('Select default plane')) {
|
||||
modelingSend({
|
||||
type: 'Select default plane',
|
||||
data: { planeId: plane_id },
|
||||
})
|
||||
}
|
||||
})
|
||||
}, [kclManager.defaultPlanes, modelingSend, modelingState.nextEvents])
|
||||
|
||||
// useStateMachineCommands({
|
||||
// state: settingsState,
|
||||
// send: settingsSend,
|
||||
// commands,
|
||||
// owner: 'settings',
|
||||
// commandBarMeta: settingsCommandBarMeta,
|
||||
// })
|
||||
|
||||
return (
|
||||
<ModelingMachineContext.Provider
|
||||
value={{
|
||||
state: modelingState,
|
||||
context: modelingState.context,
|
||||
send: modelingSend,
|
||||
}}
|
||||
>
|
||||
{/* TODO #818: maybe pass reff down to children/app.ts or render app.tsx directly?
|
||||
since realistically it won't ever have generic children that isn't app.tsx */}
|
||||
<div className="h-screen overflow-hidden select-none" ref={streamRef}>
|
||||
{children}
|
||||
</div>
|
||||
</ModelingMachineContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelingMachineProvider
|
@ -7,28 +7,17 @@ import {
|
||||
} from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useStore } from '../useStore'
|
||||
import { getNormalisedCoordinates, roundOff } from '../lib/utils'
|
||||
import { getNormalisedCoordinates } from '../lib/utils'
|
||||
import Loading from './Loading'
|
||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { addStartSketch } from 'lang/modifyAst'
|
||||
import {
|
||||
addCloseToPipe,
|
||||
addNewSketchLn,
|
||||
compareVec2Epsilon,
|
||||
} from 'lang/std/sketch'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import {
|
||||
Program,
|
||||
VariableDeclarator,
|
||||
rangeTypeFix,
|
||||
modifyAstForSketch,
|
||||
} from 'lang/wasm'
|
||||
import { KCLError } from 'lang/errors'
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
import { Program, VariableDeclarator, modifyAstForSketch } from 'lang/wasm'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
||||
|
||||
export const Stream = ({ className = '' }) => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
@ -40,35 +29,17 @@ export const Stream = ({ className = '' }) => {
|
||||
didDragInStream,
|
||||
setDidDragInStream,
|
||||
streamDimensions,
|
||||
isExecuting,
|
||||
guiMode,
|
||||
ast,
|
||||
updateAst,
|
||||
setGuiMode,
|
||||
programMemory,
|
||||
defaultPlanes,
|
||||
currentPlane,
|
||||
} = useStore((s) => ({
|
||||
mediaStream: s.mediaStream,
|
||||
setButtonDownInStream: s.setButtonDownInStream,
|
||||
fileId: s.fileId,
|
||||
didDragInStream: s.didDragInStream,
|
||||
setDidDragInStream: s.setDidDragInStream,
|
||||
streamDimensions: s.streamDimensions,
|
||||
isExecuting: s.isExecuting,
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
setGuiMode: s.setGuiMode,
|
||||
programMemory: s.programMemory,
|
||||
defaultPlanes: s.defaultPlanes,
|
||||
currentPlane: s.currentPlane,
|
||||
}))
|
||||
const {
|
||||
settings: {
|
||||
context: { cameraControls },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
const { settings } = useGlobalStateContext()
|
||||
const cameraControls = settings?.context?.cameraControls
|
||||
const { send, state, context } = useModelingContext()
|
||||
const { isExecuting } = useKclContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -112,7 +83,7 @@ export const Stream = ({ className = '' }) => {
|
||||
interaction = 'zoom'
|
||||
}
|
||||
|
||||
if (guiMode.mode === 'sketch' && guiMode.sketchMode === ('move' as any)) {
|
||||
if (state.matches('Sketch.Move Tool')) {
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
@ -121,12 +92,7 @@ export const Stream = ({ className = '' }) => {
|
||||
},
|
||||
cmd_id: newId,
|
||||
})
|
||||
} else if (
|
||||
!(
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any)
|
||||
)
|
||||
) {
|
||||
} else if (!state.matches('Sketch.Line Tool')) {
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
@ -182,87 +148,122 @@ export const Stream = ({ className = '' }) => {
|
||||
cmd_id: newCmdId,
|
||||
}
|
||||
|
||||
if (!didDragInStream) {
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'select_with_point',
|
||||
selection_type: 'add',
|
||||
selected_at_window: { x, y },
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
}
|
||||
|
||||
if (!didDragInStream && guiMode.mode === 'default') {
|
||||
if (!didDragInStream && state.matches('Sketch no face')) {
|
||||
command.cmd = {
|
||||
type: 'select_with_point',
|
||||
selection_type: 'add',
|
||||
selected_at_window: { x, y },
|
||||
}
|
||||
} else if (
|
||||
(!didDragInStream &&
|
||||
guiMode.mode === 'sketch' &&
|
||||
['move', 'select'].includes(guiMode.sketchMode)) ||
|
||||
(guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('sketch_line' as any))
|
||||
) {
|
||||
engineCommandManager.sendSceneCommand(command)
|
||||
} else if (!didDragInStream && state.matches('Sketch.Line Tool')) {
|
||||
command.cmd = {
|
||||
type: 'mouse_click',
|
||||
window: { x, y },
|
||||
}
|
||||
engineCommandManager.sendSceneCommand(command).then(async (resp) => {
|
||||
const entities_modified = resp?.data?.data?.entities_modified
|
||||
if (!entities_modified) return
|
||||
if (state.matches('Sketch.Line Tool.No Points')) {
|
||||
send('Add point')
|
||||
} else if (state.matches('Sketch.Line Tool.Point Added')) {
|
||||
const curve = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
// We need the normal for the plane we are on.
|
||||
const plane = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'get_sketch_mode_plane',
|
||||
},
|
||||
})
|
||||
const z_axis = plane.data.data.z_axis
|
||||
|
||||
// Get the current axis.
|
||||
let currentAxis: 'xy' | 'xz' | 'yz' | '-xy' | '-xz' | '-yz' | null =
|
||||
null
|
||||
if (context.sketchPlaneId === kclManager.getPlaneId('xy')) {
|
||||
if (z_axis.z === -1) {
|
||||
currentAxis = '-xy'
|
||||
} else {
|
||||
currentAxis = 'xy'
|
||||
}
|
||||
} else if (context.sketchPlaneId === kclManager.getPlaneId('yz')) {
|
||||
if (z_axis.x === -1) {
|
||||
currentAxis = '-yz'
|
||||
} else {
|
||||
currentAxis = 'yz'
|
||||
}
|
||||
} else if (context.sketchPlaneId === kclManager.getPlaneId('xz')) {
|
||||
if (z_axis.y === -1) {
|
||||
currentAxis = '-xz'
|
||||
} else {
|
||||
currentAxis = 'xz'
|
||||
}
|
||||
}
|
||||
|
||||
send({ type: 'Add point', data: { coords, axis: currentAxis } })
|
||||
} else if (state.matches('Sketch.Line Tool.Segment Added')) {
|
||||
const curve = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
send({ type: 'Add point', data: { coords, axis: null } })
|
||||
}
|
||||
})
|
||||
} else if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === ('move' as any)
|
||||
!didDragInStream &&
|
||||
(state.matches('Sketch.SketchIdle') ||
|
||||
state.matches('idle') ||
|
||||
state.matches('awaiting selection'))
|
||||
) {
|
||||
command.cmd = {
|
||||
type: 'select_with_point',
|
||||
selected_at_window: { x, y },
|
||||
selection_type: 'add',
|
||||
}
|
||||
engineCommandManager.sendSceneCommand(command)
|
||||
} else if (!didDragInStream && state.matches('Sketch.Move Tool')) {
|
||||
command.cmd = {
|
||||
type: 'select_with_point',
|
||||
selected_at_window: { x, y },
|
||||
selection_type: 'add',
|
||||
}
|
||||
engineCommandManager.sendSceneCommand(command)
|
||||
} else if (didDragInStream && state.matches('Sketch.Move Tool')) {
|
||||
command.cmd = {
|
||||
type: 'handle_mouse_drag_end',
|
||||
window: { x, y },
|
||||
}
|
||||
}
|
||||
engineCommandManager.sendSceneCommand(command).then(async (resp) => {
|
||||
if (!(guiMode.mode === 'sketch')) return
|
||||
|
||||
if (guiMode.sketchMode === 'selectFace') return
|
||||
|
||||
// Check if the sketch group already exists.
|
||||
const varDec = getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
guiMode.pathToNode,
|
||||
'VariableDeclarator'
|
||||
).node
|
||||
const variableName = varDec?.id?.name
|
||||
const sketchGroup = programMemory.root[variableName]
|
||||
const isEditingExistingSketch =
|
||||
sketchGroup?.type === 'SketchGroup' && sketchGroup.value.length
|
||||
let sketchGroupId = ''
|
||||
if (sketchGroup && sketchGroup.type === 'SketchGroup') {
|
||||
sketchGroupId = sketchGroup.id
|
||||
}
|
||||
|
||||
if (
|
||||
guiMode.sketchMode === ('move' as any as 'line') &&
|
||||
command.cmd.type === 'handle_mouse_drag_end'
|
||||
) {
|
||||
// Let's get the updated ast.
|
||||
if (sketchGroupId === '') return
|
||||
// We have a problem if we do not have an id for the sketch group.
|
||||
if (
|
||||
guiMode.pathId === undefined ||
|
||||
guiMode.pathId === null ||
|
||||
guiMode.pathId === ''
|
||||
)
|
||||
return
|
||||
|
||||
let engineId = guiMode.pathId
|
||||
engineCommandManager.sendSceneCommand(command).then(async () => {
|
||||
if (!context.sketchPathToNode) return
|
||||
const varDec = getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
context.sketchPathToNode,
|
||||
'VariableDeclarator'
|
||||
).node
|
||||
const variableName = varDec?.id?.name
|
||||
|
||||
// Get the current plane string for plane we are on.
|
||||
let currentPlaneString = ''
|
||||
if (currentPlane === defaultPlanes?.xy) {
|
||||
if (context.sketchPlaneId === kclManager.getPlaneId('xy')) {
|
||||
currentPlaneString = 'XY'
|
||||
} else if (currentPlane === defaultPlanes?.yz) {
|
||||
} else if (context.sketchPlaneId === kclManager.getPlaneId('yz')) {
|
||||
currentPlaneString = 'YZ'
|
||||
} else if (currentPlane === defaultPlanes?.xz) {
|
||||
} else if (context.sketchPlaneId === kclManager.getPlaneId('xz')) {
|
||||
currentPlaneString = 'XZ'
|
||||
}
|
||||
|
||||
@ -273,163 +274,15 @@ export const Stream = ({ className = '' }) => {
|
||||
|
||||
const updatedAst: Program = await modifyAstForSketch(
|
||||
engineCommandManager,
|
||||
ast,
|
||||
kclManager.ast,
|
||||
variableName,
|
||||
currentPlaneString,
|
||||
engineId
|
||||
context.sketchEnginePathId
|
||||
)
|
||||
kclManager.executeAstMock(updatedAst, true)
|
||||
})
|
||||
}
|
||||
|
||||
updateAst(updatedAst, false)
|
||||
return
|
||||
}
|
||||
|
||||
if (command?.cmd?.type !== 'mouse_click' || !ast) return
|
||||
|
||||
if (!(guiMode.sketchMode === ('sketch_line' as any as 'line'))) return
|
||||
|
||||
if (
|
||||
resp?.data?.data?.entities_modified?.length &&
|
||||
guiMode.waitingFirstClick &&
|
||||
!isEditingExistingSketch
|
||||
) {
|
||||
const curve = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: resp?.data?.data?.entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
|
||||
// We need the normal for the plane we are on.
|
||||
const plane = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'get_sketch_mode_plane',
|
||||
},
|
||||
})
|
||||
const z_axis = plane.data.data.z_axis
|
||||
|
||||
// Get the current axis.
|
||||
let currentAxis: 'xy' | 'xz' | 'yz' | '-xy' | '-xz' | '-yz' | null =
|
||||
null
|
||||
if (currentPlane === defaultPlanes?.xy) {
|
||||
if (z_axis.z === -1) {
|
||||
currentAxis = '-xy'
|
||||
} else {
|
||||
currentAxis = 'xy'
|
||||
}
|
||||
} else if (currentPlane === defaultPlanes?.yz) {
|
||||
if (z_axis.x === -1) {
|
||||
currentAxis = '-yz'
|
||||
} else {
|
||||
currentAxis = 'yz'
|
||||
}
|
||||
} else if (currentPlane === defaultPlanes?.xz) {
|
||||
if (z_axis.y === -1) {
|
||||
currentAxis = '-xz'
|
||||
} else {
|
||||
currentAxis = 'xz'
|
||||
}
|
||||
}
|
||||
|
||||
// Do not support starting a new sketch on a non-default plane.
|
||||
if (!currentAxis) return
|
||||
|
||||
const _addStartSketch = addStartSketch(
|
||||
ast,
|
||||
currentAxis,
|
||||
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
||||
[
|
||||
roundOff(coords[1].x - coords[0].x),
|
||||
roundOff(coords[1].y - coords[0].y),
|
||||
]
|
||||
)
|
||||
const _modifiedAst = _addStartSketch.modifiedAst
|
||||
const _pathToNode = _addStartSketch.pathToNode
|
||||
|
||||
// We need to update the guiMode with the right pathId so that we can
|
||||
// move lines later and send the right sketch id to the engine.
|
||||
for (const [id, artifact] of Object.entries(
|
||||
engineCommandManager.artifactMap
|
||||
)) {
|
||||
if (artifact.commandType === 'start_path') {
|
||||
guiMode.pathId = id
|
||||
}
|
||||
}
|
||||
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
pathToNode: _pathToNode,
|
||||
waitingFirstClick: false,
|
||||
})
|
||||
updateAst(_modifiedAst, false)
|
||||
} else if (
|
||||
resp?.data?.data?.entities_modified?.length &&
|
||||
(!guiMode.waitingFirstClick || isEditingExistingSketch)
|
||||
) {
|
||||
const curve = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: resp?.data?.data?.entities_modified[0],
|
||||
},
|
||||
})
|
||||
const coords: { x: number; y: number }[] =
|
||||
curve.data.data.control_points
|
||||
|
||||
const { node: varDec } = getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
guiMode.pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
const variableName = varDec.id.name
|
||||
const sketchGroup = programMemory.root[variableName]
|
||||
if (!sketchGroup || sketchGroup.type !== 'SketchGroup') return
|
||||
const initialCoords = sketchGroup.value[0].from
|
||||
|
||||
const isClose = compareVec2Epsilon(initialCoords, [
|
||||
coords[1].x,
|
||||
coords[1].y,
|
||||
])
|
||||
|
||||
let _modifiedAst: Program
|
||||
if (!isClose) {
|
||||
_modifiedAst = addNewSketchLn({
|
||||
node: ast,
|
||||
programMemory,
|
||||
to: [coords[1].x, coords[1].y],
|
||||
fnName: 'line',
|
||||
pathToNode: guiMode.pathToNode,
|
||||
}).modifiedAst
|
||||
updateAst(_modifiedAst, false)
|
||||
} else {
|
||||
_modifiedAst = addCloseToPipe({
|
||||
node: ast,
|
||||
programMemory,
|
||||
pathToNode: guiMode.pathToNode,
|
||||
})
|
||||
setGuiMode({
|
||||
mode: 'default',
|
||||
})
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'edit_mode_exit' },
|
||||
})
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||
})
|
||||
updateAst(_modifiedAst, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
setDidDragInStream(false)
|
||||
setClickCoords(undefined)
|
||||
}
|
||||
@ -460,8 +313,8 @@ export const Stream = ({ className = '' }) => {
|
||||
onWheel={handleScroll}
|
||||
onPlay={() => setIsLoading(false)}
|
||||
onMouseMoveCapture={handleMouseMove}
|
||||
className={`w-full cursor-pointer h-full ${isExecuting && 'blur-md'}`}
|
||||
disablePictureInPicture
|
||||
className={`w-full h-full ${isExecuting && 'blur-md'}`}
|
||||
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
||||
/>
|
||||
{isLoading && (
|
||||
|
@ -11,7 +11,7 @@ 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 { useMemo, useState } from 'react'
|
||||
import { linter, lintGutter } from '@codemirror/lint'
|
||||
import { Selections, useStore } from 'useStore'
|
||||
import { LanguageServerClient } from 'editor/lsp'
|
||||
@ -29,8 +29,10 @@ import {
|
||||
import { isOverlap, roundOff } from 'lib/utils'
|
||||
import { kclErrToDiagnostic } from 'lang/errors'
|
||||
import { CSSRuleObject } from 'tailwindcss/types/config'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import interact from '@replit/codemirror-interact'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
||||
|
||||
export const editorShortcutMeta = {
|
||||
formatCode: {
|
||||
@ -49,35 +51,22 @@ export const TextEditor = ({
|
||||
theme: Themes.Light | Themes.Dark
|
||||
}) => {
|
||||
const pathParams = useParams()
|
||||
const {
|
||||
code,
|
||||
deferredSetCode,
|
||||
editorView,
|
||||
formatCode,
|
||||
isLSPServerReady,
|
||||
selectionRanges,
|
||||
selectionRangeTypeMap,
|
||||
setEditorView,
|
||||
setIsLSPServerReady,
|
||||
setSelectionRanges,
|
||||
} = useStore((s) => ({
|
||||
code: s.code,
|
||||
deferredSetCode: s.deferredSetCode,
|
||||
editorView: s.editorView,
|
||||
formatCode: s.formatCode,
|
||||
isLSPServerReady: s.isLSPServerReady,
|
||||
selectionRanges: s.selectionRanges,
|
||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||
setEditorView: s.setEditorView,
|
||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
||||
setSelectionRanges: s.setSelectionRanges,
|
||||
}))
|
||||
const { editorView, isLSPServerReady, setEditorView, setIsLSPServerReady } =
|
||||
useStore((s) => ({
|
||||
editorView: s.editorView,
|
||||
isLSPServerReady: s.isLSPServerReady,
|
||||
setEditorView: s.setEditorView,
|
||||
setIsLSPServerReady: s.setIsLSPServerReady,
|
||||
}))
|
||||
const { code, errors } = useKclContext()
|
||||
|
||||
const {
|
||||
settings: {
|
||||
context: { textWrapping },
|
||||
},
|
||||
} = useGlobalStateContext()
|
||||
context: { selectionRanges, selectionRangeTypeMap },
|
||||
send,
|
||||
} = useModelingContext()
|
||||
|
||||
const { settings: { context: { textWrapping } = {} } = {} } =
|
||||
useGlobalStateContext()
|
||||
const { setCommandBarOpen } = useCommandsContext()
|
||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||
useConvertToVariable()
|
||||
@ -122,12 +111,12 @@ export const TextEditor = ({
|
||||
}, [lspClient, isLSPServerReady])
|
||||
|
||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
||||
deferredSetCode(value)
|
||||
const onChange = (newCode: string, viewUpdate: ViewUpdate) => {
|
||||
kclManager.setCodeAndExecute(newCode)
|
||||
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(
|
||||
writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, newCode).catch(
|
||||
(err) => {
|
||||
// TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
||||
console.error('error saving file', err)
|
||||
@ -190,10 +179,17 @@ export const TextEditor = ({
|
||||
idBasedSelections,
|
||||
})
|
||||
|
||||
setSelectionRanges({
|
||||
otherSelections: [],
|
||||
codeBasedSelections,
|
||||
})
|
||||
selectionRanges &&
|
||||
send({
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'mirrorCodeMirrorSelections',
|
||||
selection: {
|
||||
...selectionRanges,
|
||||
codeBasedSelections,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const editorExtensions = useMemo(() => {
|
||||
@ -210,7 +206,7 @@ export const TextEditor = ({
|
||||
{
|
||||
key: editorShortcutMeta.formatCode.codeMirror,
|
||||
run: () => {
|
||||
formatCode()
|
||||
kclManager.format()
|
||||
return true
|
||||
},
|
||||
},
|
||||
@ -234,7 +230,7 @@ export const TextEditor = ({
|
||||
extensions.push(
|
||||
lintGutter(),
|
||||
linter((_view) => {
|
||||
return kclErrToDiagnostic(useStore.getState().kclErrors)
|
||||
return kclErrToDiagnostic(errors)
|
||||
}),
|
||||
interact({
|
||||
rules: [
|
||||
|
@ -11,34 +11,30 @@ import {
|
||||
transformSecondarySketchLinesTagFirst,
|
||||
getTransformInfos,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { updateCursors } from '../../lang/util'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import { sketchButtonClassnames } from 'Toolbar'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
/*
|
||||
export const EqualAngle = () => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||
useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
selectionRanges: s.selectionRanges,
|
||||
programMemory: s.programMemory,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const [enableEqual, setEnableEqual] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(ast, range)
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
||||
)
|
||||
const varDecs = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)?.node
|
||||
@ -46,7 +42,7 @@ export const EqualAngle = () => {
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||
isSketchVariablesLinked(secondary, primaryLine, ast)
|
||||
isSketchVariablesLinked(secondary, primaryLine, kclManager.ast)
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
@ -59,7 +55,7 @@ export const EqualAngle = () => {
|
||||
...selectionRanges,
|
||||
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||
},
|
||||
ast,
|
||||
kclManager.ast,
|
||||
'equalAngle'
|
||||
)
|
||||
setTransformInfos(theTransforms)
|
||||
@ -76,15 +72,15 @@ export const EqualAngle = () => {
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!(transformInfos && ast)) return
|
||||
if (!transformInfos) return
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast,
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
updateAst(modifiedAst, true, {
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}}
|
||||
@ -103,3 +99,4 @@ export const EqualAngle = () => {
|
||||
</button>
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { toolTips, useStore } from '../../useStore'
|
||||
import { Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import { Selections, toolTips, useStore } from '../../useStore'
|
||||
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getNodeFromPath,
|
||||
@ -10,81 +10,41 @@ import {
|
||||
TransformInfo,
|
||||
transformSecondarySketchLinesTagFirst,
|
||||
getTransformInfos,
|
||||
PathToNodeMap,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { updateCursors } from '../../lang/util'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import { sketchButtonClassnames } from 'Toolbar'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
/*
|
||||
export const EqualLength = () => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||
useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
selectionRanges: s.selectionRanges,
|
||||
programMemory: s.programMemory,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const [enableEqual, setEnableEqual] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||
)
|
||||
const varDecs = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)?.node
|
||||
)
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||
isSketchVariablesLinked(secondary, primaryLine, ast)
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
const { enabled, transforms } = setEqualLengthInfo({ selectionRanges })
|
||||
|
||||
const theTransforms = getTransformInfos(
|
||||
{
|
||||
...selectionRanges,
|
||||
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||
},
|
||||
ast,
|
||||
'equalLength'
|
||||
)
|
||||
setTransformInfos(theTransforms)
|
||||
|
||||
const _enableEqual =
|
||||
!!secondaryVarDecs.length &&
|
||||
isAllTooltips &&
|
||||
isOthersLinkedToPrimary &&
|
||||
theTransforms.every(Boolean)
|
||||
setEnableEqual(_enableEqual)
|
||||
setTransformInfos(transforms)
|
||||
setEnableEqual(enabled)
|
||||
}, [guiMode, selectionRanges])
|
||||
if (guiMode.mode !== 'sketch') return null
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!(transformInfos && ast)) return
|
||||
if (!transformInfos) return
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast,
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
updateAst(modifiedAst, true, {
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}}
|
||||
@ -103,3 +63,72 @@ export const EqualLength = () => {
|
||||
</button>
|
||||
)
|
||||
}
|
||||
*/
|
||||
export function setEqualLengthInfo({
|
||||
selectionRanges,
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
}) {
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
||||
)
|
||||
const varDecs = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)?.node
|
||||
)
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||
isSketchVariablesLinked(secondary, primaryLine, kclManager.ast)
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const transforms = getTransformInfos(
|
||||
{
|
||||
...selectionRanges,
|
||||
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||
},
|
||||
kclManager.ast,
|
||||
'equalLength'
|
||||
)
|
||||
|
||||
const enabled =
|
||||
!!secondaryVarDecs.length &&
|
||||
isAllTooltips &&
|
||||
isOthersLinkedToPrimary &&
|
||||
transforms.every(Boolean)
|
||||
|
||||
return { enabled, transforms }
|
||||
}
|
||||
|
||||
export function applyConstraintEqualLength({
|
||||
selectionRanges,
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
}): {
|
||||
modifiedAst: Program
|
||||
pathToNodeMap: PathToNodeMap
|
||||
} {
|
||||
const { enabled, transforms } = setEqualLengthInfo({ selectionRanges })
|
||||
const { modifiedAst, pathToNodeMap } = transformSecondarySketchLinesTagFirst({
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos: transforms,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
return { modifiedAst, pathToNodeMap }
|
||||
// kclManager.updateAst(modifiedAst, true, {
|
||||
// // callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
}
|
||||
|
@ -1,69 +1,53 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { toolTips, useStore } from '../../useStore'
|
||||
import { Value } from '../../lang/wasm'
|
||||
import { Program, ProgramMemory, Value } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getNodeFromPath,
|
||||
} from '../../lang/queryAst'
|
||||
import {
|
||||
PathToNodeMap,
|
||||
TransformInfo,
|
||||
getTransformInfos,
|
||||
transformAstSketchLines,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { updateCursors } from '../../lang/util'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import { sketchButtonClassnames } from 'Toolbar'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { Selections } from 'useStore'
|
||||
|
||||
/*
|
||||
export const HorzVert = ({
|
||||
horOrVert,
|
||||
}: {
|
||||
horOrVert: 'vertical' | 'horizontal'
|
||||
}) => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||
useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
selectionRanges: s.selectionRanges,
|
||||
programMemory: s.programMemory,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const [enableHorz, setEnableHorz] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const theTransforms = getTransformInfos(selectionRanges, ast, horOrVert)
|
||||
setTransformInfos(theTransforms)
|
||||
|
||||
const _enableHorz = isAllTooltips && theTransforms.every(Boolean)
|
||||
setEnableHorz(_enableHorz)
|
||||
const { enabled, transforms } = horzVertInfo(selectionRanges, horOrVert)
|
||||
setTransformInfos(transforms)
|
||||
setEnableHorz(enabled)
|
||||
}, [guiMode, selectionRanges])
|
||||
if (guiMode.mode !== 'sketch') return null
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!transformInfos || !ast) return
|
||||
if (!transformInfos) return
|
||||
const { modifiedAst, pathToNodeMap } = transformAstSketchLines({
|
||||
ast,
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
updateAst(modifiedAst, true, {
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}}
|
||||
@ -82,3 +66,51 @@ export const HorzVert = ({
|
||||
</button>
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
||||
export function horzVertInfo(
|
||||
selectionRanges: Selections,
|
||||
horOrVert: 'vertical' | 'horizontal'
|
||||
) {
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const theTransforms = getTransformInfos(
|
||||
selectionRanges,
|
||||
kclManager.ast,
|
||||
horOrVert
|
||||
)
|
||||
const _enableHorz = isAllTooltips && theTransforms.every(Boolean)
|
||||
return { enabled: _enableHorz, transforms: theTransforms }
|
||||
}
|
||||
|
||||
export function applyConstraintHorzVert(
|
||||
selectionRanges: Selections,
|
||||
horOrVert: 'vertical' | 'horizontal',
|
||||
ast: Program,
|
||||
programMemory: ProgramMemory
|
||||
): {
|
||||
modifiedAst: Program
|
||||
pathToNodeMap: PathToNodeMap
|
||||
} {
|
||||
const transformInfos = horzVertInfo(selectionRanges, horOrVert).transforms
|
||||
return transformAstSketchLines({
|
||||
ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
// kclManager.updateAst(modifiedAst, true, {
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
}
|
||||
|
@ -16,26 +16,22 @@ import {
|
||||
import { GetInfoModal } from '../SetHorVertDistanceModal'
|
||||
import { createVariableDeclaration } from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { updateCursors } from '../../lang/util'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
const getModalInfo = create(GetInfoModal as any)
|
||||
|
||||
/*
|
||||
export const Intersect = () => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||
useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
selectionRanges: s.selectionRanges,
|
||||
programMemory: s.programMemory,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const [enable, setEnable] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
const [forecdSelectionRanges, setForcedSelectionRanges] =
|
||||
useState<typeof selectionRanges>()
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
if (selectionRanges.codeBasedSelections.length < 2) {
|
||||
setEnable(false)
|
||||
setForcedSelectionRanges({ ...selectionRanges })
|
||||
@ -45,8 +41,8 @@ export const Intersect = () => {
|
||||
const previousSegment =
|
||||
selectionRanges.codeBasedSelections.length > 1 &&
|
||||
isLinesParallelAndConstrained(
|
||||
ast,
|
||||
programMemory,
|
||||
kclManager.ast,
|
||||
kclManager.programMemory,
|
||||
selectionRanges.codeBasedSelections[0],
|
||||
selectionRanges.codeBasedSelections[1]
|
||||
)
|
||||
@ -70,15 +66,15 @@ export const Intersect = () => {
|
||||
setForcedSelectionRanges(_forcedSelectionRanges)
|
||||
|
||||
const paths = _forcedSelectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(ast, range)
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
||||
)
|
||||
const varDecs = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)?.node
|
||||
@ -86,7 +82,7 @@ export const Intersect = () => {
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||
isSketchVariablesLinked(secondary, primaryLine, ast)
|
||||
isSketchVariablesLinked(secondary, primaryLine, kclManager.ast)
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
@ -103,7 +99,7 @@ export const Intersect = () => {
|
||||
codeBasedSelections:
|
||||
_forcedSelectionRanges.codeBasedSelections.slice(1),
|
||||
},
|
||||
ast,
|
||||
kclManager.ast,
|
||||
'intersect'
|
||||
)
|
||||
setTransformInfos(theTransforms)
|
||||
@ -121,13 +117,13 @@ export const Intersect = () => {
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!(transformInfos && ast && forecdSelectionRanges)) return
|
||||
if (!(transformInfos && forecdSelectionRanges)) return
|
||||
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: JSON.parse(JSON.stringify(ast)),
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges: forecdSelectionRanges,
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
const {
|
||||
segName,
|
||||
@ -150,7 +146,7 @@ export const Intersect = () => {
|
||||
initialVariableName: 'offset',
|
||||
} as any)
|
||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||
updateAst(modifiedAst, true, {
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
} else {
|
||||
@ -162,10 +158,10 @@ export const Intersect = () => {
|
||||
)
|
||||
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast,
|
||||
ast: kclManager.ast,
|
||||
selectionRanges: forecdSelectionRanges,
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: kclManager.programMemory,
|
||||
forceSegName: segName,
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
@ -178,7 +174,7 @@ export const Intersect = () => {
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
updateAst(_modifiedAst, true, {
|
||||
kclManager.updateAst(_modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}
|
||||
@ -190,3 +186,4 @@ export const Intersect = () => {
|
||||
</button>
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
@ -10,27 +10,23 @@ import {
|
||||
getRemoveConstraintsTransforms,
|
||||
transformAstSketchLines,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { updateCursors } from '../../lang/util'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
/*
|
||||
export const RemoveConstrainingValues = () => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||
useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
selectionRanges: s.selectionRanges,
|
||||
programMemory: s.programMemory,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const [enableHorz, setEnableHorz] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(ast, range)
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
@ -41,7 +37,7 @@ export const RemoveConstrainingValues = () => {
|
||||
try {
|
||||
const theTransforms = getRemoveConstraintsTransforms(
|
||||
selectionRanges,
|
||||
ast,
|
||||
kclManager.ast,
|
||||
'removeConstrainingValues'
|
||||
)
|
||||
setTransformInfos(theTransforms)
|
||||
@ -57,15 +53,15 @@ export const RemoveConstrainingValues = () => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!transformInfos || !ast) return
|
||||
if (!transformInfos) return
|
||||
const { modifiedAst, pathToNodeMap } = transformAstSketchLines({
|
||||
ast,
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
updateAst(modifiedAst, true, {
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}}
|
||||
@ -76,3 +72,4 @@ export const RemoveConstrainingValues = () => {
|
||||
</button>
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
createVariableDeclaration,
|
||||
} from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { updateCursors } from '../../lang/util'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
const getModalInfo = create(SetAngleLengthModal as any)
|
||||
|
||||
@ -31,16 +31,13 @@ const buttonLabels: Record<ButtonType, string> = {
|
||||
snapToXAxis: 'Snap To X Axis',
|
||||
}
|
||||
|
||||
/*
|
||||
export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||
useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
selectionRanges: s.selectionRanges,
|
||||
programMemory: s.programMemory,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const disType: ConstraintType =
|
||||
buttonType === 'xAbs' || buttonType === 'yAbs'
|
||||
? buttonType
|
||||
@ -50,13 +47,13 @@ export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => {
|
||||
const [enableAngLen, setEnableAngLen] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(ast, range)
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<Value>(ast, pathToNode, 'CallExpression').node
|
||||
getNodeFromPath<Value>(kclManager.ast, pathToNode, 'CallExpression')
|
||||
.node
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
@ -64,7 +61,11 @@ export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => {
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const theTransforms = getTransformInfos(selectionRanges, ast, disType)
|
||||
const theTransforms = getTransformInfos(
|
||||
selectionRanges,
|
||||
kclManager.ast,
|
||||
disType
|
||||
)
|
||||
setTransformInfos(theTransforms)
|
||||
|
||||
const enableY =
|
||||
@ -90,12 +91,12 @@ export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => {
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!(transformInfos && ast)) return
|
||||
if (!transformInfos) return
|
||||
const { valueUsedInTransform } = transformAstSketchLines({
|
||||
ast: JSON.parse(JSON.stringify(ast)),
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges: selectionRanges,
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
try {
|
||||
@ -112,10 +113,10 @@ export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => {
|
||||
|
||||
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||
transformAstSketchLines({
|
||||
ast: JSON.parse(JSON.stringify(ast)),
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges: selectionRanges,
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
@ -129,7 +130,7 @@ export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => {
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
|
||||
updateAst(_modifiedAst, true, {
|
||||
kclManager.updateAst(_modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
} catch (e) {
|
||||
@ -143,3 +144,4 @@ export const SetAbsDistance = ({ buttonType }: { buttonType: ButtonType }) => {
|
||||
</button>
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { create } from 'react-modal-promise'
|
||||
import { toolTips, useStore } from '../../useStore'
|
||||
import { BinaryPart, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import { Selections, toolTips, useStore } from '../../useStore'
|
||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getNodeFromPath,
|
||||
@ -11,82 +11,41 @@ import {
|
||||
TransformInfo,
|
||||
transformSecondarySketchLinesTagFirst,
|
||||
getTransformInfos,
|
||||
PathToNodeMap,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { GetInfoModal } from '../SetHorVertDistanceModal'
|
||||
import { createVariableDeclaration } from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { updateCursors } from '../../lang/util'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
const getModalInfo = create(GetInfoModal as any)
|
||||
|
||||
/*
|
||||
export const SetAngleBetween = () => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||
useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
selectionRanges: s.selectionRanges,
|
||||
programMemory: s.programMemory,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const [enable, setEnable] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||
)
|
||||
const varDecs = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)?.node
|
||||
)
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||
isSketchVariablesLinked(secondary, primaryLine, ast)
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const theTransforms = getTransformInfos(
|
||||
{
|
||||
...selectionRanges,
|
||||
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||
},
|
||||
ast,
|
||||
'setAngleBetween'
|
||||
)
|
||||
setTransformInfos(theTransforms)
|
||||
|
||||
const _enableEqual =
|
||||
secondaryVarDecs.length === 1 &&
|
||||
isAllTooltips &&
|
||||
isOthersLinkedToPrimary &&
|
||||
theTransforms.every(Boolean)
|
||||
setEnable(_enableEqual)
|
||||
const { enabled, transforms } = angleBetweenInfo({ selectionRanges })
|
||||
setTransformInfos(transforms)
|
||||
setEnable(enabled)
|
||||
}, [guiMode, selectionRanges])
|
||||
if (guiMode.mode !== 'sketch') return null
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!(transformInfos && ast)) return
|
||||
if (!transformInfos) return
|
||||
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: JSON.parse(JSON.stringify(ast)),
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
const {
|
||||
segName,
|
||||
@ -109,7 +68,7 @@ export const SetAngleBetween = () => {
|
||||
initialVariableName: 'angle',
|
||||
} as any)
|
||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||
updateAst(modifiedAst, true, {
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
} else {
|
||||
@ -121,10 +80,10 @@ export const SetAngleBetween = () => {
|
||||
// transform again but forcing certain values
|
||||
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast,
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: kclManager.programMemory,
|
||||
forceSegName: segName,
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
@ -137,7 +96,7 @@ export const SetAngleBetween = () => {
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
updateAst(_modifiedAst, true, {
|
||||
kclManager.updateAst(_modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}
|
||||
@ -149,3 +108,135 @@ export const SetAngleBetween = () => {
|
||||
</button>
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
||||
export function angleBetweenInfo({
|
||||
selectionRanges,
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
}) {
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
||||
)
|
||||
const varDecs = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)?.node
|
||||
)
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||
isSketchVariablesLinked(secondary, primaryLine, kclManager.ast)
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const theTransforms = getTransformInfos(
|
||||
{
|
||||
...selectionRanges,
|
||||
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||
},
|
||||
kclManager.ast,
|
||||
'setAngleBetween'
|
||||
)
|
||||
|
||||
const _enableEqual =
|
||||
secondaryVarDecs.length === 1 &&
|
||||
isAllTooltips &&
|
||||
isOthersLinkedToPrimary &&
|
||||
theTransforms.every(Boolean)
|
||||
return { enabled: _enableEqual, transforms: theTransforms }
|
||||
}
|
||||
|
||||
export async function applyConstraintAngleBetween({
|
||||
selectionRanges,
|
||||
}: // constraint,
|
||||
{
|
||||
selectionRanges: Selections
|
||||
// constraint: 'setHorzDistance' | 'setVertDistance'
|
||||
}): Promise<{
|
||||
modifiedAst: Program
|
||||
pathToNodeMap: PathToNodeMap
|
||||
}> {
|
||||
const transformInfos = angleBetweenInfo({ selectionRanges }).transforms
|
||||
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
const {
|
||||
segName,
|
||||
value,
|
||||
valueNode,
|
||||
variableName,
|
||||
newVariableInsertIndex,
|
||||
sign,
|
||||
}: {
|
||||
segName: string
|
||||
value: number
|
||||
valueNode: Value
|
||||
variableName?: string
|
||||
newVariableInsertIndex: number
|
||||
sign: number
|
||||
} = await getModalInfo({
|
||||
segName: tagInfo?.tag,
|
||||
isSegNameEditable: !tagInfo?.isTagExisting,
|
||||
value: valueUsedInTransform,
|
||||
initialVariableName: 'angle',
|
||||
} as any)
|
||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||
return {
|
||||
modifiedAst,
|
||||
pathToNodeMap,
|
||||
}
|
||||
// kclManager.updateAst(modifiedAst, true, {
|
||||
// TODO handle cursor
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
}
|
||||
|
||||
const finalValue = removeDoubleNegatives(
|
||||
valueNode as BinaryPart,
|
||||
sign,
|
||||
variableName
|
||||
)
|
||||
// transform again but forcing certain values
|
||||
const { modifiedAst: _modifiedAst, pathToNodeMap: _pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
forceSegName: segName,
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
if (variableName) {
|
||||
const newBody = [..._modifiedAst.body]
|
||||
newBody.splice(
|
||||
newVariableInsertIndex,
|
||||
0,
|
||||
createVariableDeclaration(variableName, valueNode)
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
return {
|
||||
modifiedAst: _modifiedAst,
|
||||
pathToNodeMap: _pathToNodeMap,
|
||||
}
|
||||
// kclManager.updateAst(_modifiedAst, true, {
|
||||
// TODO handle cursor
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { create } from 'react-modal-promise'
|
||||
import { toolTips, useStore } from '../../useStore'
|
||||
import { BinaryPart, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getNodeFromPath,
|
||||
@ -12,11 +12,13 @@ import {
|
||||
transformSecondarySketchLinesTagFirst,
|
||||
getTransformInfos,
|
||||
ConstraintType,
|
||||
PathToNodeMap,
|
||||
} from '../../lang/std/sketchcombos'
|
||||
import { GetInfoModal } from '../SetHorVertDistanceModal'
|
||||
import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { updateCursors } from '../../lang/util'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { Selections } from 'useStore'
|
||||
|
||||
const getModalInfo = create(GetInfoModal as any)
|
||||
|
||||
@ -33,20 +35,17 @@ const buttonLabels: Record<ButtonType, string> = {
|
||||
alignEndsVertically: 'Align Ends Vertically',
|
||||
}
|
||||
|
||||
/*
|
||||
export const SetHorzVertDistance = ({
|
||||
buttonType,
|
||||
}: {
|
||||
buttonType: ButtonType
|
||||
}) => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||
useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
selectionRanges: s.selectionRanges,
|
||||
programMemory: s.programMemory,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const constraint: ConstraintType =
|
||||
buttonType === 'setHorzDistance' || buttonType === 'setVertDistance'
|
||||
? buttonType
|
||||
@ -56,51 +55,12 @@ export const SetHorzVertDistance = ({
|
||||
const [enable, setEnable] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
|
||||
)
|
||||
const varDecs = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)?.node
|
||||
)
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||
isSketchVariablesLinked(secondary, primaryLine, ast)
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
[
|
||||
...toolTips,
|
||||
'startSketchAt', // TODO probably a better place for this to live
|
||||
].includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const theTransforms = getTransformInfos(
|
||||
{
|
||||
...selectionRanges,
|
||||
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||
},
|
||||
ast,
|
||||
constraint
|
||||
)
|
||||
setTransformInfos(theTransforms)
|
||||
|
||||
const _enableEqual =
|
||||
secondaryVarDecs.length === 1 &&
|
||||
isAllTooltips &&
|
||||
isOthersLinkedToPrimary &&
|
||||
theTransforms.every(Boolean)
|
||||
setEnable(_enableEqual)
|
||||
const { transforms, enabled } = horzVertDistanceInfo({
|
||||
selectionRanges,
|
||||
constraint,
|
||||
})
|
||||
setTransformInfos(transforms)
|
||||
setEnable(enabled)
|
||||
}, [guiMode, selectionRanges])
|
||||
if (guiMode.mode !== 'sketch') return null
|
||||
|
||||
@ -111,13 +71,13 @@ export const SetHorzVertDistance = ({
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!(transformInfos && ast)) return
|
||||
if (!transformInfos) return
|
||||
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: JSON.parse(JSON.stringify(ast)),
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
const {
|
||||
segName,
|
||||
@ -142,7 +102,7 @@ export const SetHorzVertDistance = ({
|
||||
constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
|
||||
} as any))
|
||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||
updateAst(modifiedAst, true, {
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
} else {
|
||||
@ -152,10 +112,10 @@ export const SetHorzVertDistance = ({
|
||||
// transform again but forcing certain values
|
||||
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast,
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: kclManager.programMemory,
|
||||
forceSegName: segName,
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
@ -168,7 +128,7 @@ export const SetHorzVertDistance = ({
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
updateAst(_modifiedAst, true, {
|
||||
kclManager.updateAst(_modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
}
|
||||
@ -180,3 +140,175 @@ export const SetHorzVertDistance = ({
|
||||
</button>
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
||||
export function horzVertDistanceInfo({
|
||||
selectionRanges,
|
||||
constraint,
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
constraint: 'setHorzDistance' | 'setVertDistance'
|
||||
}) {
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) => getNodeFromPath<Value>(kclManager.ast, pathToNode).node
|
||||
)
|
||||
const varDecs = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)?.node
|
||||
)
|
||||
const primaryLine = varDecs[0]
|
||||
const secondaryVarDecs = varDecs.slice(1)
|
||||
const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) =>
|
||||
isSketchVariablesLinked(secondary, primaryLine, kclManager.ast)
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
[
|
||||
...toolTips,
|
||||
'startSketchAt', // TODO probably a better place for this to live
|
||||
].includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const theTransforms = getTransformInfos(
|
||||
{
|
||||
...selectionRanges,
|
||||
codeBasedSelections: selectionRanges.codeBasedSelections.slice(1),
|
||||
},
|
||||
kclManager.ast,
|
||||
constraint
|
||||
)
|
||||
const _enableEqual =
|
||||
secondaryVarDecs.length === 1 &&
|
||||
isAllTooltips &&
|
||||
isOthersLinkedToPrimary &&
|
||||
theTransforms.every(Boolean)
|
||||
return { enabled: _enableEqual, transforms: theTransforms }
|
||||
}
|
||||
|
||||
export async function applyConstraintHorzVertDistance({
|
||||
selectionRanges,
|
||||
constraint,
|
||||
// TODO align will always be false (covered by synconous applyConstraintHorzVertAlign), remove it
|
||||
isAlign = false,
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
constraint: 'setHorzDistance' | 'setVertDistance'
|
||||
isAlign?: boolean
|
||||
}): Promise<{
|
||||
modifiedAst: Program
|
||||
pathToNodeMap: PathToNodeMap
|
||||
}> {
|
||||
const transformInfos = horzVertDistanceInfo({
|
||||
selectionRanges,
|
||||
constraint,
|
||||
}).transforms
|
||||
const { modifiedAst, tagInfo, valueUsedInTransform, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
const {
|
||||
segName,
|
||||
value,
|
||||
valueNode,
|
||||
variableName,
|
||||
newVariableInsertIndex,
|
||||
sign,
|
||||
}: {
|
||||
segName: string
|
||||
value: number
|
||||
valueNode: Value
|
||||
variableName?: string
|
||||
newVariableInsertIndex: number
|
||||
sign: number
|
||||
} = await (!isAlign &&
|
||||
getModalInfo({
|
||||
segName: tagInfo?.tag,
|
||||
isSegNameEditable: !tagInfo?.isTagExisting,
|
||||
value: valueUsedInTransform,
|
||||
initialVariableName: constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
|
||||
} as any))
|
||||
if (segName === tagInfo?.tag && value === valueUsedInTransform) {
|
||||
return {
|
||||
modifiedAst,
|
||||
pathToNodeMap,
|
||||
}
|
||||
// TODO handle cursor stuff
|
||||
// kclManager.updateAst(modifiedAst, true, {
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
} else {
|
||||
let finalValue = isAlign
|
||||
? createLiteral(0)
|
||||
: removeDoubleNegatives(valueNode as BinaryPart, sign, variableName)
|
||||
// transform again but forcing certain values
|
||||
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||
transformSecondarySketchLinesTagFirst({
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
forceSegName: segName,
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
if (variableName) {
|
||||
const newBody = [..._modifiedAst.body]
|
||||
newBody.splice(
|
||||
newVariableInsertIndex,
|
||||
0,
|
||||
createVariableDeclaration(variableName, valueNode)
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
return {
|
||||
modifiedAst: _modifiedAst,
|
||||
pathToNodeMap,
|
||||
}
|
||||
// TODO handle cursor stuff
|
||||
// kclManager.updateAst(_modifiedAst, true, {
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
export function applyConstraintHorzVertAlign({
|
||||
selectionRanges,
|
||||
constraint,
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
constraint: 'setHorzDistance' | 'setVertDistance'
|
||||
}): {
|
||||
modifiedAst: Program
|
||||
pathToNodeMap: PathToNodeMap
|
||||
} {
|
||||
const transformInfos = horzVertDistanceInfo({
|
||||
selectionRanges,
|
||||
constraint,
|
||||
}).transforms
|
||||
let finalValue = createLiteral(0)
|
||||
const { modifiedAst, pathToNodeMap } = transformSecondarySketchLinesTagFirst({
|
||||
ast: kclManager.ast,
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory: kclManager.programMemory,
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
return {
|
||||
modifiedAst: modifiedAst,
|
||||
pathToNodeMap,
|
||||
}
|
||||
// TODO handle cursor stuff
|
||||
// kclManager.updateAst(_modifiedAst, true, {
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { create } from 'react-modal-promise'
|
||||
import { toolTips, useStore } from '../../useStore'
|
||||
import { Value } from '../../lang/wasm'
|
||||
import { Selections, toolTips, useStore } from '../../useStore'
|
||||
import { Program, Value } from '../../lang/wasm'
|
||||
import {
|
||||
getNodePathFromSourceRange,
|
||||
getNodeFromPath,
|
||||
} from '../../lang/queryAst'
|
||||
import {
|
||||
PathToNodeMap,
|
||||
TransformInfo,
|
||||
getTransformInfos,
|
||||
transformAstSketchLines,
|
||||
@ -19,7 +20,7 @@ import {
|
||||
} from '../../lang/modifyAst'
|
||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||
import { normaliseAngle } from '../../lib/utils'
|
||||
import { updateCursors } from '../../lang/util'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
const getModalInfo = create(SetAngleLengthModal as any)
|
||||
|
||||
@ -30,54 +31,39 @@ const buttonLabels: Record<ButtonType, string> = {
|
||||
setLength: 'Set Length',
|
||||
}
|
||||
|
||||
/*
|
||||
export const SetAngleLength = ({
|
||||
angleOrLength,
|
||||
}: {
|
||||
angleOrLength: ButtonType
|
||||
}) => {
|
||||
const { guiMode, selectionRanges, ast, programMemory, updateAst, setCursor } =
|
||||
useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
ast: s.ast,
|
||||
updateAst: s.updateAst,
|
||||
selectionRanges: s.selectionRanges,
|
||||
programMemory: s.programMemory,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const { guiMode, selectionRanges, setCursor } = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
setCursor: s.setCursor,
|
||||
}))
|
||||
const [enableAngLen, setEnableAngLen] = useState(false)
|
||||
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<Value>(ast, pathToNode, 'CallExpression').node
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
const { enabled, transforms } = setAngleLengthInfo({
|
||||
selectionRanges,
|
||||
angleOrLength,
|
||||
})
|
||||
|
||||
const theTransforms = getTransformInfos(selectionRanges, ast, angleOrLength)
|
||||
setTransformInfos(theTransforms)
|
||||
|
||||
const _enableHorz = isAllTooltips && theTransforms.every(Boolean)
|
||||
setEnableAngLen(_enableHorz)
|
||||
setTransformInfos(transforms)
|
||||
setEnableAngLen(enabled)
|
||||
}, [guiMode, selectionRanges])
|
||||
if (guiMode.mode !== 'sketch') return null
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!(transformInfos && ast)) return
|
||||
if (!transformInfos) return
|
||||
const { valueUsedInTransform } = transformAstSketchLines({
|
||||
ast: JSON.parse(JSON.stringify(ast)),
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
try {
|
||||
@ -126,10 +112,10 @@ export const SetAngleLength = ({
|
||||
|
||||
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||
transformAstSketchLines({
|
||||
ast: JSON.parse(JSON.stringify(ast)),
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges,
|
||||
transformInfos,
|
||||
programMemory,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
@ -143,7 +129,7 @@ export const SetAngleLength = ({
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
|
||||
updateAst(_modifiedAst, true, {
|
||||
kclManager.updateAst(_modifiedAst, true, {
|
||||
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
} catch (e) {
|
||||
@ -157,3 +143,119 @@ export const SetAngleLength = ({
|
||||
</button>
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
||||
export function setAngleLengthInfo({
|
||||
selectionRanges,
|
||||
angleOrLength = 'setLength',
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
angleOrLength?: 'setLength' | 'setAngle'
|
||||
}) {
|
||||
const paths = selectionRanges.codeBasedSelections.map(({ range }) =>
|
||||
getNodePathFromSourceRange(kclManager.ast, range)
|
||||
)
|
||||
const nodes = paths.map(
|
||||
(pathToNode) =>
|
||||
getNodeFromPath<Value>(kclManager.ast, pathToNode, 'CallExpression').node
|
||||
)
|
||||
const isAllTooltips = nodes.every(
|
||||
(node) =>
|
||||
node?.type === 'CallExpression' &&
|
||||
toolTips.includes(node.callee.name as any)
|
||||
)
|
||||
|
||||
const transforms = getTransformInfos(
|
||||
selectionRanges,
|
||||
kclManager.ast,
|
||||
angleOrLength
|
||||
)
|
||||
const enabled = isAllTooltips && transforms.every(Boolean)
|
||||
return { enabled, transforms }
|
||||
}
|
||||
|
||||
export async function applyConstraintAngleLength({
|
||||
selectionRanges,
|
||||
angleOrLength = 'setLength',
|
||||
}: {
|
||||
selectionRanges: Selections
|
||||
angleOrLength?: 'setLength' | 'setAngle'
|
||||
}): Promise<{
|
||||
modifiedAst: Program
|
||||
pathToNodeMap: PathToNodeMap
|
||||
}> {
|
||||
const { transforms } = setAngleLengthInfo({ selectionRanges, angleOrLength })
|
||||
const { valueUsedInTransform } = transformAstSketchLines({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges,
|
||||
transformInfos: transforms,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
})
|
||||
try {
|
||||
const isReferencingYAxis =
|
||||
selectionRanges.otherSelections.length === 1 &&
|
||||
selectionRanges.otherSelections[0] === 'y-axis'
|
||||
const isReferencingYAxisAngle =
|
||||
isReferencingYAxis && angleOrLength === 'setAngle'
|
||||
|
||||
const isReferencingXAxis =
|
||||
selectionRanges.otherSelections.length === 1 &&
|
||||
selectionRanges.otherSelections[0] === 'x-axis'
|
||||
const isReferencingXAxisAngle =
|
||||
isReferencingXAxis && angleOrLength === 'setAngle'
|
||||
|
||||
let forceVal = valueUsedInTransform || 0
|
||||
let calcIdentifier = createIdentifier('_0')
|
||||
if (isReferencingYAxisAngle) {
|
||||
calcIdentifier = createIdentifier(forceVal < 0 ? '_270' : '_90')
|
||||
forceVal = normaliseAngle(forceVal + (forceVal < 0 ? 90 : -90))
|
||||
} else if (isReferencingXAxisAngle) {
|
||||
calcIdentifier = createIdentifier(Math.abs(forceVal) > 90 ? '_180' : '_0')
|
||||
forceVal =
|
||||
Math.abs(forceVal) > 90 ? normaliseAngle(forceVal - 180) : forceVal
|
||||
}
|
||||
const { valueNode, variableName, newVariableInsertIndex, sign } =
|
||||
await getModalInfo({
|
||||
value: forceVal,
|
||||
valueName: angleOrLength === 'setAngle' ? 'angle' : 'length',
|
||||
shouldCreateVariable: true,
|
||||
} as any)
|
||||
let finalValue = removeDoubleNegatives(valueNode, sign, variableName)
|
||||
if (
|
||||
isReferencingYAxisAngle ||
|
||||
(isReferencingXAxisAngle && calcIdentifier.name !== '_0')
|
||||
) {
|
||||
finalValue = createBinaryExpressionWithUnary([calcIdentifier, finalValue])
|
||||
}
|
||||
|
||||
const { modifiedAst: _modifiedAst, pathToNodeMap } =
|
||||
transformAstSketchLines({
|
||||
ast: JSON.parse(JSON.stringify(kclManager.ast)),
|
||||
selectionRanges,
|
||||
transformInfos: transforms,
|
||||
programMemory: kclManager.programMemory,
|
||||
referenceSegName: '',
|
||||
forceValueUsedInTransform: finalValue,
|
||||
})
|
||||
if (variableName) {
|
||||
const newBody = [..._modifiedAst.body]
|
||||
newBody.splice(
|
||||
newVariableInsertIndex,
|
||||
0,
|
||||
createVariableDeclaration(variableName, valueNode)
|
||||
)
|
||||
_modifiedAst.body = newBody
|
||||
}
|
||||
return {
|
||||
modifiedAst: _modifiedAst,
|
||||
pathToNodeMap,
|
||||
}
|
||||
// kclManager.updateAst(_modifiedAst, true, {
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
// })
|
||||
} catch (e) {
|
||||
console.log('erorr', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
@ -22,9 +22,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
const displayedName = getDisplayName(user)
|
||||
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
const {
|
||||
auth: { send },
|
||||
} = useGlobalStateContext()
|
||||
const send = useGlobalStateContext()?.auth?.send
|
||||
|
||||
// Fallback logic for displaying user's "name":
|
||||
// 1. user.name
|
||||
|
@ -1,8 +1,5 @@
|
||||
// all web app environment variables are defined here, jest doesn't like import.meta.env so centralising them here
|
||||
// allows us to mock them in one place, see src/setupTests.ts, it pulls the variable names and valuse from .env.development
|
||||
// note the exported variable name must match the env var name for the jest mocks to work
|
||||
// i.e. const VITE_MY_VAR = import.meta.env.VITE_MY_VAR
|
||||
// Maybe this file should be generated in a GHA from .env.development?
|
||||
// env vars were centralised so they could be mocked in jest
|
||||
// but isn't needed anymore with vite, so is now just a convention
|
||||
|
||||
export const VITE_KC_API_WS_MODELING_URL = import.meta.env
|
||||
.VITE_KC_API_WS_MODELING_URL
|
||||
@ -12,3 +9,4 @@ export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
|
||||
.VITE_KC_CONNECTION_TIMEOUT_MS
|
||||
export const VITE_KC_SENTRY_DSN = import.meta.env.VITE_KC_SENTRY_DSN
|
||||
export const TEST = import.meta.env.TEST
|
||||
export const DEV = import.meta.env.DEV
|
||||
|
@ -1,355 +0,0 @@
|
||||
// needed somewhere to dump this logic,
|
||||
// Once we have xState this should be removed
|
||||
|
||||
import { useStore, Selections } from 'useStore'
|
||||
import { useEffect } from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ArtifactMap, EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import { Models } from '@kittycad/lib/dist/types/src'
|
||||
import { isReducedMotion } from 'lang/util'
|
||||
import { isOverlap } from 'lib/utils'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { DefaultPlanes } from '../wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
import { getNodeFromPath } from '../lang/queryAst'
|
||||
import { CallExpression, PipeExpression } from '../lang/wasm'
|
||||
|
||||
export function useAppMode() {
|
||||
const {
|
||||
guiMode,
|
||||
setGuiMode,
|
||||
selectionRanges,
|
||||
selectionRangeTypeMap,
|
||||
defaultPlanes,
|
||||
setDefaultPlanes,
|
||||
setCurrentPlane,
|
||||
ast,
|
||||
} = useStore((s) => ({
|
||||
guiMode: s.guiMode,
|
||||
setGuiMode: s.setGuiMode,
|
||||
selectionRanges: s.selectionRanges,
|
||||
selectionRangeTypeMap: s.selectionRangeTypeMap,
|
||||
defaultPlanes: s.defaultPlanes,
|
||||
setDefaultPlanes: s.setDefaultPlanes,
|
||||
setCurrentPlane: s.setCurrentPlane,
|
||||
ast: s.ast,
|
||||
}))
|
||||
useEffect(() => {
|
||||
if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === 'selectFace' &&
|
||||
engineCommandManager
|
||||
) {
|
||||
const createAndShowPlanes = async () => {
|
||||
let localDefaultPlanes: DefaultPlanes
|
||||
if (!defaultPlanes) {
|
||||
const newDefaultPlanes = await initDefaultPlanes(engineCommandManager)
|
||||
if (!newDefaultPlanes) return
|
||||
setDefaultPlanes(newDefaultPlanes)
|
||||
localDefaultPlanes = newDefaultPlanes
|
||||
} else {
|
||||
localDefaultPlanes = defaultPlanes
|
||||
}
|
||||
setDefaultPlanesHidden(engineCommandManager, localDefaultPlanes, false)
|
||||
}
|
||||
createAndShowPlanes()
|
||||
}
|
||||
if (
|
||||
guiMode.mode === 'sketch' &&
|
||||
guiMode.sketchMode === 'enterSketchEdit' &&
|
||||
engineCommandManager
|
||||
) {
|
||||
const enableSketchMode = async () => {
|
||||
let localDefaultPlanes: DefaultPlanes
|
||||
if (!defaultPlanes) {
|
||||
const newDefaultPlanes = await initDefaultPlanes(engineCommandManager)
|
||||
if (!newDefaultPlanes) return
|
||||
setDefaultPlanes(newDefaultPlanes)
|
||||
localDefaultPlanes = newDefaultPlanes
|
||||
} else {
|
||||
localDefaultPlanes = defaultPlanes
|
||||
}
|
||||
setDefaultPlanesHidden(engineCommandManager, localDefaultPlanes, true)
|
||||
|
||||
const pipeExpression = getNodeFromPath<PipeExpression>(
|
||||
ast,
|
||||
guiMode.pathToNode,
|
||||
'PipeExpression'
|
||||
).node
|
||||
if (pipeExpression.type !== 'PipeExpression') return /// bad bad bad
|
||||
const sketchCallExpression = pipeExpression.body.find(
|
||||
(e) =>
|
||||
e.type === 'CallExpression' && e.callee.name === 'startSketchOn'
|
||||
) as CallExpression
|
||||
if (!sketchCallExpression) return // also bad bad bad
|
||||
const firstArg = sketchCallExpression.arguments[0]
|
||||
let planeId = ''
|
||||
if (firstArg.type === 'Literal' && firstArg.value) {
|
||||
const planeStrCleaned = firstArg.value
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace('-', '')
|
||||
if (
|
||||
planeStrCleaned === 'xy' ||
|
||||
planeStrCleaned === 'xz' ||
|
||||
planeStrCleaned === 'yz'
|
||||
) {
|
||||
planeId = localDefaultPlanes[planeStrCleaned]
|
||||
}
|
||||
}
|
||||
|
||||
if (!planeId) return // they are on some non default plane, which we don't support yet
|
||||
|
||||
setCurrentPlane(planeId)
|
||||
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'sketch_mode_enable',
|
||||
plane_id: planeId,
|
||||
ortho: true,
|
||||
animated: !isReducedMotion(),
|
||||
},
|
||||
})
|
||||
const proms: any[] = []
|
||||
proms.push(
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'edit_mode_enter',
|
||||
target: guiMode.pathId,
|
||||
},
|
||||
})
|
||||
)
|
||||
await Promise.all(proms)
|
||||
}
|
||||
enableSketchMode()
|
||||
setGuiMode({
|
||||
...guiMode,
|
||||
sketchMode: 'sketchEdit',
|
||||
})
|
||||
}
|
||||
if (guiMode.mode !== 'sketch' && defaultPlanes) {
|
||||
setDefaultPlanesHidden(engineCommandManager, defaultPlanes, true)
|
||||
}
|
||||
if (guiMode.mode === 'default') {
|
||||
const pathId =
|
||||
engineCommandManager &&
|
||||
isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
selectionRanges
|
||||
)
|
||||
if (pathId) {
|
||||
setGuiMode({
|
||||
mode: 'canEditSketch',
|
||||
rotation: [0, 0, 0, 1],
|
||||
position: [0, 0, 0],
|
||||
pathToNode: [],
|
||||
pathId,
|
||||
})
|
||||
}
|
||||
} else if (guiMode.mode === 'canEditSketch') {
|
||||
if (
|
||||
!engineCommandManager ||
|
||||
!isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
selectionRanges
|
||||
)
|
||||
) {
|
||||
setGuiMode({
|
||||
mode: 'default',
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [
|
||||
guiMode,
|
||||
guiMode.mode,
|
||||
engineCommandManager,
|
||||
selectionRanges,
|
||||
selectionRangeTypeMap,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const unSub = engineCommandManager.subscribeTo({
|
||||
event: 'select_with_point',
|
||||
callback: async ({ data }) => {
|
||||
if (!data.entity_id) return
|
||||
if (!defaultPlanes) return
|
||||
if (!Object.values(defaultPlanes || {}).includes(data.entity_id)) {
|
||||
// user clicked something else in the scene
|
||||
return
|
||||
}
|
||||
setCurrentPlane(data.entity_id)
|
||||
const sketchModeResponse = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'sketch_mode_enable',
|
||||
plane_id: data.entity_id,
|
||||
ortho: true,
|
||||
animated: !isReducedMotion(),
|
||||
},
|
||||
})
|
||||
setDefaultPlanesHidden(engineCommandManager, defaultPlanes, true)
|
||||
const sketchUuid = uuidv4()
|
||||
const proms: any[] = []
|
||||
proms.push(
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: sketchUuid,
|
||||
cmd: {
|
||||
type: 'start_path',
|
||||
},
|
||||
})
|
||||
)
|
||||
proms.push(
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'edit_mode_enter',
|
||||
target: sketchUuid,
|
||||
},
|
||||
})
|
||||
)
|
||||
await Promise.all(proms)
|
||||
setGuiMode({
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
rotation: [0, 0, 0, 1],
|
||||
position: [0, 0, 0],
|
||||
pathToNode: [],
|
||||
pathId: sketchUuid,
|
||||
})
|
||||
|
||||
console.log('sketchModeResponse', sketchModeResponse)
|
||||
},
|
||||
})
|
||||
return unSub
|
||||
}, [engineCommandManager, defaultPlanes])
|
||||
}
|
||||
|
||||
async function createPlane(
|
||||
engineCommandManager: EngineCommandManager,
|
||||
{
|
||||
x_axis,
|
||||
y_axis,
|
||||
color,
|
||||
hidden,
|
||||
}: {
|
||||
x_axis: Models['Point3d_type']
|
||||
y_axis: Models['Point3d_type']
|
||||
color: Models['Color_type']
|
||||
hidden: boolean
|
||||
}
|
||||
) {
|
||||
const planeId = uuidv4()
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'make_plane',
|
||||
size: 60,
|
||||
origin: { x: 0, y: 0, z: 0 },
|
||||
x_axis,
|
||||
y_axis,
|
||||
clobber: false,
|
||||
hide: hidden,
|
||||
},
|
||||
cmd_id: planeId,
|
||||
})
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'plane_set_color',
|
||||
plane_id: planeId,
|
||||
color,
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
return planeId
|
||||
}
|
||||
|
||||
export function setDefaultPlanesHidden(
|
||||
engineCommandManager: EngineCommandManager,
|
||||
defaultPlanes: DefaultPlanes,
|
||||
hidden: boolean
|
||||
) {
|
||||
Object.values(defaultPlanes).forEach((planeId) => {
|
||||
hidePlane(engineCommandManager, planeId, hidden)
|
||||
})
|
||||
}
|
||||
|
||||
function hidePlane(
|
||||
engineCommandManager: EngineCommandManager,
|
||||
planeId: string,
|
||||
hidden: boolean
|
||||
) {
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'object_visible',
|
||||
object_id: planeId,
|
||||
hidden: hidden,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function initDefaultPlanes(
|
||||
engineCommandManager: EngineCommandManager,
|
||||
hidePlanes?: boolean
|
||||
): Promise<DefaultPlanes | null> {
|
||||
if (!engineCommandManager.engineConnection?.isReady()) {
|
||||
return null
|
||||
}
|
||||
const xy = await createPlane(engineCommandManager, {
|
||||
x_axis: { x: 1, y: 0, z: 0 },
|
||||
y_axis: { x: 0, y: 1, z: 0 },
|
||||
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
|
||||
hidden: hidePlanes ? true : false,
|
||||
})
|
||||
if (hidePlanes) {
|
||||
hidePlane(engineCommandManager, xy, true)
|
||||
}
|
||||
const yz = await createPlane(engineCommandManager, {
|
||||
x_axis: { x: 0, y: 1, z: 0 },
|
||||
y_axis: { x: 0, y: 0, z: 1 },
|
||||
color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
|
||||
hidden: hidePlanes ? true : false,
|
||||
})
|
||||
if (hidePlanes) {
|
||||
hidePlane(engineCommandManager, yz, true)
|
||||
}
|
||||
const xz = await createPlane(engineCommandManager, {
|
||||
x_axis: { x: 1, y: 0, z: 0 },
|
||||
y_axis: { x: 0, y: 0, z: 1 },
|
||||
color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
|
||||
hidden: hidePlanes ? true : false,
|
||||
})
|
||||
return { xy, yz, xz }
|
||||
}
|
||||
|
||||
function isCursorInSketchCommandRange(
|
||||
artifactMap: ArtifactMap,
|
||||
selectionRanges: Selections
|
||||
): string | false {
|
||||
const overlapingEntries: [string, ArtifactMap[string]][] = Object.entries(
|
||||
artifactMap
|
||||
).filter(([id, artifact]: [string, ArtifactMap[string]]) =>
|
||||
selectionRanges.codeBasedSelections.some(
|
||||
(selection) =>
|
||||
Array.isArray(selection?.range) &&
|
||||
Array.isArray(artifact?.range) &&
|
||||
isOverlap(selection.range, artifact.range) &&
|
||||
(artifact.commandType === 'start_path' ||
|
||||
artifact.commandType === 'extend_path' ||
|
||||
artifact.commandType === 'close_path')
|
||||
)
|
||||
)
|
||||
return overlapingEntries.length && overlapingEntries[0][1].parentId
|
||||
? overlapingEntries[0][1].parentId
|
||||
: overlapingEntries.find(
|
||||
([, artifact]) => artifact.commandType === 'start_path'
|
||||
)?.[0] || false
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useStore } from 'useStore'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { useModelingContext } from './useModelingContext'
|
||||
|
||||
export function useEngineConnectionSubscriptions() {
|
||||
const { setCursor2, setHighlightRange, highlightRange } = useStore((s) => ({
|
||||
setCursor2: s.setCursor2,
|
||||
const { setHighlightRange, highlightRange } = useStore((s) => ({
|
||||
setHighlightRange: s.setHighlightRange,
|
||||
highlightRange: s.highlightRange,
|
||||
}))
|
||||
const { send } = useModelingContext()
|
||||
useEffect(() => {
|
||||
if (!engineCommandManager) return
|
||||
|
||||
@ -30,16 +31,25 @@ export function useEngineConnectionSubscriptions() {
|
||||
event: 'select_with_point',
|
||||
callback: ({ data }) => {
|
||||
if (!data?.entity_id) {
|
||||
setCursor2()
|
||||
send({
|
||||
type: 'Set selection',
|
||||
data: { selectionType: 'singleCodeCursor' },
|
||||
})
|
||||
return
|
||||
}
|
||||
const sourceRange = engineCommandManager.sourceRangeMap[data.entity_id]
|
||||
setCursor2({ range: sourceRange, type: 'default' })
|
||||
send({
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'singleCodeCursor',
|
||||
selection: { range: sourceRange, type: 'default' },
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
return () => {
|
||||
unSubHover()
|
||||
unSubClick()
|
||||
}
|
||||
}, [engineCommandManager, setCursor2, setHighlightRange, highlightRange])
|
||||
}, [engineCommandManager, setHighlightRange, highlightRange])
|
||||
}
|
||||
|
6
src/hooks/useModelingContext.ts
Normal file
6
src/hooks/useModelingContext.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { ModelingMachineContext } from 'components/ModelingMachineProvider'
|
||||
import { useContext } from 'react'
|
||||
|
||||
export const useModelingContext = () => {
|
||||
return useContext(ModelingMachineContext)
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import { useLayoutEffect, useEffect, useRef } from 'react'
|
||||
import { _executor } from '../lang/wasm'
|
||||
import { _executor, parse } from '../lang/wasm'
|
||||
import { useStore } from '../useStore'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { deferExecution } from 'lib/utils'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
export function useSetupEngineManager(
|
||||
streamRef: React.RefObject<HTMLDivElement>,
|
||||
@ -13,13 +14,11 @@ export function useSetupEngineManager(
|
||||
setIsStreamReady,
|
||||
setStreamDimensions,
|
||||
streamDimensions,
|
||||
executeCode,
|
||||
} = useStore((s) => ({
|
||||
setMediaStream: s.setMediaStream,
|
||||
setIsStreamReady: s.setIsStreamReady,
|
||||
setStreamDimensions: s.setStreamDimensions,
|
||||
streamDimensions: s.streamDimensions,
|
||||
executeCode: s.executeCode,
|
||||
}))
|
||||
|
||||
const streamWidth = streamRef?.current?.offsetWidth
|
||||
@ -28,7 +27,7 @@ export function useSetupEngineManager(
|
||||
const hasSetNonZeroDimensions = useRef<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
executeCode()
|
||||
kclManager.executeCode()
|
||||
}, [])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@ -44,7 +43,10 @@ export function useSetupEngineManager(
|
||||
setIsStreamReady,
|
||||
width: quadWidth,
|
||||
height: quadHeight,
|
||||
executeCode,
|
||||
executeCode: (code?: string) => {
|
||||
const _ast = parse(code || kclManager.code)
|
||||
return kclManager.executeAst(_ast, true)
|
||||
},
|
||||
token,
|
||||
})
|
||||
setStreamDimensions({
|
||||
|
@ -1,52 +1,43 @@
|
||||
import { SetVarNameModal } from 'components/SetVarNameModal'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
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'
|
||||
import { useModelingContext } from './useModelingContext'
|
||||
|
||||
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 { context } = useModelingContext()
|
||||
const [enable, setEnabled] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!ast) return
|
||||
|
||||
const { isSafe, value } = isNodeSafeToReplace(
|
||||
ast,
|
||||
selectionRanges.codeBasedSelections?.[0]?.range || []
|
||||
kclManager.ast,
|
||||
context.selectionRanges.codeBasedSelections?.[0]?.range || []
|
||||
)
|
||||
const canReplace = isSafe && value.type !== 'Identifier'
|
||||
const isOnlyOneSelection = selectionRanges.codeBasedSelections.length === 1
|
||||
const isOnlyOneSelection =
|
||||
context.selectionRanges.codeBasedSelections.length === 1
|
||||
|
||||
const _enableHorz = canReplace && isOnlyOneSelection
|
||||
setEnabled(_enableHorz)
|
||||
}, [guiMode, selectionRanges])
|
||||
}, [context.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,
|
||||
kclManager.ast,
|
||||
kclManager.programMemory,
|
||||
context.selectionRanges.codeBasedSelections[0].range,
|
||||
variableName
|
||||
)
|
||||
|
||||
updateAst(_modifiedAst, true)
|
||||
kclManager.updateAst(_modifiedAst, true)
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
}
|
||||
|
@ -4,6 +4,13 @@ import reportWebVitals from './reportWebVitals'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { Router } from './Router'
|
||||
import { HotkeysProvider } from 'react-hotkeys-hook'
|
||||
import { inspect } from '@xstate/inspect'
|
||||
import { DEV } from 'env'
|
||||
|
||||
if (DEV)
|
||||
inspect({
|
||||
iframe: false,
|
||||
})
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||
|
||||
|
347
src/lang/KclSinglton.tsx
Normal file
347
src/lang/KclSinglton.tsx
Normal file
@ -0,0 +1,347 @@
|
||||
import { Selections, executeAst, executeCode } from 'useStore'
|
||||
import { KCLError } from './errors'
|
||||
import {
|
||||
EngineCommandManager,
|
||||
engineCommandManager,
|
||||
} from './std/engineConnection'
|
||||
import { deferExecution } from 'lib/utils'
|
||||
import {
|
||||
initPromise,
|
||||
parse,
|
||||
PathToNode,
|
||||
Program,
|
||||
ProgramMemory,
|
||||
recast,
|
||||
} from 'lang/wasm'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { getNodeFromPath } from './queryAst'
|
||||
|
||||
const PERSIST_CODE_TOKEN = 'persistCode'
|
||||
|
||||
class KclManager {
|
||||
private _code = bracket
|
||||
private _ast: Program = {
|
||||
body: [],
|
||||
start: 0,
|
||||
end: 0,
|
||||
nonCodeMeta: {
|
||||
nonCodeNodes: {},
|
||||
start: null,
|
||||
},
|
||||
}
|
||||
private _programMemory: ProgramMemory = {
|
||||
root: {},
|
||||
return: null,
|
||||
}
|
||||
private _logs: string[] = []
|
||||
private _kclErrors: KCLError[] = []
|
||||
private _isExecuting = false
|
||||
|
||||
engineCommandManager: EngineCommandManager
|
||||
private _defferer = deferExecution((code: string) => {
|
||||
const ast = parse(code)
|
||||
this.executeAst(ast)
|
||||
}, 600)
|
||||
|
||||
private _isExecutingCallback: (a: boolean) => void = () => {}
|
||||
private _codeCallBack: (arg: string) => void = () => {}
|
||||
private _astCallBack: (arg: Program) => void = () => {}
|
||||
private _programMemoryCallBack: (arg: ProgramMemory) => void = () => {}
|
||||
private _logsCallBack: (arg: string[]) => void = () => {}
|
||||
private _kclErrorsCallBack: (arg: KCLError[]) => void = () => {}
|
||||
|
||||
get ast() {
|
||||
return this._ast
|
||||
}
|
||||
set ast(ast) {
|
||||
this._ast = ast
|
||||
this._astCallBack(ast)
|
||||
}
|
||||
|
||||
get code() {
|
||||
return this._code
|
||||
}
|
||||
set code(code) {
|
||||
this._code = code
|
||||
this._codeCallBack(code)
|
||||
}
|
||||
|
||||
get programMemory() {
|
||||
return this._programMemory
|
||||
}
|
||||
set programMemory(programMemory) {
|
||||
this._programMemory = programMemory
|
||||
this._programMemoryCallBack(programMemory)
|
||||
}
|
||||
|
||||
get defaultPlanes() {
|
||||
return this?.engineCommandManager?.defaultPlanes
|
||||
}
|
||||
|
||||
get logs() {
|
||||
return this._logs
|
||||
}
|
||||
set logs(logs) {
|
||||
this._logs = logs
|
||||
this._logsCallBack(logs)
|
||||
}
|
||||
|
||||
get kclErrors() {
|
||||
return this._kclErrors
|
||||
}
|
||||
set kclErrors(kclErrors) {
|
||||
this._kclErrors = kclErrors
|
||||
this._kclErrorsCallBack(kclErrors)
|
||||
}
|
||||
|
||||
get isExecuting() {
|
||||
return this._isExecuting
|
||||
}
|
||||
set isExecuting(isExecuting) {
|
||||
this._isExecuting = isExecuting
|
||||
this._isExecutingCallback(isExecuting)
|
||||
}
|
||||
|
||||
constructor(engineCommandManager: EngineCommandManager) {
|
||||
this.engineCommandManager = engineCommandManager
|
||||
const storedCode = localStorage.getItem(PERSIST_CODE_TOKEN)
|
||||
// TODO #819 remove zustand persistance logic in a few months
|
||||
// short term migration, shouldn't make a difference for tauri app users
|
||||
// anyway since that's filesystem based.
|
||||
const zustandStore = JSON.parse(localStorage.getItem('store') || '{}')
|
||||
if (storedCode === null && zustandStore?.state?.code) {
|
||||
this.code = zustandStore.state.code
|
||||
localStorage.setItem(PERSIST_CODE_TOKEN, this._code)
|
||||
zustandStore.state.code = ''
|
||||
localStorage.setItem('store', JSON.stringify(zustandStore))
|
||||
} else if (storedCode === null) {
|
||||
this.code = bracket
|
||||
} else {
|
||||
this.code = storedCode
|
||||
}
|
||||
}
|
||||
registerCallBacks({
|
||||
setCode,
|
||||
setProgramMemory,
|
||||
setAst,
|
||||
setLogs,
|
||||
setKclErrors,
|
||||
setIsExecuting,
|
||||
}: {
|
||||
setCode: (arg: string) => void
|
||||
setProgramMemory: (arg: ProgramMemory) => void
|
||||
setAst: (arg: Program) => void
|
||||
setLogs: (arg: string[]) => void
|
||||
setKclErrors: (arg: KCLError[]) => void
|
||||
setIsExecuting: (arg: boolean) => void
|
||||
}) {
|
||||
this._codeCallBack = setCode
|
||||
this._programMemoryCallBack = setProgramMemory
|
||||
this._astCallBack = setAst
|
||||
this._logsCallBack = setLogs
|
||||
this._kclErrorsCallBack = setKclErrors
|
||||
this._isExecutingCallback = setIsExecuting
|
||||
}
|
||||
|
||||
async executeAst(ast: Program = this._ast, updateCode = false) {
|
||||
this.isExecuting = true
|
||||
await initPromise
|
||||
const { logs, errors, programMemory } = await executeAst({
|
||||
ast,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
defaultPlanes: this.defaultPlanes,
|
||||
})
|
||||
this.isExecuting = false
|
||||
this._logs = logs
|
||||
this._kclErrors = errors
|
||||
this._programMemory = programMemory
|
||||
this._ast = { ...ast }
|
||||
if (updateCode) {
|
||||
this._code = recast(ast)
|
||||
this._codeCallBack(this._code)
|
||||
}
|
||||
}
|
||||
async executeAstMock(ast: Program = this._ast, updateCode = false) {
|
||||
await initPromise
|
||||
const newCode = recast(ast)
|
||||
const newAst = parse(newCode)
|
||||
await this?.engineCommandManager?.waitForReady
|
||||
if (updateCode) {
|
||||
this.setCode(recast(ast))
|
||||
}
|
||||
this._ast = { ...newAst }
|
||||
|
||||
const { logs, errors, programMemory } = await executeAst({
|
||||
ast: newAst,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
defaultPlanes: this.defaultPlanes,
|
||||
useFakeExecutor: true,
|
||||
})
|
||||
this._logs = logs
|
||||
this._kclErrors = errors
|
||||
this._programMemory = programMemory
|
||||
}
|
||||
async executeCode(code?: string) {
|
||||
await initPromise
|
||||
await this?.engineCommandManager?.waitForReady
|
||||
const result = await executeCode({
|
||||
engineCommandManager,
|
||||
code: code || this._code,
|
||||
lastAst: this._ast,
|
||||
defaultPlanes: this.defaultPlanes,
|
||||
force: false,
|
||||
})
|
||||
if (!result.isChange) return
|
||||
const { logs, errors, programMemory, ast } = result
|
||||
this.logs = logs
|
||||
this.kclErrors = errors
|
||||
this.programMemory = programMemory
|
||||
this.ast = ast
|
||||
if (code) this.code = code
|
||||
}
|
||||
setCode(code: string) {
|
||||
this._code = code
|
||||
this._codeCallBack(code)
|
||||
localStorage.setItem(PERSIST_CODE_TOKEN, code)
|
||||
}
|
||||
setCodeAndExecute(code: string) {
|
||||
this.setCode(code)
|
||||
if (code.trim()) {
|
||||
this._defferer(code)
|
||||
return
|
||||
}
|
||||
this._ast = {
|
||||
body: [],
|
||||
start: 0,
|
||||
end: 0,
|
||||
nonCodeMeta: {
|
||||
nonCodeNodes: {},
|
||||
start: null,
|
||||
},
|
||||
}
|
||||
this._programMemory = {
|
||||
root: {},
|
||||
return: null,
|
||||
}
|
||||
this.engineCommandManager.endSession()
|
||||
}
|
||||
format() {
|
||||
this.code = recast(parse(kclManager.code))
|
||||
}
|
||||
// There's overlapping resposibility between updateAst and executeAst.
|
||||
// updateAst was added as it was used a lot before xState migration so makes the port easier.
|
||||
// but should probably have think about which of the function to keep
|
||||
async updateAst(
|
||||
ast: Program,
|
||||
execute: boolean,
|
||||
optionalParams?: {
|
||||
focusPath?: PathToNode
|
||||
callBack?: (ast: Program) => void
|
||||
}
|
||||
): Promise<Selections | null> {
|
||||
const newCode = recast(ast)
|
||||
const astWithUpdatedSource = parse(newCode)
|
||||
optionalParams?.callBack?.(astWithUpdatedSource)
|
||||
let returnVal: Selections | null = null
|
||||
|
||||
this.code = newCode
|
||||
if (optionalParams?.focusPath) {
|
||||
const { node } = getNodeFromPath<any>(
|
||||
astWithUpdatedSource,
|
||||
optionalParams?.focusPath
|
||||
)
|
||||
const { start, end } = node
|
||||
if (!start || !end) return null
|
||||
returnVal = {
|
||||
codeBasedSelections: [
|
||||
{
|
||||
type: 'default',
|
||||
range: [start, end],
|
||||
},
|
||||
],
|
||||
otherSelections: [],
|
||||
}
|
||||
}
|
||||
|
||||
if (execute) {
|
||||
// Call execute on the set ast.
|
||||
await this.executeAst(astWithUpdatedSource)
|
||||
} else {
|
||||
// When we don't re-execute, we still want to update the program
|
||||
// memory with the new ast. So we will hit the mock executor
|
||||
// instead.
|
||||
await this.executeAstMock(astWithUpdatedSource)
|
||||
}
|
||||
return returnVal
|
||||
}
|
||||
|
||||
getPlaneId(axis: 'xy' | 'xz' | 'yz'): string {
|
||||
return this.defaultPlanes[axis]
|
||||
}
|
||||
|
||||
showPlanes() {
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, false)
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, false)
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, false)
|
||||
}
|
||||
|
||||
hidePlanes() {
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xy, true)
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.yz, true)
|
||||
this.engineCommandManager.setPlaneHidden(this.defaultPlanes.xz, true)
|
||||
}
|
||||
}
|
||||
|
||||
export const kclManager = new KclManager(engineCommandManager)
|
||||
|
||||
const KclContext = createContext({
|
||||
code: kclManager.code,
|
||||
programMemory: kclManager.programMemory,
|
||||
ast: kclManager.ast,
|
||||
isExecuting: kclManager.isExecuting,
|
||||
errors: kclManager.kclErrors,
|
||||
logs: kclManager.logs,
|
||||
})
|
||||
|
||||
export function useKclContext() {
|
||||
return useContext(KclContext)
|
||||
}
|
||||
|
||||
export function KclContextProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [code, setCode] = useState(kclManager.code)
|
||||
const [programMemory, setProgramMemory] = useState(kclManager.programMemory)
|
||||
const [ast, setAst] = useState(kclManager.ast)
|
||||
const [isExecuting, setIsExecuting] = useState(false)
|
||||
const [errors, setErrors] = useState<KCLError[]>([])
|
||||
const [logs, setLogs] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
kclManager.registerCallBacks({
|
||||
setCode,
|
||||
setProgramMemory,
|
||||
setAst,
|
||||
setLogs,
|
||||
setKclErrors: setErrors,
|
||||
setIsExecuting,
|
||||
})
|
||||
}, [])
|
||||
return (
|
||||
<KclContext.Provider
|
||||
value={{
|
||||
code,
|
||||
programMemory,
|
||||
ast,
|
||||
isExecuting,
|
||||
errors,
|
||||
logs,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</KclContext.Provider>
|
||||
)
|
||||
}
|
@ -1520,15 +1520,15 @@ const key = 'c'`
|
||||
},
|
||||
}
|
||||
const { nonCodeMeta } = parse(code)
|
||||
expect(nonCodeMeta.nonCodeNodes[0]).toEqual(nonCodeMetaInstance)
|
||||
expect(nonCodeMeta.nonCodeNodes[0][0]).toEqual(nonCodeMetaInstance)
|
||||
|
||||
// extra whitespace won't change it's position (0) or value (NB the start end would have changed though)
|
||||
const codeWithExtraStartWhitespace = '\n\n\n' + code
|
||||
const { nonCodeMeta: nonCodeMeta2 } = parse(codeWithExtraStartWhitespace)
|
||||
expect(nonCodeMeta2.nonCodeNodes[0].value).toStrictEqual(
|
||||
expect(nonCodeMeta2.nonCodeNodes[0][0].value).toStrictEqual(
|
||||
nonCodeMetaInstance.value
|
||||
)
|
||||
expect(nonCodeMeta2.nonCodeNodes[0].start).not.toBe(
|
||||
expect(nonCodeMeta2.nonCodeNodes[0][0].start).not.toBe(
|
||||
nonCodeMetaInstance.start
|
||||
)
|
||||
})
|
||||
@ -1545,7 +1545,7 @@ const key = 'c'`
|
||||
const { body } = parse(code)
|
||||
const indexOfSecondLineToExpression = 2
|
||||
const sketchNonCodeMeta = (body as any)[0].declarations[0].init.nonCodeMeta
|
||||
.nonCodeNodes
|
||||
.nonCodeNodes[0]
|
||||
expect(sketchNonCodeMeta[indexOfSecondLineToExpression]).toEqual({
|
||||
type: 'NonCodeNode',
|
||||
start: 106,
|
||||
@ -1568,7 +1568,7 @@ const key = 'c'`
|
||||
|
||||
const { body } = parse(code)
|
||||
const sketchNonCodeMeta = (body[0] as any).declarations[0].init.nonCodeMeta
|
||||
.nonCodeNodes
|
||||
.nonCodeNodes[0]
|
||||
expect(sketchNonCodeMeta[3]).toEqual({
|
||||
type: 'NonCodeNode',
|
||||
start: 125,
|
||||
|
@ -72,7 +72,7 @@ export class KCLUndefinedValueError extends KCLError {
|
||||
* Currently the diagnostics are all errors, but in the future they could include lints.
|
||||
* */
|
||||
export function kclErrToDiagnostic(errors: KCLError[]): Diagnostic[] {
|
||||
return errors.flatMap((err) => {
|
||||
return errors?.flatMap((err) => {
|
||||
return err.sourceRanges.map(([from, to]) => {
|
||||
return { from, to, message: err.msg, severity: 'error' }
|
||||
})
|
||||
|
@ -4,6 +4,8 @@ import {
|
||||
isNodeSafeToReplace,
|
||||
isTypeInValue,
|
||||
getNodePathFromSourceRange,
|
||||
doesPipeHaveCallExp,
|
||||
hasExtrudeSketchGroup,
|
||||
} from './queryAst'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
import {
|
||||
@ -246,3 +248,114 @@ show(part001)`
|
||||
expect(selectWholeThing).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing doesPipeHave', () => {
|
||||
it('finds close', () => {
|
||||
const exampleCode = `const length001 = 2
|
||||
const part001 = startSketchAt([-1.41, 3.46])
|
||||
|> line({ to: [19.49, 1.16], tag: 'seg01' }, %)
|
||||
|> angledLine([-35, length001], %)
|
||||
|> line([-3.22, -7.36], %)
|
||||
|> angledLine([-175, segLen('seg01', %)], %)
|
||||
|> close(%)
|
||||
`
|
||||
const ast = parse(exampleCode)
|
||||
const result = doesPipeHaveCallExp({
|
||||
calleeName: 'close',
|
||||
ast,
|
||||
selection: { type: 'default', range: [100, 101] },
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it('finds extrude', () => {
|
||||
const exampleCode = `const length001 = 2
|
||||
const part001 = startSketchAt([-1.41, 3.46])
|
||||
|> line({ to: [19.49, 1.16], tag: 'seg01' }, %)
|
||||
|> angledLine([-35, length001], %)
|
||||
|> line([-3.22, -7.36], %)
|
||||
|> angledLine([-175, segLen('seg01', %)], %)
|
||||
|> close(%)
|
||||
|> extrude(1, %)
|
||||
`
|
||||
const ast = parse(exampleCode)
|
||||
const result = doesPipeHaveCallExp({
|
||||
calleeName: 'extrude',
|
||||
ast,
|
||||
selection: { type: 'default', range: [100, 101] },
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it('does NOT find close', () => {
|
||||
const exampleCode = `const length001 = 2
|
||||
const part001 = startSketchAt([-1.41, 3.46])
|
||||
|> line({ to: [19.49, 1.16], tag: 'seg01' }, %)
|
||||
|> angledLine([-35, length001], %)
|
||||
|> line([-3.22, -7.36], %)
|
||||
|> angledLine([-175, segLen('seg01', %)], %)
|
||||
`
|
||||
const ast = parse(exampleCode)
|
||||
const result = doesPipeHaveCallExp({
|
||||
calleeName: 'close',
|
||||
ast,
|
||||
selection: { type: 'default', range: [100, 101] },
|
||||
})
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns false if not a pipe', () => {
|
||||
const exampleCode = `const length001 = 2`
|
||||
const ast = parse(exampleCode)
|
||||
const result = doesPipeHaveCallExp({
|
||||
calleeName: 'close',
|
||||
ast,
|
||||
selection: { type: 'default', range: [9, 10] },
|
||||
})
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing hasExtrudeSketchGroup', () => {
|
||||
it('find sketch group', async () => {
|
||||
const exampleCode = `const length001 = 2
|
||||
const part001 = startSketchAt([-1.41, 3.46])
|
||||
|> line({ to: [19.49, 1.16], tag: 'seg01' }, %)
|
||||
|> angledLine([-35, length001], %)
|
||||
|> line([-3.22, -7.36], %)
|
||||
|> angledLine([-175, segLen('seg01', %)], %)`
|
||||
const ast = parse(exampleCode)
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const result = hasExtrudeSketchGroup({
|
||||
ast,
|
||||
selection: { type: 'default', range: [100, 101] },
|
||||
programMemory,
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it('find extrude group', async () => {
|
||||
const exampleCode = `const length001 = 2
|
||||
const part001 = startSketchAt([-1.41, 3.46])
|
||||
|> line({ to: [19.49, 1.16], tag: 'seg01' }, %)
|
||||
|> angledLine([-35, length001], %)
|
||||
|> line([-3.22, -7.36], %)
|
||||
|> angledLine([-175, segLen('seg01', %)], %)
|
||||
|> extrude(1, %)`
|
||||
const ast = parse(exampleCode)
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const result = hasExtrudeSketchGroup({
|
||||
ast,
|
||||
selection: { type: 'default', range: [100, 101] },
|
||||
programMemory,
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it('finds nothing', async () => {
|
||||
const exampleCode = `const length001 = 2`
|
||||
const ast = parse(exampleCode)
|
||||
const programMemory = await enginelessExecutor(ast)
|
||||
const result = hasExtrudeSketchGroup({
|
||||
ast,
|
||||
selection: { type: 'default', range: [10, 11] },
|
||||
programMemory,
|
||||
})
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
ProgramMemory,
|
||||
SketchGroup,
|
||||
SourceRange,
|
||||
PipeExpression,
|
||||
} from './wasm'
|
||||
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
|
||||
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
|
||||
@ -511,3 +512,47 @@ export function isLinesParallelAndConstrained(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function doesPipeHaveCallExp({
|
||||
ast,
|
||||
selection,
|
||||
calleeName,
|
||||
}: {
|
||||
calleeName: string
|
||||
ast: Program
|
||||
selection: Selection
|
||||
}): boolean {
|
||||
const pathToNode = getNodePathFromSourceRange(ast, selection.range)
|
||||
const pipeExpression = getNodeFromPath<PipeExpression>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
).node
|
||||
if (pipeExpression.type !== 'PipeExpression') return false
|
||||
return pipeExpression.body.some(
|
||||
(expression) =>
|
||||
expression.type === 'CallExpression' &&
|
||||
expression.callee.name === calleeName
|
||||
)
|
||||
}
|
||||
|
||||
export function hasExtrudeSketchGroup({
|
||||
ast,
|
||||
selection,
|
||||
programMemory,
|
||||
}: {
|
||||
ast: Program
|
||||
selection: Selection
|
||||
programMemory: ProgramMemory
|
||||
}): boolean {
|
||||
const pathToNode = getNodePathFromSourceRange(ast, selection.range)
|
||||
const varDec = getNodeFromPath<VariableDeclaration>(
|
||||
ast,
|
||||
pathToNode,
|
||||
'VariableDeclaration'
|
||||
).node
|
||||
if (varDec.type !== 'VariableDeclaration') return false
|
||||
const varName = varDec.declarations[0].id.name
|
||||
const varValue = programMemory?.root[varName]
|
||||
return varValue?.type === 'ExtrudeGroup' || varValue?.type === 'SketchGroup'
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import { exportSave } from 'lib/exportSave'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
|
||||
let lastMessage = ''
|
||||
|
||||
@ -286,14 +287,18 @@ export class EngineConnection {
|
||||
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
|
||||
|
||||
if (!message.success) {
|
||||
const errorsString = message?.errors
|
||||
?.map((error) => {
|
||||
return ` - ${error.error_code}: ${error.message}`
|
||||
})
|
||||
.join('\n')
|
||||
if (message.request_id) {
|
||||
console.error(`Error in response to request ${message.request_id}:`)
|
||||
console.error(
|
||||
`Error in response to request ${message.request_id}:\n${errorsString}`
|
||||
)
|
||||
} else {
|
||||
console.error(`Error from server:`)
|
||||
console.error(`Error from server:\n${errorsString}`)
|
||||
}
|
||||
message?.errors?.forEach((error) => {
|
||||
console.error(` - ${error.error_code}: ${error.message}`)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@ -593,11 +598,14 @@ export class EngineCommandManager {
|
||||
outSequence = 1
|
||||
inSequence = 1
|
||||
engineConnection?: EngineConnection
|
||||
defaultPlanes: DefaultPlanes = { xy: '', yz: '', xz: '' }
|
||||
// Folks should realize that wait for ready does not get called _everytime_
|
||||
// the connection resets and restarts, it only gets called the first time.
|
||||
// Be careful what you put here.
|
||||
waitForReady: Promise<void> = new Promise(() => {})
|
||||
private resolveReady = () => {}
|
||||
waitForReady: Promise<void> = new Promise((resolve) => {
|
||||
this.resolveReady = resolve
|
||||
})
|
||||
|
||||
subscriptions: {
|
||||
[event: string]: {
|
||||
@ -639,9 +647,6 @@ export class EngineCommandManager {
|
||||
return
|
||||
}
|
||||
|
||||
this.waitForReady = new Promise((resolve) => {
|
||||
this.resolveReady = resolve
|
||||
})
|
||||
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}`
|
||||
this.engineConnection = new EngineConnection({
|
||||
url,
|
||||
@ -669,12 +674,15 @@ export class EngineCommandManager {
|
||||
},
|
||||
})
|
||||
|
||||
// We execute the code here to make sure if the stream was to
|
||||
// restart in a session, we want to make sure to execute the code.
|
||||
// We force it to re-execute the code because we want to make sure
|
||||
// the code is executed everytime the stream is restarted.
|
||||
// We pass undefined for the code so it reads from the current state.
|
||||
executeCode(undefined, true)
|
||||
// Inisialize the planes.
|
||||
this.initPlanes().then(() => {
|
||||
// We execute the code here to make sure if the stream was to
|
||||
// restart in a session, we want to make sure to execute the code.
|
||||
// We force it to re-execute the code because we want to make sure
|
||||
// the code is executed everytime the stream is restarted.
|
||||
// We pass undefined for the code so it reads from the current state.
|
||||
executeCode(undefined, true)
|
||||
})
|
||||
},
|
||||
onClose: () => {
|
||||
setIsStreamReady(false)
|
||||
@ -1177,6 +1185,95 @@ export class EngineCommandManager {
|
||||
}
|
||||
})
|
||||
}
|
||||
private async initPlanes() {
|
||||
const [xy, yz, xz] = [
|
||||
await this.createPlane({
|
||||
x_axis: { x: 1, y: 0, z: 0 },
|
||||
y_axis: { x: 0, y: 1, z: 0 },
|
||||
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
|
||||
}),
|
||||
await this.createPlane({
|
||||
x_axis: { x: 0, y: 1, z: 0 },
|
||||
y_axis: { x: 0, y: 0, z: 1 },
|
||||
color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
|
||||
}),
|
||||
await this.createPlane({
|
||||
x_axis: { x: 1, y: 0, z: 0 },
|
||||
y_axis: { x: 0, y: 0, z: 1 },
|
||||
color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
|
||||
}),
|
||||
]
|
||||
this.defaultPlanes = { xy, yz, xz }
|
||||
|
||||
this.subscribeTo({
|
||||
event: 'select_with_point',
|
||||
callback: ({ data }) => {
|
||||
if (!data?.entity_id) return
|
||||
if (
|
||||
![
|
||||
this.defaultPlanes.xy,
|
||||
this.defaultPlanes.yz,
|
||||
this.defaultPlanes.xz,
|
||||
].includes(data.entity_id)
|
||||
)
|
||||
return
|
||||
this.onPlaneSelectCallback(data.entity_id)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onPlaneSelectCallback = (id: string) => {}
|
||||
onPlaneSelected(callback: (id: string) => void) {
|
||||
this.onPlaneSelectCallback = callback
|
||||
}
|
||||
|
||||
async setPlaneHidden(id: string, hidden: boolean): Promise<string> {
|
||||
return await this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'object_visible',
|
||||
object_id: id,
|
||||
hidden: hidden,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private async createPlane({
|
||||
x_axis,
|
||||
y_axis,
|
||||
color,
|
||||
}: {
|
||||
x_axis: Models['Point3d_type']
|
||||
y_axis: Models['Point3d_type']
|
||||
color: Models['Color_type']
|
||||
}): Promise<string> {
|
||||
const planeId = uuidv4()
|
||||
await this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'make_plane',
|
||||
size: 60,
|
||||
origin: { x: 0, y: 0, z: 0 },
|
||||
x_axis,
|
||||
y_axis,
|
||||
clobber: false,
|
||||
hide: true,
|
||||
},
|
||||
cmd_id: planeId,
|
||||
})
|
||||
await this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'plane_set_color',
|
||||
plane_id: planeId,
|
||||
color,
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
await this.setPlaneHidden(planeId, true)
|
||||
return planeId
|
||||
}
|
||||
}
|
||||
|
||||
export const engineCommandManager = new EngineCommandManager()
|
||||
|
@ -113,20 +113,6 @@ show(mySketch001)
|
||||
programMemory,
|
||||
[sourceStart, sourceStart + lineToChange.length],
|
||||
[2, 3],
|
||||
{
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
pathId: '',
|
||||
rotation: [0, 0, 0, 1],
|
||||
position: [0, 0, 0],
|
||||
pathToNode: [
|
||||
['body', ''],
|
||||
[0, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
],
|
||||
},
|
||||
[0, 0]
|
||||
)
|
||||
expect(recast(modifiedAst)).toBe(expectedCode)
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
getNodePathFromSourceRange,
|
||||
} from '../queryAst'
|
||||
import { isLiteralArrayOrStatic } from './sketchcombos'
|
||||
import { GuiModes, toolTips, ToolTip } from '../../useStore'
|
||||
import { toolTips, ToolTip } from '../../useStore'
|
||||
import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
|
||||
import { generateUuidFromHashSeed } from '../../lib/uuid'
|
||||
|
||||
@ -908,7 +908,6 @@ export function changeSketchArguments(
|
||||
programMemory: ProgramMemory,
|
||||
sourceRange: SourceRange,
|
||||
args: [number, number],
|
||||
guiMode: GuiModes,
|
||||
from: [number, number]
|
||||
): { modifiedAst: Program } {
|
||||
const _node = { ...node }
|
||||
@ -917,7 +916,6 @@ export function changeSketchArguments(
|
||||
_node,
|
||||
thePath
|
||||
)
|
||||
if (guiMode.mode !== 'sketch') throw new Error('not in sketch mode')
|
||||
|
||||
if (callExpression?.callee?.name in sketchLineHelperMap) {
|
||||
const { updateArgs } = sketchLineHelperMap[callExpression.callee.name]
|
||||
|
@ -1306,7 +1306,7 @@ export function getRemoveConstraintsTransforms(
|
||||
return theTransforms
|
||||
}
|
||||
|
||||
type PathToNodeMap = { [key: number]: PathToNode }
|
||||
export type PathToNodeMap = { [key: number]: PathToNode }
|
||||
|
||||
export function transformSecondarySketchLinesTagFirst({
|
||||
ast,
|
||||
|
@ -1,29 +1,29 @@
|
||||
import { Selections, StoreState } from '../useStore'
|
||||
import { Program, PathToNode } from './wasm'
|
||||
import { getNodeFromPath } from './queryAst'
|
||||
import { ArtifactMap } from './std/engineConnection'
|
||||
import { isOverlap } from 'lib/utils'
|
||||
|
||||
export function updateCursors(
|
||||
setCursor: StoreState['setCursor'],
|
||||
selectionRanges: Selections,
|
||||
export function pathMapToSelections(
|
||||
ast: Program,
|
||||
prevSelections: Selections,
|
||||
pathToNodeMap: { [key: number]: PathToNode }
|
||||
): (newAst: Program) => void {
|
||||
return (newAst: Program) => {
|
||||
const newSelections: Selections = {
|
||||
...selectionRanges,
|
||||
codeBasedSelections: [],
|
||||
}
|
||||
Object.entries(pathToNodeMap).forEach(([index, path]) => {
|
||||
const node = getNodeFromPath(newAst, path).node as any
|
||||
const type = selectionRanges.codeBasedSelections[Number(index)].type
|
||||
if (node) {
|
||||
newSelections.codeBasedSelections.push({
|
||||
range: [node.start, node.end],
|
||||
type: type || 'default',
|
||||
})
|
||||
}
|
||||
})
|
||||
setCursor(newSelections)
|
||||
): Selections {
|
||||
const newSelections: Selections = {
|
||||
...prevSelections,
|
||||
codeBasedSelections: [],
|
||||
}
|
||||
Object.entries(pathToNodeMap).forEach(([index, path]) => {
|
||||
const node = getNodeFromPath(ast, path).node as any
|
||||
const type = prevSelections.codeBasedSelections[Number(index)].type
|
||||
if (node) {
|
||||
newSelections.codeBasedSelections.push({
|
||||
range: [node.start, node.end],
|
||||
type: type || 'default',
|
||||
})
|
||||
}
|
||||
})
|
||||
return newSelections
|
||||
}
|
||||
|
||||
export function isReducedMotion(): boolean {
|
||||
@ -34,3 +34,27 @@ export function isReducedMotion(): boolean {
|
||||
window.matchMedia('(prefers-reduced-motion)').matches
|
||||
)
|
||||
}
|
||||
|
||||
export function isCursorInSketchCommandRange(
|
||||
artifactMap: ArtifactMap,
|
||||
selectionRanges: Selections
|
||||
): string | false {
|
||||
const overlapingEntries: [string, ArtifactMap[string]][] = Object.entries(
|
||||
artifactMap
|
||||
).filter(([id, artifact]: [string, ArtifactMap[string]]) =>
|
||||
selectionRanges.codeBasedSelections.some(
|
||||
(selection) =>
|
||||
Array.isArray(selection?.range) &&
|
||||
Array.isArray(artifact?.range) &&
|
||||
isOverlap(selection.range, artifact.range) &&
|
||||
(artifact.commandType === 'start_path' ||
|
||||
artifact.commandType === 'extend_path' ||
|
||||
artifact.commandType === 'close_path')
|
||||
)
|
||||
)
|
||||
return overlapingEntries.length && overlapingEntries[0][1].parentId
|
||||
? overlapingEntries[0][1].parentId
|
||||
: overlapingEntries.find(
|
||||
([, artifact]) => artifact.commandType === 'start_path'
|
||||
)?.[0] || false
|
||||
}
|
||||
|
853
src/machines/modelingMachine.ts
Normal file
853
src/machines/modelingMachine.ts
Normal file
@ -0,0 +1,853 @@
|
||||
import { PathToNode } from 'lang/wasm'
|
||||
import { engineCommandManager } from 'lang/std/engineConnection'
|
||||
import { isReducedMotion } from 'lang/util'
|
||||
import { Axis, Selection, SelectionRangeTypeMap, Selections } from 'useStore'
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||
import {
|
||||
doesPipeHaveCallExp,
|
||||
getNodePathFromSourceRange,
|
||||
hasExtrudeSketchGroup,
|
||||
} from 'lang/queryAst'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import {
|
||||
horzVertInfo,
|
||||
applyConstraintHorzVert,
|
||||
} from 'components/Toolbar/HorzVert'
|
||||
import {
|
||||
applyConstraintHorzVertAlign,
|
||||
horzVertDistanceInfo,
|
||||
} from 'components/Toolbar/SetHorzVertDistance'
|
||||
import { angleBetweenInfo } from 'components/Toolbar/SetAngleBetween'
|
||||
import { setAngleLengthInfo } from 'components/Toolbar/setAngleLength'
|
||||
import {
|
||||
applyConstraintEqualLength,
|
||||
setEqualLengthInfo,
|
||||
} from 'components/Toolbar/EqualLength'
|
||||
import { extrudeSketch } from 'lang/modifyAst'
|
||||
import { getNodeFromPath } from '../lang/queryAst'
|
||||
import { CallExpression, PipeExpression } from '../lang/wasm'
|
||||
|
||||
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
|
||||
|
||||
export type SetSelections =
|
||||
| {
|
||||
selectionType: 'singleCodeCursor'
|
||||
selection?: Selection
|
||||
}
|
||||
| {
|
||||
selectionType: 'otherSelection'
|
||||
selection: Axis
|
||||
}
|
||||
| {
|
||||
selectionType: 'completeSelection'
|
||||
selection: Selections
|
||||
}
|
||||
| {
|
||||
selectionType: 'mirrorCodeMirrorSelections'
|
||||
selection: Selections
|
||||
}
|
||||
|
||||
export const modelingMachine = createMachine(
|
||||
{
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogDMAVgDsAOmEiAjLMGCAHIIAsAJlHKANCACeiaQDZBEsctGrVATmmiaNQQF8H2tBhz5x2CJjAEAymDsAASwWGDknNy0DEggLGyRPLECCMrCCuL20qrCRpYagvnaegjZBtLiBqLploIGCtZVyk4u6Fh4UJ7evgAicGERQSx47NG88RxcSaApdcKSNKIKi8ppwsrLwsX60oriqgaWBgbKImpWqi0gru0eXj4EfaE+g5AwY7ETibyzx5lLu1sygM61ERV0+ho0mU4gURlOwihlis2SuN3cnXuvX6L2CJD42FgH2YrEm3B+QnM4mqdhoyPUILqBm2CAaFTS5houQUCMczmubQxXQeAVxQ1QI2JcVJ32SiGUXPEhjBtWklgaokMzIhqVUNHEG0WgjVqkEUN2aMFHWFvlF4WCbzAUq+UwpqRoGQ2pwacPW4JKJxhgYU0kWu3Wymklrc1qx-gGeIJRPo4xlrrlqUEqkqdMKOSRNEOLPWGTVhkswlU0O5fNaMbu3XjYoAZiRKM60+SMwqMgY7Go6mbalCWaIjLCjtJ0n36tUFNHbpjGwBRXDsMAAJxCAGtAuQABYdhLpmY7SPiSzAyOXwGFUQs01BtXVEEV9VSZr89Gxldrzc7vdD2kGISWPLtTwQepBGpEN8lDGhVBDLYdTUfVZzvYFQ3MS4vytBsHieBMglbdsU0+Ttpn4Sk0KzBR1EKdING1EozQqYxajyNQ6MOBchTjO1BhITBMCPMlKJSSNoIsEEllQrlhGQko9RhZEDFUL1vWEUMcLrRcbUeHF7SCISRLI0CxLdRR2ROJQwQQ2RmMQENJD1OxjR5aE+1rAV6yXB4wD4dgNwAVwwIIRjANdRNlCDlGRJU6kfapK0vdYWSnBQJEsfJMtODYrM-XS+MbAKgtCsBwr-KLgNTMDxPlawJ0DKtXNkLl0o2NjNULPMPQ2HSfL0vxd3YA9iDIShMGGwDopPKjSg0fUaDURFFoVbJ7x1S9oOSmQcl2CtRF461ptG-dxFOg8AElGwE4JhiiszpTqt1pB9C8NA8plynVFlyn1EMzVfdTrAU46PEu87IZukUiNCKAAFtItGJ6XXA+a3vVD6vV2Y41IQlkzAMakLCnD0K0jK9wc6SGLpG67G0IsUHpRkDnosjM3oUi91SsRYjmUywWRUDJRFEY0PXzdQrGpunALls6YexZ4jPhpHHrZtH6tKHkJHkLJ1AUGpsofQxxBEWpxayCXFFl2noZXABHYLsCYIJ2FQVBTM1ijLK5Njp20wtPMEUc+yVRC4vgpZlqjXDfIVg9E-3JWCGXZ3XaCBHUAANwqj2vdm9HZg9NjpPUZbIykFkKYNTD1nsGycjt+modb1OAmCFWIimIvtbVMwczF4wtXqRyEDHfVsiNwtahyTU46Kk7W+T1PkBIXcQjARHkaCPON04cghL716kKVdYjErEWFEy9LoXZKsQT7I3jWEFv5Ydh5183tXd-3VANzYAAF7cHYMfVGvtOZggyOkewqkjBqQrOlOBlQqjyCrEoacR145DRXp-XwRBuCwCCiQPAQR-6AJAWuISQQICEjARQJ0ECXpQOqJULm+QVArFvjqcsMIDg2UwRsdIhVBpCntu3RshDcDEI3KQ3Ae9NyHxoXQ4hE0mE+xYRBGwlh9QpQOp5awdR0piAqOLLS5g0HmlEd+CGeDJEPGkbI+Rxl8A+BPpzOEKk+wOT6nYHk6VNgGkOLkUJOQ1jvzOqvKRRCSFkJ8Pgdgh5mEc20UbDIN4GjHAKopfQjQLzLAVCIrkdE344PEfYwCqcnFxIURQ4BoCTI6GMjgKAuAPHaKWMTJQCFzBZIOEbdK6kYSRg0EsRCdhLyWEiUnfBxBYlyLIfvZRwlmlCWwG0jpGNljQQVMCKohZzBiHSiIHasg1DIhEFpOEMy25VJiTI2pQQwDOxoQkqASStkpDejYA0voqg5HKFOLQvC0gwK0sOPs1g3K3PEAAGTwBVAAKp7TAacM5u2znnd2qKvlCEUDA8ZCk1TpE8qOJQSouQ9SnMIfIGhYUItwMi1F4gAAKEo1xBAAIIQAwBAAgPKIDiklCkmK80zTAnNg0dIUg1JqFWMWKESoThQkyn6I2UgGWIqCCir2F1t7q2CIKyAAreXCo1rVVJ4rEIVEOJlLyZpCz1EJqaA0Bx1JPx+dJLVTKdUsoCDvTlxr+WEIRkwHw64gjuA0ZasVsxgQSBnsPMFyJcmpBvkqXKozlpWDij65leqA2Gu5byk16cXZuyZQAdxxYXUVc1ZgV32G9MeCEjhHGFm9JU2Uq5yGlTYvCNMV6MoLZgcQV1cAcAIHihA6k1T-CuZqXIVc-r2EyPUMQmokJ2AMPmv1eqJ1TqoDVciWj5qmmypkS5cClAghBSUSsxNCglJSosRYu7ynL3liO-dY6AByqAgjspGLAU1QqWYzoZJYC8G0wSvuRMWdSSoim6N0a-TKsKuVVtIcEepVCwGYFofQ9RlVmyoAIBAbgYBPC4BzqgXc4gYDsAALR4caZgZjeAyOQaWPMRCyJNTGmBccdKS7YRmmMG5fs0zP12PllhnD5CAENOoYR1RDDKCkfI5uDcADxDhpIOwMjG4EaMcCKx5T+GhKcdwNx+txdECG3mIdaWjQjaXnSlUDIqroTmHDOsTD2GOCKIPtgI+aniOMK0xRqjNG6MMaY8x5ZYXrNcdQDx3RkhzDytonCYwJjGqIUyvIWwiJnyBcU8l8LRG1FRbSwQHTemDNGYAaZxLVXUu2fS-Z7W6hMtiHUupXLRhNolGBTtG+4tjBwjpLUCrwWyBQB8NFyjTK4v0eo4lxbPgbN2c0ValIfXSynGylBKcVZAmVgvB6ZayxR4enm3iNxFV6uNY3PpzAhnjNtfM9tsAu3uv7bjY5lUkhgTqnbR6qcgTdhXtkDYeHxojiPajZFD5+4VuxbwPFzb5n3lJIBxl4m6hKwKQ0Ju1Q6V1QVGWh6E4Fh8wbBR-jjHr2Ny6fe8177ZmWMs8Jz1t0fXibbSrPkBUsc01qj2Cqxu8gn5KFuUEXAgGSK2iIhgVswVMD3U+0ymd2Q4qSArG9a8PJgShxQlOGDUI6hG2nuYbytjOgkCC5wfAW9cRTHjF3BMvcBec2DtSXRY4eTHEjND3hy7qR9g2Oqe1l55yyc6AecI24Oge-tF7-Xoz9jLo5AcXpJi1J-NWOaOosdZYp-IGn933dEjTpPeZYHpRKaVGNBXDQ6pwcdSkOwlQtLr3LBwvyZXGB4CxCd7GhtQg6SSBkHIRQKh1D3sQMx5zyxzRSCkLog4i8xE-h8FPhzqRi-b8OYcTU8ILdjcHBHXRdI3ohlOI7wdycj-90ONmHNq09lQnUCyZydzRQTqLSY4KcWFfBd-V6S5fYJkG3eQQoKoGuPUXPKQBAkMB3AdBOWmH9XVTAKAjMaSEwS5WlaoLMCwFfWdM0SoHtK+RCDBGWJPZOeFbVPA8QHoKjAgiCYwag3IcWG1VVBkRDaCZ9f6WlKwBXJgnA1gllYDINUtCALg89NDc2ThXKBAk4UbRASsIMFDG+EEdYCwPdNgotXeYNJQxtcOODCmfZIcRDXQrkOPAw84YwllQ9dgCwxzV+I3U0UgqQBnSg65fYC9FQFQU4OkD9JeOTKJXAllADIDDldgcfJvafWdE4YmU4H6U0aeOEewyoFDOkLIRiWFNAbFPAzwhAErLKQFMwBNOoaQGuagy8coVaeeKoGTKIodeTV3JTShdjGrDTF7LrCoi9JaGwARDBAfTzfrXIIrUJKEEEFHDrCLWrTTNLEYhUHaQcKEHNbIZaExDYWA0AjiCwI4RPTo5ghTBbZ7LTDY35PUIwIcZaEQJQQJRYTIEWBEewOoCsZnNHJJW409A7RzcPD6d8dBCFXIKnEQTINtZKaoPUAaJ3ZOJXFXNsMACos46kEQVVawCsQsSnFCd4woHkCItqKsWWF3HDdPOvOaLWN0XKC8A5dzG1PgkxTKHmJYAofKJ-SvfcVPGk33dGekzmBnSQE4anQ2OKQodk7MWQCxREFVOiJwJwIAA */
|
||||
id: 'Modeling',
|
||||
|
||||
tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
|
||||
predictableActionArguments: true,
|
||||
preserveActionOrder: true,
|
||||
|
||||
context: {
|
||||
guiMode: 'default',
|
||||
selection: [] as string[],
|
||||
selectionRanges: {
|
||||
otherSelections: [],
|
||||
codeBasedSelections: [],
|
||||
} as Selections,
|
||||
selectionRangeTypeMap: {} as SelectionRangeTypeMap,
|
||||
sketchPathToNode: null as PathToNode | null, // maybe too specific, and we should have a generic pathToNode, but being specific seems less risky when I'm not sure
|
||||
sketchEnginePathId: '' as string,
|
||||
sketchPlaneId: '' as string,
|
||||
},
|
||||
|
||||
schema: {
|
||||
events: {} as
|
||||
| { type: 'Deselect all' }
|
||||
| { type: 'Deselect edge'; data: Selection & { type: 'edge' } }
|
||||
| { type: 'Deselect axis'; data: Axis }
|
||||
| {
|
||||
type: 'Deselect segment'
|
||||
data: Selection & { type: 'line' | 'arc' }
|
||||
}
|
||||
| { type: 'Deselect face'; data: Selection & { type: 'face' } }
|
||||
| {
|
||||
type: 'Deselect point'
|
||||
data: Selection & { type: 'point' | 'line-end' | 'line-mid' }
|
||||
}
|
||||
| { type: 'Enter sketch' }
|
||||
| { type: 'Select all'; data: Selection & { type: 'all ' } }
|
||||
| { type: 'Select edge'; data: Selection & { type: 'edge' } }
|
||||
| { type: 'Select axis'; data: Axis }
|
||||
| { type: 'Select segment'; data: Selection & { type: 'line' | 'arc' } }
|
||||
| { type: 'Select face'; data: Selection & { type: 'face' } }
|
||||
| { type: 'Select default plane'; data: { planeId: string } }
|
||||
| { type: 'Set selection'; data: SetSelections }
|
||||
| {
|
||||
type: 'Select point'
|
||||
data: Selection & { type: 'point' | 'line-end' | 'line-mid' }
|
||||
}
|
||||
| { type: 'Sketch no face' }
|
||||
| { type: 'Toggle gui mode' }
|
||||
| { type: 'Cancel' }
|
||||
| { type: 'CancelSketch' }
|
||||
| {
|
||||
type: 'Add point'
|
||||
data: {
|
||||
coords: { x: number; y: number }[]
|
||||
axis: 'xy' | 'xz' | 'yz' | '-xy' | '-xz' | '-yz' | null
|
||||
}
|
||||
}
|
||||
| { type: 'Equip tool' }
|
||||
| { type: 'Equip move tool' }
|
||||
| { type: 'Set radius' }
|
||||
| { type: 'Complete line' }
|
||||
| { type: 'Set distance' }
|
||||
| { type: 'Equip new tool' }
|
||||
| { type: 'update_code'; data: string }
|
||||
| { type: 'Make segment horizontal' }
|
||||
| { type: 'Make segment vertical' }
|
||||
| { type: 'Constrain horizontal distance' }
|
||||
| { type: 'Constrain vertical distance' }
|
||||
| { type: 'Constrain angle' }
|
||||
| { type: 'Constrain horizontally align' }
|
||||
| { type: 'Constrain vertically align' }
|
||||
| { type: 'Constrain length' }
|
||||
| { type: 'Constrain equal length' }
|
||||
| { type: 'extrude intent' },
|
||||
// ,
|
||||
},
|
||||
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
'Set selection': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: 'Set selection',
|
||||
},
|
||||
|
||||
'Deselect point': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: [
|
||||
'Remove from code-based selection',
|
||||
'Update code selection cursors',
|
||||
// 'Engine: remove highlight',
|
||||
],
|
||||
cond: 'Selection contains point',
|
||||
},
|
||||
|
||||
'Deselect edge': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: [
|
||||
'Remove from code-based selection',
|
||||
'Update code selection cursors',
|
||||
// 'Engine: remove highlight',
|
||||
],
|
||||
cond: 'Selection contains edge',
|
||||
},
|
||||
|
||||
'Deselect axis': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: [
|
||||
'Remove from other selection',
|
||||
'Update code selection cursors',
|
||||
// 'Engine: remove highlight',
|
||||
],
|
||||
cond: 'Selection contains axis',
|
||||
},
|
||||
|
||||
'Select point': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: [
|
||||
'Add to code-based selection',
|
||||
'Update code selection cursors',
|
||||
// 'Engine: add highlight',
|
||||
],
|
||||
},
|
||||
|
||||
'Select edge': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: [
|
||||
'Add to code-based selection',
|
||||
'Update code selection cursors',
|
||||
// 'Engine: add highlight',
|
||||
],
|
||||
},
|
||||
|
||||
'Select axis': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: [
|
||||
'Add to other selection',
|
||||
// 'Engine: add highlight',
|
||||
],
|
||||
},
|
||||
|
||||
'Select face': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: [
|
||||
'Add to code-based selection',
|
||||
'Update code selection cursors',
|
||||
// 'Engine: add highlight',
|
||||
],
|
||||
},
|
||||
|
||||
'Enter sketch': [
|
||||
{
|
||||
target: 'Sketch',
|
||||
cond: 'Selection is one face',
|
||||
actions: [
|
||||
'set sketch metadata',
|
||||
'sketch mode enabled',
|
||||
'edit mode enter',
|
||||
],
|
||||
},
|
||||
'Sketch no face',
|
||||
],
|
||||
|
||||
'Deselect face': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: [
|
||||
'Remove from code-based selection',
|
||||
'Update code selection cursors',
|
||||
// 'Engine: remove highlight',
|
||||
],
|
||||
cond: 'Selection contains face',
|
||||
},
|
||||
|
||||
'Select all': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: 'Add to code-based selection',
|
||||
},
|
||||
|
||||
'Deselect all': {
|
||||
target: 'idle',
|
||||
internal: true,
|
||||
actions: [
|
||||
'Clear selection',
|
||||
'Update code selection cursors',
|
||||
// 'Engine: remove highlight',
|
||||
],
|
||||
cond: 'Selection is not empty',
|
||||
},
|
||||
|
||||
'extrude intent': [
|
||||
{
|
||||
target: 'awaiting selection',
|
||||
cond: 'has no selection',
|
||||
},
|
||||
{
|
||||
target: 'idle',
|
||||
cond: 'has valid extrude selection',
|
||||
internal: true,
|
||||
actions: 'AST extrude',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
Sketch: {
|
||||
states: {
|
||||
SketchIdle: {
|
||||
on: {
|
||||
'Select point': {
|
||||
target: 'SketchIdle',
|
||||
internal: true,
|
||||
actions: [
|
||||
'Update code selection cursors',
|
||||
'Add to code-based selection',
|
||||
],
|
||||
},
|
||||
|
||||
'Select segment': {
|
||||
target: 'SketchIdle',
|
||||
internal: true,
|
||||
actions: [
|
||||
'Update code selection cursors',
|
||||
'Add to code-based selection',
|
||||
],
|
||||
},
|
||||
|
||||
'Deselect point': {
|
||||
target: 'SketchIdle',
|
||||
internal: true,
|
||||
cond: 'Selection contains point',
|
||||
actions: [
|
||||
'Update code selection cursors',
|
||||
'Add to code-based selection',
|
||||
],
|
||||
},
|
||||
|
||||
'Deselect segment': {
|
||||
target: 'SketchIdle',
|
||||
internal: true,
|
||||
cond: 'Selection contains line',
|
||||
actions: [
|
||||
'Update code selection cursors',
|
||||
'Add to code-based selection',
|
||||
],
|
||||
},
|
||||
|
||||
'Equip tool': {
|
||||
target: 'Line Tool',
|
||||
actions: 'set tool line',
|
||||
},
|
||||
|
||||
'Equip move tool': 'Move Tool',
|
||||
|
||||
'Set selection': {
|
||||
target: 'SketchIdle',
|
||||
internal: true,
|
||||
actions: 'Set selection',
|
||||
},
|
||||
|
||||
'Make segment vertical': {
|
||||
cond: 'Can make selection vertical',
|
||||
target: 'SketchIdle',
|
||||
internal: true,
|
||||
actions: ['Make selection vertical'],
|
||||
},
|
||||
|
||||
'Make segment horizontal': {
|
||||
target: 'SketchIdle',
|
||||
internal: true,
|
||||
cond: 'Can make selection horizontal',
|
||||
actions: ['Make selection horizontal'],
|
||||
},
|
||||
|
||||
'Constrain horizontal distance': {
|
||||
target: 'Await horizontal distance info',
|
||||
cond: 'Can constrain horizontal distance',
|
||||
},
|
||||
|
||||
'Constrain vertical distance': {
|
||||
target: 'Await vertical distance info',
|
||||
cond: 'Can constrain vertical distance',
|
||||
},
|
||||
|
||||
'Constrain angle': {
|
||||
target: 'Await angle info',
|
||||
cond: 'Can constrain angle',
|
||||
},
|
||||
|
||||
'Constrain length': {
|
||||
target: 'Await length info',
|
||||
cond: 'Can constrain length',
|
||||
},
|
||||
|
||||
'Constrain horizontally align': {
|
||||
cond: 'Can constrain horizontally align',
|
||||
target: 'SketchIdle',
|
||||
internal: true,
|
||||
actions: ['Constrain horizontally align'],
|
||||
},
|
||||
|
||||
'Constrain vertically align': {
|
||||
cond: 'Can constrain vertically align',
|
||||
target: 'SketchIdle',
|
||||
internal: true,
|
||||
actions: ['Constrain vertically align'],
|
||||
},
|
||||
|
||||
'Constrain equal length': {
|
||||
cond: 'Can constrain equal length',
|
||||
target: 'SketchIdle',
|
||||
internal: true,
|
||||
actions: ['Constrain equal length'],
|
||||
},
|
||||
},
|
||||
|
||||
entry: 'equip select',
|
||||
},
|
||||
|
||||
'Line Tool': {
|
||||
states: {
|
||||
Done: {
|
||||
type: 'final',
|
||||
},
|
||||
|
||||
'Point Added': {
|
||||
on: {
|
||||
'Add point': {
|
||||
target: 'Segment Added',
|
||||
actions: ['AST start new sketch'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'Segment Added': {
|
||||
on: {
|
||||
'Add point': {
|
||||
target: 'Segment Added',
|
||||
internal: true,
|
||||
actions: ['AST add line segment'],
|
||||
},
|
||||
|
||||
'Complete line': {
|
||||
target: 'Done',
|
||||
actions: ['Modify AST', 'Update code selection cursors'],
|
||||
},
|
||||
|
||||
'Equip new tool': {
|
||||
target: 'Segment Added',
|
||||
internal: true,
|
||||
actions: 'set tool',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Init: {
|
||||
always: [
|
||||
{
|
||||
target: 'Segment Added',
|
||||
cond: 'is editing existing sketch',
|
||||
},
|
||||
'No Points',
|
||||
],
|
||||
},
|
||||
|
||||
'No Points': {
|
||||
on: {
|
||||
'Add point': 'Point Added',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// invoke: [
|
||||
// {
|
||||
// src: 'createLine',
|
||||
// id: 'Create line',
|
||||
// onDone: 'SketchIdle',
|
||||
// },
|
||||
// ],
|
||||
initial: 'Init',
|
||||
|
||||
on: {
|
||||
'Equip move tool': 'Move Tool',
|
||||
},
|
||||
},
|
||||
|
||||
'Move Tool': {
|
||||
entry: 'set tool move',
|
||||
},
|
||||
|
||||
'Await horizontal distance info': {
|
||||
invoke: {
|
||||
src: 'Get horizontal info',
|
||||
id: 'get-horizontal-info',
|
||||
onDone: {
|
||||
target: 'SketchIdle',
|
||||
actions: 'Set selection',
|
||||
},
|
||||
onError: 'SketchIdle',
|
||||
},
|
||||
},
|
||||
|
||||
'Await vertical distance info': {
|
||||
invoke: {
|
||||
src: 'Get vertical info',
|
||||
id: 'get-vertical-info',
|
||||
onDone: {
|
||||
target: 'SketchIdle',
|
||||
actions: 'Set selection',
|
||||
},
|
||||
onError: 'SketchIdle',
|
||||
},
|
||||
},
|
||||
|
||||
'Await angle info': {
|
||||
invoke: {
|
||||
src: 'Get angle info',
|
||||
id: 'get-angle-info',
|
||||
onDone: {
|
||||
target: 'SketchIdle',
|
||||
actions: 'Set selection',
|
||||
},
|
||||
onError: 'SketchIdle',
|
||||
},
|
||||
},
|
||||
|
||||
'Await length info': {
|
||||
invoke: {
|
||||
src: 'Get length info',
|
||||
id: 'get-length-info',
|
||||
onDone: {
|
||||
target: 'SketchIdle',
|
||||
actions: 'Set selection',
|
||||
},
|
||||
onError: 'SketchIdle',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
initial: 'SketchIdle',
|
||||
|
||||
on: {
|
||||
CancelSketch: '.SketchIdle',
|
||||
},
|
||||
|
||||
exit: 'sketch exit execute',
|
||||
},
|
||||
|
||||
'Sketch no face': {
|
||||
entry: 'show default planes',
|
||||
|
||||
exit: 'hide default planes',
|
||||
on: {
|
||||
'Select default plane': {
|
||||
target: 'Sketch',
|
||||
actions: [
|
||||
'reset sketch metadata',
|
||||
'set default plane id',
|
||||
'sketch mode enabled',
|
||||
'create path',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'awaiting selection': {
|
||||
on: {
|
||||
'Set selection': {
|
||||
target: 'checking selection',
|
||||
actions: 'Set selection',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'checking selection': {
|
||||
always: [
|
||||
{
|
||||
target: 'idle',
|
||||
cond: 'has valid extrude selection',
|
||||
actions: 'AST extrude',
|
||||
},
|
||||
{
|
||||
target: 'idle',
|
||||
actions: 'toast extrude failed',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
initial: 'idle',
|
||||
|
||||
on: {
|
||||
Cancel: {
|
||||
target: 'idle',
|
||||
// TODO what if we're existing extrude equiped, should these actions still be fired?
|
||||
// mabye cancel needs to have a guard for if else logic?
|
||||
actions: [
|
||||
'edit_mode_exit',
|
||||
'default_camera_disable_sketch_mode',
|
||||
'reset sketch metadata',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
guards: {
|
||||
'is editing existing sketch': ({ sketchPathToNode }) =>
|
||||
!!sketchPathToNode,
|
||||
'Can make selection horizontal': ({ selectionRanges }) =>
|
||||
horzVertInfo(selectionRanges, 'horizontal').enabled,
|
||||
'Can make selection vertical': ({ selectionRanges }) =>
|
||||
horzVertInfo(selectionRanges, 'vertical').enabled,
|
||||
'Can constrain horizontal distance': ({ selectionRanges }) =>
|
||||
horzVertDistanceInfo({ selectionRanges, constraint: 'setHorzDistance' })
|
||||
.enabled,
|
||||
'Can constrain vertical distance': ({ selectionRanges }) =>
|
||||
horzVertDistanceInfo({ selectionRanges, constraint: 'setVertDistance' })
|
||||
.enabled,
|
||||
'Can constrain angle': ({ selectionRanges }) =>
|
||||
angleBetweenInfo({ selectionRanges }).enabled,
|
||||
'Can constrain length': ({ selectionRanges }) =>
|
||||
setAngleLengthInfo({ selectionRanges }).enabled,
|
||||
'Can constrain horizontally align': ({ selectionRanges }) =>
|
||||
horzVertDistanceInfo({ selectionRanges, constraint: 'setHorzDistance' })
|
||||
.enabled,
|
||||
'Can constrain vertically align': ({ selectionRanges }) =>
|
||||
horzVertDistanceInfo({ selectionRanges, constraint: 'setHorzDistance' })
|
||||
.enabled,
|
||||
'Can constrain equal length': ({ selectionRanges }) =>
|
||||
setEqualLengthInfo({ selectionRanges }).enabled,
|
||||
'has no selection': ({ selectionRanges }) => {
|
||||
if (selectionRanges?.codeBasedSelections?.length < 1) return true
|
||||
const selection = selectionRanges?.codeBasedSelections?.[0] || {}
|
||||
|
||||
return (
|
||||
selectionRanges.codeBasedSelections.length === 1 &&
|
||||
!hasExtrudeSketchGroup({
|
||||
ast: kclManager.ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
selection,
|
||||
})
|
||||
)
|
||||
},
|
||||
'has valid extrude selection': ({ selectionRanges }) => {
|
||||
if (selectionRanges.codeBasedSelections.length !== 1) return false
|
||||
const isSketchPipe = isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
selectionRanges
|
||||
)
|
||||
const common = {
|
||||
selection: selectionRanges.codeBasedSelections[0],
|
||||
ast: kclManager.ast,
|
||||
}
|
||||
const hasClose = doesPipeHaveCallExp({ calleeName: 'close', ...common })
|
||||
const hasExtrude = doesPipeHaveCallExp({
|
||||
calleeName: 'extrude',
|
||||
...common,
|
||||
})
|
||||
return !!isSketchPipe && hasClose && !hasExtrude
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
'Add to code-based selection': assign({
|
||||
selectionRanges: ({ selectionRanges }, event) => ({
|
||||
...selectionRanges,
|
||||
codeBasedSelections: [
|
||||
...selectionRanges.codeBasedSelections,
|
||||
event.data,
|
||||
],
|
||||
}),
|
||||
}),
|
||||
'Add to other selection': assign({
|
||||
selectionRanges: ({ selectionRanges }, event) => ({
|
||||
...selectionRanges,
|
||||
otherSelections: [...selectionRanges.otherSelections, event.data],
|
||||
}),
|
||||
}),
|
||||
'Remove from code-based selection': assign({
|
||||
selectionRanges: ({ selectionRanges }, event) => ({
|
||||
...selectionRanges,
|
||||
codeBasedSelections: [
|
||||
...selectionRanges.codeBasedSelections,
|
||||
event.data,
|
||||
],
|
||||
}),
|
||||
}),
|
||||
'Remove from other selection': assign({
|
||||
selectionRanges: ({ selectionRanges }, event) => ({
|
||||
...selectionRanges,
|
||||
otherSelections: [...selectionRanges.otherSelections, event.data],
|
||||
}),
|
||||
}),
|
||||
'Clear selection': assign({
|
||||
selectionRanges: () => ({
|
||||
otherSelections: [],
|
||||
codeBasedSelections: [],
|
||||
}),
|
||||
}),
|
||||
'sketch mode enabled': ({ sketchPlaneId }) => {
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'sketch_mode_enable',
|
||||
plane_id: sketchPlaneId,
|
||||
ortho: true,
|
||||
animated: !isReducedMotion(),
|
||||
},
|
||||
})
|
||||
},
|
||||
'edit mode enter': ({ selectionRanges }) => {
|
||||
const pathId = isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
selectionRanges
|
||||
)
|
||||
pathId &&
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'edit_mode_enter',
|
||||
target: pathId,
|
||||
},
|
||||
})
|
||||
},
|
||||
'hide default planes': ({}) => {
|
||||
kclManager.hidePlanes()
|
||||
},
|
||||
edit_mode_exit: () =>
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'edit_mode_exit' },
|
||||
}),
|
||||
default_camera_disable_sketch_mode: () =>
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||
}),
|
||||
'reset sketch metadata': assign({
|
||||
sketchPathToNode: null,
|
||||
sketchEnginePathId: '',
|
||||
sketchPlaneId: '',
|
||||
}),
|
||||
'set sketch metadata': assign(({ selectionRanges }) => {
|
||||
const sourceRange = selectionRanges.codeBasedSelections[0].range
|
||||
const sketchPathToNode = getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
sourceRange
|
||||
)
|
||||
const pipeExpression = getNodeFromPath<PipeExpression>(
|
||||
kclManager.ast,
|
||||
sketchPathToNode,
|
||||
'PipeExpression'
|
||||
).node
|
||||
if (pipeExpression.type !== 'PipeExpression') return {}
|
||||
const sketchCallExpression = pipeExpression.body.find(
|
||||
(e) =>
|
||||
e.type === 'CallExpression' && e.callee.name === 'startSketchOn'
|
||||
) as CallExpression
|
||||
if (!sketchCallExpression) return {}
|
||||
const firstArg = sketchCallExpression.arguments[0]
|
||||
let planeId = ''
|
||||
if (firstArg.type === 'Literal' && firstArg.value) {
|
||||
const planeStrCleaned = firstArg.value
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace('-', '')
|
||||
if (
|
||||
planeStrCleaned === 'xy' ||
|
||||
planeStrCleaned === 'xz' ||
|
||||
planeStrCleaned === 'yz'
|
||||
) {
|
||||
planeId = kclManager.getPlaneId(planeStrCleaned)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('planeId', planeId)
|
||||
|
||||
const sketchEnginePathId =
|
||||
isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
selectionRanges
|
||||
) || ''
|
||||
return {
|
||||
sketchPathToNode,
|
||||
sketchEnginePathId,
|
||||
sketchPlaneId: planeId,
|
||||
}
|
||||
}),
|
||||
'set tool line': () =>
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_tool',
|
||||
tool: 'sketch_line',
|
||||
},
|
||||
}),
|
||||
'equip select': () =>
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_tool',
|
||||
tool: 'select',
|
||||
},
|
||||
}),
|
||||
'set tool move': () =>
|
||||
engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_tool',
|
||||
tool: 'move',
|
||||
},
|
||||
}),
|
||||
'Make selection horizontal': ({ selectionRanges }) => {
|
||||
const { modifiedAst, pathToNodeMap } = applyConstraintHorzVert(
|
||||
selectionRanges,
|
||||
'horizontal',
|
||||
kclManager.ast,
|
||||
kclManager.programMemory
|
||||
)
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
// TODO re implement cursor shit
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
},
|
||||
'Make selection vertical': ({ selectionRanges }) => {
|
||||
const { modifiedAst, pathToNodeMap } = applyConstraintHorzVert(
|
||||
selectionRanges,
|
||||
'vertical',
|
||||
kclManager.ast,
|
||||
kclManager.programMemory
|
||||
)
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
// TODO re implement cursor shit
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
},
|
||||
'Constrain horizontally align': ({ selectionRanges }) => {
|
||||
const { modifiedAst, pathToNodeMap } = applyConstraintHorzVertAlign({
|
||||
selectionRanges,
|
||||
constraint: 'setVertDistance',
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
// TODO re implement cursor shit
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
},
|
||||
'Constrain vertically align': ({ selectionRanges }) => {
|
||||
const { modifiedAst, pathToNodeMap } = applyConstraintHorzVertAlign({
|
||||
selectionRanges,
|
||||
constraint: 'setHorzDistance',
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
// TODO re implement cursor shit
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
},
|
||||
'Constrain equal length': ({ selectionRanges }) => {
|
||||
const { modifiedAst, pathToNodeMap } = applyConstraintEqualLength({
|
||||
selectionRanges,
|
||||
})
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
// TODO re implement cursor shit
|
||||
// callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
|
||||
})
|
||||
},
|
||||
'AST extrude': ({ selectionRanges }) => {
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
selectionRanges.codeBasedSelections[0].range
|
||||
)
|
||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
|
||||
kclManager.ast,
|
||||
pathToNode
|
||||
)
|
||||
// TODO not handling focusPath correctly I think
|
||||
kclManager.updateAst(modifiedAst, true, {
|
||||
focusPath: pathToExtrudeArg,
|
||||
})
|
||||
},
|
||||
'set default plane id': assign({
|
||||
sketchPlaneId: (_, { data }) => data.planeId,
|
||||
}),
|
||||
},
|
||||
}
|
||||
)
|
98
src/machines/modelingMachine.typegen.ts
Normal file
98
src/machines/modelingMachine.typegen.ts
Normal file
@ -0,0 +1,98 @@
|
||||
|
||||
// This file was automatically generated. Edits will be overwritten
|
||||
|
||||
export interface Typegen0 {
|
||||
'@@xstate/typegen': true;
|
||||
internalEvents: {
|
||||
"": { type: "" };
|
||||
"done.invoke.get-angle-info": { type: "done.invoke.get-angle-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"done.invoke.get-horizontal-info": { type: "done.invoke.get-horizontal-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"done.invoke.get-length-info": { type: "done.invoke.get-length-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"done.invoke.get-vertical-info": { type: "done.invoke.get-vertical-info"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
|
||||
"error.platform.get-angle-info": { type: "error.platform.get-angle-info"; data: unknown };
|
||||
"error.platform.get-horizontal-info": { type: "error.platform.get-horizontal-info"; data: unknown };
|
||||
"error.platform.get-length-info": { type: "error.platform.get-length-info"; data: unknown };
|
||||
"error.platform.get-vertical-info": { type: "error.platform.get-vertical-info"; data: unknown };
|
||||
"xstate.init": { type: "xstate.init" };
|
||||
"xstate.stop": { type: "xstate.stop" };
|
||||
};
|
||||
invokeSrcNameMap: {
|
||||
"Get angle info": "done.invoke.get-angle-info";
|
||||
"Get horizontal info": "done.invoke.get-horizontal-info";
|
||||
"Get length info": "done.invoke.get-length-info";
|
||||
"Get vertical info": "done.invoke.get-vertical-info";
|
||||
};
|
||||
missingImplementations: {
|
||||
actions: "AST add line segment" | "AST start new sketch" | "Modify AST" | "Set selection" | "Update code selection cursors" | "create path" | "set tool" | "show default planes" | "sketch exit execute" | "toast extrude failed";
|
||||
delays: never;
|
||||
guards: "Selection contains axis" | "Selection contains edge" | "Selection contains face" | "Selection contains line" | "Selection contains point" | "Selection is not empty" | "Selection is one face";
|
||||
services: "Get angle info" | "Get horizontal info" | "Get length info" | "Get vertical info";
|
||||
};
|
||||
eventsCausingActions: {
|
||||
"AST add line segment": "Add point";
|
||||
"AST extrude": "" | "extrude intent";
|
||||
"AST start new sketch": "Add point";
|
||||
"Add to code-based selection": "Deselect point" | "Deselect segment" | "Select all" | "Select edge" | "Select face" | "Select point" | "Select segment";
|
||||
"Add to other selection": "Select axis";
|
||||
"Clear selection": "Deselect all";
|
||||
"Constrain equal length": "Constrain equal length";
|
||||
"Constrain horizontally align": "Constrain horizontally align";
|
||||
"Constrain vertically align": "Constrain vertically align";
|
||||
"Make selection horizontal": "Make segment horizontal";
|
||||
"Make selection vertical": "Make segment vertical";
|
||||
"Modify AST": "Complete line";
|
||||
"Remove from code-based selection": "Deselect edge" | "Deselect face" | "Deselect point";
|
||||
"Remove from other selection": "Deselect axis";
|
||||
"Set selection": "Set selection" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-vertical-info";
|
||||
"Update code selection cursors": "Complete line" | "Deselect all" | "Deselect axis" | "Deselect edge" | "Deselect face" | "Deselect point" | "Deselect segment" | "Select edge" | "Select face" | "Select point" | "Select segment";
|
||||
"create path": "Select default plane";
|
||||
"default_camera_disable_sketch_mode": "Cancel";
|
||||
"edit mode enter": "Enter sketch";
|
||||
"edit_mode_exit": "Cancel";
|
||||
"equip select": "CancelSketch" | "Constrain equal length" | "Constrain horizontally align" | "Constrain vertically align" | "Deselect point" | "Deselect segment" | "Enter sketch" | "Make segment horizontal" | "Make segment vertical" | "Select default plane" | "Select point" | "Select segment" | "Set selection" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-vertical-info" | "error.platform.get-angle-info" | "error.platform.get-horizontal-info" | "error.platform.get-length-info" | "error.platform.get-vertical-info";
|
||||
"hide default planes": "Cancel" | "Select default plane" | "xstate.stop";
|
||||
"reset sketch metadata": "Cancel" | "Select default plane";
|
||||
"set default plane id": "Select default plane";
|
||||
"set sketch metadata": "Enter sketch";
|
||||
"set tool": "Equip new tool";
|
||||
"set tool line": "Equip tool";
|
||||
"set tool move": "Equip move tool";
|
||||
"show default planes": "Enter sketch";
|
||||
"sketch exit execute": "Cancel" | "Complete line" | "xstate.stop";
|
||||
"sketch mode enabled": "Enter sketch" | "Select default plane";
|
||||
"toast extrude failed": "";
|
||||
};
|
||||
eventsCausingDelays: {
|
||||
|
||||
};
|
||||
eventsCausingGuards: {
|
||||
"Can constrain angle": "Constrain angle";
|
||||
"Can constrain equal length": "Constrain equal length";
|
||||
"Can constrain horizontal distance": "Constrain horizontal distance";
|
||||
"Can constrain horizontally align": "Constrain horizontally align";
|
||||
"Can constrain length": "Constrain length";
|
||||
"Can constrain vertical distance": "Constrain vertical distance";
|
||||
"Can constrain vertically align": "Constrain vertically align";
|
||||
"Can make selection horizontal": "Make segment horizontal";
|
||||
"Can make selection vertical": "Make segment vertical";
|
||||
"Selection contains axis": "Deselect axis";
|
||||
"Selection contains edge": "Deselect edge";
|
||||
"Selection contains face": "Deselect face";
|
||||
"Selection contains line": "Deselect segment";
|
||||
"Selection contains point": "Deselect point";
|
||||
"Selection is not empty": "Deselect all";
|
||||
"Selection is one face": "Enter sketch";
|
||||
"has no selection": "extrude intent";
|
||||
"has valid extrude selection": "" | "extrude intent";
|
||||
"is editing existing sketch": "";
|
||||
};
|
||||
eventsCausingServices: {
|
||||
"Get angle info": "Constrain angle";
|
||||
"Get horizontal info": "Constrain horizontal distance";
|
||||
"Get length info": "Constrain length";
|
||||
"Get vertical info": "Constrain vertical distance";
|
||||
};
|
||||
matchesStates: "Sketch" | "Sketch no face" | "Sketch.Await angle info" | "Sketch.Await horizontal distance info" | "Sketch.Await length info" | "Sketch.Await vertical distance info" | "Sketch.Line Tool" | "Sketch.Line Tool.Done" | "Sketch.Line Tool.Init" | "Sketch.Line Tool.No Points" | "Sketch.Line Tool.Point Added" | "Sketch.Line Tool.Segment Added" | "Sketch.Move Tool" | "Sketch.SketchIdle" | "awaiting selection" | "checking selection" | "idle" | { "Sketch"?: "Await angle info" | "Await horizontal distance info" | "Await length info" | "Await vertical distance info" | "Line Tool" | "Move Tool" | "SketchIdle" | { "Line Tool"?: "Done" | "Init" | "No Points" | "Point Added" | "Segment Added"; }; };
|
||||
tags: never;
|
||||
}
|
||||
|
@ -2,18 +2,15 @@ import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionButton } from '../../components/ActionButton'
|
||||
import { useDismiss } from '.'
|
||||
import { useEffect } from 'react'
|
||||
import { useStore } from 'useStore'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
export default function FutureWork() {
|
||||
const dismiss = useDismiss()
|
||||
const { deferredSetCode } = useStore((s) => ({
|
||||
deferredSetCode: s.deferredSetCode,
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
deferredSetCode(bracket)
|
||||
}, [deferredSetCode])
|
||||
kclManager.setCode(bracket)
|
||||
}, [kclManager.setCode])
|
||||
|
||||
return (
|
||||
<div className="fixed grid justify-center items-center inset-0 bg-chalkboard-100/50 z-50">
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
import { useStore } from 'useStore'
|
||||
import {
|
||||
createNewProject,
|
||||
getNextProjectIndex,
|
||||
@ -20,14 +19,12 @@ import { isTauri } from 'lib/isTauri'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { paths } from 'Router'
|
||||
import { useEffect } from 'react'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
function OnboardingWithNewFile() {
|
||||
const navigate = useNavigate()
|
||||
const dismiss = useDismiss()
|
||||
const next = useNextClick(onboardingPaths.INDEX)
|
||||
const { deferredSetCode } = useStore((s) => ({
|
||||
deferredSetCode: s.deferredSetCode,
|
||||
}))
|
||||
const {
|
||||
settings: {
|
||||
context: { defaultDirectory },
|
||||
@ -76,7 +73,7 @@ function OnboardingWithNewFile() {
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
deferredSetCode(bracket)
|
||||
kclManager.setCode(bracket)
|
||||
next()
|
||||
}}
|
||||
icon={{ icon: faArrowRight }}
|
||||
@ -127,10 +124,6 @@ function OnboardingWithNewFile() {
|
||||
}
|
||||
|
||||
export default function Introduction() {
|
||||
const { deferredSetCode, code } = useStore((s) => ({
|
||||
code: s.code,
|
||||
deferredSetCode: s.deferredSetCode,
|
||||
}))
|
||||
const {
|
||||
settings: {
|
||||
state: {
|
||||
@ -147,10 +140,10 @@ export default function Introduction() {
|
||||
const next = useNextClick(onboardingPaths.CAMERA)
|
||||
|
||||
useEffect(() => {
|
||||
if (code === '') deferredSetCode(bracket)
|
||||
}, [code, deferredSetCode])
|
||||
if (kclManager.code === '') kclManager.setCode(bracket)
|
||||
}, [kclManager.code, kclManager.setCode])
|
||||
|
||||
return !(code !== '' && code !== bracket) ? (
|
||||
return !(kclManager.code !== '' && kclManager.code !== bracket) ? (
|
||||
<div className="fixed grid place-content-center inset-0 bg-chalkboard-110/50 z-50">
|
||||
<div className="max-w-3xl bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded">
|
||||
<h1 className="text-2xl font-bold flex gap-4 flex-wrap items-center">
|
||||
|
@ -3,18 +3,16 @@ import { ActionButton } from '../../components/ActionButton'
|
||||
import { onboardingPaths, useDismiss, useNextClick } from '.'
|
||||
import { useStore } from 'useStore'
|
||||
import { useEffect } from 'react'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
|
||||
export default function Sketching() {
|
||||
const { deferredSetCode, buttonDownInStream } = useStore((s) => ({
|
||||
deferredSetCode: s.deferredSetCode,
|
||||
buttonDownInStream: s.buttonDownInStream,
|
||||
}))
|
||||
const buttonDownInStream = useStore((s) => s.buttonDownInStream)
|
||||
const dismiss = useDismiss()
|
||||
const next = useNextClick(onboardingPaths.FUTURE_WORK)
|
||||
|
||||
useEffect(() => {
|
||||
deferredSetCode('')
|
||||
}, [deferredSetCode])
|
||||
kclManager.setCode('')
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="fixed grid justify-center items-end inset-0 z-50 pointer-events-none">
|
||||
|
455
src/useStore.ts
455
src/useStore.ts
@ -5,30 +5,36 @@ import {
|
||||
parse,
|
||||
Program,
|
||||
_executor,
|
||||
recast,
|
||||
ProgramMemory,
|
||||
Position,
|
||||
PathToNode,
|
||||
Rotation,
|
||||
SourceRange,
|
||||
} from './lang/wasm'
|
||||
import { getNodeFromPath } from './lang/queryAst'
|
||||
import { enginelessExecutor } from './lib/testHelpers'
|
||||
import { EditorSelection } from '@codemirror/state'
|
||||
import { EngineCommandManager } from './lang/std/engineConnection'
|
||||
import { KCLError } from './lang/errors'
|
||||
import { deferExecution } from 'lib/utils'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
import { engineCommandManager } from './lang/std/engineConnection'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { DefaultPlanes } from './wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
import { initDefaultPlanes } from './hooks/useAppMode'
|
||||
|
||||
export type Axis = 'y-axis' | 'x-axis' | 'z-axis'
|
||||
|
||||
export type Selection = {
|
||||
type: 'default' | 'line-end' | 'line-mid'
|
||||
type:
|
||||
| 'default'
|
||||
| 'line-end'
|
||||
| 'line-mid'
|
||||
| 'face'
|
||||
| 'point'
|
||||
| 'edge'
|
||||
| 'line'
|
||||
| 'arc'
|
||||
| 'all'
|
||||
range: SourceRange
|
||||
}
|
||||
export type Selections = {
|
||||
otherSelections: ('y-axis' | 'x-axis' | 'z-axis')[]
|
||||
otherSelections: Axis[]
|
||||
codeBasedSelections: Selection[]
|
||||
}
|
||||
export type ToolTip =
|
||||
@ -63,54 +69,6 @@ export const toolTips = [
|
||||
'angledLineThatIntersects',
|
||||
] as any as ToolTip[]
|
||||
|
||||
export type GuiModes =
|
||||
| {
|
||||
mode: 'default'
|
||||
}
|
||||
| {
|
||||
mode: 'sketch'
|
||||
sketchMode: ToolTip
|
||||
isTooltip: true
|
||||
waitingFirstClick: boolean
|
||||
rotation: Rotation
|
||||
position: Position
|
||||
pathId: string
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
| {
|
||||
mode: 'sketch'
|
||||
sketchMode: 'sketchEdit'
|
||||
rotation: Rotation
|
||||
position: Position
|
||||
pathToNode: PathToNode
|
||||
pathId: string
|
||||
}
|
||||
| {
|
||||
mode: 'sketch'
|
||||
sketchMode: 'enterSketchEdit'
|
||||
rotation: Rotation
|
||||
position: Position
|
||||
pathToNode: PathToNode
|
||||
pathId: string
|
||||
}
|
||||
| {
|
||||
mode: 'sketch'
|
||||
sketchMode: 'selectFace'
|
||||
}
|
||||
| {
|
||||
mode: 'canEditSketch'
|
||||
pathId: string
|
||||
pathToNode: PathToNode
|
||||
rotation: Rotation
|
||||
position: Position
|
||||
}
|
||||
| {
|
||||
mode: 'canEditExtrude'
|
||||
pathToNode: PathToNode
|
||||
rotation: Rotation
|
||||
position: Position
|
||||
}
|
||||
|
||||
export type PaneType =
|
||||
| 'code'
|
||||
| 'variables'
|
||||
@ -119,50 +77,15 @@ export type PaneType =
|
||||
| 'logs'
|
||||
| 'lspMessages'
|
||||
|
||||
export interface SelectionRangeTypeMap {
|
||||
[key: number]: Selection['type']
|
||||
}
|
||||
|
||||
export interface StoreState {
|
||||
editorView: EditorView | null
|
||||
setEditorView: (editorView: EditorView) => void
|
||||
highlightRange: [number, number]
|
||||
setHighlightRange: (range: Selection['range']) => void
|
||||
setCursor: (selections: Selections) => void
|
||||
setCursor2: (a?: Selection) => void
|
||||
selectionRanges: Selections
|
||||
selectionRangeTypeMap: { [key: number]: Selection['type'] }
|
||||
setSelectionRanges: (range: Selections) => void
|
||||
guiMode: GuiModes
|
||||
lastGuiMode: GuiModes
|
||||
setGuiMode: (guiMode: GuiModes) => void
|
||||
logs: string[]
|
||||
addLog: (log: string) => void
|
||||
setLogs: (logs: string[]) => void
|
||||
kclErrors: KCLError[]
|
||||
addKCLError: (err: KCLError) => void
|
||||
setErrors: (errors: KCLError[]) => void
|
||||
resetKCLErrors: () => void
|
||||
ast: Program
|
||||
setAst: (ast: Program) => void
|
||||
executeAst: (ast?: Program) => void
|
||||
executeAstMock: (ast?: Program) => void
|
||||
updateAst: (
|
||||
ast: Program,
|
||||
execute: boolean,
|
||||
optionalParams?: {
|
||||
focusPath?: PathToNode
|
||||
callBack?: (ast: Program) => void
|
||||
}
|
||||
) => void
|
||||
updateAstAsync: (
|
||||
ast: Program,
|
||||
reexecute: boolean,
|
||||
focusPath?: PathToNode
|
||||
) => void
|
||||
code: string
|
||||
setCode: (code: string) => void
|
||||
deferredSetCode: (code: string) => void
|
||||
executeCode: (code?: string, force?: boolean) => void
|
||||
formatCode: () => void
|
||||
programMemory: ProgramMemory
|
||||
setProgramMemory: (programMemory: ProgramMemory) => void
|
||||
isShiftDown: boolean
|
||||
setIsShiftDown: (isShiftDown: boolean) => void
|
||||
mediaStream?: MediaStream
|
||||
@ -182,12 +105,6 @@ export interface StoreState {
|
||||
streamWidth: number
|
||||
streamHeight: number
|
||||
}) => void
|
||||
isExecuting: boolean
|
||||
setIsExecuting: (isExecuting: boolean) => void
|
||||
defaultPlanes: DefaultPlanes | null
|
||||
setDefaultPlanes: (defaultPlanes: DefaultPlanes) => void
|
||||
currentPlane: string | null
|
||||
setCurrentPlane: (currentPlane: string) => void
|
||||
|
||||
showHomeMenu: boolean
|
||||
setHomeShowMenu: (showMenu: boolean) => void
|
||||
@ -202,18 +119,9 @@ export interface StoreState {
|
||||
setHomeMenuItems: (items: { name: string; path: string }[]) => void
|
||||
}
|
||||
|
||||
let pendingAstUpdates: number[] = []
|
||||
|
||||
export const useStore = create<StoreState>()(
|
||||
persist(
|
||||
(set, get) => {
|
||||
// We defer this so that likely our ast has caught up to the code.
|
||||
// If we are making changes that are not reflected in the ast, we
|
||||
// should not be updating the ast.
|
||||
const setDeferredCode = deferExecution((code: string) => {
|
||||
set({ code })
|
||||
get().executeCode(code)
|
||||
}, 600)
|
||||
return {
|
||||
editorView: null,
|
||||
setEditorView: (editorView) => {
|
||||
@ -227,243 +135,6 @@ export const useStore = create<StoreState>()(
|
||||
editorView.dispatch({ effects: addLineHighlight.of(selection) })
|
||||
}
|
||||
},
|
||||
executeCode: async (code, force) => {
|
||||
if (!get().defaultPlanes) {
|
||||
let defaultPlanes = await initDefaultPlanes(
|
||||
engineCommandManager,
|
||||
true
|
||||
)
|
||||
if (!defaultPlanes) return
|
||||
get().setDefaultPlanes(defaultPlanes)
|
||||
}
|
||||
|
||||
const result = await executeCode({
|
||||
code: code || get().code,
|
||||
lastAst: get().ast,
|
||||
engineCommandManager: engineCommandManager,
|
||||
defaultPlanes: get().defaultPlanes!,
|
||||
force,
|
||||
})
|
||||
if (!result.isChange) {
|
||||
return
|
||||
}
|
||||
set({
|
||||
ast: result.ast,
|
||||
logs: result.logs,
|
||||
kclErrors: result.errors,
|
||||
programMemory: result.programMemory,
|
||||
})
|
||||
},
|
||||
setCursor: (selections) => {
|
||||
const { editorView } = get()
|
||||
if (!editorView) return
|
||||
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
|
||||
const selectionRangeTypeMap: { [key: number]: Selection['type'] } = {}
|
||||
set({ selectionRangeTypeMap })
|
||||
selections.codeBasedSelections.forEach(({ range, type }) => {
|
||||
if (range?.[1]) {
|
||||
ranges.push(EditorSelection.cursor(range[1]))
|
||||
selectionRangeTypeMap[range[1]] = type
|
||||
}
|
||||
})
|
||||
setTimeout(() => {
|
||||
ranges.length &&
|
||||
editorView.dispatch({
|
||||
selection: EditorSelection.create(
|
||||
ranges,
|
||||
selections.codeBasedSelections.length - 1
|
||||
),
|
||||
})
|
||||
})
|
||||
},
|
||||
setCursor2: (codeSelections) => {
|
||||
const currestSelections = get().selectionRanges
|
||||
const code = get().code
|
||||
if (!codeSelections) {
|
||||
get().setCursor({
|
||||
otherSelections: currestSelections.otherSelections,
|
||||
codeBasedSelections: [
|
||||
{
|
||||
range: [0, code.length ? code.length - 1 : 0],
|
||||
type: 'default',
|
||||
},
|
||||
],
|
||||
})
|
||||
return
|
||||
}
|
||||
const selections: Selections = {
|
||||
...currestSelections,
|
||||
codeBasedSelections: get().isShiftDown
|
||||
? [...currestSelections.codeBasedSelections, codeSelections]
|
||||
: [codeSelections],
|
||||
}
|
||||
get().setCursor(selections)
|
||||
},
|
||||
selectionRangeTypeMap: {},
|
||||
selectionRanges: {
|
||||
otherSelections: [],
|
||||
codeBasedSelections: [],
|
||||
},
|
||||
setSelectionRanges: (selectionRanges) =>
|
||||
set({ selectionRanges, selectionRangeTypeMap: {} }),
|
||||
guiMode: { mode: 'default' },
|
||||
lastGuiMode: { mode: 'default' },
|
||||
setGuiMode: (guiMode) => {
|
||||
set({ guiMode })
|
||||
},
|
||||
logs: [],
|
||||
addLog: (log) => {
|
||||
if (Array.isArray(log)) {
|
||||
const cleanLog: any = log.map(({ __geoMeta, ...rest }) => rest)
|
||||
set((state) => ({ logs: [...state.logs, cleanLog] }))
|
||||
} else {
|
||||
set((state) => ({ logs: [...state.logs, log] }))
|
||||
}
|
||||
},
|
||||
setLogs: (logs) => {
|
||||
set({ logs })
|
||||
},
|
||||
kclErrors: [],
|
||||
addKCLError: (e) => {
|
||||
set((state) => ({ kclErrors: [...state.kclErrors, e] }))
|
||||
},
|
||||
resetKCLErrors: () => {
|
||||
set({ kclErrors: [] })
|
||||
},
|
||||
setErrors: (errors) => {
|
||||
set({ kclErrors: errors })
|
||||
},
|
||||
ast: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
body: [],
|
||||
nonCodeMeta: {
|
||||
nonCodeNodes: {},
|
||||
start: null,
|
||||
},
|
||||
},
|
||||
setAst: (ast) => {
|
||||
set({ ast })
|
||||
},
|
||||
executeAst: async (ast) => {
|
||||
const _ast = ast || get().ast
|
||||
if (!get().isStreamReady) return
|
||||
if (!get().defaultPlanes) {
|
||||
let defaultPlanes = await initDefaultPlanes(
|
||||
engineCommandManager,
|
||||
true
|
||||
)
|
||||
if (!defaultPlanes) return
|
||||
get().setDefaultPlanes(defaultPlanes)
|
||||
}
|
||||
|
||||
set({ isExecuting: true })
|
||||
const { logs, errors, programMemory } = await executeAst({
|
||||
ast: _ast,
|
||||
engineCommandManager,
|
||||
defaultPlanes: get().defaultPlanes!,
|
||||
})
|
||||
set({
|
||||
programMemory,
|
||||
logs,
|
||||
kclErrors: errors,
|
||||
isExecuting: false,
|
||||
})
|
||||
},
|
||||
executeAstMock: async (ast) => {
|
||||
const _ast = ast || get().ast
|
||||
if (!get().isStreamReady) return
|
||||
|
||||
if (!get().defaultPlanes) {
|
||||
let defaultPlanes = await initDefaultPlanes(
|
||||
engineCommandManager,
|
||||
true
|
||||
)
|
||||
if (!defaultPlanes) return
|
||||
get().setDefaultPlanes(defaultPlanes)
|
||||
}
|
||||
|
||||
const { logs, errors, programMemory } = await executeAst({
|
||||
ast: _ast,
|
||||
engineCommandManager,
|
||||
useFakeExecutor: true,
|
||||
defaultPlanes: get().defaultPlanes!,
|
||||
})
|
||||
set({
|
||||
programMemory,
|
||||
logs,
|
||||
kclErrors: errors,
|
||||
isExecuting: false,
|
||||
})
|
||||
},
|
||||
updateAst: async (
|
||||
ast,
|
||||
reexecute,
|
||||
{ focusPath, callBack = () => {} } = {}
|
||||
) => {
|
||||
const newCode = recast(ast)
|
||||
const astWithUpdatedSource = parse(newCode)
|
||||
callBack(astWithUpdatedSource)
|
||||
|
||||
set({
|
||||
ast: astWithUpdatedSource,
|
||||
code: newCode,
|
||||
})
|
||||
if (focusPath) {
|
||||
const { node } = getNodeFromPath<any>(
|
||||
astWithUpdatedSource,
|
||||
focusPath
|
||||
)
|
||||
const { start, end } = node
|
||||
if (!start || !end) return
|
||||
setTimeout(() => {
|
||||
get().setCursor({
|
||||
codeBasedSelections: [
|
||||
{
|
||||
type: 'default',
|
||||
range: [start, end],
|
||||
},
|
||||
],
|
||||
otherSelections: [],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (reexecute) {
|
||||
// Call execute on the set ast.
|
||||
get().executeAst(astWithUpdatedSource)
|
||||
} else {
|
||||
// When we don't re-execute, we still want to update the program
|
||||
// memory with the new ast. So we will hit the mock executor
|
||||
// instead.
|
||||
get().executeAstMock(astWithUpdatedSource)
|
||||
}
|
||||
},
|
||||
updateAstAsync: async (ast, reexecute, focusPath) => {
|
||||
// clear any pending updates
|
||||
pendingAstUpdates.forEach((id) => clearTimeout(id))
|
||||
pendingAstUpdates = []
|
||||
// setup a new update
|
||||
pendingAstUpdates.push(
|
||||
setTimeout(() => {
|
||||
get().updateAst(ast, reexecute, { focusPath })
|
||||
}, 100) as unknown as number
|
||||
)
|
||||
},
|
||||
code: bracket,
|
||||
setCode: (code) => set({ code }),
|
||||
deferredSetCode: (code) => {
|
||||
set({ code })
|
||||
setDeferredCode(code)
|
||||
},
|
||||
formatCode: async () => {
|
||||
const code = get().code
|
||||
const ast = parse(code)
|
||||
const newCode = recast(ast)
|
||||
set({ code: newCode, ast })
|
||||
},
|
||||
programMemory: { root: {}, return: null },
|
||||
setProgramMemory: (programMemory) => set({ programMemory }),
|
||||
isShiftDown: false,
|
||||
setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
|
||||
setMediaStream: (mediaStream) => set({ mediaStream }),
|
||||
@ -486,12 +157,6 @@ export const useStore = create<StoreState>()(
|
||||
setStreamDimensions: (streamDimensions) => {
|
||||
set({ streamDimensions })
|
||||
},
|
||||
isExecuting: false,
|
||||
setIsExecuting: (isExecuting) => set({ isExecuting }),
|
||||
defaultPlanes: null,
|
||||
setDefaultPlanes: (defaultPlanes) => set({ defaultPlanes }),
|
||||
currentPlane: null,
|
||||
setCurrentPlane: (currentPlane) => set({ currentPlane }),
|
||||
|
||||
// tauri specific app settings
|
||||
defaultDir: {
|
||||
@ -511,9 +176,7 @@ export const useStore = create<StoreState>()(
|
||||
name: 'store',
|
||||
partialize: (state) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(state).filter(([key]) =>
|
||||
['code', 'openPanes'].includes(key)
|
||||
)
|
||||
Object.entries(state).filter(([key]) => ['openPanes'].includes(key))
|
||||
),
|
||||
}
|
||||
)
|
||||
@ -547,7 +210,7 @@ const defaultProgramMemory: ProgramMemory['root'] = {
|
||||
},
|
||||
}
|
||||
|
||||
async function executeCode({
|
||||
export async function executeCode({
|
||||
engineCommandManager,
|
||||
code,
|
||||
lastAst,
|
||||
@ -618,7 +281,7 @@ async function executeCode({
|
||||
}
|
||||
}
|
||||
|
||||
async function executeAst({
|
||||
export async function executeAst({
|
||||
ast,
|
||||
engineCommandManager,
|
||||
defaultPlanes,
|
||||
@ -682,3 +345,79 @@ async function executeAst({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function dispatchCodeMirrorCursor({
|
||||
selections,
|
||||
editorView,
|
||||
}: {
|
||||
selections: Selections
|
||||
editorView: EditorView
|
||||
}): {
|
||||
selectionRangeTypeMap: SelectionRangeTypeMap
|
||||
} {
|
||||
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
|
||||
const selectionRangeTypeMap: SelectionRangeTypeMap = {}
|
||||
selections.codeBasedSelections.forEach(({ range, type }) => {
|
||||
if (range?.[1]) {
|
||||
ranges.push(EditorSelection.cursor(range[1]))
|
||||
selectionRangeTypeMap[range[1]] = type
|
||||
}
|
||||
})
|
||||
setTimeout(() => {
|
||||
ranges.length &&
|
||||
editorView.dispatch({
|
||||
selection: EditorSelection.create(
|
||||
ranges,
|
||||
selections.codeBasedSelections.length - 1
|
||||
),
|
||||
})
|
||||
})
|
||||
return {
|
||||
selectionRangeTypeMap,
|
||||
}
|
||||
}
|
||||
|
||||
export function setCodeMirrorCursor({
|
||||
codeSelection,
|
||||
currestSelections,
|
||||
editorView,
|
||||
isShiftDown,
|
||||
}: {
|
||||
codeSelection?: Selection
|
||||
currestSelections: Selections
|
||||
editorView: EditorView
|
||||
isShiftDown: boolean
|
||||
}): SelectionRangeTypeMap {
|
||||
// This DOES NOT set the `selectionRanges` in xstate context
|
||||
// instead it updates/dispatches to the editor, which in turn updates the xstate context
|
||||
// I've found this the best way to deal with the editor without causing an infinite loop
|
||||
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
|
||||
// because we want to respect the user manually placing the cursor too.
|
||||
const code = kclManager.code
|
||||
if (!codeSelection) {
|
||||
const selectionRangeTypeMap = dispatchCodeMirrorCursor({
|
||||
editorView,
|
||||
selections: {
|
||||
otherSelections: currestSelections.otherSelections,
|
||||
codeBasedSelections: [
|
||||
{
|
||||
range: [0, code.length ? code.length - 1 : 0],
|
||||
type: 'default',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
return selectionRangeTypeMap
|
||||
}
|
||||
const selections: Selections = {
|
||||
...currestSelections,
|
||||
codeBasedSelections: isShiftDown
|
||||
? [...currestSelections.codeBasedSelections, codeSelection]
|
||||
: [codeSelection],
|
||||
}
|
||||
const selectionRangeTypeMap = dispatchCodeMirrorCursor({
|
||||
editorView,
|
||||
selections,
|
||||
})
|
||||
return selectionRangeTypeMap
|
||||
}
|
||||
|
52
src/wasm-lib/Cargo.lock
generated
52
src/wasm-lib/Cargo.lock
generated
@ -156,7 +156,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -167,7 +167,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -472,7 +472,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -692,7 +692,7 @@ dependencies = [
|
||||
"quote",
|
||||
"serde",
|
||||
"serde_tokenstream",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -706,7 +706,7 @@ dependencies = [
|
||||
"quote",
|
||||
"serde",
|
||||
"serde_tokenstream",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -976,7 +976,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1733,7 +1733,7 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
|
||||
[[package]]
|
||||
name = "openapitor"
|
||||
version = "0.0.9"
|
||||
source = "git+https://github.com/KittyCAD/kittycad.rs?branch=main#fa0345c514fcc9ae6cd74ae35c8e5c2800fec34f"
|
||||
source = "git+https://github.com/KittyCAD/kittycad.rs?branch=main#c122a9b1d6afe51c25e545b5e0bbeb91d367e6d2"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"anyhow",
|
||||
@ -1879,7 +1879,7 @@ dependencies = [
|
||||
"regex",
|
||||
"regex-syntax 0.7.5",
|
||||
"structmeta",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1914,7 +1914,7 @@ dependencies = [
|
||||
"bincode",
|
||||
"either",
|
||||
"fnv",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.10.5",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"quick-xml",
|
||||
@ -1943,7 +1943,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2573,7 +2573,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2607,7 +2607,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2619,7 +2619,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2836,7 +2836,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"structmeta-derive",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2847,7 +2847,7 @@ checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2885,9 +2885,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.37"
|
||||
version = "2.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8"
|
||||
checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2996,7 +2996,7 @@ checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3102,7 +3102,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3218,7 +3218,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3247,7 +3247,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3333,7 +3333,7 @@ dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
@ -3535,7 +3535,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -3570,7 +3570,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
"syn 2.0.38",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@ -3830,9 +3830,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.15"
|
||||
version = "0.5.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc"
|
||||
checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -16,7 +16,7 @@ proc-macro2 = "1"
|
||||
quote = "1"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde_tokenstream = "0.2"
|
||||
syn = { version = "2.0.37", features = ["full"] }
|
||||
syn = { version = "2.0.38", features = ["full"] }
|
||||
|
||||
[dev-dependencies]
|
||||
expectorate = "1.1.0"
|
||||
|
@ -24,7 +24,7 @@ serde_json = "1.0.107"
|
||||
thiserror = "1.0.49"
|
||||
ts-rs = { version = "7", package = "ts-rs-json-value", features = ["serde-json-impl", "schemars-impl", "uuid-impl"] }
|
||||
uuid = { version = "1.4.1", features = ["v4", "js", "serde"] }
|
||||
winnow = "0.5.15"
|
||||
winnow = "0.5.16"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
js-sys = { version = "0.3.64" }
|
||||
|
@ -6,36 +6,30 @@ pub fn bench_lex(c: &mut Criterion) {
|
||||
c.bench_function("lex_pipes_on_pipes", |b| b.iter(|| lex(PIPES_PROGRAM)));
|
||||
}
|
||||
|
||||
pub fn bench_lex_parse(c: &mut Criterion) {
|
||||
c.bench_function("parse_lex_cube", |b| b.iter(|| lex_and_parse(CUBE_PROGRAM)));
|
||||
c.bench_function("parse_lex_big_kitt", |b| b.iter(|| lex_and_parse(KITT_PROGRAM)));
|
||||
c.bench_function("parse_lex_pipes_on_pipes", |b| b.iter(|| lex_and_parse(PIPES_PROGRAM)));
|
||||
pub fn bench_parse(c: &mut Criterion) {
|
||||
for (name, file) in [
|
||||
("pipes_on_pipes", PIPES_PROGRAM),
|
||||
("big_kitt", KITT_PROGRAM),
|
||||
("cube", CUBE_PROGRAM),
|
||||
] {
|
||||
let tokens = kcl_lib::token::lexer(file);
|
||||
c.bench_function(&format!("parse_{name}"), move |b| {
|
||||
let tok = tokens.clone();
|
||||
b.iter(move || {
|
||||
let parser = kcl_lib::parser::Parser::new(tok.clone());
|
||||
black_box(parser.ast().unwrap());
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn lex(program: &str) {
|
||||
black_box(kcl_lib::token::lexer(program));
|
||||
}
|
||||
|
||||
fn lex_and_parse(program: &str) {
|
||||
let tokens = kcl_lib::token::lexer(program);
|
||||
let parser = kcl_lib::parser::Parser::new(tokens);
|
||||
black_box(parser.ast().unwrap());
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_lex, bench_lex_parse);
|
||||
criterion_group!(benches, bench_lex, bench_parse);
|
||||
criterion_main!(benches);
|
||||
|
||||
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
|
||||
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
|
||||
const CUBE_PROGRAM: &str = r#"fn cube = (pos, scale) => {
|
||||
const sg = startSketchAt(pos)
|
||||
|> line([0, scale], %)
|
||||
|> line([scale, 0], %)
|
||||
|> line([0, -scale], %)
|
||||
|
||||
return sg
|
||||
}
|
||||
|
||||
const b1 = cube([0,0], 10)
|
||||
const pt1 = b1[0]
|
||||
show(b1)"#;
|
||||
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
|
||||
|
@ -82,7 +82,10 @@ impl Program {
|
||||
};
|
||||
|
||||
let custom_white_space_or_comment = match self.non_code_meta.non_code_nodes.get(&index) {
|
||||
Some(custom_white_space_or_comment) => custom_white_space_or_comment.format(&indentation),
|
||||
Some(noncodes) => noncodes
|
||||
.iter()
|
||||
.map(|custom_white_space_or_comment| custom_white_space_or_comment.format(&indentation))
|
||||
.collect::<String>(),
|
||||
None => String::new(),
|
||||
};
|
||||
let end_string = if custom_white_space_or_comment.is_empty() {
|
||||
@ -707,30 +710,35 @@ pub struct NonCodeNode {
|
||||
impl NonCodeNode {
|
||||
pub fn value(&self) -> String {
|
||||
match &self.value {
|
||||
NonCodeValue::InlineComment { value } => value.clone(),
|
||||
NonCodeValue::BlockComment { value } => value.clone(),
|
||||
NonCodeValue::NewLineBlockComment { value } => value.clone(),
|
||||
NonCodeValue::InlineComment { value, style: _ } => value.clone(),
|
||||
NonCodeValue::BlockComment { value, style: _ } => value.clone(),
|
||||
NonCodeValue::NewLineBlockComment { value, style: _ } => value.clone(),
|
||||
NonCodeValue::NewLine => "\n\n".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(&self, indentation: &str) -> String {
|
||||
match &self.value {
|
||||
NonCodeValue::InlineComment { value } => format!(" // {}\n", value),
|
||||
NonCodeValue::BlockComment { value } => {
|
||||
NonCodeValue::InlineComment {
|
||||
value,
|
||||
style: CommentStyle::Line,
|
||||
} => format!(" // {}\n", value),
|
||||
NonCodeValue::InlineComment {
|
||||
value,
|
||||
style: CommentStyle::Block,
|
||||
} => format!(" /* {} */", value),
|
||||
NonCodeValue::BlockComment { value, style } => {
|
||||
let add_start_new_line = if self.start == 0 { "" } else { "\n" };
|
||||
if value.contains('\n') {
|
||||
format!("{}{}/* {} */\n", add_start_new_line, indentation, value)
|
||||
} else {
|
||||
format!("{}{}// {}\n", add_start_new_line, indentation, value)
|
||||
match style {
|
||||
CommentStyle::Block => format!("{}{}/* {} */\n", add_start_new_line, indentation, value),
|
||||
CommentStyle::Line => format!("{}{}// {}\n", add_start_new_line, indentation, value),
|
||||
}
|
||||
}
|
||||
NonCodeValue::NewLineBlockComment { value } => {
|
||||
NonCodeValue::NewLineBlockComment { value, style } => {
|
||||
let add_start_new_line = if self.start == 0 { "" } else { "\n\n" };
|
||||
if value.contains('\n') {
|
||||
format!("{}{}/* {} */\n", add_start_new_line, indentation, value)
|
||||
} else {
|
||||
format!("{}{}// {}\n", add_start_new_line, indentation, value)
|
||||
match style {
|
||||
CommentStyle::Block => format!("{}{}/* {} */\n", add_start_new_line, indentation, value),
|
||||
CommentStyle::Line => format!("{}{}// {}\n", add_start_new_line, indentation, value),
|
||||
}
|
||||
}
|
||||
NonCodeValue::NewLine => "\n\n".to_string(),
|
||||
@ -738,14 +746,27 @@ impl NonCodeNode {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum CommentStyle {
|
||||
/// Like // foo
|
||||
Line,
|
||||
/// Like /* foo */
|
||||
Block,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum NonCodeValue {
|
||||
/// An inline comment.
|
||||
/// An example of this is the following: `1 + 1 // This is an inline comment`.
|
||||
/// Here are examples:
|
||||
/// `1 + 1 // This is an inline comment`.
|
||||
/// `1 + 1 /* Here's another */`.
|
||||
InlineComment {
|
||||
value: String,
|
||||
style: CommentStyle,
|
||||
},
|
||||
/// A block comment.
|
||||
/// An example of this is the following:
|
||||
@ -759,11 +780,13 @@ pub enum NonCodeValue {
|
||||
/// If it did it would be a `NewLineBlockComment`.
|
||||
BlockComment {
|
||||
value: String,
|
||||
style: CommentStyle,
|
||||
},
|
||||
/// A block comment that has a new line above it.
|
||||
/// The user explicitly added a new line above the block comment.
|
||||
NewLineBlockComment {
|
||||
value: String,
|
||||
style: CommentStyle,
|
||||
},
|
||||
// A new line like `\n\n` NOT a new line like `\n`.
|
||||
// This is also not a comment.
|
||||
@ -774,7 +797,7 @@ pub enum NonCodeValue {
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NonCodeMeta {
|
||||
pub non_code_nodes: HashMap<usize, NonCodeNode>,
|
||||
pub non_code_nodes: HashMap<usize, Vec<NonCodeNode>>,
|
||||
pub start: Option<NonCodeNode>,
|
||||
}
|
||||
|
||||
@ -795,7 +818,10 @@ impl<'de> Deserialize<'de> for NonCodeMeta {
|
||||
let helper = NonCodeMetaHelper::deserialize(deserializer)?;
|
||||
let mut non_code_nodes = HashMap::new();
|
||||
for (key, value) in helper.non_code_nodes {
|
||||
non_code_nodes.insert(key.parse().map_err(serde::de::Error::custom)?, value);
|
||||
non_code_nodes
|
||||
.entry(key.parse().map_err(serde::de::Error::custom)?)
|
||||
.or_insert(Vec::new())
|
||||
.push(value);
|
||||
}
|
||||
Ok(NonCodeMeta {
|
||||
non_code_nodes,
|
||||
@ -804,6 +830,12 @@ impl<'de> Deserialize<'de> for NonCodeMeta {
|
||||
}
|
||||
}
|
||||
|
||||
impl NonCodeMeta {
|
||||
pub fn insert(&mut self, i: usize, new: NonCodeNode) {
|
||||
self.non_code_nodes.entry(i).or_default().push(new);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
@ -2385,7 +2417,9 @@ impl PipeExpression {
|
||||
let mut s = statement.recast(options, indentation_level + 1, true);
|
||||
let non_code_meta = self.non_code_meta.clone();
|
||||
if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
|
||||
s += non_code_meta_value.format(&indentation).trim_end_matches('\n')
|
||||
for val in non_code_meta_value {
|
||||
s += val.format(&indentation).trim_end_matches('\n')
|
||||
}
|
||||
}
|
||||
|
||||
if index != self.body.len() - 1 {
|
||||
@ -2869,9 +2903,9 @@ show(part001)"#;
|
||||
recasted,
|
||||
r#"fn myFn = () => {
|
||||
// this is a comment
|
||||
const yo = { a: { b: { c: '123' } } }
|
||||
/* block
|
||||
const yo = { a: { b: { c: '123' } } } /* block
|
||||
comment */
|
||||
|
||||
const key = 'c'
|
||||
// this is also a comment
|
||||
return things
|
||||
@ -2913,14 +2947,13 @@ const mySk1 = startSketchOn('XY')
|
||||
|> lineTo({ to: [0, 1], tag: 'myTag' }, %)
|
||||
|> lineTo([1, 1], %)
|
||||
/* and
|
||||
here
|
||||
|
||||
a comment between pipe expression statements */
|
||||
here */
|
||||
// a comment between pipe expression statements
|
||||
|> rx(90, %)
|
||||
// and another with just white space between others below
|
||||
|> ry(45, %)
|
||||
|> rx(45, %)
|
||||
// one more for good measure
|
||||
// one more for good measure
|
||||
"#
|
||||
);
|
||||
}
|
||||
@ -2988,16 +3021,19 @@ const things = "things"
|
||||
let program = parser.ast().unwrap();
|
||||
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
assert_eq!(recasted.trim(), some_program_string.trim());
|
||||
let expected = some_program_string.trim();
|
||||
// Currently new parser removes an empty line
|
||||
let actual = recasted.trim();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recast_comment_tokens_inside_strings() {
|
||||
let some_program_string = r#"let b = {
|
||||
"end": 141,
|
||||
"start": 125,
|
||||
"type": "NonCodeNode",
|
||||
"value": "
|
||||
end: 141,
|
||||
start: 125,
|
||||
type: "NonCodeNode",
|
||||
value: "
|
||||
// a comment
|
||||
"
|
||||
}"#;
|
||||
|
@ -4,7 +4,7 @@ use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
|
||||
|
||||
use crate::executor::SourceRange;
|
||||
|
||||
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS)]
|
||||
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum KclError {
|
||||
@ -28,7 +28,7 @@ pub enum KclError {
|
||||
Engine(KclErrorDetails),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ts_rs::TS)]
|
||||
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone)]
|
||||
#[ts(export)]
|
||||
pub struct KclErrorDetails {
|
||||
#[serde(rename = "sourceRanges")]
|
||||
@ -78,6 +78,22 @@ impl KclError {
|
||||
KclError::Engine(e) => e.source_ranges.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the inner error message.
|
||||
pub fn message(&self) -> &str {
|
||||
match &self {
|
||||
KclError::Syntax(e) => &e.message,
|
||||
KclError::Semantic(e) => &e.message,
|
||||
KclError::Type(e) => &e.message,
|
||||
KclError::Unimplemented(e) => &e.message,
|
||||
KclError::Unexpected(e) => &e.message,
|
||||
KclError::ValueAlreadyDefined(e) => &e.message,
|
||||
KclError::UndefinedValue(e) => &e.message,
|
||||
KclError::InvalidExpression(e) => &e.message,
|
||||
KclError::Engine(e) => &e.message,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
|
||||
let (message, _, _) = self.get_message_line_column(code);
|
||||
let source_ranges = self.source_ranges();
|
||||
|
@ -2,7 +2,7 @@ use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use crate::{
|
||||
ast::types::{
|
||||
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, ExpressionStatement,
|
||||
ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, CommentStyle, ExpressionStatement,
|
||||
FunctionExpression, Identifier, Literal, LiteralIdentifier, MemberExpression, MemberObject, NonCodeMeta,
|
||||
NonCodeNode, NonCodeValue, ObjectExpression, ObjectKeyInfo, ObjectProperty, PipeExpression, PipeSubstitution,
|
||||
Program, ReturnStatement, UnaryExpression, UnaryOperator, Value, VariableDeclaration, VariableDeclarator,
|
||||
@ -13,6 +13,8 @@ use crate::{
|
||||
token::{Token, TokenType},
|
||||
};
|
||||
|
||||
mod parser_impl;
|
||||
|
||||
pub const PIPE_SUBSTITUTION_OPERATOR: &str = "%";
|
||||
pub const PIPE_OPERATOR: &str = "|>";
|
||||
|
||||
@ -180,24 +182,7 @@ impl Parser {
|
||||
}
|
||||
|
||||
pub fn ast(&self) -> Result<Program, KclError> {
|
||||
let body = self.make_body(
|
||||
0,
|
||||
vec![],
|
||||
NonCodeMeta {
|
||||
non_code_nodes: HashMap::new(),
|
||||
start: None,
|
||||
},
|
||||
)?;
|
||||
let end = match self.get_token(body.last_index) {
|
||||
Ok(token) => token.end,
|
||||
Err(_) => self.tokens[self.tokens.len() - 1].end,
|
||||
};
|
||||
Ok(Program {
|
||||
start: 0,
|
||||
end,
|
||||
body: body.body,
|
||||
non_code_meta: body.non_code_meta,
|
||||
})
|
||||
parser_impl::run_parser(&mut self.tokens.as_slice())
|
||||
}
|
||||
|
||||
fn make_identifier(&self, index: usize) -> Result<Identifier, KclError> {
|
||||
@ -209,7 +194,7 @@ impl Parser {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn make_literal(&self, index: usize) -> Result<Literal, KclError> {
|
||||
fn make_literal(&self, index: usize) -> Result<Literal, KclError> {
|
||||
let token = self.get_token(index)?;
|
||||
let value = if token.token_type == TokenType::Number {
|
||||
if let Ok(value) = token.value.parse::<i64>() {
|
||||
@ -295,6 +280,11 @@ impl Parser {
|
||||
));
|
||||
}
|
||||
|
||||
let is_block_style = non_code_tokens
|
||||
.first()
|
||||
.map(|tok| matches!(tok.token_type, TokenType::BlockComment))
|
||||
.unwrap_or_default();
|
||||
|
||||
let full_string = non_code_tokens
|
||||
.iter()
|
||||
.map(|t| {
|
||||
@ -336,11 +326,32 @@ impl Parser {
|
||||
value: if start_end_string.starts_with("\n\n") && is_new_line_comment {
|
||||
// Preserve if they want a whitespace line before the comment.
|
||||
// But let's just allow one.
|
||||
NonCodeValue::NewLineBlockComment { value: full_string }
|
||||
NonCodeValue::NewLineBlockComment {
|
||||
value: full_string,
|
||||
style: if is_block_style {
|
||||
CommentStyle::Block
|
||||
} else {
|
||||
CommentStyle::Line
|
||||
},
|
||||
}
|
||||
} else if is_new_line_comment {
|
||||
NonCodeValue::BlockComment { value: full_string }
|
||||
NonCodeValue::BlockComment {
|
||||
value: full_string,
|
||||
style: if is_block_style {
|
||||
CommentStyle::Block
|
||||
} else {
|
||||
CommentStyle::Line
|
||||
},
|
||||
}
|
||||
} else {
|
||||
NonCodeValue::InlineComment { value: full_string }
|
||||
NonCodeValue::InlineComment {
|
||||
value: full_string,
|
||||
style: if is_block_style {
|
||||
CommentStyle::Block
|
||||
} else {
|
||||
CommentStyle::Line
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
Ok((Some(node), end_index - 1))
|
||||
@ -1064,7 +1075,7 @@ impl Parser {
|
||||
let mut _non_code_meta: NonCodeMeta;
|
||||
if let Some(node) = next_pipe.non_code_node {
|
||||
_non_code_meta = non_code_meta;
|
||||
_non_code_meta.non_code_nodes.insert(previous_values.len(), node);
|
||||
_non_code_meta.insert(previous_values.len(), node);
|
||||
} else {
|
||||
_non_code_meta = non_code_meta;
|
||||
}
|
||||
@ -1435,7 +1446,7 @@ impl Parser {
|
||||
self.make_params(next_brace_or_comma_token.index, _previous_params)
|
||||
}
|
||||
|
||||
pub fn make_unary_expression(&self, index: usize) -> Result<UnaryExpressionResult, KclError> {
|
||||
fn make_unary_expression(&self, index: usize) -> Result<UnaryExpressionResult, KclError> {
|
||||
let current_token = self.get_token(index)?;
|
||||
let next_token = self.next_meaningful_token(index, None)?;
|
||||
if next_token.token.is_none() {
|
||||
@ -1633,7 +1644,7 @@ impl Parser {
|
||||
if previous_body.is_empty() {
|
||||
non_code_meta.start = next_token.non_code_node;
|
||||
} else {
|
||||
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
|
||||
non_code_meta.insert(previous_body.len(), node.clone());
|
||||
}
|
||||
}
|
||||
return self.make_body(next_token.index, previous_body, non_code_meta);
|
||||
@ -1641,14 +1652,14 @@ impl Parser {
|
||||
|
||||
let next = self.next_meaningful_token(token_index, None)?;
|
||||
if let Some(node) = &next.non_code_node {
|
||||
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
|
||||
non_code_meta.insert(previous_body.len(), node.clone());
|
||||
}
|
||||
|
||||
if token.token_type == TokenType::Keyword && VariableKind::from_str(&token.value).is_ok() {
|
||||
let declaration = self.make_variable_declaration(token_index)?;
|
||||
let next_thing = self.next_meaningful_token(declaration.last_index, None)?;
|
||||
if let Some(node) = &next_thing.non_code_node {
|
||||
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
|
||||
non_code_meta.insert(previous_body.len(), node.clone());
|
||||
}
|
||||
let mut _previous_body = previous_body;
|
||||
_previous_body.push(BodyItem::VariableDeclaration(VariableDeclaration {
|
||||
@ -1669,7 +1680,7 @@ impl Parser {
|
||||
let statement = self.make_return_statement(token_index)?;
|
||||
let next_thing = self.next_meaningful_token(statement.last_index, None)?;
|
||||
if let Some(node) = &next_thing.non_code_node {
|
||||
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
|
||||
non_code_meta.insert(previous_body.len(), node.clone());
|
||||
}
|
||||
let mut _previous_body = previous_body;
|
||||
_previous_body.push(BodyItem::ReturnStatement(ReturnStatement {
|
||||
@ -1693,7 +1704,7 @@ impl Parser {
|
||||
let expression = self.make_expression_statement(token_index)?;
|
||||
let next_thing = self.next_meaningful_token(expression.last_index, None)?;
|
||||
if let Some(node) = &next_thing.non_code_node {
|
||||
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
|
||||
non_code_meta.insert(previous_body.len(), node.clone());
|
||||
}
|
||||
let mut _previous_body = previous_body;
|
||||
_previous_body.push(BodyItem::ExpressionStatement(ExpressionStatement {
|
||||
@ -1716,7 +1727,7 @@ impl Parser {
|
||||
&& next_thing_token.token_type == TokenType::Operator
|
||||
{
|
||||
if let Some(node) = &next_thing.non_code_node {
|
||||
non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
|
||||
non_code_meta.insert(previous_body.len(), node.clone());
|
||||
}
|
||||
let expression = self.make_expression_statement(token_index)?;
|
||||
let mut _previous_body = previous_body;
|
||||
@ -1913,6 +1924,7 @@ const key = 'c'"#,
|
||||
end: 60,
|
||||
value: NonCodeValue::BlockComment {
|
||||
value: "this is a comment".to_string(),
|
||||
style: CommentStyle::Line,
|
||||
},
|
||||
}),
|
||||
31,
|
||||
@ -1966,6 +1978,35 @@ const key = 'c'"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_math_parse() {
|
||||
let tokens = crate::token::lexer(r#"5 + "a""#);
|
||||
let actual = Parser::new(tokens).ast().unwrap().body;
|
||||
let expr = BinaryExpression {
|
||||
start: 0,
|
||||
end: 7,
|
||||
operator: BinaryOperator::Add,
|
||||
left: BinaryPart::Literal(Box::new(Literal {
|
||||
start: 0,
|
||||
end: 1,
|
||||
value: serde_json::Value::Number(serde_json::Number::from(5)),
|
||||
raw: "5".to_owned(),
|
||||
})),
|
||||
right: BinaryPart::Literal(Box::new(Literal {
|
||||
start: 4,
|
||||
end: 7,
|
||||
value: serde_json::Value::String("a".to_owned()),
|
||||
raw: r#""a""#.to_owned(),
|
||||
})),
|
||||
};
|
||||
let expected = vec![BodyItem::ExpressionStatement(ExpressionStatement {
|
||||
start: 0,
|
||||
end: 7,
|
||||
expression: Value::BinaryExpression(Box::new(expr)),
|
||||
})];
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_code_token() {
|
||||
let tokens = [
|
||||
@ -2812,10 +2853,6 @@ z(-[["#,
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([1, 2])], message: "missing a closing brace for the function call" }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2831,7 +2868,7 @@ z(-[["#,
|
||||
// https://github.com/KittyCAD/modeling-app/issues/696
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"semantic: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
|
||||
r#"syntax: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
|
||||
);
|
||||
}
|
||||
|
||||
@ -2845,7 +2882,7 @@ z(-[["#,
|
||||
// https://github.com/KittyCAD/modeling-app/issues/696
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"semantic: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
|
||||
r#"syntax: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
|
||||
);
|
||||
}
|
||||
|
||||
@ -2863,7 +2900,7 @@ e
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("expected to be started on a identifier or literal"));
|
||||
.contains("expected whitespace, found ')' which is brace"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2872,7 +2909,11 @@ e
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("expected another token"));
|
||||
assert!(result
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("expected whitespace, found ')' which is brace"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2884,11 +2925,7 @@ e
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("unexpected end of expression"));
|
||||
assert!(result.err().unwrap().to_string().contains("Unexpected token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -3022,7 +3059,9 @@ e
|
||||
|
||||
#[test]
|
||||
fn test_error_stdlib_in_fn_name() {
|
||||
let some_program_string = r#"fn cos = () {}"#;
|
||||
let some_program_string = r#"fn cos = () => {
|
||||
return 1
|
||||
}"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
@ -3123,9 +3162,12 @@ thing(false)
|
||||
let parser = Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
// TODO: https://github.com/KittyCAD/modeling-app/issues/784
|
||||
// Improve this error message.
|
||||
// It should say that the compiler is expecting a function expression on the RHS.
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([0, 2])], message: "Expected a `let` variable kind, found: `fn`" }"#
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([11, 18])], message: "Unexpected token" }"#
|
||||
);
|
||||
}
|
||||
|
||||
@ -3163,15 +3205,6 @@ let other_thing = 2 * cos(3)"#;
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_pipes_on_pipes() {
|
||||
let code = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
|
||||
|
||||
let tokens = crate::token::lexer(code);
|
||||
let parser = Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_negative_arguments() {
|
||||
let some_program_string = r#"fn box = (p, h, l, w) => {
|
||||
|
1967
src/wasm-lib/kcl/src/parser/parser_impl.rs
Normal file
1967
src/wasm-lib/kcl/src/parser/parser_impl.rs
Normal file
File diff suppressed because it is too large
Load Diff
107
src/wasm-lib/kcl/src/parser/parser_impl/error.rs
Normal file
107
src/wasm-lib/kcl/src/parser/parser_impl/error.rs
Normal file
@ -0,0 +1,107 @@
|
||||
use winnow::error::{ErrorKind, ParseError, StrContext};
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
token::Token,
|
||||
};
|
||||
|
||||
/// Accumulate context while backtracking errors
|
||||
/// Very similar to [`winnow::error::ContextError`] type,
|
||||
/// but the 'cause' field is always a [`KclError`],
|
||||
/// instead of a dynamic [`std::error::Error`] trait object.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContextError<C = StrContext> {
|
||||
pub context: Vec<C>,
|
||||
pub cause: Option<KclError>,
|
||||
}
|
||||
|
||||
impl From<ParseError<&[Token], ContextError>> for KclError {
|
||||
fn from(err: ParseError<&[Token], ContextError>) -> Self {
|
||||
let Some(last_token) = err.input().last() else {
|
||||
return KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: Default::default(),
|
||||
message: "file is empty".to_owned(),
|
||||
});
|
||||
};
|
||||
|
||||
let (input, offset, err) = (err.input().to_vec(), err.offset(), err.into_inner());
|
||||
|
||||
if let Some(e) = err.cause {
|
||||
return e;
|
||||
}
|
||||
|
||||
// See docs on `offset`.
|
||||
if offset >= input.len() {
|
||||
let context = err.context.first();
|
||||
return KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: last_token.as_source_ranges(),
|
||||
message: match context {
|
||||
Some(what) => format!("Unexpected end of file. The compiler {what}"),
|
||||
None => "Unexpected end of file while still parsing".to_owned(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let bad_token = &input[offset];
|
||||
// TODO: Add the Winnow parser context to the error.
|
||||
// See https://github.com/KittyCAD/modeling-app/issues/784
|
||||
KclError::Syntax(KclErrorDetails {
|
||||
source_ranges: bad_token.as_source_ranges(),
|
||||
message: "Unexpected token".to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> From<KclError> for ContextError<C> {
|
||||
fn from(e: KclError) -> Self {
|
||||
Self {
|
||||
context: Default::default(),
|
||||
cause: Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> std::default::Default for ContextError<C> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
context: Default::default(),
|
||||
cause: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, C> winnow::error::ParserError<I> for ContextError<C> {
|
||||
#[inline]
|
||||
fn from_error_kind(_input: &I, _kind: ErrorKind) -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn append(self, _input: &I, _kind: ErrorKind) -> Self {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn or(self, other: Self) -> Self {
|
||||
other
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, I> winnow::error::AddContext<I, C> for ContextError<C> {
|
||||
#[inline]
|
||||
fn add_context(mut self, _input: &I, ctx: C) -> Self {
|
||||
self.context.push(ctx);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, I> winnow::error::FromExternalError<I, KclError> for ContextError<C> {
|
||||
#[inline]
|
||||
fn from_external_error(_input: &I, _kind: ErrorKind, e: KclError) -> Self {
|
||||
let mut err = Self::default();
|
||||
{
|
||||
err.cause = Some(e);
|
||||
}
|
||||
err
|
||||
}
|
||||
}
|
@ -6,6 +6,8 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower_lsp::lsp_types::SemanticTokenType;
|
||||
|
||||
use crate::{ast::types::VariableKind, executor::SourceRange};
|
||||
|
||||
mod tokeniser;
|
||||
|
||||
/// The types of tokens.
|
||||
@ -142,15 +144,39 @@ impl Token {
|
||||
TokenType::Whitespace | TokenType::LineComment | TokenType::BlockComment
|
||||
)
|
||||
}
|
||||
|
||||
pub fn as_source_range(&self) -> SourceRange {
|
||||
SourceRange([self.start, self.end])
|
||||
}
|
||||
|
||||
pub fn as_source_ranges(&self) -> Vec<SourceRange> {
|
||||
vec![self.as_source_range()]
|
||||
}
|
||||
|
||||
/// Is this token the beginning of a variable/function declaration?
|
||||
/// If so, what kind?
|
||||
/// If not, returns None.
|
||||
pub fn declaration_keyword(&self) -> Option<VariableKind> {
|
||||
if !matches!(self.token_type, TokenType::Keyword) {
|
||||
return None;
|
||||
}
|
||||
Some(match self.value.as_str() {
|
||||
"var" => VariableKind::Var,
|
||||
"let" => VariableKind::Let,
|
||||
"fn" => VariableKind::Fn,
|
||||
"const" => VariableKind::Const,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Token> for crate::executor::SourceRange {
|
||||
impl From<Token> for SourceRange {
|
||||
fn from(token: Token) -> Self {
|
||||
Self([token.start, token.end])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Token> for crate::executor::SourceRange {
|
||||
impl From<&Token> for SourceRange {
|
||||
fn from(token: &Token) -> Self {
|
||||
Self([token.start, token.end])
|
||||
}
|
||||
|
12
src/wasm-lib/tests/executor/inputs/cube.kcl
Normal file
12
src/wasm-lib/tests/executor/inputs/cube.kcl
Normal file
@ -0,0 +1,12 @@
|
||||
fn cube = (pos, scale) => {
|
||||
const sg = startSketchAt(pos)
|
||||
|> line([0, scale], %)
|
||||
|> line([scale, 0], %)
|
||||
|> line([0, -scale], %)
|
||||
|
||||
return sg
|
||||
}
|
||||
|
||||
const b1 = cube([0,0], 10)
|
||||
const pt1 = b1[0]
|
||||
show(b1)
|
@ -87,7 +87,7 @@ const fnBox = box(3, 6, 10)
|
||||
show(fnBox)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/function_sketch.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/function_sketch.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -107,7 +107,11 @@ async fn serial_test_execute_with_function_sketch_with_position() {
|
||||
show(box([0,0], 3, 6, 10))"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/function_sketch_with_position.png", &result, 1.0);
|
||||
twenty_twenty::assert_image(
|
||||
"tests/executor/outputs/function_sketch_with_position.png",
|
||||
&result,
|
||||
0.999,
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -125,7 +129,7 @@ async fn serial_test_execute_with_angled_line() {
|
||||
show(part001)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/angled_line.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/angled_line.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -152,7 +156,7 @@ const bracket = startSketchOn('XY')
|
||||
show(bracket)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/parametric.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/parametric.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -187,7 +191,7 @@ const bracket = startSketchAt([0, 0])
|
||||
show(bracket)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/parametric_with_tan_arc.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/parametric_with_tan_arc.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -215,7 +219,7 @@ async fn serial_test_execute_pipes_on_pipes() {
|
||||
let code = include_str!("inputs/pipes_on_pipes.kcl");
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/pipes_on_pipes.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/pipes_on_pipes.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -223,7 +227,7 @@ async fn serial_test_execute_kittycad_svg() {
|
||||
let code = include_str!("inputs/kittycad_svg.kcl");
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/kittycad_svg.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/kittycad_svg.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -270,7 +274,7 @@ const body = startSketchOn('XY')
|
||||
show(body)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/close_arc.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/close_arc.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -296,7 +300,7 @@ let thing = box(-12, -15, 10)
|
||||
box(-20, -5, 10)"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/negative_args.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/negative_args.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -309,7 +313,7 @@ async fn test_basic_tangental_arc() {
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/tangental_arc.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/tangental_arc.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -322,7 +326,7 @@ async fn test_basic_tangental_arc_with_point() {
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/tangental_arc_with_point.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/tangental_arc_with_point.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -335,7 +339,7 @@ async fn test_basic_tangental_arc_to() {
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/tangental_arc_to.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/tangental_arc_to.png", &result, 0.999);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -362,7 +366,11 @@ let thing = box(-12, -15, 10, 'yz')
|
||||
box(-20, -5, 10, 'xy')"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/different_planes_same_drawing.png", &result, 1.0);
|
||||
twenty_twenty::assert_image(
|
||||
"tests/executor/outputs/different_planes_same_drawing.png",
|
||||
&result,
|
||||
0.999,
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -421,5 +429,5 @@ const part004 = startSketchOn('YZ')
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
twenty_twenty::assert_image("tests/executor/outputs/lots_of_planes.png", &result, 1.0);
|
||||
twenty_twenty::assert_image("tests/executor/outputs/lots_of_planes.png", &result, 0.999);
|
||||
}
|
||||
|
12
yarn.lock
12
yarn.lock
@ -2218,6 +2218,13 @@
|
||||
loupe "^2.3.6"
|
||||
pretty-format "^29.5.0"
|
||||
|
||||
"@xstate/inspect@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@xstate/inspect/-/inspect-0.8.0.tgz#f99d3706cd823d4922c47ce4f4376eecac502cc7"
|
||||
integrity sha512-wSkFeOnp+7dhn+zTThO0M4D2FEqZN9lGIWowJu5JLa2ojjtlzRwK8SkjcHZ4rLX8VnMev7kGjgQLrGs8kxy+hw==
|
||||
dependencies:
|
||||
fast-safe-stringify "^2.1.1"
|
||||
|
||||
"@xstate/react@^3.2.2":
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.2.2.tgz#ddf0f9d75e2c19375b1e1b7335e72cb99762aed8"
|
||||
@ -3380,6 +3387,11 @@ fast-levenshtein@^2.0.6:
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
||||
|
||||
fast-safe-stringify@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
|
||||
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.15.0"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
|
||||
|
Reference in New Issue
Block a user