massive overhall to how program memory works and how artifacts their metada are rendered
This commit is contained in:
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"geos"
|
||||
]
|
||||
}
|
44
src/App.tsx
44
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<ViewerArtifact[]>([])
|
||||
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) => {
|
||||
log: {
|
||||
type: 'userVal',
|
||||
value: (a: any) => {
|
||||
console.log('raw log', a)
|
||||
let b = a
|
||||
if (Array.isArray(a)) {
|
||||
b = a.map(({ geo, ...rest }) => rest)
|
||||
b = a.map(({ geo, __geoMeta, ...rest }) => rest)
|
||||
b = JSON.stringify(b, null, 2)
|
||||
} else if (typeof a === 'object') {
|
||||
b = JSON.stringify(a, null, 2)
|
||||
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() {
|
||||
/>
|
||||
<ambientLight />
|
||||
<pointLight position={[10, 10, 10]} />
|
||||
{geoArray.map((artifact, index) => (
|
||||
<RenderViewerArtifacts artifact={artifact} key={index} />
|
||||
))}
|
||||
<RenderViewerArtifacts artifacts={geoArray} />
|
||||
<BasePlanes />
|
||||
<SketchPlane />
|
||||
<AxisIndicator />
|
||||
|
@ -34,7 +34,7 @@ export const Toolbar = () => {
|
||||
mode: 'sketch',
|
||||
sketchMode: 'sketchEdit',
|
||||
pathToNode: guiMode.pathToNode,
|
||||
quaternion: guiMode.quaternion,
|
||||
rotation: guiMode.rotation,
|
||||
position: guiMode.position,
|
||||
})
|
||||
}}
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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<BufferGeometry | undefined>() as any
|
||||
const [hovered, setHover] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<mesh
|
||||
ref={ref}
|
||||
onPointerOver={(event) => {
|
||||
setHover(true)
|
||||
setHighlightRange(sourceRange)
|
||||
}}
|
||||
onPointerOut={(event) => {
|
||||
setHover(false)
|
||||
setHighlightRange([0, 0])
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<primitive object={geo.line} />
|
||||
<meshStandardMaterial
|
||||
color={hovered ? 'hotpink' : forceHighlight ? 'skyblue' : 'orange'}
|
||||
/>
|
||||
</mesh>
|
||||
<MovingSphere
|
||||
geo={geo.tip}
|
||||
sourceRange={sourceRange}
|
||||
editorCursor={forceHighlight}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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<BufferGeometry | undefined>() as any
|
||||
const [hovered, setHover] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<mesh
|
||||
ref={ref}
|
||||
onPointerOver={(event) => {
|
||||
setHover(true)
|
||||
setHighlightRange(sourceRange)
|
||||
}}
|
||||
onPointerOut={(event) => {
|
||||
setHover(false)
|
||||
setHighlightRange([0, 0])
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<primitive object={geo} />
|
||||
<meshStandardMaterial
|
||||
side={DoubleSide}
|
||||
color={hovered ? 'hotpink' : forceHighlight ? 'skyblue' : 'orange'}
|
||||
/>
|
||||
</mesh>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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<BufferGeometry | undefined>() as any
|
||||
const detectionPlaneRef = useRef<BufferGeometry | undefined>() as any
|
||||
const lastPointerRef = useRef<Vector3>(new Vector3())
|
||||
const point2DRef = useRef<Vector2>(new Vector2())
|
||||
const point2DRef = useRef<Vector3>(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 (
|
||||
<>
|
||||
<mesh
|
||||
position={position}
|
||||
quaternion={rotation}
|
||||
ref={ref}
|
||||
onPointerOver={(event) => {
|
||||
setHover(true)
|
||||
inEditMode && setHover(true)
|
||||
setHighlightRange(sourceRange)
|
||||
}}
|
||||
onPointerOut={(event) => {
|
||||
setHover(false)
|
||||
setHighlightRange([0, 0])
|
||||
}}
|
||||
onPointerDown={() => setIsMouseDown(true)}
|
||||
onPointerDown={() => inEditMode && setIsMouseDown(true)}
|
||||
>
|
||||
<primitive object={geo} scale={hovered ? 2 : 1} />
|
||||
<meshStandardMaterial
|
||||
@ -202,7 +127,6 @@ function MovingSphere({
|
||||
</mesh>
|
||||
{isMouseDown && (
|
||||
<mesh
|
||||
position={position}
|
||||
quaternion={clickDetectPlaneQuaternion}
|
||||
onPointerMove={(a) => {
|
||||
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 (
|
||||
<SketchLine
|
||||
geo={geo}
|
||||
sourceRange={sourceRange}
|
||||
forceHighlight={forceHighlight || editorCursor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (artifact.type === 'sketchBase') {
|
||||
console.log('BASE TODO')
|
||||
return null
|
||||
}
|
||||
if (artifact.type === 'extrudeWall') {
|
||||
return (
|
||||
<ExtrudeWall
|
||||
geo={artifact.geo}
|
||||
sourceRange={artifact.sourceRange}
|
||||
forceHighlight={forceHighlight || editorCursor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{artifact.children.map((artifact, index) => (
|
||||
<RenderViewerArtifacts
|
||||
artifact={artifact}
|
||||
key={index}
|
||||
forceHighlight={forceHighlight || editorCursor}
|
||||
/>
|
||||
{artifacts.map((artifact, i) => (
|
||||
<RenderViewerArtifact key={i} artifact={artifact} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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) => (
|
||||
<PathRender
|
||||
geoInfo={geoInfo}
|
||||
key={key}
|
||||
forceHighlight={editorCursor}
|
||||
rotation={artifact.rotation}
|
||||
position={artifact.position}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
if (artifact.type === 'extrudeGroup') {
|
||||
return (
|
||||
<>
|
||||
{artifact.value.map((geoInfo, key) => (
|
||||
<WallRender
|
||||
geoInfo={geoInfo}
|
||||
key={key}
|
||||
forceHighlight={editorCursor}
|
||||
rotation={artifact.rotation}
|
||||
position={artifact.position}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
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<BufferGeometry | undefined>() 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 (
|
||||
<>
|
||||
<mesh
|
||||
quaternion={rotation}
|
||||
position={position}
|
||||
ref={ref}
|
||||
onPointerOver={(event) => {
|
||||
setHover(true)
|
||||
setHighlightRange(geoInfo.__geoMeta.sourceRange)
|
||||
}}
|
||||
onPointerOut={(event) => {
|
||||
setHover(false)
|
||||
setHighlightRange([0, 0])
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<primitive object={geoInfo.__geoMeta.geo} />
|
||||
<meshStandardMaterial
|
||||
side={DoubleSide}
|
||||
color={
|
||||
hovered
|
||||
? 'hotpink'
|
||||
: forceHighlight || editorCursor
|
||||
? 'skyblue'
|
||||
: 'orange'
|
||||
}
|
||||
/>
|
||||
</mesh>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<LineRender
|
||||
key={i}
|
||||
geo={meta.geo}
|
||||
sourceRange={geoInfo.__geoMeta.sourceRange}
|
||||
forceHighlight={forceHighlight || editorCursor}
|
||||
rotation={rotation}
|
||||
position={position}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (meta.type === 'lineEnd') {
|
||||
return (
|
||||
<MovingSphere
|
||||
key={i}
|
||||
geo={meta.geo}
|
||||
sourceRange={geoInfo.__geoMeta.sourceRange}
|
||||
editorCursor={forceHighlight || editorCursor}
|
||||
rotation={rotation}
|
||||
position={position}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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<BufferGeometry | undefined>() as any
|
||||
const [hovered, setHover] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<mesh
|
||||
quaternion={rotation}
|
||||
position={position}
|
||||
ref={ref}
|
||||
onPointerOver={(event) => {
|
||||
setHover(true)
|
||||
setHighlightRange(sourceRange)
|
||||
}}
|
||||
onPointerOut={(event) => {
|
||||
setHover(false)
|
||||
setHighlightRange([0, 0])
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<primitive object={geo} />
|
||||
<meshStandardMaterial
|
||||
color={hovered ? 'hotpink' : forceHighlight ? 'skyblue' : 'orange'}
|
||||
/>
|
||||
</mesh>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -453,7 +453,7 @@ export interface Literal extends GeneralStatement {
|
||||
raw: string
|
||||
}
|
||||
|
||||
interface Identifier extends GeneralStatement {
|
||||
export interface Identifier extends GeneralStatement {
|
||||
type: 'Identifier'
|
||||
name: string
|
||||
}
|
||||
|
@ -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: 'sketch',
|
||||
sourceRange: [20, 68],
|
||||
children: [
|
||||
{
|
||||
type: 'sketchBase',
|
||||
sourceRange: [0, 0],
|
||||
},
|
||||
{
|
||||
type: 'sketchLine',
|
||||
type: 'toPoint',
|
||||
to: [-1.59, -1.54],
|
||||
from: [0, 0],
|
||||
__geoMeta: {
|
||||
sourceRange: [24, 44],
|
||||
pathToNode: [],
|
||||
geos: ['line', 'lineEnd'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'sketchLine',
|
||||
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: [
|
||||
{
|
||||
sourceRange: [20, 68],
|
||||
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),
|
||||
value: art.value.map((v) => ({
|
||||
...v,
|
||||
__geoMeta: {
|
||||
...v.__geoMeta,
|
||||
geos: v.__geoMeta.geos.map((g) => g.type),
|
||||
},
|
||||
})),
|
||||
}
|
||||
}
|
||||
return art
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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],
|
||||
from: [0, 0],
|
||||
__geoMeta: {
|
||||
sourceRange: [17, 28],
|
||||
pathToNode: [],
|
||||
geos: ['line', 'lineEnd'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'toPoint',
|
||||
to: [0, 1],
|
||||
from: [1, 1],
|
||||
__geoMeta: {
|
||||
sourceRange: [36, 57],
|
||||
pathToNode: [],
|
||||
geos: ['line', 'lineEnd'],
|
||||
},
|
||||
name: 'myPath',
|
||||
},
|
||||
{
|
||||
type: 'toPoint',
|
||||
to: [1, 1],
|
||||
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: [],
|
||||
},
|
||||
],
|
||||
})
|
||||
// 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' },
|
||||
expect(root.myVar).toEqual({
|
||||
type: 'userVal',
|
||||
value: '123',
|
||||
__meta: [
|
||||
{
|
||||
pathToNode: ['body', 1, 'declarations', 0, 'init'],
|
||||
sourceRange: [41, 50],
|
||||
},
|
||||
myVar: '123',
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
}
|
||||
if (sketch.type === 'sketchGeo') {
|
||||
function removeGeoFromSketch(sketch: SketchGroup): any {
|
||||
return {
|
||||
...sketch,
|
||||
sketch: sketch.sketch.map(({ geo, previousPath, ...rest }: any) => rest),
|
||||
value: removeGeoFromPaths(sketch.value),
|
||||
}
|
||||
}
|
||||
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,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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,33 +116,82 @@ 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
|
||||
)
|
||||
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] = declaration.init.value
|
||||
_programMemory.root[variableName] = {
|
||||
type: 'userVal',
|
||||
value: declaration.init.value,
|
||||
__meta,
|
||||
}
|
||||
} else if (declaration.init.type === 'BinaryExpression') {
|
||||
_programMemory.root[variableName] = getBinaryExpressionResult(
|
||||
declaration.init,
|
||||
_programMemory
|
||||
)
|
||||
_programMemory.root[variableName] = {
|
||||
type: 'userVal',
|
||||
value: getBinaryExpressionResult(declaration.init, _programMemory),
|
||||
__meta,
|
||||
}
|
||||
} else if (declaration.init.type === 'ArrayExpression') {
|
||||
_programMemory.root[variableName] = declaration.init.elements.map(
|
||||
(element) => {
|
||||
const valueInfo: { value: any; __meta?: Metadata }[] =
|
||||
declaration.init.elements.map(
|
||||
(element): { value: any; __meta?: Metadata } => {
|
||||
if (element.type === 'Literal') {
|
||||
return element.value
|
||||
return {
|
||||
value: element.value,
|
||||
}
|
||||
} else if (element.type === 'BinaryExpression') {
|
||||
return getBinaryExpressionResult(element, _programMemory)
|
||||
return {
|
||||
value: getBinaryExpressionResult(element, _programMemory),
|
||||
}
|
||||
} else if (element.type === 'PipeExpression') {
|
||||
return getPipeExpressionResult(element, _programMemory)
|
||||
return {
|
||||
value: getPipeExpressionResult(
|
||||
element,
|
||||
_programMemory,
|
||||
pathToNode
|
||||
),
|
||||
}
|
||||
} else if (element.type === 'Identifier') {
|
||||
return _programMemory.root[element.name]
|
||||
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`
|
||||
@ -63,9 +199,20 @@ export const executor = (
|
||||
}
|
||||
}
|
||||
)
|
||||
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,26 +224,20 @@ 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[]) => {
|
||||
_programMemory.root[declaration.id.name] = {
|
||||
type: 'userVal',
|
||||
value: (...args: any[]) => {
|
||||
const fnMemory: ProgramMemory = {
|
||||
root: {
|
||||
..._programMemory.root,
|
||||
@ -113,28 +254,36 @@ export const executor = (
|
||||
)
|
||||
}
|
||||
fnInit.params.forEach((param, index) => {
|
||||
fnMemory.root[param.name] = args[index]
|
||||
fnMemory.root[param.name] = {
|
||||
type: 'userVal',
|
||||
value: args[index],
|
||||
__meta,
|
||||
}
|
||||
})
|
||||
return executor(fnInit.body, fnMemory, { bodyType: 'block' }).return
|
||||
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
|
||||
}
|
||||
// _programMemory.root[variableName] = _sketch
|
||||
const newSketch: SketchGeo = {
|
||||
type: 'sketchGeo',
|
||||
sketch: _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],
|
||||
},
|
||||
],
|
||||
}
|
||||
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')
|
||||
}
|
||||
|
@ -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] }),
|
||||
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] }),
|
||||
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,
|
||||
extrudeSurfaces.push({
|
||||
type: 'extrudePlane',
|
||||
position, // todo should come from extrudeGeo
|
||||
rotation, // todo should come from extrudeGeo
|
||||
__geoMeta: {
|
||||
geo,
|
||||
sourceRanges: [line.sourceRange, sourceRange],
|
||||
sourceRange: line.__geoMeta.sourceRange,
|
||||
pathToNode: line.__geoMeta.pathToNode,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
return {
|
||||
type: 'extrudeGeo',
|
||||
type: 'extrudeGroup',
|
||||
value: extrudeSurfaces,
|
||||
height: length,
|
||||
position,
|
||||
rotation,
|
||||
__meta: [
|
||||
{
|
||||
sourceRange,
|
||||
surfaces: extrudeFaces,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
translate,
|
||||
}
|
||||
|
||||
function rotateOnAxis(axisMultiplier: [number, number, number]) {
|
||||
function rotateOnAxis<T extends SketchGroup | ExtrudeGroup>(
|
||||
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,
|
||||
...sketch,
|
||||
rotation,
|
||||
position,
|
||||
__meta: [
|
||||
...sketch.__meta,
|
||||
{
|
||||
sourceRange,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function translate(
|
||||
function translate<T extends SketchGroup | ExtrudeGroup>(
|
||||
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,
|
||||
...sketch,
|
||||
position: [newPosition.x, newPosition.y, newPosition.z],
|
||||
__meta: [
|
||||
...sketch.__meta,
|
||||
{
|
||||
rotation: sketchVal.rotation,
|
||||
transform: sketchVal.transform,
|
||||
sourceRange,
|
||||
pathToNode: [], // TODO
|
||||
},
|
||||
]
|
||||
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],
|
||||
],
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user