drag sphere - edit sketch working for xy plane

This commit is contained in:
Kurt Hutten IrevDev
2022-12-07 10:02:21 +11:00
parent 034c903672
commit cecc4b1f01
6 changed files with 374 additions and 137 deletions

View File

@ -3,14 +3,9 @@ import { Canvas } from '@react-three/fiber'
import { Allotment } from 'allotment' import { Allotment } from 'allotment'
import { OrbitControls, OrthographicCamera } from '@react-three/drei' import { OrbitControls, OrthographicCamera } from '@react-three/drei'
import { lexer } from './lang/tokeniser' import { lexer } from './lang/tokeniser'
import { import { abstractSyntaxTree } from './lang/abstractSyntaxTree'
abstractSyntaxTree,
getNodePathFromSourceRange,
getNodeFromPath
} from './lang/abstractSyntaxTree'
import { executor, processShownObjects, ViewerArtifact } from './lang/executor' import { executor, processShownObjects, ViewerArtifact } from './lang/executor'
import { recast } from './lang/recast' import { recast } from './lang/recast'
import { BufferGeometry } from 'three'
import CodeMirror from '@uiw/react-codemirror' import CodeMirror from '@uiw/react-codemirror'
import { javascript } from '@codemirror/lang-javascript' import { javascript } from '@codemirror/lang-javascript'
import { ViewUpdate } from '@codemirror/view' import { ViewUpdate } from '@codemirror/view'
@ -19,12 +14,12 @@ import {
addLineHighlight, addLineHighlight,
} from './editor/highlightextension' } from './editor/highlightextension'
import { useStore } from './useStore' import { useStore } from './useStore'
import { isOverlapping } from './lib/utils'
import { Toolbar } from './Toolbar' import { Toolbar } from './Toolbar'
import { BasePlanes } from './components/BasePlanes' import { BasePlanes } from './components/BasePlanes'
import { SketchPlane } from './components/SketchPlane' import { SketchPlane } from './components/SketchPlane'
import { Logs } from './components/Logs' import { Logs } from './components/Logs'
import { AxisIndicator } from './components/AxisIndicator' import { AxisIndicator } from './components/AxisIndicator'
import { RenderViewerArtifacts } from './components/SketchLine'
const OrrthographicCamera = OrthographicCamera as any const OrrthographicCamera = OrthographicCamera as any
@ -160,7 +155,7 @@ function App() {
<OrbitControls <OrbitControls
enableDamping={false} enableDamping={false}
enablePan enablePan
enableRotate enableRotate={!(guiMode.mode === 'canEditSketch' || guiMode.mode === 'sketch')}
enableZoom enableZoom
reverseOrbit={false} reverseOrbit={false}
/> />
@ -199,111 +194,3 @@ function App() {
} }
export default App export default App
function Line({
geo,
sourceRange,
forceHighlight = false,
}: {
geo: BufferGeometry
sourceRange: [number, number]
forceHighlight?: boolean
}) {
const { setHighlightRange, selectionRange, guiMode, setGuiMode, ast } =
useStore(
({ setHighlightRange, selectionRange, guiMode, setGuiMode, ast }) => ({
setHighlightRange,
selectionRange,
guiMode,
setGuiMode,
ast,
})
)
// 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)
const [didSetCanEdit, setDidSetCanEdit] = useState(false)
useEffect(() => {
const shouldHighlight = isOverlapping(sourceRange, selectionRange)
setEditorCursor(shouldHighlight)
if (shouldHighlight && guiMode.mode === 'default' && ast) {
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const piper = getNodeFromPath(ast, pathToNode, 'PipeExpression')
const axis = piper.type !== 'PipeExpression' ? 'xy'
: piper?.body?.[1]?.callee?.name === 'rx' ? 'xz' : 'yz'
setGuiMode({ mode: 'canEditSketch', pathToNode, axis })
setDidSetCanEdit(true)
} else if (
!shouldHighlight &&
didSetCanEdit &&
guiMode.mode === 'canEditSketch'
) {
setGuiMode({ mode: 'default' })
setDidSetCanEdit(false)
}
}, [selectionRange, sourceRange])
return (
<mesh
ref={ref}
onPointerOver={(event) => {
setHover(true)
setHighlightRange(sourceRange)
}}
onPointerOut={(event) => {
setHover(false)
setHighlightRange([0, 0])
}}
>
<primitive object={geo} />
<meshStandardMaterial
color={
hovered
? 'hotpink'
: editorCursor || forceHighlight
? 'skyblue'
: 'orange'
}
/>
</mesh>
)
}
function RenderViewerArtifacts({
artifact,
forceHighlight = false,
}: {
artifact: ViewerArtifact
forceHighlight?: boolean
}) {
const { selectionRange } = useStore(({ selectionRange }) => ({
selectionRange,
}))
const [editorCursor, setEditorCursor] = useState(false)
useEffect(() => {
const shouldHighlight = isOverlapping(artifact.sourceRange, selectionRange)
setEditorCursor(shouldHighlight)
}, [selectionRange, artifact.sourceRange])
if (artifact.type === 'sketchLine') {
const { geo, sourceRange } = artifact
return (
<Line
geo={geo}
sourceRange={sourceRange}
forceHighlight={forceHighlight || editorCursor}
/>
)
}
return (
<>
{artifact.children.map((artifact, index) => (
<RenderViewerArtifacts
artifact={artifact}
key={index}
forceHighlight={forceHighlight || editorCursor}
/>
))}
</>
)
}

