Skip to content
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

Merged
merged 7 commits into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"dependencies": {
"@headlessui/react": "^1.7.16",
"@heroicons/react": "^2.0.18",
"@react-three/drei": "^9.80.2",
"@react-three/fiber": "^8.13.6",
"@types/react-color": "^3.0.6",
"@uiw/react-color": "^1.3.3",
Copy link
Member

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:
Screenshot 2023-08-11 at 10 07 33 AM

Copy link
Member Author

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)

Copy link
Member Author

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"?

Copy link
Member Author

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

"@y-sweet/react": "0.0.4",
"@y-sweet/sdk": "0.0.4",
"autoprefixer": "10.4.14",
Expand All @@ -23,6 +27,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "3.3.3",
"three": "^0.155.0",
"typescript": "5.1.6",
"y-codemirror": "^3.0.1",
"y-protocols": "^1.0.5",
Expand Down
215 changes: 215 additions & 0 deletions examples/src/app/voxels/VoxelEditor.tsx
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)} />
</>
)
}
18 changes: 18 additions & 0 deletions examples/src/app/voxels/page.tsx
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>
)
}
15 changes: 11 additions & 4 deletions js-pkg/react/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change should be in another pr

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you think so?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to agree actually, opened #66.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you think so?

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
Copy link
Member

Choose a reason for hiding this comment

The 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 includeSelf?

Copy link
Member Author

Choose a reason for hiding this comment

The 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)
}
Expand Down
8 changes: 4 additions & 4 deletions js-pkg/react/tsup.config.ts
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,
})
Loading