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

Raycaster #2266

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
84 changes: 84 additions & 0 deletions .storybook/stories/Raycaster.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import * as THREE from 'three'
import * as React from 'react'

import { Vector3 } from 'three'
import { Meta, StoryObj } from '@storybook/react'

import { Setup } from '../Setup'

import { Raycaster } from '../../src'
import { ComponentProps, useRef } from 'react'
import { useFrame } from '@react-three/fiber'

export default {
title: 'Abstractions/Raycaster',
component: Raycaster,
decorators: [
(Story) => (
<Setup cameraPosition={new Vector3(0, 0, 10)}>
<Story />
</Setup>
),
],
argTypes: {
near: { control: { type: 'range', min: 0, max: 15 } },
far: { control: { type: 'range', min: 0, max: 15 } },
},
} satisfies Meta<typeof Raycaster>

type Story = StoryObj<typeof Raycaster>

function RaycasterScene(props: React.ComponentProps<typeof Raycaster>) {
const raycasterRef = useRef<THREE.Raycaster>(null)

React.useEffect(() => {
console.log('raycasterRef', raycasterRef)
})

return (
<>
<color attach="background" args={['#303030']} />

<Raycaster ref={raycasterRef} {...props} />

<Capsule position-x={-2} />
<Capsule />
<Capsule position-x={2} />
</>
)
}

export const RaycasterSt = {
render: (args) => <RaycasterScene {...args} />,
args: {
origin: [-4, 0, 0],
direction: [1, 0, 0],
near: 1,
far: 8,
helper: [20],
},

name: 'Default',
} satisfies Story

const Capsule = ({
// layers,
...props
}: ComponentProps<'mesh'>) => {
const meshRef = useRef<THREE.Mesh>(null)

useFrame(({ clock }) => {
if (!meshRef.current) return
meshRef.current.position.y = Math.sin(clock.getElapsedTime() * 0.5 + meshRef.current.position.x)
meshRef.current.rotation.z = Math.sin(clock.getElapsedTime() * 0.5) * Math.PI * 1
})

return (
<mesh ref={meshRef} {...props}>
{/* <Layers layers={layers} /> */}

<capsuleGeometry args={[0.5, 0.5, 4, 32]} />
<meshNormalMaterial side={THREE.DoubleSide} />
</mesh>
)
}
14 changes: 14 additions & 0 deletions docs/abstractions/raycaster.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
title: Raycaster
sourcecode: src/core/Raycaster.tsx
---

