add the ability to edit sketch later

This commit is contained in:
Kurt Hutten IrevDev
2022-12-06 05:40:05 +11:00
parent 847b0b5b28
commit 034c903672
9 changed files with 186 additions and 53 deletions

View File

@ -3,7 +3,11 @@ import { Canvas } from '@react-three/fiber'
import { Allotment } from 'allotment' import { Allotment } from 'allotment'
import { OrbitControls, OrthographicCamera } from '@react-three/drei' import { OrbitControls, OrthographicCamera } from '@react-three/drei'
import { lexer } from './lang/tokeniser' import { lexer } from './lang/tokeniser'
import { abstractSyntaxTree } from './lang/abstractSyntaxTree' import {
abstractSyntaxTree,
getNodePathFromSourceRange,
getNodeFromPath
} from './lang/abstractSyntaxTree'
import { executor, processShownObjects, ViewerArtifact } from './lang/executor' import { executor, processShownObjects, ViewerArtifact } from './lang/executor'
import { recast } from './lang/recast' import { recast } from './lang/recast'
import { BufferGeometry } from 'three' import { BufferGeometry } from 'three'
@ -32,7 +36,6 @@ function App() {
setSelectionRange, setSelectionRange,
selectionRange, selectionRange,
guiMode, guiMode,
setGuiMode,
lastGuiMode, lastGuiMode,
removeError, removeError,
addLog, addLog,
@ -41,6 +44,8 @@ function App() {
setAst, setAst,
formatCode, formatCode,
ast, ast,
setError,
errorState,
} = useStore((s) => ({ } = useStore((s) => ({
editorView: s.editorView, editorView: s.editorView,
setEditorView: s.setEditorView, setEditorView: s.setEditorView,
@ -56,6 +61,8 @@ function App() {
setAst: s.setAst, setAst: s.setAst,
lastGuiMode: s.lastGuiMode, lastGuiMode: s.lastGuiMode,
formatCode: s.formatCode, formatCode: s.formatCode,
setError: s.setError,
errorState: s.errorState,
})) }))
// 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) => {
@ -110,8 +117,9 @@ function App() {
setGeoArray(geos) setGeoArray(geos)
removeError() removeError()
console.log(programMemory) console.log(programMemory)
setError()
} catch (e: any) { } catch (e: any) {
setGuiMode({ mode: 'codeError' }) setError('problem')
console.log(e) console.log(e)
addLog(e) addLog(e)
} }
@ -173,7 +181,7 @@ function App() {
<AxisIndicator /> <AxisIndicator />
</Canvas> </Canvas>
</div> </div>
{guiMode.mode === 'codeError' && ( {errorState.isError && (
<div className="absolute inset-0 bg-gray-700/20"> <div className="absolute inset-0 bg-gray-700/20">
<pre> <pre>
{'last first: \n\n' + {'last first: \n\n' +
@ -201,19 +209,39 @@ function Line({
sourceRange: [number, number] sourceRange: [number, number]
forceHighlight?: boolean forceHighlight?: boolean
}) { }) {
const { setHighlightRange, selectionRange } = useStore( const { setHighlightRange, selectionRange, guiMode, setGuiMode, ast } =
({ setHighlightRange, selectionRange }) => ({ useStore(
setHighlightRange, ({ setHighlightRange, selectionRange, guiMode, setGuiMode, ast }) => ({
selectionRange, setHighlightRange,
}) selectionRange,
) guiMode,
setGuiMode,
ast,
})
)
// This reference will give us direct access to the mesh // This reference will give us direct access to the mesh
const ref = useRef<BufferGeometry | undefined>() as any const ref = useRef<BufferGeometry | undefined>() as any
const [hovered, setHover] = useState(false) const [hovered, setHover] = useState(false)
const [editorCursor, setEditorCursor] = useState(false) const [editorCursor, setEditorCursor] = useState(false)
const [didSetCanEdit, setDidSetCanEdit] = useState(false)
useEffect(() => { useEffect(() => {
const shouldHighlight = isOverlapping(sourceRange, selectionRange) const shouldHighlight = isOverlapping(sourceRange, selectionRange)
setEditorCursor(shouldHighlight) setEditorCursor(shouldHighlight)
if (shouldHighlight && guiMode.mode === 'default' && ast) {
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const piper = getNodeFromPath(ast, pathToNode, 'PipeExpression')
const axis = piper.type !== 'PipeExpression' ? 'xy'
: piper?.body?.[1]?.callee?.name === 'rx' ? 'xz' : 'yz'
setGuiMode({ mode: 'canEditSketch', pathToNode, axis })
setDidSetCanEdit(true)
} else if (
!shouldHighlight &&
didSetCanEdit &&
guiMode.mode === 'canEditSketch'
) {
setGuiMode({ mode: 'default' })
setDidSetCanEdit(false)
}
}, [selectionRange, sourceRange]) }, [selectionRange, sourceRange])
return ( return (
@ -257,7 +285,7 @@ function RenderViewerArtifacts({
const shouldHighlight = isOverlapping(artifact.sourceRange, selectionRange) const shouldHighlight = isOverlapping(artifact.sourceRange, selectionRange)
setEditorCursor(shouldHighlight) setEditorCursor(shouldHighlight)
}, [selectionRange, artifact.sourceRange]) }, [selectionRange, artifact.sourceRange])
if (artifact.type === 'geo') { if (artifact.type === 'sketchLine') {
const { geo, sourceRange } = artifact const { geo, sourceRange } = artifact
return ( return (
<Line <Line

View File

@ -15,16 +15,50 @@ export const Toolbar = () => {
sketchMode: 'selectFace', sketchMode: 'selectFace',
}) })
}} }}
className="border m-1 px-1 rounded"
> >
Start sketch Start sketch
</button> </button>
)} )}
{guiMode.mode === 'sketch' && guiMode.sketchMode === 'points' && ( {guiMode.mode === 'canEditSketch' && (
<button>LineTo TODO</button> <button
onClick={() => {
setGuiMode({
mode: 'sketch',
sketchMode: 'sketchEdit',
pathToNode: guiMode.pathToNode,
axis: guiMode.axis,
})
}}
className="border m-1 px-1 rounded"
>
EditSketch
</button>
)} )}
{guiMode.mode !== 'default' && ( {guiMode.mode !== 'default' && (
<button onClick={() => setGuiMode({ mode: 'default' })}>exit</button> <button
onClick={() => setGuiMode({ mode: 'default' })}
className="border m-1 px-1 rounded"
>
Exit sketch
</button>
)} )}
{guiMode.mode === 'sketch' &&
(guiMode.sketchMode === 'points' ||
guiMode.sketchMode === 'sketchEdit') && (
<button
className={`border m-1 px-1 rounded ${
guiMode.sketchMode === 'points' && 'bg-gray-400'
}`}
onClick={() => setGuiMode({
...guiMode,
sketchMode: guiMode.sketchMode === 'points' ? 'sketchEdit' :'points',
})}
>
LineTo{guiMode.sketchMode === 'points' && '✅'}
</button>
)}
</div> </div>
) )
} }

