start of code gen from direct manipulation

This commit is contained in:
Kurt Hutten IrevDev
2022-11-28 09:37:46 +11:00
parent 1831aad321
commit ade1e9fb82
11 changed files with 411 additions and 48 deletions

View File

@ -14,6 +14,6 @@ let listener: ((rect: any) => void) | undefined = undefined
test('renders learn react link', () => { test('renders learn react link', () => {
render(<App />) render(<App />)
const linkElement = screen.getByText(/viewer/i) const linkElement = screen.getByText(/reset/i)
expect(linkElement).toBeInTheDocument() expect(linkElement).toBeInTheDocument()
}) })

View File

@ -20,19 +20,11 @@ import { BasePlanes } from './components/BasePlanes'
import { SketchPlane } from './components/SketchPlane' import { SketchPlane } from './components/SketchPlane'
import { Logs } from './components/Logs' import { Logs } from './components/Logs'
const _code = `sketch mySketch {
path myPath = lineTo(0,1)
lineTo(1,5)
path rightPath = lineTo(1,0)
close()
}
show(mySketch)`
const OrrthographicCamera = OrthographicCamera as any const OrrthographicCamera = OrthographicCamera as any
function App() { function App() {
const cam = useRef() const cam = useRef()
const [code, setCode] = useState(_code)
const { const {
editorView, editorView,
setEditorView, setEditorView,
@ -40,8 +32,12 @@ function App() {
selectionRange, selectionRange,
guiMode, guiMode,
setGuiMode, setGuiMode,
lastGuiMode,
removeError, removeError,
addLog, addLog,
code,
setCode,
setAst,
} = useStore((s) => ({ } = useStore((s) => ({
editorView: s.editorView, editorView: s.editorView,
setEditorView: s.setEditorView, setEditorView: s.setEditorView,
@ -51,6 +47,11 @@ function App() {
setGuiMode: s.setGuiMode, setGuiMode: s.setGuiMode,
removeError: s.removeError, removeError: s.removeError,
addLog: s.addLog, addLog: s.addLog,
code: s.code,
setCode: s.setCode,
ast: s.ast,
setAst: s.setAst,
lastGuiMode: s.lastGuiMode
})) }))
// 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) => {
@ -76,12 +77,14 @@ function App() {
try { try {
if (!code) { if (!code) {
setGeoArray([]) setGeoArray([])
setAst(null)
removeError() removeError()
return return
} }
const tokens = lexer(code) const tokens = lexer(code)
const ast = abstractSyntaxTree(tokens) const _ast = abstractSyntaxTree(tokens)
const programMemory = executor(ast, { setAst(_ast)
const programMemory = executor(_ast, {
root: { root: {
log: (a: any) => { log: (a: any) => {
addLog(a) addLog(a)
@ -90,17 +93,20 @@ function App() {
_sketch: [], _sketch: [],
}) })
const geos: { geo: BufferGeometry; sourceRange: [number, number] }[] = const geos: { geo: BufferGeometry; sourceRange: [number, number] }[] =
programMemory.root.mySketch programMemory?.return?.flatMap(
.map( ({ name }: { name: string }) =>
({ programMemory?.root?.[name]
geo, ?.map(
sourceRange, ({
}: { geo,
geo: BufferGeometry sourceRange,
sourceRange: [number, number] }: {
}) => ({ geo, sourceRange }) geo: BufferGeometry
) sourceRange: [number, number]
.filter((a: any) => !!a.geo) }) => ({ geo, sourceRange })
)
.filter((a: any) => !!a.geo) || []
) || []
setGeoArray(geos) setGeoArray(geos)
removeError() removeError()
console.log(programMemory) console.log(programMemory)
@ -114,10 +120,10 @@ function App() {
<div className="h-screen"> <div className="h-screen">
<Allotment> <Allotment>
<Logs /> <Logs />
<div className="bg-red h-full"> <div className="bg-red h-full overflow-auto">
<CodeMirror <CodeMirror
value={_code} className="h-full"
height="200px" value={code}
extensions={[javascript({ jsx: true }), lineHighlightField]} extensions={[javascript({ jsx: true }), lineHighlightField]}
onChange={onChange} onChange={onChange}
onUpdate={onUpdate} onUpdate={onUpdate}
@ -125,7 +131,6 @@ function App() {
/> />
</div> </div>
<div className="h-full"> <div className="h-full">
viewer
<Toolbar /> <Toolbar />
<div className="border h-full border-gray-300 relative"> <div className="border h-full border-gray-300 relative">
<div className="absolute inset-0"> <div className="absolute inset-0">
@ -162,7 +167,8 @@ function App() {
</Canvas> </Canvas>
</div> </div>
{guiMode.mode === 'codeError' && ( {guiMode.mode === 'codeError' && (
<div className="absolute inset-0 bg-gray-700/20">yo</div> <div className="absolute inset-0 bg-gray-700/20">
<pre>{'last first: \n\n' + JSON.stringify(lastGuiMode, null, 2) + '\n\n' + JSON.stringify(guiMode)}</pre></div>
)} )}
</div> </div>
</div> </div>

View File

@ -19,6 +19,11 @@ export const Toolbar = () => {
Start sketch Start sketch
</button> </button>
)} )}
{guiMode.mode === 'sketch' && guiMode.sketchMode === 'points' && (
<button>
LineTo TODO
</button>
)}
{guiMode.mode !== 'default' && ( {guiMode.mode !== 'default' && (
<button onClick={() => setGuiMode({ mode: 'default' })}>exit</button> <button onClick={() => setGuiMode({ mode: 'default' })}>exit</button>
)} )}

View File

@ -2,15 +2,20 @@ import { useState } from 'react'
import { DoubleSide } from 'three' import { DoubleSide } from 'three'
import { useStore } from '../useStore' import { useStore } from '../useStore'
import { Intersection } from '@react-three/fiber' import { Intersection } from '@react-three/fiber'
import { addSketchTo, Program } from '../lang/abstractSyntaxTree'
const opacity = 0.1 const opacity = 0.1
export const BasePlanes = () => { export const BasePlanes = () => {
const [axisIndex, setAxisIndex] = useState<null | number>(null) const [axisIndex, setAxisIndex] = useState<null | number>(null)
const { setGuiMode, guiMode } = useStore(({ guiMode, setGuiMode }) => ({ const { setGuiMode, guiMode, ast, updateAst } = useStore(
guiMode, ({ guiMode, setGuiMode, ast, updateAst }) => ({
setGuiMode, guiMode,
})) setGuiMode,
ast,
updateAst,
})
)
const onPointerEvent = ({ const onPointerEvent = ({
intersections, intersections,
@ -36,11 +41,25 @@ export const BasePlanes = () => {
if (guiMode.sketchMode !== 'selectFace') { if (guiMode.sketchMode !== 'selectFace') {
return null return null
} }
let _ast: Program = ast
? ast
: {
type: 'Program',
start: 0,
end: 0,
body: [],
}
const { modifiedAst, id } = addSketchTo(_ast)
setGuiMode({ setGuiMode({
mode: 'sketch', mode: 'sketch',
sketchMode: 'points', sketchMode: 'points',
axis: axisIndex === 0 ? 'yz' : axisIndex === 1 ? 'xy' : 'xz', axis: axisIndex === 0 ? 'yz' : axisIndex === 1 ? 'xy' : 'xz',
id
}) })
updateAst(modifiedAst)
} }
if (guiMode.mode !== 'sketch') { if (guiMode.mode !== 'sketch') {
return null return null

View File

@ -1,10 +1,16 @@
import { useStore } from '../useStore' import { useStore } from '../useStore'
import { DoubleSide } from 'three'
import { addLine, Program } from '../lang/abstractSyntaxTree'
export const SketchPlane = () => { export const SketchPlane = () => {
const { setGuiMode, guiMode } = useStore(({ guiMode, setGuiMode }) => ({ const { ast, setGuiMode, guiMode, updateAst } = useStore(
guiMode, ({ guiMode, setGuiMode, ast, updateAst }) => ({
setGuiMode, guiMode,
})) setGuiMode,
ast,
updateAst,
})
)
if (guiMode.mode !== 'sketch') { if (guiMode.mode !== 'sketch') {
return null return null
} }
@ -12,14 +18,66 @@ export const SketchPlane = () => {
return null return null
} }
const ninty = Math.PI / 2 const sketchGridName = 'sketchGrid'
const rotation: [number, number, number] = [0, 0, 0]
const ninety = Math.PI / 2
const gridRotation: [number, number, number] = [0, 0, 0]
const clickDetectPlaneRotation: [number, number, number] = [0, 0, 0]
if (guiMode.axis === 'yz') { if (guiMode.axis === 'yz') {
rotation[0] = ninty gridRotation[0] = ninety
} else if (guiMode.axis === 'xy') { } else if (guiMode.axis === 'xy') {
rotation[1] = ninty clickDetectPlaneRotation[0] = ninety
} else if (guiMode.axis === 'xz') { } else if (guiMode.axis === 'xz') {
rotation[2] = ninty gridRotation[2] = ninety
clickDetectPlaneRotation[1] = ninety
}
return (
<>
<mesh
rotation={clickDetectPlaneRotation}
name={sketchGridName}
onClick={(e) => {
const sketchGridIntersection = e.intersections.find(
({ object }) => object.name === sketchGridName
)
const point = roundy(sketchGridIntersection?.point)
let _ast: Program = ast
? ast
: {
type: 'Program',
start: 0,
end: 0,
body: [],
}
const { modifiedAst, id } = addLine(_ast, guiMode.id, [
point.x,
point.y,
])
updateAst(modifiedAst)
}}
>
<planeGeometry args={[30, 40]} />
<meshStandardMaterial
color="blue"
side={DoubleSide}
opacity={0}
transparent
/>
</mesh>
<gridHelper args={[30, 40, 'blue', 'hotpink']} rotation={gridRotation} />
</>
)
}
function roundy({ x, y, z }: any) {
const roundOff = (num: number, places: number): number => {
const x = Math.pow(10, places)
return Math.round(num * x) / x
}
return {
x: roundOff(x, 2),
y: roundOff(y, 2),
z: roundOff(z, 2),
} }
return <gridHelper args={[30, 40, 'blue', 'hotpink']} rotation={rotation} />
} }

View File

@ -557,7 +557,7 @@ function makeBlockStatement(
block: { block: {
type: 'BlockStatement', type: 'BlockStatement',
start: openingCurly.start, start: openingCurly.start,
end: tokens[lastIndex].end, end: tokens[lastIndex]?.end || 0,
body, body,
}, },
lastIndex, lastIndex,
@ -766,3 +766,237 @@ export function findClosingBrace(
// non-brace token, increment and continue // non-brace token, increment and continue
return findClosingBrace(tokens, index + 1, _braceCount, searchOpeningBrace) return findClosingBrace(tokens, index + 1, _braceCount, searchOpeningBrace)
} }
export function addSketchTo(
node: Program,
name = ''
): { modifiedAst: Program; id: string } {
const _node = { ...node }
const dumbyStartend = { start: 0, end: 0 }
const _name = name || findUniqueName(node, 'mySketch')
const sketchBody: BlockStatement = {
type: 'BlockStatement',
...dumbyStartend,
body: [],
}
const sketch: SketchExpression = {
type: 'SketchExpression',
...dumbyStartend,
body: sketchBody,
}
const sketchVariableDeclaration: VariableDeclaration = {
type: 'VariableDeclaration',
...dumbyStartend,
kind: 'sketch',
declarations: [
{
type: 'VariableDeclarator',
...dumbyStartend,
id: {
type: 'Identifier',
...dumbyStartend,
name: _name,
},
init: sketch,
},
],
}
const showCallIndex = getShowIndex(_node)
if (showCallIndex === -1) {
_node.body = [...node.body, sketchVariableDeclaration]
} else {
const newBody = [...node.body]
newBody.splice(showCallIndex, 0, sketchVariableDeclaration)
_node.body = newBody
}
return {
modifiedAst: addToShow(_node, _name),
id: _name,
}
}
function findUniqueName(
ast: Program | string,
name: string,
index = 1
): string {
let searchStr = ''
if (typeof ast === 'string') {
searchStr = ast
} else {
searchStr = JSON.stringify(ast)
}
const indexStr = `${index}`.padStart(3, '0')
const newName = `${name}${indexStr}`
const isInString = searchStr.includes(newName)
if (!isInString) {
return newName
}
return findUniqueName(searchStr, name, index + 1)
}
function addToShow(node: Program, name: string): Program {
const _node = { ...node }
const dumbyStartend = { start: 0, end: 0 }
const showCallIndex = getShowIndex(_node)
if (showCallIndex === -1) {
const showCall: CallExpression = {
type: 'CallExpression',
...dumbyStartend,
callee: {
type: 'Identifier',
...dumbyStartend,
name: 'show',
},
optional: false,
arguments: [
{
type: 'Identifier',
...dumbyStartend,
name,
},
],
}
const showExpressionStatement: ExpressionStatement = {
type: 'ExpressionStatement',
...dumbyStartend,
expression: showCall,
}
_node.body = [..._node.body, showExpressionStatement]
return _node
}
const showCall = { ..._node.body[showCallIndex] } as ExpressionStatement
const showCallArgs = (showCall.expression as CallExpression).arguments
const newShowCallArgs: Value[] = [
...showCallArgs,
{
type: 'Identifier',
...dumbyStartend,
name,
},
]
const newShowExpression: CallExpression = {
type: 'CallExpression',
...dumbyStartend,
callee: {
type: 'Identifier',
...dumbyStartend,
name: 'show',
},
optional: false,
arguments: newShowCallArgs,
}
_node.body[showCallIndex] = {
...showCall,
expression: newShowExpression,
}
return _node
}
function getShowIndex(node: Program): number {
return node.body.findIndex(
(statement) =>
statement.type === 'ExpressionStatement' &&
statement.expression.type === 'CallExpression' &&
statement.expression.callee.type === 'Identifier' &&
statement.expression.callee.name === 'show'
)
}
export function addLine(
node: Program,
id: string,
to: [number, number]
): { modifiedAst: Program; id: string } {
const _node = { ...node }
const dumbyStartend = { start: 0, end: 0 }
const { index, sketchDeclaration, sketchExpression } = getSketchStatement(_node, id)
const line: ExpressionStatement = {
type: 'ExpressionStatement',
...dumbyStartend,
expression: {
type: 'CallExpression',
...dumbyStartend,
callee: {
type: 'Identifier',
...dumbyStartend,
name: 'lineTo',
},
optional: false,
arguments: [
{
type: 'Literal',
...dumbyStartend,
value: to[0],
raw: `${to[0]}`,
},
{
type: 'Literal',
...dumbyStartend,
value: to[1],
raw: `${to[1]}`,
},
],
},
}
const newBody = [...sketchExpression.body.body, line]
const newSketchExpression: SketchExpression = {
...sketchExpression,
body: {
...sketchExpression.body,
body: newBody,
},
}
const newSketchDeclaration: VariableDeclaration = {
...sketchDeclaration,
declarations: [
{
...sketchDeclaration.declarations[0],
init: newSketchExpression,
},
],
}
_node.body[index] = newSketchDeclaration
return {
modifiedAst: _node,
id,
}
}
function getSketchStatement(
node: Program,
id: string
): {
sketchDeclaration: VariableDeclaration
sketchExpression: SketchExpression
index: number
} {
const sketchStatementIndex = node.body.findIndex(
(statement) =>
statement.type === 'VariableDeclaration' &&
statement.kind === 'sketch' &&
statement.declarations[0].id.type === 'Identifier' &&
statement.declarations[0].id.name === id
)
const sketchStatement = node.body.find(
(statement) =>
statement.type === 'VariableDeclaration' &&
statement.kind === 'sketch' &&
statement.declarations[0].id.type === 'Identifier' &&
statement.declarations[0].id.name === id
)
if (
!sketchStatement ||
sketchStatement.type !== 'VariableDeclaration' ||
sketchStatement.declarations[0].init.type !== 'SketchExpression'
)
throw new Error('No sketch found')
return {
sketchDeclaration: sketchStatement,
sketchExpression: sketchStatement.declarations[0].init,
index: sketchStatementIndex,
}
}

View File

@ -1,6 +1,12 @@
import { BoxGeometry, SphereGeometry, BufferGeometry } from 'three' import { BoxGeometry, SphereGeometry, BufferGeometry } from 'three'
import { mergeBufferGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils' import { mergeBufferGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils'
export function baseGeo({from}: {from: [number, number, number]}) {
const baseSphere = new SphereGeometry(0.25)
baseSphere.translate(from[0], from[1], from[2])
return baseSphere
}
export function lineGeo({ export function lineGeo({
from, from,
to, to,

View File

@ -63,7 +63,9 @@ show(mySketch)
` `
const { root, return: _return } = exe(code) const { root, return: _return } = exe(code)
expect( expect(
root.mySketch.map(({ previousPath, geo, ...rest }: any) => rest) root.mySketch.map(
({ previousPath, firstPath, geo, ...rest }: any) => rest
)
).toEqual([ ).toEqual([
{ type: 'base', from: [0, 0], sourceRange: [0, 0] }, { type: 'base', from: [0, 0], sourceRange: [0, 0] },
{ type: 'toPoint', to: [0, 1], sourceRange: [25, 45], name: 'myPath' }, { type: 'toPoint', to: [0, 1], sourceRange: [25, 45], name: 'myPath' },
@ -71,7 +73,6 @@ show(mySketch)
{ type: 'toPoint', to: [1, 0], sourceRange: [67, 90], name: 'rightPath' }, { type: 'toPoint', to: [1, 0], sourceRange: [67, 90], name: 'rightPath' },
{ {
type: 'close', type: 'close',
firstPath: { type: 'base', from: [0, 0], sourceRange: [0, 0] },
sourceRange: [93, 100], sourceRange: [93, 100],
}, },
]) ])

View File

@ -11,7 +11,7 @@ export const executor = (
node: Program, node: Program,
programMemory: ProgramMemory = { root: {}, _sketch: [] }, programMemory: ProgramMemory = { root: {}, _sketch: [] },
options: { bodyType: 'root' | 'sketch' | 'block' } = { bodyType: 'root' } options: { bodyType: 'root' | 'sketch' | 'block' } = { bodyType: 'root' }
): any => { ): ProgramMemory => {
const _programMemory: ProgramMemory = { const _programMemory: ProgramMemory = {
root: { root: {
...programMemory.root, ...programMemory.root,
@ -39,9 +39,13 @@ export const executor = (
}, },
_sketch: [], _sketch: [],
} }
const { _sketch } = executor(sketchInit.body, fnMemory, { let { _sketch } = executor(sketchInit.body, fnMemory, {
bodyType: 'sketch', bodyType: 'sketch',
}) })
if (_sketch.length === 0) {
const {programMemory: newProgramMemory} = sketchFns.base(fnMemory, '', [0, 0], 0, 0)
_sketch = newProgramMemory._sketch
}
_programMemory.root[variableName] = _sketch _programMemory.root[variableName] = _sketch
} else if (declaration.init.type === 'FunctionExpression') { } else if (declaration.init.type === 'FunctionExpression') {
const fnInit = declaration.init const fnInit = declaration.init

View File

@ -1,5 +1,5 @@
import { ProgramMemory } from './executor' import { ProgramMemory } from './executor'
import { lineGeo } from './engine' import { lineGeo, baseGeo } from './engine'
import { BufferGeometry } from 'three' import { BufferGeometry } from 'three'
type Coords2d = [number, number] type Coords2d = [number, number]
@ -49,14 +49,17 @@ export type Path =
| { | {
type: 'base' type: 'base'
from: Coords2d from: Coords2d
geo: BufferGeometry
sourceRange: SourceRange sourceRange: SourceRange
} }
function addBasePath(programMemory: ProgramMemory) { function addBasePath(programMemory: ProgramMemory) {
const geo = baseGeo({ from: [0, 0, 0] })
const base: Path = { const base: Path = {
type: 'base', type: 'base',
from: [0, 0], from: [0, 0],
sourceRange: [0, 0], sourceRange: [0, 0],
geo,
} }
if (programMemory._sketch?.length === 0) { if (programMemory._sketch?.length === 0) {
return { return {
@ -103,10 +106,12 @@ export const sketchFns = {
} }
const [x, y] = args as [number, number] const [x, y] = args as [number, number]
let from: [number, number] = [x, y] let from: [number, number] = [x, y]
const geo = baseGeo({ from: [x, y, 0] })
const newPath: Path = { const newPath: Path = {
type: 'base', type: 'base',
from, from,
sourceRange, sourceRange,
geo,
} }
return { return {
programMemory: { programMemory: {

View File

@ -1,5 +1,7 @@
import create from 'zustand' import create from 'zustand'
import { addLineHighlight, EditorView } from './editor/highlightextension' import { addLineHighlight, EditorView } from './editor/highlightextension'
import { Program } from './lang/abstractSyntaxTree'
import { recast } from './lang/recast'
export type Range = [number, number] export type Range = [number, number]
@ -11,6 +13,7 @@ type GuiModes =
mode: 'sketch' mode: 'sketch'
sketchMode: 'points' sketchMode: 'points'
axis: 'xy' | 'xz' | 'yz' axis: 'xy' | 'xz' | 'yz'
id: string
} }
| { | {
mode: 'sketch' mode: 'sketch'
@ -34,6 +37,11 @@ interface StoreState {
logs: string[] logs: string[]
addLog: (log: string) => void addLog: (log: string) => void
resetLogs: () => void resetLogs: () => void
ast: Program | null
setAst: (ast: Program | null) => void
updateAst: (ast: Program) => void
code: string
setCode: (code: string) => void
} }
export const useStore = create<StoreState>()((set, get) => ({ export const useStore = create<StoreState>()((set, get) => ({
@ -58,7 +66,12 @@ export const useStore = create<StoreState>()((set, get) => ({
setGuiMode: (guiMode) => { setGuiMode: (guiMode) => {
const lastGuiMode = get().guiMode const lastGuiMode = get().guiMode
set({ guiMode }) set({ guiMode })
set({ lastGuiMode }) 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
@ -74,4 +87,16 @@ export const useStore = create<StoreState>()((set, get) => ({
resetLogs: () => { resetLogs: () => {
set({ logs: [] }) set({ logs: [] })
}, },
ast: null,
setAst: (ast) => {
set({ ast })
},
updateAst: (ast) => {
const newCode = recast(ast)
set({ ast, code: newCode })
},
code: '',
setCode: (code) => {
set({ code })
}
})) }))