@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.
Files changed (72) hide show
  1. package/AGENTS.md +1206 -224
  2. package/README.md +16 -7
  3. package/examples/README.md +88 -9
  4. package/examples/aim.tsx +2 -3
  5. package/examples/cascades.tsx +1 -4
  6. package/examples/collision.tsx +263 -0
  7. package/examples/first-person.tsx +129 -0
  8. package/examples/fleet.tsx +200 -0
  9. package/examples/fog.tsx +2 -3
  10. package/examples/instanced.tsx +72 -32
  11. package/examples/lamps.tsx +92 -0
  12. package/examples/lit.tsx +3 -5
  13. package/examples/materials.tsx +150 -0
  14. package/examples/model-load.tsx +95 -0
  15. package/examples/model.glb +0 -0
  16. package/examples/model.tsx +35 -9
  17. package/examples/pick.tsx +145 -37
  18. package/examples/probe.tsx +107 -0
  19. package/examples/scene-background.tsx +2 -4
  20. package/examples/scene-basic.tsx +2 -4
  21. package/examples/scene-views.tsx +68 -48
  22. package/examples/shadows.tsx +2 -4
  23. package/examples/sky-lit.tsx +92 -0
  24. package/examples/skybox.tsx +134 -0
  25. package/examples/sprites.tsx +4 -6
  26. package/examples/standard.tsx +138 -0
  27. package/examples/sweep-paths.tsx +2 -4
  28. package/package.json +3 -3
  29. package/src/camera.ts +148 -0
  30. package/src/clip.ts +96 -0
  31. package/src/collision.ts +20 -0
  32. package/src/color.ts +32 -0
  33. package/src/components/context.tsx +35 -0
  34. package/src/components/directional-light.tsx +48 -0
  35. package/src/components/first-person-camera.tsx +72 -0
  36. package/src/components/group.tsx +18 -0
  37. package/src/components/hemisphere-light.tsx +24 -0
  38. package/src/components/index.ts +46 -0
  39. package/src/components/instanced-mesh.tsx +105 -0
  40. package/src/components/mesh.tsx +92 -0
  41. package/src/components/node-props.ts +90 -0
  42. package/src/components/orbit-camera.tsx +75 -0
  43. package/src/components/perspective-camera.tsx +32 -0
  44. package/src/components/point-light.tsx +52 -0
  45. package/src/components/record-mesh.tsx +68 -0
  46. package/src/components/scene.tsx +281 -0
  47. package/src/components/spot-light.tsx +65 -0
  48. package/src/components/sprite.tsx +38 -0
  49. package/src/components/view3d.tsx +152 -0
  50. package/src/environment-bake.ts +492 -0
  51. package/src/environment.ts +325 -0
  52. package/src/first-person.ts +258 -0
  53. package/src/geometry.ts +6 -2
  54. package/src/glsl.ts +1343 -82
  55. package/src/gltf.ts +432 -112
  56. package/src/index.ts +61 -9
  57. package/src/input.ts +70 -0
  58. package/src/light.ts +383 -0
  59. package/src/material.ts +667 -232
  60. package/src/math.ts +110 -0
  61. package/src/mesh.ts +728 -0
  62. package/src/mixer.ts +476 -0
  63. package/src/model-file.ts +80 -15
  64. package/src/model.ts +204 -35
  65. package/src/node.ts +579 -0
  66. package/src/orbit.ts +292 -212
  67. package/src/scene-pointer.ts +294 -0
  68. package/src/scene-shadows.ts +471 -0
  69. package/src/scene.ts +1435 -1576
  70. package/tools/environment.ts +69 -0
  71. package/tools/model.ts +4 -3
  72. 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
+ }