@solidrt/2d 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 +211 -42
- package/README.md +7 -3
- package/examples/README.md +1 -1
- package/examples/camera-probe.tsx +4 -4
- package/examples/markers.tsx +3 -3
- package/package.json +6 -5
- package/src/animation.ts +26 -5
- package/src/atlas.ts +8 -4
- package/src/camera.ts +2 -2
- package/src/components/camera2d.tsx +14 -5
- package/src/components/group.tsx +15 -3
- package/src/components/shots.tsx +2 -2
- package/src/components/sprite-layer.tsx +44 -12
- package/src/components/sprite.tsx +31 -3
- package/src/components/text2d.tsx +2 -2
- package/src/components/tile-layer.tsx +21 -7
- package/src/components/view2d.tsx +2 -2
- package/src/dispatch.ts +10 -6
- package/src/frames.ts +1 -1
- package/src/glsl.ts +399 -0
- package/src/index.ts +14 -6
- package/src/layer.ts +240 -101
- package/src/material.ts +368 -0
- package/src/records.ts +135 -32
- package/src/style.ts +98 -0
- package/src/tiles.ts +119 -24
- package/src/views.ts +17 -9
- package/src/shaders.ts +0 -323
package/src/layer.ts
CHANGED
|
@@ -12,19 +12,27 @@
|
|
|
12
12
|
//
|
|
13
13
|
// Two instance-buffer slots split ownership: slot 0 is the pose buffer,
|
|
14
14
|
// written ONLY by the core (one coalesced write per flush however many
|
|
15
|
-
// nodes moved); slot 1 is the
|
|
16
|
-
// renderOrder, minScreenPx, maxScreenPx, atlas], JS-owned
|
|
17
|
-
// through the
|
|
18
|
-
//
|
|
19
|
-
//
|
|
15
|
+
// nodes moved); slot 1 is the SPRITE record [u0, v0, u1, v1, tint rgba,
|
|
16
|
+
// renderOrder, minScreenPx, maxScreenPx, atlas, outline], JS-owned
|
|
17
|
+
// through the sprite verbs and published through the zero-copy write
|
|
18
|
+
// lease. After them come the MATERIAL's style streams (style.ts), one
|
|
19
|
+
// per instance buffer its material declares: app-owned per-sprite data
|
|
20
|
+
// in any vertex format, written with setInstanceStyle, instanceAttribute
|
|
21
|
+
// or records(layer, stream). Never write the pose buffer from JS - the
|
|
22
|
+
// core's staging mirror is the owner and will overwrite. For motion only
|
|
23
|
+
// JS can compute at large populations, the records layer (records.ts)
|
|
20
24
|
// is the escape hatch.
|
|
21
25
|
//
|
|
22
26
|
// A layer draws from the ATLASES it declares at creation, bound together
|
|
23
|
-
// as one draw: every frame carries its texture, the
|
|
24
|
-
// that texture's index in the list, and the generated
|
|
25
|
-
// (
|
|
26
|
-
// sheets interleave freely in one draw, key order included, and
|
|
27
|
-
// from a sheet the layer did not declare throws at the write.
|
|
27
|
+
// as one draw: every frame carries its texture, the sprite record stores
|
|
28
|
+
// that texture's index in the list, and the fragment set generated for
|
|
29
|
+
// the list (glsl.ts) picks the sampler per instance - so sprites from
|
|
30
|
+
// several sheets interleave freely in one draw, key order included, and
|
|
31
|
+
// a frame from a sheet the layer did not declare throws at the write.
|
|
32
|
+
// What the pixels look like is the layer's MATERIAL (material.ts): the
|
|
33
|
+
// stock `unlit()` unless the options name one, which owns the pipeline
|
|
34
|
+
// (its blend, its program over the layer's set) and whose params and
|
|
35
|
+
// textures every view seeds.
|
|
28
36
|
//
|
|
29
37
|
// Sprites hold FIXED instance slots (freed slots recycle): draw order is
|
|
30
38
|
// slot order, so removal never shifts records and pose sinks never rebind.
|
|
@@ -36,7 +44,7 @@
|
|
|
36
44
|
import { getOwner, onBeforeRender, onCleanup, runWithOwner } from "@solidrt/core"
|
|
37
45
|
import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEvent } from "@solidrt/core"
|
|
38
46
|
import { checkScreenSize, createBuffer, createRecordStream, destroyBuffer, screenSizeScale } from "@solidrt/core/gpu"
|
|
39
|
-
import type {
|
|
47
|
+
import type { BufferId, RecordStream, ShaderParams, TextureId } from "@solidrt/core/gpu"
|
|
40
48
|
import * as spatial from "flux:spatial"
|
|
41
49
|
import type {
|
|
42
50
|
Impact as CoreImpact,
|
|
@@ -56,7 +64,11 @@ import type { Frame } from "./frames.ts"
|
|
|
56
64
|
import { fullFrame, isFrame, writeFrame } from "./frames.ts"
|
|
57
65
|
import type { RecordLayer } from "./records.ts"
|
|
58
66
|
import { floorReach, pointInSprite } from "./pick.ts"
|
|
59
|
-
import {
|
|
67
|
+
import { atlasBindings, POSE_ATTRIBUTES, SPRITE_ATTRIBUTES } from "./glsl.ts"
|
|
68
|
+
import { materialSamplers, unlit } from "./material.ts"
|
|
69
|
+
import type { Material } from "./material.ts"
|
|
70
|
+
import { blankStyle, createStyleStreams, growStyle } from "./style.ts"
|
|
71
|
+
import type { StyleStream } from "./style.ts"
|
|
60
72
|
import { createViews } from "./views.ts"
|
|
61
73
|
import type { ViewHandle, ViewOptions } from "./views.ts"
|
|
62
74
|
|
|
@@ -65,24 +77,24 @@ export const POSE_FLOATS = 5
|
|
|
65
77
|
|
|
66
78
|
// Float offset of world y in a pose record - what `orderBy: "y"` keys on.
|
|
67
79
|
const POSE_Y_FIELD = 1
|
|
68
|
-
/** Floats per
|
|
80
|
+
/** Floats per sprite record (the node layer's slot 1, SPRITE_ATTRIBUTES):
|
|
69
81
|
* [u0, v0, u1, v1, tintR, tintG, tintB, tintA, renderOrder, minScreenPx,
|
|
70
82
|
* maxScreenPx, atlas, outlineR, outlineG, outlineB, outlineWidth]. */
|
|
71
|
-
export const
|
|
83
|
+
export const SPRITE_FLOATS = 16
|
|
72
84
|
|
|
73
|
-
// Float offset of renderOrder in a
|
|
85
|
+
// Float offset of renderOrder in a sprite record - what `orderBy: "renderOrder"`
|
|
74
86
|
// keys on.
|
|
75
|
-
const
|
|
76
|
-
// Float offsets of the screen-size clamp in a
|
|
77
|
-
const
|
|
78
|
-
const
|
|
87
|
+
const SPRITE_KEY_FIELD = 8
|
|
88
|
+
// Float offsets of the screen-size clamp in a sprite record.
|
|
89
|
+
const SPRITE_MIN_PX_FIELD = 9
|
|
90
|
+
const SPRITE_MAX_PX_FIELD = 10
|
|
79
91
|
// Float offset of the outline (rgb plus width in world pixels) a
|
|
80
92
|
// distance-field atlas draws under the sprite; text runs write it, a
|
|
81
93
|
// colour atlas ignores it.
|
|
82
|
-
const
|
|
94
|
+
const SPRITE_OUTLINE_FIELD = 12
|
|
83
95
|
// Float offset of the atlas sampler index (the frame's texture's position
|
|
84
96
|
// in the layer's atlas list).
|
|
85
|
-
const
|
|
97
|
+
const SPRITE_ATLAS_FIELD = 11
|
|
86
98
|
|
|
87
99
|
const RESOLVED = Promise.resolve()
|
|
88
100
|
|
|
@@ -287,7 +299,7 @@ export type SpriteOptions = {
|
|
|
287
299
|
/**
|
|
288
300
|
* Show or hide the sprite (default true), @solidrt/3d's setVisible in
|
|
289
301
|
* the setSprite bag: hidden, its pose slot zeroes (nothing drawn) and
|
|
290
|
-
* pick/pickRect skip it; the handle, slot,
|
|
302
|
+
* pick/pickRect skip it; the handle, slot, sprite records and any
|
|
291
303
|
* running transition state stay, so showing again restores the sprite
|
|
292
304
|
* as it was. Node layer only - a record sprite has no node (hide it by
|
|
293
305
|
* zeroing w or h); setting this there throws.
|
|
@@ -320,6 +332,9 @@ export type LayerEventBase = {
|
|
|
320
332
|
predicted: boolean
|
|
321
333
|
pointerId: number
|
|
322
334
|
pointerType: string
|
|
335
|
+
/** The pressed button on a down, up or tap (0 = primary, 2 = secondary;
|
|
336
|
+
* touch is always 0), as the element event carries it; absent on a
|
|
337
|
+
* move, cancel or wheel. */
|
|
323
338
|
button?: number
|
|
324
339
|
shiftKey: boolean
|
|
325
340
|
ctrlKey: boolean
|
|
@@ -378,10 +393,10 @@ type TapFields = {
|
|
|
378
393
|
export type SpriteWheelEvent = SpritePointerEvent & WheelFields
|
|
379
394
|
export type LayerWheelEvent = LayerPointerEvent & WheelFields
|
|
380
395
|
/**
|
|
381
|
-
* A press
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
* point.
|
|
396
|
+
* A primary-button press (DOM click: a right click never taps) that
|
|
397
|
+
* released on the target it pressed without travelling past the slop, the
|
|
398
|
+
* only pointer down for its whole press (a pinch never taps). Dispatched
|
|
399
|
+
* after the up, bubbling the same way; `x`/`y` are the release point.
|
|
385
400
|
*/
|
|
386
401
|
export type SpriteTapEvent = SpritePointerEvent & TapFields
|
|
387
402
|
export type LayerTapEvent = LayerPointerEvent & TapFields
|
|
@@ -415,6 +430,17 @@ export function checkTint(verb: string, tint: [number, number, number, number]):
|
|
|
415
430
|
}
|
|
416
431
|
}
|
|
417
432
|
|
|
433
|
+
/** Validate a params record (throws - the dev validation policy): a
|
|
434
|
+
* plain object of finite numbers or number lists. Internal - every
|
|
435
|
+
* layer kind's setParams calls it. */
|
|
436
|
+
export function checkParams(verb: string, params: ShaderParams): void {
|
|
437
|
+
if (typeof params !== "object" || params === null || Array.isArray(params)) throw new Error(`${verb}: params must be an object of uniform values, got ${JSON.stringify(params)}`)
|
|
438
|
+
for (let [name, value] of Object.entries(params)) {
|
|
439
|
+
let ok = typeof value === "number" ? Number.isFinite(value) : Array.isArray(value) && value.length > 0 && value.every(Number.isFinite)
|
|
440
|
+
if (!ok) throw new Error(`${verb}: ${name} must be a finite number or a list of them, got ${JSON.stringify(value)}`)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
418
444
|
export type SpriteLayerOptions = {
|
|
419
445
|
/**
|
|
420
446
|
* Initial slot reservation; default 1024. The layer grows past it on
|
|
@@ -427,13 +453,25 @@ export type SpriteLayerOptions = {
|
|
|
427
453
|
* setTint. */
|
|
428
454
|
tint?: [number, number, number, number]
|
|
429
455
|
/**
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
456
|
+
* What the sprites look like: the layer's material (material.ts),
|
|
457
|
+
* fixed at creation - its pipeline (the blend mode, its program over
|
|
458
|
+
* the layer's atlases), its params and its textures on every view,
|
|
459
|
+
* its instance buffers as per-sprite style streams after the layer's
|
|
460
|
+
* own records. Default `unlit()`: the atlas texel times the tints,
|
|
461
|
+
* alpha-blended in draw order. An additive glow layer is
|
|
462
|
+
* `material: unlit({ blend: "add" })` over the same atlases; a look is
|
|
463
|
+
* `unlit({ prelude, surface })` or a shaderMaterialClass instance. The
|
|
464
|
+
* material sits on the LAYER because the layer is the batch - two
|
|
465
|
+
* looks are two layers, which is also how you order them.
|
|
435
466
|
*/
|
|
436
|
-
|
|
467
|
+
material?: Material
|
|
468
|
+
/**
|
|
469
|
+
* Uniform values seeded on every view of the layer over the
|
|
470
|
+
* material's own (a prelude's uniforms: a flash amount, a palette
|
|
471
|
+
* row, a clock); see setParams. Names the program does not declare
|
|
472
|
+
* are skipped.
|
|
473
|
+
*/
|
|
474
|
+
params?: ShaderParams
|
|
437
475
|
/** Names the GPU resources (buffers, pipeline; views default to
|
|
438
476
|
* `<label>-view`); default "sprites". */
|
|
439
477
|
label?: string
|
|
@@ -443,17 +481,21 @@ export type SpriteLayerOptions = {
|
|
|
443
481
|
* group's `stagger` (a group declaring its own wins for what is under
|
|
444
482
|
* it). See setStagger. */
|
|
445
483
|
stagger?: number
|
|
484
|
+
/** The rate the layer's native motion runs at (setTimeScale at
|
|
485
|
+
* creation): 0 freezes every sprite and group transition, 1 is app
|
|
486
|
+
* time; a group declaring its own wins for what is under it. */
|
|
487
|
+
timeScale?: number
|
|
446
488
|
/** Skip the owner-scoped auto-dispose (see createSpriteLayer). */
|
|
447
489
|
autoFree?: boolean
|
|
448
490
|
/**
|
|
449
491
|
* Draw sprites in KEY order instead of slot order, produced by core at
|
|
450
|
-
* every publish (the gpu `instanceOrder` primitive across the pose/
|
|
492
|
+
* every publish (the gpu `instanceOrder` primitive across the pose/sprite
|
|
451
493
|
* buffer pair - no per-sprite JS anywhere): `"y"` keys on the sprite's
|
|
452
494
|
* WORLD y, the pose the core itself writes, so a perspective crowd
|
|
453
495
|
* paints back to front (smaller y = further up the screen = drawn
|
|
454
496
|
* first) - and because the key is core-owned, sprites moved by native
|
|
455
497
|
* transitions or any other core producer re-sort with zero JS per
|
|
456
|
-
* frame. Slots stay fixed - handles, picking and the
|
|
498
|
+
* frame. Slots stay fixed - handles, picking and the sprite records are
|
|
457
499
|
* untouched, only the draw order changes - and ties keep slot order, so
|
|
458
500
|
* sprites at equal y draw exactly as without it.
|
|
459
501
|
*
|
|
@@ -551,6 +593,9 @@ export type LayerBase = {
|
|
|
551
593
|
* in draw order.
|
|
552
594
|
*/
|
|
553
595
|
readonly atlases: readonly Atlas[]
|
|
596
|
+
/** The material the layer draws with, as declared at creation (the
|
|
597
|
+
* stock `unlit()` when none was). */
|
|
598
|
+
readonly material: Material
|
|
554
599
|
/** Live sprite count. */
|
|
555
600
|
readonly count: number
|
|
556
601
|
/**
|
|
@@ -562,6 +607,16 @@ export type LayerBase = {
|
|
|
562
607
|
* kinds.
|
|
563
608
|
*/
|
|
564
609
|
setTint(tint: [number, number, number, number]): void
|
|
610
|
+
/**
|
|
611
|
+
* Write uniform values to every view of the layer (one shared-params
|
|
612
|
+
* write per view, cheap to animate): the material's prelude uniforms
|
|
613
|
+
* - a flash amount, a palette row, a scroll offset, a clock - over the
|
|
614
|
+
* material's own `params`; @solidrt/3d's setMeshParams one dimension
|
|
615
|
+
* down, the layer being the draw entry. Absent names keep their
|
|
616
|
+
* values; a name the program does not declare is skipped. Views
|
|
617
|
+
* created later seed from the merged record.
|
|
618
|
+
*/
|
|
619
|
+
setParams(params: ShaderParams): void
|
|
565
620
|
/** Every shown sprite whose rotated rect contains the layer-pixel point
|
|
566
621
|
* (a hidden sprite or subtree is never hit), topmost first - the
|
|
567
622
|
* all-hits shape of @solidrt/3d's pick; `pick(x, y)[0]` is the topmost.
|
|
@@ -592,12 +647,16 @@ export type LayerBase = {
|
|
|
592
647
|
_destroy(sprite: SpriteState): void
|
|
593
648
|
_schedule(): void
|
|
594
649
|
/** The layer's JS-owned record stream, what records()/updateRecords
|
|
595
|
-
* read and mark: a records layer's whole records, a node layer's
|
|
596
|
-
* records (its poses are the core's). Internal. */
|
|
650
|
+
* read and mark: a records layer's whole records, a node layer's
|
|
651
|
+
* sprite records (its poses are the core's). Internal. */
|
|
597
652
|
_stream: RecordStream
|
|
598
653
|
/** The stream's mirror as floats, re-pointed by growth; records() hands
|
|
599
654
|
* it out. Internal. */
|
|
600
655
|
_records: Float32Array
|
|
656
|
+
/** The material's style streams, one per instance buffer it declares,
|
|
657
|
+
* in its order; what setInstanceStyle, instanceAttribute, records(layer,
|
|
658
|
+
* stream) and updateRecords reach. Internal. */
|
|
659
|
+
_styles: StyleStream[]
|
|
601
660
|
}
|
|
602
661
|
|
|
603
662
|
/** A query volume for overlap/sweep/moveAndSlide, in layer pixels: a
|
|
@@ -726,6 +785,20 @@ export type SpriteLayer = LayerBase & {
|
|
|
726
785
|
* straight under the layer that begin in one frame (SpriteLayerOptions
|
|
727
786
|
* `stagger`). */
|
|
728
787
|
setStagger(ms: number | null): void
|
|
788
|
+
/** Set (or with null clear) the time scale on the layer's root: the
|
|
789
|
+
* rate every sprite and group transition in the layer runs at, with
|
|
790
|
+
* their delays, enters, exits and stagger slots - 0 a pause that leaves
|
|
791
|
+
* the app's UI moving, 1 app time, other values slow or fast motion
|
|
792
|
+
* (SpriteLayerOptions `timeScale`). The nearest declaring node wins:
|
|
793
|
+
* a group or sprite with its own (setGroupTimeScale,
|
|
794
|
+
* setSpriteTimeScale) keeps its rate inside a frozen layer. A frozen
|
|
795
|
+
* layer asks for no frames; this write brings the one that resumes it.
|
|
796
|
+
* Not covered: frame animations (createAnimation), which are JS clocks
|
|
797
|
+
* on app time unless given this layer as their `clock`, and the app's
|
|
798
|
+
* own onFrame logic, which multiplies its step by `timeRate()`. */
|
|
799
|
+
setTimeScale(scale: number | null): void
|
|
800
|
+
/** The rate the layer's root runs at (setTimeScale); 1 after dispose. */
|
|
801
|
+
timeRate(): number
|
|
729
802
|
/** The core node every sprite and group of the layer sits under: what
|
|
730
803
|
* scopes the layer's queries in the arena shared with 3d scenes. */
|
|
731
804
|
_root: NodeId
|
|
@@ -833,14 +906,22 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
833
906
|
let label = opts?.label ?? "sprites"
|
|
834
907
|
let tint = opts?.tint ?? [1, 1, 1, 1]
|
|
835
908
|
checkTint("createSpriteLayer", tint)
|
|
909
|
+
let material = opts?.material ?? unlit()
|
|
910
|
+
checkAtlases("createSpriteLayer", atlases, materialSamplers(material))
|
|
911
|
+
// The params every view seeds: the material's, the layer's over them,
|
|
912
|
+
// merged as setParams writes land so a later view starts current.
|
|
913
|
+
let params: ShaderParams = { ...material.params, ...opts?.params }
|
|
836
914
|
let pose: BufferId = createBuffer(capacity * POSE_FLOATS * 4, { label: `${label}-pose`, autoFree: false })
|
|
837
|
-
// The
|
|
915
|
+
// The sprite records: JS-written through the sprite verbs (writeSprite),
|
|
838
916
|
// records()/updateRecords for a bulk restyle, published by the flush as
|
|
839
917
|
// the dirty slot range, or the used prefix whole under an order (the
|
|
840
|
-
// core gathers
|
|
841
|
-
let
|
|
918
|
+
// core gathers every instance buffer under the one permutation).
|
|
919
|
+
let sprites = createRecordStream(SPRITE_FLOATS * 4, capacity, { label: `${label}-sprites`, autoFree: false })
|
|
920
|
+
// The material's style streams, after the layer's own: the same
|
|
921
|
+
// publish, the same growth.
|
|
922
|
+
let styles = createStyleStreams(material, capacity, label)
|
|
842
923
|
let ordered = opts?.orderBy !== undefined
|
|
843
|
-
let gpu =
|
|
924
|
+
let gpu = material.pipeline(atlases, [POSE_ATTRIBUTES, SPRITE_ATTRIBUTES], label)
|
|
844
925
|
// The sprites with a screen-size clamp (either bound on), and the
|
|
845
926
|
// furthest any of them reaches from its center at its floor (floorReach,
|
|
846
927
|
// view pixels): the candidate box a pick searches around the pointer. A
|
|
@@ -859,8 +940,8 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
859
940
|
// ceiling set alone must not drop below the floor already there.
|
|
860
941
|
let checkClamp = (verb: string, sprite: SpriteState, opts: SpriteOptions): void => {
|
|
861
942
|
if (opts.minScreenPx === undefined && opts.maxScreenPx === undefined) return
|
|
862
|
-
let at = sprite._slot *
|
|
863
|
-
checkScreenSize(verb, opts.minScreenPx ??
|
|
943
|
+
let at = sprite._slot * SPRITE_FLOATS
|
|
944
|
+
checkScreenSize(verb, opts.minScreenPx ?? spriteData[at + SPRITE_MIN_PX_FIELD]!, opts.maxScreenPx ?? spriteData[at + SPRITE_MAX_PX_FIELD]!)
|
|
864
945
|
}
|
|
865
946
|
|
|
866
947
|
let disposed = false
|
|
@@ -889,7 +970,7 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
889
970
|
// slot stays taken until the core says it is gone; dispose frees them.
|
|
890
971
|
let leaving = new Set<NodeId>()
|
|
891
972
|
let gpuCapacity = capacity
|
|
892
|
-
let
|
|
973
|
+
let spriteData = new Float32Array(sprites.bytes.buffer)
|
|
893
974
|
let byNode = new Map<NodeId, SpriteState>()
|
|
894
975
|
// The layer's root node (identity): parentless sprites and groups hang
|
|
895
976
|
// off it, so a query scoped to it sees exactly this layer.
|
|
@@ -897,6 +978,7 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
897
978
|
let root = spatial.createNode(TRANSFORM, true)
|
|
898
979
|
let filter: QueryFilter = { root }
|
|
899
980
|
if (opts?.stagger !== undefined) spatial.setTransition(root, { stagger: opts.stagger })
|
|
981
|
+
if (opts?.timeScale !== undefined) spatial.setTimeScale(root, opts.timeScale)
|
|
900
982
|
// Refill the layer's one filter for a query: the root always, the
|
|
901
983
|
// include-list as the sprites' nodes when given.
|
|
902
984
|
let queryFilter = (opts: QueryOptions | undefined, site: string): void => {
|
|
@@ -920,7 +1002,8 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
920
1002
|
let flush = () => {
|
|
921
1003
|
scheduled = false
|
|
922
1004
|
if (disposed) return
|
|
923
|
-
if (
|
|
1005
|
+
if (sprites.dirty) sprites.publish(ordered, highWater)
|
|
1006
|
+
for (let s of styles) if (s.stream.dirty) s.stream.publish(ordered, highWater)
|
|
924
1007
|
if (published !== highWater) {
|
|
925
1008
|
views.setCount(highWater)
|
|
926
1009
|
published = highWater
|
|
@@ -930,20 +1013,25 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
930
1013
|
spatial.flush()
|
|
931
1014
|
}
|
|
932
1015
|
|
|
933
|
-
//
|
|
1016
|
+
// The instance buffers in pipeline order after the quad: the pose, the
|
|
1017
|
+
// sprite records, the material's style streams.
|
|
1018
|
+
let buffers = (): BufferId[] => [pose, sprites.buffer, ...styles.map(s => s.stream.buffer)]
|
|
1019
|
+
// Grow every instance buffer to `next` slots: the pose sinks move in one
|
|
934
1020
|
// retargetRecords call (the whole used range republishes at the next
|
|
935
|
-
// flush), the
|
|
1021
|
+
// flush), the record streams grow into replacements marked whole, which
|
|
936
1022
|
// the next flush publishes. The entries hold the old buffers alive until
|
|
937
1023
|
// the swaps land, so the destroys are safe to issue right after.
|
|
938
1024
|
let grow = (next: number) => {
|
|
939
|
-
let
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1025
|
+
let oldPose = pose
|
|
1026
|
+
pose = createBuffer(next * POSE_FLOATS * 4, { label: `${label}-pose`, autoFree: false })
|
|
1027
|
+
spatial.retargetRecords(oldPose, pose)
|
|
1028
|
+
let oldSprites = sprites.grow(next)
|
|
1029
|
+
spriteData = new Float32Array(sprites.bytes.buffer)
|
|
1030
|
+
let oldStyles = styles.map(s => growStyle(s, next))
|
|
1031
|
+
views.setBuffers(buffers())
|
|
1032
|
+
destroyBuffer(oldPose)
|
|
1033
|
+
destroyBuffer(oldSprites)
|
|
1034
|
+
for (let old of oldStyles) destroyBuffer(old)
|
|
947
1035
|
gpuCapacity = next
|
|
948
1036
|
}
|
|
949
1037
|
|
|
@@ -956,8 +1044,8 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
956
1044
|
return frameIndex(verb, atlasIndex, opts.frame)
|
|
957
1045
|
}
|
|
958
1046
|
|
|
959
|
-
let
|
|
960
|
-
let at = sprite._slot *
|
|
1047
|
+
let writeSprite = (sprite: SpriteState, opts: SpriteOptions, atlas: number) => {
|
|
1048
|
+
let at = sprite._slot * SPRITE_FLOATS
|
|
961
1049
|
let changed = false
|
|
962
1050
|
let flipX = opts.flipX !== undefined && opts.flipX !== sprite._flipX
|
|
963
1051
|
let flipY = opts.flipY !== undefined && opts.flipY !== sprite._flipY
|
|
@@ -965,36 +1053,36 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
965
1053
|
if (flipY) sprite._flipY = !sprite._flipY
|
|
966
1054
|
if (opts.frame !== undefined) {
|
|
967
1055
|
let f = opts.frame
|
|
968
|
-
writeFrame(
|
|
969
|
-
|
|
1056
|
+
writeFrame(spriteData, at, f.u0, f.v0, f.u1, f.v1, sprite._flipX, sprite._flipY)
|
|
1057
|
+
spriteData[at + SPRITE_ATLAS_FIELD] = atlas
|
|
970
1058
|
changed = true
|
|
971
1059
|
} else if (flipX || flipY) {
|
|
972
1060
|
// No new frame: toggle the changed axes on the stored UVs.
|
|
973
|
-
writeFrame(
|
|
1061
|
+
writeFrame(spriteData, at, spriteData[at]!, spriteData[at + 1]!, spriteData[at + 2]!, spriteData[at + 3]!, flipX, flipY)
|
|
974
1062
|
changed = true
|
|
975
1063
|
}
|
|
976
1064
|
if (opts.tint !== undefined) {
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1065
|
+
spriteData[at + 4] = opts.tint[0]
|
|
1066
|
+
spriteData[at + 5] = opts.tint[1]
|
|
1067
|
+
spriteData[at + 6] = opts.tint[2]
|
|
1068
|
+
spriteData[at + 7] = opts.tint[3]
|
|
981
1069
|
changed = true
|
|
982
1070
|
}
|
|
983
1071
|
if (opts.renderOrder !== undefined) {
|
|
984
|
-
|
|
1072
|
+
spriteData[at + SPRITE_KEY_FIELD] = opts.renderOrder
|
|
985
1073
|
changed = true
|
|
986
1074
|
}
|
|
987
1075
|
if (opts.minScreenPx !== undefined) {
|
|
988
|
-
|
|
1076
|
+
spriteData[at + SPRITE_MIN_PX_FIELD] = opts.minScreenPx
|
|
989
1077
|
changed = true
|
|
990
1078
|
}
|
|
991
1079
|
if (opts.maxScreenPx !== undefined) {
|
|
992
|
-
|
|
1080
|
+
spriteData[at + SPRITE_MAX_PX_FIELD] = opts.maxScreenPx
|
|
993
1081
|
changed = true
|
|
994
1082
|
}
|
|
995
|
-
if (changed)
|
|
996
|
-
let min =
|
|
997
|
-
if (min > 0 ||
|
|
1083
|
+
if (changed) sprites.mark(sprite._slot, sprite._slot + 1)
|
|
1084
|
+
let min = spriteData[at + SPRITE_MIN_PX_FIELD]!
|
|
1085
|
+
if (min > 0 || spriteData[at + SPRITE_MAX_PX_FIELD]! > 0) {
|
|
998
1086
|
clamped.add(sprite)
|
|
999
1087
|
let r = floorReach(sprite._w, sprite._h, min)
|
|
1000
1088
|
if (r >= reach) reach = r
|
|
@@ -1008,13 +1096,14 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1008
1096
|
label,
|
|
1009
1097
|
pipeline: gpu.pipeline,
|
|
1010
1098
|
quad: gpu.quad,
|
|
1011
|
-
|
|
1012
|
-
buffers
|
|
1099
|
+
textures: { ...atlasBindings(atlases.map(a => a.texture)), ...material.textures },
|
|
1100
|
+
buffers,
|
|
1013
1101
|
count: () => published,
|
|
1014
1102
|
tint: () => tint,
|
|
1103
|
+
params: () => params,
|
|
1015
1104
|
pick: (x, y, zoom) => layer.pick(x, y, zoom),
|
|
1016
1105
|
// The order key one view entry declares: "y" on world y in the pose
|
|
1017
|
-
// record (slot 0), "renderOrder" on the app-owned key in the
|
|
1106
|
+
// record (slot 0), "renderOrder" on the app-owned key in the sprite
|
|
1018
1107
|
// record (slot 1); either way the core gathers BOTH buffers under the
|
|
1019
1108
|
// one permutation at every publish and republishes the sibling itself
|
|
1020
1109
|
// when the key buffer re-orders.
|
|
@@ -1022,12 +1111,13 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1022
1111
|
opts?.orderBy === "y"
|
|
1023
1112
|
? { field: POSE_Y_FIELD }
|
|
1024
1113
|
: opts?.orderBy === "renderOrder"
|
|
1025
|
-
? { field:
|
|
1114
|
+
? { field: SPRITE_KEY_FIELD, buffer: 2 }
|
|
1026
1115
|
: undefined,
|
|
1027
1116
|
})
|
|
1028
1117
|
|
|
1029
1118
|
let layer: SpriteLayer = {
|
|
1030
1119
|
atlases,
|
|
1120
|
+
material,
|
|
1031
1121
|
get count() {
|
|
1032
1122
|
return byNode.size
|
|
1033
1123
|
},
|
|
@@ -1037,6 +1127,12 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1037
1127
|
tint = next
|
|
1038
1128
|
views.setTint(next)
|
|
1039
1129
|
},
|
|
1130
|
+
setParams(next) {
|
|
1131
|
+
if (disposed) return
|
|
1132
|
+
checkParams("setParams", next)
|
|
1133
|
+
Object.assign(params, next)
|
|
1134
|
+
views.setParams(next)
|
|
1135
|
+
},
|
|
1040
1136
|
createView(vopts) {
|
|
1041
1137
|
if (disposed) throw new Error("createView: layer is disposed")
|
|
1042
1138
|
return views.create(vopts)
|
|
@@ -1072,7 +1168,7 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1072
1168
|
// the vertex stage draws.
|
|
1073
1169
|
if (reachDirty) {
|
|
1074
1170
|
reach = 0
|
|
1075
|
-
for (let s of clamped) reach = Math.max(reach, floorReach(s._w, s._h,
|
|
1171
|
+
for (let s of clamped) reach = Math.max(reach, floorReach(s._w, s._h, spriteData[s._slot * SPRITE_FLOATS + SPRITE_MIN_PX_FIELD]!))
|
|
1076
1172
|
reachDirty = false
|
|
1077
1173
|
}
|
|
1078
1174
|
let candidates = new Set<NodeId>()
|
|
@@ -1088,8 +1184,8 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1088
1184
|
spatial.worldMatrix(sprite.node!, WORLD)
|
|
1089
1185
|
let w = Math.hypot(WORLD[0]!, WORLD[1]!)
|
|
1090
1186
|
let h = Math.hypot(WORLD[4]!, WORLD[5]!)
|
|
1091
|
-
let at = sprite._slot *
|
|
1092
|
-
let scale = screenSizeScale(w, h, zoom,
|
|
1187
|
+
let at = sprite._slot * SPRITE_FLOATS
|
|
1188
|
+
let scale = screenSizeScale(w, h, zoom, spriteData[at + SPRITE_MIN_PX_FIELD]!, spriteData[at + SPRITE_MAX_PX_FIELD]!)
|
|
1093
1189
|
if (pointInSprite(x, y, WORLD[12]!, WORLD[13]!, w * scale, h * scale, Math.atan2(WORLD[1]!, WORLD[0]!))) out.push(sprite)
|
|
1094
1190
|
}
|
|
1095
1191
|
}
|
|
@@ -1199,18 +1295,18 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1199
1295
|
spatial.flush()
|
|
1200
1296
|
views.dispose()
|
|
1201
1297
|
destroyBuffer(pose)
|
|
1202
|
-
|
|
1203
|
-
|
|
1298
|
+
sprites.destroy()
|
|
1299
|
+
for (let s of styles) s.stream.destroy()
|
|
1204
1300
|
},
|
|
1205
1301
|
_add(opts) {
|
|
1206
1302
|
if (disposed) throw new Error("addSprite: layer is disposed")
|
|
1207
|
-
// The
|
|
1303
|
+
// The fields bag with its defaults - renderOrder and the clamp at 0
|
|
1208
1304
|
// explicitly, since a recycled slot holds the previous occupant's
|
|
1209
1305
|
// values otherwise - resolved before a slot or node is taken, so a
|
|
1210
1306
|
// bad frame allocates nothing. An absent key and an explicit
|
|
1211
1307
|
// undefined both take the default (the options convention
|
|
1212
1308
|
// everywhere; a plain spread would let undefined win).
|
|
1213
|
-
let
|
|
1309
|
+
let fields: SpriteOptions = {
|
|
1214
1310
|
...opts,
|
|
1215
1311
|
frame: opts?.frame ?? fullFrame(atlases[0]!),
|
|
1216
1312
|
tint: opts?.tint ?? [1, 1, 1, 1],
|
|
@@ -1218,7 +1314,7 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1218
1314
|
minScreenPx: opts?.minScreenPx ?? 0,
|
|
1219
1315
|
maxScreenPx: opts?.maxScreenPx ?? 0,
|
|
1220
1316
|
}
|
|
1221
|
-
let atlas = frameAtlas("addSprite",
|
|
1317
|
+
let atlas = frameAtlas("addSprite", fields)
|
|
1222
1318
|
if (opts?.parent && opts.parent.layer !== layer) throw new Error("addSprite: parent group belongs to another layer")
|
|
1223
1319
|
let slot = freeSlots.pop() ?? highWater++
|
|
1224
1320
|
if (slot >= gpuCapacity) grow(gpuCapacity * 2)
|
|
@@ -1248,20 +1344,22 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1248
1344
|
spatial.setBounds(node, COLUMN_BOUNDS)
|
|
1249
1345
|
spatial.bindPoseRecord(node, pose, slot)
|
|
1250
1346
|
byNode.set(node, sprite)
|
|
1251
|
-
|
|
1347
|
+
writeSprite(sprite, fields, atlas)
|
|
1252
1348
|
// No outline until a text run sets one: a recycled slot would
|
|
1253
|
-
// otherwise leak the previous occupant's.
|
|
1254
|
-
|
|
1349
|
+
// otherwise leak the previous occupant's. The material's sprites
|
|
1350
|
+
// slots start over for the same reason.
|
|
1351
|
+
spriteData.fill(0, slot * SPRITE_FLOATS + SPRITE_OUTLINE_FIELD, slot * SPRITE_FLOATS + SPRITE_OUTLINE_FIELD + 4)
|
|
1352
|
+
for (let style of styles) blankStyle(style, slot)
|
|
1255
1353
|
layer._schedule()
|
|
1256
1354
|
return sprite
|
|
1257
1355
|
},
|
|
1258
1356
|
_outline(sprite, r, g, b, width) {
|
|
1259
|
-
let at = sprite._slot *
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1357
|
+
let at = sprite._slot * SPRITE_FLOATS + SPRITE_OUTLINE_FIELD
|
|
1358
|
+
spriteData[at] = r
|
|
1359
|
+
spriteData[at + 1] = g
|
|
1360
|
+
spriteData[at + 2] = b
|
|
1361
|
+
spriteData[at + 3] = width
|
|
1362
|
+
sprites.mark(sprite._slot, sprite._slot + 1)
|
|
1265
1363
|
layer._schedule()
|
|
1266
1364
|
},
|
|
1267
1365
|
_write(sprite, opts) {
|
|
@@ -1289,25 +1387,25 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1289
1387
|
opts.maxScreenPx !== undefined ||
|
|
1290
1388
|
(moved && clamped.has(sprite))
|
|
1291
1389
|
) {
|
|
1292
|
-
|
|
1390
|
+
writeSprite(sprite, opts, atlas)
|
|
1293
1391
|
}
|
|
1294
|
-
if (moved ||
|
|
1392
|
+
if (moved || sprites.dirty) layer._schedule()
|
|
1295
1393
|
},
|
|
1296
1394
|
_read(sprite) {
|
|
1297
|
-
let at = sprite._slot *
|
|
1395
|
+
let at = sprite._slot * SPRITE_FLOATS
|
|
1298
1396
|
return {
|
|
1299
1397
|
x: sprite._x,
|
|
1300
1398
|
y: sprite._y,
|
|
1301
1399
|
w: sprite._w,
|
|
1302
1400
|
h: sprite._h,
|
|
1303
|
-
frame: readFrame(
|
|
1401
|
+
frame: readFrame(spriteData, at, sprite, atlases[spriteData[at + SPRITE_ATLAS_FIELD]!]!.texture),
|
|
1304
1402
|
flipX: sprite._flipX,
|
|
1305
1403
|
flipY: sprite._flipY,
|
|
1306
1404
|
rotation: sprite._rot,
|
|
1307
|
-
tint: [
|
|
1308
|
-
minScreenPx:
|
|
1309
|
-
maxScreenPx:
|
|
1310
|
-
renderOrder:
|
|
1405
|
+
tint: [spriteData[at + 4]!, spriteData[at + 5]!, spriteData[at + 6]!, spriteData[at + 7]!],
|
|
1406
|
+
minScreenPx: spriteData[at + SPRITE_MIN_PX_FIELD]!,
|
|
1407
|
+
maxScreenPx: spriteData[at + SPRITE_MAX_PX_FIELD]!,
|
|
1408
|
+
renderOrder: spriteData[at + SPRITE_KEY_FIELD]!,
|
|
1311
1409
|
visible: sprite._visible,
|
|
1312
1410
|
}
|
|
1313
1411
|
},
|
|
@@ -1351,6 +1449,13 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1351
1449
|
if (disposed) return
|
|
1352
1450
|
spatial.setTransition(root, ms === null ? null : { stagger: ms })
|
|
1353
1451
|
},
|
|
1452
|
+
setTimeScale(scale) {
|
|
1453
|
+
if (disposed) return
|
|
1454
|
+
spatial.setTimeScale(root, scale)
|
|
1455
|
+
},
|
|
1456
|
+
timeRate() {
|
|
1457
|
+
return disposed ? 1 : spatial.timeRate(root)
|
|
1458
|
+
},
|
|
1354
1459
|
_schedule() {
|
|
1355
1460
|
if (disposed || scheduled) return
|
|
1356
1461
|
scheduled = true
|
|
@@ -1358,10 +1463,11 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1358
1463
|
},
|
|
1359
1464
|
_groups: new Set(),
|
|
1360
1465
|
_root: root,
|
|
1361
|
-
_stream:
|
|
1466
|
+
_stream: sprites,
|
|
1362
1467
|
get _records() {
|
|
1363
|
-
return
|
|
1468
|
+
return spriteData
|
|
1364
1469
|
},
|
|
1470
|
+
_styles: styles,
|
|
1365
1471
|
}
|
|
1366
1472
|
if (opts?.autoFree !== false && getOwner()) onCleanup(() => layer.dispose())
|
|
1367
1473
|
return layer
|
|
@@ -1425,7 +1531,8 @@ export function destroySprite(sprite: Sprite): void {
|
|
|
1425
1531
|
* through every enclosing group (@solidrt/3d's worldPosition, Three's
|
|
1426
1532
|
* getWorldPosition), read from the core's world matrix as the tree stands
|
|
1427
1533
|
* now, pending writes included. Under a transition this is the
|
|
1428
|
-
* mid-flight pose
|
|
1534
|
+
* mid-flight pose of THIS frame (native motion steps before any of the
|
|
1535
|
+
* frame's JS) - what picking and the screen show - where the handle's
|
|
1429
1536
|
* own fields hold the target. A record layer's sprite has no groups, so
|
|
1430
1537
|
* its world position is its own, read as getSprite reads it (the records
|
|
1431
1538
|
* array). A fresh pair per call; null once the handle is inert.
|
|
@@ -1588,6 +1695,38 @@ export function setGroupTransition(group: SpriteGroup, transition: SpriteTransit
|
|
|
1588
1695
|
declareTransition(group.node, group, node)
|
|
1589
1696
|
}
|
|
1590
1697
|
|
|
1698
|
+
/**
|
|
1699
|
+
* Declare (null clears) the rate the sprite's own native motion runs at
|
|
1700
|
+
* (its transitions, with their delays, enter and exit): 0 freezes it, 1
|
|
1701
|
+
* is app time, other values slow or fast motion. Overrides the layer's
|
|
1702
|
+
* and the enclosing groups' rate for this sprite alone (the nearest
|
|
1703
|
+
* declaring node wins); see SpriteLayer.setTimeScale. Node layer only.
|
|
1704
|
+
*/
|
|
1705
|
+
export function setSpriteTimeScale(sprite: Sprite, scale: number | null): void {
|
|
1706
|
+
if (sprite.layer === null) return
|
|
1707
|
+
if (sprite.node === null) throw new Error("setSpriteTimeScale: record sprites have no native motion")
|
|
1708
|
+
spatial.setTimeScale(sprite.node, scale)
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/** The group counterpart of setSpriteTimeScale: the rate everything under
|
|
1712
|
+
* the group runs at, down to the next group or sprite declaring its own
|
|
1713
|
+
* - a frozen world with one live group inside it. */
|
|
1714
|
+
export function setGroupTimeScale(group: SpriteGroup, scale: number | null): void {
|
|
1715
|
+
if (group.layer === null) return
|
|
1716
|
+
spatial.setTimeScale(group.node, scale)
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* The rate a sprite's or group's motion runs at: its own time scale, else
|
|
1721
|
+
* the nearest declaring group's, else the layer's, else 1. What JS-driven
|
|
1722
|
+
* motion of the handle multiplies its own frame step by. Null for a
|
|
1723
|
+
* destroyed handle or a record layer's sprite (no native motion).
|
|
1724
|
+
*/
|
|
1725
|
+
export function timeRate(target: Sprite | SpriteGroup): number | null {
|
|
1726
|
+
if (target.layer === null || target.node === null) return null
|
|
1727
|
+
return spatial.timeRate(target.node)
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1591
1730
|
/** Add a transform group (see SpriteGroup). */
|
|
1592
1731
|
export function addGroup(layer: SpriteLayer, opts?: GroupOptions): SpriteGroup {
|
|
1593
1732
|
let group = {
|