@solidrt/2d 0.0.70 → 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 +222 -40
- 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 +5 -4
- package/src/animation.ts +26 -5
- package/src/atlas.ts +8 -4
- package/src/camera.ts +2 -2
- package/src/components/camera2d.tsx +2 -2
- package/src/components/group.tsx +15 -3
- package/src/components/shots.tsx +2 -2
- package/src/components/sprite-layer.tsx +40 -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 +260 -113
- package/src/material.ts +368 -0
- package/src/records.ts +198 -104
- package/src/style.ts +98 -0
- package/src/text.ts +64 -11
- 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.
|
|
@@ -35,8 +43,8 @@
|
|
|
35
43
|
// (dispatch.ts) undoes it with unprojectCamera.
|
|
36
44
|
import { getOwner, onBeforeRender, onCleanup, runWithOwner } from "@solidrt/core"
|
|
37
45
|
import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEvent } from "@solidrt/core"
|
|
38
|
-
import {
|
|
39
|
-
import type {
|
|
46
|
+
import { checkScreenSize, createBuffer, createRecordStream, destroyBuffer, screenSizeScale } from "@solidrt/core/gpu"
|
|
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.
|
|
@@ -591,6 +646,17 @@ export type LayerBase = {
|
|
|
591
646
|
_read(sprite: Sprite): Required<SpriteOptions>
|
|
592
647
|
_destroy(sprite: SpriteState): void
|
|
593
648
|
_schedule(): void
|
|
649
|
+
/** The layer's JS-owned record stream, what records()/updateRecords
|
|
650
|
+
* read and mark: a records layer's whole records, a node layer's
|
|
651
|
+
* sprite records (its poses are the core's). Internal. */
|
|
652
|
+
_stream: RecordStream
|
|
653
|
+
/** The stream's mirror as floats, re-pointed by growth; records() hands
|
|
654
|
+
* it out. Internal. */
|
|
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[]
|
|
594
660
|
}
|
|
595
661
|
|
|
596
662
|
/** A query volume for overlap/sweep/moveAndSlide, in layer pixels: a
|
|
@@ -719,6 +785,20 @@ export type SpriteLayer = LayerBase & {
|
|
|
719
785
|
* straight under the layer that begin in one frame (SpriteLayerOptions
|
|
720
786
|
* `stagger`). */
|
|
721
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
|
|
722
802
|
/** The core node every sprite and group of the layer sits under: what
|
|
723
803
|
* scopes the layer's queries in the arena shared with 3d scenes. */
|
|
724
804
|
_root: NodeId
|
|
@@ -826,9 +906,22 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
826
906
|
let label = opts?.label ?? "sprites"
|
|
827
907
|
let tint = opts?.tint ?? [1, 1, 1, 1]
|
|
828
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 }
|
|
829
914
|
let pose: BufferId = createBuffer(capacity * POSE_FLOATS * 4, { label: `${label}-pose`, autoFree: false })
|
|
830
|
-
|
|
831
|
-
|
|
915
|
+
// The sprite records: JS-written through the sprite verbs (writeSprite),
|
|
916
|
+
// records()/updateRecords for a bulk restyle, published by the flush as
|
|
917
|
+
// the dirty slot range, or the used prefix whole under an order (the
|
|
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)
|
|
923
|
+
let ordered = opts?.orderBy !== undefined
|
|
924
|
+
let gpu = material.pipeline(atlases, [POSE_ATTRIBUTES, SPRITE_ATTRIBUTES], label)
|
|
832
925
|
// The sprites with a screen-size clamp (either bound on), and the
|
|
833
926
|
// furthest any of them reaches from its center at its floor (floorReach,
|
|
834
927
|
// view pixels): the candidate box a pick searches around the pointer. A
|
|
@@ -847,13 +940,12 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
847
940
|
// ceiling set alone must not drop below the floor already there.
|
|
848
941
|
let checkClamp = (verb: string, sprite: SpriteState, opts: SpriteOptions): void => {
|
|
849
942
|
if (opts.minScreenPx === undefined && opts.maxScreenPx === undefined) return
|
|
850
|
-
let at = sprite._slot *
|
|
851
|
-
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]!)
|
|
852
945
|
}
|
|
853
946
|
|
|
854
947
|
let disposed = false
|
|
855
948
|
let scheduled = false
|
|
856
|
-
let styleDirty = false
|
|
857
949
|
let published = 0
|
|
858
950
|
// The publish pass of every frame runs the pending flush ahead of the
|
|
859
951
|
// paint, so a write made anywhere in the frame's JS is in that frame's
|
|
@@ -878,7 +970,7 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
878
970
|
// slot stays taken until the core says it is gone; dispose frees them.
|
|
879
971
|
let leaving = new Set<NodeId>()
|
|
880
972
|
let gpuCapacity = capacity
|
|
881
|
-
let
|
|
973
|
+
let spriteData = new Float32Array(sprites.bytes.buffer)
|
|
882
974
|
let byNode = new Map<NodeId, SpriteState>()
|
|
883
975
|
// The layer's root node (identity): parentless sprites and groups hang
|
|
884
976
|
// off it, so a query scoped to it sees exactly this layer.
|
|
@@ -886,6 +978,7 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
886
978
|
let root = spatial.createNode(TRANSFORM, true)
|
|
887
979
|
let filter: QueryFilter = { root }
|
|
888
980
|
if (opts?.stagger !== undefined) spatial.setTransition(root, { stagger: opts.stagger })
|
|
981
|
+
if (opts?.timeScale !== undefined) spatial.setTimeScale(root, opts.timeScale)
|
|
889
982
|
// Refill the layer's one filter for a query: the root always, the
|
|
890
983
|
// include-list as the sprites' nodes when given.
|
|
891
984
|
let queryFilter = (opts: QueryOptions | undefined, site: string): void => {
|
|
@@ -909,12 +1002,8 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
909
1002
|
let flush = () => {
|
|
910
1003
|
scheduled = false
|
|
911
1004
|
if (disposed) return
|
|
912
|
-
if (
|
|
913
|
-
|
|
914
|
-
let out = beginBufferWrite(style)
|
|
915
|
-
out.set(styleData.subarray(0, highWater * STYLE_FLOATS))
|
|
916
|
-
endBufferWrite(style, highWater * STYLE_FLOATS * 4)
|
|
917
|
-
}
|
|
1005
|
+
if (sprites.dirty) sprites.publish(ordered, highWater)
|
|
1006
|
+
for (let s of styles) if (s.stream.dirty) s.stream.publish(ordered, highWater)
|
|
918
1007
|
if (published !== highWater) {
|
|
919
1008
|
views.setCount(highWater)
|
|
920
1009
|
published = highWater
|
|
@@ -924,25 +1013,26 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
924
1013
|
spatial.flush()
|
|
925
1014
|
}
|
|
926
1015
|
|
|
927
|
-
//
|
|
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
|
|
928
1020
|
// retargetRecords call (the whole used range republishes at the next
|
|
929
|
-
// flush), the
|
|
930
|
-
// The entries hold the old buffers alive until
|
|
931
|
-
// destroys are safe to issue right after.
|
|
1021
|
+
// flush), the record streams grow into replacements marked whole, which
|
|
1022
|
+
// the next flush publishes. The entries hold the old buffers alive until
|
|
1023
|
+
// the swaps land, so the destroys are safe to issue right after.
|
|
932
1024
|
let grow = (next: number) => {
|
|
933
|
-
let
|
|
934
|
-
|
|
935
|
-
spatial.retargetRecords(
|
|
936
|
-
let
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
views.setBuffers(
|
|
940
|
-
destroyBuffer(
|
|
941
|
-
destroyBuffer(
|
|
942
|
-
|
|
943
|
-
style = newStyle
|
|
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)
|
|
944
1035
|
gpuCapacity = next
|
|
945
|
-
styleDirty = true
|
|
946
1036
|
}
|
|
947
1037
|
|
|
948
1038
|
// A frame's atlas index, or a throw: run before ANY field of a write
|
|
@@ -954,43 +1044,45 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
954
1044
|
return frameIndex(verb, atlasIndex, opts.frame)
|
|
955
1045
|
}
|
|
956
1046
|
|
|
957
|
-
let
|
|
958
|
-
let at = sprite._slot *
|
|
1047
|
+
let writeSprite = (sprite: SpriteState, opts: SpriteOptions, atlas: number) => {
|
|
1048
|
+
let at = sprite._slot * SPRITE_FLOATS
|
|
1049
|
+
let changed = false
|
|
959
1050
|
let flipX = opts.flipX !== undefined && opts.flipX !== sprite._flipX
|
|
960
1051
|
let flipY = opts.flipY !== undefined && opts.flipY !== sprite._flipY
|
|
961
1052
|
if (flipX) sprite._flipX = !sprite._flipX
|
|
962
1053
|
if (flipY) sprite._flipY = !sprite._flipY
|
|
963
1054
|
if (opts.frame !== undefined) {
|
|
964
1055
|
let f = opts.frame
|
|
965
|
-
writeFrame(
|
|
966
|
-
|
|
967
|
-
|
|
1056
|
+
writeFrame(spriteData, at, f.u0, f.v0, f.u1, f.v1, sprite._flipX, sprite._flipY)
|
|
1057
|
+
spriteData[at + SPRITE_ATLAS_FIELD] = atlas
|
|
1058
|
+
changed = true
|
|
968
1059
|
} else if (flipX || flipY) {
|
|
969
1060
|
// No new frame: toggle the changed axes on the stored UVs.
|
|
970
|
-
writeFrame(
|
|
971
|
-
|
|
1061
|
+
writeFrame(spriteData, at, spriteData[at]!, spriteData[at + 1]!, spriteData[at + 2]!, spriteData[at + 3]!, flipX, flipY)
|
|
1062
|
+
changed = true
|
|
972
1063
|
}
|
|
973
1064
|
if (opts.tint !== undefined) {
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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]
|
|
1069
|
+
changed = true
|
|
979
1070
|
}
|
|
980
1071
|
if (opts.renderOrder !== undefined) {
|
|
981
|
-
|
|
982
|
-
|
|
1072
|
+
spriteData[at + SPRITE_KEY_FIELD] = opts.renderOrder
|
|
1073
|
+
changed = true
|
|
983
1074
|
}
|
|
984
1075
|
if (opts.minScreenPx !== undefined) {
|
|
985
|
-
|
|
986
|
-
|
|
1076
|
+
spriteData[at + SPRITE_MIN_PX_FIELD] = opts.minScreenPx
|
|
1077
|
+
changed = true
|
|
987
1078
|
}
|
|
988
1079
|
if (opts.maxScreenPx !== undefined) {
|
|
989
|
-
|
|
990
|
-
|
|
1080
|
+
spriteData[at + SPRITE_MAX_PX_FIELD] = opts.maxScreenPx
|
|
1081
|
+
changed = true
|
|
991
1082
|
}
|
|
992
|
-
|
|
993
|
-
|
|
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) {
|
|
994
1086
|
clamped.add(sprite)
|
|
995
1087
|
let r = floorReach(sprite._w, sprite._h, min)
|
|
996
1088
|
if (r >= reach) reach = r
|
|
@@ -1004,13 +1096,14 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1004
1096
|
label,
|
|
1005
1097
|
pipeline: gpu.pipeline,
|
|
1006
1098
|
quad: gpu.quad,
|
|
1007
|
-
|
|
1008
|
-
buffers
|
|
1099
|
+
textures: { ...atlasBindings(atlases.map(a => a.texture)), ...material.textures },
|
|
1100
|
+
buffers,
|
|
1009
1101
|
count: () => published,
|
|
1010
1102
|
tint: () => tint,
|
|
1103
|
+
params: () => params,
|
|
1011
1104
|
pick: (x, y, zoom) => layer.pick(x, y, zoom),
|
|
1012
1105
|
// The order key one view entry declares: "y" on world y in the pose
|
|
1013
|
-
// record (slot 0), "renderOrder" on the app-owned key in the
|
|
1106
|
+
// record (slot 0), "renderOrder" on the app-owned key in the sprite
|
|
1014
1107
|
// record (slot 1); either way the core gathers BOTH buffers under the
|
|
1015
1108
|
// one permutation at every publish and republishes the sibling itself
|
|
1016
1109
|
// when the key buffer re-orders.
|
|
@@ -1018,12 +1111,13 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1018
1111
|
opts?.orderBy === "y"
|
|
1019
1112
|
? { field: POSE_Y_FIELD }
|
|
1020
1113
|
: opts?.orderBy === "renderOrder"
|
|
1021
|
-
? { field:
|
|
1114
|
+
? { field: SPRITE_KEY_FIELD, buffer: 2 }
|
|
1022
1115
|
: undefined,
|
|
1023
1116
|
})
|
|
1024
1117
|
|
|
1025
1118
|
let layer: SpriteLayer = {
|
|
1026
1119
|
atlases,
|
|
1120
|
+
material,
|
|
1027
1121
|
get count() {
|
|
1028
1122
|
return byNode.size
|
|
1029
1123
|
},
|
|
@@ -1033,6 +1127,12 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1033
1127
|
tint = next
|
|
1034
1128
|
views.setTint(next)
|
|
1035
1129
|
},
|
|
1130
|
+
setParams(next) {
|
|
1131
|
+
if (disposed) return
|
|
1132
|
+
checkParams("setParams", next)
|
|
1133
|
+
Object.assign(params, next)
|
|
1134
|
+
views.setParams(next)
|
|
1135
|
+
},
|
|
1036
1136
|
createView(vopts) {
|
|
1037
1137
|
if (disposed) throw new Error("createView: layer is disposed")
|
|
1038
1138
|
return views.create(vopts)
|
|
@@ -1068,7 +1168,7 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1068
1168
|
// the vertex stage draws.
|
|
1069
1169
|
if (reachDirty) {
|
|
1070
1170
|
reach = 0
|
|
1071
|
-
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]!))
|
|
1072
1172
|
reachDirty = false
|
|
1073
1173
|
}
|
|
1074
1174
|
let candidates = new Set<NodeId>()
|
|
@@ -1084,8 +1184,8 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1084
1184
|
spatial.worldMatrix(sprite.node!, WORLD)
|
|
1085
1185
|
let w = Math.hypot(WORLD[0]!, WORLD[1]!)
|
|
1086
1186
|
let h = Math.hypot(WORLD[4]!, WORLD[5]!)
|
|
1087
|
-
let at = sprite._slot *
|
|
1088
|
-
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]!)
|
|
1089
1189
|
if (pointInSprite(x, y, WORLD[12]!, WORLD[13]!, w * scale, h * scale, Math.atan2(WORLD[1]!, WORLD[0]!))) out.push(sprite)
|
|
1090
1190
|
}
|
|
1091
1191
|
}
|
|
@@ -1195,18 +1295,18 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1195
1295
|
spatial.flush()
|
|
1196
1296
|
views.dispose()
|
|
1197
1297
|
destroyBuffer(pose)
|
|
1198
|
-
|
|
1199
|
-
|
|
1298
|
+
sprites.destroy()
|
|
1299
|
+
for (let s of styles) s.stream.destroy()
|
|
1200
1300
|
},
|
|
1201
1301
|
_add(opts) {
|
|
1202
1302
|
if (disposed) throw new Error("addSprite: layer is disposed")
|
|
1203
|
-
// The
|
|
1303
|
+
// The fields bag with its defaults - renderOrder and the clamp at 0
|
|
1204
1304
|
// explicitly, since a recycled slot holds the previous occupant's
|
|
1205
1305
|
// values otherwise - resolved before a slot or node is taken, so a
|
|
1206
1306
|
// bad frame allocates nothing. An absent key and an explicit
|
|
1207
1307
|
// undefined both take the default (the options convention
|
|
1208
1308
|
// everywhere; a plain spread would let undefined win).
|
|
1209
|
-
let
|
|
1309
|
+
let fields: SpriteOptions = {
|
|
1210
1310
|
...opts,
|
|
1211
1311
|
frame: opts?.frame ?? fullFrame(atlases[0]!),
|
|
1212
1312
|
tint: opts?.tint ?? [1, 1, 1, 1],
|
|
@@ -1214,7 +1314,7 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1214
1314
|
minScreenPx: opts?.minScreenPx ?? 0,
|
|
1215
1315
|
maxScreenPx: opts?.maxScreenPx ?? 0,
|
|
1216
1316
|
}
|
|
1217
|
-
let atlas = frameAtlas("addSprite",
|
|
1317
|
+
let atlas = frameAtlas("addSprite", fields)
|
|
1218
1318
|
if (opts?.parent && opts.parent.layer !== layer) throw new Error("addSprite: parent group belongs to another layer")
|
|
1219
1319
|
let slot = freeSlots.pop() ?? highWater++
|
|
1220
1320
|
if (slot >= gpuCapacity) grow(gpuCapacity * 2)
|
|
@@ -1244,20 +1344,22 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1244
1344
|
spatial.setBounds(node, COLUMN_BOUNDS)
|
|
1245
1345
|
spatial.bindPoseRecord(node, pose, slot)
|
|
1246
1346
|
byNode.set(node, sprite)
|
|
1247
|
-
|
|
1347
|
+
writeSprite(sprite, fields, atlas)
|
|
1248
1348
|
// No outline until a text run sets one: a recycled slot would
|
|
1249
|
-
// otherwise leak the previous occupant's.
|
|
1250
|
-
|
|
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)
|
|
1251
1353
|
layer._schedule()
|
|
1252
1354
|
return sprite
|
|
1253
1355
|
},
|
|
1254
1356
|
_outline(sprite, r, g, b, width) {
|
|
1255
|
-
let at = sprite._slot *
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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)
|
|
1261
1363
|
layer._schedule()
|
|
1262
1364
|
},
|
|
1263
1365
|
_write(sprite, opts) {
|
|
@@ -1285,25 +1387,25 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1285
1387
|
opts.maxScreenPx !== undefined ||
|
|
1286
1388
|
(moved && clamped.has(sprite))
|
|
1287
1389
|
) {
|
|
1288
|
-
|
|
1390
|
+
writeSprite(sprite, opts, atlas)
|
|
1289
1391
|
}
|
|
1290
|
-
if (moved ||
|
|
1392
|
+
if (moved || sprites.dirty) layer._schedule()
|
|
1291
1393
|
},
|
|
1292
1394
|
_read(sprite) {
|
|
1293
|
-
let at = sprite._slot *
|
|
1395
|
+
let at = sprite._slot * SPRITE_FLOATS
|
|
1294
1396
|
return {
|
|
1295
1397
|
x: sprite._x,
|
|
1296
1398
|
y: sprite._y,
|
|
1297
1399
|
w: sprite._w,
|
|
1298
1400
|
h: sprite._h,
|
|
1299
|
-
frame: readFrame(
|
|
1401
|
+
frame: readFrame(spriteData, at, sprite, atlases[spriteData[at + SPRITE_ATLAS_FIELD]!]!.texture),
|
|
1300
1402
|
flipX: sprite._flipX,
|
|
1301
1403
|
flipY: sprite._flipY,
|
|
1302
1404
|
rotation: sprite._rot,
|
|
1303
|
-
tint: [
|
|
1304
|
-
minScreenPx:
|
|
1305
|
-
maxScreenPx:
|
|
1306
|
-
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]!,
|
|
1307
1409
|
visible: sprite._visible,
|
|
1308
1410
|
}
|
|
1309
1411
|
},
|
|
@@ -1347,6 +1449,13 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1347
1449
|
if (disposed) return
|
|
1348
1450
|
spatial.setTransition(root, ms === null ? null : { stagger: ms })
|
|
1349
1451
|
},
|
|
1452
|
+
setTimeScale(scale) {
|
|
1453
|
+
if (disposed) return
|
|
1454
|
+
spatial.setTimeScale(root, scale)
|
|
1455
|
+
},
|
|
1456
|
+
timeRate() {
|
|
1457
|
+
return disposed ? 1 : spatial.timeRate(root)
|
|
1458
|
+
},
|
|
1350
1459
|
_schedule() {
|
|
1351
1460
|
if (disposed || scheduled) return
|
|
1352
1461
|
scheduled = true
|
|
@@ -1354,6 +1463,11 @@ export function createSpriteLayer(atlases: Atlas[], opts?: SpriteLayerOptions):
|
|
|
1354
1463
|
},
|
|
1355
1464
|
_groups: new Set(),
|
|
1356
1465
|
_root: root,
|
|
1466
|
+
_stream: sprites,
|
|
1467
|
+
get _records() {
|
|
1468
|
+
return spriteData
|
|
1469
|
+
},
|
|
1470
|
+
_styles: styles,
|
|
1357
1471
|
}
|
|
1358
1472
|
if (opts?.autoFree !== false && getOwner()) onCleanup(() => layer.dispose())
|
|
1359
1473
|
return layer
|
|
@@ -1417,7 +1531,8 @@ export function destroySprite(sprite: Sprite): void {
|
|
|
1417
1531
|
* through every enclosing group (@solidrt/3d's worldPosition, Three's
|
|
1418
1532
|
* getWorldPosition), read from the core's world matrix as the tree stands
|
|
1419
1533
|
* now, pending writes included. Under a transition this is the
|
|
1420
|
-
* 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
|
|
1421
1536
|
* own fields hold the target. A record layer's sprite has no groups, so
|
|
1422
1537
|
* its world position is its own, read as getSprite reads it (the records
|
|
1423
1538
|
* array). A fresh pair per call; null once the handle is inert.
|
|
@@ -1580,6 +1695,38 @@ export function setGroupTransition(group: SpriteGroup, transition: SpriteTransit
|
|
|
1580
1695
|
declareTransition(group.node, group, node)
|
|
1581
1696
|
}
|
|
1582
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
|
+
|
|
1583
1730
|
/** Add a transform group (see SpriteGroup). */
|
|
1584
1731
|
export function addGroup(layer: SpriteLayer, opts?: GroupOptions): SpriteGroup {
|
|
1585
1732
|
let group = {
|