Skip to content

Commit da7c70e

Browse files
committed
feat: add currentIntersection, intersection, intersections to event
1 parent 033f8a4 commit da7c70e

9 files changed

Lines changed: 280 additions & 210 deletions

File tree

playground/controls/type-utils.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ export type Args<T> = T extends new (...args: any) => any ? ConstructorParameter
22

33
export type Mandatory<T, K extends keyof T> = T & { [P in K]-?: T[P] }
44

5-
type OmitFunctionProperties<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]
6-
/** Overwrites the properties in `T` with the properties from `O`. */
7-
export type Overwrite<T, O> = Omit<T, OmitFunctionProperties<O>> & O
8-
95
export type KeyOfOptionals<T> = keyof {
106
[K in keyof T as T extends Record<K, T[K]> ? never : K]: T[K]
117
}
128

139
/** Allows using a TS v4 labeled tuple even with older typescript versions */
1410
export type NamedArrayTuple<T extends (...args: any) => any> = Parameters<T>
1511

16-
export type Ref<TRef> = TRef | ((value: TRef) => void)
12+
export type Intersect<T extends any[]> = T extends [infer U, ...infer Rest]
13+
? Rest["length"] extends 0
14+
? U
15+
: U & Intersect<Rest>
16+
: T

playground/examples/SolarExample.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ function CelestialBody(
9898
ref={ref}
9999
position={props.position || [0, 0, 0]}
100100
rotation={props.rotation || [0, 0, 0]}
101+
onPointerDown={console.log}
101102
onPointerEnter={() => setHovered(true)}
102103
onPointerLeave={() => setHovered(false)}
103104
>

src/canvas.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,39 @@ import {
1010
} from "three"
1111
import { createThree } from "./create-three.tsx"
1212
import type { EventRaycaster } from "./raycasters.tsx"
13-
import type { Context, EventHandlers, Props } from "./types.ts"
13+
import type { CanvasEventHandlers, Context, Props } from "./types.ts"
1414

1515
/**
1616
* Props for the Canvas component, which initializes the Three.js rendering context and acts as the root for your 3D scene.
1717
*/
18-
export interface CanvasProps extends ParentProps<Partial<EventHandlers>> {
18+
export interface CanvasProps extends ParentProps<Partial<CanvasEventHandlers>> {
19+
ref?: Ref<Context>
1920
class?: string
2021
/** Configuration for the camera used in the scene. */
2122
defaultCamera?: Partial<Props<PerspectiveCamera> | Props<OrthographicCamera>> | Camera
22-
/** Configuration for the Scene instance. */
23-
scene?: Partial<Props<Scene>> | Scene
23+
/** Configuration for the Raycaster used for mouse and pointer events. */
24+
defaultRaycaster?: Partial<Props<EventRaycaster>> | EventRaycaster | Raycaster
2425
/** Element to render while the main content is loading asynchronously. */
2526
fallback?: JSX.Element
27+
/** Toggles flat interpolation for texture filtering. */
28+
flat?: boolean
29+
/** Controls the rendering loop's operation mode. */
30+
frameloop?: "never" | "demand" | "always"
2631
/** Options for the WebGLRenderer or a function returning a customized renderer. */
2732
gl?:
2833
| Partial<Props<WebGLRenderer>>
2934
| ((canvas: HTMLCanvasElement) => WebGLRenderer)
3035
| WebGLRenderer
36+
/** Toggles linear interpolation for texture filtering. */
37+
linear?: boolean
3138
/** Toggles between Orthographic and Perspective camera. */
3239
orthographic?: boolean
33-
/** Configuration for the Raycaster used for mouse and pointer events. */
34-
raycaster?: Partial<Props<EventRaycaster>> | EventRaycaster | Raycaster
35-
ref?: Ref<Context>
36-
/** Custom CSS styles for the canvas container. */
37-
style?: JSX.CSSProperties
40+
/** Configuration for the Scene instance. */
41+
scene?: Partial<Props<Scene>> | Scene
3842
/** Enables and configures shadows in the scene. */
3943
shadows?: boolean | "basic" | "percentage" | "soft" | "variance" | WebGLRenderer["shadowMap"]
40-
/** Toggles linear interpolation for texture filtering. */
41-
linear?: boolean
42-
/** Toggles flat interpolation for texture filtering. */
43-
flat?: boolean
44-
/** Controls the rendering loop's operation mode. */
45-
frameloop?: "never" | "demand" | "always"
44+
/** Custom CSS styles for the canvas container. */
45+
style?: JSX.CSSProperties
4646
}
4747

4848
/**

src/components.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,13 @@ export const Portal = (props: PortalProps) => {
7171
/**********************************************************************************/
7272

7373
type EntityProps<T extends object | Constructor<object>> = Overwrite<
74-
Props<T>,
75-
{
76-
from: T | undefined
77-
children?: JSXElement
78-
}
74+
[
75+
Props<T>,
76+
{
77+
from: T | undefined
78+
children?: JSXElement
79+
},
80+
]
7981
>
8082
/**
8183
* Wraps a `ThreeElement` and allows it to be used as a JSX-component within a `solid-three` scene.
@@ -97,10 +99,10 @@ export function Entity<T extends object | Constructor<object>>(props: EntityProp
9799
props,
98100
},
99101
) as Meta<T>
102+
useProps(instance, rest)
100103
return instance
101104
},
102105
)
103-
useProps(memo, rest)
104106
return memo as unknown as JSX.Element
105107
}
106108

src/create-events.ts

Lines changed: 85 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Object3D, type Intersection } from "three"
2+
import type { Intersect } from "../playground/controls/type-utils.ts"
23
import { $S3C } from "./constants.ts"
3-
import type { Context, EventName, Meta, ThreeEvent } from "./types.ts"
4+
import type { BaseEvent, Context, EventName, Meta, StoppableEvent } from "./types.ts"
45
import { isInstance } from "./utils.ts"
56

67
const eventNameMap = {
@@ -52,36 +53,46 @@ export const isEventType = (type: string): type is EventName =>
5253

5354
/**********************************************************************************/
5455
/* */
55-
/* Create Three Event */
56+
/* Events */
5657
/* */
5758
/**********************************************************************************/
59+
//
60+
/** Creates a `ThreeEvent` (intersection excluded) from the current `MouseEvent` | `WheelEvent`. */
61+
function createThreeEvent<
62+
TEvent extends Event,
63+
TConfig extends { stoppable?: boolean; intersections?: Intersection[] },
64+
>(nativeEvent: TEvent, { stoppable = true, intersections }: TConfig = {}) {
65+
const event: Record<string, any> = stoppable
66+
? {
67+
nativeEvent,
68+
stopped: false,
69+
stopPropagation() {
70+
event.stopped = true
71+
},
72+
}
73+
: { nativeEvent }
5874

59-
// Creates a `ThreeEvent` from the current `MouseEvent` | `WheelEvent`.
60-
function createThreeEvent<TEvent extends Event>(
61-
nativeEvent: TEvent,
62-
stoppable?: true,
63-
): ThreeEvent<TEvent>
64-
function createThreeEvent<TEvent extends Event>(
65-
nativeEvent: TEvent,
66-
stoppable: false,
67-
): ThreeEvent<TEvent, false>
68-
function createThreeEvent<TEvent extends Event>(
69-
nativeEvent: TEvent,
70-
stoppable = true,
71-
): ThreeEvent<TEvent, boolean> {
72-
if (!stoppable) {
73-
return {
74-
nativeEvent,
75-
}
75+
if (intersections) {
76+
event.intersections = intersections
77+
event.intersection = intersections[0]
7678
}
77-
const event = {
78-
nativeEvent,
79-
stopped: false,
80-
stopPropagation() {
81-
event.stopped = true
82-
},
83-
}
84-
return event
79+
80+
return event as Intersect<
81+
[
82+
TConfig["stoppable"] extends false
83+
? TConfig["stoppable"] extends undefined
84+
? StoppableEvent<TEvent>
85+
: BaseEvent<TEvent>
86+
: StoppableEvent<TEvent>,
87+
TConfig["intersections"] extends Intersection[]
88+
? {
89+
intersections: Intersection[]
90+
intersection: Intersection
91+
currentIntersection?: Intersection
92+
}
93+
: unknown,
94+
]
95+
>
8596
}
8697

8798
/**********************************************************************************/
@@ -139,7 +150,6 @@ function createMissableEventRegistry(
139150

140151
context.canvas.addEventListener(eventNameMap[type], nativeEvent => {
141152
if (registry.array.length === 0) return
142-
const event = createThreeEvent(nativeEvent)
143153
const missedType = `${type}Missed` as const
144154

145155
// Track which objects have been visited during event processing
@@ -149,22 +159,30 @@ function createMissableEventRegistry(
149159
// Phase #1 - Process normal click events
150160
const intersections = raycast(context, registry.array, nativeEvent)
151161

152-
for (const { object } of intersections) {
153-
let node: Object3D | null = object
154-
while (node && !event.stopped && !visitedObjects.has(node)) {
162+
const stoppableEvent = createThreeEvent(nativeEvent, { intersections })
163+
164+
for (const intersection of intersections) {
165+
let node: Object3D | null = intersection.object
166+
167+
stoppableEvent.currentIntersection = intersection
168+
169+
while (node && !stoppableEvent.stopped && !visitedObjects.has(node)) {
155170
missedObjects.delete(node)
156171
visitedObjects.add(node)
157172
if (isInstance(node)) {
158-
node[$S3C].props?.[type]?.(event)
173+
node[$S3C].props[type]?.(stoppableEvent)
159174
}
160175
node = node.parent
161176
}
162177
}
163178

164179
// Call the respective canvas event-handler
165180
// if event propagated all the way down
166-
if (!event.stopped) {
167-
context.props[type]?.(event)
181+
if (!stoppableEvent.stopped) {
182+
if ("currentIntersection" in stoppableEvent) {
183+
delete stoppableEvent.currentIntersection
184+
}
185+
context.props[type]?.(stoppableEvent)
168186
}
169187

170188
// Phase #2 - Raycast remaining missed objects
@@ -186,11 +204,11 @@ function createMissableEventRegistry(
186204
}
187205

188206
// Phase #3 - Fire missed event-handler on missed objects
189-
const missedEvent = createThreeEvent(nativeEvent, false)
207+
const missedEvent = createThreeEvent(nativeEvent, { stoppable: false })
190208

191209
for (const object of missedObjects) {
192210
if (isInstance(object)) {
193-
object[$S3C].props?.[missedType]?.(missedEvent)
211+
object[$S3C].props[missedType]?.(missedEvent)
194212
}
195213
}
196214

@@ -229,17 +247,20 @@ function createHoverEventRegistry(type: "Mouse" | "Pointer", context: Context) {
229247
intersections = raycast(context, registry.array, nativeEvent)
230248

231249
// Phase #1 - Enter
232-
const enterEvent = createThreeEvent(nativeEvent, false)
250+
const enterEvent = createThreeEvent(nativeEvent, { stoppable: false, intersections })
233251
const enterSet = new Set<Object3D>()
234252

235-
for (const { object } of intersections) {
253+
for (const intersection of intersections) {
254+
// Mutate event
255+
enterEvent.intersection = intersection
256+
236257
// Bubble up
237-
let current: Object3D | null = object
258+
let current: Object3D | null = intersection.object
238259
while (current && !enterSet.has(current)) {
239260
enterSet.add(current)
240261

241262
if (isInstance(current) && !hoveredSet.has(current)) {
242-
current[$S3C].props?.[`on${type}Enter`]?.(enterEvent)
263+
current[$S3C].props[`on${type}Enter`]?.(enterEvent)
243264
}
244265
// We bubble a layer down.
245266
current = current.parent
@@ -252,18 +273,22 @@ function createHoverEventRegistry(type: "Mouse" | "Pointer", context: Context) {
252273
}
253274

254275
// Phase #2 - Move
255-
const moveEvent = createThreeEvent(nativeEvent)
276+
const moveEvent = createThreeEvent(nativeEvent, { intersections })
256277
const moveSet = new Set()
257278

258-
for (const { object } of intersections) {
259-
if (moveEvent.stopped) break
279+
for (const intersection of intersections) {
280+
// Mutate currentIntersection
281+
moveEvent.currentIntersection = intersection
282+
260283
// Bubble up
261-
let current: Object3D | null = object
284+
let current: Object3D | null = intersection.object
285+
262286
while (current && !moveSet.has(current)) {
263287
moveSet.add(current)
264288

265289
if (isInstance(current)) {
266-
current[$S3C].props?.[`on${type}Move`]?.(moveEvent)
290+
// @ts-expect-error TODO: fix type-error
291+
current[$S3C].props[`on${type}Move`]?.(moveEvent)
267292
// Break if event was
268293
if (moveEvent.stopped) {
269294
break
@@ -275,6 +300,8 @@ function createHoverEventRegistry(type: "Mouse" | "Pointer", context: Context) {
275300
}
276301

277302
if (!moveEvent.stopped) {
303+
// Remove currentIntersection from moveEvent
304+
delete moveEvent.currentIntersection
278305
context.props[`on${type}Move`]?.(moveEvent)
279306
}
280307

@@ -291,13 +318,13 @@ function createHoverEventRegistry(type: "Mouse" | "Pointer", context: Context) {
291318
})
292319

293320
context.canvas.addEventListener(eventNameMap[`on${type}Leave`], nativeEvent => {
294-
const leaveEvent = createThreeEvent(nativeEvent, false)
321+
const leaveEvent = createThreeEvent(nativeEvent, { stoppable: false })
295322
context.props[`on${type}Leave`]?.(leaveEvent)
296323
hoveredCanvas = false
297324

298325
for (const object of hoveredSet) {
299326
if (isInstance(object)) {
300-
object[$S3C].props?.[`on${type}Leave`]?.(leaveEvent)
327+
object[$S3C].props[`on${type}Leave`]?.(leaveEvent)
301328
}
302329
}
303330
hoveredSet.clear()
@@ -330,23 +357,29 @@ function createDefaultEventRegistry(
330357
context.canvas.addEventListener(
331358
eventNameMap[type],
332359
nativeEvent => {
333-
const event = createThreeEvent(nativeEvent)
334360
const intersections = raycast(context, registry.array, nativeEvent)
361+
const event = createThreeEvent(nativeEvent, { intersections })
335362

336-
for (const { object } of intersections) {
363+
for (const intersection of intersections) {
337364
// Bubble up
338-
let node: Object3D | null = object
365+
let node: Object3D | null = intersection.object
366+
367+
event.intersection = intersection
339368

340369
while (node && !event.stopped) {
341-
if (isInstance(object)) {
370+
if (isInstance(intersection.object)) {
342371
// @ts-expect-error TODO: fix type-error
343-
object[$S3C].props?.[type]?.(event)
372+
intersection.object[$S3C].props[type]?.(event)
344373
}
345374
node = node.parent
346375
}
347376
}
348377

349378
if (!event.stopped) {
379+
// Remove trailing intersection from event
380+
if ("intersection" in event) {
381+
delete event.intersection
382+
}
350383
// @ts-expect-error TODO: fix type-error
351384
context.props[type]?.(event)
352385
}

src/create-three.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,10 @@ export function createThree(canvas: HTMLCanvasElement, props: CanvasProps) {
195195

196196
const defaultRaycaster = createMemo(() =>
197197
augment<Raycaster | EventRaycaster>(
198-
props.raycaster instanceof Raycaster ? props.raycaster : new CursorRaycaster(),
198+
props.defaultRaycaster instanceof Raycaster ? props.defaultRaycaster : new CursorRaycaster(),
199199
{
200200
get props() {
201-
return props.raycaster || {}
201+
return props.defaultRaycaster || {}
202202
},
203203
},
204204
),
@@ -314,8 +314,8 @@ export function createThree(canvas: HTMLCanvasElement, props: CanvasProps) {
314314

315315
// Manage raycaster
316316
createRenderEffect(() => {
317-
if (!props.raycaster || props.raycaster instanceof Raycaster) return
318-
useProps(defaultRaycaster, props.raycaster)
317+
if (!props.defaultRaycaster || props.defaultRaycaster instanceof Raycaster) return
318+
useProps(defaultRaycaster, props.defaultRaycaster)
319319
})
320320

321321
// Manage gl

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ export { useFrame, useThree } from "./hooks.ts"
66
export { useProps } from "./props.ts"
77
export * from "./raycasters.tsx"
88
export * as S3 from "./types.ts"
9-
export { autodispose, autolisten, load } from "./utils.ts"
9+
export { augment, autodispose, autolisten, load } from "./utils.ts"

0 commit comments

Comments
 (0)