Gui scafolding toolbar, log console, axis for sketching
This commit is contained in:
120
src/App.tsx
120
src/App.tsx
@ -15,6 +15,10 @@ import {
|
|||||||
} from './editor/highlightextension'
|
} from './editor/highlightextension'
|
||||||
import { useStore } from './useStore'
|
import { useStore } from './useStore'
|
||||||
import { isOverlapping } from './lib/utils'
|
import { isOverlapping } from './lib/utils'
|
||||||
|
import { Toolbar } from './Toolbar'
|
||||||
|
import { BasePlanes } from './components/BasePlanes'
|
||||||
|
import { SketchPlane } from './components/SketchPlane'
|
||||||
|
import { Logs } from './components/Logs'
|
||||||
|
|
||||||
const _code = `sketch mySketch {
|
const _code = `sketch mySketch {
|
||||||
path myPath = lineTo(0,1)
|
path myPath = lineTo(0,1)
|
||||||
@ -29,15 +33,25 @@ const OrrthographicCamera = OrthographicCamera as any
|
|||||||
function App() {
|
function App() {
|
||||||
const cam = useRef()
|
const cam = useRef()
|
||||||
const [code, setCode] = useState(_code)
|
const [code, setCode] = useState(_code)
|
||||||
const { editorView, setEditorView, setSelectionRange, selectionRange } =
|
const {
|
||||||
useStore(
|
editorView,
|
||||||
({ editorView, setEditorView, setSelectionRange, selectionRange }) => ({
|
setEditorView,
|
||||||
editorView,
|
setSelectionRange,
|
||||||
setEditorView,
|
selectionRange,
|
||||||
setSelectionRange,
|
guiMode,
|
||||||
selectionRange,
|
setGuiMode,
|
||||||
})
|
removeError,
|
||||||
)
|
addLog,
|
||||||
|
} = useStore((s) => ({
|
||||||
|
editorView: s.editorView,
|
||||||
|
setEditorView: s.setEditorView,
|
||||||
|
setSelectionRange: s.setSelectionRange,
|
||||||
|
selectionRange: s.selectionRange,
|
||||||
|
guiMode: s.guiMode,
|
||||||
|
setGuiMode: s.setGuiMode,
|
||||||
|
removeError: s.removeError,
|
||||||
|
addLog: s.addLog,
|
||||||
|
}))
|
||||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||||
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
const onChange = (value: string, viewUpdate: ViewUpdate) => {
|
||||||
setCode(value)
|
setCode(value)
|
||||||
@ -60,9 +74,21 @@ function App() {
|
|||||||
>([])
|
>([])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
|
if (!code) {
|
||||||
|
setGeoArray([])
|
||||||
|
removeError()
|
||||||
|
return
|
||||||
|
}
|
||||||
const tokens = lexer(code)
|
const tokens = lexer(code)
|
||||||
const ast = abstractSyntaxTree(tokens)
|
const ast = abstractSyntaxTree(tokens)
|
||||||
const programMemory = executor(ast)
|
const programMemory = executor(ast, {
|
||||||
|
root: {
|
||||||
|
log: (a: any) => {
|
||||||
|
addLog(a)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_sketch: [],
|
||||||
|
})
|
||||||
const geos: { geo: BufferGeometry; sourceRange: [number, number] }[] =
|
const geos: { geo: BufferGeometry; sourceRange: [number, number] }[] =
|
||||||
programMemory.root.mySketch
|
programMemory.root.mySketch
|
||||||
.map(
|
.map(
|
||||||
@ -76,14 +102,18 @@ function App() {
|
|||||||
)
|
)
|
||||||
.filter((a: any) => !!a.geo)
|
.filter((a: any) => !!a.geo)
|
||||||
setGeoArray(geos)
|
setGeoArray(geos)
|
||||||
|
removeError()
|
||||||
console.log(programMemory)
|
console.log(programMemory)
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
|
setGuiMode({ mode: 'codeError' })
|
||||||
console.log(e)
|
console.log(e)
|
||||||
|
addLog(e)
|
||||||
}
|
}
|
||||||
}, [code])
|
}, [code])
|
||||||
return (
|
return (
|
||||||
<div className="h-screen">
|
<div className="h-screen">
|
||||||
<Allotment>
|
<Allotment>
|
||||||
|
<Logs />
|
||||||
<div className="bg-red h-full">
|
<div className="bg-red h-full">
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={_code}
|
value={_code}
|
||||||
@ -96,36 +126,44 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
viewer
|
viewer
|
||||||
<div className="border h-full border-gray-300">
|
<Toolbar />
|
||||||
<Canvas>
|
<div className="border h-full border-gray-300 relative">
|
||||||
<OrbitControls
|
<div className="absolute inset-0">
|
||||||
enableDamping={false}
|
<Canvas>
|
||||||
enablePan
|
<OrbitControls
|
||||||
enableRotate
|
enableDamping={false}
|
||||||
enableZoom
|
enablePan
|
||||||
reverseOrbit={false}
|
enableRotate
|
||||||
/>
|
enableZoom
|
||||||
<OrrthographicCamera
|
reverseOrbit={false}
|
||||||
ref={cam}
|
/>
|
||||||
makeDefault
|
<OrrthographicCamera
|
||||||
position={[0, 0, 10]}
|
ref={cam}
|
||||||
zoom={40}
|
makeDefault
|
||||||
rotation={[0, 0, 0]}
|
position={[0, 0, 10]}
|
||||||
/>
|
zoom={40}
|
||||||
<ambientLight />
|
rotation={[0, 0, 0]}
|
||||||
<pointLight position={[10, 10, 10]} />
|
/>
|
||||||
{geoArray.map(
|
<ambientLight />
|
||||||
(
|
<pointLight position={[10, 10, 10]} />
|
||||||
{
|
{geoArray.map(
|
||||||
geo,
|
(
|
||||||
sourceRange,
|
{
|
||||||
}: { geo: BufferGeometry; sourceRange: [number, number] },
|
geo,
|
||||||
index
|
sourceRange,
|
||||||
) => (
|
}: { geo: BufferGeometry; sourceRange: [number, number] },
|
||||||
<Line key={index} geo={geo} sourceRange={sourceRange} />
|
index
|
||||||
)
|
) => (
|
||||||
)}
|
<Line key={index} geo={geo} sourceRange={sourceRange} />
|
||||||
</Canvas>
|
)
|
||||||
|
)}
|
||||||
|
<BasePlanes />
|
||||||
|
<SketchPlane />
|
||||||
|
</Canvas>
|
||||||
|
</div>
|
||||||
|
{guiMode.mode === 'codeError' && (
|
||||||
|
<div className="absolute inset-0 bg-gray-700/20">yo</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Allotment>
|
</Allotment>
|
||||||
|
27
src/Toolbar.tsx
Normal file
27
src/Toolbar.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useStore } from './useStore'
|
||||||
|
|
||||||
|
export const Toolbar = () => {
|
||||||
|
const { setGuiMode, guiMode } = useStore(({ guiMode, setGuiMode }) => ({
|
||||||
|
guiMode,
|
||||||
|
setGuiMode,
|
||||||
|
}))
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{guiMode.mode === 'default' && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setGuiMode({
|
||||||
|
mode: 'sketch',
|
||||||
|
sketchMode: 'selectFace',
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Start sketch
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{guiMode.mode !== 'default' && (
|
||||||
|
<button onClick={() => setGuiMode({ mode: 'default' })}>exit</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
74
src/components/BasePlanes.tsx
Normal file
74
src/components/BasePlanes.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { DoubleSide } from 'three'
|
||||||
|
import { useStore } from '../useStore'
|
||||||
|
import { Intersection } from '@react-three/fiber'
|
||||||
|
|
||||||
|
const opacity = 0.1
|
||||||
|
|
||||||
|
export const BasePlanes = () => {
|
||||||
|
const [axisIndex, setAxisIndex] = useState<null | number>(null)
|
||||||
|
const { setGuiMode, guiMode } = useStore(({ guiMode, setGuiMode }) => ({
|
||||||
|
guiMode,
|
||||||
|
setGuiMode,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const onPointerEvent = ({
|
||||||
|
intersections,
|
||||||
|
}: {
|
||||||
|
intersections: Intersection[]
|
||||||
|
}) => {
|
||||||
|
if (!intersections.length) {
|
||||||
|
setAxisIndex(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let closestIntersection = intersections[0]
|
||||||
|
intersections.forEach((intersection) => {
|
||||||
|
if (intersection.distance < closestIntersection.distance)
|
||||||
|
closestIntersection = intersection
|
||||||
|
})
|
||||||
|
const smallestIndex = Number(closestIntersection.eventObject.name)
|
||||||
|
setAxisIndex(smallestIndex)
|
||||||
|
}
|
||||||
|
const onClick = () => {
|
||||||
|
if (guiMode.mode !== 'sketch') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (guiMode.sketchMode !== 'selectFace') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
setGuiMode({
|
||||||
|
mode: 'sketch',
|
||||||
|
sketchMode: 'points',
|
||||||
|
axis: axisIndex === 0 ? 'yz' : axisIndex === 1 ? 'xy' : 'xz',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (guiMode.mode !== 'sketch') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (guiMode.sketchMode !== 'selectFace') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
|
<mesh
|
||||||
|
key={index}
|
||||||
|
rotation-x={index === 1 ? -Math.PI / 2 : 0}
|
||||||
|
rotation-y={index === 2 ? -Math.PI / 2 : 0}
|
||||||
|
onPointerMove={onPointerEvent}
|
||||||
|
onPointerOut={onPointerEvent}
|
||||||
|
onClick={onClick}
|
||||||
|
name={`${index}`}
|
||||||
|
>
|
||||||
|
<planeGeometry args={[5, 5]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
color="blue"
|
||||||
|
side={DoubleSide}
|
||||||
|
transparent
|
||||||
|
opacity={opacity + (axisIndex === index ? 0.3 : 0)}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
34
src/components/Logs.tsx
Normal file
34
src/components/Logs.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useStore } from '../useStore'
|
||||||
|
|
||||||
|
export const Logs = () => {
|
||||||
|
const { logs, resetLogs } = useStore(({ logs, resetLogs }) => ({
|
||||||
|
logs,
|
||||||
|
resetLogs,
|
||||||
|
}))
|
||||||
|
useEffect(() => {
|
||||||
|
const element = document.querySelector('.console-tile')
|
||||||
|
if (element) {
|
||||||
|
element.scrollTop = element.scrollHeight - element.clientHeight
|
||||||
|
}
|
||||||
|
}, [logs])
|
||||||
|
return (
|
||||||
|
<div className="h-full relative">
|
||||||
|
<div className="absolute inset-0 flex flex-col items-start">
|
||||||
|
<button onClick={resetLogs}>reset</button>
|
||||||
|
<div className=" overflow-auto h-full console-tile w-full">
|
||||||
|
{logs.map((msg, index) => {
|
||||||
|
return (
|
||||||
|
<pre className="text-xs pl-2 text-sky-600" key={index}>
|
||||||
|
<code style={{ fontFamily: 'monospace' }} key={index}>
|
||||||
|
<span className="text-gray-400">{'- '}</span>
|
||||||
|
{String(msg)}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
25
src/components/SketchPlane.tsx
Normal file
25
src/components/SketchPlane.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useStore } from '../useStore'
|
||||||
|
|
||||||
|
export const SketchPlane = () => {
|
||||||
|
const { setGuiMode, guiMode } = useStore(({ guiMode, setGuiMode }) => ({
|
||||||
|
guiMode,
|
||||||
|
setGuiMode,
|
||||||
|
}))
|
||||||
|
if (guiMode.mode !== 'sketch') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (guiMode.sketchMode !== 'points') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const ninty = Math.PI / 2
|
||||||
|
const rotation: [number, number, number] = [0, 0, 0]
|
||||||
|
if (guiMode.axis === 'yz') {
|
||||||
|
rotation[0] = ninty
|
||||||
|
} else if (guiMode.axis === 'xy') {
|
||||||
|
rotation[1] = ninty
|
||||||
|
} else if (guiMode.axis === 'xz') {
|
||||||
|
rotation[2] = ninty
|
||||||
|
}
|
||||||
|
return <gridHelper args={[30, 40, 'blue', 'hotpink']} rotation={rotation} />
|
||||||
|
}
|
@ -3,6 +3,23 @@ import { addLineHighlight, EditorView } from './editor/highlightextension'
|
|||||||
|
|
||||||
export type Range = [number, number]
|
export type Range = [number, number]
|
||||||
|
|
||||||
|
type GuiModes =
|
||||||
|
| {
|
||||||
|
mode: 'default'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
mode: 'sketch'
|
||||||
|
sketchMode: 'points'
|
||||||
|
axis: 'xy' | 'xz' | 'yz'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
mode: 'sketch'
|
||||||
|
sketchMode: 'selectFace'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
mode: 'codeError'
|
||||||
|
}
|
||||||
|
|
||||||
interface StoreState {
|
interface StoreState {
|
||||||
editorView: EditorView | null
|
editorView: EditorView | null
|
||||||
setEditorView: (editorView: EditorView) => void
|
setEditorView: (editorView: EditorView) => void
|
||||||
@ -10,6 +27,13 @@ interface StoreState {
|
|||||||
setHighlightRange: (range: Range) => void
|
setHighlightRange: (range: Range) => void
|
||||||
selectionRange: [number, number]
|
selectionRange: [number, number]
|
||||||
setSelectionRange: (range: Range) => void
|
setSelectionRange: (range: Range) => void
|
||||||
|
guiMode: GuiModes
|
||||||
|
lastGuiMode: GuiModes
|
||||||
|
setGuiMode: (guiMode: GuiModes) => void
|
||||||
|
removeError: () => void
|
||||||
|
logs: string[]
|
||||||
|
addLog: (log: string) => void
|
||||||
|
resetLogs: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useStore = create<StoreState>()((set, get) => ({
|
export const useStore = create<StoreState>()((set, get) => ({
|
||||||
@ -29,4 +53,25 @@ export const useStore = create<StoreState>()((set, get) => ({
|
|||||||
setSelectionRange: (selectionRange) => {
|
setSelectionRange: (selectionRange) => {
|
||||||
set({ selectionRange })
|
set({ selectionRange })
|
||||||
},
|
},
|
||||||
|
guiMode: { mode: 'default' },
|
||||||
|
lastGuiMode: { mode: 'default' },
|
||||||
|
setGuiMode: (guiMode) => {
|
||||||
|
const lastGuiMode = get().guiMode
|
||||||
|
set({ guiMode })
|
||||||
|
set({ lastGuiMode })
|
||||||
|
},
|
||||||
|
removeError: () => {
|
||||||
|
const lastGuiMode = get().lastGuiMode
|
||||||
|
const currentGuiMode = get().guiMode
|
||||||
|
if (currentGuiMode.mode === 'codeError') {
|
||||||
|
set({ guiMode: lastGuiMode })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
logs: [],
|
||||||
|
addLog: (log) => {
|
||||||
|
set((state) => ({ logs: [...state.logs, log] }))
|
||||||
|
},
|
||||||
|
resetLogs: () => {
|
||||||
|
set({ logs: [] })
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
Reference in New Issue
Block a user