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', () => {
render(<App />)
const linkElement = screen.getByText(/viewer/i)
const linkElement = screen.getByText(/reset/i)
expect(linkElement).toBeInTheDocument()
})

View File

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

View File

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

View File

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

View File

@ -1,10 +1,16 @@
import { useStore } from '../useStore'
import { DoubleSide } from 'three'
import { addLine, Program } from '../lang/abstractSyntaxTree'
export const SketchPlane = () => {
const { setGuiMode, guiMode } = useStore(({ guiMode, setGuiMode }) => ({
guiMode,
setGuiMode,
}))
const { ast, setGuiMode, guiMode, updateAst } = useStore(
({ guiMode, setGuiMode, ast, updateAst }) => ({
guiMode,
setGuiMode,
ast,
updateAst,
})
)
if (guiMode.mode !== 'sketch') {
return null
}
@ -12,14 +18,66 @@ export const SketchPlane = () => {
return null
}
const ninty = Math.PI / 2
const rotation: [number, number, number] = [0, 0, 0]
const sketchGridName = 'sketchGrid'
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') {
rotation[0] = ninty
gridRotation[0] = ninety
} else if (guiMode.axis === 'xy') {
rotation[1] = ninty
clickDetectPlaneRotation[0] = ninety
} 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: {
type: 'BlockStatement',
start: openingCurly.start,
end: tokens[lastIndex].end,
end: tokens[lastIndex]?.end || 0,
body,
},
lastIndex,
@ -766,3 +766,237 @@ export function findClosingBrace(
// non-brace token, increment and continue
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 { 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({
from,
to,

View File

@ -63,7 +63,9 @@ show(mySketch)
`
const { root, return: _return } = exe(code)
expect(
root.mySketch.map(({ previousPath, geo, ...rest }: any) => rest)
root.mySketch.map(
({ previousPath, firstPath, geo, ...rest }: any) => rest
)
).toEqual([
{ type: 'base', from: [0, 0], sourceRange: [0, 0] },
{ 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: 'close',
firstPath: { type: 'base', from: [0, 0], sourceRange: [0, 0] },
sourceRange: [93, 100],
},
])

View File

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

View File

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

View File

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