Skip to content

Commit bf4f681

Browse files
committed
feat: stack-based store.camera, Resource-component, remove store.pointer
- Extract raycaster implementations into separate module - Add EventRaycaster interface with CursorRaycaster and CenterRaycaster - EventRaycaster extends Raycaster with an update(event)-method - Remove store.pointer and instead let it be handled by the passed Raycaster - Pass context to internal event-utilities instead of using useContext - Fix circular import: move useProps from hooks.ts to props.ts - Improve type definitions and remove unused internal context properties - Rename canvasProps.camera to canvasProps.defaultCamera - Rename store.camera to store.currentCamera - Use stack to represent currentCamera - store.currentCamera is cameraStack.peek() ?? props.defaultCamera - store.setCurrentCamera pushes camera to stack and returns function to remove camera from stack
1 parent 5d79d05 commit bf4f681

16 files changed

Lines changed: 653 additions & 545 deletions

src/canvas.tsx

Lines changed: 19 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
1-
import {
2-
type JSX,
3-
type ParentProps,
4-
type Ref,
5-
createRenderEffect,
6-
onCleanup,
7-
onMount,
8-
} from "solid-js"
1+
import { createResizeObserver } from "@solid-primitives/resize-observer"
2+
import { onMount, type JSX, type ParentProps, type Ref } from "solid-js"
93
import {
104
Camera,
115
OrthographicCamera,
@@ -15,15 +9,18 @@ import {
159
WebGLRenderer,
1610
} from "three"
1711
import { createThree } from "./create-three.tsx"
18-
import type { EventHandlers, Props } from "./types.ts"
12+
import type { EventRaycaster } from "./raycasters.tsx"
13+
import type { Context, EventHandlers, Props } from "./types.ts"
1914

2015
/**
2116
* Props for the Canvas component, which initializes the Three.js rendering context and acts as the root for your 3D scene.
2217
*/
2318
export interface CanvasProps extends ParentProps<Partial<EventHandlers>> {
2419
class?: string
2520
/** Configuration for the camera used in the scene. */
26-
camera?: Partial<Props<PerspectiveCamera> | Props<OrthographicCamera>> | Camera
21+
defaultCamera?: Partial<Props<PerspectiveCamera> | Props<OrthographicCamera>> | Camera
22+
/** Configuration for the Scene instance. */
23+
scene?: Partial<Props<Scene>> | Scene
2724
/** Element to render while the main content is loading asynchronously. */
2825
fallback?: JSX.Element
2926
/** Options for the WebGLRenderer or a function returning a customized renderer. */
@@ -34,10 +31,8 @@ export interface CanvasProps extends ParentProps<Partial<EventHandlers>> {
3431
/** Toggles between Orthographic and Perspective camera. */
3532
orthographic?: boolean
3633
/** Configuration for the Raycaster used for mouse and pointer events. */
37-
raycaster?: Partial<Props<Raycaster>> | Raycaster
38-
ref?: Ref<HTMLDivElement>
39-
/** Configuration for the Scene instance. */
40-
scene?: Partial<Props<Scene>> | Scene
34+
raycaster?: Partial<Props<EventRaycaster>> | EventRaycaster | Raycaster
35+
ref?: Ref<Context>
4136
/** Custom CSS styles for the canvas container. */
4237
style?: JSX.CSSProperties
4338
/** Enables and configures shadows in the scene. */
@@ -68,32 +63,22 @@ export function Canvas(props: ParentProps<CanvasProps>) {
6863
const context = createThree(canvas, props)
6964

7065
// Resize observer for the canvas to adjust camera and renderer on size change
71-
function onResize() {
66+
createResizeObserver(container, function onResize() {
7267
const { width, height } = container.getBoundingClientRect()
7368
context.gl.setSize(width, height)
7469
context.gl.setPixelRatio(globalThis.devicePixelRatio)
7570

76-
if (context.camera instanceof OrthographicCamera) {
77-
context.camera.left = width / -2
78-
context.camera.right = width / 2
79-
context.camera.top = height / 2
80-
context.camera.bottom = height / -2
71+
if (context.currentCamera instanceof OrthographicCamera) {
72+
context.currentCamera.left = width / -2
73+
context.currentCamera.right = width / 2
74+
context.currentCamera.top = height / 2
75+
context.currentCamera.bottom = height / -2
8176
} else {
82-
context.camera.aspect = width / height
77+
context.currentCamera.aspect = width / height
8378
}
8479

85-
context.camera.updateProjectionMatrix()
80+
context.currentCamera.updateProjectionMatrix()
8681
context.render(performance.now())
87-
}
88-
const observer = new ResizeObserver(onResize)
89-
observer.observe(container)
90-
onResize()
91-
onCleanup(() => observer.disconnect())
92-
93-
// Assign ref
94-
createRenderEffect(() => {
95-
if (props.ref instanceof Function) props.ref(container)
96-
else props.ref = container
9782
})
9883
})
9984

@@ -105,12 +90,13 @@ export function Canvas(props: ParentProps<CanvasProps>) {
10590
width: "100%",
10691
height: "100%",
10792
overflow: "hidden",
93+
contain: "strict",
10894
display: "flex",
10995
...props.style,
11096
}}
11197
class={props.class}
11298
>
113-
<canvas ref={canvas!} style={{ width: "100%", height: "100%" }} />
99+
<canvas ref={canvas!} />
114100
</div>
115101
)
116102
}

src/components.tsx

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import {
2+
type Accessor,
23
type JSX,
4+
type JSXElement,
35
type ParentProps,
6+
Show,
47
createMemo,
58
createRenderEffect,
9+
createResource,
610
mergeProps,
711
splitProps,
812
} from "solid-js"
913
import { Object3D } from "three"
1014
import { threeContext, useThree } from "./hooks.ts"
1115
import { manageSceneGraph, useProps } from "./props.ts"
12-
import type { Constructor, Instance, Overwrite, Props } from "./types.ts"
16+
import type { Constructor, Instance, Loader, Overwrite, Props } from "./types.ts"
1317
import { type InstanceFromConstructor } from "./types.ts"
14-
import { augment, autodispose, isConstructor, isInstance, withContext } from "./utils.ts"
18+
import { augment, autodispose, isConstructor, isInstance, load, withContext } from "./utils.ts"
19+
import { whenMemo } from "./utils/conditionals.ts"
1520

1621
/**********************************************************************************/
1722
/* */
@@ -68,7 +73,7 @@ export const Portal = (props: PortalProps) => {
6873
type EntityProps<T extends object | Constructor<object>> = Overwrite<
6974
Props<T>,
7075
{
71-
from: T
76+
from: T | undefined
7277
}
7378
>
7479
/**
@@ -82,14 +87,52 @@ type EntityProps<T extends object | Constructor<object>> = Overwrite<
8287
*/
8388
export function Entity<T extends object | Constructor<object>>(props: EntityProps<T>) {
8489
const [config, rest] = splitProps(props, ["from", "args"])
85-
const memo = createMemo(() => {
86-
return augment(
87-
isConstructor(config.from)
88-
? autodispose(new config.from(...(config.args ?? [])))
89-
: config.from,
90-
{ props },
91-
) as Instance<T>
92-
})
90+
const memo = whenMemo(
91+
() => config.from,
92+
from => {
93+
const instance = augment(
94+
isConstructor(from) ? autodispose(new from(...(config.args ?? []))) : from,
95+
{
96+
props,
97+
},
98+
) as Instance<T>
99+
return instance
100+
},
101+
)
93102
useProps(memo, rest)
94103
return memo as unknown as JSX.Element
95104
}
105+
106+
/**********************************************************************************/
107+
/* */
108+
/* Resource */
109+
/* */
110+
/**********************************************************************************/
111+
112+
type ResourceProps<TSource, TResult extends object> = Omit<Props<TResult>, "children"> & {
113+
loader: new () => Loader<TSource, TResult>
114+
url: TSource
115+
path?: string
116+
children?: (result: Accessor<TResult>) => JSXElement
117+
}
118+
119+
export function Resource<TSource, TResult extends object>(props: ResourceProps<TSource, TResult>) {
120+
const [config, rest] = splitProps(props, ["loader", "path", "url"])
121+
const loader = createMemo(() => new config.loader())
122+
const [resource] = createResource(
123+
() => [config.url, loader(), config.path] as const,
124+
([url, loader, path]) => {
125+
loader.setPath?.(path ?? "")
126+
return load(loader, url) as Promise<TResult>
127+
},
128+
)
129+
return (
130+
<Show
131+
when={"children" in props && resource()}
132+
// @ts-expect-error FIXME
133+
fallback={<Entity from={resource()} {...rest} />}
134+
>
135+
{resource => props.children?.(resource)}
136+
</Show>
137+
)
138+
}

src/create-events.ts

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Object3D, type Intersection } from "three"
2-
import type { CanvasProps } from "./canvas.tsx"
32
import { $S3C } from "./constants.ts"
43
import type { Context, EventName, Instance, ThreeEvent } from "./types.ts"
54
import { isInstance } from "./utils.ts"
@@ -97,14 +96,11 @@ function createThreeEvent<TEvent extends Event>(
9796
function raycast<TNativeEvent extends MouseEvent | WheelEvent>(
9897
context: Context,
9998
registry: Object3D[],
100-
nativeEvent: TNativeEvent,
99+
event: TNativeEvent,
101100
): Intersection<Instance<Object3D>>[] {
102-
context.setPointer(pointer => {
103-
pointer.x = (nativeEvent.offsetX / context.bounds.width) * 2 - 1
104-
pointer.y = -(nativeEvent.offsetY / context.bounds.height) * 2 + 1
105-
return pointer
106-
})
107-
context.raycaster.setFromCamera(context.pointer, context.camera)
101+
if ("update" in context.raycaster) {
102+
context.raycaster.update(event, context)
103+
}
108104

109105
const nodeSet = new Set<Object3D>()
110106
const visitedSet = new Set<Object3D>()
@@ -138,7 +134,6 @@ function raycast<TNativeEvent extends MouseEvent | WheelEvent>(
138134
function createMissableEventRegistry(
139135
type: "onClick" | "onDoubleClick" | "onContextMenu",
140136
context: Context,
141-
props: CanvasProps,
142137
) {
143138
const registry = createRegistry<Object3D>()
144139

@@ -169,13 +164,11 @@ function createMissableEventRegistry(
169164
// Call the respective canvas event-handler
170165
// if event propagated all the way down
171166
if (!event.stopped) {
172-
props[type]?.(event)
167+
context.props[type]?.(event)
173168
}
174169

175170
// Phase #2 - Raycast remaining missed objects
176171
for (const remainingObject of missedObjects) {
177-
// Perform raycast on unvisited missed objects
178-
context.raycaster.setFromCamera(context.pointer, context.camera)
179172
const intersections = context.raycaster.intersectObject(remainingObject, true)
180173

181174
// Bubble down intersections
@@ -202,7 +195,7 @@ function createMissableEventRegistry(
202195
}
203196

204197
if (visitedObjects.size > 0) {
205-
props[`${type}Missed`]?.(missedEvent)
198+
context.props[`${type}Missed`]?.(missedEvent)
206199
}
207200
})
208201

@@ -226,7 +219,7 @@ function createMissableEventRegistry(
226219
* - `onPointerMove`
227220
* - `onPointerLeave`
228221
*/
229-
function createHoverEventRegistry(type: "Mouse" | "Pointer", context: Context, props: CanvasProps) {
222+
function createHoverEventRegistry(type: "Mouse" | "Pointer", context: Context) {
230223
const registry = createRegistry<Object3D>()
231224
let hoveredSet = new Set<Object3D>()
232225
let intersections: Intersection<Instance<Object3D>>[] = []
@@ -254,7 +247,7 @@ function createHoverEventRegistry(type: "Mouse" | "Pointer", context: Context, p
254247
}
255248

256249
if (hoveredCanvas === false) {
257-
props[`on${type}Enter`]?.(enterEvent)
250+
context.props[`on${type}Enter`]?.(enterEvent)
258251
hoveredCanvas = true
259252
}
260253

@@ -282,7 +275,7 @@ function createHoverEventRegistry(type: "Mouse" | "Pointer", context: Context, p
282275
}
283276

284277
if (!moveEvent.stopped) {
285-
props[`on${type}Move`]?.(moveEvent)
278+
context.props[`on${type}Move`]?.(moveEvent)
286279
}
287280

288281
// Handle leave-event
@@ -299,7 +292,7 @@ function createHoverEventRegistry(type: "Mouse" | "Pointer", context: Context, p
299292

300293
context.canvas.addEventListener(eventNameMap[`on${type}Leave`], nativeEvent => {
301294
const leaveEvent = createThreeEvent(nativeEvent, false)
302-
props[`on${type}Leave`]?.(leaveEvent)
295+
context.props[`on${type}Leave`]?.(leaveEvent)
303296
hoveredCanvas = false
304297

305298
for (const object of hoveredSet) {
@@ -330,7 +323,6 @@ function createHoverEventRegistry(type: "Mouse" | "Pointer", context: Context, p
330323
function createDefaultEventRegistry(
331324
type: "onMouseDown" | "onMouseUp" | "onPointerDown" | "onPointerUp" | "onWheel",
332325
context: Context,
333-
props: CanvasProps,
334326
options?: AddEventListenerOptions,
335327
) {
336328
const registry = createRegistry<Object3D>()
@@ -356,7 +348,7 @@ function createDefaultEventRegistry(
356348

357349
if (!event.stopped) {
358350
// @ts-expect-error TODO: fix type-error
359-
props[type]?.(event)
351+
context.props[type]?.(event)
360352
}
361353
},
362354
options,
@@ -374,27 +366,27 @@ function createDefaultEventRegistry(
374366
/**
375367
* Initializes and manages event handling for all `Instance<Object3D>`.
376368
*/
377-
export function createEvents(context: Context, props: CanvasProps) {
369+
export function createEvents(context: Context) {
378370
// onMouseMove/onMouseEnter/onMouseLeave
379-
const hoverMouseRegistry = createHoverEventRegistry("Mouse", context, props)
371+
const hoverMouseRegistry = createHoverEventRegistry("Mouse", context)
380372
// onPointerMove/onPointerEnter/onPointerLeave
381-
const hoverPointerRegistry = createHoverEventRegistry("Pointer", context, props)
373+
const hoverPointerRegistry = createHoverEventRegistry("Pointer", context)
382374

383375
// onClick/onClickMissed
384-
const missableClickRegistry = createMissableEventRegistry("onClick", context, props)
376+
const missableClickRegistry = createMissableEventRegistry("onClick", context)
385377
// onContextMenu/onContextMenuMissed
386-
const missableContextMenuRegistry = createMissableEventRegistry("onContextMenu", context, props)
378+
const missableContextMenuRegistry = createMissableEventRegistry("onContextMenu", context)
387379
// onDoubleClick/onDoubleClickMissed
388-
const missableDoubleClickRegistry = createMissableEventRegistry("onDoubleClick", context, props)
380+
const missableDoubleClickRegistry = createMissableEventRegistry("onDoubleClick", context)
389381

390382
// Default mouse-events
391-
const mouseDownRegistry = createDefaultEventRegistry("onMouseDown", context, props)
392-
const mouseUpRegistry = createDefaultEventRegistry("onMouseUp", context, props)
383+
const mouseDownRegistry = createDefaultEventRegistry("onMouseDown", context)
384+
const mouseUpRegistry = createDefaultEventRegistry("onMouseUp", context)
393385
// Default pointer-events
394-
const pointerDownRegistry = createDefaultEventRegistry("onPointerDown", context, props)
395-
const pointerUpRegistry = createDefaultEventRegistry("onPointerUp", context, props)
386+
const pointerDownRegistry = createDefaultEventRegistry("onPointerDown", context)
387+
const pointerUpRegistry = createDefaultEventRegistry("onPointerUp", context)
396388
// Default wheel-event
397-
const wheelRegistry = createDefaultEventRegistry("onWheel", context, props, { passive: true })
389+
const wheelRegistry = createDefaultEventRegistry("onWheel", context, { passive: true })
398390

399391
return {
400392
/**

0 commit comments

Comments
 (0)