Skip to content

Commit eacf21d

Browse files
committed
refactor: rename Instance to Meta and add priority-based frame listeners
- Rename Instance type to Meta throughout codebase for clarity - Rename InstanceFromConstructor to InstanceOf for consistency - Add priority and stage options to useFrame hook - Implement sorted priority system for frame listeners (before/after render stages) - Use binary search for efficient priority insertion - Fix useProps to handle undefined objects gracefully
1 parent a276bf3 commit eacf21d

File tree

13 files changed

+192
-129
lines changed

13 files changed

+192
-129
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"type": "git",
77
"url": "git+https://github.com/solidjs-community/solid-three.git"
88
},
9+
"type": "module",
910
"module": "./dist/index/index.js",
1011
"main": "./dist/index/index.js",
1112
"types": "./dist/index/index.d.ts",
@@ -112,6 +113,5 @@
112113
"./dist/testing/index.d.ts"
113114
]
114115
}
115-
},
116-
"type": "module"
116+
}
117117
}

playground/examples/SolarExample.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createEffect, createMemo, createSignal, Show, type ParentProps, type Ref } from "solid-js"
2-
import type { Instance } from "src/types.ts"
2+
import type { Meta } from "src/types.ts"
33
import * as THREE from "three"
44
import { Canvas, createT, Entity, useFrame } from "../../src/index.ts"
55
import { OrbitControls } from "../controls/OrbitControls.tsx"
@@ -66,7 +66,7 @@ function CelestialBody(
6666
}>,
6767
) {
6868
const [hovered, setHovered] = createSignal(false)
69-
let ref: Instance<THREE.Mesh> = null!
69+
let ref: Meta<THREE.Mesh> = null!
7070

7171
createEffect(() => {
7272
if (!props.orbit) return

src/components.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import {
1313
import { Object3D } from "three"
1414
import { threeContext, useThree } from "./hooks.ts"
1515
import { manageSceneGraph, useProps } from "./props.ts"
16-
import type { Constructor, Instance, Loader, Overwrite, Props } from "./types.ts"
17-
import { type InstanceFromConstructor } from "./types.ts"
16+
import type { Constructor, Loader, Meta, Overwrite, Props } from "./types.ts"
17+
import { type InstanceOf } from "./types.ts"
1818
import { augment, autodispose, isConstructor, isInstance, load, withContext } from "./utils.ts"
1919
import { whenMemo } from "./utils/conditionals.ts"
2020

@@ -25,7 +25,7 @@ import { whenMemo } from "./utils/conditionals.ts"
2525
/**********************************************************************************/
2626

2727
type PortalProps = ParentProps<{
28-
element?: InstanceFromConstructor<Object3D> | Instance<Object3D>
28+
element?: InstanceOf<Object3D> | Meta<Object3D>
2929
}>
3030
/**
3131
* A component for placing its children outside the regular `solid-three` scene graph managed by Solid's reactive system.
@@ -40,7 +40,7 @@ export const Portal = (props: PortalProps) => {
4040
const scene = createMemo(() => {
4141
return props.element
4242
? isInstance(props.element)
43-
? (props.element as Instance<Object3D>)
43+
? (props.element as Meta<Object3D>)
4444
: augment(props.element, { props: {} })
4545
: context.scene
4646
})
@@ -49,7 +49,7 @@ export const Portal = (props: PortalProps) => {
4949
// @ts-expect-error TODO: fix type-error
5050
manageSceneGraph(scene(), () =>
5151
withContext(
52-
() => props.children as unknown as Instance | Instance[],
52+
() => props.children as unknown as Meta | Meta[],
5353
// @ts-expect-error TODO: fix type-error
5454
threeContext,
5555
mergeProps(context, {
@@ -74,6 +74,7 @@ type EntityProps<T extends object | Constructor<object>> = Overwrite<
7474
Props<T>,
7575
{
7676
from: T | undefined
77+
children?: JSXElement
7778
}
7879
>
7980
/**
@@ -95,7 +96,7 @@ export function Entity<T extends object | Constructor<object>>(props: EntityProp
9596
{
9697
props,
9798
},
98-
) as Instance<T>
99+
) as Meta<T>
99100
return instance
100101
},
101102
)

src/create-events.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Object3D, type Intersection } from "three"
22
import { $S3C } from "./constants.ts"
3-
import type { Context, EventName, Instance, ThreeEvent } from "./types.ts"
3+
import type { Context, EventName, Meta, ThreeEvent } from "./types.ts"
44
import { isInstance } from "./utils.ts"
55

66
const eventNameMap = {
@@ -97,7 +97,7 @@ function raycast<TNativeEvent extends MouseEvent | WheelEvent>(
9797
context: Context,
9898
registry: Object3D[],
9999
event: TNativeEvent,
100-
): Intersection<Instance<Object3D>>[] {
100+
): Intersection<Meta<Object3D>>[] {
101101
if ("update" in context.raycaster) {
102102
context.raycaster.update(event, context)
103103
}
@@ -222,7 +222,7 @@ function createMissableEventRegistry(
222222
function createHoverEventRegistry(type: "Mouse" | "Pointer", context: Context) {
223223
const registry = createRegistry<Object3D>()
224224
let hoveredSet = new Set<Object3D>()
225-
let intersections: Intersection<Instance<Object3D>>[] = []
225+
let intersections: Intersection<Meta<Object3D>>[] = []
226226
let hoveredCanvas = false
227227

228228
context.canvas.addEventListener(eventNameMap[`on${type}Move`], nativeEvent => {
@@ -395,7 +395,7 @@ export function createEvents(context: Context) {
395395
* @param object - The 3D object to register.
396396
* @param type - The type of event the object should listen for.
397397
*/
398-
addEventListener(object: Instance<Object3D>, type: EventName) {
398+
addEventListener(object: Meta<Object3D>, type: EventName) {
399399
switch (type) {
400400
// Missable Events
401401
case "onClick":

src/create-three.tsx

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
createEffect,
44
createMemo,
55
createRenderEffect,
6+
createRoot,
67
mergeProps,
78
onCleanup,
89
} from "solid-js"
@@ -29,9 +30,10 @@ import { frameContext, threeContext } from "./hooks.ts"
2930
import { eventContext } from "./internal-context.ts"
3031
import { manageSceneGraph, useProps } from "./props.ts"
3132
import { CursorRaycaster, type EventRaycaster } from "./raycasters.tsx"
32-
import type { CameraKind, Context } from "./types.ts"
33+
import type { CameraKind, Context, FrameListener, FrameListenerCallback } from "./types.ts"
3334
import {
3435
augment,
36+
binarySearch,
3537
defaultProps,
3638
getCurrentViewport,
3739
removeElementFromArray,
@@ -54,15 +56,55 @@ export function createThree(canvas: HTMLCanvasElement, props: CanvasProps) {
5456
/* */
5557
/**********************************************************************************/
5658

57-
type FrameListener = (context: Context, delta: number, frame?: XRFrame) => void
59+
const frameListeners = {
60+
before: {
61+
map: new Map<number, FrameListenerCallback[]>(),
62+
priorities: [] as number[], // Keep this sorted
63+
},
64+
after: {
65+
map: new Map<number, FrameListenerCallback[]>(),
66+
priorities: [] as number[],
67+
},
68+
}
69+
70+
const addFrameListener: FrameListener = (callback, options) => {
71+
return createRoot(dispose => {
72+
createRenderEffect(() => {
73+
const { stage = "before", priority = 0 } = options ?? {}
74+
75+
const listeners = frameListeners[stage]
76+
77+
let array = listeners.map.get(priority)
78+
79+
if (!array) {
80+
array = []
81+
listeners.map.set(priority, array)
82+
const index = binarySearch(listeners.priorities, priority)
83+
listeners.priorities.splice(index, 0, priority)
84+
}
85+
86+
array.push(callback)
5887

59-
const frameListeners: FrameListener[] = []
60-
// Adds a callback to be called on each frame
61-
function addFrameListener(callback: FrameListener) {
62-
frameListeners.push(callback)
63-
const cleanup = () => removeElementFromArray(frameListeners, callback)
64-
onCleanup(cleanup)
65-
return cleanup
88+
onCleanup(() => {
89+
removeElementFromArray(array, callback)
90+
if (array.length === 0) {
91+
listeners.map.delete(priority)
92+
listeners.priorities.splice(listeners.priorities.indexOf(priority), 1)
93+
}
94+
})
95+
})
96+
97+
return dispose
98+
})
99+
}
100+
101+
function updateFrameListeners(stage: "before" | "after", delta: number, frame?: XRFrame) {
102+
for (const priority of frameListeners[stage].priorities) {
103+
const callbacks = frameListeners[stage].map.get(priority)!
104+
for (const callback of callbacks) {
105+
callback(context, delta, frame)
106+
}
107+
}
66108
}
67109

68110
/**********************************************************************************/
@@ -100,6 +142,7 @@ export function createThree(canvas: HTMLCanvasElement, props: CanvasProps) {
100142
/**********************************************************************************/
101143

102144
let pendingRenderRequest: number | undefined
145+
103146
function render(timestamp: number, frame?: XRFrame) {
104147
if (!context.gl) {
105148
return
@@ -108,8 +151,11 @@ export function createThree(canvas: HTMLCanvasElement, props: CanvasProps) {
108151
context.clock.elapsedTime = timestamp
109152
}
110153
pendingRenderRequest = undefined
154+
155+
const delta = context.clock.getDelta()
156+
updateFrameListeners("before", delta, frame)
111157
context.gl.render(context.scene, context.currentCamera)
112-
frameListeners.forEach(listener => listener(context, context.clock.getDelta(), frame))
158+
updateFrameListeners("after", delta, frame)
113159
}
114160
function requestRender() {
115161
if (pendingRenderRequest) return

src/data-structure/augmented-stack.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { Accessor } from "solid-js"
2-
import type { Instance } from "src/types.ts"
2+
import type { Meta } from "src/types.ts"
33
import { augment } from "../utils.ts"
44
import { Stack } from "./stack.ts"
55

66
/** A generic stack data structure. It augments each value before pushing it onto the stack. */
77
export class AugmentedStack<T> {
8-
#stack = new Stack<Instance<T>>(null!)
8+
#stack = new Stack<Meta<T>>(null!)
99
constructor(public name: string) {
1010
this.#stack.name = name
1111
}

src/hooks.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { type Accessor, createContext, useContext } from "solid-js"
2-
import type { Context } from "./types"
2+
import type { Context, FrameListener } from "./types"
33

44
/**********************************************************************************/
55
/* */
66
/* Use Frame */
77
/* */
88
/**********************************************************************************/
99

10-
type FrameContext = (callback: (context: Context, delta: number, frame?: XRFrame) => void) => void
11-
export const frameContext = createContext<FrameContext>()
10+
export const frameContext = createContext<FrameListener>()
1211

1312
/**
1413
* Hook to register a callback that will be executed on each animation frame within the `<Canvas/>` component.
@@ -17,12 +16,12 @@ export const frameContext = createContext<FrameContext>()
1716
* @param callback - The callback function to be executed on each frame.
1817
* @throws Throws an error if used outside of the Canvas component context.
1918
*/
20-
export const useFrame = (callback: (context: Context, delta: number, frame?: XRFrame) => void) => {
19+
export const useFrame: FrameListener = (callback, options) => {
2120
const addFrameListener = useContext(frameContext)
2221
if (!addFrameListener) {
2322
throw new Error("S3: Hooks can only be used within the Canvas component!")
2423
}
25-
addFrameListener(callback)
24+
return addFrameListener(callback, options)
2625
}
2726

2827
/**********************************************************************************/

src/internal-context.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type JSX, createContext, useContext } from "solid-js"
22
import { Object3D } from "three"
3-
import type { EventName, Instance } from "./types.ts"
3+
import type { EventName, Meta } from "./types.ts"
44

55
/**
66
* Registers an event listener for an `AugmentedElement` to the nearest Canvas component up the component tree.
@@ -10,15 +10,14 @@ import type { EventName, Instance } from "./types.ts"
1010
* @param type - The type of event to listen for (e.g., 'click', 'mouseenter').
1111
* @throws Throws an error if used outside of the Canvas component context.
1212
*/
13-
export const addToEventListeners = (object: Instance<Object3D>, type: EventName) => {
13+
export const addToEventListeners = (object: Meta<Object3D>, type: EventName) => {
1414
const addToEventListeners = useContext(eventContext)
1515
if (!addToEventListeners) {
1616
throw new Error("S3: Hooks can only be used within the Canvas component!")
1717
}
1818
return addToEventListeners(object, type)
1919
}
20-
export const eventContext =
21-
createContext<(object: Instance<Object3D>, type: EventName) => () => void>()
20+
export const eventContext = createContext<(object: Meta<Object3D>, type: EventName) => () => void>()
2221

2322
/**
2423
* This function facilitates the rendering of JSX elements outside the normal scene

0 commit comments

Comments
 (0)