View File

@ -0,0 +1,274 @@
import { useRef, useState, useEffect, useMemo } from 'react'
import {
getNodePathFromSourceRange,
getNodeFromPath,
CallExpression,
changeArguments,
} from '../lang/abstractSyntaxTree'
import { ViewerArtifact } from '../lang/executor'
import { BufferGeometry } from 'three'
import { useStore } from '../useStore'
import { isOverlapping } from '../lib/utils'
import { LineGeos } from '../lang/engine'
import { Vector3 } from 'three'
function useHeightlight(sourceRange: [number, number]) {
const { selectionRange, guiMode, setGuiMode, ast } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
selectionRange: s.selectionRange,
guiMode: s.guiMode,
setGuiMode: s.setGuiMode,
ast: s.ast,
}))
// This reference will give us direct access to the mesh
const [editorCursor, setEditorCursor] = useState(false)
const [didSetCanEdit, setDidSetCanEdit] = useState(false)
useEffect(() => {
const shouldHighlight = isOverlapping(sourceRange, selectionRange)
setEditorCursor(shouldHighlight)
if (shouldHighlight && guiMode.mode === 'default' && ast) {
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const piper = getNodeFromPath(ast, pathToNode, 'PipeExpression')
const axis =
piper.type !== 'PipeExpression'
? 'xy'
: piper?.body?.[1]?.callee?.name === 'rx'
? 'xz'
: 'yz'
setGuiMode({ mode: 'canEditSketch', pathToNode, axis })
setDidSetCanEdit(true)
} else if (
!shouldHighlight &&
didSetCanEdit &&
guiMode.mode === 'canEditSketch'
) {
setGuiMode({ mode: 'default' })
setDidSetCanEdit(false)
}
}, [selectionRange, sourceRange])
return {
editorCursor,
}
}
function SketchLine({
geo,
sourceRange,
forceHighlight = false,
}: {
geo: LineGeos
sourceRange: [number, number]
forceHighlight?: boolean
}) {
const { editorCursor } = useHeightlight(sourceRange)
const { setHighlightRange } = useStore(
({ setHighlightRange, selectionRange, guiMode, setGuiMode, ast }) => ({
setHighlightRange,
selectionRange,
guiMode,
setGuiMode,
ast,
})
)
// 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])
}}
>
<primitive object={geo.line} />
<meshStandardMaterial
color={
hovered
? 'hotpink'
: editorCursor || forceHighlight
? 'skyblue'
: 'orange'
}
/>
</mesh>
<MovingSphere
geo={geo.tip}
sourceRange={sourceRange}
editorCursor={editorCursor || forceHighlight}
/>
</>
)
}
const roundOff = (num: number, places: number): number => {
const x = Math.pow(10, places)
return Math.round(num * x) / x
}
function MovingSphere({
geo,
sourceRange,
editorCursor,
}: {
geo: BufferGeometry
sourceRange: [number, number]
editorCursor: boolean
}) {
const ref = useRef<BufferGeometry | undefined>() as any
const lastPointerRef = useRef<Vector3>(new Vector3())
const [hovered, setHover] = useState(false)
const [isMouseDown, setIsMouseDown] = useState(false)
const { setHighlightRange, guiMode, ast, updateAst } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
selectionRange: s.selectionRange,
guiMode: s.guiMode,
setGuiMode: s.setGuiMode,
ast: s.ast,
updateAst: s.updateAst,
}))
const { originalXY } = useMemo(() => {
if (ast) {
const thePath = getNodePathFromSourceRange(ast, sourceRange)
const callExpression = getNodeFromPath(ast, thePath) as CallExpression
const [xArg, yArg] = callExpression?.arguments || []
const x = xArg?.type === 'Literal' ? xArg.value : -1
const y = yArg?.type === 'Literal' ? yArg.value : -1
console.log(callExpression)
return {
originalXY: [x, y],
}
}
return {
originalXY: [-1, -1],
}
}, [ast])
useEffect(() => {
const handleMouseUp = () => {
if (isMouseDown && ast) {
const thePath = getNodePathFromSourceRange(ast, sourceRange)
const theNewPoints: [number, number] = [
roundOff(lastPointerRef.current.x, 2),
roundOff(lastPointerRef.current.y, 2),
]
console.log('theNewPoints', theNewPoints)
const { modifiedAst } = changeArguments(ast, thePath, theNewPoints)
updateAst(modifiedAst)
ref.current.position.set(0, 0, 0)
}
setIsMouseDown(false)
}
window.addEventListener('mouseup', handleMouseUp)
return () => {
window.removeEventListener('mouseup', handleMouseUp)
}
}, [isMouseDown, ast])
return (
<>
<mesh
ref={ref}
onPointerOver={(event) => {
setHover(true)
setHighlightRange(sourceRange)
}}
onPointerOut={(event) => {
setHover(false)
setHighlightRange([0, 0])
}}
onPointerDown={() => setIsMouseDown(true)}
>
<primitive object={geo} scale={hovered ? 2 : 1} />
<meshStandardMaterial
color={hovered ? 'hotpink' : editorCursor ? 'skyblue' : 'orange'}
/>
</mesh>
{isMouseDown && (
<mesh
position={[0, 0, 0.05]}
onPointerMove={(a) => {
const point = a.point
if (
lastPointerRef.current.x === 0 &&
lastPointerRef.current.y === 0 &&
lastPointerRef.current.z === 0
) {
lastPointerRef.current.set(point.x, point.y, point.z)
return
}
if (guiMode.mode)
if (ref.current) {
const diff = new Vector3().subVectors(
point,
lastPointerRef.current
)
console.log(originalXY)
if (originalXY[0] === -1) {
diff.x = 0
}
if (originalXY[1] === -1) {
diff.y = 0
}
ref.current.position.add(diff)
lastPointerRef.current.set(point.x, point.y, point.z)
}
}}
name="my-mesh"
>
<planeGeometry args={[50, 50]} />
<meshStandardMaterial color="blue" transparent opacity={0.2} />
</mesh>
)}
</>
)
}
export function RenderViewerArtifacts({
artifact,
forceHighlight = false,
}: {
artifact: ViewerArtifact
forceHighlight?: boolean
}) {
const { selectionRange } = useStore(({ selectionRange }) => ({
selectionRange,
}))
const [editorCursor, setEditorCursor] = useState(false)
useEffect(() => {
const shouldHighlight = isOverlapping(artifact.sourceRange, selectionRange)
setEditorCursor(shouldHighlight)
}, [selectionRange, artifact.sourceRange])
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
}
return (
<>
{artifact.children.map((artifact, index) => (
<RenderViewerArtifacts
artifact={artifact}
key={index}
forceHighlight={forceHighlight || editorCursor}
/>
))}
</>
)
}