[![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/abstractions-raycaster)

A `<raycaster>` wrapper with a `helper` prop to visualize it.

```tsx
const raycasterRef = useRef<THREE.Raycaster>(null)

<Raycaster ref={raycasterRef} origin={[-4,0,0]} direction={[1,0,0]} near={1} far={8} helper={[20]} />
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
},
"dependencies": {
"@babel/runtime": "^7.26.0",
"@gsimone/three-raycaster-helper": "^0.1.0",
"@mediapipe/tasks-vision": "0.10.17",
"@monogrid/gainmap-js": "^3.0.6",
"@react-spring/three": "~9.7.5",
Expand Down
75 changes: 49 additions & 26 deletions src/core/Helper.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,84 @@
/* eslint react-hooks/exhaustive-deps: 1 */
import * as React from 'react'
import { Object3D } from 'three'
import { useThree, useFrame } from '@react-three/fiber'
import { Falsey } from 'utility-types'

type HelperType = Object3D & { update: () => void; dispose: () => void }
type HelperConstructor = new (...args: any[]) => any
type HelperArgs<T> = T extends [infer _, ...infer R] ? R : never
type HelperConstructor = new (...args: any[]) => Object3D & { update: () => void; dispose?: () => void }
type HelperArgs<T> = T extends [any, ...infer R] ? R : never

export function useHelper<T extends HelperConstructor>(
object3D: React.MutableRefObject<Object3D> | Falsey,
helperConstructor: T,
...args: HelperArgs<ConstructorParameters<T>>
/**
* Instantiate a `THREE.*Helper` for an existing node and add it to the scene.
*
* Examples:
*
* ```ts
* useHelper(sphereRef, BoxHelper, 'royalblue')
* useHelper(sphereRef, VertexNormalsHelper, 1, 0xff0000)

* useHelper(raycasterRef, RaycasterHelper, 20)
* ```
*/
export function useHelper<H extends HelperConstructor>(
/** A ref to the node the helper is instantiate on (type inferred from H's ctor 1st param) */
nodeRef: React.RefObject<ConstructorParameters<H>[0]> | Falsey,
/** `*Helper` class */
helperConstructor: H,
/** Rest of arguments for H (types inferred from H's ctor params, omitting first) */
...args: HelperArgs<ConstructorParameters<H>>
) {
const helper = React.useRef<HelperType>()
const helperRef = React.useRef<InstanceType<H>>(null!)
const scene = useThree((state) => state.scene)

React.useLayoutEffect(() => {
let currentHelper: HelperType = undefined!
let currentHelper: InstanceType<H> = undefined!

if (object3D && object3D?.current && helperConstructor) {
helper.current = currentHelper = new (helperConstructor as any)(object3D.current, ...args)
if (nodeRef && nodeRef?.current && helperConstructor) {
helperRef.current = currentHelper = new helperConstructor(nodeRef.current, ...args) as InstanceType<H>
}

if (currentHelper) {
// Prevent the helpers from blocking rays
currentHelper.traverse((child) => (child.raycast = () => null))
scene.add(currentHelper)
return () => {
helper.current = undefined
helperRef.current = null!
scene.remove(currentHelper)
currentHelper.dispose?.()
}
}
}, [scene, helperConstructor, object3D, ...args])
}, [scene, helperConstructor, nodeRef, args])

useFrame(() => void helper.current?.update?.())
return helper
useFrame(() => void helperRef.current?.update?.())
return helperRef
}

//

export type HelperProps<T extends HelperConstructor> = {
type: T
args?: HelperArgs<ConstructorParameters<T>>
export type HelperProps<H extends HelperConstructor> = {
/** `*Helper` class */
type: H
/** Rest of arguments for H (types inferred from H's ctor params, omitting first) */
args?: HelperArgs<ConstructorParameters<H>>
}

export const Helper = <T extends HelperConstructor>({
/**
* Instantiate a `THREE.*Helper` for parent node and add it to the scene.
*/

export const Helper = <H extends HelperConstructor>({
type: helperConstructor,
args = [] as never,
}: HelperProps<T>) => {
const thisRef = React.useRef<Object3D>(null!)
}: HelperProps<H>) => {
const parentRef = React.useRef<Object3D>(null!)

React.useLayoutEffect(() => {
parentRef.current = thisRef.current.parent!
})

useHelper(parentRef, helperConstructor, ...args)

return <object3D ref={thisRef} />
return (
<object3D
ref={(obj) => {
parentRef.current = obj?.parent!
}}
/>
)
}
55 changes: 55 additions & 0 deletions src/core/Raycaster.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as THREE from 'three'
import * as React from 'react'
import { ComponentProps, forwardRef, useRef, useState } from 'react'
import { useFrame, type Vector3 } from '@react-three/fiber'
// import { RaycasterHelper } from '@gsimone/three-raycaster-helper'
import { RaycasterHelper } from '../tmp/raycaster-helper'

import { useHelper } from '..'
import { Falsey } from 'utility-types'

type HelperArgs<T> = T extends [any, ...infer R] ? R : never

type RaycasterProps = Omit<ComponentProps<'raycaster'>, 'args'> & {
/** Origin of the raycaster */
origin: Vector3
/** Direction of the raycaster */
direction: Vector3
} & {
/** Whether or not to display the RaycasterHelper - you can pass additional params for the ctor here */
helper?: Falsey | HelperArgs<ConstructorParameters<typeof RaycasterHelper>>
}

function toThreeVec3(v: Vector3) {
return v instanceof THREE.Vector3 ? v : new THREE.Vector3(...(typeof v === 'number' ? [v, v, v] : v))
}

/**
* `<raycaster>` wrapper, with a `helper` prop to visualize it
*/
export const Raycaster = forwardRef<THREE.Raycaster, RaycasterProps>(
({ origin: _origin, direction: _direction, near, far, helper = false, ...props }, fref) => {
const origin = toThreeVec3(_origin)
const direction = toThreeVec3(_direction)

const [r] = useState(() => new THREE.Raycaster(origin, direction))

const raycasterRef = useRef<THREE.Raycaster>(null)
const ref = fref || raycasterRef
const isCallbackRef = typeof ref === 'function'

const args = helper || []
const raycasterHelperRef = useHelper(helper && !isCallbackRef && ref, RaycasterHelper, ...args)

// Update the hits with intersection results
useFrame(({ scene }) => {
if (!helper) return
if (!ref || isCallbackRef) return

if (!raycasterHelperRef.current || !ref.current) return
raycasterHelperRef.current.hits = ref.current.intersectObjects(scene.children)
})

return <primitive ref={ref} object={r} {...{ origin, direction, near, far }} {...props} />
}
)
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export * from './Svg'
export * from './Gltf'
export * from './AsciiRenderer'
export * from './Splat'
export * from './Raycaster'

// Cameras
export * from './OrthographicCamera'
Expand Down
Loading
Loading