@solidrt/3d 0.0.71 → 0.0.73

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 CHANGED
@@ -301,7 +301,10 @@ shadow })`, `setLight`) makes the scene own an internal
301
301
  `createView({ depth: "texture", overrideMaterial: depth pass })` drawing
302
302
  the `castShadow` meshes (`<Mesh castShadow>`, `setCastShadow`) from an
303
303
  orthographic camera at the light's WORLD position along its world
304
- direction, `shadow.camera` (+-5, 0.5..500 by default) as the frustum.
304
+ direction, `shadow.camera` (+-5, 0.5..500 by default) as the frustum,
305
+ re-placed whenever the light's world matrix moves - by a JS write or
306
+ by native motion (a transition on the light or an ancestor, a clip
307
+ player), in the frame the lighting moves.
305
308
  Any light may cast, bounded by the shadow-slot budget
306
309
  (`MAX_SHADOW_MAPS` = 8, its own constant, exported from the root and
307
310
  `/glsl` with `MAX_LIGHTS` and `MAX_CASCADES`: a directional light claims
@@ -382,7 +385,18 @@ RETARGETED motion is native: `setTransition(node, { position:
382
385
  animates toward every frame (position/scale per lane, rotation along
383
386
  the quaternion geodesic - a spring keeps its velocity through
384
387
  retargets), so a mesh gliding to a slot or a camera rig easing costs
385
- one JS write per target change, zero per frame. The declaration lives
388
+ one JS write per target change, zero per frame - for the mesh, its
389
+ lighting and its shadow alike (a casting light on a transitioned group
390
+ re-places its shadow cameras in the scene's sync, which runs every
391
+ frame). One rule for every native mover, transitions and clip players
392
+ both: the core steps running motion BEFORE the frame's callbacks and
393
+ starts what they declared (an enter from its `from`, an exit) AFTER
394
+ them and their flush - Unity's animation update between Update and
395
+ LateUpdate - so `worldPosition`, `getTransform` and a late-pass follow
396
+ read the pose the frame draws (a node created in a frame's callback
397
+ reads its created pose there and its `from` from the late pass on),
398
+ and a raw `setTransform` made in the frame wins that frame (the next
399
+ step overwrites it again - the producer rule, last write wins). The declaration lives
386
400
  on the SceneNode and re-applies on every scene enter; the pose a node
387
401
  enters with snaps, unless a component's `from` (`position: { duration,
388
402
  from: [x, y, z] }`, a quaternion for `rotation`) animates it in from
@@ -418,6 +432,27 @@ a cancel, snap, leave or exit; the raw "spatialTransitionEnd" engine
418
432
  event (sol:events, carrying the CORE node id `_node`) stays for
419
433
  flux:spatial consumers.
420
434
 
435
+ PAUSE and time scale are a node declaration too: `setTimeScale(node,
436
+ scale)` / the `timeScale` prop sets the rate the native motion under the
437
+ node runs at - transitions with their delays, enters, exits and stagger
438
+ slots, morph weight tracks, and the clip players of the models under it
439
+ (a mixer's players are clocked on their model's node, so this is Three's
440
+ `mixer.timeScale` as well). 0 freezes, 1 is app time, 0.5 is slow
441
+ motion; `scene.setTimeScale` / `<Scene timeScale>` declares it on
442
+ `scene.root`, the whole-world pause. The NEAREST declaring ancestor
443
+ wins, never a product (Godot's process_mode shape): a world at 0 with a
444
+ `<Group timeScale={1}>` inside keeps that group running - the spinning
445
+ preview on a pause screen, which a product could not express. A frozen
446
+ scene asks for no frames (the loop idles; the write that resumes it
447
+ brings the frame), writes while frozen behave as always (undeclared
448
+ components snap, declared ones retarget and wait), a `destroy` with an
449
+ `exit` stays a ghost until its time moves again (a scene `dispose`
450
+ still cuts it short), and a node created frozen sits at its `from`. The
451
+ scale covers the CORE's motion only: your own `onFrame` logic, timers,
452
+ video and audio stay on app time - gate them yourself, multiplying your
453
+ step by `timeRate(node)` / `scene.timeRate()`, the effective rate read.
454
+ The dev clock (`/clock?scale=`) scales app time underneath all of this.
455
+
421
456
  ### Geometry layout, indices and topology
422
457
 
423
458
  One interleaved vertex buffer per geometry, described by an open layout
@@ -1098,11 +1133,14 @@ the root as it crosses meshes; a drag from a mesh keeps naming it, with
1098
1133
  `point`/`distance` null while the ray misses it). Enter/leave fire on
1099
1134
  the struck node alone - a group never receives them - while the root
1100
1135
  sees every move, so "hovering empty space" is a root move with `mesh`
1101
- null. Taps are synthesized by the dispatch (DOM click, Unity's click
1102
- handler): `onTap` fires after the up when the press released on the
1103
- target it pressed within the slop (8 window px, core's recognizer slop,
1104
- so a press is never both a tap and a drag), was the only pointer down
1105
- for its whole press (a pinch never taps), with `tapCount` counting
1136
+ null. Taps are synthesized by the dispatch, as DOM click (Unity's click
1137
+ handler fires for every button; ours, like DOM's, for the primary alone):
1138
+ `onTap` fires after the up when a PRIMARY-button press (a right click
1139
+ walks its down and up with `button` 2 but never taps, like core's press;
1140
+ read `button` on the down to act on it) released on the target it
1141
+ pressed within the slop (8 window px, core's recognizer slop, so a press
1142
+ is never both a tap and a drag), was the only pointer down for its whole
1143
+ press (a pinch never taps), with `tapCount` counting
1106
1144
  repeats within 300 ms and 20 px on the same target (a double tap is
1107
1145
  `tapCount === 2`); a release on another instance of the same mesh is no
1108
1146
  tap. Wheel walks like a move with `deltaX/deltaY` (a node stopping it
@@ -1322,7 +1360,7 @@ uint16/uint32 indices by vertex count automatically.
1322
1360
 
1323
1361
  #### unlit
1324
1362
 
1325
- `unlit({ color?, map?, vertexColors?, transparent?, blend?, cull?, alphaTest?, fog? })` -
1363
+ `unlit({ color?, map?, vertexColors?, transparent?, blend?, cull?, alphaTest?, fog?, prelude?, surface? })` -
1326
1364
  straight `[r, g, b, a?]` 0..1 sRGB (decoded to linear light, see Color
1327
1365
  below), premultiplied internally; `vertexColors: true` multiplies by the
1328
1366
  "colored" layout's aColor (the geometry must carry it: withColors, a
@@ -1331,12 +1369,16 @@ gridHelper or axesHelper); `blend` the factors of a transparent draw,
1331
1369
  `transparent` unless told `transparent: false`, the shaderMaterial
1332
1370
  rule); `cull` and
1333
1371
  `alphaTest` as on phong (a mapped cutout casts its cutout); `fog: false`
1334
- opts out of the scene's fog (all four library materials take it).
1372
+ opts out of the scene's fog (all four library materials take it);
1373
+ `prelude`/`surface` the tier-2 slot every stock material takes (Custom
1374
+ looks below): `void surface(inout Surface s)` called with the base
1375
+ resolved, `s.base` the only field the unlit program reads back, a
1376
+ discard casting through the shadow twin like phong's.
1335
1377
 
1336
1378
  #### sprite
1337
1379
 
1338
1380
  `sprite({ color?, map?, transparent?, blend?, billboard?, shape?,
1339
- falloff?, minScreenPx?, maxScreenPx? })` - unlit on a quad
1381
+ falloff?, minScreenPx?, maxScreenPx?, prelude?, surface? })` - unlit on a quad
1340
1382
  that turns to face the camera IN THE VERTEX STAGE (off the shared
1341
1383
  uCamRight/uCamUp, or uCamPos for `billboard: "fixed-y"`, which yaws
1342
1384
  only and stays upright on world y - Godot's BILLBOARD_FIXED_Y, the
@@ -1996,7 +2038,14 @@ same constants - customizing never means leaving the system.
1996
2038
 
1997
2039
  A custom look is a citizen of the scene - lit by its lights, shadowed
1998
2040
  and fogged like the stock materials, exposed and tone mapped by the same
1999
- resolve - at one of three tiers, top first:
2041
+ resolve - at one of three tiers, top first. The same three tiers, the
2042
+ same words (`unlit`, `prelude`/`surface`, `shaderMaterialClass`,
2043
+ `instanceBuffers`/`instanceStyle`/`setInstanceStyle`) are
2044
+ `@solidrt/2d`'s, where the material sits on the LAYER (the layer is the
2045
+ batch, as the mesh is the draw entry here); the one deliberate
2046
+ asymmetry is that a 3d `blend` implies `transparent` and the scene's
2047
+ back-to-front sort, while a 2d layer has no depth and draws in record
2048
+ order (okf/plans/materials-one-model.md).
2000
2049
 
2001
2050
  1. STANDARD FRAGMENT, CUSTOM VERTEX. Any vertex stage that writes the lit
2002
2051
  varyings (vWorldPos, vNormal, vUv, plus vColor with `vertexColors`,
@@ -2014,7 +2063,10 @@ resolve - at one of three tiers, top first:
2014
2063
  is the white starting point (the stock materials seed exactly this
2015
2064
  from their `color` option).
2016
2065
  2. A SURFACE FUNCTION inside the stock material. `phong({ ..., prelude,
2017
- surface })` / `standard({ ..., prelude, surface })`: every other
2066
+ surface })` / `standard({ ..., prelude, surface })`, and the same two
2067
+ slots on `unlit` and `sprite` (the unlit program shades nothing, so
2068
+ `s.base` is the one field read back, after the cutout and the
2069
+ sprite's radial falloff): every other
2018
2070
  option seeds as usual (`color`, the maps, metalness and roughness),
2019
2071
  `prelude` is file scope (uniforms and helpers; a uniform it declares
2020
2072
  is an ordinary per-entry param, `params` on the mesh or
@@ -2616,7 +2668,11 @@ sample, weight-blend and write joint TRS natively each frame, so a
2616
2668
  playing character costs zero JS per frame (gate other work on
2617
2669
  `mixer.playing()`). play() requires the model to be IN a scene (players
2618
2670
  bind live arena nodes; removing the model drops them - a re-added model
2619
- plays again from play()). Three traps that follow from core ownership:
2671
+ plays again from play()). The players run on the model node's time:
2672
+ `setTimeScale(model, 0)` (or on any ancestor, the scene root included)
2673
+ pauses every clip and crossfade of the model where it stands and resumes
2674
+ it from there - Three's `mixer.timeScale`, Unity's `Animator.speed`
2675
+ under `Time.timeScale` (see Retargeted motion). Three traps that follow from core ownership:
2620
2676
  (1) players advance BEFORE your onFrame, which is therefore the
2621
2677
  post-animation hook - read a freshly posed joint and overwrite it
2622
2678
  (root-motion strips, skeleton copies) in plain setTransform, last write
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/3d",
3
- "version": "0.0.71",
3
+ "version": "0.0.73",
4
4
  "license": "MIT",
5
5
  "funding": "https://github.com/sponsors/wellawaretech",
6
6
  "author": "Antoine van Wel",
@@ -20,10 +20,10 @@
20
20
  "AGENTS.md"
21
21
  ],
22
22
  "devDependencies": {
23
- "@solidrt/test": "0.0.71"
23
+ "@solidrt/test": "0.0.73"
24
24
  },
25
25
  "peerDependencies": {
26
- "@solidjs/signals": "2.0.0-rc.13",
27
- "@solidrt/core": "0.0.71"
26
+ "@solidjs/signals": "2.0.0-rc.14",
27
+ "@solidrt/core": "0.0.73"
28
28
  }
29
29
  }
@@ -1,4 +1,4 @@
1
- import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
1
+ import { callRef, createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
2
2
  import type { VoidComponent } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import { syncNode } from "./node-props.ts"
@@ -46,7 +46,7 @@ export let DirectionalLight: VoidComponent<DirectionalLightProps> = props => {
46
46
  () => [props.direction, props.color, props.intensity, props.layers, props.castShadow, props.shadow] as const,
47
47
  ([direction, color, intensity, layers, castShadow, shadow]) => setLight(light, { direction, color, intensity, layers, castShadow, shadow }),
48
48
  )
49
- untrack(() => props.ref)?.(light)
49
+ callRef(() => props.ref, light)
50
50
  onCleanup(() => destroy(light))
51
51
  return null
52
52
  }
@@ -1,4 +1,4 @@
1
- import { createEffect, merge, onFrame, untrack, useContext } from "@solidrt/core"
1
+ import { callRef, createEffect, merge, onFrame, untrack, useContext } from "@solidrt/core"
2
2
  import type { InputMap, VoidComponent } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import { createFirstPersonCamera } from "../first-person.ts"
@@ -67,6 +67,6 @@ export let FirstPersonCamera: VoidComponent<FirstPersonCameraProps> = props => {
67
67
  () => untrack(() => camera.set({})),
68
68
  { defer: true },
69
69
  )
70
- untrack(() => props.ref)?.(camera)
70
+ callRef(() => props.ref, camera)
71
71
  return null
72
72
  }
@@ -1,4 +1,4 @@
1
- import { onCleanup, untrack, useContext } from "@solidrt/core"
1
+ import { callRef, onCleanup, useContext } from "@solidrt/core"
2
2
  import type { ParentComponent } from "@solidrt/core"
3
3
  import { SceneContext, provide } from "./context.tsx"
4
4
  import { syncNode } from "./node-props.ts"
@@ -16,7 +16,7 @@ export let Group: ParentComponent<GroupProps> = props => {
16
16
  let node = createGroup()
17
17
  add(ctx.parent, node)
18
18
  syncNode(node, props)
19
- untrack(() => props.ref)?.(node)
19
+ callRef(() => props.ref, node)
20
20
  onCleanup(() => destroy(node))
21
21
  return provide(ctx, node, props)
22
22
  }
@@ -1,4 +1,4 @@
1
- import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
1
+ import { callRef, createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
2
2
  import type { VoidComponent } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import { add, destroy } from "../node.ts"
@@ -26,7 +26,7 @@ export let HemisphereLight: VoidComponent<HemisphereLightProps> = props => {
26
26
  () => [props.sky, props.ground, props.intensity, props.layers] as const,
27
27
  ([sky, ground, intensity, layers]) => setLight(light, { sky, ground, intensity, layers }),
28
28
  )
29
- untrack(() => props.ref)?.(light)
29
+ callRef(() => props.ref, light)
30
30
  onCleanup(() => destroy(light))
31
31
  return null
32
32
  }
@@ -1,4 +1,4 @@
1
- import { createContext, createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
1
+ import { callRef, createContext, createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
2
2
  import type { ParentComponent } from "@solidrt/core"
3
3
  import { SceneContext, provide } from "./context.tsx"
4
4
  import { syncNode } from "./node-props.ts"
@@ -80,7 +80,7 @@ export let InstancedMesh: ParentComponent<InstancedMeshProps> = props => {
80
80
  c => setCastShadow(mesh, c === true),
81
81
  )
82
82
  syncMesh(mesh, props)
83
- untrack(() => props.ref)?.(mesh)
83
+ callRef(() => props.ref, mesh)
84
84
  onCleanup(() => {
85
85
  destroy(mesh)
86
86
  disposeInstances(mesh)
@@ -140,7 +140,7 @@ export let Instance: ParentComponent<InstanceProps> = props => {
140
140
  if (w !== undefined) setMorphWeights(instance, w)
141
141
  },
142
142
  )
143
- untrack(() => props.ref)?.(instance)
143
+ callRef(() => props.ref, instance)
144
144
  onCleanup(() => destroy(instance))
145
145
  return provide(ctx, instance, props)
146
146
  }
@@ -1,4 +1,4 @@
1
- import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
1
+ import { callRef, createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
2
2
  import type { ParentComponent } from "@solidrt/core"
3
3
  import { LodContext, SceneContext, provide } from "./context.tsx"
4
4
  import type { LodRegistry } from "./context.tsx"
@@ -54,7 +54,7 @@ export let Lod: ParentComponent<LodProps> = props => {
54
54
  () => declare(),
55
55
  { defer: true },
56
56
  )
57
- untrack(() => props.ref)?.(node)
57
+ callRef(() => props.ref, node)
58
58
  onCleanup(() => destroy(node))
59
59
  return <LodContext value={registry}>{provide(ctx, node, props)}</LodContext>
60
60
  }
@@ -91,7 +91,7 @@ export let InstancedLod: ParentComponent<InstancedLodProps> = props => {
91
91
  c => setCastShadow(mesh, c === true),
92
92
  )
93
93
  syncMesh(mesh, props)
94
- untrack(() => props.ref)?.(mesh)
94
+ callRef(() => props.ref, mesh)
95
95
  onCleanup(() => {
96
96
  destroy(mesh)
97
97
  disposeInstances(mesh)
@@ -1,4 +1,4 @@
1
- import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
1
+ import { callRef, createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
2
2
  import type { VoidComponent } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import { syncNode } from "./node-props.ts"
@@ -110,7 +110,7 @@ export let Mesh: VoidComponent<MeshProps> = props => {
110
110
  },
111
111
  )
112
112
  syncMesh(mesh, props)
113
- untrack(() => props.ref)?.(mesh)
113
+ callRef(() => props.ref, mesh)
114
114
  onCleanup(() => destroy(mesh))
115
115
  return null
116
116
  }
@@ -1,6 +1,6 @@
1
1
  import { createEffect, onCleanup, useContext } from "@solidrt/core"
2
2
  import { LodContext } from "./context.tsx"
3
- import { setTransform, setTransition, setVisible } from "../node.ts"
3
+ import { setTimeScale, setTransform, setTransition, setVisible } from "../node.ts"
4
4
  import type { HoverHandlers, NodePointerEvent, NodeTapEvent, NodeWheelEvent, SceneNode, TransitionEndEvent } from "../node.ts"
5
5
  import type { NodeTransition } from "flux:spatial"
6
6
  import type { Quat, Vec3 } from "../math.ts"
@@ -19,6 +19,10 @@ export type TransformProps = {
19
19
  transition?: NodeTransition | string | null
20
20
  /** A declared transition settled on one component. */
21
21
  onTransitionEnd?: (event: TransitionEndEvent) => void
22
+ /** The rate the native motion under this node runs at (see
23
+ * setTimeScale): 0 freezes the subtree, 1 is app time; the nearest
24
+ * declaring ancestor wins. Undefined or null inherits. */
25
+ timeScale?: number | null
22
26
  /** Under a `<Lod>`: this node is a level of it, drawn while the group's
23
27
  * projected size is at or above this and below the next larger level's
24
28
  * (see createLod). Only on a direct child of `<Lod>`; throws elsewhere. */
@@ -104,6 +108,14 @@ export function syncNode(node: SceneNode & HoverHandlers, props: TransformProps
104
108
  () => props.transition,
105
109
  transition => setTransition(node, transition ?? null),
106
110
  )
111
+ // Built only when the prop is present (the compiler emits the getter
112
+ // only then), so a plain node pays no effect for it.
113
+ if ("timeScale" in props) {
114
+ createEffect(
115
+ () => props.timeScale,
116
+ scale => setTimeScale(node, scale ?? null),
117
+ )
118
+ }
107
119
  // A level of the enclosing <Lod>, keyed by size; built only when the
108
120
  // prop is present (the compiler emits the getter only then), so a plain
109
121
  // node pays nothing.
@@ -1,4 +1,4 @@
1
- import { createEffect, merge, onFrame, untrack, useContext } from "@solidrt/core"
1
+ import { callRef, createEffect, merge, onFrame, untrack, useContext } from "@solidrt/core"
2
2
  import type { InputMap, VoidComponent } from "@solidrt/core"
3
3
  import type { Vec2 } from "@solidrt/core/input"
4
4
  import { SceneContext } from "./context.tsx"
@@ -158,6 +158,6 @@ export let OrbitCamera: VoidComponent<OrbitCameraProps> = props => {
158
158
  () => untrack(() => orbit.set({})),
159
159
  { defer: true },
160
160
  )
161
- untrack(() => props.ref)?.(orbit)
161
+ callRef(() => props.ref, orbit)
162
162
  return null
163
163
  }
@@ -1,4 +1,4 @@
1
- import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
1
+ import { callRef, createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
2
2
  import type { VoidComponent } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import { syncNode } from "./node-props.ts"
@@ -50,7 +50,7 @@ export let PointLight: VoidComponent<PointLightProps> = props => {
50
50
  () => [props.color, props.intensity, props.layers, props.distance, props.decay, props.castShadow, props.shadow] as const,
51
51
  ([color, intensity, layers, distance, decay, castShadow, shadow]) => setLight(light, { color, intensity, layers, distance, decay, castShadow, shadow }),
52
52
  )
53
- untrack(() => props.ref)?.(light)
53
+ callRef(() => props.ref, light)
54
54
  onCleanup(() => destroy(light))
55
55
  return null
56
56
  }
@@ -1,4 +1,4 @@
1
- import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
1
+ import { callRef, createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
2
2
  import type { VoidComponent } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import { syncMesh } from "./mesh.tsx"
@@ -76,7 +76,7 @@ export let RecordMesh: VoidComponent<RecordMeshProps> = props => {
76
76
  c => setCastShadow(mesh, c === true),
77
77
  )
78
78
  syncMesh(mesh, props)
79
- untrack(() => props.ref)?.(mesh)
79
+ callRef(() => props.ref, mesh)
80
80
  onCleanup(() => {
81
81
  destroy(mesh)
82
82
  disposeInstances(mesh)
@@ -1,4 +1,4 @@
1
- import { createEffect, createRenderEffect, createSignal, displayScale, getBoundingBoxViewport, getLayoutBox, onCleanup, onLayout, untrack, warnOnce } from "@solidrt/core"
1
+ import { callRef, createEffect, createRenderEffect, createSignal, displayScale, getBoundingBoxViewport, getLayoutBox, onCleanup, onLayout, untrack, warnOnce } from "@solidrt/core"
2
2
  import type { Element, ParentComponent, PointerFeed, TextureId } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import { createScene } from "../scene.ts"
@@ -65,6 +65,11 @@ export type SceneProps = ScenePointerProps & {
65
65
  * stagger`, the whole-scene form of `<Group transition={{ stagger }}>`
