-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Voxel demo #65
Voxel demo #65
Changes from 2 commits
1b91195
43c28ab
c02a6e0
4f9662a
ee7f634
c6e0fa8
0246f66
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
'use client' | ||
|
||
import { OrbitControls } from '@react-three/drei' | ||
import { Canvas, ThreeEvent, useFrame } from '@react-three/fiber' | ||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react' | ||
import { Vector3, Vector3Tuple } from 'three' | ||
import { useMap, usePresence } from '@y-sweet/react' | ||
import { Circle, Compact, Sketch } from '@uiw/react-color' | ||
|
||
const DIM = 15 | ||
const TRANSITION_RATE = 0.08 | ||
|
||
interface Voxel { | ||
position: Vector3Tuple | ||
color: any | ||
opacity: number | ||
} | ||
|
||
function MovingVoxel(props: { voxel: Voxel; name?: string }) { | ||
const [position, setPosition] = useState<Vector3Tuple>([0, 0, 0]) // this gets set before the first paint | ||
const destPositionRef = useRef<Vector3Tuple | null>(null) | ||
|
||
useFrame(() => { | ||
if (destPositionRef.current) { | ||
const delta = [ | ||
destPositionRef.current[0] - position[0], | ||
destPositionRef.current[1] - position[1], | ||
destPositionRef.current[2] - position[2], | ||
] | ||
|
||
const x = position[0] + delta[0] * TRANSITION_RATE | ||
const y = position[1] + delta[1] * TRANSITION_RATE | ||
const z = position[2] + delta[2] * TRANSITION_RATE | ||
|
||
setPosition([x, y, z]) | ||
|
||
const squaredDist = delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2] | ||
if (squaredDist < 0.00001) { | ||
destPositionRef.current = null | ||
} | ||
} | ||
}) | ||
|
||
useLayoutEffect(() => { | ||
const dest: Vector3Tuple = [ | ||
props.voxel.position[0], | ||
props.voxel.position[1] + 0.5, | ||
props.voxel.position[2], | ||
] | ||
|
||
const squaredDist = | ||
Math.pow(dest[0] - position[0], 2) + | ||
Math.pow(dest[1] - position[1], 2) + | ||
Math.pow(dest[2] - position[2], 2) | ||
if (squaredDist > 0.00001) { | ||
destPositionRef.current = dest | ||
} | ||
}, [props.voxel.position]) | ||
|
||
return ( | ||
<mesh name={props.name} position={position} scale={1}> | ||
<boxGeometry args={[1, 1, 1]} /> | ||
|
||
<meshPhongMaterial | ||
color={props.voxel.color} | ||
opacity={props.voxel.opacity} | ||
transparent={props.voxel.opacity < 1} | ||
/> | ||
</mesh> | ||
) | ||
} | ||
|
||
function Voxel(props: { voxel: Voxel; name?: string }) { | ||
const position: Vector3Tuple = [ | ||
props.voxel.position[0], | ||
props.voxel.position[1] + 0.5, | ||
props.voxel.position[2], | ||
] | ||
return ( | ||
<mesh name={props.name} position={position} scale={1}> | ||
<boxGeometry args={[1, 1, 1]} /> | ||
|
||
<meshPhongMaterial | ||
color={props.voxel.color} | ||
opacity={props.voxel.opacity} | ||
transparent={props.voxel.opacity < 1} | ||
/> | ||
</mesh> | ||
) | ||
} | ||
|
||
function getPosition(event: ThreeEvent<PointerEvent>): Vector3Tuple | null { | ||
if (event.intersections.length === 0) return null | ||
|
||
const { face, point } = event.intersections[0] | ||
const normal: Vector3 = face!.normal.clone() | ||
|
||
const pos: Vector3 = point.clone().add(new Vector3(0.5, 0.0, 0.5)) | ||
|
||
const c = pos.add(normal.multiplyScalar(0.5)).floor() | ||
return c.toArray() | ||
} | ||
|
||
interface VoxelSetProps { | ||
voxels: Record<string, Voxel> | ||
} | ||
|
||
function VoxelSet(props: VoxelSetProps) { | ||
return ( | ||
<> | ||
{Object.entries(props.voxels).map(([index, voxel]) => ( | ||
<Voxel key={index} voxel={voxel} name={index} /> | ||
))} | ||
</> | ||
) | ||
} | ||
|
||
export function VoxelEditor() { | ||
const [ghostPosition, setGhostPosition] = useState<[number, number, number] | null>(null) | ||
const voxels = useMap<Voxel>('voxels') | ||
const [color, setColor] = useState('#D33115') | ||
|
||
const positionHasBeenSet = useRef(false) | ||
const setInitialCameraPosition = (controls: any) => { | ||
if (controls && !positionHasBeenSet.current) { | ||
controls.object.position.set(0, DIM, DIM) | ||
positionHasBeenSet.current = true | ||
} | ||
} | ||
|
||
const [presence, updatePresence] = usePresence<{ | ||
position: [number, number, number] | null | ||
color: string | ||
}>() | ||
|
||
useMemo(() => { | ||
updatePresence({ | ||
position: ghostPosition, | ||
color: color, | ||
}) | ||
}, [color, ghostPosition, updatePresence]) | ||
|
||
const pointerMove = useCallback( | ||
(event: ThreeEvent<PointerEvent>) => { | ||
setGhostPosition(getPosition(event)) | ||
}, | ||
[setGhostPosition], | ||
) | ||
|
||
const handleClick = useCallback( | ||
(event: ThreeEvent<MouseEvent>) => { | ||
if (event.delta > 5) { | ||
// ignore drag events, which are handled by the orbit control | ||
return | ||
} | ||
|
||
if (event.shiftKey) { | ||
if (event.object.name) { | ||
voxels.delete(event.object.name) | ||
} | ||
|
||
event.stopPropagation() | ||
return | ||
} | ||
|
||
const position = getPosition(event as any) | ||
if (position) { | ||
voxels.set(position.join(':'), { color, position, opacity: 1 }) | ||
} | ||
|
||
event.stopPropagation() | ||
}, | ||
[color, voxels], | ||
) | ||
|
||
let voxelArray: Record<string, Voxel> = voxels.toJSON() | ||
|
||
return ( | ||
<> | ||
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, bottom: 0 }}> | ||
<Canvas> | ||
<OrbitControls ref={setInitialCameraPosition} /> | ||
<ambientLight intensity={1.8} /> | ||
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} /> | ||
|
||
{ghostPosition ? ( | ||
<Voxel voxel={{ position: ghostPosition, color: 0x000000, opacity: 0.5 }} /> | ||
) : null} | ||
|
||
{Array.from(presence.entries()).map(([id, user]) => { | ||
if (user.position === null) return null | ||
|
||
return ( | ||
<MovingVoxel | ||
key={id} | ||
voxel={{ position: user.position, color: user.color, opacity: 0.5 }} | ||
/> | ||
) | ||
})} | ||
|
||
<gridHelper args={[DIM, DIM]} position={[0, 0.001, 0]} /> | ||
|
||
<group onPointerMove={pointerMove} onClick={handleClick}> | ||
<mesh scale={1} position={[0, -0.05, 0]}> | ||
<boxGeometry args={[DIM, 0.1, DIM]} /> | ||
<meshStandardMaterial color="#eee" opacity={1} /> | ||
</mesh> | ||
<VoxelSet voxels={voxelArray} /> | ||
</group> | ||
</Canvas> | ||
</div> | ||
<Compact color={color} onChange={(color) => setColor(color.hex)} /> | ||
</> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { ENV_CONFIG } from '@/lib/config' | ||
import { VoxelEditor } from './VoxelEditor' | ||
import { YDocProvider } from '@y-sweet/react' | ||
import { getOrCreateDoc } from '@y-sweet/sdk' | ||
|
||
type HomeProps = { | ||
searchParams: Record<string, string> | ||
} | ||
|
||
export default async function Home({ searchParams }: HomeProps) { | ||
const clientToken = await getOrCreateDoc(searchParams.doc, ENV_CONFIG) | ||
|
||
return ( | ||
<YDocProvider clientToken={clientToken} setQueryParam="doc"> | ||
<VoxelEditor /> | ||
</YDocProvider> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,18 +31,25 @@ export function useAwareness(): Awareness { | |
return yjsCtx.provider.awareness | ||
} | ||
|
||
export function usePresence<T extends Record<string, any>>(): [ | ||
Map<number, T>, | ||
(presence: T) => void, | ||
] { | ||
type UsePresenceOptions = { | ||
excludeSelf?: boolean | ||
} | ||
|
||
export function usePresence<T extends Record<string, any>>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this change should be in another pr There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you think so? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tend to agree actually, opened #66. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So it's easier to revert in isolation, since we squash commits. |
||
options?: UsePresenceOptions, | ||
): [Map<number, T>, (presence: T) => void] { | ||
const awareness = useAwareness() | ||
const [presence, setPresence] = useState<Map<number, T>>(new Map()) | ||
|
||
const excludeSelf = options?.excludeSelf ?? true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor, but if the default is to exclude self, maybe this option could be called There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good call, I like the principle that bools should be oriented such that the default is false. |
||
|
||
useEffect(() => { | ||
if (awareness) { | ||
const callback = () => { | ||
const map = new Map() | ||
awareness.getStates().forEach((state, clientID) => { | ||
if (excludeSelf && clientID === awareness.clientID) return | ||
|
||
if (Object.keys(state).length > 0) { | ||
map.set(clientID, state) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,11 @@ | ||
import { defineConfig } from "tsup" | ||
import { defineConfig } from 'tsup' | ||
|
||
export default defineConfig({ | ||
entry: ["src/main.tsx"], | ||
entry: ['src/main.tsx'], | ||
dts: true, | ||
splitting: true, | ||
clean: true, | ||
target: "es2020", | ||
format: ["esm", "cjs"], | ||
target: 'es2020', | ||
format: ['esm', 'cjs'], | ||
sourcemap: true, | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use
react-color
?I don't know if this is related, but I noticed the color picker is off:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
react-color
hasn't been touched in two years and causes noisy warnings with recent versions of react (github issue)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do you mean by "off"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, I see what you mean, the colors don't line up by column