diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..95725b469 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "geos" + ] +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 7448b3cc7..169c2751b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,7 @@ import { Allotment } from 'allotment' import { OrbitControls, OrthographicCamera } from '@react-three/drei' import { lexer } from './lang/tokeniser' import { abstractSyntaxTree } from './lang/abstractSyntaxTree' -import { executor, processShownObjects, ViewerArtifact } from './lang/executor' +import { executor, ExtrudeGroup, SketchGroup } from './lang/executor' import { recast } from './lang/recast' import CodeMirror from '@uiw/react-codemirror' import { javascript } from '@codemirror/lang-javascript' @@ -78,7 +78,7 @@ function App() { if (isNoChange) return setSelectionRange([range.from, range.to]) } - const [geoArray, setGeoArray] = useState([]) + const [geoArray, setGeoArray] = useState<(ExtrudeGroup | SketchGroup)[]>([]) useEffect(() => { try { if (!code) { @@ -92,26 +92,44 @@ function App() { setAst(_ast) const programMemory = executor(_ast, { root: { - log: (a: any) => { - let b = a - if (Array.isArray(a)) { - b = a.map(({ geo, ...rest }) => rest) - b = JSON.stringify(b, null, 2) - } else if (typeof a === 'object') { - b = JSON.stringify(a, null, 2) - } - addLog(b) + log: { + type: 'userVal', + value: (a: any) => { + console.log('raw log', a) + let b = a + if (Array.isArray(a)) { + b = a.map(({ geo, __geoMeta, ...rest }) => rest) + b = JSON.stringify(b, null, 2) + } else if (typeof a === 'object') { + const { geo, __geoMeta, ...rest } = a + b = JSON.stringify(rest, null, 2) + } + addLog(b) + }, + __meta: [ + { + pathToNode: [], + sourceRange: [0, 0], + }, + ], }, }, _sketch: [], }) setProgramMemory(programMemory) - const geos: ViewerArtifact[] = - programMemory?.return?.flatMap( - ({ name }: { name: string }) => - processShownObjects(programMemory, programMemory?.root?.[name]) || - [] - ) || [] + const geos = programMemory?.return + ?.map(({ name }: { name: string }) => { + const artifact = programMemory?.root?.[name] + if ( + artifact.type === 'extrudeGroup' || + artifact.type === 'sketchGroup' + ) { + return artifact + } + return null + }) + .filter((a) => a) as (ExtrudeGroup | SketchGroup)[] + setGeoArray(geos) removeError() console.log(programMemory) @@ -177,9 +195,7 @@ function App() { /> - {geoArray.map((artifact, index) => ( - - ))} + diff --git a/src/Toolbar.tsx b/src/Toolbar.tsx index 96fd95efd..c34fb88e4 100644 --- a/src/Toolbar.tsx +++ b/src/Toolbar.tsx @@ -34,7 +34,7 @@ export const Toolbar = () => { mode: 'sketch', sketchMode: 'sketchEdit', pathToNode: guiMode.pathToNode, - quaternion: guiMode.quaternion, + rotation: guiMode.rotation, position: guiMode.position, }) }} diff --git a/src/components/BasePlanes.tsx b/src/components/BasePlanes.tsx index 60d013cf4..832d9e3b2 100644 --- a/src/components/BasePlanes.tsx +++ b/src/components/BasePlanes.tsx @@ -65,7 +65,7 @@ export const BasePlanes = () => { setGuiMode({ mode: 'sketch', sketchMode: 'sketchEdit', - quaternion, + rotation: quaternion.toArray() as [number, number, number, number], position: [0, 0, 0], pathToNode, }) diff --git a/src/components/SketchLine.tsx b/src/components/SketchLine.tsx index a326028ba..e1015ff82 100644 --- a/src/components/SketchLine.tsx +++ b/src/components/SketchLine.tsx @@ -3,110 +3,22 @@ import { getNodePathFromSourceRange, getNodeFromPath, CallExpression, - VariableDeclarator, } from '../lang/abstractSyntaxTree' import { changeArguments } from '../lang/modifyAst' -import { ViewerArtifact } from '../lang/executor' +import { + ExtrudeGroup, + ExtrudeSurface, + SketchGroup, + Path, + Rotation, + Position, +} from '../lang/executor' import { BufferGeometry } from 'three' import { useStore } from '../useStore' import { isOverlapping } from '../lib/utils' -import { LineGeos } from '../lang/engine' -import { Vector3, DoubleSide, Quaternion, Vector2 } from 'three' -import { combineTransformsAlt } from '../lang/sketch' +import { Vector3, DoubleSide, Quaternion } from 'three' import { useSetCursor } from '../hooks/useSetCursor' -function SketchLine({ - geo, - sourceRange, - forceHighlight = false, -}: { - geo: LineGeos - sourceRange: [number, number] - forceHighlight?: boolean -}) { - const { setHighlightRange } = useStore(({ setHighlightRange }) => ({ - setHighlightRange, - })) - const onClick = useSetCursor(sourceRange) - // This reference will give us direct access to the mesh - const ref = useRef() as any - const [hovered, setHover] = useState(false) - - return ( - <> - { - setHover(true) - setHighlightRange(sourceRange) - }} - onPointerOut={(event) => { - setHover(false) - setHighlightRange([0, 0]) - }} - onClick={onClick} - > - - - - - - ) -} - -function ExtrudeWall({ - geo, - sourceRange, - forceHighlight = false, -}: { - geo: BufferGeometry - sourceRange: [number, number] - forceHighlight?: boolean -}) { - const { setHighlightRange } = useStore( - ({ setHighlightRange, selectionRange, guiMode, setGuiMode, ast }) => ({ - setHighlightRange, - selectionRange, - guiMode, - setGuiMode, - ast, - }) - ) - const onClick = useSetCursor(sourceRange) - // This reference will give us direct access to the mesh - const ref = useRef() as any - const [hovered, setHover] = useState(false) - - return ( - <> - { - setHover(true) - setHighlightRange(sourceRange) - }} - onPointerOut={(event) => { - setHover(false) - setHighlightRange([0, 0]) - }} - onClick={onClick} - > - - - - - ) -} - const roundOff = (num: number, places: number): number => { const x = Math.pow(10, places) return Math.round(num * x) / x @@ -116,15 +28,19 @@ function MovingSphere({ geo, sourceRange, editorCursor, + rotation, + position, }: { geo: BufferGeometry sourceRange: [number, number] editorCursor: boolean + rotation: Rotation + position: Position }) { const ref = useRef() as any const detectionPlaneRef = useRef() as any const lastPointerRef = useRef(new Vector3()) - const point2DRef = useRef(new Vector2()) + const point2DRef = useRef(new Vector3()) const [hovered, setHover] = useState(false) const [isMouseDown, setIsMouseDown] = useState(false) @@ -154,14 +70,22 @@ function MovingSphere({ const handleMouseUp = () => { if (isMouseDown && ast) { const thePath = getNodePathFromSourceRange(ast, sourceRange) - let [x, y] = [ - roundOff(point2DRef.current.x, 2), - roundOff(point2DRef.current.y, 2), - ] + const yo = point2DRef.current.clone() + const inverseQuaternion = new Quaternion() + if ( + guiMode.mode === 'canEditSketch' || + (guiMode.mode === 'sketch' && guiMode.sketchMode === 'sketchEdit') + ) { + inverseQuaternion.set(...guiMode.rotation) + inverseQuaternion.invert() + } + yo.sub(new Vector3(...position).applyQuaternion(inverseQuaternion)) + let [x, y] = [roundOff(yo.x, 2), roundOff(yo.y, 2)] let theNewPoints: [number, number] = [x, y] const { modifiedAst } = changeArguments(ast, thePath, theNewPoints) updateAst(modifiedAst) - ref.current.position.set(0, 0, 0) + console.log('reset position') + ref.current.position.set(...position) } setIsMouseDown(false) } @@ -169,31 +93,32 @@ function MovingSphere({ return () => { window.removeEventListener('mouseup', handleMouseUp) } - }, [isMouseDown, ast]) + }, [isMouseDown]) - let clickDetectPlaneQuaternion = new Quaternion() - let position = new Vector3(0, 0, 0) - if ( + const inEditMode = guiMode.mode === 'canEditSketch' || (guiMode.mode === 'sketch' && guiMode.sketchMode === 'sketchEdit') - ) { - clickDetectPlaneQuaternion = guiMode.quaternion.clone() - position = new Vector3(...guiMode.position) + + let clickDetectPlaneQuaternion = new Quaternion() + if (inEditMode) { + clickDetectPlaneQuaternion = new Quaternion(...rotation) } return ( <> { - setHover(true) + inEditMode && setHover(true) setHighlightRange(sourceRange) }} onPointerOut={(event) => { setHover(false) setHighlightRange([0, 0]) }} - onPointerDown={() => setIsMouseDown(true)} + onPointerDown={() => inEditMode && setIsMouseDown(true)} > {isMouseDown && ( { const point = a.point @@ -213,13 +137,11 @@ function MovingSphere({ guiMode.mode === 'canEditSketch' || (guiMode.mode === 'sketch' && guiMode.sketchMode === 'sketchEdit') ) { - inverseQuaternion.copy(guiMode.quaternion.clone().invert()) + inverseQuaternion.set(...guiMode.rotation) + inverseQuaternion.invert() } transformedPoint.applyQuaternion(inverseQuaternion) - transformedPoint.sub( - position.clone().applyQuaternion(inverseQuaternion) - ) - point2DRef.current.set(transformedPoint.x, transformedPoint.y) + point2DRef.current.copy(transformedPoint) if ( lastPointerRef.current.x === 0 && @@ -248,7 +170,7 @@ function MovingSphere({ ref.current.position.add( diff.applyQuaternion(inverseQuaternion.invert()) ) - lastPointerRef.current.set(point.x, point.y, point.z) + lastPointerRef.current.copy(point.clone()) } }} > @@ -266,86 +188,258 @@ function MovingSphere({ } export function RenderViewerArtifacts({ - artifact, - forceHighlight = false, + artifacts, }: { - artifact: ViewerArtifact - forceHighlight?: boolean + artifacts: (ExtrudeGroup | SketchGroup)[] }) { - const { selectionRange, guiMode, ast, setGuiMode, programMemory } = useStore( - ({ selectionRange, guiMode, ast, setGuiMode, programMemory }) => ({ - selectionRange, - guiMode, - ast, - setGuiMode, - programMemory, - }) - ) - const [editorCursor, setEditorCursor] = useState(false) - useEffect(() => { - const shouldHighlight = isOverlapping(artifact.sourceRange, selectionRange) - setEditorCursor(shouldHighlight && artifact.type !== 'sketch') - }, [selectionRange, artifact.sourceRange]) - - useEffect(() => { - const shouldHighlight = isOverlapping(artifact.sourceRange, selectionRange) - if ( - shouldHighlight && - (guiMode.mode === 'default' || guiMode.mode === 'canEditSketch') && - artifact.type === 'sketch' && - ast - ) { - const pathToNode = getNodePathFromSourceRange(ast, artifact.sourceRange) - const varDec: VariableDeclarator = getNodeFromPath( - ast, - pathToNode, - 'VariableDeclarator' - ) - const varName = varDec?.id?.name - const { quaternion, position } = combineTransformsAlt( - programMemory.root[varName] - ) - setGuiMode({ mode: 'canEditSketch', pathToNode, quaternion, position }) - } else if ( - !shouldHighlight && - guiMode.mode === 'canEditSketch' && - artifact.type === 'sketch' - ) { - setGuiMode({ mode: 'default' }) - } - }, [selectionRange, artifact.sourceRange, ast, guiMode.mode, setGuiMode]) - if (artifact.type === 'sketchLine') { - const { geo, sourceRange } = artifact - return ( - - ) - } - if (artifact.type === 'sketchBase') { - console.log('BASE TODO') - return null - } - if (artifact.type === 'extrudeWall') { - return ( - - ) - } return ( <> - {artifact.children.map((artifact, index) => ( - + {artifacts.map((artifact, i) => ( + ))} ) } + +function RenderViewerArtifact({ + artifact, +}: { + artifact: ExtrudeGroup | SketchGroup +}) { + const { selectionRange, guiMode, ast, setGuiMode } = useStore( + ({ selectionRange, guiMode, ast, setGuiMode }) => ({ + selectionRange, + guiMode, + ast, + setGuiMode, + }) + ) + const [editorCursor, setEditorCursor] = useState(false) + useEffect(() => { + const shouldHighlight = isOverlapping( + artifact.__meta.slice(-1)[0].sourceRange, + selectionRange + ) + setEditorCursor(shouldHighlight) + }, [selectionRange, artifact.__meta]) + + useEffect(() => { + const shouldHighlight = artifact.__meta.some((aMeta) => + isOverlapping(aMeta.sourceRange, selectionRange) + ) + if ( + shouldHighlight && + (guiMode.mode === 'default' || guiMode.mode === 'canEditSketch') && + ast && + artifact.type === 'sketchGroup' + ) { + const pathToNode = getNodePathFromSourceRange( + ast, + artifact.__meta[0].sourceRange + ) + const { rotation, position } = artifact + setGuiMode({ mode: 'canEditSketch', pathToNode, rotation, position }) + } else if ( + !shouldHighlight && + guiMode.mode === 'canEditSketch' && + artifact.type === 'sketchGroup' + ) { + setGuiMode({ mode: 'default' }) + } + }, [selectionRange, artifact, ast, guiMode.mode, setGuiMode]) + + if (artifact.type === 'sketchGroup') { + return ( + <> + {artifact.value.map((geoInfo, key) => ( + + ))} + + ) + } + if (artifact.type === 'extrudeGroup') { + return ( + <> + {artifact.value.map((geoInfo, key) => ( + + ))} + + ) + } + return null +} + +function WallRender({ + geoInfo, + forceHighlight = false, + rotation, + position, +}: { + geoInfo: ExtrudeSurface + forceHighlight?: boolean + rotation: Rotation + position: Position +}) { + const { setHighlightRange, selectionRange } = useStore( + ({ setHighlightRange, selectionRange }) => ({ + setHighlightRange, + selectionRange, + }) + ) + const onClick = useSetCursor(geoInfo.__geoMeta.sourceRange) + // This reference will give us direct access to the mesh + const ref = useRef() as any + const [hovered, setHover] = useState(false) + + const [editorCursor, setEditorCursor] = useState(false) + useEffect(() => { + const shouldHighlight = isOverlapping( + geoInfo.__geoMeta.sourceRange, + selectionRange + ) + setEditorCursor(shouldHighlight) + }, [selectionRange, geoInfo]) + + return ( + <> + { + setHover(true) + setHighlightRange(geoInfo.__geoMeta.sourceRange) + }} + onPointerOut={(event) => { + setHover(false) + setHighlightRange([0, 0]) + }} + onClick={onClick} + > + + + + + ) +} + +function PathRender({ + geoInfo, + forceHighlight = false, + rotation, + position, +}: { + geoInfo: Path + forceHighlight?: boolean + rotation: Rotation + position: Position +}) { + const { selectionRange } = useStore(({ selectionRange }) => ({ + selectionRange, + })) + const [editorCursor, setEditorCursor] = useState(false) + useEffect(() => { + const shouldHighlight = isOverlapping( + geoInfo.__geoMeta.sourceRange, + selectionRange + ) + setEditorCursor(shouldHighlight) + }, [selectionRange, geoInfo]) + return ( + <> + {geoInfo.__geoMeta.geos.map((meta, i) => { + if (meta.type === 'line') { + return ( + + ) + } + if (meta.type === 'lineEnd') { + return ( + + ) + } + })} + + ) +} + +function LineRender({ + geo, + sourceRange, + forceHighlight = false, + rotation, + position, +}: { + geo: BufferGeometry + sourceRange: [number, number] + forceHighlight?: boolean + rotation: Rotation + position: Position +}) { + const { setHighlightRange } = useStore(({ setHighlightRange }) => ({ + setHighlightRange, + })) + const onClick = useSetCursor(sourceRange) + // This reference will give us direct access to the mesh + const ref = useRef() as any + const [hovered, setHover] = useState(false) + + return ( + <> + { + setHover(true) + setHighlightRange(sourceRange) + }} + onPointerOut={(event) => { + setHover(false) + setHighlightRange([0, 0]) + }} + onClick={onClick} + > + + + + + ) +} diff --git a/src/components/SketchPlane.tsx b/src/components/SketchPlane.tsx index f183ad079..00705b4d8 100644 --- a/src/components/SketchPlane.tsx +++ b/src/components/SketchPlane.tsx @@ -20,7 +20,7 @@ export const SketchPlane = () => { const sketchGridName = 'sketchGrid' - let clickDetectQuaternion = guiMode.quaternion.clone() + let clickDetectQuaternion = new Quaternion(...guiMode.rotation) let temp = new Quaternion().setFromAxisAngle( new Vector3(1, 0, 0), @@ -28,7 +28,7 @@ export const SketchPlane = () => { ) let position = guiMode.position const gridQuaternion = new Quaternion().multiplyQuaternions( - guiMode.quaternion, + new Quaternion(...guiMode.rotation), temp ) @@ -47,8 +47,12 @@ export const SketchPlane = () => { ) const inverseQuaternion = clickDetectQuaternion.clone().invert() let transformedPoint = sketchGridIntersection?.point.clone() - if (transformedPoint) + if (transformedPoint) { transformedPoint.applyQuaternion(inverseQuaternion) + transformedPoint?.sub( + new Vector3(...position).applyQuaternion(inverseQuaternion) + ) + } const point = roundy(transformedPoint) let _ast: Program = ast diff --git a/src/lang/abstractSyntaxTree.ts b/src/lang/abstractSyntaxTree.ts index 4510970f4..1cf39d626 100644 --- a/src/lang/abstractSyntaxTree.ts +++ b/src/lang/abstractSyntaxTree.ts @@ -453,7 +453,7 @@ export interface Literal extends GeneralStatement { raw: string } -interface Identifier extends GeneralStatement { +export interface Identifier extends GeneralStatement { type: 'Identifier' name: string } diff --git a/src/lang/artifact.test.ts b/src/lang/artifact.test.ts index a2cc37c0e..904efd28f 100644 --- a/src/lang/artifact.test.ts +++ b/src/lang/artifact.test.ts @@ -1,9 +1,9 @@ import { abstractSyntaxTree } from './abstractSyntaxTree' import { lexer } from './tokeniser' -import { executor, ViewerArtifact, processShownObjects } from './executor' +import { executor, SketchGroup, ExtrudeGroup } from './executor' -describe('findClosingBrace', () => { - test('finds the closing brace', () => { +describe('testing artifacts', () => { + test('sketch artifacts', () => { const code = ` sketch mySketch001 { lineTo(-1.59, -1.54) @@ -12,34 +12,86 @@ sketch mySketch001 { |> rx(45, %) show(mySketch001)` const programMemory = executor(abstractSyntaxTree(lexer(code))) - const geos: ViewerArtifact[] = - programMemory?.return?.flatMap( - ({ name }: { name: string }) => - processShownObjects(programMemory, programMemory?.root?.[name]) || [] - ) || [] - const artifactsWithouGeos = removeGeo(geos) - expect(artifactsWithouGeos).toEqual([ + const geos = programMemory?.return?.map( + (a) => programMemory?.root?.[a.name] + ) + const artifactsWithoutGeos = removeGeo(geos as any) + expect(artifactsWithoutGeos).toEqual([ { - type: 'parent', - sourceRange: [74, 83], - children: [ + type: 'sketchGroup', + value: [ + { + type: 'toPoint', + to: [-1.59, -1.54], + from: [0, 0], + __geoMeta: { + sourceRange: [24, 44], + pathToNode: [], + geos: ['line', 'lineEnd'], + }, + }, + { + type: 'toPoint', + to: [0.46, -5.82], + from: [-1.59, -1.54], + __geoMeta: { + sourceRange: [47, 66], + pathToNode: [], + geos: ['line', 'lineEnd'], + }, + }, + ], + position: [0, 0, 0], + rotation: [0.3826834323650898, 0, 0, 0.9238795325112867], + __meta: [ { - type: 'sketch', sourceRange: [20, 68], - children: [ - { - type: 'sketchBase', - sourceRange: [0, 0], - }, - { - type: 'sketchLine', - sourceRange: [24, 44], - }, - { - type: 'sketchLine', - sourceRange: [47, 66], - }, - ], + pathToNode: ['body', 0, 'declarations', 0, 'init', 0], + }, + { + sourceRange: [74, 83], + pathToNode: [], + }, + ], + }, + ]) + }) + test('extrude artifacts', () => { + const code = ` +sketch mySketch001 { + lineTo(-1.59, -1.54) + lineTo(0.46, -5.82) +} + |> rx(45, %) + |> extrude(2, %) +show(mySketch001)` + const programMemory = executor(abstractSyntaxTree(lexer(code))) + const geos = programMemory?.return?.map( + (a) => programMemory?.root?.[a.name] + ) + const artifactsWithoutGeos = removeGeo(geos as any) + expect(artifactsWithoutGeos).toEqual([ + { + type: 'extrudeGroup', + value: [ + { + type: 'extrudePlane', + position: [0, 0, 0], + rotation: [0.3826834323650898, 0, 0, 0.9238795325112867], + __geoMeta: { + geo: 'PlaneGeometry', + sourceRange: [47, 66], + pathToNode: [], + }, + }, + ], + height: 2, + position: [0, 0, 0], + rotation: [0.3826834323650898, 0, 0, 0.9238795325112867], + __meta: [ + { + sourceRange: [89, 102], + pathToNode: [], }, ], }, @@ -47,24 +99,29 @@ show(mySketch001)` }) }) -function removeGeo(arts: ViewerArtifact[]): any { +function removeGeo(arts: (SketchGroup | ExtrudeGroup)[]): any { return arts.map((art) => { - if (art.type === 'sketchLine' || art.type === 'sketchBase') { - const { geo, ...rest } = art - return rest - } - if (art.type === 'parent') { + if (art.type === 'extrudeGroup') { return { ...art, - children: removeGeo(art.children), + value: art.value.map((v) => ({ + ...v, + __geoMeta: { + ...v.__geoMeta, + geo: v.__geoMeta.geo.type, + }, + })), } } - if (art.type === 'sketch') { - return { - ...art, - children: removeGeo(art.children), - } + return { + ...art, + value: art.value.map((v) => ({ + ...v, + __geoMeta: { + ...v.__geoMeta, + geos: v.__geoMeta.geos.map((g) => g.type), + }, + })), } - return art }) } diff --git a/src/lang/engine.tsx b/src/lang/engine.tsx index 05c12f283..ecf031fb8 100644 --- a/src/lang/engine.tsx +++ b/src/lang/engine.tsx @@ -44,19 +44,17 @@ function trigCalcs({ } } -export interface LineGeos { - line: BufferGeometry - tip: BufferGeometry - centre: BufferGeometry -} - export function lineGeo({ from, to, }: { from: [number, number, number] to: [number, number, number] -}): LineGeos { +}): { + line: BufferGeometry + tip: BufferGeometry + centre: BufferGeometry +} { const { centre, Hypotenuse: Hypotenuse3d, diff --git a/src/lang/executor.test.ts b/src/lang/executor.test.ts index c53e1a1e0..46c41a36d 100644 --- a/src/lang/executor.test.ts +++ b/src/lang/executor.test.ts @@ -2,21 +2,20 @@ import fs from 'node:fs' import { abstractSyntaxTree } from './abstractSyntaxTree' import { lexer } from './tokeniser' -import { executor, ProgramMemory } from './executor' -import { Transform, SketchGeo } from './sketch' +import { executor, ProgramMemory, Path, SketchGroup } from './executor' describe('test', () => { it('test assigning two variables, the second summing with the first', () => { const code = `const myVar = 5 const newVar = myVar + 1` const { root } = exe(code) - expect(root.myVar).toBe(5) - expect(root.newVar).toBe(6) + expect(root.myVar.value).toBe(5) + expect(root.newVar.value).toBe(6) }) it('test assigning a var with a string', () => { const code = `const myVar = "a str"` const { root } = exe(code) - expect(root.myVar).toBe('a str') + expect(root.myVar.value).toBe('a str') }) it('test assigning a var by cont concatenating two strings string execute', () => { const code = fs.readFileSync( @@ -24,21 +23,30 @@ const newVar = myVar + 1` 'utf-8' ) const { root } = exe(code) - expect(root.myVar).toBe('a str another str') + expect(root.myVar.value).toBe('a str another str') }) it('test with function call', () => { const code = ` const myVar = "hello" log(5, myVar)` - const programMemoryOverride = { - log: jest.fn(), + const programMemoryOverride: ProgramMemory['root'] = { + log: { + type: 'userVal', + value: jest.fn(), + __meta: [ + { + sourceRange: [0, 0], + pathToNode: [], + }, + ], + }, } const { root } = executor(abstractSyntaxTree(lexer(code)), { root: programMemoryOverride, _sketch: [], }) - expect(root.myVar).toBe('hello') - expect(programMemoryOverride.log).toHaveBeenCalledWith(5, 'hello') + expect(root.myVar.value).toBe('hello') + expect(programMemoryOverride.log.value).toHaveBeenCalledWith(5, 'hello') }) it('fn funcN = () => {} execute', () => { const { root } = exe( @@ -50,39 +58,71 @@ log(5, myVar)` 'const magicNum = funcN(9, theVar)', ].join('\n') ) - expect(root.theVar).toBe(60) - expect(root.magicNum).toBe(69) + expect(root.theVar.value).toBe(60) + expect(root.magicNum.value).toBe(69) }) it('sketch declaration', () => { let code = `sketch mySketch { - path myPath = lineTo(0,1) - lineTo(1,1) - path rightPath = lineTo(1,0) + path myPath = lineTo(0,2) + lineTo(2,3) + path rightPath = lineTo(5,-1) close() } show(mySketch) ` const { root, return: _return } = exe(code) - expect( - root.mySketch.sketch.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' }, - { type: 'toPoint', to: [1, 1], sourceRange: [48, 59] }, - { type: 'toPoint', to: [1, 0], sourceRange: [67, 90], name: 'rightPath' }, + // geo is three js buffer geometry and is very bloated to have in tests + const minusGeo = removeGeoFromPaths(root.mySketch.value) + expect(minusGeo).toEqual([ { - type: 'close', - sourceRange: [93, 100], + type: 'toPoint', + to: [0, 2], + from: [5, -1], + __geoMeta: { + sourceRange: [25, 45], + pathToNode: [], + geos: ['line', 'lineEnd'], + }, + name: 'myPath', + }, + { + type: 'toPoint', + to: [2, 3], + from: [0, 2], + __geoMeta: { + sourceRange: [48, 59], + pathToNode: [], + geos: ['line', 'lineEnd'], + }, + }, + { + type: 'toPoint', + to: [5, -1], + from: [2, 3], + __geoMeta: { + sourceRange: [67, 91], + pathToNode: [], + geos: ['line', 'lineEnd'], + }, + name: 'rightPath', + }, + { + type: 'toPoint', + from: [5, -1], + to: [0, 2], + __geoMeta: { + sourceRange: [94, 101], + pathToNode: [], + geos: ['line', 'lineEnd'], + }, }, ]) // expect(root.mySketch.sketch[0]).toEqual(root.mySketch.sketch[4].firstPath) expect(_return).toEqual([ { type: 'Identifier', - start: 108, - end: 116, + start: 109, + end: 117, name: 'mySketch', }, ]) @@ -94,7 +134,7 @@ show(mySketch) 'const myVar = 5 + 1 |> myFn(%)', ].join('\n') const { root } = exe(code) - expect(root.myVar).toBe(7) + expect(root.myVar.value).toBe(7) }) it('rotated sketch', () => { @@ -108,8 +148,20 @@ show(mySketch) // 'show(mySk1)', ].join('\n') const { root } = exe(code) - expect(root.mySk1.sketch).toHaveLength(4) - expect(root?.rotated?.type).toBe('transform') + expect(root.mySk1.value).toHaveLength(3) + expect(root?.rotated?.type).toBe('sketchGroup') + if ( + root?.mySk1?.type !== 'sketchGroup' || + root?.rotated?.type !== 'sketchGroup' + ) + throw new Error('not a sketch group') + expect(root.mySk1.rotation).toEqual([0, 0, 0, 1]) + expect(root.rotated.rotation.map((a) => a.toFixed(4))).toEqual([ + '0.7071', + '0.0000', + '0.0000', + '0.7071', + ]) }) it('execute pipe sketch into call expression', () => { @@ -121,73 +173,87 @@ show(mySketch) '} |> rx(90, %)', ].join('\n') const { root } = exe(code) - const striptVersion = removeGeoFromSketch(root.mySk1) + const striptVersion = removeGeoFromSketch(root.mySk1 as SketchGroup) expect(striptVersion).toEqual({ - type: 'sketchGeo', - sketch: [ - { - type: 'base', - from: [0, 0], - sourceRange: [0, 0], - }, + type: 'sketchGroup', + value: [ { type: 'toPoint', to: [1, 1], - sourceRange: [17, 28], + from: [0, 0], + __geoMeta: { + sourceRange: [17, 28], + pathToNode: [], + geos: ['line', 'lineEnd'], + }, }, { type: 'toPoint', to: [0, 1], - sourceRange: [36, 57], + from: [1, 1], + __geoMeta: { + sourceRange: [36, 57], + pathToNode: [], + geos: ['line', 'lineEnd'], + }, name: 'myPath', }, { type: 'toPoint', to: [1, 1], - sourceRange: [60, 71], + from: [0, 1], + __geoMeta: { + sourceRange: [60, 71], + pathToNode: [], + geos: ['line', 'lineEnd'], + }, + }, + ], + position: [0, 0, 0], + rotation: [0.7071067811865475, 0, 0, 0.7071067811865476], + __meta: [ + { + sourceRange: [13, 73], + pathToNode: ['body', 0, 'declarations', 0, 'init', 0], + }, + { + sourceRange: [77, 86], + pathToNode: [], }, ], - 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], - // }) }) it('execute array expression', () => { const code = ['const three = 3', "const yo = [1, '2', three, 4 + 5]"].join( '\n' ) const { root } = exe(code) + // TODO path to node is probably wrong here, zero indexes are not correct expect(root).toEqual({ - three: 3, - yo: [1, '2', 3, 9], + three: { + type: 'userVal', + value: 3, + __meta: [ + { + pathToNode: ['body', 0, 'declarations', 0, 'init'], + sourceRange: [14, 15], + }, + ], + }, + yo: { + type: 'userVal', + value: [1, '2', 3, 9], + __meta: [ + { + pathToNode: ['body', 1, 'declarations', 0, 'init'], + sourceRange: [27, 49], + }, + { + pathToNode: ['body', 0, 'declarations', 0, 'init'], + sourceRange: [14, 15], + }, + ], + }, }) }) it('execute object expression', () => { @@ -196,14 +262,20 @@ show(mySketch) "const yo = {aStr: 'str', anum: 2, identifier: three, binExp: 4 + 5}", ].join('\n') const { root } = exe(code) - expect(root).toEqual({ - three: 3, - yo: { + expect(root.yo).toEqual({ + type: 'userVal', + value: { aStr: 'str', anum: 2, identifier: 3, binExp: 9, }, + __meta: [ + { + pathToNode: ['body', 1, 'declarations', 0, 'init'], + sourceRange: [27, 83], + }, + ], }) }) it('execute memberExpression', () => { @@ -211,11 +283,15 @@ show(mySketch) '\n' ) const { root } = exe(code) - expect(root).toEqual({ - yo: { - a: { b: '123' }, - }, - myVar: '123', + expect(root.myVar).toEqual({ + type: 'userVal', + value: '123', + __meta: [ + { + pathToNode: ['body', 1, 'declarations', 0, 'init'], + sourceRange: [41, 50], + }, + ], }) }) }) @@ -231,15 +307,22 @@ function exe( return executor(ast, programMemory) } -function removeGeoFromSketch(sketch: Transform | SketchGeo): any { - if (sketch.type !== 'sketchGeo' && sketch.type === 'transform') { - return removeGeoFromSketch(sketch.sketch as any) // TODO fix type +function removeGeoFromSketch(sketch: SketchGroup): any { + return { + ...sketch, + value: removeGeoFromPaths(sketch.value), } - if (sketch.type === 'sketchGeo') { - return { - ...sketch, - sketch: sketch.sketch.map(({ geo, previousPath, ...rest }: any) => rest), - } - } - throw new Error('not a sketch') +} + +function removeGeoFromPaths(paths: Path[]): any[] { + return paths.map((path: Path) => { + const newGeos = path?.__geoMeta?.geos.map((geo) => geo.type) + return { + ...path, + __geoMeta: { + ...path.__geoMeta, + geos: newGeos, + }, + } + }) } diff --git a/src/lang/executor.ts b/src/lang/executor.ts index d44785682..706c3b79b 100644 --- a/src/lang/executor.ts +++ b/src/lang/executor.ts @@ -5,21 +5,108 @@ import { PipeExpression, ObjectExpression, MemberExpression, + Identifier, } from './abstractSyntaxTree' -import { Path, Transform, SketchGeo, sketchFns, ExtrudeGeo } from './sketch' -import { BufferGeometry, Quaternion, Vector3 } from 'three' -import { LineGeos } from './engine' +import { sketchFns } from './sketch' +import { BufferGeometry } from 'three' + +export type SourceRange = [number, number] +export type PathToNode = (string | number)[] +export type Metadata = { + sourceRange: SourceRange + pathToNode: PathToNode +} +export type Position = [number, number, number] +export type Rotation = [number, number, number, number] + +interface BasePath { + from: [number, number] + to: [number, number] + name?: string + __geoMeta: { + geos: { + geo: BufferGeometry + type: 'line' | 'lineEnd' + }[] + sourceRange: SourceRange + pathToNode: PathToNode + } +} + +export interface ToPoint extends BasePath { + type: 'toPoint' +} + +export interface HorizontalLineTo extends BasePath { + type: 'horizontalLineTo' + x: number +} + +export interface AngledLineTo extends BasePath { + type: 'angledLineTo' + angle: number + x?: number + y?: number +} + +interface GeoMeta { + __geoMeta: { + geo: BufferGeometry + sourceRange: SourceRange + pathToNode: PathToNode + } +} + +export type Path = ToPoint | HorizontalLineTo | AngledLineTo + +export interface SketchGroup { + type: 'sketchGroup' + value: Path[] + position: Position + rotation: Rotation + __meta: Metadata[] +} + +interface ExtrudePlane { + type: 'extrudePlane' + position: Position + rotation: Rotation +} + +export type ExtrudeSurface = GeoMeta & + ExtrudePlane /* | ExtrudeRadius | ExtrudeSpline */ + +export interface ExtrudeGroup { + type: 'extrudeGroup' + value: ExtrudeSurface[] + height: number + position: Position + rotation: Rotation + __meta: Metadata[] +} + +/** UserVal not produced by one of our internal functions */ +export interface UserVal { + type: 'userVal' + value: any + __meta: Metadata[] +} + +interface Memory { + [key: string]: UserVal | SketchGroup | ExtrudeGroup // | Memory +} export interface ProgramMemory { - root: { [key: string]: any } - return?: any - _sketch: Path[] + root: Memory + return?: Identifier[] + _sketch?: Path[] } export const executor = ( node: Program, programMemory: ProgramMemory = { root: {}, _sketch: [] }, - options: { bodyType: 'root' | 'sketch' | 'block' } = { bodyType: 'root' } + options: { bodyType: 'root' | 'sketch' | 'block' } = { bodyType: 'root' }, + previousPathToNode: PathToNode = [] ): ProgramMemory => { const _programMemory: ProgramMemory = { root: { @@ -29,43 +116,103 @@ export const executor = ( return: programMemory.return, } const { body } = node - body.forEach((statement) => { + body.forEach((statement, bodyIndex) => { if (statement.type === 'VariableDeclaration') { - statement.declarations.forEach((declaration) => { + statement.declarations.forEach((declaration, index) => { const variableName = declaration.id.name + const pathToNode = [ + ...previousPathToNode, + 'body', + bodyIndex, + 'declarations', + index, + 'init', + ] + const sourceRange: SourceRange = [ + declaration.init.start, + declaration.init.end, + ] + const __meta: Metadata[] = [ + { + pathToNode, + sourceRange, + }, + ] + if (declaration.init.type === 'PipeExpression') { - _programMemory.root[variableName] = getPipeExpressionResult( + const value = getPipeExpressionResult( declaration.init, - _programMemory + _programMemory, + pathToNode ) - } else if (declaration.init.type === 'Literal') { - _programMemory.root[variableName] = declaration.init.value - } else if (declaration.init.type === 'BinaryExpression') { - _programMemory.root[variableName] = getBinaryExpressionResult( - declaration.init, - _programMemory - ) - } else if (declaration.init.type === 'ArrayExpression') { - _programMemory.root[variableName] = declaration.init.elements.map( - (element) => { - if (element.type === 'Literal') { - return element.value - } else if (element.type === 'BinaryExpression') { - return getBinaryExpressionResult(element, _programMemory) - } else if (element.type === 'PipeExpression') { - return getPipeExpressionResult(element, _programMemory) - } else if (element.type === 'Identifier') { - return _programMemory.root[element.name] - } else { - throw new Error( - `Unexpected element type ${element.type} in array expression` - ) - } + if (value?.type === 'sketchGroup' || value?.type === 'extrudeGroup') { + _programMemory.root[variableName] = value + } else { + _programMemory.root[variableName] = { + type: 'userVal', + value, + __meta, } - ) + } + } else if (declaration.init.type === 'Literal') { + _programMemory.root[variableName] = { + type: 'userVal', + value: declaration.init.value, + __meta, + } + } else if (declaration.init.type === 'BinaryExpression') { + _programMemory.root[variableName] = { + type: 'userVal', + value: getBinaryExpressionResult(declaration.init, _programMemory), + __meta, + } + } else if (declaration.init.type === 'ArrayExpression') { + const valueInfo: { value: any; __meta?: Metadata }[] = + declaration.init.elements.map( + (element): { value: any; __meta?: Metadata } => { + if (element.type === 'Literal') { + return { + value: element.value, + } + } else if (element.type === 'BinaryExpression') { + return { + value: getBinaryExpressionResult(element, _programMemory), + } + } else if (element.type === 'PipeExpression') { + return { + value: getPipeExpressionResult( + element, + _programMemory, + pathToNode + ), + } + } else if (element.type === 'Identifier') { + const node = _programMemory.root[element.name] + return { + value: node.value, + __meta: node.__meta[node.__meta.length - 1], + } + } else { + throw new Error( + `Unexpected element type ${element.type} in array expression` + ) + } + } + ) + const meta = valueInfo + .filter(({ __meta }) => __meta) + .map(({ __meta }) => __meta) as Metadata[] + _programMemory.root[variableName] = { + type: 'userVal', + value: valueInfo.map(({ value }) => value), + __meta: [...__meta, ...meta], + } } else if (declaration.init.type === 'ObjectExpression') { - const obj = executeObjectExpression(_programMemory, declaration.init) - _programMemory.root[variableName] = obj + _programMemory.root[variableName] = { + type: 'userVal', + value: executeObjectExpression(_programMemory, declaration.init), + __meta, + } } else if (declaration.init.type === 'SketchExpression') { const sketchInit = declaration.init const fnMemory: ProgramMemory = { @@ -77,64 +224,66 @@ export const executor = ( 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 - } - const newSketch: SketchGeo = { - type: 'sketchGeo', - sketch: _sketch, - sourceRange: [sketchInit.start, sketchInit.end], + const newSketch: SketchGroup = { + type: 'sketchGroup', + value: _sketch || [], + position: [0, 0, 0], + rotation: [0, 0, 0, 1], //x,y,z,w + __meta, } _programMemory.root[variableName] = newSketch } else if (declaration.init.type === 'FunctionExpression') { const fnInit = declaration.init - _programMemory.root[declaration.id.name] = (...args: any[]) => { - const fnMemory: ProgramMemory = { - root: { - ..._programMemory.root, - }, - _sketch: [], - } - if (args.length > fnInit.params.length) { - throw new Error( - `Too many arguments passed to function ${declaration.id.name}` - ) - } else if (args.length < fnInit.params.length) { - throw new Error( - `Too few arguments passed to function ${declaration.id.name}` - ) - } - fnInit.params.forEach((param, index) => { - fnMemory.root[param.name] = args[index] - }) - return executor(fnInit.body, fnMemory, { bodyType: 'block' }).return + _programMemory.root[declaration.id.name] = { + type: 'userVal', + value: (...args: any[]) => { + const fnMemory: ProgramMemory = { + root: { + ..._programMemory.root, + }, + _sketch: [], + } + if (args.length > fnInit.params.length) { + throw new Error( + `Too many arguments passed to function ${declaration.id.name}` + ) + } else if (args.length < fnInit.params.length) { + throw new Error( + `Too few arguments passed to function ${declaration.id.name}` + ) + } + fnInit.params.forEach((param, index) => { + fnMemory.root[param.name] = { + type: 'userVal', + value: args[index], + __meta, + } + }) + return executor(fnInit.body, fnMemory, { bodyType: 'block' }) + .return + }, + __meta, } } else if (declaration.init.type === 'MemberExpression') { - _programMemory.root[variableName] = getMemberExpressionResult( - declaration.init, - _programMemory - ) + _programMemory.root[variableName] = { + type: 'userVal', + value: getMemberExpressionResult(declaration.init, _programMemory), + __meta, + } } else if (declaration.init.type === 'CallExpression') { const functionName = declaration.init.callee.name const fnArgs = declaration.init.arguments.map((arg) => { if (arg.type === 'Literal') { return arg.value } else if (arg.type === 'Identifier') { - return _programMemory.root[arg.name] + return _programMemory.root[arg.name].value } }) if ( 'lineTo' === functionName || - 'close' === functionName || - 'base' === functionName + 'close' === functionName // || + // 'base' === functionName ) { if (options.bodyType !== 'sketch') { throw new Error( @@ -148,7 +297,11 @@ export const executor = ( ...fnArgs ) _programMemory._sketch = result.programMemory._sketch - _programMemory.root[variableName] = result.currentPath + _programMemory.root[variableName] = { + type: 'userVal', + value: result.currentPath, + __meta, + } } else if ( 'rx' === functionName || 'ry' === functionName || @@ -162,9 +315,9 @@ export const executor = ( _programMemory, [declaration.start, declaration.end], fnArgs[0], - sketchVal + sketchVal as any // todo memory redo ) - _programMemory.root[variableName] = result + _programMemory.root[variableName] = result as any // todo memory redo } else if (functionName === 'extrude') { const sketch = declaration.init.arguments[1] if (sketch.type !== 'Identifier') @@ -175,9 +328,9 @@ export const executor = ( 'yo', [declaration.start, declaration.end], fnArgs[0], - sketchVal + sketchVal as any // todo memory redo ) - _programMemory.root[variableName] = result + _programMemory.root[variableName] = result as any // todo memory redo } else if (functionName === 'translate') { const sketch = declaration.init.arguments[1] if (sketch.type !== 'Identifier') @@ -187,13 +340,15 @@ export const executor = ( _programMemory, [declaration.start, declaration.end], fnArgs[0], - sketchVal + sketchVal as any // todo memory redo ) - _programMemory.root[variableName] = result + _programMemory.root[variableName] = result as any // todo memory redo } else { - _programMemory.root[variableName] = _programMemory.root[ - functionName - ](...fnArgs) + _programMemory.root[variableName] = { + type: 'userVal', + value: _programMemory.root[functionName].value(...fnArgs), + __meta, + } } } else { throw new Error( @@ -209,13 +364,13 @@ export const executor = ( if (arg.type === 'Literal') { return arg.value } else if (arg.type === 'Identifier') { - return _programMemory.root[arg.name] + return _programMemory.root[arg.name].value } }) if ( 'lineTo' === functionName || - 'close' === functionName || - 'base' === functionName + 'close' === functionName + // || 'base' === functionName ) { if (options.bodyType !== 'sketch') { throw new Error( @@ -228,14 +383,14 @@ export const executor = ( [statement.start, statement.end], ...args ) - _programMemory._sketch = [...result.programMemory._sketch] + _programMemory._sketch = [...(result.programMemory._sketch || [])] } else if ('show' === functionName) { if (options.bodyType !== 'root') { throw new Error(`Cannot call ${functionName} outside of a root`) } - _programMemory.return = expression.arguments + _programMemory.return = expression.arguments as any // todo memory redo } else { - _programMemory.root[functionName](...args) + _programMemory.root[functionName].value(...args) } } } else if (statement.type === 'ReturnStatement') { @@ -262,7 +417,7 @@ function getMemberExpressionResult( const object: any = expression.object.type === 'MemberExpression' ? getMemberExpressionResult(expression.object, programMemory) - : programMemory.root[expression.object.name] + : programMemory.root[expression.object.name].value return object[propertyName] } @@ -274,7 +429,7 @@ function getBinaryExpressionResult( if (part.type === 'Literal') { return part.value } else if (part.type === 'Identifier') { - return programMemory.root[part.name] + return programMemory.root[part.name].value } } const left = getVal(expression.left) @@ -284,9 +439,14 @@ function getBinaryExpressionResult( function getPipeExpressionResult( expression: PipeExpression, - programMemory: ProgramMemory + programMemory: ProgramMemory, + previousPathToNode: PathToNode = [] ) { - const executedBody = executePipeBody(expression.body, programMemory) + const executedBody = executePipeBody( + expression.body, + programMemory, + previousPathToNode + ) const result = executedBody[executedBody.length - 1] return result } @@ -294,6 +454,7 @@ function getPipeExpressionResult( function executePipeBody( body: PipeExpression['body'], programMemory: ProgramMemory, + previousPathToNode: PathToNode = [], expressionIndex = 0, previousResults: any[] = [] ): any[] { @@ -303,10 +464,13 @@ function executePipeBody( const expression = body[expressionIndex] if (expression.type === 'BinaryExpression') { const result = getBinaryExpressionResult(expression, programMemory) - return executePipeBody(body, programMemory, expressionIndex + 1, [ - ...previousResults, - result, - ]) + return executePipeBody( + body, + programMemory, + previousPathToNode, + expressionIndex + 1, + [...previousResults, result] + ) } else if (expression.type === 'CallExpression') { const functionName = expression.callee.name const fnArgs = expression.arguments.map((arg) => { @@ -341,10 +505,13 @@ function executePipeBody( fnArgs[0], fnArgs[1] ) - return executePipeBody(body, programMemory, expressionIndex + 1, [ - ...previousResults, - result, - ]) + return executePipeBody( + body, + programMemory, + previousPathToNode, + expressionIndex + 1, + [...previousResults, result] + ) } if (functionName === 'extrude') { const result = sketchFns[functionName]( @@ -354,10 +521,13 @@ function executePipeBody( fnArgs[0], fnArgs[1] ) - return executePipeBody(body, programMemory, expressionIndex + 1, [ - ...previousResults, - result, - ]) + return executePipeBody( + body, + programMemory, + previousPathToNode, + expressionIndex + 1, + [...previousResults, result] + ) } if (functionName === 'translate') { const result = sketchFns[functionName]( @@ -366,16 +536,22 @@ function executePipeBody( fnArgs[0], fnArgs[1] ) - return executePipeBody(body, programMemory, expressionIndex + 1, [ - ...previousResults, - result, - ]) + return executePipeBody( + body, + programMemory, + previousPathToNode, + expressionIndex + 1, + [...previousResults, result] + ) } - const result = programMemory.root[functionName](...fnArgs) - return executePipeBody(body, programMemory, expressionIndex + 1, [ - ...previousResults, - result, - ]) + const result = programMemory.root[functionName].value(...fnArgs) + return executePipeBody( + body, + programMemory, + previousPathToNode, + expressionIndex + 1, + [...previousResults, result] + ) } else if (expression.type === 'SketchExpression') { const sketchBody = expression.body const fnMemory: ProgramMemory = { @@ -387,26 +563,25 @@ function executePipeBody( let { _sketch } = executor(sketchBody, fnMemory, { bodyType: 'sketch', }) - if (_sketch.length === 0) { - const { programMemory: newProgramMemory } = sketchFns.base( - fnMemory, - '', - [0, 0], - 0, - 0 - ) - _sketch = newProgramMemory._sketch + const newSketch: SketchGroup = { + type: 'sketchGroup', + value: _sketch || [], + position: [0, 0, 0], + rotation: [0, 0, 0, 1], //x,y,z,w + __meta: [ + { + sourceRange: [expression.start, expression.end], + pathToNode: [...previousPathToNode, expressionIndex], + }, + ], } - // _programMemory.root[variableName] = _sketch - const newSketch: SketchGeo = { - type: 'sketchGeo', - sketch: _sketch, - sourceRange: [expression.start, expression.end], - } - return executePipeBody(body, programMemory, expressionIndex + 1, [ - ...previousResults, - newSketch, - ]) + return executePipeBody( + body, + programMemory, + previousPathToNode, + expressionIndex + 1, + [...previousResults, newSketch] + ) } throw new Error('Invalid pipe expression') @@ -432,7 +607,7 @@ function executeObjectExpression( _programMemory ) } else if (property.value.type === 'Identifier') { - obj[property.key.name] = _programMemory.root[property.value.name] + obj[property.key.name] = _programMemory.root[property.value.name].value } else if (property.value.type === 'ObjectExpression') { obj[property.key.name] = executeObjectExpression( _programMemory, @@ -451,125 +626,3 @@ function executeObjectExpression( }) return obj } - -type SourceRange = [number, number] - -export type ViewerArtifact = - | { - type: 'sketchLine' - sourceRange: SourceRange - geo: LineGeos - } - | { - type: 'sketchBase' - sourceRange: SourceRange - geo: BufferGeometry - } - | { - type: 'extrudeWall' - sourceRange: SourceRange - geo: BufferGeometry - } - | { - type: 'parent' - sourceRange: SourceRange - children: ViewerArtifact[] - } - | { - type: 'sketch' - sourceRange: SourceRange - children: ViewerArtifact[] - } - -type PreviousTransforms = { - rotation: Quaternion - transform: [number, number, number] -}[] - -export const processShownObjects = ( - programMemory: ProgramMemory, - geoMeta: SketchGeo | ExtrudeGeo | Transform, - previousTransforms: PreviousTransforms = [] -): ViewerArtifact[] => { - if (geoMeta?.type === 'sketchGeo') { - return [ - { - type: 'sketch', - sourceRange: geoMeta.sourceRange, - children: geoMeta.sketch.map(({ geo, sourceRange, type }) => { - if (type === 'toPoint') { - const newGeo: LineGeos = { - line: geo.line.clone(), - tip: geo.tip.clone(), - centre: geo.centre.clone(), - } - let rotationQuaternion = new Quaternion() - let position = new Vector3(0, 0, 0) - previousTransforms.forEach(({ rotation, transform }) => { - const newQuant = rotation.clone() - newQuant.multiply(rotationQuaternion) - rotationQuaternion.copy(newQuant) - position.applyQuaternion(rotation) - position.add(new Vector3(...transform)) - }) - Object.values(newGeo).forEach((geoItem: BufferGeometry) => { - geoItem.applyQuaternion(rotationQuaternion.clone()) - const position_ = position.clone() - geoItem.translate(position_.x, position_.y, position_.z) - }) - return { - type: 'sketchLine', - geo: newGeo, - sourceRange, - } - } else if (type === 'base') { - const newGeo: BufferGeometry = geo.clone() - const rotationQuaternion = new Quaternion() - let position = new Vector3(0, 0, 0) - // todo don't think this is right - previousTransforms.forEach(({ rotation, transform }) => { - newGeo.applyQuaternion(rotationQuaternion) - newGeo.translate(position.x, position.y, position.z) - }) - newGeo.applyQuaternion(rotationQuaternion) - newGeo.translate(position.x, position.y, position.z) - return { - type: 'sketchBase', - geo: newGeo, - sourceRange, - } - } - throw new Error('Unknown geo type') - }), - }, - ] - } else if (geoMeta.type === 'transform') { - const referencedVar = geoMeta.sketch - const parentArtifact: ViewerArtifact = { - type: 'parent', - sourceRange: geoMeta.sourceRange, - children: processShownObjects(programMemory, referencedVar, [ - { - rotation: geoMeta.rotation, - transform: geoMeta.transform, - }, - ...previousTransforms, - ]), - } - return [parentArtifact] - } else if (geoMeta.type === 'extrudeGeo') { - const result: ViewerArtifact[] = geoMeta.surfaces.map((a) => { - const geo: BufferGeometry = a.geo.clone() - - geo.translate(a.translate[0], a.translate[1], a.translate[2]) - geo.applyQuaternion(a.quaternion) - return { - type: 'extrudeWall', - sourceRange: a.sourceRanges[0], - geo, - } - }) - return result - } - throw new Error('Unknown geoMeta type') -} diff --git a/src/lang/sketch.ts b/src/lang/sketch.ts index eb8891920..c1b1fa1f5 100644 --- a/src/lang/sketch.ts +++ b/src/lang/sketch.ts @@ -1,100 +1,15 @@ -import { ProgramMemory } from './executor' -import { lineGeo, baseGeo, LineGeos, extrudeGeo } from './engine' -import { BufferGeometry } from 'three' +import { + ProgramMemory, + Path, + SketchGroup, + ExtrudeGroup, + SourceRange, + ExtrudeSurface, +} from './executor' +import { lineGeo, extrudeGeo } from './engine' import { Quaternion, Vector3 } from 'three' type Coords2d = [number, number] -type SourceRange = [number, number] -type Rotation3 = Quaternion -type Translate3 = [number, number, number] - -export type Path = - | { - type: 'points' - name?: string - from: Coords2d - to: Coords2d - geo: BufferGeometry - sourceRange: SourceRange - } - | { - type: 'horizontalLineTo' - name?: string - x: number - geo: BufferGeometry - sourceRange: SourceRange - } - | { - type: 'verticalLineTo' - name?: string - y: number - geo: BufferGeometry - sourceRange: SourceRange - } - | { - type: 'toPoint' - name?: string - to: Coords2d - geo: LineGeos - sourceRange: SourceRange - } - | { - type: 'close' - name?: string - geo: LineGeos - sourceRange: SourceRange - } - | { - type: 'base' - from: Coords2d - geo: BufferGeometry - sourceRange: SourceRange - } - -export interface Transform { - type: 'transform' - rotation: Rotation3 - transform: Translate3 - sketch: SketchGeo | ExtrudeGeo | Transform - sourceRange: SourceRange -} - -export interface SketchGeo { - type: 'sketchGeo' - sketch: Path[] - sourceRange: SourceRange -} - -export interface ExtrudeFace { - type: 'extrudeFace' - quaternion: Quaternion - translate: [number, number, number] - geo: BufferGeometry - sourceRanges: SourceRange[] -} - -export interface ExtrudeGeo { - type: 'extrudeGeo' - surfaces: ExtrudeFace[] - 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 { - ...programMemory, - _sketch: [base], - } - } - return programMemory -} interface PathReturn { programMemory: ProgramMemory @@ -106,70 +21,74 @@ function getCoordsFromPaths(paths: Path[], index = 0): Coords2d { if (!currentPath) { return [0, 0] } - if (currentPath.type === 'points' || currentPath.type === 'toPoint') { - return currentPath.to - } else if (currentPath.type === 'base') { - return currentPath.from - } else if (currentPath.type === 'horizontalLineTo') { + if (currentPath.type === 'horizontalLineTo') { const pathBefore = getCoordsFromPaths(paths, index - 1) return [currentPath.x, pathBefore[1]] - } else if (currentPath.type === 'verticalLineTo') { - const pathBefore = getCoordsFromPaths(paths, index - 1) - return [pathBefore[0], currentPath.y] + } else if (currentPath.type === 'toPoint') { + return [currentPath.to[0], currentPath.to[1]] } return [0, 0] } export const sketchFns = { - base: ( - programMemory: ProgramMemory, - name: string = '', - sourceRange: SourceRange, - ...args: any[] - ): PathReturn => { - if (programMemory._sketch?.length > 0) { - throw new Error('Base can only be called once') - } - 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: { - ...programMemory, - _sketch: [...(programMemory?._sketch || []), newPath], - }, - currentPath: newPath, - } - }, + // base: ( + // programMemory: ProgramMemory, + // name: string = '', + // sourceRange: SourceRange, + // ...args: any[] + // ): PathReturn => { + // if ((programMemory?._sketch?.length || 0) > 0) { + // throw new Error('Base can only be called once') + // } + // 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: { + // ...programMemory, + // _sketch: [...(programMemory?._sketch || []), newPath], + // }, + // currentPath: newPath, + // } + // }, close: ( programMemory: ProgramMemory, name: string = '', sourceRange: SourceRange ): PathReturn => { - const lastPath = programMemory?._sketch?.[ - programMemory?._sketch.length - 1 - ] as Path + const firstPath = programMemory?._sketch?.[0] as Path let from = getCoordsFromPaths( - programMemory?._sketch, - programMemory?._sketch.length - 1 + programMemory?._sketch || [], + (programMemory?._sketch?.length || 1) - 1 ) - const firstPath = programMemory?._sketch?.[0] as Path - if (lastPath?.type === 'base') { - throw new Error('Cannot close a base path') - } - let to = getCoordsFromPaths(programMemory?._sketch, 0) + let to = getCoordsFromPaths(programMemory?._sketch || [], 0) + const geo = lineGeo({ from: [...from, 0], to: [...to, 0] }) const newPath: Path = { - type: 'close', - geo: lineGeo({ from: [...from, 0], to: [...to, 0] }), - sourceRange, + type: 'toPoint', + from, + to, + __geoMeta: { + sourceRange, + pathToNode: [], // TODO + geos: [ + { + type: 'line', + geo: geo.line, + }, + { + type: 'lineEnd', + geo: geo.tip, + }, + ], + }, } if (name) { newPath.name = name @@ -177,7 +96,14 @@ export const sketchFns = { return { programMemory: { ...programMemory, - _sketch: [...(programMemory?._sketch || []), newPath], + _sketch: [ + { + ...firstPath, + from, + }, + ...(programMemory?._sketch || []).slice(1), + newPath, + ], }, currentPath: newPath, } @@ -188,28 +114,41 @@ export const sketchFns = { sourceRange: SourceRange, ...args: any[] ): PathReturn => { - const _programMemory = addBasePath(programMemory) const [x, y] = args - if (!_programMemory._sketch) { + if (!programMemory._sketch) { throw new Error('No sketch to draw on') } let from = getCoordsFromPaths( - programMemory?._sketch, - programMemory?._sketch.length - 1 + programMemory?._sketch || [], + (programMemory?._sketch?.length || 1) - 1 ) + const geo = lineGeo({ from: [...from, 0], to: [x, y, 0] }) const currentPath: Path = { type: 'toPoint', to: [x, y], - geo: lineGeo({ from: [...from, 0], to: [x, y, 0] }), - sourceRange, + from, + __geoMeta: { + sourceRange, + pathToNode: [], // TODO + geos: [ + { + type: 'line', + geo: geo.line, + }, + { + type: 'lineEnd', + geo: geo.tip, + }, + ], + }, } if (name) { currentPath.name = name } return { programMemory: { - ..._programMemory, - _sketch: [...(_programMemory._sketch || []), currentPath], + ...programMemory, + _sketch: [...(programMemory._sketch || []), currentPath], }, currentPath, } @@ -222,31 +161,22 @@ export const sketchFns = { name: string = '', sourceRange: SourceRange, length: number, - sketchVal: SketchGeo | Transform - ): ExtrudeGeo | Transform => { - const getSketchGeo = (sketchVal: SketchGeo | Transform): SketchGeo => { - if ( - sketchVal.type === 'transform' && - sketchVal.sketch.type === 'extrudeGeo' - ) - throw new Error('Cannot extrude a extrude') - return sketchVal.type === 'transform' - ? getSketchGeo(sketchVal.sketch as any) // TODO fix types - : (sketchVal as SketchGeo) // TODO fix types + sketchVal: SketchGroup + ): ExtrudeGroup => { + const getSketchGeo = (sketchVal: SketchGroup): SketchGroup => { + return sketchVal } const sketch = getSketchGeo(sketchVal) - const { position, quaternion } = combineTransforms(sketchVal) + const { position, rotation } = sketchVal - const extrudeFaces: ExtrudeFace[] = [] - sketch.sketch.map((line, index) => { + const extrudeSurfaces: ExtrudeSurface[] = [] + sketch.value.map((line, index) => { if (line.type === 'toPoint' && index !== 0) { - const lastPoint = sketch.sketch[index - 1] + const lastPoint = sketch.value[index - 1] let from: [number, number] = [0, 0] if (lastPoint.type === 'toPoint') { from = lastPoint.to - } else if (lastPoint.type === 'base') { - from = lastPoint.from } const to = line.to const geo = extrudeGeo({ @@ -254,117 +184,87 @@ export const sketchFns = { to: [to[0], to[1], 0], length, }) - extrudeFaces.push({ - type: 'extrudeFace', - quaternion, - translate: position, - geo, - sourceRanges: [line.sourceRange, sourceRange], + extrudeSurfaces.push({ + type: 'extrudePlane', + position, // todo should come from extrudeGeo + rotation, // todo should come from extrudeGeo + __geoMeta: { + geo, + sourceRange: line.__geoMeta.sourceRange, + pathToNode: line.__geoMeta.pathToNode, + }, }) } }) return { - type: 'extrudeGeo', - sourceRange, - surfaces: extrudeFaces, + type: 'extrudeGroup', + value: extrudeSurfaces, + height: length, + position, + rotation, + __meta: [ + { + sourceRange, + pathToNode: [], // TODO + }, + ], } }, translate, } -function rotateOnAxis(axisMultiplier: [number, number, number]) { +function rotateOnAxis( + axisMultiplier: [number, number, number] +) { return ( programMemory: ProgramMemory, sourceRange: SourceRange, rotationD: number, - sketch: SketchGeo | Transform - ): Transform => { + sketch: T + ): T => { const rotationR = rotationD * (Math.PI / 180) const rotateVec = new Vector3(...axisMultiplier) const quaternion = new Quaternion() + quaternion.setFromAxisAngle(rotateVec, rotationR) + + const position = new Vector3(...sketch.position) + .applyQuaternion(quaternion) + .toArray() + + const existingQuat = new Quaternion(...sketch.rotation) + const rotation = quaternion.multiply(existingQuat).toArray() return { - type: 'transform', - rotation: quaternion.setFromAxisAngle(rotateVec, rotationR), - transform: [0, 0, 0], - sketch, - sourceRange, + ...sketch, + rotation, + position, + __meta: [ + ...sketch.__meta, + { + sourceRange, + pathToNode: [], // TODO + }, + ], } } } -function translate( +function translate( programMemory: ProgramMemory, sourceRange: SourceRange, vec3: [number, number, number], - sketch: SketchGeo | Transform -): Transform { + sketch: T +): T { + const oldPosition = new Vector3(...sketch.position) + const newPosition = oldPosition.add(new Vector3(...vec3)) return { - type: 'transform', - rotation: new Quaternion(), - transform: vec3, - sketch, - sourceRange, - } -} - -type PreviousTransforms = { - rotation: Quaternion - transform: [number, number, number] -}[] - -function collectTransforms( - sketchVal: SketchGeo | ExtrudeGeo | Transform, - previousTransforms: PreviousTransforms = [] -): PreviousTransforms { - if (sketchVal.type !== 'transform') return previousTransforms - const newTransforms = [ - ...previousTransforms, - { - rotation: sketchVal.rotation, - transform: sketchVal.transform, - }, - ] - return collectTransforms(sketchVal.sketch, newTransforms) -} - -export function combineTransforms( - sketchVal: SketchGeo | ExtrudeGeo | Transform -): { - quaternion: Quaternion - position: [number, number, number] -} { - const previousTransforms = collectTransforms(sketchVal) - const position = new Vector3(0, 0, 0) - const quaternion = new Quaternion() - previousTransforms.forEach(({ rotation, transform }) => { - quaternion.multiply(rotation) - position.applyQuaternion(rotation.clone().invert()) - position.add(new Vector3(...transform)) - }) - return { - quaternion, - position: [position.x, position.y, position.z], - } -} - -export function combineTransformsAlt( - sketchVal: SketchGeo | ExtrudeGeo | Transform -): { - quaternion: Quaternion - position: [number, number, number] -} { - const previousTransforms = collectTransforms(sketchVal) - let rotationQuaternion = new Quaternion() - let position = new Vector3(0, 0, 0) - previousTransforms.reverse().forEach(({ rotation, transform }) => { - const newQuant = rotation.clone() - newQuant.multiply(rotationQuaternion) - rotationQuaternion.copy(newQuant) - position.applyQuaternion(rotation) - position.add(new Vector3(...transform)) - }) - return { - quaternion: rotationQuaternion, - position: [position.x, position.y, position.z], + ...sketch, + position: [newPosition.x, newPosition.y, newPosition.z], + __meta: [ + ...sketch.__meta, + { + sourceRange, + pathToNode: [], // TODO + }, + ], } } diff --git a/src/useStore.ts b/src/useStore.ts index e97efcc02..1daa26a89 100644 --- a/src/useStore.ts +++ b/src/useStore.ts @@ -1,16 +1,12 @@ import create from 'zustand' import { addLineHighlight, EditorView } from './editor/highlightextension' import { Program, abstractSyntaxTree } from './lang/abstractSyntaxTree' -import { ProgramMemory } from './lang/executor' +import { ProgramMemory, Position, PathToNode, Rotation } from './lang/executor' import { recast } from './lang/recast' import { lexer } from './lang/tokeniser' -import { Quaternion } from 'three' export type Range = [number, number] -type PathToNode = (string | number)[] -type Position = [number, number, number] - type GuiModes = | { mode: 'default' @@ -18,7 +14,7 @@ type GuiModes = | { mode: 'sketch' sketchMode: 'points' - quaternion: Quaternion + rotation: Rotation position: Position id?: string pathToNode: PathToNode @@ -26,7 +22,7 @@ type GuiModes = | { mode: 'sketch' sketchMode: 'sketchEdit' - quaternion: Quaternion + rotation: Rotation position: Position pathToNode: PathToNode } @@ -37,7 +33,7 @@ type GuiModes = | { mode: 'canEditSketch' pathToNode: PathToNode - quaternion: Quaternion + rotation: Rotation position: Position }