@solidrt/2d 0.0.55 → 0.0.56
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 +241 -57
- package/examples/README.md +22 -5
- package/examples/anim.tsx +86 -0
- package/examples/camera-probe.tsx +200 -0
- package/examples/camera.tsx +190 -0
- package/examples/parity.tsx +4 -4
- package/examples/pick.tsx +50 -33
- package/examples/tiles.tsx +19 -10
- package/package.json +3 -3
- package/src/animation.ts +178 -0
- package/src/camera-motion.ts +532 -0
- package/src/camera.ts +86 -0
- package/src/camera2d.ts +140 -0
- package/src/components.tsx +330 -76
- package/src/dispatch.ts +231 -0
- package/src/index.ts +30 -5
- package/src/layer.ts +454 -186
- package/src/oversample-math.ts +73 -0
- package/src/oversample.ts +39 -11
- package/src/records.ts +132 -27
- package/src/shaders.ts +29 -12
- package/src/tiles.ts +79 -24
package/src/layer.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
//
|
|
12
12
|
// Two instance-buffer slots split ownership: slot 0 is the pose buffer,
|
|
13
13
|
// written ONLY by the core (one coalesced write per flush however many
|
|
14
|
-
// nodes moved); slot 1 is the style buffer [u0, v0, u1, v1, tint rgba
|
|
15
|
-
// JS-owned and published through the zero-copy write lease. Never write
|
|
14
|
+
// nodes moved); slot 1 is the style buffer [u0, v0, u1, v1, tint rgba,
|
|
15
|
+
// renderOrder], JS-owned and published through the zero-copy write lease. Never write
|
|
16
16
|
// the pose buffer from JS - the core's staging mirror is the owner and
|
|
17
17
|
// will overwrite. For motion only JS can compute at large populations, the
|
|
18
18
|
// records layer (records.ts) is the escape hatch.
|
|
@@ -20,9 +20,12 @@
|
|
|
20
20
|
// Sprites hold FIXED instance slots (freed slots recycle): draw order is
|
|
21
21
|
// slot order, so removal never shifts records and pose sinks never rebind.
|
|
22
22
|
// Layer space is pixels, top-left origin, y-down - the render tree's
|
|
23
|
-
// frame. The camera
|
|
23
|
+
// frame. The camera (offset, zoom, rotation about a pivot - CameraUpdate
|
|
24
|
+
// in camera.ts) is a shared-params write (uCamera + uCameraRot), never
|
|
25
|
+
// per-sprite; pointer dispatch (dispatch.ts) undoes it with
|
|
26
|
+
// unprojectCamera.
|
|
24
27
|
import { getOwner, onCleanup } from "@solidrt/core"
|
|
25
|
-
import type { PointerEvent as ElementPointerEvent } from "@solidrt/core"
|
|
28
|
+
import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEvent } from "@solidrt/core"
|
|
26
29
|
import {
|
|
27
30
|
beginBufferWrite,
|
|
28
31
|
createBuffer,
|
|
@@ -38,7 +41,10 @@ import type { BufferId, TextureId } from "@solidrt/core/gpu"
|
|
|
38
41
|
import * as spatial from "flux:spatial"
|
|
39
42
|
import type { NodeId, NodeTransition } from "flux:spatial"
|
|
40
43
|
import { on } from "srt:events"
|
|
41
|
-
import {
|
|
44
|
+
import { checkCamera, projectCamera, unprojectCamera } from "./camera.ts"
|
|
45
|
+
import type { CameraState, CameraUpdate } from "./camera.ts"
|
|
46
|
+
import { spriteDispatch } from "./dispatch.ts"
|
|
47
|
+
import { checkOversample, thrashSentinel } from "./oversample.ts"
|
|
42
48
|
import type { Frame } from "./frames.ts"
|
|
43
49
|
import { FULL_FRAME, writeFrame } from "./frames.ts"
|
|
44
50
|
import type { RecordLayer } from "./records.ts"
|
|
@@ -46,8 +52,16 @@ import { FRAGMENT, INSTANCE_ATTRIBUTES_SPLIT, VERTEX_SPLIT } from "./shaders.ts"
|
|
|
46
52
|
|
|
47
53
|
/** Floats per pose record (the core's Pose2D projection). */
|
|
48
54
|
export const POSE_FLOATS = 5
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
|
|
56
|
+
// Float offset of world y in a pose record - what `orderBy: "y"` keys on.
|
|
57
|
+
const POSE_Y_FIELD = 1
|
|
58
|
+
/** Floats per style record:
|
|
59
|
+
* [u0, v0, u1, v1, tintR, tintG, tintB, tintA, renderOrder]. */
|
|
60
|
+
export const STYLE_FLOATS = 9
|
|
61
|
+
|
|
62
|
+
// Float offset of renderOrder in a style record - what `orderBy: "renderOrder"`
|
|
63
|
+
// keys on.
|
|
64
|
+
const STYLE_KEY_FIELD = 8
|
|
51
65
|
|
|
52
66
|
const RESOLVED = Promise.resolve()
|
|
53
67
|
|
|
@@ -56,7 +70,9 @@ const TRANSFORM = new Float32Array(10)
|
|
|
56
70
|
const FLAT_BOUNDS = new Float32Array([-0.5, -0.5, 0, 0.5, 0.5, 0])
|
|
57
71
|
const RAY_ORIGIN = new Float32Array(3)
|
|
58
72
|
const RAY_DIR = new Float32Array([0, 0, 1])
|
|
59
|
-
const BOX = new Float32Array(
|
|
73
|
+
const BOX = new Float32Array([0, 0, 0, 0, 0, 1, 0, 0, 0, 1])
|
|
74
|
+
// worldPosition's world-matrix scratch (column-major; translation at 12, 13).
|
|
75
|
+
const WORLD = new Float32Array(16)
|
|
60
76
|
|
|
61
77
|
// Settle routing: the core's "spatialTransitionEnd" event carries the node
|
|
62
78
|
// id, so the handles with a transition DECLARED are indexed by node (only
|
|
@@ -91,37 +107,60 @@ function declareTransition(node: NodeId, handle: Sprite | SpriteGroup, transitio
|
|
|
91
107
|
* fields - they touch no GPU state (the scene-graph handler rule).
|
|
92
108
|
*/
|
|
93
109
|
export type Sprite = {
|
|
94
|
-
/** The owning layer, null after removeSprite. */
|
|
95
|
-
layer: SpriteLayer | RecordLayer | null
|
|
110
|
+
/** The owning layer, null after removeSprite (which owns the write). */
|
|
111
|
+
readonly layer: SpriteLayer | RecordLayer | null
|
|
96
112
|
/**
|
|
97
113
|
* The sprite's SPATIAL ARENA node - the citizenship handle: bind core
|
|
98
114
|
* producers to it or reach it through flux:spatial directly (the layer
|
|
99
115
|
* still owns the node's life; destroy it only via removeSprite). Null
|
|
100
116
|
* on a record layer's sprites.
|
|
101
117
|
*/
|
|
102
|
-
node: NodeId | null
|
|
118
|
+
readonly node: NodeId | null
|
|
103
119
|
/** Instance slot: fixed for the sprite's life on the node layer, the
|
|
104
|
-
* shifting draw-order index on a record layer.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
* shifting draw-order index on a record layer. Readable (readonly, like
|
|
121
|
+
* the other underscore fields: cheap reads without getSprite's
|
|
122
|
+
* allocation); writes go through setSprite. */
|
|
123
|
+
readonly _slot: number
|
|
124
|
+
/** Pose mirror (node layer): what setSprite composes transforms from.
|
|
125
|
+
* On a RECORD layer this mirror goes stale once records are written
|
|
126
|
+
* raw - there the records array is the truth. */
|
|
127
|
+
readonly _x: number
|
|
128
|
+
readonly _y: number
|
|
129
|
+
readonly _w: number
|
|
130
|
+
readonly _h: number
|
|
131
|
+
readonly _rot: number
|
|
132
|
+
/** Mirror flags (both layer kinds): re-applied to every frame write.
|
|
133
|
+
* The cheap way to read flip state. */
|
|
134
|
+
readonly _flipX: boolean
|
|
135
|
+
readonly _flipY: boolean
|
|
136
|
+
/** Visibility mirror (node layer; always true on a record layer's
|
|
137
|
+
* sprites). The cheap read; writes go through setSprite. */
|
|
138
|
+
readonly _visible: boolean
|
|
139
|
+
/** The enclosing group (null at the layer root, always null on a record
|
|
140
|
+
* layer's sprites) - the bubble path; writes go through
|
|
141
|
+
* setSpriteParent/addSprite's `parent`. */
|
|
142
|
+
readonly _parent: SpriteGroup | null
|
|
115
143
|
onPointerDown?: (event: SpritePointerEvent) => void
|
|
116
144
|
onPointerMove?: (event: SpritePointerEvent) => void
|
|
117
145
|
onPointerUp?: (event: SpritePointerEvent) => void
|
|
118
146
|
onPointerEnter?: (event: SpritePointerEvent) => void
|
|
119
147
|
onPointerLeave?: (event: SpritePointerEvent) => void
|
|
148
|
+
/** A wheel notch over the sprite; bubbles like down/move/up. */
|
|
149
|
+
onWheel?: (event: SpriteWheelEvent) => void
|
|
150
|
+
/** A press that released on this sprite without dragging (see
|
|
151
|
+
* SpriteTapEvent); bubbles like down/move/up. */
|
|
152
|
+
onTap?: (event: SpriteTapEvent) => void
|
|
120
153
|
/** A declared transition (setSpriteTransition) settled naturally on
|
|
121
154
|
* one component; a cancel or snap never fires. */
|
|
122
155
|
onTransitionEnd?: (event: TransitionEndEvent) => void
|
|
123
156
|
}
|
|
124
157
|
|
|
158
|
+
/** The layer-internal mutable view of a Sprite: the readonly on the
|
|
159
|
+
* underscore fields is for the public surface; internal write sites
|
|
160
|
+
* annotate with this (readonly does not affect assignability, so the two
|
|
161
|
+
* types flow into each other freely). */
|
|
162
|
+
export type SpriteState = { -readonly [K in keyof Sprite]: Sprite[K] }
|
|
163
|
+
|
|
125
164
|
/** The settled component of a node transition: `position` is x/y,
|
|
126
165
|
* `scale` w/h (a group's uniform scale). */
|
|
127
166
|
export type TransitionEndEvent = {
|
|
@@ -148,17 +187,35 @@ export type SpriteOptions = {
|
|
|
148
187
|
rotation?: number
|
|
149
188
|
/** RGBA multiplier 0..1 each; default opaque white (the texture as-is). */
|
|
150
189
|
tint?: [number, number, number, number]
|
|
190
|
+
/**
|
|
191
|
+
* Explicit draw-order key, read only by a layer created with `orderBy:
|
|
192
|
+
* "renderOrder"` (default 0; ties keep slot order, so untouched sprites draw
|
|
193
|
+
* as without one). The raise idiom: `setSprite(hit, { renderOrder: ++top })`
|
|
194
|
+
* on interaction, back to 0 to restore. Node layer only - a record
|
|
195
|
+
* layer's 13-float record has no key field (order it by one of its own
|
|
196
|
+
* fields with `orderBy: { field }`); setting this there throws.
|
|
197
|
+
*/
|
|
198
|
+
renderOrder?: number
|
|
199
|
+
/**
|
|
200
|
+
* Show or hide the sprite (default true), @solidrt/3d's setVisible in
|
|
201
|
+
* the setSprite bag: hidden, its pose slot zeroes (nothing drawn) and
|
|
202
|
+
* pick/pickRect skip it; the handle, slot, style records and any
|
|
203
|
+
* running transition state stay, so showing again restores the sprite
|
|
204
|
+
* as it was. Node layer only - a record sprite has no node (hide it by
|
|
205
|
+
* zeroing w or h); setting this there throws.
|
|
206
|
+
*/
|
|
207
|
+
visible?: boolean
|
|
151
208
|
}
|
|
152
209
|
|
|
153
210
|
export type AddSpriteOptions = SpriteOptions & {
|
|
154
|
-
/** Mount under this group (node layer only
|
|
155
|
-
* to it. Reparent later with
|
|
156
|
-
|
|
211
|
+
/** Mount under this group (node layer only; null = the layer root, the
|
|
212
|
+
* default); pose fields are then local to it. Reparent later with
|
|
213
|
+
* setSpriteParent. */
|
|
214
|
+
parent?: SpriteGroup | null
|
|
157
215
|
}
|
|
158
216
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
sprite: Sprite
|
|
217
|
+
/** What every layer pointer event carries, whichever handler sees it. */
|
|
218
|
+
export type LayerEventBase = {
|
|
162
219
|
/** Pointer position in LAYER pixels (the camera mapping undone). */
|
|
163
220
|
x: number
|
|
164
221
|
y: number
|
|
@@ -169,6 +226,74 @@ export type SpritePointerEvent = {
|
|
|
169
226
|
ctrlKey: boolean
|
|
170
227
|
altKey: boolean
|
|
171
228
|
metaKey: boolean
|
|
229
|
+
/**
|
|
230
|
+
* The element event the layer's leaf received, in the leaf's own frame
|
|
231
|
+
* (localX/localY, clientX/clientY, movementX/Y): what core's
|
|
232
|
+
* recognizers consume, so a sprite drags itself through `createPan` and
|
|
233
|
+
* a camera rides `createTransform` on the same events the walk carries.
|
|
234
|
+
*/
|
|
235
|
+
native: ElementPointerEvent
|
|
236
|
+
/**
|
|
237
|
+
* Stops the walk after the current handler: no enclosing group and none
|
|
238
|
+
* of the layer's listeners see the event. Stopping a DOWN claims the
|
|
239
|
+
* whole press - that pointer's move, up and tap never reach the layer
|
|
240
|
+
* either, so a sprite that drags itself stops its down once and a
|
|
241
|
+
* camera attached at the root never pans it.
|
|
242
|
+
*/
|
|
243
|
+
stopPropagation(): void
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The event as a sprite's or group's handler sees it. */
|
|
247
|
+
export type SpritePointerEvent = LayerEventBase & {
|
|
248
|
+
/** The sprite the event is about (the topmost hit at the point, or the
|
|
249
|
+
* captured sprite during a drag) - constant while the event bubbles. */
|
|
250
|
+
sprite: Sprite
|
|
251
|
+
/** The sprite or group whose handler is running; changes as the event
|
|
252
|
+
* bubbles from the hit sprite through its enclosing groups. */
|
|
253
|
+
currentTarget: Sprite | SpriteGroup
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The event as the LAYER's listeners see it (LayerBase.listen), the last
|
|
258
|
+
* stop of the walk: `sprite` is the hit sprite it bubbled from, or null
|
|
259
|
+
* over empty space, where the layer is the only target.
|
|
260
|
+
*/
|
|
261
|
+
export type LayerPointerEvent = LayerEventBase & {
|
|
262
|
+
sprite: Sprite | null
|
|
263
|
+
currentTarget: SpriteLayer | RecordLayer
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
type WheelFields = {
|
|
267
|
+
/** The wheel delta as the element event reports it. */
|
|
268
|
+
deltaX: number
|
|
269
|
+
deltaY: number
|
|
270
|
+
native: ElementWheelEvent
|
|
271
|
+
}
|
|
272
|
+
type TapFields = {
|
|
273
|
+
/** 1 for a tap, 2 for the second of a double tap (the same target,
|
|
274
|
+
* within the repeat interval and distance), and so on - DOM's `detail`,
|
|
275
|
+
* Unity's `clickCount`. */
|
|
276
|
+
tapCount: number
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export type SpriteWheelEvent = SpritePointerEvent & WheelFields
|
|
280
|
+
export type LayerWheelEvent = LayerPointerEvent & WheelFields
|
|
281
|
+
/**
|
|
282
|
+
* A press that released on the target it pressed without travelling past
|
|
283
|
+
* the slop, the only pointer down for its whole press (a pinch never taps).
|
|
284
|
+
* Dispatched after the up, bubbling the same way; `x`/`y` are the release
|
|
285
|
+
* point.
|
|
286
|
+
*/
|
|
287
|
+
export type SpriteTapEvent = SpritePointerEvent & TapFields
|
|
288
|
+
export type LayerTapEvent = LayerPointerEvent & TapFields
|
|
289
|
+
|
|
290
|
+
/** A listener at the layer root; see LayerBase.listen. */
|
|
291
|
+
export type LayerPointerListener = {
|
|
292
|
+
onPointerDown?: (event: LayerPointerEvent) => void
|
|
293
|
+
onPointerMove?: (event: LayerPointerEvent) => void
|
|
294
|
+
onPointerUp?: (event: LayerPointerEvent) => void
|
|
295
|
+
onWheel?: (event: LayerWheelEvent) => void
|
|
296
|
+
onTap?: (event: LayerTapEvent) => void
|
|
172
297
|
}
|
|
173
298
|
|
|
174
299
|
export type SpriteHandlers = {
|
|
@@ -176,14 +301,15 @@ export type SpriteHandlers = {
|
|
|
176
301
|
onPointerMove: (event: ElementPointerEvent) => void
|
|
177
302
|
onPointerUp: (event: ElementPointerEvent) => void
|
|
178
303
|
onPointerLeave: (event: ElementPointerEvent) => void
|
|
304
|
+
onWheel: (event: ElementWheelEvent) => void
|
|
179
305
|
}
|
|
180
306
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
307
|
+
/** Validate an [r, g, b, a] tint (throws - the dev validation policy).
|
|
308
|
+
* Internal - every layer kind (sprite, record, tile) calls it. */
|
|
309
|
+
export function checkTint(verb: string, tint: [number, number, number, number]): void {
|
|
310
|
+
if (!(Array.isArray(tint) && tint.length === 4 && tint.every(Number.isFinite))) {
|
|
311
|
+
throw new Error(`${verb}: tint must be [r, g, b, a], got ${JSON.stringify(tint)}`)
|
|
312
|
+
}
|
|
187
313
|
}
|
|
188
314
|
|
|
189
315
|
export type SpriteLayerOptions = {
|
|
@@ -194,6 +320,9 @@ export type SpriteLayerOptions = {
|
|
|
194
320
|
*/
|
|
195
321
|
capacity?: number
|
|
196
322
|
clearColor?: [number, number, number, number]
|
|
323
|
+
/** Layer tint, [r, g, b, a] in 0..1, multiplied over every sprite's own
|
|
324
|
+
* tint; default opaque white (sprites as-is). See setTint. */
|
|
325
|
+
tint?: [number, number, number, number]
|
|
197
326
|
label?: string
|
|
198
327
|
/**
|
|
199
328
|
* Target texels per layer pixel (positive integer, default 1). The layer
|
|
@@ -206,6 +335,30 @@ export type SpriteLayerOptions = {
|
|
|
206
335
|
oversample?: number
|
|
207
336
|
/** Skip the owner-scoped auto-dispose (see createSpriteLayer). */
|
|
208
337
|
autoFree?: boolean
|
|
338
|
+
/**
|
|
339
|
+
* Draw sprites in KEY order instead of slot order, produced by core at
|
|
340
|
+
* every publish (the gpu `instanceOrder` primitive across the pose/style
|
|
341
|
+
* buffer pair - no per-sprite JS anywhere): `"y"` keys on the sprite's
|
|
342
|
+
* WORLD y, the pose the core itself writes, so a perspective crowd
|
|
343
|
+
* paints back to front (smaller y = further up the screen = drawn
|
|
344
|
+
* first) - and because the key is core-owned, sprites moved by native
|
|
345
|
+
* transitions or any other core producer re-sort with zero JS per
|
|
346
|
+
* frame. Slots stay fixed - handles, picking and the style records are
|
|
347
|
+
* untouched, only the draw order changes - and ties keep slot order, so
|
|
348
|
+
* sprites at equal y draw exactly as without it.
|
|
349
|
+
*
|
|
350
|
+
* `"renderOrder"` keys on the app-owned per-sprite `renderOrder` field instead
|
|
351
|
+
* (setSprite, default 0): explicit layering for painter-order scenes -
|
|
352
|
+
* raise a dragged piece, click-to-front, hover emphasis - with stable
|
|
353
|
+
* handles and no record churn.
|
|
354
|
+
*
|
|
355
|
+
* Known limitation for both keys: pick() resolves overlapping sprites by
|
|
356
|
+
* slot order, not visual order, when a key is set. (The record layer's
|
|
357
|
+
* RecordLayerOptions instead takes `"y"` or a raw `{ field }` offset
|
|
358
|
+
* into its own records; pose records are core-owned, so the node layer
|
|
359
|
+
* names its keys.)
|
|
360
|
+
*/
|
|
361
|
+
orderBy?: "y" | "renderOrder"
|
|
209
362
|
}
|
|
210
363
|
|
|
211
364
|
/**
|
|
@@ -213,21 +366,44 @@ export type SpriteLayerOptions = {
|
|
|
213
366
|
* uniform scale - never a sprite size) that sprites and other groups
|
|
214
367
|
* parent under, so a ship with turrets or a dragged stack moves as one
|
|
215
368
|
* subtree recomputed in native code. Groups render nothing and cannot be
|
|
216
|
-
* picked
|
|
369
|
+
* picked - sprites are always the leaves - but down/move/up events bubble
|
|
370
|
+
* from a hit child sprite through its enclosing groups (enter/leave pair
|
|
371
|
+
* on the sprite alone), so one group handler covers a whole assembly.
|
|
217
372
|
*/
|
|
218
373
|
export type SpriteGroup = {
|
|
219
|
-
/** The owning layer, null after removeGroup. */
|
|
220
|
-
layer: SpriteLayer | null
|
|
374
|
+
/** The owning layer, null after removeGroup (which owns the write). */
|
|
375
|
+
readonly layer: SpriteLayer | null
|
|
221
376
|
/** The group's spatial arena node. */
|
|
222
|
-
node: NodeId
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
377
|
+
readonly node: NodeId
|
|
378
|
+
/** Pose mirror (readonly, like Sprite's underscore fields: cheap reads);
|
|
379
|
+
* writes go through setGroup. */
|
|
380
|
+
readonly _x: number
|
|
381
|
+
readonly _y: number
|
|
382
|
+
readonly _rot: number
|
|
383
|
+
readonly _scale: number
|
|
384
|
+
/** Visibility mirror; writes go through setGroup. */
|
|
385
|
+
readonly _visible: boolean
|
|
386
|
+
/** See Sprite._parent. */
|
|
387
|
+
readonly _parent: SpriteGroup | null
|
|
388
|
+
/** The handles parented here (removeGroup removes them with it);
|
|
389
|
+
* internal -
|
|
390
|
+
* membership writes go through addSprite/addGroup/setSpriteParent/
|
|
391
|
+
* setGroup. */
|
|
392
|
+
readonly _children: Set<SpriteState | GroupState>
|
|
393
|
+
/** Bubbled from a hit child sprite; see SpritePointerEvent. A group
|
|
394
|
+
* never receives enter/leave (hover pairs on the sprite alone). */
|
|
395
|
+
onPointerDown?: (event: SpritePointerEvent) => void
|
|
396
|
+
onPointerMove?: (event: SpritePointerEvent) => void
|
|
397
|
+
onPointerUp?: (event: SpritePointerEvent) => void
|
|
398
|
+
onWheel?: (event: SpriteWheelEvent) => void
|
|
399
|
+
onTap?: (event: SpriteTapEvent) => void
|
|
227
400
|
/** See Sprite.onTransitionEnd. */
|
|
228
401
|
onTransitionEnd?: (event: TransitionEndEvent) => void
|
|
229
402
|
}
|
|
230
403
|
|
|
404
|
+
/** The layer-internal mutable view of a SpriteGroup (see SpriteState). */
|
|
405
|
+
export type GroupState = { -readonly [K in keyof SpriteGroup]: SpriteGroup[K] }
|
|
406
|
+
|
|
231
407
|
/** Group fields, all optional: absent keys keep values. */
|
|
232
408
|
export type GroupOptions = {
|
|
233
409
|
/** Position in the parent frame (layer pixels at the root). */
|
|
@@ -238,6 +414,10 @@ export type GroupOptions = {
|
|
|
238
414
|
/** Uniform scale on the whole subtree (this one scales child sprites -
|
|
239
415
|
* a group is a frame, not a sprite size). */
|
|
240
416
|
scale?: number
|
|
417
|
+
/** Show or hide the group's WHOLE subtree (default true); each child
|
|
418
|
+
* keeps its own `visible`, so showing the group again restores the
|
|
419
|
+
* subtree as it was. See SpriteOptions.visible. */
|
|
420
|
+
visible?: boolean
|
|
241
421
|
/** Reparent (null = make the group a root). */
|
|
242
422
|
parent?: SpriteGroup | null
|
|
243
423
|
}
|
|
@@ -246,11 +426,27 @@ export type GroupOptions = {
|
|
|
246
426
|
export type LayerBase = {
|
|
247
427
|
/** The layer's output: an ordinary texture id (`<texture src>`). */
|
|
248
428
|
texture: TextureId
|
|
249
|
-
/** Element handlers wiring
|
|
429
|
+
/** Element handlers wiring the layer's pointer events (sprites, groups
|
|
430
|
+
* and the root listeners); see handlersFor. */
|
|
250
431
|
handlers: SpriteHandlers
|
|
251
432
|
/** Live sprite count. */
|
|
252
433
|
readonly count: number
|
|
434
|
+
/** Layer pixels, as created or last set by setSize. */
|
|
435
|
+
readonly width: number
|
|
436
|
+
readonly height: number
|
|
253
437
|
setSize(width: number, height: number): void
|
|
438
|
+
/**
|
|
439
|
+
* Listen at the root of the event walk. Every down, move, up, wheel and
|
|
440
|
+
* tap arrives here after the hit sprite and its enclosing groups
|
|
441
|
+
* (`sprite` set) or as the walk's only stop over empty space (`sprite`
|
|
442
|
+
* null), unless a handler stopped it on the way. Listeners run in
|
|
443
|
+
* registration order and all of them run - the root is the last stop,
|
|
444
|
+
* there is nothing left to claim. Returns the remover. The app's own
|
|
445
|
+
* root handling (deselect on a miss, a marquee) and controls
|
|
446
|
+
* (createCamera2d's attach) meet here, which is why the root is a list
|
|
447
|
+
* where a sprite has plain fields.
|
|
448
|
+
*/
|
|
449
|
+
listen(listener: LayerPointerListener): () => void
|
|
254
450
|
/** Target texels per layer pixel; see setOversample. */
|
|
255
451
|
readonly oversample: number
|
|
256
452
|
/**
|
|
@@ -262,118 +458,54 @@ export type LayerBase = {
|
|
|
262
458
|
*/
|
|
263
459
|
setOversample(n: number): void
|
|
264
460
|
setCamera(update: CameraUpdate): void
|
|
265
|
-
/**
|
|
266
|
-
|
|
461
|
+
/** The camera as last set (a fresh object per call, every field
|
|
462
|
+
* present): the argument for projectCamera/unprojectCamera -
|
|
463
|
+
* @solidrt/3d's scene.camera(). */
|
|
464
|
+
camera(): CameraState
|
|
465
|
+
/** World (layer) pixels -> viewport pixels under the current camera:
|
|
466
|
+
* projectCamera over camera(). */
|
|
467
|
+
project(x: number, y: number): [number, number]
|
|
468
|
+
/** Viewport pixels -> world (layer) pixels, the inverse: what pointer
|
|
469
|
+
* dispatch applies to every event. */
|
|
470
|
+
unproject(x: number, y: number): [number, number]
|
|
471
|
+
/**
|
|
472
|
+
* Tint the whole layer, [r, g, b, a] in 0..1: a uniform multiplied over
|
|
473
|
+
* every sprite's own tint (day/night, a dimmed parallax plane, a
|
|
474
|
+
* fade-in). One shared-params write - no record touches, cheap to
|
|
475
|
+
* animate - and the same contract as TileLayer.setTint, so one signal
|
|
476
|
+
* drives a whole scene across layer kinds.
|
|
477
|
+
*/
|
|
478
|
+
setTint(tint: [number, number, number, number]): void
|
|
479
|
+
/** Every shown sprite whose rotated rect contains the layer-pixel point
|
|
480
|
+
* (a hidden sprite or subtree is never hit), topmost first - the
|
|
481
|
+
* all-hits shape of @solidrt/3d's pick; `pick(x, y)[0]` is the topmost.
|
|
482
|
+
* Topmost means draw order (highest slot on the node layer, last added
|
|
483
|
+
* on a record layer); an `orderBy` key is not consulted (see
|
|
484
|
+
* SpriteLayerOptions.orderBy's known limitation). */
|
|
485
|
+
pick(x: number, y: number): Sprite[]
|
|
267
486
|
/**
|
|
268
|
-
*
|
|
269
|
-
* (events scale by layer/layout;
|
|
270
|
-
*
|
|
487
|
+
* handlers for a leaf whose LAYOUT size differs from the layer size
|
|
488
|
+
* (events scale by layer/layout; a leaf laid out AT layer size just uses
|
|
489
|
+
* `handlers`). `layout` is read per event, so a resize-reactive layout
|
|
490
|
+
* just works - @solidrt/3d's handlersFor, one dimension down.
|
|
271
491
|
*/
|
|
272
|
-
handlersFor(layout: (
|
|
492
|
+
handlersFor(layout: () => { width: number; height: number }): SpriteHandlers
|
|
273
493
|
dispose(): void
|
|
274
494
|
_add(opts?: AddSpriteOptions): Sprite
|
|
275
|
-
_write(sprite:
|
|
495
|
+
_write(sprite: SpriteState, opts: SpriteOptions): void
|
|
276
496
|
_read(sprite: Sprite): Required<SpriteOptions>
|
|
277
|
-
_remove(sprite:
|
|
497
|
+
_remove(sprite: SpriteState): void
|
|
278
498
|
_schedule(): void
|
|
279
499
|
}
|
|
280
500
|
|
|
281
501
|
export type SpriteLayer = LayerBase & {
|
|
282
502
|
/**
|
|
283
|
-
* Every sprite whose rotated rect overlaps the layer-pixel rect
|
|
284
|
-
* core BVH overlap query, exact for rotated sprites), unordered -
|
|
285
|
-
* marquee query. Node layer only.
|
|
503
|
+
* Every shown sprite whose rotated rect overlaps the layer-pixel rect
|
|
504
|
+
* (the core BVH overlap query, exact for rotated sprites), unordered -
|
|
505
|
+
* the marquee query. Node layer only.
|
|
286
506
|
*/
|
|
287
|
-
pickRect(x: number, y: number,
|
|
288
|
-
_groups: Set<
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Pointer dispatch shared by both layer kinds: capture per pointer, hover
|
|
293
|
-
* pairing, no bubbling (the sprite list is flat). Layout null = the leaf is
|
|
294
|
-
* laid out at layer size, so localX/localY are layer pixels already (the
|
|
295
|
-
* element hit test undid every ancestor transform - never getBoundingBox
|
|
296
|
-
* here). Internal - layers expose the result as handlers/handlersFor.
|
|
297
|
-
*/
|
|
298
|
-
export function spriteDispatch(state: {
|
|
299
|
-
size: () => [number, number]
|
|
300
|
-
camera: () => [number, number, number]
|
|
301
|
-
pick: (x: number, y: number) => Sprite | null
|
|
302
|
-
}): (layout: (() => { width: number; height: number } | null) | null) => SpriteHandlers {
|
|
303
|
-
let capture = new Map<number, Sprite>()
|
|
304
|
-
let hover = new Map<number, Sprite>()
|
|
305
|
-
return layout => {
|
|
306
|
-
let toLayer = (e: ElementPointerEvent): [number, number] => {
|
|
307
|
-
let x = e.localX
|
|
308
|
-
let y = e.localY
|
|
309
|
-
let l = layout?.()
|
|
310
|
-
let [width, height] = state.size()
|
|
311
|
-
if (l && l.width > 0 && l.height > 0) {
|
|
312
|
-
x *= width / l.width
|
|
313
|
-
y *= height / l.height
|
|
314
|
-
}
|
|
315
|
-
// Undo the camera: screen -> world.
|
|
316
|
-
let [camX, camY, camZoom] = state.camera()
|
|
317
|
-
return [x / camZoom + camX, y / camZoom + camY]
|
|
318
|
-
}
|
|
319
|
-
let makeEvent = (sprite: Sprite, x: number, y: number, e: ElementPointerEvent): SpritePointerEvent => ({
|
|
320
|
-
sprite,
|
|
321
|
-
x,
|
|
322
|
-
y,
|
|
323
|
-
pointerId: e.pointerId,
|
|
324
|
-
pointerType: e.pointerType,
|
|
325
|
-
button: e.button,
|
|
326
|
-
shiftKey: e.shiftKey,
|
|
327
|
-
ctrlKey: e.ctrlKey,
|
|
328
|
-
altKey: e.altKey,
|
|
329
|
-
metaKey: e.metaKey,
|
|
330
|
-
})
|
|
331
|
-
return {
|
|
332
|
-
onPointerDown(e) {
|
|
333
|
-
let [x, y] = toLayer(e)
|
|
334
|
-
let hit = state.pick(x, y)
|
|
335
|
-
if (!hit) return
|
|
336
|
-
capture.set(e.pointerId, hit)
|
|
337
|
-
hit.onPointerDown?.(makeEvent(hit, x, y, e))
|
|
338
|
-
},
|
|
339
|
-
onPointerMove(e) {
|
|
340
|
-
let [x, y] = toLayer(e)
|
|
341
|
-
let captured = capture.get(e.pointerId)
|
|
342
|
-
if (captured) {
|
|
343
|
-
if (captured.layer) captured.onPointerMove?.(makeEvent(captured, x, y, e))
|
|
344
|
-
return
|
|
345
|
-
}
|
|
346
|
-
let hit = state.pick(x, y)
|
|
347
|
-
let prev = hover.get(e.pointerId) ?? null
|
|
348
|
-
if (prev !== hit) {
|
|
349
|
-
if (prev && prev.layer) prev.onPointerLeave?.(makeEvent(prev, x, y, e))
|
|
350
|
-
if (hit) hit.onPointerEnter?.(makeEvent(hit, x, y, e))
|
|
351
|
-
if (hit) hover.set(e.pointerId, hit)
|
|
352
|
-
else hover.delete(e.pointerId)
|
|
353
|
-
}
|
|
354
|
-
hit?.onPointerMove?.(makeEvent(hit, x, y, e))
|
|
355
|
-
},
|
|
356
|
-
onPointerUp(e) {
|
|
357
|
-
let [x, y] = toLayer(e)
|
|
358
|
-
let captured = capture.get(e.pointerId)
|
|
359
|
-
if (captured) {
|
|
360
|
-
capture.delete(e.pointerId)
|
|
361
|
-
if (captured.layer) captured.onPointerUp?.(makeEvent(captured, x, y, e))
|
|
362
|
-
return
|
|
363
|
-
}
|
|
364
|
-
let hit = state.pick(x, y)
|
|
365
|
-
hit?.onPointerUp?.(makeEvent(hit, x, y, e))
|
|
366
|
-
},
|
|
367
|
-
onPointerLeave(e) {
|
|
368
|
-
let [x, y] = toLayer(e)
|
|
369
|
-
let prev = hover.get(e.pointerId)
|
|
370
|
-
if (prev) {
|
|
371
|
-
hover.delete(e.pointerId)
|
|
372
|
-
if (prev.layer) prev.onPointerLeave?.(makeEvent(prev, x, y, e))
|
|
373
|
-
}
|
|
374
|
-
},
|
|
375
|
-
}
|
|
376
|
-
}
|
|
507
|
+
pickRect(x: number, y: number, width: number, height: number): Sprite[]
|
|
508
|
+
_groups: Set<GroupState>
|
|
377
509
|
}
|
|
378
510
|
|
|
379
511
|
/** The stored UVs at `at` un-mirrored by the sprite's flags: the frame as
|
|
@@ -428,6 +560,9 @@ export function createSpriteLayer(
|
|
|
428
560
|
let label = opts?.label ?? "sprites"
|
|
429
561
|
let oversample = opts?.oversample ?? 1
|
|
430
562
|
checkOversample("createSpriteLayer", oversample, width, height)
|
|
563
|
+
let tint = opts?.tint ?? [1, 1, 1, 1]
|
|
564
|
+
checkTint("createSpriteLayer", tint)
|
|
565
|
+
let thrash = thrashSentinel(`sprite layer "${label}"`)
|
|
431
566
|
// One unit quad (triangle strip), reused by every instance.
|
|
432
567
|
let quad = createBuffer(new Float32Array([-0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5]), {
|
|
433
568
|
label: `${label}-quad`,
|
|
@@ -440,7 +575,7 @@ export function createSpriteLayer(
|
|
|
440
575
|
FRAGMENT,
|
|
441
576
|
width * oversample,
|
|
442
577
|
height * oversample,
|
|
443
|
-
{ uViewport: [width, height], uCamera: [0, 0, 1, 1] },
|
|
578
|
+
{ uViewport: [width, height], uCamera: [0, 0, 1, 1], uCameraRot: [1, 0, 0, 0], uTint: tint },
|
|
444
579
|
{
|
|
445
580
|
label,
|
|
446
581
|
topology: "triangle-strip",
|
|
@@ -449,6 +584,16 @@ export function createSpriteLayer(
|
|
|
449
584
|
buffer: quad,
|
|
450
585
|
instanceAttributes: INSTANCE_ATTRIBUTES_SPLIT,
|
|
451
586
|
instanceBuffers: [pose, style],
|
|
587
|
+
// "y" keys on world y in the pose record (slot 0), "renderOrder" on the
|
|
588
|
+
// app-owned key in the style record (slot 1); either way the core
|
|
589
|
+
// gathers BOTH buffers under the one permutation at every publish
|
|
590
|
+
// and republishes the sibling itself when the key buffer re-orders.
|
|
591
|
+
instanceOrder:
|
|
592
|
+
opts?.orderBy === "y"
|
|
593
|
+
? { field: POSE_Y_FIELD }
|
|
594
|
+
: opts?.orderBy === "renderOrder"
|
|
595
|
+
? { field: STYLE_KEY_FIELD, slot: 1 }
|
|
596
|
+
: undefined,
|
|
452
597
|
instanceCount: 0,
|
|
453
598
|
blend: "alpha",
|
|
454
599
|
textures: { uAtlas: atlas },
|
|
@@ -461,6 +606,9 @@ export function createSpriteLayer(
|
|
|
461
606
|
let camX = 0
|
|
462
607
|
let camY = 0
|
|
463
608
|
let camZoom = 1
|
|
609
|
+
let camRot = 0
|
|
610
|
+
let camPivotX = 0
|
|
611
|
+
let camPivotY = 0
|
|
464
612
|
let disposed = false
|
|
465
613
|
let scheduled = false
|
|
466
614
|
let styleDirty = false
|
|
@@ -473,7 +621,7 @@ export function createSpriteLayer(
|
|
|
473
621
|
let freeSlots: number[] = []
|
|
474
622
|
let gpuCapacity = capacity
|
|
475
623
|
let styleData = new Float32Array(capacity * STYLE_FLOATS)
|
|
476
|
-
let byNode = new Map<NodeId,
|
|
624
|
+
let byNode = new Map<NodeId, SpriteState>()
|
|
477
625
|
|
|
478
626
|
let flush = () => {
|
|
479
627
|
scheduled = false
|
|
@@ -514,7 +662,7 @@ export function createSpriteLayer(
|
|
|
514
662
|
styleDirty = true
|
|
515
663
|
}
|
|
516
664
|
|
|
517
|
-
let writeStyle = (sprite:
|
|
665
|
+
let writeStyle = (sprite: SpriteState, opts: SpriteOptions) => {
|
|
518
666
|
let at = sprite._slot * STYLE_FLOATS
|
|
519
667
|
let flipX = opts.flipX !== undefined && opts.flipX !== sprite._flipX
|
|
520
668
|
let flipY = opts.flipY !== undefined && opts.flipY !== sprite._flipY
|
|
@@ -536,13 +684,13 @@ export function createSpriteLayer(
|
|
|
536
684
|
styleData[at + 7] = opts.tint[3]
|
|
537
685
|
styleDirty = true
|
|
538
686
|
}
|
|
687
|
+
if (opts.renderOrder !== undefined) {
|
|
688
|
+
styleData[at + STYLE_KEY_FIELD] = opts.renderOrder
|
|
689
|
+
styleDirty = true
|
|
690
|
+
}
|
|
539
691
|
}
|
|
540
692
|
|
|
541
|
-
let
|
|
542
|
-
size: () => [width, height],
|
|
543
|
-
camera: () => [camX, camY, camZoom],
|
|
544
|
-
pick: (x, y) => layer.pick(x, y),
|
|
545
|
-
})
|
|
693
|
+
let listeners = new Set<LayerPointerListener>()
|
|
546
694
|
|
|
547
695
|
let layer: SpriteLayer = {
|
|
548
696
|
texture,
|
|
@@ -550,6 +698,12 @@ export function createSpriteLayer(
|
|
|
550
698
|
get count() {
|
|
551
699
|
return byNode.size
|
|
552
700
|
},
|
|
701
|
+
get width() {
|
|
702
|
+
return width
|
|
703
|
+
},
|
|
704
|
+
get height() {
|
|
705
|
+
return height
|
|
706
|
+
},
|
|
553
707
|
setSize(w, h) {
|
|
554
708
|
if (disposed || (w === width && h === height)) return
|
|
555
709
|
checkOversample("setSize", oversample, w, h)
|
|
@@ -558,24 +712,49 @@ export function createSpriteLayer(
|
|
|
558
712
|
setTargetSize(texture, w * oversample, h * oversample)
|
|
559
713
|
setTargetParams(texture, { uViewport: [w, h] })
|
|
560
714
|
},
|
|
715
|
+
listen(listener) {
|
|
716
|
+
listeners.add(listener)
|
|
717
|
+
return () => {
|
|
718
|
+
listeners.delete(listener)
|
|
719
|
+
}
|
|
720
|
+
},
|
|
561
721
|
get oversample() {
|
|
562
722
|
return oversample
|
|
563
723
|
},
|
|
564
724
|
setOversample(n) {
|
|
565
725
|
if (disposed || n === oversample) return
|
|
566
726
|
checkOversample("setOversample", n, width, height)
|
|
727
|
+
thrash()
|
|
567
728
|
oversample = n
|
|
568
729
|
setTargetSize(texture, width * n, height * n)
|
|
569
730
|
},
|
|
570
731
|
setCamera(update) {
|
|
571
732
|
if (disposed) return
|
|
733
|
+
checkCamera(update)
|
|
572
734
|
if (update.x !== undefined) camX = update.x
|
|
573
735
|
if (update.y !== undefined) camY = update.y
|
|
574
|
-
if (update.zoom !== undefined)
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
setTargetParams(texture, {
|
|
736
|
+
if (update.zoom !== undefined) camZoom = update.zoom
|
|
737
|
+
if (update.rotation !== undefined) camRot = update.rotation
|
|
738
|
+
if (update.pivotX !== undefined) camPivotX = update.pivotX
|
|
739
|
+
if (update.pivotY !== undefined) camPivotY = update.pivotY
|
|
740
|
+
setTargetParams(texture, {
|
|
741
|
+
uCamera: [camX, camY, camZoom, camZoom],
|
|
742
|
+
uCameraRot: [Math.cos(camRot), Math.sin(camRot), camPivotX, camPivotY],
|
|
743
|
+
})
|
|
744
|
+
},
|
|
745
|
+
camera() {
|
|
746
|
+
return { x: camX, y: camY, zoom: camZoom, rotation: camRot, pivotX: camPivotX, pivotY: camPivotY }
|
|
747
|
+
},
|
|
748
|
+
project(x, y) {
|
|
749
|
+
return projectCamera(layer.camera(), x, y)
|
|
750
|
+
},
|
|
751
|
+
unproject(x, y) {
|
|
752
|
+
return unprojectCamera(layer.camera(), x, y)
|
|
753
|
+
},
|
|
754
|
+
setTint(next) {
|
|
755
|
+
if (disposed) return
|
|
756
|
+
checkTint("setTint", next)
|
|
757
|
+
setTargetParams(texture, { uTint: next })
|
|
579
758
|
},
|
|
580
759
|
pick(x, y) {
|
|
581
760
|
// The index reads as of the last core flush; run any pending batch
|
|
@@ -584,26 +763,29 @@ export function createSpriteLayer(
|
|
|
584
763
|
RAY_ORIGIN[0] = x
|
|
585
764
|
RAY_ORIGIN[1] = y
|
|
586
765
|
RAY_ORIGIN[2] = -1
|
|
587
|
-
let
|
|
766
|
+
let out: Sprite[] = []
|
|
588
767
|
for (let hit of spatial.raycast(RAY_ORIGIN, RAY_DIR)) {
|
|
589
768
|
// The arena is shared (a 3d scene lives in the same index): only
|
|
590
|
-
// this layer's nodes count.
|
|
769
|
+
// this layer's nodes count.
|
|
591
770
|
let sprite = byNode.get(hit.node)
|
|
592
|
-
if (sprite
|
|
771
|
+
if (sprite) out.push(sprite)
|
|
593
772
|
}
|
|
594
|
-
|
|
773
|
+
// Topmost first: higher slot = drawn later = on top.
|
|
774
|
+
return out.sort((a, b) => b._slot - a._slot)
|
|
595
775
|
},
|
|
596
|
-
pickRect(x, y,
|
|
776
|
+
pickRect(x, y, width, height) {
|
|
597
777
|
if (scheduled) flush()
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
BOX[
|
|
601
|
-
BOX[
|
|
602
|
-
BOX[
|
|
778
|
+
// The marquee as a "box" volume: center, half extents, no rotation,
|
|
779
|
+
// a unit deep so every sprite plane (z = 0) lies inside it.
|
|
780
|
+
BOX[0] = x + width / 2
|
|
781
|
+
BOX[1] = y + height / 2
|
|
782
|
+
BOX[2] = 0
|
|
783
|
+
BOX[3] = width / 2
|
|
784
|
+
BOX[4] = height / 2
|
|
603
785
|
BOX[5] = 1
|
|
604
786
|
let out: Sprite[] = []
|
|
605
|
-
for (let
|
|
606
|
-
let sprite = byNode.get(node)
|
|
787
|
+
for (let hit of spatial.overlap("box", BOX)) {
|
|
788
|
+
let sprite = byNode.get(hit.node)
|
|
607
789
|
if (sprite) out.push(sprite)
|
|
608
790
|
}
|
|
609
791
|
return out
|
|
@@ -614,6 +796,7 @@ export function createSpriteLayer(
|
|
|
614
796
|
dispose() {
|
|
615
797
|
if (disposed) return
|
|
616
798
|
disposed = true
|
|
799
|
+
listeners.clear()
|
|
617
800
|
for (let sprite of byNode.values()) {
|
|
618
801
|
sprite.layer = null
|
|
619
802
|
declared.delete(sprite.node!)
|
|
@@ -638,7 +821,7 @@ export function createSpriteLayer(
|
|
|
638
821
|
if (disposed) throw new Error("addSprite: layer is disposed")
|
|
639
822
|
let slot = freeSlots.pop() ?? highWater++
|
|
640
823
|
if (slot >= gpuCapacity) grow(gpuCapacity * 2)
|
|
641
|
-
let sprite:
|
|
824
|
+
let sprite: SpriteState = {
|
|
642
825
|
layer,
|
|
643
826
|
node: null,
|
|
644
827
|
_slot: slot,
|
|
@@ -649,18 +832,23 @@ export function createSpriteLayer(
|
|
|
649
832
|
_rot: opts?.rotation ?? 0,
|
|
650
833
|
_flipX: false,
|
|
651
834
|
_flipY: false,
|
|
835
|
+
_visible: opts?.visible ?? true,
|
|
836
|
+
_parent: opts?.parent ?? null,
|
|
652
837
|
}
|
|
653
838
|
fillTransform(sprite._x, sprite._y, sprite._rot, sprite._w, sprite._h)
|
|
654
|
-
let node = spatial.createNode(TRANSFORM,
|
|
839
|
+
let node = spatial.createNode(TRANSFORM, sprite._visible)
|
|
655
840
|
sprite.node = node
|
|
656
841
|
if (opts?.parent) {
|
|
657
842
|
if (opts.parent.layer !== layer) throw new Error("addSprite: parent group belongs to another layer")
|
|
658
843
|
spatial.setParent(node, opts.parent.node)
|
|
844
|
+
opts.parent._children.add(sprite)
|
|
659
845
|
}
|
|
660
846
|
spatial.setBounds(node, FLAT_BOUNDS)
|
|
661
847
|
spatial.bindPoseRecord(node, pose, slot)
|
|
662
848
|
byNode.set(node, sprite)
|
|
663
|
-
|
|
849
|
+
// renderOrder defaults to 0 explicitly: a recycled slot holds the
|
|
850
|
+
// previous occupant's key otherwise.
|
|
851
|
+
writeStyle(sprite, { frame: FULL_FRAME, tint: [1, 1, 1, 1], renderOrder: 0, ...opts })
|
|
664
852
|
layer._schedule()
|
|
665
853
|
return sprite
|
|
666
854
|
},
|
|
@@ -672,7 +860,18 @@ export function createSpriteLayer(
|
|
|
672
860
|
if (opts.h !== undefined && opts.h !== sprite._h) (sprite._h = opts.h), (moved = true)
|
|
673
861
|
if (opts.rotation !== undefined && opts.rotation !== sprite._rot) (sprite._rot = opts.rotation), (moved = true)
|
|
674
862
|
if (moved) writeTransform(sprite)
|
|
675
|
-
if (opts.
|
|
863
|
+
if (opts.visible !== undefined && opts.visible !== sprite._visible) {
|
|
864
|
+
sprite._visible = opts.visible
|
|
865
|
+
spatial.setVisible(sprite.node!, opts.visible)
|
|
866
|
+
moved = true
|
|
867
|
+
}
|
|
868
|
+
if (
|
|
869
|
+
opts.frame !== undefined ||
|
|
870
|
+
opts.tint !== undefined ||
|
|
871
|
+
opts.flipX !== undefined ||
|
|
872
|
+
opts.flipY !== undefined ||
|
|
873
|
+
opts.renderOrder !== undefined
|
|
874
|
+
) {
|
|
676
875
|
writeStyle(sprite, opts)
|
|
677
876
|
}
|
|
678
877
|
if (moved || styleDirty) layer._schedule()
|
|
@@ -689,10 +888,16 @@ export function createSpriteLayer(
|
|
|
689
888
|
flipY: sprite._flipY,
|
|
690
889
|
rotation: sprite._rot,
|
|
691
890
|
tint: [styleData[at + 4]!, styleData[at + 5]!, styleData[at + 6]!, styleData[at + 7]!],
|
|
891
|
+
renderOrder: styleData[at + STYLE_KEY_FIELD]!,
|
|
892
|
+
visible: sprite._visible,
|
|
692
893
|
}
|
|
693
894
|
},
|
|
694
895
|
_remove(sprite) {
|
|
695
896
|
sprite.layer = null
|
|
897
|
+
if (sprite._parent) {
|
|
898
|
+
sprite._parent._children.delete(sprite)
|
|
899
|
+
sprite._parent = null
|
|
900
|
+
}
|
|
696
901
|
// Destroying the node zeroes its pose slot at the next core flush
|
|
697
902
|
// (zero scale = nothing drawn); the slot then recycles.
|
|
698
903
|
byNode.delete(sprite.node!)
|
|
@@ -708,6 +913,13 @@ export function createSpriteLayer(
|
|
|
708
913
|
},
|
|
709
914
|
_groups: new Set(),
|
|
710
915
|
}
|
|
916
|
+
let dispatch = spriteDispatch({
|
|
917
|
+
size: () => [width, height],
|
|
918
|
+
camera: () => layer.camera(),
|
|
919
|
+
pick: (x, y) => layer.pick(x, y),
|
|
920
|
+
root: layer,
|
|
921
|
+
listeners,
|
|
922
|
+
})
|
|
711
923
|
layer.handlers = dispatch(null)
|
|
712
924
|
|
|
713
925
|
if (opts?.autoFree !== false && getOwner()) onCleanup(() => layer.dispose())
|
|
@@ -719,7 +931,8 @@ export function createSpriteLayer(
|
|
|
719
931
|
* order, and a removed sprite's slot recycles to the next add - so unlike
|
|
720
932
|
* the record layer there is no painter's-insertion-order guarantee across
|
|
721
933
|
* removals. Opaque-or-transparent pixel art (the overwhelming case) never
|
|
722
|
-
* notices;
|
|
934
|
+
* notices; a scene that needs depth order sorts by world y with the
|
|
935
|
+
* layer's `orderBy: "y"` (core-produced, zero JS per frame). Past the
|
|
723
936
|
* layer's reservation both instance buffers double (pose sinks move in one
|
|
724
937
|
* core retarget); reserve with `capacity` to avoid the copies.
|
|
725
938
|
*/
|
|
@@ -745,10 +958,33 @@ export function removeSprite(sprite: Sprite): void {
|
|
|
745
958
|
sprite.layer?._remove(sprite)
|
|
746
959
|
}
|
|
747
960
|
|
|
961
|
+
/**
|
|
962
|
+
* A sprite's or group's position in LAYER pixels: its local x/y composed
|
|
963
|
+
* through every enclosing group (@solidrt/3d's worldPosition, Three's
|
|
964
|
+
* getWorldPosition), read from the core's world matrix as the tree stands
|
|
965
|
+
* now, pending writes included. Under a transition this is the
|
|
966
|
+
* mid-flight pose - what picking and the screen show - where the handle's
|
|
967
|
+
* own fields hold the target. A record layer's sprite has no groups, so
|
|
968
|
+
* its world position is its own, read as getSprite reads it (the records
|
|
969
|
+
* array). A fresh pair per call; null once the handle is inert.
|
|
970
|
+
*/
|
|
971
|
+
export function worldPosition(target: Sprite | SpriteGroup): [number, number] | null {
|
|
972
|
+
if (target.layer === null) return null
|
|
973
|
+
// The set only groups carry tells the two handle kinds apart (as in
|
|
974
|
+
// removeGroup); a node-less sprite is a record layer's.
|
|
975
|
+
if (!("_children" in target) && target.node === null) {
|
|
976
|
+
let s = target.layer._read(target)
|
|
977
|
+
return [s.x, s.y]
|
|
978
|
+
}
|
|
979
|
+
spatial.worldMatrix(target.node!, WORLD)
|
|
980
|
+
return [WORLD[12]!, WORLD[13]!]
|
|
981
|
+
}
|
|
982
|
+
|
|
748
983
|
/**
|
|
749
984
|
* Re-parent a sprite under a group (null = back to the layer root); its
|
|
750
985
|
* pose fields then read in the new parent's frame, where the sprite keeps
|
|
751
|
-
* them (it holds its local pose, not its world pose
|
|
986
|
+
* them (it holds its local pose, not its world pose; worldPosition reads
|
|
987
|
+
* the composed one). Node layer only.
|
|
752
988
|
*/
|
|
753
989
|
export function setSpriteParent(sprite: Sprite, parent: SpriteGroup | null): void {
|
|
754
990
|
let layer = sprite.layer
|
|
@@ -756,6 +992,10 @@ export function setSpriteParent(sprite: Sprite, parent: SpriteGroup | null): voi
|
|
|
756
992
|
if (sprite.node === null) throw new Error("setSpriteParent: record layers have no groups")
|
|
757
993
|
if (parent && parent.layer !== layer) throw new Error("setSpriteParent: group belongs to another layer")
|
|
758
994
|
spatial.setParent(sprite.node, parent ? parent.node : null)
|
|
995
|
+
let s: SpriteState = sprite
|
|
996
|
+
if (s._parent) s._parent._children.delete(s)
|
|
997
|
+
s._parent = parent
|
|
998
|
+
if (parent) parent._children.add(s)
|
|
759
999
|
layer._schedule()
|
|
760
1000
|
}
|
|
761
1001
|
|
|
@@ -796,12 +1036,16 @@ export function addGroup(layer: SpriteLayer, opts?: GroupOptions): SpriteGroup {
|
|
|
796
1036
|
_y: opts?.y ?? 0,
|
|
797
1037
|
_rot: opts?.rotation ?? 0,
|
|
798
1038
|
_scale: opts?.scale ?? 1,
|
|
799
|
-
|
|
1039
|
+
_visible: opts?.visible ?? true,
|
|
1040
|
+
_parent: opts?.parent ?? null,
|
|
1041
|
+
_children: new Set(),
|
|
1042
|
+
} as GroupState
|
|
800
1043
|
writeGroupTransform(group)
|
|
801
|
-
group.node = spatial.createNode(TRANSFORM,
|
|
1044
|
+
group.node = spatial.createNode(TRANSFORM, group._visible)
|
|
802
1045
|
if (opts?.parent) {
|
|
803
1046
|
if (opts.parent.layer !== layer) throw new Error("addGroup: parent group belongs to another layer")
|
|
804
1047
|
spatial.setParent(group.node, opts.parent.node)
|
|
1048
|
+
opts.parent._children.add(group)
|
|
805
1049
|
}
|
|
806
1050
|
layer._groups.add(group)
|
|
807
1051
|
layer._schedule()
|
|
@@ -816,33 +1060,57 @@ function writeGroupTransform(group: SpriteGroup): void {
|
|
|
816
1060
|
export function setGroup(group: SpriteGroup, opts: GroupOptions): void {
|
|
817
1061
|
let layer = group.layer
|
|
818
1062
|
if (!layer) return
|
|
1063
|
+
let g: GroupState = group
|
|
819
1064
|
let moved = false
|
|
820
|
-
if (opts.x !== undefined && opts.x !==
|
|
821
|
-
if (opts.y !== undefined && opts.y !==
|
|
822
|
-
if (opts.rotation !== undefined && opts.rotation !==
|
|
823
|
-
if (opts.scale !== undefined && opts.scale !==
|
|
1065
|
+
if (opts.x !== undefined && opts.x !== g._x) (g._x = opts.x), (moved = true)
|
|
1066
|
+
if (opts.y !== undefined && opts.y !== g._y) (g._y = opts.y), (moved = true)
|
|
1067
|
+
if (opts.rotation !== undefined && opts.rotation !== g._rot) (g._rot = opts.rotation), (moved = true)
|
|
1068
|
+
if (opts.scale !== undefined && opts.scale !== g._scale) (g._scale = opts.scale), (moved = true)
|
|
824
1069
|
if (moved) {
|
|
825
1070
|
writeGroupTransform(group)
|
|
826
1071
|
spatial.writeTransform(group.node, TRANSFORM)
|
|
827
1072
|
}
|
|
1073
|
+
if (opts.visible !== undefined && opts.visible !== g._visible) {
|
|
1074
|
+
g._visible = opts.visible
|
|
1075
|
+
spatial.setVisible(group.node, opts.visible)
|
|
1076
|
+
moved = true
|
|
1077
|
+
}
|
|
828
1078
|
if (opts.parent !== undefined) {
|
|
829
1079
|
if (opts.parent && opts.parent.layer !== layer) throw new Error("setGroup: parent group belongs to another layer")
|
|
830
1080
|
spatial.setParent(group.node, opts.parent ? opts.parent.node : null)
|
|
1081
|
+
if (g._parent) g._parent._children.delete(g)
|
|
1082
|
+
g._parent = opts.parent
|
|
1083
|
+
if (opts.parent) opts.parent._children.add(g)
|
|
831
1084
|
moved = true
|
|
832
1085
|
}
|
|
833
1086
|
if (moved) layer._schedule()
|
|
834
1087
|
}
|
|
835
1088
|
|
|
836
1089
|
/**
|
|
837
|
-
* Remove a group:
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
1090
|
+
* Remove a group AND everything under it: child sprites and groups are
|
|
1091
|
+
* removed with it - Unity's Destroy, Godot's free, the subtree form of
|
|
1092
|
+
* removeSprite. To keep a child, re-parent it out first (setSpriteParent /
|
|
1093
|
+
* setGroup's `parent`). Every removed handle goes inert. A component tree
|
|
1094
|
+
* unmounts children first and never sees the recursion. (@solidrt/3d's
|
|
1095
|
+
* `remove` DETACHES a re-addable subtree instead - its nodes exist outside
|
|
1096
|
+
* a scene; a sprite cannot exist outside its layer, so here remove means
|
|
1097
|
+
* destroy, exactly as it does for removeSprite.)
|
|
841
1098
|
*/
|
|
842
1099
|
export function removeGroup(group: SpriteGroup): void {
|
|
843
1100
|
let layer = group.layer
|
|
844
1101
|
if (!layer) return
|
|
845
|
-
|
|
1102
|
+
let g: GroupState = group
|
|
1103
|
+
// Children first, over a snapshot (each removal edits _children); the
|
|
1104
|
+
// set only groups carry tells the two handle kinds apart.
|
|
1105
|
+
for (let child of [...g._children]) {
|
|
1106
|
+
if ("_children" in child) removeGroup(child)
|
|
1107
|
+
else removeSprite(child)
|
|
1108
|
+
}
|
|
1109
|
+
g.layer = null
|
|
1110
|
+
if (g._parent) {
|
|
1111
|
+
g._parent._children.delete(g)
|
|
1112
|
+
g._parent = null
|
|
1113
|
+
}
|
|
846
1114
|
layer._groups.delete(group)
|
|
847
1115
|
declared.delete(group.node)
|
|
848
1116
|
spatial.destroyNode(group.node)
|