Add multi-cursor support (#30)

* update execution of callExpressions

* tweak types to store multiple cursor ranges and hook up with artifact highlighting

* multi-cursor from 3d scene

Working but has to be capslock for the time being

* tweak pannel headers

* add issue to todo comment
This commit is contained in:
Kurt Hutten
2023-02-21 10:28:34 +11:00
committed by GitHub
parent cb8e97eceb
commit ea05f804cc
12 changed files with 153 additions and 135 deletions

View File

@ -22,19 +22,20 @@ import { AxisIndicator } from './components/AxisIndicator'
import { RenderViewerArtifacts } from './components/RenderViewerArtifacts'
import { PanelHeader } from './components/PanelHeader'
import { MemoryPanel } from './components/MemoryPanel'
import { useHotKeyListener } from './hooks/useHotKeyListener'
const OrrthographicCamera = OrthographicCamera as any
function App() {
const cam = useRef()
useHotKeyListener()
const {
editorView,
setEditorView,
setSelectionRange,
selectionRange,
setSelectionRanges,
selectionRanges: selectionRange,
guiMode,
lastGuiMode,
removeError,
addLog,
code,
setCode,
@ -48,11 +49,10 @@ function App() {
} = useStore((s) => ({
editorView: s.editorView,
setEditorView: s.setEditorView,
setSelectionRange: s.setSelectionRange,
selectionRange: s.selectionRange,
setSelectionRanges: s.setSelectionRanges,
selectionRanges: s.selectionRanges,
guiMode: s.guiMode,
setGuiMode: s.setGuiMode,
removeError: s.removeError,
addLog: s.addLog,
code: s.code,
setCode: s.setCode,
@ -76,13 +76,16 @@ function App() {
if (!editorView) {
setEditorView(viewUpdate.view)
}
const range = viewUpdate.state.selection.ranges[0]
// console.log(viewUpdate.state.selection.ranges)
// TODO allow multiple cursors so that we can do constrain style features
const isNoChange =
range.from === selectionRange[0] && range.to === selectionRange[1]
if (isNoChange) return
setSelectionRange([range.from, range.to])
const ranges = viewUpdate.state.selection.ranges
const isChange =
ranges.length !== selectionRange.length ||
ranges.some(({ from, to }, i) => {
return from !== selectionRange[i][0] || to !== selectionRange[i][1]
})
if (!isChange) return
setSelectionRanges(ranges.map(({ from, to }) => [from, to]))
}
const [geoArray, setGeoArray] = useState<(ExtrudeGroup | SketchGroup)[]>([])
useEffect(() => {
@ -90,7 +93,6 @@ function App() {
if (!code) {
setGeoArray([])
setAst(null)
removeError()
return
}
const tokens = lexer(code)
@ -129,7 +131,6 @@ function App() {
.filter((a) => a) as (ExtrudeGroup | SketchGroup)[]
setGeoArray(geos)
removeError()
console.log(programMemory)
setError()
} catch (e: any) {
@ -138,15 +139,15 @@ function App() {
addLog(e)
}
}, [code])
const shouldFormat = useMemo(() => {
if (!ast) return false
const recastedCode = recast(ast)
return recastedCode !== code
}, [code, ast])
// const shouldFormat = useMemo(() => {
// if (!ast) return false
// const recastedCode = recast(ast)
// return recastedCode !== code
// }, [code, ast])
return (
<div className="h-screen">
<Allotment snap={true}>
<Allotment vertical defaultSizes={[4, 1, 1]}>
<Allotment vertical defaultSizes={[4, 1, 1]} minSize={20}>
<div className="h-full flex flex-col items-start">
<PanelHeader title="Editor" />
{/* <button

View File

@ -3,15 +3,21 @@ import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
import { getNodePathFromSourceRange } from './lang/abstractSyntaxTree'
export const Toolbar = () => {
const { setGuiMode, guiMode, selectionRange, ast, updateAst, programMemory } =
useStore((s) => ({
guiMode: s.guiMode,
setGuiMode: s.setGuiMode,
selectionRange: s.selectionRange,
ast: s.ast,
updateAst: s.updateAst,
programMemory: s.programMemory,
}))
const {
setGuiMode,
guiMode,
selectionRanges,
ast,
updateAst,
programMemory,
} = useStore((s) => ({
guiMode: s.guiMode,
setGuiMode: s.setGuiMode,
selectionRanges: s.selectionRanges,
ast: s.ast,
updateAst: s.updateAst,
programMemory: s.programMemory,
}))
return (
<div>
@ -32,7 +38,10 @@ export const Toolbar = () => {
<button
onClick={() => {
if (!ast) return
const pathToNode = getNodePathFromSourceRange(ast, selectionRange)
const pathToNode = getNodePathFromSourceRange(
ast,
selectionRanges[0]
)
const { modifiedAst } = sketchOnExtrudedFace(
ast,
pathToNode,
@ -54,7 +63,6 @@ export const Toolbar = () => {
pathToNode: guiMode.pathToNode,
rotation: guiMode.rotation,
position: guiMode.position,
isTooltip: true,
})
}}
className="border m-1 px-1 rounded"
@ -67,7 +75,10 @@ export const Toolbar = () => {
<button
onClick={() => {
if (!ast) return
const pathToNode = getNodePathFromSourceRange(ast, selectionRange)
const pathToNode = getNodePathFromSourceRange(
ast,
selectionRanges[0]
)
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
ast,
pathToNode
@ -81,7 +92,10 @@ export const Toolbar = () => {
<button
onClick={() => {
if (!ast) return
const pathToNode = getNodePathFromSourceRange(ast, selectionRange)
const pathToNode = getNodePathFromSourceRange(
ast,
selectionRanges[0]
)
const { modifiedAst, pathToExtrudeArg } = extrudeSketch(
ast,
pathToNode,
@ -105,7 +119,11 @@ export const Toolbar = () => {
</button>
)}
{toolTips.map((sketchFnName) => {
if (guiMode.mode !== 'sketch' || !('isTooltip' in guiMode)) return null
if (
guiMode.mode !== 'sketch' ||
!('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit')
)
return null
return (
<button
key={sketchFnName}
@ -115,10 +133,14 @@ export const Toolbar = () => {
onClick={() =>
setGuiMode({
...guiMode,
sketchMode:
guiMode.sketchMode === sketchFnName
? 'sketchEdit'
: sketchFnName,
...(guiMode.sketchMode === sketchFnName
? {
sketchMode: 'sketchEdit',
}
: {
sketchMode: sketchFnName,
isTooltip: true,
}),
})
}
>

View File

@ -69,7 +69,6 @@ export const BasePlanes = () => {
rotation: quaternion.toArray() as [number, number, number, number],
position: [0, 0, 0],
pathToNode,
isTooltip: true,
})
updateAst(modifiedAst)

View File

@ -1,7 +1,7 @@
export const PanelHeader = ({ title }: { title: string }) => {
return (
<div className="font-mono text-xs bg-stone-100 w-full pl-4 h-[30px] text-stone-700 flex items-center">
{title}
<div className="font-mono text-[11px] bg-stone-100 w-full pl-4 h-[20px] text-stone-700 flex items-center">
<span className="pt-1">{title}</span>
</div>
)
}

View File

@ -88,7 +88,9 @@ function MovingSphere({
inverseQuaternion.set(...guiMode.rotation)
inverseQuaternion.invert()
}
current2d.sub(new Vector3(...position).applyQuaternion(inverseQuaternion))
current2d.sub(
new Vector3(...position).applyQuaternion(inverseQuaternion)
)
let [x, y] = [roundOff(current2d.x, 2), roundOff(current2d.y, 2)]
let theNewPoints: [number, number] = [x, y]
const { modifiedAst } = changeSketchArguments(
@ -284,10 +286,10 @@ function WallRender({
rotation: Rotation
position: Position
}) {
const { setHighlightRange, selectionRange } = useStore(
({ setHighlightRange, selectionRange }) => ({
const { setHighlightRange, selectionRanges } = useStore(
({ setHighlightRange, selectionRanges }) => ({
setHighlightRange,
selectionRange,
selectionRanges,
})
)
const onClick = useSetCursor(geoInfo.__geoMeta.sourceRange)
@ -297,12 +299,11 @@ function WallRender({
const [editorCursor, setEditorCursor] = useState(false)
useEffect(() => {
const shouldHighlight = isOverlap(
geoInfo.__geoMeta.sourceRange,
selectionRange
const shouldHighlight = selectionRanges.some((range) =>
isOverlap(geoInfo.__geoMeta.sourceRange, range)
)
setEditorCursor(shouldHighlight)
}, [selectionRange, geoInfo])
}, [selectionRanges, geoInfo])
return (
<>
@ -347,17 +348,16 @@ function PathRender({
rotation: Rotation
position: Position
}) {
const { selectionRange } = useStore(({ selectionRange }) => ({
selectionRange,
const { selectionRanges } = useStore(({ selectionRanges }) => ({
selectionRanges,
}))
const [editorCursor, setEditorCursor] = useState(false)
useEffect(() => {
const shouldHighlight = isOverlap(
geoInfo.__geoMeta.sourceRange,
selectionRange
const shouldHighlight = selectionRanges.some((range) =>
isOverlap(geoInfo.__geoMeta.sourceRange, range)
)
setEditorCursor(shouldHighlight)
}, [selectionRange, geoInfo])
}, [selectionRanges, geoInfo])
return (
<>
{geoInfo.__geoMeta.geos.map((meta, i) => {
@ -404,8 +404,8 @@ function LineRender({
rotation: Rotation
position: Position
}) {
const { setHighlightRange } = useStore(({ setHighlightRange }) => ({
setHighlightRange,
const { setHighlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
}))
const onClick = useSetCursor(sourceRange)
// This reference will give us direct access to the mesh
@ -440,9 +440,9 @@ function LineRender({
type Artifact = ExtrudeGroup | SketchGroup
function useSetAppModeFromCursorLocation(artifacts: Artifact[]) {
const { selectionRange, guiMode, setGuiMode, ast } = useStore(
({ selectionRange, guiMode, setGuiMode, ast }) => ({
selectionRange,
const { selectionRanges, guiMode, setGuiMode, ast } = useStore(
({ selectionRanges, guiMode, setGuiMode, ast }) => ({
selectionRanges,
guiMode,
setGuiMode,
ast,
@ -469,7 +469,7 @@ function useSetAppModeFromCursorLocation(artifacts: Artifact[]) {
)[] = []
artifacts?.forEach((artifact) => {
artifact.value.forEach((geo) => {
if (isOverlap(geo.__geoMeta.sourceRange, selectionRange)) {
if (isOverlap(geo.__geoMeta.sourceRange, selectionRanges[0])) {
artifactsWithinCursorRange.push({
parentType: artifact.type,
isParent: false,
@ -481,7 +481,7 @@ function useSetAppModeFromCursorLocation(artifacts: Artifact[]) {
}
})
artifact.__meta.forEach((meta) => {
if (isOverlap(meta.sourceRange, selectionRange)) {
if (isOverlap(meta.sourceRange, selectionRanges[0])) {
artifactsWithinCursorRange.push({
parentType: artifact.type,
isParent: true,
@ -530,5 +530,5 @@ function useSetAppModeFromCursorLocation(artifacts: Artifact[]) {
) {
setGuiMode({ mode: 'default' })
}
}, [artifacts, selectionRange])
}, [artifacts, selectionRanges])
}

View File

@ -14,7 +14,7 @@ export const SketchPlane = () => {
if (guiMode.mode !== 'sketch') {
return null
}
if (!(guiMode.sketchMode === 'lineTo') && !('isTooltip' in guiMode)) {
if (!(guiMode.sketchMode === 'sketchEdit') && !('isTooltip' in guiMode)) {
return null
}

View File

@ -0,0 +1,21 @@
import { useStore } from '../useStore'
import { useEffect } from 'react'
export function useHotKeyListener() {
const { setIsShiftDown } = useStore((s) => ({
setIsShiftDown: s.setIsShiftDown,
}))
const keyName = 'CapsLock' // TODO #32 should be shift, but shift conflicts with the editor's use of the shift key atm.
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) =>
event.key === keyName && setIsShiftDown(true)
const handleKeyUp = (event: KeyboardEvent) =>
event.key === keyName && setIsShiftDown(false)
window.addEventListener('keydown', handleKeyDown)
window.addEventListener('keyup', handleKeyUp)
return () => {
window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('keyup', handleKeyUp)
}
}, [setIsShiftDown])
}

View File

@ -1,9 +1,17 @@
import { useStore } from '../useStore'
import { useStore, Range } from '../useStore'
export function useSetCursor(sourceRange: [number, number]) {
const setCursor = useStore((state) => state.setCursor)
export function useSetCursor(sourceRange: Range) {
const { setCursor, selectionRanges, isShiftDown } = useStore((s) => ({
setCursor: s.setCursor,
selectionRanges: s.selectionRanges,
isShiftDown: s.isShiftDown,
}))
return () => {
setCursor(sourceRange[1])
console.log('isShiftDown', isShiftDown, selectionRanges, sourceRange)
const ranges = isShiftDown
? [...selectionRanges, sourceRange]
: [sourceRange]
setCursor(ranges)
const element: HTMLDivElement | null = document.querySelector('.cm-content')
if (element) {
element.focus()

View File

@ -190,7 +190,7 @@ show(theExtrude, sk2)`
0.9230002039112792,
],
__meta: [
{ sourceRange: [190, 218], pathToNode: [] },
{ sourceRange: [203, 218], pathToNode: [] },
{ sourceRange: [13, 34], pathToNode: [] },
],
},

View File

@ -258,53 +258,19 @@ export const executor = (
__meta,
}
} else if (declaration.init.type === 'CallExpression') {
const functionName = declaration.init.callee.name
const fnArgs = declaration.init.arguments.map((arg) => {
if (arg.type === 'Literal') {
return arg.value
} else if (arg.type === 'Identifier') {
return _programMemory.root[arg.name].value
} else if (arg.type === 'ObjectExpression') {
return executeObjectExpression(_programMemory, arg)
} else if (arg.type === 'ArrayExpression') {
return executeArrayExpression(_programMemory, arg)
}
throw new Error(
`Unexpected argument type ${arg.type} in function call`
)
})
if (functionName in internalFns) {
const result = executeCallExpression(
_programMemory,
declaration.init,
previousPathToNode,
{
sourceRangeOverride: [declaration.start, declaration.end],
isInPipe: false,
previousResults: [],
expressionIndex: 0,
body: [],
}
)
if (
result.type === 'extrudeGroup' ||
result.type === 'sketchGroup'
) {
_programMemory.root[variableName] = result
} else {
_programMemory.root[variableName] = {
type: 'userVal',
value: result,
__meta,
}
}
} else {
_programMemory.root[variableName] = {
type: 'userVal',
value: _programMemory.root[functionName].value(...fnArgs),
__meta,
}
}
const result = executeCallExpression(
_programMemory,
declaration.init,
previousPathToNode
)
_programMemory.root[variableName] =
result?.type === 'sketchGroup' || result?.type === 'extrudeGroup'
? result
: {
type: 'userVal',
value: result,
__meta,
}
} else {
throw new Error(
'Unsupported declaration type: ' + declaration.init.type

View File

@ -118,7 +118,6 @@ show(mySketch001)`
{
mode: 'sketch',
sketchMode: 'sketchEdit',
isTooltip: true,
rotation: [0, 0, 0, 1],
position: [0, 0, 0],
pathToNode: ['body', 0, 'declarations', '0', 'init'],

View File

@ -8,8 +8,10 @@ import {
import { ProgramMemory, Position, PathToNode, Rotation } from './lang/executor'
import { recast } from './lang/recast'
import { asyncLexer } from './lang/tokeniser'
import { EditorSelection } from '@codemirror/state'
export type Range = [number, number]
export type Ranges = Range[]
export type TooTip =
| 'lineTo'
| 'line'
@ -53,7 +55,6 @@ export type GuiModes =
| {
mode: 'sketch'
sketchMode: 'sketchEdit'
isTooltip: true
rotation: Rotation
position: Position
pathToNode: PathToNode
@ -80,13 +81,12 @@ interface StoreState {
setEditorView: (editorView: EditorView) => void
highlightRange: [number, number]
setHighlightRange: (range: Range) => void
setCursor: (start: number, end?: number) => void
selectionRange: [number, number]
setSelectionRange: (range: Range) => void
setCursor: (selections: Ranges) => void
selectionRanges: Ranges
setSelectionRanges: (range: Ranges) => void
guiMode: GuiModes
lastGuiMode: GuiModes
setGuiMode: (guiMode: GuiModes) => void
removeError: () => void
logs: string[]
addLog: (log: string) => void
resetLogs: () => void
@ -103,6 +103,8 @@ interface StoreState {
setError: (error?: string) => void
programMemory: ProgramMemory
setProgramMemory: (programMemory: ProgramMemory) => void
isShiftDown: boolean
setIsShiftDown: (isShiftDown: boolean) => void
}
export const useStore = create<StoreState>()((set, get) => ({
@ -118,27 +120,25 @@ export const useStore = create<StoreState>()((set, get) => ({
editorView.dispatch({ effects: addLineHighlight.of(highlightRange) })
}
},
setCursor: (start: number, end: number = start) => {
const editorView = get().editorView
setCursor: (ranges: Ranges) => {
const { editorView } = get()
if (!editorView) return
editorView.dispatch({
selection: { anchor: start, head: end },
selection: EditorSelection.create(
[...ranges.map(([start, end]) => EditorSelection.cursor(end))],
ranges.length - 1
),
})
},
selectionRange: [0, 0],
setSelectionRange: (selectionRange) => {
set({ selectionRange })
selectionRanges: [[0, 0]],
setSelectionRanges: (selectionRanges) => {
set({ selectionRanges })
},
guiMode: { mode: 'default' },
lastGuiMode: { mode: 'default' },
setGuiMode: (guiMode) => {
const lastGuiMode = get().guiMode
set({ guiMode })
},
removeError: () => {
const lastGuiMode = get().lastGuiMode
const currentGuiMode = get().guiMode
},
logs: [],
addLog: (log) => {
if (Array.isArray(log)) {
@ -165,7 +165,7 @@ export const useStore = create<StoreState>()((set, get) => ({
const { start, end } = node
if (!start || !end) return
setTimeout(() => {
get().setCursor(start, end)
get().setCursor([[start, end]])
})
}
},
@ -188,4 +188,6 @@ export const useStore = create<StoreState>()((set, get) => ({
},
programMemory: { root: {}, _sketch: [] },
setProgramMemory: (programMemory) => set({ programMemory }),
isShiftDown: false,
setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
}))