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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ export const SketchPlane = () => {
if (guiMode.mode !== 'sketch') { if (guiMode.mode !== 'sketch') {
return null return null
} }
if (!(guiMode.sketchMode === 'lineTo') && !('isTooltip' in guiMode)) { if (!(guiMode.sketchMode === 'sketchEdit') && !('isTooltip' in guiMode)) {
return null 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]) { export function useSetCursor(sourceRange: Range) {
const setCursor = useStore((state) => state.setCursor) const { setCursor, selectionRanges, isShiftDown } = useStore((s) => ({
setCursor: s.setCursor,
selectionRanges: s.selectionRanges,
isShiftDown: s.isShiftDown,
}))
return () => { 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') const element: HTMLDivElement | null = document.querySelector('.cm-content')
if (element) { if (element) {
element.focus() element.focus()

View File

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

View File

@ -258,53 +258,19 @@ export const executor = (
__meta, __meta,
} }
} else if (declaration.init.type === 'CallExpression') { } else if (declaration.init.type === 'CallExpression') {
const functionName = declaration.init.callee.name const result = executeCallExpression(
const fnArgs = declaration.init.arguments.map((arg) => { _programMemory,
if (arg.type === 'Literal') { declaration.init,
return arg.value previousPathToNode
} else if (arg.type === 'Identifier') { )
return _programMemory.root[arg.name].value _programMemory.root[variableName] =
} else if (arg.type === 'ObjectExpression') { result?.type === 'sketchGroup' || result?.type === 'extrudeGroup'
return executeObjectExpression(_programMemory, arg) ? result
} else if (arg.type === 'ArrayExpression') { : {
return executeArrayExpression(_programMemory, arg) type: 'userVal',
} value: result,
throw new Error( __meta,
`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,
}
}
} else { } else {
throw new Error( throw new Error(
'Unsupported declaration type: ' + declaration.init.type 'Unsupported declaration type: ' + declaration.init.type

View File

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

View File

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