View File

@ -56,9 +56,8 @@ export const BasePlanes = () => {
setGuiMode({ setGuiMode({
mode: 'sketch', mode: 'sketch',
sketchMode: 'points', sketchMode: 'sketchEdit',
axis, axis,
id,
pathToNode, pathToNode,
}) })

View File

@ -14,7 +14,7 @@ export const SketchPlane = () => {
if (guiMode.mode !== 'sketch') { if (guiMode.mode !== 'sketch') {
return null return null
} }
if (guiMode.sketchMode !== 'points') { if (guiMode.sketchMode !== 'points' && guiMode.sketchMode !== 'sketchEdit' ) {
return null return null
} }
@ -38,6 +38,9 @@ export const SketchPlane = () => {
rotation={clickDetectPlaneRotation} rotation={clickDetectPlaneRotation}
name={sketchGridName} name={sketchGridName}
onClick={(e) => { onClick={(e) => {
if (guiMode.sketchMode !== 'points') {
return
}
const sketchGridIntersection = e.intersections.find( const sketchGridIntersection = e.intersections.find(
({ object }) => object.name === sketchGridName ({ object }) => object.name === sketchGridName
) )

View File

@ -1176,7 +1176,8 @@ export function addLine(
const dumbyStartend = { start: 0, end: 0 } const dumbyStartend = { start: 0, end: 0 }
const sketchExpression = getNodeFromPath( const sketchExpression = getNodeFromPath(
_node, _node,
pathToNode pathToNode,
'SketchExpression'
) as SketchExpression ) as SketchExpression
const line: ExpressionStatement = { const line: ExpressionStatement = {
type: 'ExpressionStatement', type: 'ExpressionStatement',
@ -1263,8 +1264,13 @@ function debuggerr(tokens: Token[], indexes: number[], msg = ''): string {
return debugResult return debugResult
} }
export function getNodeFromPath(node: Program, path: (string | number)[]) { export function getNodeFromPath(
node: Program,
path: (string | number)[],
stopAt: string = ''
) {
let currentNode = node as any let currentNode = node as any
let stopAtNode = null
let successfulPaths: (string | number)[] = [] let successfulPaths: (string | number)[] = []
for (const pathItem of path) { for (const pathItem of path) {
try { try {
@ -1272,6 +1278,11 @@ export function getNodeFromPath(node: Program, path: (string | number)[]) {
throw new Error('not an object') throw new Error('not an object')
currentNode = currentNode[pathItem] currentNode = currentNode[pathItem]
successfulPaths.push(pathItem) successfulPaths.push(pathItem)
if (currentNode.type === stopAt) {
// it will match the deepest node of the type
// instead of returning at the first match
stopAtNode = currentNode
}
} catch (e) { } catch (e) {
throw new Error( throw new Error(
`Could not find path ${pathItem} in node ${JSON.stringify( `Could not find path ${pathItem} in node ${JSON.stringify(
@ -1282,7 +1293,7 @@ export function getNodeFromPath(node: Program, path: (string | number)[]) {
) )
} }
} }
return currentNode return stopAtNode || currentNode
} }
type Path = (string | number)[] type Path = (string | number)[]

View File

@ -3,7 +3,7 @@ import fs from 'node:fs'
import { abstractSyntaxTree } from './abstractSyntaxTree' import { abstractSyntaxTree } from './abstractSyntaxTree'
import { lexer } from './tokeniser' import { lexer } from './tokeniser'
import { executor, ProgramMemory } from './executor' import { executor, ProgramMemory } from './executor'
import { Transform } from './sketch' import { Transform, SketchGeo } from './sketch'
describe('test', () => { describe('test', () => {
it('test assigning two variables, the second summing with the first', () => { it('test assigning two variables, the second summing with the first', () => {
@ -64,7 +64,7 @@ show(mySketch)
` `
const { root, return: _return } = exe(code) const { root, return: _return } = exe(code)
expect( expect(
root.mySketch.map( root.mySketch.sketch.map(
({ previousPath, firstPath, geo, ...rest }: any) => rest ({ previousPath, firstPath, geo, ...rest }: any) => rest
) )
).toEqual([ ).toEqual([
@ -77,7 +77,7 @@ show(mySketch)
sourceRange: [93, 100], sourceRange: [93, 100],
}, },
]) ])
expect(root.mySketch[0]).toEqual(root.mySketch[4].firstPath) expect(root.mySketch.sketch[0]).toEqual(root.mySketch.sketch[4].firstPath)
// hmm not sure what handle the "show" function // hmm not sure what handle the "show" function
expect(_return).toEqual([ expect(_return).toEqual([
{ {
@ -109,7 +109,7 @@ show(mySketch)
// 'show(mySk1)', // 'show(mySk1)',
].join('\n') ].join('\n')
const { root } = exe(code) const { root } = exe(code)
expect(root.mySk1).toHaveLength(4) expect(root.mySk1.sketch).toHaveLength(4)
expect(root?.rotated?.type).toBe('transform') expect(root?.rotated?.type).toBe('transform')
}) })
@ -124,9 +124,7 @@ show(mySketch)
const { root } = exe(code) const { root } = exe(code)
const striptVersion = removeGeoFromSketch(root.mySk1) const striptVersion = removeGeoFromSketch(root.mySk1)
expect(striptVersion).toEqual({ expect(striptVersion).toEqual({
type: 'transform', type: 'sketchGeo',
rotation: [1.5707963267948966, 0, 0],
transform: [0, 0, 0],
sketch: [ sketch: [
{ {
type: 'base', type: 'base',
@ -150,8 +148,38 @@ show(mySketch)
sourceRange: [60, 71], sourceRange: [60, 71],
}, },
], ],
sourceRange: [77, 86], sourceRange: [13, 73],
}) })
// old expect
// expect(striptVersion).toEqual({
// type: 'transform',
// rotation: [1.5707963267948966, 0, 0],
// transform: [0, 0, 0],
// sketch: [
// {
// type: 'base',
// from: [0, 0],
// sourceRange: [0, 0],
// },
// {
// type: 'toPoint',
// to: [1, 1],
// sourceRange: [17, 28],
// },
// {
// type: 'toPoint',
// to: [0, 1],
// sourceRange: [36, 57],
// name: 'myPath',
// },
// {
// type: 'toPoint',
// to: [1, 1],
// sourceRange: [60, 71],
// },
// ],
// sourceRange: [77, 86],
// })
}) })
}) })
@ -166,8 +194,8 @@ function exe(
return executor(ast, programMemory) return executor(ast, programMemory)
} }
function removeGeoFromSketch(sketch: Transform): any { function removeGeoFromSketch(sketch: Transform | SketchGeo): any {
if (!Array.isArray(sketch.sketch)) { if (sketch.type !== 'sketchGeo') {
return removeGeoFromSketch(sketch.sketch) return removeGeoFromSketch(sketch.sketch)
} }
return { return {

View File

@ -4,7 +4,7 @@ import {
BinaryExpression, BinaryExpression,
PipeExpression, PipeExpression,
} from './abstractSyntaxTree' } from './abstractSyntaxTree'
import { Path, Transform, sketchFns } from './sketch' import { Path, Transform, SketchGeo, sketchFns } from './sketch'
import { BufferGeometry } from 'three' import { BufferGeometry } from 'three'
export interface ProgramMemory { export interface ProgramMemory {
@ -63,7 +63,12 @@ export const executor = (
) )
_sketch = newProgramMemory._sketch _sketch = newProgramMemory._sketch
} }
_programMemory.root[variableName] = _sketch const newSketch: SketchGeo = {
type: 'sketchGeo',
sketch: _sketch,
sourceRange: [sketchInit.start, sketchInit.end],
}
_programMemory.root[variableName] = newSketch
} else if (declaration.init.type === 'FunctionExpression') { } else if (declaration.init.type === 'FunctionExpression') {
const fnInit = declaration.init const fnInit = declaration.init
@ -271,9 +276,14 @@ function executePipeBody(
_sketch = newProgramMemory._sketch _sketch = newProgramMemory._sketch
} }
// _programMemory.root[variableName] = _sketch // _programMemory.root[variableName] = _sketch
const newSketch: SketchGeo = {
type: 'sketchGeo',
sketch: _sketch,
sourceRange: [expression.start, expression.end],
}
return executePipeBody(body, programMemory, expressionIndex + 1, [ return executePipeBody(body, programMemory, expressionIndex + 1, [
...previousResults, ...previousResults,
_sketch, newSketch,
]) ])
} }
@ -284,7 +294,7 @@ type SourceRange = [number, number]
export type ViewerArtifact = export type ViewerArtifact =
| { | {
type: 'geo' type: 'sketchLine'
sourceRange: SourceRange sourceRange: SourceRange
geo: BufferGeometry geo: BufferGeometry
} }
@ -301,11 +311,11 @@ type PreviousTransforms = {
export const processShownObjects = ( export const processShownObjects = (
programMemory: ProgramMemory, programMemory: ProgramMemory,
geoMeta: Path[] | Transform, geoMeta: SketchGeo | Transform,
previousTransforms: PreviousTransforms = [] previousTransforms: PreviousTransforms = []
): ViewerArtifact[] => { ): ViewerArtifact[] => {
if (Array.isArray(geoMeta)) { if (geoMeta?.type === 'sketchGeo') {
return geoMeta.map(({ geo, sourceRange }) => { return geoMeta.sketch.map(({ geo, sourceRange }) => {
const newGeo = geo.clone() const newGeo = geo.clone()
previousTransforms.forEach(({ rotation, transform }) => { previousTransforms.forEach(({ rotation, transform }) => {
newGeo.rotateX(rotation[0]) newGeo.rotateX(rotation[0])
@ -315,7 +325,7 @@ export const processShownObjects = (
}) })
return { return {
type: 'geo', type: 'sketchLine',
geo: newGeo, geo: newGeo,
sourceRange, sourceRange,
} }

View File

@ -59,7 +59,13 @@ export interface Transform {
type: 'transform' type: 'transform'
rotation: Rotation3 rotation: Rotation3
transform: Translate3 transform: Translate3
sketch: Path[] | Transform sketch: SketchGeo | Transform
sourceRange: SourceRange
}
export interface SketchGeo {
type: 'sketchGeo'
sketch: Path[]
sourceRange: SourceRange sourceRange: SourceRange
} }
@ -213,7 +219,7 @@ function RotateOnAxis(axisMultiplier: [number, number, number]) {
programMemory: ProgramMemory, programMemory: ProgramMemory,
sourceRange: SourceRange, sourceRange: SourceRange,
rotationD: number, rotationD: number,
sketch: Path[] | Transform sketch: SketchGeo | Transform
): Transform => { ): Transform => {
const rotationR = rotationD * (Math.PI / 180) const rotationR = rotationD * (Math.PI / 180)
return { return {

View File

@ -6,6 +6,9 @@ import { lexer } from './lang/tokeniser'
export type Range = [number, number] export type Range = [number, number]
type Plane = 'xy' | 'xz' | 'yz'
type PathToNode = (string | number)[]
type GuiModes = type GuiModes =
| { | {
mode: 'default' mode: 'default'
@ -13,17 +16,25 @@ type GuiModes =
| { | {
mode: 'sketch' mode: 'sketch'
sketchMode: 'points' sketchMode: 'points'
axis: 'xy' | 'xz' | 'yz' axis: Plane
id?: string id?: string
pathToNode: (string | number)[] pathToNode: PathToNode
} }
| {
mode: 'sketch'
sketchMode: 'sketchEdit'
axis: Plane
pathToNode: PathToNode
}
| { | {
mode: 'sketch' mode: 'sketch'
sketchMode: 'selectFace' sketchMode: 'selectFace'
} }
| { | {
mode: 'codeError' mode: 'canEditSketch'
} pathToNode: PathToNode
axis: Plane
}
interface StoreState { interface StoreState {
editorView: EditorView | null editorView: EditorView | null
@ -45,6 +56,11 @@ interface StoreState {
code: string code: string
setCode: (code: string) => void setCode: (code: string) => void
formatCode: () => void formatCode: () => void
errorState: {
isError: boolean
error: string
}
setError: (error?: string) => void
} }
export const useStore = create<StoreState>()((set, get) => ({ export const useStore = create<StoreState>()((set, get) => ({
@ -69,19 +85,10 @@ export const useStore = create<StoreState>()((set, get) => ({
setGuiMode: (guiMode) => { setGuiMode: (guiMode) => {
const lastGuiMode = get().guiMode const lastGuiMode = get().guiMode
set({ guiMode }) set({ guiMode })
if (guiMode.mode !== 'codeError') {
// don't set lastGuiMode to and error state
// as the point fo lastGuiMode is to restore the last healthy state
// todo maybe rename to lastHealthyGuiMode and remove this comment
set({ lastGuiMode })
}
}, },
removeError: () => { removeError: () => {
const lastGuiMode = get().lastGuiMode const lastGuiMode = get().lastGuiMode
const currentGuiMode = get().guiMode const currentGuiMode = get().guiMode
if (currentGuiMode.mode === 'codeError') {
set({ guiMode: lastGuiMode })
}
}, },
logs: [], logs: [],
addLog: (log) => { addLog: (log) => {
@ -108,4 +115,11 @@ export const useStore = create<StoreState>()((set, get) => ({
const newCode = recast(ast) const newCode = recast(ast)
set({ code: newCode, ast }) set({ code: newCode, ast })
}, },
errorState: {
isError: false,
error: '',
},
setError: (error = '') => {
set({ errorState: { isError: !!error, error } })
}
})) }))