66
66
  * (see SceneOptions.stagger). Live (scene.setStagger). */
67
67
  stagger?: number
68
+ /** The rate the scene's native motion runs at (scene.setTimeScale as a
69
+ * prop): 0 pauses the world while the app's UI keeps moving, 1 is app
70
+ * time; a `<Group timeScale={1}>` inside keeps running. Undefined or
71
+ * null is app time. Live. */
72
+ timeScale?: number | null
68
73
  /** Scene-wide fog (scene.setFog): linear `{ color, near, far }` or exp2
69
74
  * `{ color, density }`, optionally thinning above `height` by
70
75
  * `heightFalloff`; every standard material fades toward `color` by
@@ -187,6 +192,7 @@ export let Scene: ParentComponent<SceneProps> = props => {
187
192
  bloom: props.bloom,
188
193
  layers: props.layers,
189
194
  stagger: props.stagger,
195
+ timeScale: props.timeScale ?? undefined,
190
196
  onError: e => {
191
197
  pendingError = e
192
198
  setErrorTick(errorTick() + 1)
@@ -261,7 +267,12 @@ export let Scene: ParentComponent<SceneProps> = props => {
261
267
  ms => scene.setStagger(ms ?? null),
262
268
  { defer: true },
263
269
  )
264
- untrack(() => props.ref)?.(scene)
270
+ createEffect(
271
+ () => props.timeScale,
272
+ scale => scene.setTimeScale(scale ?? null),
273
+ { defer: true },
274
+ )
275
+ callRef(() => props.ref, scene)
265
276
  // The pointer feed the app handed in listens at the scene's root, and
266
277
  // children reach it through useScene().pointer. Fixed at creation, like
267
278
  // output.
@@ -1,4 +1,4 @@
1
- import { createContext, createEffect, onCleanup, onFrame, untrack, useContext } from "@solidrt/core"
1
+ import { callRef, createContext, createEffect, onCleanup, onFrame, untrack, useContext } from "@solidrt/core"
2
2
  import type { Element, ParentComponent } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import { createShots } from "../shots.ts"
@@ -60,7 +60,7 @@ export let Shots: ParentComponent<ShotsProps> = props => {
60
60
  })
61
61
  },
62
62
  )
63
- untrack(() => props.ref)?.(shots)
63
+ callRef(() => props.ref, shots)
64
64
  return <ShotsContext value={shots}>{props.children}</ShotsContext>
65
65
  }
66
66
 
@@ -1,4 +1,4 @@
1
- import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
1
+ import { callRef, createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
2
2
  import type { VoidComponent } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import { syncMesh } from "./mesh.tsx"
@@ -41,7 +41,7 @@ export let SplatMesh: VoidComponent<SplatMeshProps> = props => {
41
41
  { defer: true },
42
42
  )
43
43
  syncMesh(mesh, props)
44
- untrack(() => props.ref)?.(mesh)
44
+ callRef(() => props.ref, mesh)
45
45
  onCleanup(() => {
46
46
  destroy(mesh)
47
47
  disposeInstances(mesh)
@@ -1,4 +1,4 @@
1
- import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
1
+ import { callRef, createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
2
2
  import type { VoidComponent } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import { syncNode } from "./node-props.ts"
@@ -63,7 +63,7 @@ export let SpotLight: VoidComponent<SpotLightProps> = props => {
63
63
  ([direction, color, intensity, layers, distance, angle, penumbra, decay, castShadow, shadow]) =>
64
64
  setLight(light, { direction, color, intensity, layers, distance, angle, penumbra, decay, castShadow, shadow }),
65
65
  )
66
- untrack(() => props.ref)?.(light)
66
+ callRef(() => props.ref, light)
67
67
  onCleanup(() => destroy(light))
68
68
  return null
69
69
  }
@@ -1,4 +1,4 @@
1
- import { onCleanup, untrack, useContext } from "@solidrt/core"
1
+ import { callRef, onCleanup, untrack, useContext } from "@solidrt/core"
2
2
  import type { VoidComponent } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import type { TransformProps, NodePointerProps } from "./node-props.ts"
@@ -32,7 +32,7 @@ export let Sprite: VoidComponent<SpriteProps> = props => {
32
32
  let mesh = untrack(() => createSprite(props.material))
33
33
  add(ctx.parent, mesh)
34
34
  syncMesh(mesh, props)
35
- untrack(() => props.ref)?.(mesh)
35
+ callRef(() => props.ref, mesh)
36
36
  onCleanup(() => destroy(mesh))
37
37
  return null
38
38
  }
@@ -1,4 +1,4 @@
1
- import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
1
+ import { callRef, createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
2
2
  import type { Element, ParentComponent, PointerFeed, TextureId } from "@solidrt/core"
3
3
  import { SceneContext } from "./context.tsx"
4
4
  import { feedPointer } from "../scene-pointer.ts"
@@ -116,7 +116,7 @@ export let View3d: ParentComponent<View3dProps> = props => {
116
116
  b => view.setBloom(b ?? null),
117
117
  { defer: true },
118
118
  )
119
- untrack(() => props.ref)?.(view)
119
+ callRef(() => props.ref, view)
120
120
  onCleanup(() => view.dispose())
121
121
  // The pointer feed the app handed in (see Scene) listens at the view's
122
122
  // root; children reach it through useScene().pointer.