@solidrt/3d 0.0.55 → 0.0.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1206 -224
- package/README.md +16 -7
- package/examples/README.md +88 -9
- package/examples/aim.tsx +2 -3
- package/examples/cascades.tsx +1 -4
- package/examples/collision.tsx +263 -0
- package/examples/first-person.tsx +129 -0
- package/examples/fleet.tsx +200 -0
- package/examples/fog.tsx +2 -3
- package/examples/instanced.tsx +72 -32
- package/examples/lamps.tsx +92 -0
- package/examples/lit.tsx +3 -5
- package/examples/materials.tsx +150 -0
- package/examples/model-load.tsx +95 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +35 -9
- package/examples/pick.tsx +145 -37
- package/examples/probe.tsx +107 -0
- package/examples/scene-background.tsx +2 -4
- package/examples/scene-basic.tsx +2 -4
- package/examples/scene-views.tsx +68 -48
- package/examples/shadows.tsx +2 -4
- package/examples/sky-lit.tsx +92 -0
- package/examples/skybox.tsx +134 -0
- package/examples/sprites.tsx +4 -6
- package/examples/standard.tsx +138 -0
- package/examples/sweep-paths.tsx +2 -4
- package/package.json +3 -3
- package/src/camera.ts +148 -0
- package/src/clip.ts +96 -0
- package/src/collision.ts +20 -0
- package/src/color.ts +32 -0
- package/src/components/context.tsx +35 -0
- package/src/components/directional-light.tsx +48 -0
- package/src/components/first-person-camera.tsx +72 -0
- package/src/components/group.tsx +18 -0
- package/src/components/hemisphere-light.tsx +24 -0
- package/src/components/index.ts +46 -0
- package/src/components/instanced-mesh.tsx +105 -0
- package/src/components/mesh.tsx +92 -0
- package/src/components/node-props.ts +90 -0
- package/src/components/orbit-camera.tsx +75 -0
- package/src/components/perspective-camera.tsx +32 -0
- package/src/components/point-light.tsx +52 -0
- package/src/components/record-mesh.tsx +68 -0
- package/src/components/scene.tsx +281 -0
- package/src/components/spot-light.tsx +65 -0
- package/src/components/sprite.tsx +38 -0
- package/src/components/view3d.tsx +152 -0
- package/src/environment-bake.ts +492 -0
- package/src/environment.ts +325 -0
- package/src/first-person.ts +258 -0
- package/src/geometry.ts +6 -2
- package/src/glsl.ts +1343 -82
- package/src/gltf.ts +432 -112
- package/src/index.ts +61 -9
- package/src/input.ts +70 -0
- package/src/light.ts +383 -0
- package/src/material.ts +667 -232
- package/src/math.ts +110 -0
- package/src/mesh.ts +728 -0
- package/src/mixer.ts +476 -0
- package/src/model-file.ts +80 -15
- package/src/model.ts +204 -35
- package/src/node.ts +579 -0
- package/src/orbit.ts +292 -212
- package/src/scene-pointer.ts +294 -0
- package/src/scene-shadows.ts +471 -0
- package/src/scene.ts +1435 -1576
- package/tools/environment.ts +69 -0
- package/tools/model.ts +4 -3
- package/src/components.tsx +0 -420
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Pointer dispatch behind scene.handlers (and a view's): the element event
|
|
2
|
+
// model one tree deeper, with the SCENE - or the view whose leaf received
|
|
3
|
+
// the event - as the root of the walk. Down, move, up and wheel dispatch
|
|
4
|
+
// on the nearest hit, bubble from the struck instance (when the mesh is
|
|
5
|
+
// instanced) through the mesh and its ancestors and end at the root's
|
|
6
|
+
// listeners; over empty space the walk is the root alone (`mesh` null).
|
|
7
|
+
// Claiming is stopPropagation: a handler that stops an event keeps every
|
|
8
|
+
// later target, the root included, from seeing it, and the DOWN decides
|
|
9
|
+
// the press - a pointer whose down never reached the root stays claimed
|
|
10
|
+
// by the node chain for its move, up and tap too, so a mesh that drags
|
|
11
|
+
// itself stops its down once and an orbit control fed at the root never
|
|
12
|
+
// turns. Capture is per pointer to the press target, the root included: a
|
|
13
|
+
// drag from empty space keeps delivering to the root as it crosses meshes,
|
|
14
|
+
// exactly as a drag from a mesh keeps naming that mesh. Enter/leave pair
|
|
15
|
+
// on the struck node alone. Taps are synthesized here (DOM's click,
|
|
16
|
+
// Unity's IPointerClickHandler): a press that releases on the same target
|
|
17
|
+
// within the slop, the only pointer down for its whole press, counted up
|
|
18
|
+
// for repeats (DOM's detail, Unity's clickCount). @solidrt/2d's
|
|
19
|
+
// dispatch.ts is the same model one dimension down.
|
|
20
|
+
//
|
|
21
|
+
// Pure BY DESIGN (types only, nothing with GPU imports) so
|
|
22
|
+
// checks/dispatch-check.ts drives it headless with a fake pick.
|
|
23
|
+
|
|
24
|
+
import type { PointerEvent as ElementPointerEvent, PointerFeed } from "@solidrt/core"
|
|
25
|
+
import type { NodePointerEvent, SceneNode, ScenePointerListener } from "./node.ts"
|
|
26
|
+
import type { InstanceNode, Mesh } from "./mesh.ts"
|
|
27
|
+
import type { Hit, Scene, SceneHandlers, ViewHandle } from "./scene.ts"
|
|
28
|
+
import type { Vec3 } from "./math.ts"
|
|
29
|
+
|
|
30
|
+
// Finger travel from the down point, in window pixels, past which a press
|
|
31
|
+
// is a drag and never a tap. Core's pan and transform recognizers engage
|
|
32
|
+
// at the same 8, so a press is never both a tap and a drag.
|
|
33
|
+
const TAP_SLOP = 8
|
|
34
|
+
// Successive taps closer in time than this, on the same target and within
|
|
35
|
+
// TAP_REPEAT_SLOP of each other, count up (Unity's 0.3 s clickCount
|
|
36
|
+
// window, Android's double-tap timeout).
|
|
37
|
+
const TAP_INTERVAL_MS = 300
|
|
38
|
+
// Window pixels a repeat tap may land from the previous one and still
|
|
39
|
+
// count: a thumb repeats within about this (Hammer.js uses 10, Android
|
|
40
|
+
// 100 dp); 20 tolerates touch without a mouse double-click spanning
|
|
41
|
+
// neighbouring meshes.
|
|
42
|
+
const TAP_REPEAT_SLOP = 20
|
|
43
|
+
|
|
44
|
+
type HandlerName = "onPointerDown" | "onPointerMove" | "onPointerUp" | "onWheel" | "onTap"
|
|
45
|
+
|
|
46
|
+
// What a press captures, a hover tracks and a tap compares: the struck
|
|
47
|
+
// mesh, and the instance when the mesh is instanced.
|
|
48
|
+
type Target = { mesh: Mesh; instance: InstanceNode | null }
|
|
49
|
+
|
|
50
|
+
// One runtime event object serves every view (NodePointerEvent for the
|
|
51
|
+
// node chain, ScenePointerEvent at the root, plus the wheel and tap
|
|
52
|
+
// fields); the public types narrow it per handler.
|
|
53
|
+
type InternalEvent = {
|
|
54
|
+
mesh: Mesh | null
|
|
55
|
+
instance: InstanceNode | null
|
|
56
|
+
currentTarget: SceneNode | Scene | ViewHandle
|
|
57
|
+
point: Vec3 | null
|
|
58
|
+
distance: number | null
|
|
59
|
+
x: number
|
|
60
|
+
y: number
|
|
61
|
+
pointerId: number
|
|
62
|
+
pointerType: string
|
|
63
|
+
button?: number
|
|
64
|
+
shiftKey: boolean
|
|
65
|
+
ctrlKey: boolean
|
|
66
|
+
altKey: boolean
|
|
67
|
+
metaKey: boolean
|
|
68
|
+
native: ElementPointerEvent
|
|
69
|
+
deltaX?: number
|
|
70
|
+
deltaY?: number
|
|
71
|
+
tapCount?: number
|
|
72
|
+
_stopped: boolean
|
|
73
|
+
stopPropagation(): void
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A pointer between its down and up: what it pressed (null = the root),
|
|
77
|
+
// whether the down reached the root, the down point for the slop, and
|
|
78
|
+
// whether it has been the only pointer down for its whole press.
|
|
79
|
+
type Press = {
|
|
80
|
+
target: Target | null
|
|
81
|
+
claimed: boolean
|
|
82
|
+
cx: number
|
|
83
|
+
cy: number
|
|
84
|
+
moved: boolean
|
|
85
|
+
alone: boolean
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type PointerInputDeps = {
|
|
89
|
+
/** The root's pick: the camera ray through one of its pixels, hits
|
|
90
|
+
* nearest first. */
|
|
91
|
+
pick(x: number, y: number): Hit[]
|
|
92
|
+
/** The root target's current size (handlersFor's layout scaling). */
|
|
93
|
+
targetSize(): { width: number; height: number }
|
|
94
|
+
/** The scene or view: the walk's root and the listeners' currentTarget. */
|
|
95
|
+
root: Scene | ViewHandle
|
|
96
|
+
/** The root's listeners, in registration order. */
|
|
97
|
+
listeners: Set<ScenePointerListener>
|
|
98
|
+
/** The clock for tap repeats (default performance.now; checks inject). */
|
|
99
|
+
now?: () => number
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type PointerInput = {
|
|
103
|
+
/** scene.handlers: for a leaf laid out at the target size. */
|
|
104
|
+
handlers: SceneHandlers
|
|
105
|
+
/** scene.handlersFor: for a leaf whose layout size differs (the
|
|
106
|
+
* supersampling pattern); `layout` is read per event. */
|
|
107
|
+
handlersFor(layout: () => { width: number; height: number }): SceneHandlers
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function makePointerInput(deps: PointerInputDeps): PointerInput {
|
|
111
|
+
let now = deps.now ?? (() => performance.now())
|
|
112
|
+
let presses = new Map<number, Press>()
|
|
113
|
+
let hover = new Map<number, Target>()
|
|
114
|
+
let lastTap: { time: number; cx: number; cy: number; target: Target | null; count: number } | null = null
|
|
115
|
+
|
|
116
|
+
let targetOf = (hit: Hit): Target => ({ mesh: hit.mesh, instance: hit.instance ?? null })
|
|
117
|
+
let sameHit = (hit: Hit, target: Target): boolean => hit.mesh === target.mesh && (hit.instance ?? null) === target.instance
|
|
118
|
+
let sameTarget = (a: Target | null, b: Target | null): boolean => (a === null ? b === null : b !== null && a.mesh === b.mesh && a.instance === b.instance)
|
|
119
|
+
// The node the walk starts at, and the one enter/leave fire on.
|
|
120
|
+
let nodeOf = (target: Target): SceneNode => target.instance ?? target.mesh
|
|
121
|
+
// The captured target's own hit, if the ray still strikes it.
|
|
122
|
+
let hitOn = (target: Target | null, x: number, y: number): Hit | null => {
|
|
123
|
+
if (target === null) return null
|
|
124
|
+
for (let h of deps.pick(x, y)) if (sameHit(h, target)) return h
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Bubble from the struck node through its ancestors; then, when the
|
|
129
|
+
// press lets it, the root's listeners (every one of them - the root is
|
|
130
|
+
// the last stop, there is nothing left to claim). Returns whether the
|
|
131
|
+
// root ran.
|
|
132
|
+
let walk = (name: HandlerName, event: InternalEvent, toRoot: boolean): boolean => {
|
|
133
|
+
for (let n: SceneNode | null = event.instance ?? event.mesh; n !== null && !event._stopped; n = n.parent) {
|
|
134
|
+
// A node that left the scene mid-press has no handlers to run.
|
|
135
|
+
if (n._scene === null) continue
|
|
136
|
+
let handler = n[name] as ((event: InternalEvent) => void) | undefined
|
|
137
|
+
if (handler) {
|
|
138
|
+
event.currentTarget = n
|
|
139
|
+
handler(event)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (event._stopped || !toRoot) return false
|
|
143
|
+
event.currentTarget = deps.root
|
|
144
|
+
for (let listener of deps.listeners) {
|
|
145
|
+
let handler = listener[name] as ((event: InternalEvent) => void) | undefined
|
|
146
|
+
if (handler) handler(event)
|
|
147
|
+
}
|
|
148
|
+
return true
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// localX/localY arrive in the leaf's LAYOUT frame (the hit test undoes
|
|
152
|
+
// every transform above it, design-size fits included), so a leaf laid
|
|
153
|
+
// out at the target size - the built-in <Scene> leaf, a d-texture at
|
|
154
|
+
// natural size - is already in scene pixels. Only a leaf deliberately
|
|
155
|
+
// laid out at a DIFFERENT size (the supersampling pattern) needs the
|
|
156
|
+
// ratio, and only the app knows that layout: handlersFor takes it.
|
|
157
|
+
let makeHandlers = (layout: (() => { width: number; height: number }) | null): SceneHandlers => {
|
|
158
|
+
let toScene = (e: ElementPointerEvent): [number, number] => {
|
|
159
|
+
if (layout === null) return [e.localX, e.localY]
|
|
160
|
+
let l = layout()
|
|
161
|
+
let size = deps.targetSize()
|
|
162
|
+
return [e.localX * (l.width > 0 ? size.width / l.width : 1), e.localY * (l.height > 0 ? size.height / l.height : 1)]
|
|
163
|
+
}
|
|
164
|
+
let makeEvent = (e: ElementPointerEvent, target: Target | null, x: number, y: number, hit: Hit | null): InternalEvent => {
|
|
165
|
+
let event: InternalEvent = {
|
|
166
|
+
mesh: target === null ? null : target.mesh,
|
|
167
|
+
instance: target === null ? null : target.instance,
|
|
168
|
+
currentTarget: target === null ? deps.root : nodeOf(target),
|
|
169
|
+
point: hit === null ? null : hit.point,
|
|
170
|
+
distance: hit === null ? null : hit.distance,
|
|
171
|
+
x,
|
|
172
|
+
y,
|
|
173
|
+
pointerId: e.pointerId,
|
|
174
|
+
pointerType: e.pointerType,
|
|
175
|
+
button: e.button,
|
|
176
|
+
shiftKey: e.shiftKey,
|
|
177
|
+
ctrlKey: e.ctrlKey,
|
|
178
|
+
altKey: e.altKey,
|
|
179
|
+
metaKey: e.metaKey,
|
|
180
|
+
native: e,
|
|
181
|
+
_stopped: false,
|
|
182
|
+
stopPropagation() {
|
|
183
|
+
event._stopped = true
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
return event
|
|
187
|
+
}
|
|
188
|
+
let nearest = (x: number, y: number): Hit | null => deps.pick(x, y)[0] ?? null
|
|
189
|
+
// Enter/leave go to one node directly, outside the walk: the event
|
|
190
|
+
// in its node view.
|
|
191
|
+
let hoverEvent = (e: ElementPointerEvent, target: Target, x: number, y: number, hit: Hit | null): NodePointerEvent =>
|
|
192
|
+
makeEvent(e, target, x, y, hit) as unknown as NodePointerEvent
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
onPointerDown(e) {
|
|
196
|
+
let [x, y] = toScene(e)
|
|
197
|
+
let hit = nearest(x, y)
|
|
198
|
+
// A second pointer landing ends "alone" for every press in flight.
|
|
199
|
+
for (let p of presses.values()) p.alone = false
|
|
200
|
+
let press: Press = { target: hit === null ? null : targetOf(hit), claimed: false, cx: e.clientX, cy: e.clientY, moved: false, alone: presses.size === 0 }
|
|
201
|
+
presses.set(e.pointerId, press)
|
|
202
|
+
press.claimed = !walk("onPointerDown", makeEvent(e, press.target, x, y, hit), true)
|
|
203
|
+
},
|
|
204
|
+
onPointerMove(e) {
|
|
205
|
+
let [x, y] = toScene(e)
|
|
206
|
+
let press = presses.get(e.pointerId)
|
|
207
|
+
if (press) {
|
|
208
|
+
if (!press.moved && Math.hypot(e.clientX - press.cx, e.clientY - press.cy) >= TAP_SLOP) press.moved = true
|
|
209
|
+
walk("onPointerMove", makeEvent(e, press.target, x, y, hitOn(press.target, x, y)), !press.claimed)
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
let hit = nearest(x, y)
|
|
213
|
+
let prev = hover.get(e.pointerId) ?? null
|
|
214
|
+
let changed = prev === null ? hit !== null : hit === null || !sameHit(hit, prev)
|
|
215
|
+
if (changed) {
|
|
216
|
+
if (prev !== null) {
|
|
217
|
+
hover.delete(e.pointerId)
|
|
218
|
+
let node = nodeOf(prev)
|
|
219
|
+
if (node._scene !== null) node.onPointerLeave?.(hoverEvent(e, prev, x, y, null))
|
|
220
|
+
}
|
|
221
|
+
if (hit !== null) {
|
|
222
|
+
let target = targetOf(hit)
|
|
223
|
+
hover.set(e.pointerId, target)
|
|
224
|
+
nodeOf(target).onPointerEnter?.(hoverEvent(e, target, x, y, hit))
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
walk("onPointerMove", makeEvent(e, hit === null ? null : targetOf(hit), x, y, hit), true)
|
|
228
|
+
},
|
|
229
|
+
onPointerUp(e) {
|
|
230
|
+
let [x, y] = toScene(e)
|
|
231
|
+
let press = presses.get(e.pointerId)
|
|
232
|
+
if (!press) {
|
|
233
|
+
// A press this root never saw go down: deliver to what is under
|
|
234
|
+
// it, as the element model does.
|
|
235
|
+
let hit = nearest(x, y)
|
|
236
|
+
walk("onPointerUp", makeEvent(e, hit === null ? null : targetOf(hit), x, y, hit), true)
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
presses.delete(e.pointerId)
|
|
240
|
+
let under = hitOn(press.target, x, y)
|
|
241
|
+
walk("onPointerUp", makeEvent(e, press.target, x, y, under), !press.claimed)
|
|
242
|
+
// The tap rule: no travel past the slop, alone for the whole
|
|
243
|
+
// press, released over the target it pressed (empty space for the
|
|
244
|
+
// root itself).
|
|
245
|
+
if (press.moved || !press.alone) return
|
|
246
|
+
let release = nearest(x, y)
|
|
247
|
+
if (press.target === null ? release !== null : release === null || !sameHit(release, press.target)) return
|
|
248
|
+
let time = now()
|
|
249
|
+
let repeat = lastTap !== null && time - lastTap.time <= TAP_INTERVAL_MS && sameTarget(lastTap.target, press.target) && Math.hypot(e.clientX - lastTap.cx, e.clientY - lastTap.cy) <= TAP_REPEAT_SLOP
|
|
250
|
+
let count = repeat ? lastTap!.count + 1 : 1
|
|
251
|
+
lastTap = { time, cx: e.clientX, cy: e.clientY, target: press.target, count }
|
|
252
|
+
let tap = makeEvent(e, press.target, x, y, release)
|
|
253
|
+
tap.tapCount = count
|
|
254
|
+
walk("onTap", tap, !press.claimed)
|
|
255
|
+
},
|
|
256
|
+
onWheel(e) {
|
|
257
|
+
let [x, y] = toScene(e)
|
|
258
|
+
let hit = nearest(x, y)
|
|
259
|
+
let event = makeEvent(e, hit === null ? null : targetOf(hit), x, y, hit)
|
|
260
|
+
event.deltaX = e.deltaX
|
|
261
|
+
event.deltaY = e.deltaY
|
|
262
|
+
walk("onWheel", event, true)
|
|
263
|
+
},
|
|
264
|
+
onPointerLeave(e) {
|
|
265
|
+
let prev = hover.get(e.pointerId)
|
|
266
|
+
if (prev === undefined) return
|
|
267
|
+
hover.delete(e.pointerId)
|
|
268
|
+
let node = nodeOf(prev)
|
|
269
|
+
if (node._scene === null) return
|
|
270
|
+
let [x, y] = toScene(e)
|
|
271
|
+
node.onPointerLeave?.(hoverEvent(e, prev, x, y, null))
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { handlers: makeHandlers(null), handlersFor: makeHandlers }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Feed a pointer feed (createPointerFeed) from a root's listeners: the
|
|
281
|
+
* events the meshes let through - a mesh that claims its press
|
|
282
|
+
* (stopPropagation on its down) keeps a camera control bound to the feed
|
|
283
|
+
* out of that drag, a drag on empty space or an unclaimed mesh orbits, a
|
|
284
|
+
* wheel anywhere zooms. Returns the detach. The components do this for
|
|
285
|
+
* their `pointer` prop; an imperative scene or view calls it directly.
|
|
286
|
+
*/
|
|
287
|
+
export function feedPointer(root: { listen(listener: ScenePointerListener): () => void }, feed: PointerFeed): () => void {
|
|
288
|
+
return root.listen({
|
|
289
|
+
onPointerDown: e => feed.handlers.onPointerDown(e.native),
|
|
290
|
+
onPointerMove: e => feed.handlers.onPointerMove(e.native),
|
|
291
|
+
onPointerUp: e => feed.handlers.onPointerUp(e.native),
|
|
292
|
+
onWheel: e => feed.handlers.onWheel(e.native),
|
|
293
|
+
})
|
|
294
|
+
}
|