@solidrt/3d 0.0.71 → 0.0.72
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 +69 -13
- package/package.json +3 -3
- package/src/components/directional-light.tsx +2 -2
- package/src/components/first-person-camera.tsx +2 -2
- package/src/components/group.tsx +2 -2
- package/src/components/hemisphere-light.tsx +2 -2
- package/src/components/instanced-mesh.tsx +3 -3
- package/src/components/lod.tsx +3 -3
- package/src/components/mesh.tsx +2 -2
- package/src/components/node-props.ts +13 -1
- package/src/components/orbit-camera.tsx +2 -2
- package/src/components/point-light.tsx +2 -2
- package/src/components/record-mesh.tsx +2 -2
- package/src/components/scene.tsx +13 -2
- package/src/components/shots.tsx +2 -2
- package/src/components/splat-mesh.tsx +2 -2
- package/src/components/spot-light.tsx +2 -2
- package/src/components/sprite.tsx +2 -2
- package/src/components/view3d.tsx +2 -2
- package/src/geometry.ts +23 -188
- package/src/glsl.ts +13 -3
- package/src/index.ts +1 -1
- package/src/material.ts +74 -50
- package/src/mesh.ts +8 -51
- package/src/mixer.ts +6 -1
- package/src/node.ts +47 -3
- package/src/scene-pointer.ts +11 -8
- package/src/scene.ts +48 -4
package/src/node.ts
CHANGED
|
@@ -217,8 +217,9 @@ export type SceneNode = {
|
|
|
217
217
|
/** The wheel over the node (NodeWheelEvent: `deltaX`/`deltaY`), bubbling
|
|
218
218
|
* like down/move/up. */
|
|
219
219
|
onWheel?: (event: NodeWheelEvent) => void
|
|
220
|
-
/** A press
|
|
221
|
-
*
|
|
220
|
+
/** A primary-button press (a right click never taps) released on this
|
|
221
|
+
* node within the slop, alone for its whole press (NodeTapEvent:
|
|
222
|
+
* `tapCount`); bubbles like down/move/up. */
|
|
222
223
|
onTap?: (event: NodeTapEvent) => void
|
|
223
224
|
/** A declared transition (setTransition) settled naturally on one
|
|
224
225
|
* component; a cancel, snap, scene leave or exit never fires. */
|
|
@@ -239,6 +240,9 @@ export type SceneNode = {
|
|
|
239
240
|
_scene: SceneHooks | null
|
|
240
241
|
/** The declared transition, re-applied on every scene enter. */
|
|
241
242
|
_transition: NodeTransition | string | null
|
|
243
|
+
/** The declared time scale (setTimeScale), re-applied on every scene
|
|
244
|
+
* enter; null inherits. */
|
|
245
|
+
_timeScale: number | null
|
|
242
246
|
/** Skin palette rows this node feeds (a model joint carries one per
|
|
243
247
|
* skin): bound to the core at every scene enter, so the flush writes
|
|
244
248
|
* `inverse(anchorWorld) * world * post` - the model-local bone matrix -
|
|
@@ -289,6 +293,9 @@ export type SceneEventBase = {
|
|
|
289
293
|
predicted: boolean
|
|
290
294
|
pointerId: number
|
|
291
295
|
pointerType: string
|
|
296
|
+
/** The pressed button on a down, up or tap (0 = primary, 2 = secondary;
|
|
297
|
+
* touch is always 0), as the element event carries it; absent on a
|
|
298
|
+
* move, cancel or wheel. */
|
|
292
299
|
button?: number
|
|
293
300
|
shiftKey: boolean
|
|
294
301
|
ctrlKey: boolean
|
|
@@ -407,6 +414,7 @@ export function makeNode(kind: SceneNode["kind"]): SceneNode {
|
|
|
407
414
|
_moved: false,
|
|
408
415
|
_scene: null,
|
|
409
416
|
_transition: null,
|
|
417
|
+
_timeScale: null,
|
|
410
418
|
_palettes: null,
|
|
411
419
|
_morph: null,
|
|
412
420
|
_cullBounds: null,
|
|
@@ -548,6 +556,7 @@ export function enterScene(node: SceneNode, scene: SceneHooks): void {
|
|
|
548
556
|
spatial.setTransition(node._node, node._transition)
|
|
549
557
|
declareTransition(node._node, node)
|
|
550
558
|
}
|
|
559
|
+
if (node._timeScale !== null) spatial.setTimeScale(node._node, node._timeScale)
|
|
551
560
|
// The parent is in the scene already (add() enters the child only then),
|
|
552
561
|
// and the scene root is the one node without a parent.
|
|
553
562
|
if (node.parent !== null && node.parent._node !== null) spatial.setParent(node._node, node.parent._node)
|
|
@@ -799,6 +808,37 @@ export function setTransition(node: SceneNode, transition: NodeTransition | stri
|
|
|
799
808
|
}
|
|
800
809
|
}
|
|
801
810
|
|
|
811
|
+
/**
|
|
812
|
+
* Declare (null clears) the rate the native motion in the node's subtree
|
|
813
|
+
* runs at: transitions with their delays, enters, exits and stagger,
|
|
814
|
+
* morph weight tracks, and the clip players of the models under it
|
|
815
|
+
* (a mixer's players are clocked on their model's node, so this is also
|
|
816
|
+
* Three's `mixer.timeScale`). 0 freezes, 1 is app time, other values
|
|
817
|
+
* are slow or fast motion. The nearest declaring ancestor wins, never a
|
|
818
|
+
* product: a world at 0 with a group inside it declaring 1 keeps that
|
|
819
|
+
* group running (a preview on a pause screen). A frozen subtree asks for
|
|
820
|
+
* no frames; this write brings the one that resumes it. The declaration
|
|
821
|
+
* lives on the SceneNode and re-applies on every scene enter. What it
|
|
822
|
+
* does not cover: `onFrame` logic, timers, video and audio stay on app
|
|
823
|
+
* time - multiply your own step by `timeRate(node)`. For the whole scene,
|
|
824
|
+
* `scene.setTimeScale` / `<Scene timeScale>` declare it on `scene.root`.
|
|
825
|
+
*/
|
|
826
|
+
export function setTimeScale(node: SceneNode, scale: number | null): void {
|
|
827
|
+
if (node._destroyed) return
|
|
828
|
+
node._timeScale = scale
|
|
829
|
+
if (node._node !== null) spatial.setTimeScale(node._node, scale)
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* The rate the node's motion runs at (setTimeScale): its own scale, else
|
|
834
|
+
* the nearest declaring ancestor's, else 1. Null outside a scene, where
|
|
835
|
+
* nothing runs and no ancestry resolves.
|
|
836
|
+
*/
|
|
837
|
+
export function timeRate(node: SceneNode): number | null {
|
|
838
|
+
if (node._node === null) return null
|
|
839
|
+
return spatial.timeRate(node._node)
|
|
840
|
+
}
|
|
841
|
+
|
|
802
842
|
/**
|
|
803
843
|
* The one write path for node transforms (so the scene knows to sync).
|
|
804
844
|
* Values are copied in; absent keys keep their current value. This is also
|
|
@@ -941,7 +981,11 @@ export function getTransform(node: SceneNode): { position: Vec3; quaternion: Qua
|
|
|
941
981
|
/**
|
|
942
982
|
* A node's position in world space, copied into `out` (or a fresh Vec3) -
|
|
943
983
|
* Three's `getWorldPosition`. Brings the ancestor chain up to date first,
|
|
944
|
-
* so it is exact before the pending sync has run
|
|
984
|
+
* so it is exact before the pending sync has run, and exact against
|
|
985
|
+
* native motion: a transition or a clip player has stepped to this
|
|
986
|
+
* frame's pose before any of the frame's JS runs, so a read in onFrame
|
|
987
|
+
* or the late pass of onBeforeRender is where the node is drawn this
|
|
988
|
+
* frame.
|
|
945
989
|
*/
|
|
946
990
|
export function worldPosition(node: SceneNode, out: Vec3 = [0, 0, 0]): Vec3 {
|
|
947
991
|
let world = worldInto(worldScratch, node)
|
package/src/scene-pointer.ts
CHANGED
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
// turns. Capture is per pointer to the press target, the root included: a
|
|
13
13
|
// drag from empty space keeps delivering to the root as it crosses meshes,
|
|
14
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
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
15
|
+
// on the struck node alone. Taps are synthesized here (DOM's click, which
|
|
16
|
+
// is primary-button only; Unity's IPointerClickHandler fires for every
|
|
17
|
+
// button and reports it, we do not): a primary-button press that releases
|
|
18
|
+
// on the same target within the slop, the only pointer down for its whole
|
|
19
|
+
// press, counted up for repeats (DOM's detail, Unity's clickCount).
|
|
20
|
+
// @solidrt/2d's dispatch.ts is the same model one dimension down.
|
|
20
21
|
//
|
|
21
22
|
// Pure BY DESIGN (types only, nothing with GPU imports) so
|
|
22
23
|
// tests/dispatch.test.ts drives it headless with a fake pick.
|
|
@@ -240,9 +241,11 @@ export function makePointerInput(deps: PointerInputDeps): PointerInput {
|
|
|
240
241
|
presses.delete(e.pointerId)
|
|
241
242
|
let under = hitOn(press.target, x, y)
|
|
242
243
|
walk("onPointerUp", makeEvent(e, press.target, x, y, under), !press.claimed)
|
|
243
|
-
// The tap rule:
|
|
244
|
-
// press
|
|
245
|
-
//
|
|
244
|
+
// The tap rule: the primary button (DOM click; core's press and
|
|
245
|
+
// long-press gate the same way), no travel past the slop, alone
|
|
246
|
+
// for the whole press, released over the target it pressed (empty
|
|
247
|
+
// space for the root itself).
|
|
248
|
+
if (e.button != null && e.button !== 0) return
|
|
246
249
|
if (press.moved || !press.alone) return
|
|
247
250
|
let release = nearest(x, y)
|
|
248
251
|
if (press.target === null ? release !== null : release === null || !sameHit(release, press.target)) return
|
package/src/scene.ts
CHANGED
|
@@ -63,7 +63,7 @@ import type { EnvironmentOptions, Prefilter } from "./environment.ts"
|
|
|
63
63
|
|
|
64
64
|
export type { EnvironmentOptions } from "./environment.ts"
|
|
65
65
|
import type { Material } from "./material.ts"
|
|
66
|
-
import { activateMorph, fillTransform, freeLeaving, leaveScene, makeNode, setTransition, worldInto } from "./node.ts"
|
|
66
|
+
import { activateMorph, fillTransform, freeLeaving, leaveScene, makeNode, setTimeScale, setTransition, timeRate, worldInto } from "./node.ts"
|
|
67
67
|
import type { SceneHooks, SceneNode, ScenePointerListener } from "./node.ts"
|
|
68
68
|
import { checkInstancePairing, checkMask, checkOrderPairing, drawCount, instanceBinding, localBounds, markLiveRecords, publishRecords, spriteScreenPx } from "./mesh.ts"
|
|
69
69
|
import type { InstancedMesh, InstanceNode, Mesh, MeshInstances, ResolvedInstanceOrder } from "./mesh.ts"
|
|
@@ -474,6 +474,10 @@ export type SceneOptions = {
|
|
|
474
474
|
* the whole-scene form of a Group's stagger (nested groups declaring
|
|
475
475
|
* their own win for what is under them). See setStagger. */
|
|
476
476
|
stagger?: number
|
|
477
|
+
/** The rate the scene's native motion runs at - `setTimeScale(scene.root,
|
|
478
|
+
* scale)` at creation: 0 freezes the world, 1 is app time (nodes
|
|
479
|
+
* declaring their own win for what is under them). See setTimeScale. */
|
|
480
|
+
timeScale?: number
|
|
477
481
|
/** `autoFree: false` opts out of owner-scoped auto-dispose (then call dispose yourself). */
|
|
478
482
|
autoFree?: boolean
|
|
479
483
|
filter?: FilterMode
|
|
@@ -833,6 +837,16 @@ export type Scene = {
|
|
|
833
837
|
/** Stagger (ms) on the scene's root node, live (SceneOptions.stagger
|
|
834
838
|
* is its creation form): null removes it. The 2d layer's setStagger. */
|
|
835
839
|
setStagger(ms: number | null): void
|
|
840
|
+
/** The time scale on the scene's root node, live (SceneOptions.timeScale
|
|
841
|
+
* is its creation form): the rate every transition, enter, exit and
|
|
842
|
+
* clip player in the scene runs at, 0 a pause that leaves the app's own
|
|
843
|
+
* UI moving; null removes it (app time). A node declaring its own keeps
|
|
844
|
+
* its subtree at that rate (see node setTimeScale). The 2d layer's
|
|
845
|
+
* setTimeScale. */
|
|
846
|
+
setTimeScale(scale: number | null): void
|
|
847
|
+
/** The rate the scene's root runs at (setTimeScale): what the app's own
|
|
848
|
+
* per-frame logic in this scene multiplies its step by. */
|
|
849
|
+
timeRate(): number
|
|
836
850
|
/** The level a LOD group draws in the scene's own render (the thing to
|
|
837
851
|
* read while tuning thresholds): the index into its levels, nearest
|
|
838
852
|
* first, `levels.length` when culled past the last one, null before
|
|
@@ -2272,22 +2286,45 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2272
2286
|
root._scene = hooks
|
|
2273
2287
|
root._node = spatial.createNode(fillTransform(root), true)
|
|
2274
2288
|
if (opts?.stagger !== undefined) setTransition(root, { stagger: opts.stagger })
|
|
2289
|
+
if (opts?.timeScale !== undefined) setTimeScale(root, opts.timeScale)
|
|
2275
2290
|
// The first light rewrite seeds the (empty) light set and the shadow
|
|
2276
2291
|
// slots - placeholders, no casts - so receivers draw plain from the
|
|
2277
2292
|
// first frame.
|
|
2278
2293
|
lightsDirty = true
|
|
2279
2294
|
hooks._schedule()
|
|
2295
|
+
// What the sync derives from poses the core moves on its own, placed
|
|
2296
|
+
// every frame: a casting light's shadow cameras follow a light under a
|
|
2297
|
+
// transition or a clip player in the frame the lighting moves, which
|
|
2298
|
+
// the core writes itself. The compare in placeShadowCamera makes this
|
|
2299
|
+
// one world read per caster and no write when nothing moved; the
|
|
2300
|
+
// records, the lights and the budgets wait for a write to schedule the
|
|
2301
|
+
// sync, which places the cameras itself.
|
|
2302
|
+
let follow = () => {
|
|
2303
|
+
if (disposed) return
|
|
2304
|
+
shadowSys.placeCameras(false)
|
|
2305
|
+
for (let v of views) {
|
|
2306
|
+
ensureCamera(v.camera, v.width, v.height)
|
|
2307
|
+
if (!v.camera.pending) continue
|
|
2308
|
+
v.camera.pending = false
|
|
2309
|
+
setTargetParams(v.texture, cameraParams(v.camera, v.width, v.height))
|
|
2310
|
+
setView(v.texture, v.camera)
|
|
2311
|
+
if (v.shadowFilter !== null) shadowSys.markMatricesDirty()
|
|
2312
|
+
}
|
|
2313
|
+
shadowSys.flushMatrices(params => receivingTargets(t => setTargetParams(t, params)))
|
|
2314
|
+
}
|
|
2280
2315
|
// The publish pass of every frame runs the pending sync ahead of the
|
|
2281
2316
|
// paint, so a write made anywhere in the frame's JS is in that frame's
|
|
2282
2317
|
// picture: the count, the buffers and the bytes of a record write land
|
|
2283
|
-
// together, and nothing shows a frame late.
|
|
2284
|
-
//
|
|
2285
|
-
//
|
|
2318
|
+
// together, and nothing shows a frame late. With nothing scheduled it
|
|
2319
|
+
// follows native motion instead. The microtask stays for writes made
|
|
2320
|
+
// outside a frame. Registered outside any owner: the scene's own
|
|
2321
|
+
// dispose unhooks it (an autoFree: false scene outlives the owner it
|
|
2286
2322
|
// was created in).
|
|
2287
2323
|
let unhook = runWithOwner(null, () =>
|
|
2288
2324
|
onBeforeRender(
|
|
2289
2325
|
() => {
|
|
2290
2326
|
if (scheduled) sync()
|
|
2327
|
+
else follow()
|
|
2291
2328
|
},
|
|
2292
2329
|
{ publish: true },
|
|
2293
2330
|
),
|
|
@@ -2444,6 +2481,13 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2444
2481
|
if (disposed) return
|
|
2445
2482
|
setTransition(root, ms === null ? null : { stagger: ms })
|
|
2446
2483
|
},
|
|
2484
|
+
setTimeScale(scale) {
|
|
2485
|
+
if (disposed) return
|
|
2486
|
+
setTimeScale(root, scale)
|
|
2487
|
+
},
|
|
2488
|
+
timeRate() {
|
|
2489
|
+
return timeRate(root) ?? 1
|
|
2490
|
+
},
|
|
2447
2491
|
setParams(params) {
|
|
2448
2492
|
if (disposed) return
|
|
2449
2493
|
Object.assign(sceneParams, params)
|