View File

@ -1215,6 +1215,38 @@ export function addLine(
} }
} }
export function changeArguments(
node: Program,
pathToNode: (string | number)[],
args: [number, number]
): { modifiedAst: Program; pathToNode: (string | number)[] }{
const _node = { ...node }
const dumbyStartend = { start: 0, end: 0 }
// const thePath = getNodePathFromSourceRange(_node, sourceRange)
const callExpression = getNodeFromPath(_node, pathToNode) as CallExpression
const newXArg: CallExpression['arguments'][number] = callExpression.arguments[0].type === 'Literal' ? {
type: 'Literal',
...dumbyStartend,
value: args[0],
raw: `${args[0]}`,
} : {
...callExpression.arguments[0]
}
const newYArg: CallExpression['arguments'][number] = callExpression.arguments[1].type === 'Literal' ? {
type: 'Literal',
...dumbyStartend,
value: args[1],
raw: `${args[1]}`,
} : {
...callExpression.arguments[1]
}
callExpression.arguments = [newXArg, newYArg]
return {
modifiedAst: _node,
pathToNode,
}
}
function isCallExpression(tokens: Token[], index: number): number { function isCallExpression(tokens: Token[], index: number): number {
const currentToken = tokens[index] const currentToken = tokens[index]
const veryNextToken = tokens[index + 1] // i.e. no whitespace const veryNextToken = tokens[index + 1] // i.e. no whitespace

View File

@ -7,15 +7,21 @@ export function baseGeo({ from }: { from: [number, number, number] }) {
return baseSphere return baseSphere
} }
export interface LineGeos {
line: BufferGeometry
tip: BufferGeometry
centre: BufferGeometry
}
export function lineGeo({ export function lineGeo({
from, from,
to, to,
}: { }: {
from: [number, number, number] from: [number, number, number]
to: [number, number, number] to: [number, number, number]
}): BufferGeometry { }): LineGeos {
const sq = (a: number): number => a * a const sq = (a: number): number => a * a
const center = [ const centre = [
(from[0] + to[0]) / 2, (from[0] + to[0]) / 2,
(from[1] + to[1]) / 2, (from[1] + to[1]) / 2,
(from[2] + to[2]) / 2, (from[2] + to[2]) / 2,
@ -34,15 +40,23 @@ export function lineGeo({
const lineBody = new BoxGeometry(Hypotenuse3d, 0.1, 0.1) const lineBody = new BoxGeometry(Hypotenuse3d, 0.1, 0.1)
lineBody.rotateY(ang1) lineBody.rotateY(ang1)
lineBody.rotateZ(ang2) lineBody.rotateZ(ang2)
lineBody.translate(center[0], center[1], center[2]) lineBody.translate(centre[0], centre[1], centre[2])
// create line end balls with SphereGeometry at `to` and `from` with radius of 0.15 // create line end balls with SphereGeometry at `to` and `from` with radius of 0.15
const lineEnd1 = new SphereGeometry(0.15) const lineEnd1 = new SphereGeometry(0.15)
lineEnd1.translate(to[0], to[1], to[2]) lineEnd1.translate(to[0], to[1], to[2])
const centreSphere = new SphereGeometry(0.15)
centreSphere.translate(centre[0], centre[1], centre[2])
// const lineEnd2 = new SphereGeometry(0.15); // const lineEnd2 = new SphereGeometry(0.15);
// lineEnd2.translate(from[0], from[1], from[2]) // lineEnd2.translate(from[0], from[1], from[2])
// group all three geometries // group all three geometries
return mergeBufferGeometries([lineBody, lineEnd1]) // return mergeBufferGeometries([lineBody, lineEnd1])
// return mergeBufferGeometries([lineBody, lineEnd1, lineEnd2]); // return mergeBufferGeometries([lineBody, lineEnd1, lineEnd2]);
return {
line: lineBody,
tip: lineEnd1,
centre: centreSphere,
}
} }

View File

@ -6,6 +6,7 @@ import {
} from './abstractSyntaxTree' } from './abstractSyntaxTree'
import { Path, Transform, SketchGeo, sketchFns } from './sketch' import { Path, Transform, SketchGeo, sketchFns } from './sketch'
import { BufferGeometry } from 'three' import { BufferGeometry } from 'three'
import { LineGeos } from './engine'
export interface ProgramMemory { export interface ProgramMemory {
root: { [key: string]: any } root: { [key: string]: any }
@ -296,8 +297,13 @@ export type ViewerArtifact =
| { | {
type: 'sketchLine' type: 'sketchLine'
sourceRange: SourceRange sourceRange: SourceRange
geo: BufferGeometry geo: LineGeos
} }
| {
type: 'sketchBase',
sourceRange: SourceRange,
geo: BufferGeometry
}
| { | {
type: 'parent' type: 'parent'
sourceRange: SourceRange sourceRange: SourceRange
@ -315,20 +321,44 @@ export const processShownObjects = (
previousTransforms: PreviousTransforms = [] previousTransforms: PreviousTransforms = []
): ViewerArtifact[] => { ): ViewerArtifact[] => {
if (geoMeta?.type === 'sketchGeo') { if (geoMeta?.type === 'sketchGeo') {
return geoMeta.sketch.map(({ geo, sourceRange }) => { return geoMeta.sketch.map(({ geo, sourceRange, type }) => {
const newGeo = geo.clone() if(type === 'toPoint') {
previousTransforms.forEach(({ rotation, transform }) => { // const newGeo = geo.clone()
newGeo.rotateX(rotation[0]) const newGeo: LineGeos = {
newGeo.rotateY(rotation[1]) line: geo.line.clone(),
newGeo.rotateZ(rotation[2]) tip: geo.tip.clone(),
newGeo.translate(transform[0], transform[1], transform[2]) centre: geo.centre.clone(),
}) }
previousTransforms.forEach(({ rotation, transform }) => {
return { Object.values(newGeo).forEach((geoItem) => {
type: 'sketchLine', geoItem.rotateX(rotation[0])
geo: newGeo, geoItem.rotateY(rotation[1])
sourceRange, geoItem.rotateZ(rotation[2])
geoItem.translate(transform[0], transform[1], transform[2])
})
})
return {
type: 'sketchLine',
geo: newGeo,
sourceRange,
}
} else if(type === 'base') {
const newGeo = geo.clone()
previousTransforms.forEach(({ rotation, transform }) => {
newGeo.rotateX(rotation[0])
newGeo.rotateY(rotation[1])
newGeo.rotateZ(rotation[2])
newGeo.translate(transform[0], transform[1], transform[2])
})
return {
type: 'sketchBase',
geo: newGeo,
sourceRange,
}
} }
console.log('type',type)
throw new Error('Unknown geo type')
}) })
} else if (geoMeta.type === 'transform') { } else if (geoMeta.type === 'transform') {
const referencedVar = geoMeta.sketch const referencedVar = geoMeta.sketch

View File

@ -1,5 +1,5 @@
import { ProgramMemory } from './executor' import { ProgramMemory } from './executor'
import { lineGeo, baseGeo } from './engine' import { lineGeo, baseGeo, LineGeos } from './engine'
import { BufferGeometry } from 'three' import { BufferGeometry } from 'three'
type Coords2d = [number, number] type Coords2d = [number, number]
@ -37,7 +37,7 @@ export type Path =
name?: string name?: string
to: Coords2d to: Coords2d
previousPath: Path previousPath: Path
geo: BufferGeometry geo: LineGeos
sourceRange: SourceRange sourceRange: SourceRange
} }
| { | {
@ -45,7 +45,7 @@ export type Path =
name?: string name?: string
firstPath: Path firstPath: Path
previousPath: Path previousPath: Path
geo: BufferGeometry geo: LineGeos
sourceRange: SourceRange sourceRange: SourceRange
} }
| { | {