@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,281 @@
1
+ import { createEffect, displayScale, getBoundingBoxViewport, getLayoutBox, onCleanup, onLayout, untrack } from "@solidrt/core"
2
+ import type { Element, ParentComponent, PointerFeed, TextureId } from "@solidrt/core"
3
+ import { SceneContext } from "./context.tsx"
4
+ import { createScene } from "../scene.ts"
5
+ import { feedPointer } from "../scene-pointer.ts"
6
+ import type { EnvironmentOptions, FogOptions, Scene as SceneHandle, SkyboxOptions, ToneMapping } from "../scene.ts"
7
+ import type { ScenePointerEvent, SceneTapEvent, SceneWheelEvent } from "../node.ts"
8
+ import type { CameraUpdate } from "../camera.ts"
9
+
10
+ /**
11
+ * The scene's own pointer events, the root of the walk: every event the
12
+ * nodes let through arrives here last - `event.mesh` is the hit it
13
+ * bubbled from, or null over empty space (a tap there with `mesh` null is
14
+ * "deselect"), `x`/`y` scene pixels. scene.listen as props; an
15
+ * `<OrbitCamera>` child's feed listens at the same root.
16
+ */
17
+ export type ScenePointerProps = {
18
+ onPointerDown?: (event: ScenePointerEvent) => void
19
+ onPointerMove?: (event: ScenePointerEvent) => void
20
+ onPointerUp?: (event: ScenePointerEvent) => void
21
+ onWheel?: (event: SceneWheelEvent) => void
22
+ onTap?: (event: SceneTapEvent) => void
23
+ }
24
+
25
+ export type SceneProps = ScenePointerProps & {
26
+ /**
27
+ * Target pixels - give both, or neither. Omitted, the scene FILLS: the
28
+ * built-in leaf is laid out at 100% of its parent's box (give it a sized
29
+ * parent, as on the web) and the target tracks the leaf's on-screen size
30
+ * in device pixels - display scale, designSize fits and ancestor
31
+ * transforms included - so a bare `<Scene>` renders at native density on
32
+ * any display, and viewport-relative camera controls scale with the box.
33
+ * Fill or fixed is decided at mount. `output` needs explicit sizes (the
34
+ * target cannot follow a leaf it does not own); the leaf's own
35
+ * width/height are then layout, so render size and display size separate
36
+ * (supersampling).
37
+ */
38
+ width?: number
39
+ height?: number
40
+ clearColor?: [number, number, number, number]
41
+ /**
42
+ * Drive the scene camera declaratively (scene.setCamera as a prop): a
43
+ * partial CameraUpdate, absent keys keep their values - `ortho` included,
44
+ * which `<PerspectiveCamera>` by its name never sets. The prop and the
45
+ * `<PerspectiveCamera>` child write the same scene state, so use one
46
+ * form, not both (last write wins). The 2d layers' `camera` prop, one
47
+ * dimension up.
48
+ */
49
+ camera?: CameraUpdate
50
+ /** The background drawn behind the meshes, inside the scene's own pass
51
+ * (scene.setBackground): fragment GLSL (vUV/iResolution/fragColor plus
52
+ * the vRay view ray, so a createShaderTexture backdrop ports verbatim
53
+ * and a directional sky is a few lines) or a skybox `{ cube, intensity?,
54
+ * rotation? }`. Reactive - swapping the source replaces the background,
55
+ * a skybox's knobs update in place; undefined removes it. Three's
56
+ * `scene.background = color` is `clearColor` here. */
57
+ background?: string | SkyboxOptions
58
+ /** The cube map reflective `lit` materials mirror (scene.setEnvironment):
59
+ * `{ cube, intensity?, rotation? }`, typically the skybox's own cube
60
+ * turned with it. Reactive; undefined removes it. */
61
+ environment?: EnvironmentOptions
62
+ /** Scene-wide fog (scene.setFog): linear `{ color, near, far }` or exp2
63
+ * `{ color, density }`, optionally thinning above `height` by
64
+ * `heightFalloff`; every standard material fades toward `color` by
65
+ * distance from the camera. Reactive; undefined removes it. Match
66
+ * `color` to clearColor or the background, which is not fogged. */
67
+ fog?: FogOptions
68
+ /** Output tone mapping (scene.setToneMapping): "none" (default) or
69
+ * "aces". Reactive. */
70
+ toneMapping?: ToneMapping
71
+ /** Output exposure (scene.setExposure), default 1. Reactive. */
72
+ exposure?: number
73
+ /** The scene target's layer mask (scene.setLayers as a prop; default 1):
74
+ * the scene draws the meshes whose `layers` intersect it. Reactive. */
75
+ layers?: number
76
+ /** `"texture"` exposes the target's depth as `scene.depthTexture` (a
77
+ * depth-reading post effect's input). Fixed at creation; not with
78
+ * `samples`. */
79
+ depth?: true | "texture"
80
+ label?: string
81
+ /** Multisample count (1, 2, 4 or 8; default 1): anti-aliased mesh edges.
82
+ * Fixed at creation. */
83
+ samples?: 1 | 2 | 4 | 8
84
+ ref?: (scene: SceneHandle) => void
85
+ /**
86
+ * Compose the output yourself: called once (untracked) with the scene's
87
+ * texture id, and its return renders in place of the built-in `<texture>`
88
+ * leaf - a `<d-texture>`, a leaf carrying paint/pointer/layout props, or
89
+ * a post-effect chain (a shader target sampling the id; created in the
90
+ * callback it disposes with the Scene). Return null to render no leaf.
91
+ * Pointer events then need the scene's handlers on your leaf:
92
+ * `<texture src={texture} {...useScene().scene.handlers} />` - one
93
+ * spread carries the node events, the scene's own and the pointer
94
+ * feed, which listens at the scene's root.
95
+ */
96
+ output?: (texture: TextureId) => Element
97
+ /**
98
+ * Pointer events (default on): the built-in leaf carries scene.handlers,
99
+ * so nodes, the scene's own handlers and a camera control's feed
100
+ * receive input. `false` detaches them - the leaf then costs no pointer
101
+ * routing at all.
102
+ */
103
+ events?: boolean
104
+ /**
105
+ * The pointer feed of the scene's leaf (createPointerFeed), fed from
106
+ * the scene's root - the events the nodes let through: a mesh that
107
+ * claims its press (stopPropagation on its down) keeps a camera control
108
+ * bound to the feed out of that drag. The feed's gestures - drag,
109
+ * pinch, two-finger pan, wheel, mouse motion under lock - come from
110
+ * this scene, and an input map binds them to the camera controls
111
+ * inside; `useScene().pointer` is this feed. Fixed at creation. Without
112
+ * one the leaf feeds no gestures and the controls move only through
113
+ * their handles.
114
+ */
115
+ pointer?: PointerFeed
116
+ }
117
+
118
+ /**
119
+ * Owns a draw target and composites it as an ordinary `<texture>` leaf, so
120
+ * the output takes layout, transforms, blendMode, and pointer events like
121
+ * any element - or hand `output` the texture id and compose it yourself.
122
+ * Children (Mesh/Group/PerspectiveCamera) render nothing themselves - they
123
+ * populate the retained scene through context.
124
+ */
125
+ // Creation size of a fill-mode target: the first onLayout replaces it
126
+ // before the first paint, so it only has to be a valid texture size.
127
+ const FILL_INITIAL_SIZE = 1
128
+
129
+ export let Scene: ParentComponent<SceneProps> = props => {
130
+ // Fill vs fixed target is decided at mount, like `output`: both
131
+ // width and height (fixed pixels) or neither (fill).
132
+ let fill = untrack(() => {
133
+ if ((props.width === undefined) !== (props.height === undefined)) {
134
+ throw new Error("Scene: width and height come together - give both (fixed target) or neither (fill)")
135
+ }
136
+ if (props.width === undefined && props.output) {
137
+ throw new Error("Scene: fill needs the built-in leaf - with output, give width and height")
138
+ }
139
+ return props.width === undefined
140
+ })
141
+ // The initial props seed createScene, so `ref` (below) hands out a
142
+ // configured scene and the first frame draws it whole; the effects
143
+ // after this follow changes only (`defer`).
144
+ let scene = untrack(() => {
145
+ let s = createScene(props.width ?? FILL_INITIAL_SIZE, props.height ?? FILL_INITIAL_SIZE, {
146
+ clearColor: props.clearColor,
147
+ label: props.label,
148
+ samples: props.samples,
149
+ depth: props.depth,
150
+ background: props.background,
151
+ environment: props.environment,
152
+ fog: props.fog,
153
+ toneMapping: props.toneMapping,
154
+ exposure: props.exposure,
155
+ layers: props.layers,
156
+ })
157
+ if (props.camera) s.setCamera(props.camera)
158
+ return s
159
+ })
160
+ createEffect(
161
+ () => [props.width, props.height] as const,
162
+ ([w, h]) => {
163
+ if ((w === undefined) !== (h === undefined) || (w === undefined) !== fill) {
164
+ throw new Error("Scene: fill/fixed is mount-fixed - width/height cannot appear or disappear")
165
+ }
166
+ if (!fill) scene.setSize(w!, h!)
167
+ },
168
+ )
169
+ createEffect(
170
+ () => props.camera,
171
+ camera => {
172
+ if (camera) scene.setCamera(camera)
173
+ },
174
+ { defer: true },
175
+ )
176
+ createEffect(
177
+ () => props.background,
178
+ b => scene.setBackground(b ?? null),
179
+ { defer: true },
180
+ )
181
+ createEffect(
182
+ () => props.environment,
183
+ e => scene.setEnvironment(e ?? null),
184
+ { defer: true },
185
+ )
186
+ createEffect(
187
+ () => props.fog,
188
+ f => scene.setFog(f ?? null),
189
+ { defer: true },
190
+ )
191
+ createEffect(
192
+ () => props.toneMapping,
193
+ t => scene.setToneMapping(t ?? "none"),
194
+ { defer: true },
195
+ )
196
+ createEffect(
197
+ () => props.exposure,
198
+ e => scene.setExposure(e ?? 1),
199
+ { defer: true },
200
+ )
201
+ createEffect(
202
+ () => props.layers,
203
+ l => scene.setLayers(l ?? 1),
204
+ { defer: true },
205
+ )
206
+ untrack(() => props.ref)?.(scene)
207
+ // The pointer feed the app handed in listens at the scene's root, and
208
+ // children reach it through useScene().pointer. Fixed at creation, like
209
+ // output.
210
+ let pointer = untrack(() => props.pointer) ?? null
211
+ if (pointer) onCleanup(feedPointer(scene, pointer))
212
+ // The scene's own handlers at the root of the walk; the props are read
213
+ // per event, so a handler prop may change without re-registering.
214
+ onCleanup(
215
+ scene.listen({
216
+ onPointerDown: e => props.onPointerDown?.(e),
217
+ onPointerMove: e => props.onPointerMove?.(e),
218
+ onPointerUp: e => props.onPointerUp?.(e),
219
+ onWheel: e => props.onWheel?.(e),
220
+ onTap: e => props.onTap?.(e),
221
+ }),
222
+ )
223
+ let output = untrack(() => props.output)
224
+ let events = untrack(() => props.events) !== false
225
+ let leafNode: { id: number } | undefined
226
+ // The built-in leaf's laid-out box in ITS units - what pointer
227
+ // localX/localY report in and every handlersFor scales against. Fixed
228
+ // mode lays the leaf out at the target size; fill mode reads the box
229
+ // back (getLayoutBox, the untransformed read, so a designSize fit or
230
+ // ancestor transform never skews events). Zero before the first layout:
231
+ // no event can arrive before one, and a degenerate layout passes
232
+ // coordinates through unscaled. A custom output leaf registers its own
233
+ // layout via handlersFor.
234
+ let builtinLayout = fill
235
+ ? () => (leafNode && getLayoutBox(leafNode)) || { width: 0, height: 0 }
236
+ : () => ({ width: props.width!, height: props.height! })
237
+ if (fill) {
238
+ // Fill sizing: after every layout (and on display-scale changes, which
239
+ // lay nothing out) the target follows the leaf's on-screen box in
240
+ // device pixels - getBoundingBoxViewport composes designSize fits and
241
+ // ancestor transforms, so the scene renders at true density wherever
242
+ // it sits. onLayout runs before paint (no frame draws at a stale
243
+ // size); setSize no-ops when nothing changed. apply runs as an
244
+ // onLayout handler and as an effect apply, both untracked scopes, so
245
+ // its scale read is wrapped in an explicit untrack (the effect's
246
+ // compute is what tracks it).
247
+ let apply = () =>
248
+ untrack(() => {
249
+ if (!leafNode) return
250
+ let box = getBoundingBoxViewport(leafNode)
251
+ if (!box) return
252
+ let scale = displayScale()
253
+ scene.setSize(Math.max(1, Math.round(box.width * scale)), Math.max(1, Math.round(box.height * scale)))
254
+ })
255
+ onLayout(apply)
256
+ createEffect(() => displayScale(), apply)
257
+ }
258
+ // Pointer events on the built-in leaf: at target size the plain
259
+ // handlers, in fill mode scaled from the laid-out box.
260
+ let sceneHandlers = fill ? scene.handlersFor(builtinLayout) : scene.handlers
261
+ return (
262
+ <SceneContext value={{ scene, parent: scene.root, viewport: scene, pointer }}>
263
+ {output ? (
264
+ untrack(() => output(scene.texture))
265
+ ) : (
266
+ <texture
267
+ ref={(n: { id: number }) => (leafNode = n)}
268
+ src={scene.texture}
269
+ width={fill ? "100%" : props.width}
270
+ height={fill ? "100%" : props.height}
271
+ onPointerDown={events ? sceneHandlers.onPointerDown : undefined}
272
+ onPointerMove={events ? sceneHandlers.onPointerMove : undefined}
273
+ onPointerUp={events ? sceneHandlers.onPointerUp : undefined}
274
+ onPointerLeave={events ? sceneHandlers.onPointerLeave : undefined}
275
+ onWheel={events ? sceneHandlers.onWheel : undefined}
276
+ />
277
+ )}
278
+ {props.children}
279
+ </SceneContext>
280
+ )
281
+ }
@@ -0,0 +1,65 @@
1
+ import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
2
+ import type { VoidComponent } from "@solidrt/core"
3
+ import { SceneContext } from "./context.tsx"
4
+ import { syncNode } from "./node-props.ts"
5
+ import type { TransformProps } from "./node-props.ts"
6
+ import { add, remove } from "../node.ts"
7
+ import { createSpotLight, setLight } from "../light.ts"
8
+ import type { SpotLight as SpotLightNode, SpotShadowOptions } from "../light.ts"
9
+ import type { Vec3 } from "../math.ts"
10
+
11
+ export type SpotLightProps = TransformProps & {
12
+ /** Aim direction in the node's local space; default [0, -1, 0], a
13
+ * lamp pointing straight down. */
14
+ direction?: Vec3
15
+ color?: Vec3
16
+ intensity?: number
17
+ /** Falloff cutoff in world units (0 = no cutoff). */
18
+ distance?: number
19
+ /** Cone half-angle in DEGREES, (0, 90]; default 60 (degrees like
20
+ * camera fov; Three's radians convert as `angle * 180 / PI`). */
21
+ angle?: number
22
+ /** 0..1 fraction of the cone fading to the rim; default 0. */
23
+ penumbra?: number
24
+ /** Falloff exponent; default 2 (inverse square). */
25
+ decay?: number
26
+ /** Render a shadow map from this light (a perspective map of its
27
+ * cone; one shadow slot). */
28
+ castShadow?: boolean
29
+ /** Shadow-map options (mapSize, bias, normalBias, near), merged key
30
+ * by key. */
31
+ shadow?: SpotShadowOptions
32
+ ref?: (light: SpotLightNode) => void
33
+ }
34
+
35
+ /** A spot light node (createSpotLight): a cone from the node's world
36
+ * position along its local `direction` - give it a `position`, and aim
37
+ * it with `direction` or a parent's rotation. Counts against MAX_LIGHTS
38
+ * with the other non-ambient lights. */
39
+ export let SpotLight: VoidComponent<SpotLightProps> = props => {
40
+ let ctx = useContext(SceneContext)
41
+ let light = untrack(() =>
42
+ createSpotLight({
43
+ direction: props.direction,
44
+ color: props.color,
45
+ intensity: props.intensity,
46
+ distance: props.distance,
47
+ angle: props.angle,
48
+ penumbra: props.penumbra,
49
+ decay: props.decay,
50
+ castShadow: props.castShadow,
51
+ shadow: props.shadow,
52
+ }),
53
+ )
54
+ add(ctx.parent, light)
55
+ syncNode(light, props)
56
+ createEffect(
57
+ () =>
58
+ [props.direction, props.color, props.intensity, props.distance, props.angle, props.penumbra, props.decay, props.castShadow, props.shadow] as const,
59
+ ([direction, color, intensity, distance, angle, penumbra, decay, castShadow, shadow]) =>
60
+ setLight(light, { direction, color, intensity, distance, angle, penumbra, decay, castShadow, shadow }),
61
+ )
62
+ untrack(() => props.ref)?.(light)
63
+ onCleanup(() => remove(light))
64
+ return null
65
+ }
@@ -0,0 +1,38 @@
1
+ import { onCleanup, untrack, useContext } from "@solidrt/core"
2
+ import type { VoidComponent } from "@solidrt/core"
3
+ import { SceneContext } from "./context.tsx"
4
+ import type { TransformProps, PointerEventProps } from "./node-props.ts"
5
+ import { syncMesh } from "./mesh.tsx"
6
+ import { add, remove } from "../node.ts"
7
+ import { createSprite } from "../mesh.ts"
8
+ import type { Mesh as MeshNode } from "../mesh.ts"
9
+ import type { ShaderParams } from "@solidrt/core/gpu"
10
+ import type { Material } from "../material.ts"
11
+
12
+ export type SpriteProps = TransformProps & PointerEventProps & {
13
+ /** A `sprite()` material (any material draws, only a sprite one turns). */
14
+ material: Material
15
+ /** Per-mesh uniforms, merge semantics - as on Mesh. */
16
+ params?: ShaderParams
17
+ /** Explicit draw-order key (setRenderOrder as a prop); default 0. */
18
+ renderOrder?: number
19
+ /** Layer membership bitmask (setLayers as a prop; default 1). */
20
+ layers?: number
21
+ /** Frustum culling switch (setCulling as a prop; default true). */
22
+ frustumCulled?: boolean
23
+ /** World units the culled box grows by (setCulling as a prop; default 0). */
24
+ cullMargin?: number
25
+ ref?: (mesh: MeshNode) => void
26
+ }
27
+
28
+ /** A camera-facing unit quad (createSprite as a component): no geometry
29
+ * prop, `scale` is its world size, rotation is ignored. */
30
+ export let Sprite: VoidComponent<SpriteProps> = props => {
31
+ let ctx = useContext(SceneContext)
32
+ let mesh = untrack(() => createSprite(props.material))
33
+ add(ctx.parent, mesh)
34
+ syncMesh(mesh, props)
35
+ untrack(() => props.ref)?.(mesh)
36
+ onCleanup(() => remove(mesh))
37
+ return null
38
+ }
@@ -0,0 +1,152 @@
1
+ import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
2
+ import type { Element, ParentComponent, PointerFeed, TextureId } from "@solidrt/core"
3
+ import { SceneContext } from "./context.tsx"
4
+ import { feedPointer } from "../scene-pointer.ts"
5
+ import type { ViewHandle, ViewOptions } from "../scene.ts"
6
+ import type { CameraUpdate } from "../camera.ts"
7
+ import type { ScenePointerProps } from "./scene.tsx"
8
+
9
+ export type View3dProps = Pick<ViewOptions, "clearColor" | "label" | "overrideMaterial" | "fog" | "depth" | "samples" | "filter" | "wrap" | "into"> &
10
+ ScenePointerProps & {
11
+ /** Target pixels, live: the view resizes, or its tile moves. Fixed
12
+ * only, for now: a fill mode like Scene's is a later, additive step. */
13
+ width: number
14
+ height: number
15
+ /** The tile's top-left in `into` (view.setRect as props), live; without
16
+ * `into` they mean nothing. */
17
+ x?: number
18
+ y?: number
19
+ /** The view's layer mask (view.setLayers as a prop; default 1). Live. */
20
+ layers?: number
21
+ /** Partial camera update on the view's own camera (view.setCamera):
22
+ * absent keys keep their values. Live. The `<PerspectiveCamera>` child
23
+ * writes the same state - use one form, not both. */
24
+ camera?: CameraUpdate
25
+ /**
26
+ * Compose the leaf yourself: called once (untracked) with view.texture,
27
+ * and its return renders in place of the built-in `<texture>` leaf.
28
+ * Pointer events through the view then need its handlers on your leaf:
29
+ * `<texture src={texture} {...view.handlers} />` (the view from `ref`);
30
+ * the view's pointer feed listens at its root. A tiled view's id is a
31
+ * draw target, not a texture: show `into` with srcX/srcY, as the
32
+ * built-in leaf does.
33
+ */
34
+ output?: (texture: TextureId) => Element
35
+ /** Pointer events (default on): the built-in leaf carries the view's
36
+ * handlers. `false` detaches them. */
37
+ events?: boolean
38
+ /** The pointer feed of this view's leaf (see Scene's `pointer`), fed
39
+ * from the view's root - the events the nodes let through - so a map
40
+ * bound to it drives the camera controls inside this view and no other;
41
+ * inside, `useScene().pointer` is this feed. Fixed at creation. */
42
+ pointer?: PointerFeed
43
+ ref?: (view: ViewHandle) => void
44
+ }
45
+
46
+ /**
47
+ * A second rendering of the enclosing scene from a camera of its own
48
+ * (scene.createView as a component): the same meshes, materials, lights
49
+ * and shadow maps, another target and one entry per mesh, so it costs no
50
+ * per-frame JS. Composites as an ordinary `<texture>` leaf at the target
51
+ * size (a tile of `into` shown through srcX/srcY), or through `output`.
52
+ * Camera-control children (`<OrbitCamera>`, `<FirstPersonCamera>`,
53
+ * `<PerspectiveCamera>`) drive the VIEW's camera: inside, `useScene()`
54
+ * reports the view as `viewport` and the view leaf's feed as `pointer`,
55
+ * so a map over that feed moves this view alone. Node children (`<Mesh>`) mount
56
+ * to the scene as they would outside - a view mirrors the scene's meshes,
57
+ * it has none of its own. Nodes under the view's leaf get their ordinary
58
+ * pointer handlers, picked through the view's camera; the view's own
59
+ * `onPointer*`/`onWheel`/`onTap` are the last stop of that walk
60
+ * (`event.mesh` null over empty space).
61
+ */
62
+ export let View3d: ParentComponent<View3dProps> = props => {
63
+ let ctx = useContext(SceneContext)
64
+ // A tile of `into` or a target of its own is decided at mount, like
65
+ // Scene's fill/fixed: `into` cannot appear or disappear.
66
+ let tiled = untrack(() => props.into !== undefined)
67
+ // The initial props seed createView, so `ref` hands out a configured
68
+ // view and the first frame draws it whole; the effects follow changes
69
+ // only (`defer`).
70
+ let view = untrack(() => {
71
+ let v = ctx.scene.createView({
72
+ width: props.width,
73
+ height: props.height,
74
+ x: props.x,
75
+ y: props.y,
76
+ layers: props.layers,
77
+ clearColor: props.clearColor,
78
+ label: props.label,
79
+ overrideMaterial: props.overrideMaterial,
80
+ fog: props.fog,
81
+ depth: props.depth,
82
+ samples: props.samples,
83
+ filter: props.filter,
84
+ wrap: props.wrap,
85
+ into: props.into,
86
+ })
87
+ if (props.camera) v.setCamera(props.camera)
88
+ return v
89
+ })
90
+ createEffect(
91
+ () => props.camera,
92
+ camera => {
93
+ if (camera) view.setCamera(camera)
94
+ },
95
+ { defer: true },
96
+ )
97
+ createEffect(
98
+ () => [props.x, props.y, props.width, props.height] as const,
99
+ ([x, y, width, height]) => {
100
+ if (tiled) view.setRect({ x: x ?? 0, y: y ?? 0, width, height })
101
+ else view.setSize(width, height)
102
+ },
103
+ { defer: true },
104
+ )
105
+ createEffect(
106
+ () => props.layers,
107
+ l => view.setLayers(l ?? 1),
108
+ { defer: true },
109
+ )
110
+ untrack(() => props.ref)?.(view)
111
+ onCleanup(() => view.dispose())
112
+ // The pointer feed the app handed in (see Scene) listens at the view's
113
+ // root; children reach it through useScene().pointer.
114
+ let pointer = untrack(() => props.pointer) ?? null
115
+ if (pointer) onCleanup(feedPointer(view, pointer))
116
+ // The view's own handlers at the root of the walk; the props are read
117
+ // per event, so a handler prop may change without re-registering.
118
+ onCleanup(
119
+ view.listen({
120
+ onPointerDown: e => props.onPointerDown?.(e),
121
+ onPointerMove: e => props.onPointerMove?.(e),
122
+ onPointerUp: e => props.onPointerUp?.(e),
123
+ onWheel: e => props.onWheel?.(e),
124
+ onTap: e => props.onTap?.(e),
125
+ }),
126
+ )
127
+ let output = untrack(() => props.output)
128
+ let events = untrack(() => props.events) !== false
129
+ return (
130
+ <SceneContext value={{ scene: ctx.scene, parent: ctx.parent, viewport: view, pointer }}>
131
+ {output ? (
132
+ untrack(() => output(view.texture))
133
+ ) : (
134
+ <texture
135
+ src={tiled ? props.into : view.texture}
136
+ width={props.width}
137
+ height={props.height}
138
+ srcX={tiled ? (props.x ?? 0) : undefined}
139
+ srcY={tiled ? (props.y ?? 0) : undefined}
140
+ srcW={tiled ? props.width : undefined}
141
+ srcH={tiled ? props.height : undefined}
142
+ onPointerDown={events ? view.handlers.onPointerDown : undefined}
143
+ onPointerMove={events ? view.handlers.onPointerMove : undefined}
144
+ onPointerUp={events ? view.handlers.onPointerUp : undefined}
145
+ onPointerLeave={events ? view.handlers.onPointerLeave : undefined}
146
+ onWheel={events ? view.handlers.onWheel : undefined}
147
+ />
148
+ )}
149
+ {props.children}
150
+ </SceneContext>
151
+ )
152
+ }