@solidrt/2d 0.0.55 → 0.0.57
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 +417 -70
- package/README.md +3 -3
- package/examples/README.md +49 -7
- package/examples/anim.tsx +84 -0
- package/examples/camera-probe.tsx +202 -0
- package/examples/camera.tsx +196 -0
- package/examples/parity.tsx +15 -13
- package/examples/pick.tsx +89 -36
- package/examples/split-screen.tsx +111 -0
- package/examples/springs.tsx +6 -9
- package/examples/sprites.tsx +9 -12
- package/examples/tiles.tsx +42 -18
- package/examples/views.tsx +177 -0
- package/package.json +3 -3
- package/src/animation.ts +178 -0
- package/src/atlas.ts +35 -33
- package/src/camera.ts +111 -0
- package/src/camera2d.ts +713 -0
- package/src/components/auto-oversample.ts +27 -0
- package/src/components/camera2d.tsx +82 -0
- package/src/components/context.ts +48 -0
- package/src/components/group.tsx +68 -0
- package/src/components/index.ts +28 -0
- package/src/components/sprite-layer.tsx +312 -0
- package/src/components/sprite.tsx +72 -0
- package/src/components/tile-layer.tsx +160 -0
- package/src/components/view2d.tsx +151 -0
- package/src/dispatch.ts +231 -0
- package/src/frames.ts +63 -14
- package/src/index.ts +53 -12
- package/src/input.ts +50 -0
- package/src/layer.ts +716 -309
- package/src/oversample-math.ts +73 -0
- package/src/oversample.ts +39 -11
- package/src/records.ts +109 -109
- package/src/shaders.ts +93 -14
- package/src/tiles.ts +256 -48
- package/src/views.ts +294 -0
- package/src/components.tsx +0 -357
package/src/layer.ts
CHANGED
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
// SPATIAL ARENA node (arena slot: local pose, parent, world matrix, index
|
|
3
3
|
// leaf, record sink - no layout, no paint, no rendertree element) whose
|
|
4
4
|
// Pose2D record sink writes [x, y, angle, sx, sy] into the sprite's slot of
|
|
5
|
-
// the pose instance buffer at the core's flush. Rendering
|
|
6
|
-
// instanced draw
|
|
7
|
-
//
|
|
8
|
-
// instance buffer - the arena, so every core
|
|
9
|
-
// animation clips, physics) reaches sprites
|
|
10
|
-
// picking walks the core BVH instead of a JS
|
|
5
|
+
// the pose instance buffer at the core's flush. Rendering is one
|
|
6
|
+
// instanced draw per VIEW (views.ts: the layer shows through its views
|
|
7
|
+
// only, each a target with a camera of its own); what changed is who owns
|
|
8
|
+
// the pose upstream of the instance buffer - the arena, so every core
|
|
9
|
+
// producer (native transitions, animation clips, physics) reaches sprites
|
|
10
|
+
// through `sprite.node`, and picking walks the core BVH instead of a JS
|
|
11
|
+
// loop.
|
|
11
12
|
//
|
|
12
13
|
// Two instance-buffer slots split ownership: slot 0 is the pose buffer,
|
|
13
14
|
// 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
|
|
15
|
+
// nodes moved); slot 1 is the style buffer [u0, v0, u1, v1, tint rgba,
|
|
16
|
+
// renderOrder], JS-owned and published through the zero-copy write lease. Never write
|
|
16
17
|
// the pose buffer from JS - the core's staging mirror is the owner and
|
|
17
18
|
// will overwrite. For motion only JS can compute at large populations, the
|
|
18
19
|
// records layer (records.ts) is the escape hatch.
|
|
@@ -20,43 +21,69 @@
|
|
|
20
21
|
// Sprites hold FIXED instance slots (freed slots recycle): draw order is
|
|
21
22
|
// slot order, so removal never shifts records and pose sinks never rebind.
|
|
22
23
|
// Layer space is pixels, top-left origin, y-down - the render tree's
|
|
23
|
-
// frame.
|
|
24
|
+
// frame. A view's camera (offset, zoom, rotation about a pivot -
|
|
25
|
+
// CameraUpdate in camera.ts) is a shared-params write (uCamera +
|
|
26
|
+
// uCameraRot) on that view's target, never per-sprite; pointer dispatch
|
|
27
|
+
// (dispatch.ts) undoes it with unprojectCamera.
|
|
24
28
|
import { getOwner, onCleanup } from "@solidrt/core"
|
|
25
|
-
import type { PointerEvent as ElementPointerEvent } from "@solidrt/core"
|
|
26
|
-
import {
|
|
27
|
-
beginBufferWrite,
|
|
28
|
-
createBuffer,
|
|
29
|
-
createPipelineTexture,
|
|
30
|
-
destroyBuffer,
|
|
31
|
-
destroyTexture,
|
|
32
|
-
endBufferWrite,
|
|
33
|
-
setDraw,
|
|
34
|
-
setTargetParams,
|
|
35
|
-
setTargetSize,
|
|
36
|
-
} from "@solidrt/core/gpu"
|
|
29
|
+
import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEvent } from "@solidrt/core"
|
|
30
|
+
import { beginBufferWrite, createBuffer, destroyBuffer, endBufferWrite } from "@solidrt/core/gpu"
|
|
37
31
|
import type { BufferId, TextureId } from "@solidrt/core/gpu"
|
|
38
32
|
import * as spatial from "flux:spatial"
|
|
39
|
-
import type { NodeId, NodeTransition } from "flux:spatial"
|
|
33
|
+
import type { Impact as CoreImpact, MoveOptions as CoreMoveOptions, NodeId, NodeTransition, QueryFilter } from "flux:spatial"
|
|
40
34
|
import { on } from "srt:events"
|
|
41
|
-
import {
|
|
35
|
+
import type { CameraState, CameraUpdate } from "./camera.ts"
|
|
42
36
|
import type { Frame } from "./frames.ts"
|
|
43
37
|
import { FULL_FRAME, writeFrame } from "./frames.ts"
|
|
44
38
|
import type { RecordLayer } from "./records.ts"
|
|
45
|
-
import {
|
|
39
|
+
import { createSpritePipeline, INSTANCE_ATTRIBUTES_SPLIT, VERTEX_SPLIT } from "./shaders.ts"
|
|
40
|
+
import { createViews } from "./views.ts"
|
|
41
|
+
import type { ViewHandle, ViewOptions } from "./views.ts"
|
|
46
42
|
|
|
47
43
|
/** Floats per pose record (the core's Pose2D projection). */
|
|
48
44
|
export const POSE_FLOATS = 5
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
|
|
46
|
+
// Float offset of world y in a pose record - what `orderBy: "y"` keys on.
|
|
47
|
+
const POSE_Y_FIELD = 1
|
|
48
|
+
/** Floats per style record:
|
|
49
|
+
* [u0, v0, u1, v1, tintR, tintG, tintB, tintA, renderOrder]. */
|
|
50
|
+
export const STYLE_FLOATS = 9
|
|
51
|
+
|
|
52
|
+
// Float offset of renderOrder in a style record - what `orderBy: "renderOrder"`
|
|
53
|
+
// keys on.
|
|
54
|
+
const STYLE_KEY_FIELD = 8
|
|
51
55
|
|
|
52
56
|
const RESOLVED = Promise.resolve()
|
|
53
57
|
|
|
54
58
|
// Shared marshalling scratch (the bindings copy synchronously).
|
|
55
59
|
const TRANSFORM = new Float32Array(10)
|
|
56
|
-
|
|
60
|
+
// Half depth of a sprite's index box along z, node units (a sprite's z
|
|
61
|
+
// scale is 1, so world units too). A sprite is a COLUMN in the index, not
|
|
62
|
+
// a flat quad: a volume query at z = 0 then only ever meets its side
|
|
63
|
+
// faces, so every contact normal and depth is in the plane, and a circle
|
|
64
|
+
// whose center lies over a sprite pushes out sideways instead of along z
|
|
65
|
+
// (the core's contact against a flat triangle is the plane normal). Far
|
|
66
|
+
// larger than any volume's reach, so the end faces never take part.
|
|
67
|
+
const SPRITE_DEPTH = 1e4
|
|
68
|
+
const COLUMN_BOUNDS = new Float32Array([-0.5, -0.5, -SPRITE_DEPTH, 0.5, 0.5, SPRITE_DEPTH])
|
|
57
69
|
const RAY_ORIGIN = new Float32Array(3)
|
|
58
|
-
const RAY_DIR = new Float32Array(
|
|
59
|
-
|
|
70
|
+
const RAY_DIR = new Float32Array(3)
|
|
71
|
+
// Volume scratch: a "capsule" is a, b, radius (7 floats), a "box" is
|
|
72
|
+
// center, half extents, quaternion (10); both packed at z = 0.
|
|
73
|
+
const CAPSULE = new Float32Array(7)
|
|
74
|
+
const BOX = new Float32Array(10)
|
|
75
|
+
const MOTION = new Float32Array(3)
|
|
76
|
+
// A box volume's half depth: any positive value inside the columns.
|
|
77
|
+
const BOX_HALF_DEPTH = 1
|
|
78
|
+
// The pick ray starts this far in front of the plane; it crosses every
|
|
79
|
+
// column at z = 0 whatever the start, but a start inside the column meets
|
|
80
|
+
// only its far end face, which is fine for pick (draw order sorts, not
|
|
81
|
+
// distance).
|
|
82
|
+
const PICK_RAY_START = -1
|
|
83
|
+
// The direction floors face in a y-down world.
|
|
84
|
+
const UP_2D: [number, number] = [0, -1]
|
|
85
|
+
// worldPosition's world-matrix scratch (column-major; translation at 12, 13).
|
|
86
|
+
const WORLD = new Float32Array(16)
|
|
60
87
|
|
|
61
88
|
// Settle routing: the core's "spatialTransitionEnd" event carries the node
|
|
62
89
|
// id, so the handles with a transition DECLARED are indexed by node (only
|
|
@@ -91,37 +118,60 @@ function declareTransition(node: NodeId, handle: Sprite | SpriteGroup, transitio
|
|
|
91
118
|
* fields - they touch no GPU state (the scene-graph handler rule).
|
|
92
119
|
*/
|
|
93
120
|
export type Sprite = {
|
|
94
|
-
/** The owning layer, null after removeSprite. */
|
|
95
|
-
layer: SpriteLayer | RecordLayer | null
|
|
121
|
+
/** The owning layer, null after removeSprite (which owns the write). */
|
|
122
|
+
readonly layer: SpriteLayer | RecordLayer | null
|
|
96
123
|
/**
|
|
97
124
|
* The sprite's SPATIAL ARENA node - the citizenship handle: bind core
|
|
98
125
|
* producers to it or reach it through flux:spatial directly (the layer
|
|
99
126
|
* still owns the node's life; destroy it only via removeSprite). Null
|
|
100
127
|
* on a record layer's sprites.
|
|
101
128
|
*/
|
|
102
|
-
node: NodeId | null
|
|
129
|
+
readonly node: NodeId | null
|
|
103
130
|
/** 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
|
-
|
|
131
|
+
* shifting draw-order index on a record layer. Readable (readonly, like
|
|
132
|
+
* the other underscore fields: cheap reads without getSprite's
|
|
133
|
+
* allocation); writes go through setSprite. */
|
|
134
|
+
readonly _slot: number
|
|
135
|
+
/** Pose mirror (node layer): what setSprite composes transforms from.
|
|
136
|
+
* On a RECORD layer this mirror goes stale once records are written
|
|
137
|
+
* raw - there the records array is the truth. */
|
|
138
|
+
readonly _x: number
|
|
139
|
+
readonly _y: number
|
|
140
|
+
readonly _w: number
|
|
141
|
+
readonly _h: number
|
|
142
|
+
readonly _rot: number
|
|
143
|
+
/** Mirror flags (both layer kinds): re-applied to every frame write.
|
|
144
|
+
* The cheap way to read flip state. */
|
|
145
|
+
readonly _flipX: boolean
|
|
146
|
+
readonly _flipY: boolean
|
|
147
|
+
/** Visibility mirror (node layer; always true on a record layer's
|
|
148
|
+
* sprites). The cheap read; writes go through setSprite. */
|
|
149
|
+
readonly _visible: boolean
|
|
150
|
+
/** The enclosing group (null at the layer root, always null on a record
|
|
151
|
+
* layer's sprites) - the bubble path; writes go through
|
|
152
|
+
* setSpriteParent/addSprite's `parent`. */
|
|
153
|
+
readonly _parent: SpriteGroup | null
|
|
115
154
|
onPointerDown?: (event: SpritePointerEvent) => void
|
|
116
155
|
onPointerMove?: (event: SpritePointerEvent) => void
|
|
117
156
|
onPointerUp?: (event: SpritePointerEvent) => void
|
|
118
157
|
onPointerEnter?: (event: SpritePointerEvent) => void
|
|
119
158
|
onPointerLeave?: (event: SpritePointerEvent) => void
|
|
159
|
+
/** A wheel notch over the sprite; bubbles like down/move/up. */
|
|
160
|
+
onWheel?: (event: SpriteWheelEvent) => void
|
|
161
|
+
/** A press that released on this sprite without dragging (see
|
|
162
|
+
* SpriteTapEvent); bubbles like down/move/up. */
|
|
163
|
+
onTap?: (event: SpriteTapEvent) => void
|
|
120
164
|
/** A declared transition (setSpriteTransition) settled naturally on
|
|
121
165
|
* one component; a cancel or snap never fires. */
|
|
122
166
|
onTransitionEnd?: (event: TransitionEndEvent) => void
|
|
123
167
|
}
|
|
124
168
|
|
|
169
|
+
/** The layer-internal mutable view of a Sprite: the readonly on the
|
|
170
|
+
* underscore fields is for the public surface; internal write sites
|
|
171
|
+
* annotate with this (readonly does not affect assignability, so the two
|
|
172
|
+
* types flow into each other freely). */
|
|
173
|
+
export type SpriteState = { -readonly [K in keyof Sprite]: Sprite[K] }
|
|
174
|
+
|
|
125
175
|
/** The settled component of a node transition: `position` is x/y,
|
|
126
176
|
* `scale` w/h (a group's uniform scale). */
|
|
127
177
|
export type TransitionEndEvent = {
|
|
@@ -148,17 +198,41 @@ export type SpriteOptions = {
|
|
|
148
198
|
rotation?: number
|
|
149
199
|
/** RGBA multiplier 0..1 each; default opaque white (the texture as-is). */
|
|
150
200
|
tint?: [number, number, number, number]
|
|
201
|
+
/**
|
|
202
|
+
* Explicit draw-order key, read only by a layer created with `orderBy:
|
|
203
|
+
* "renderOrder"` (default 0; ties keep slot order, so untouched sprites draw
|
|
204
|
+
* as without one). The raise idiom: `setSprite(hit, { renderOrder: ++top })`
|
|
205
|
+
* on interaction, back to 0 to restore. Node layer only - a record
|
|
206
|
+
* layer's 13-float record has no key field (order it by one of its own
|
|
207
|
+
* fields with `orderBy: { field }`); setting this there throws.
|
|
208
|
+
*/
|
|
209
|
+
renderOrder?: number
|
|
210
|
+
/**
|
|
211
|
+
* Show or hide the sprite (default true), @solidrt/3d's setVisible in
|
|
212
|
+
* the setSprite bag: hidden, its pose slot zeroes (nothing drawn) and
|
|
213
|
+
* pick/pickRect skip it; the handle, slot, style records and any
|
|
214
|
+
* running transition state stay, so showing again restores the sprite
|
|
215
|
+
* as it was. Node layer only - a record sprite has no node (hide it by
|
|
216
|
+
* zeroing w or h); setting this there throws.
|
|
217
|
+
*/
|
|
218
|
+
visible?: boolean
|
|
151
219
|
}
|
|
152
220
|
|
|
221
|
+
/**
|
|
222
|
+
* addSprite's bag: the sprite fields plus the mount point. The pool idiom
|
|
223
|
+
* for a game: reserve `capacity`, add every sprite at mount with `visible:
|
|
224
|
+
* false`, then show/hide - a hidden sprite keeps its slot, so draw order
|
|
225
|
+
* stays stable and a spawn is one setSprite, never an add.
|
|
226
|
+
*/
|
|
153
227
|
export type AddSpriteOptions = SpriteOptions & {
|
|
154
|
-
/** Mount under this group (node layer only
|
|
155
|
-
* to it. Reparent later with
|
|
156
|
-
|
|
228
|
+
/** Mount under this group (node layer only; null = the layer root, the
|
|
229
|
+
* default); pose fields are then local to it. Reparent later with
|
|
230
|
+
* setSpriteParent. */
|
|
231
|
+
parent?: SpriteGroup | null
|
|
157
232
|
}
|
|
158
233
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
sprite: Sprite
|
|
234
|
+
/** What every layer pointer event carries, whichever handler sees it. */
|
|
235
|
+
export type LayerEventBase = {
|
|
162
236
|
/** Pointer position in LAYER pixels (the camera mapping undone). */
|
|
163
237
|
x: number
|
|
164
238
|
y: number
|
|
@@ -169,6 +243,74 @@ export type SpritePointerEvent = {
|
|
|
169
243
|
ctrlKey: boolean
|
|
170
244
|
altKey: boolean
|
|
171
245
|
metaKey: boolean
|
|
246
|
+
/**
|
|
247
|
+
* The element event the layer's leaf received, in the leaf's own frame
|
|
248
|
+
* (localX/localY, clientX/clientY, movementX/Y): what core's
|
|
249
|
+
* recognizers consume, so a sprite drags itself through `createPan` and
|
|
250
|
+
* a camera rides `createTransform` on the same events the walk carries.
|
|
251
|
+
*/
|
|
252
|
+
native: ElementPointerEvent
|
|
253
|
+
/**
|
|
254
|
+
* Stops the walk after the current handler: no enclosing group and none
|
|
255
|
+
* of the layer's listeners see the event. Stopping a DOWN claims the
|
|
256
|
+
* whole press - that pointer's move, up and tap never reach the layer
|
|
257
|
+
* either, so a sprite that drags itself stops its down once and a
|
|
258
|
+
* camera attached at the root never pans it.
|
|
259
|
+
*/
|
|
260
|
+
stopPropagation(): void
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** The event as a sprite's or group's handler sees it. */
|
|
264
|
+
export type SpritePointerEvent = LayerEventBase & {
|
|
265
|
+
/** The sprite the event is about (the topmost hit at the point, or the
|
|
266
|
+
* captured sprite during a drag) - constant while the event bubbles. */
|
|
267
|
+
sprite: Sprite
|
|
268
|
+
/** The sprite or group whose handler is running; changes as the event
|
|
269
|
+
* bubbles from the hit sprite through its enclosing groups. */
|
|
270
|
+
currentTarget: Sprite | SpriteGroup
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The event as the VIEW's listeners see it (ViewHandle.listen), the last
|
|
275
|
+
* stop of the walk: `sprite` is the hit sprite it bubbled from, or null
|
|
276
|
+
* over empty space, where the view is the only target.
|
|
277
|
+
*/
|
|
278
|
+
export type LayerPointerEvent = LayerEventBase & {
|
|
279
|
+
sprite: Sprite | null
|
|
280
|
+
currentTarget: ViewHandle
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
type WheelFields = {
|
|
284
|
+
/** The wheel delta as the element event reports it. */
|
|
285
|
+
deltaX: number
|
|
286
|
+
deltaY: number
|
|
287
|
+
native: ElementWheelEvent
|
|
288
|
+
}
|
|
289
|
+
type TapFields = {
|
|
290
|
+
/** 1 for a tap, 2 for the second of a double tap (the same target,
|
|
291
|
+
* within the repeat interval and distance), and so on - DOM's `detail`,
|
|
292
|
+
* Unity's `clickCount`. */
|
|
293
|
+
tapCount: number
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export type SpriteWheelEvent = SpritePointerEvent & WheelFields
|
|
297
|
+
export type LayerWheelEvent = LayerPointerEvent & WheelFields
|
|
298
|
+
/**
|
|
299
|
+
* A press that released on the target it pressed without travelling past
|
|
300
|
+
* the slop, the only pointer down for its whole press (a pinch never taps).
|
|
301
|
+
* Dispatched after the up, bubbling the same way; `x`/`y` are the release
|
|
302
|
+
* point.
|
|
303
|
+
*/
|
|
304
|
+
export type SpriteTapEvent = SpritePointerEvent & TapFields
|
|
305
|
+
export type LayerTapEvent = LayerPointerEvent & TapFields
|
|
306
|
+
|
|
307
|
+
/** A listener at a view's root; see ViewHandle.listen. */
|
|
308
|
+
export type LayerPointerListener = {
|
|
309
|
+
onPointerDown?: (event: LayerPointerEvent) => void
|
|
310
|
+
onPointerMove?: (event: LayerPointerEvent) => void
|
|
311
|
+
onPointerUp?: (event: LayerPointerEvent) => void
|
|
312
|
+
onWheel?: (event: LayerWheelEvent) => void
|
|
313
|
+
onTap?: (event: LayerTapEvent) => void
|
|
172
314
|
}
|
|
173
315
|
|
|
174
316
|
export type SpriteHandlers = {
|
|
@@ -176,14 +318,15 @@ export type SpriteHandlers = {
|
|
|
176
318
|
onPointerMove: (event: ElementPointerEvent) => void
|
|
177
319
|
onPointerUp: (event: ElementPointerEvent) => void
|
|
178
320
|
onPointerLeave: (event: ElementPointerEvent) => void
|
|
321
|
+
onWheel: (event: ElementWheelEvent) => void
|
|
179
322
|
}
|
|
180
323
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
324
|
+
/** Validate an [r, g, b, a] tint (throws - the dev validation policy).
|
|
325
|
+
* Internal - every layer kind (sprite, record, tile) calls it. */
|
|
326
|
+
export function checkTint(verb: string, tint: [number, number, number, number]): void {
|
|
327
|
+
if (!(Array.isArray(tint) && tint.length === 4 && tint.every(Number.isFinite))) {
|
|
328
|
+
throw new Error(`${verb}: tint must be [r, g, b, a], got ${JSON.stringify(tint)}`)
|
|
329
|
+
}
|
|
187
330
|
}
|
|
188
331
|
|
|
189
332
|
export type SpriteLayerOptions = {
|
|
@@ -193,19 +336,39 @@ export type SpriteLayerOptions = {
|
|
|
193
336
|
* limit.
|
|
194
337
|
*/
|
|
195
338
|
capacity?: number
|
|
196
|
-
|
|
339
|
+
/** Layer tint, [r, g, b, a] in 0..1, multiplied over every sprite's own
|
|
340
|
+
* tint in every view; default opaque white (sprites as-is). See
|
|
341
|
+
* setTint. */
|
|
342
|
+
tint?: [number, number, number, number]
|
|
343
|
+
/** Names the GPU resources (buffers, pipeline; views default to
|
|
344
|
+
* `<label>-view`); default "sprites". */
|
|
197
345
|
label?: string
|
|
198
|
-
/**
|
|
199
|
-
* Target texels per layer pixel (positive integer, default 1). The layer
|
|
200
|
-
* renders at `oversample` times its size and is composited down, so a
|
|
201
|
-
* fractional or HiDPI display scale resamples properly instead of snapping
|
|
202
|
-
* (nearest) or smearing (linear); see setOversample. The components pick
|
|
203
|
-
* it from the leaf's on-screen size - set it here when composing the
|
|
204
|
-
* output yourself.
|
|
205
|
-
*/
|
|
206
|
-
oversample?: number
|
|
207
346
|
/** Skip the owner-scoped auto-dispose (see createSpriteLayer). */
|
|
208
347
|
autoFree?: boolean
|
|
348
|
+
/**
|
|
349
|
+
* Draw sprites in KEY order instead of slot order, produced by core at
|
|
350
|
+
* every publish (the gpu `instanceOrder` primitive across the pose/style
|
|
351
|
+
* buffer pair - no per-sprite JS anywhere): `"y"` keys on the sprite's
|
|
352
|
+
* WORLD y, the pose the core itself writes, so a perspective crowd
|
|
353
|
+
* paints back to front (smaller y = further up the screen = drawn
|
|
354
|
+
* first) - and because the key is core-owned, sprites moved by native
|
|
355
|
+
* transitions or any other core producer re-sort with zero JS per
|
|
356
|
+
* frame. Slots stay fixed - handles, picking and the style records are
|
|
357
|
+
* untouched, only the draw order changes - and ties keep slot order, so
|
|
358
|
+
* sprites at equal y draw exactly as without it.
|
|
359
|
+
*
|
|
360
|
+
* `"renderOrder"` keys on the app-owned per-sprite `renderOrder` field instead
|
|
361
|
+
* (setSprite, default 0): explicit layering for painter-order scenes -
|
|
362
|
+
* raise a dragged piece, click-to-front, hover emphasis - with stable
|
|
363
|
+
* handles and no record churn.
|
|
364
|
+
*
|
|
365
|
+
* Known limitation for both keys: pick() resolves overlapping sprites by
|
|
366
|
+
* slot order, not visual order, when a key is set. (The record layer's
|
|
367
|
+
* RecordLayerOptions instead takes `"y"` or a raw `{ field }` offset
|
|
368
|
+
* into its own records; pose records are core-owned, so the node layer
|
|
369
|
+
* names its keys.)
|
|
370
|
+
*/
|
|
371
|
+
orderBy?: "y" | "renderOrder"
|
|
209
372
|
}
|
|
210
373
|
|
|
211
374
|
/**
|
|
@@ -213,21 +376,44 @@ export type SpriteLayerOptions = {
|
|
|
213
376
|
* uniform scale - never a sprite size) that sprites and other groups
|
|
214
377
|
* parent under, so a ship with turrets or a dragged stack moves as one
|
|
215
378
|
* subtree recomputed in native code. Groups render nothing and cannot be
|
|
216
|
-
* picked
|
|
379
|
+
* picked - sprites are always the leaves - but down/move/up events bubble
|
|
380
|
+
* from a hit child sprite through its enclosing groups (enter/leave pair
|
|
381
|
+
* on the sprite alone), so one group handler covers a whole assembly.
|
|
217
382
|
*/
|
|
218
383
|
export type SpriteGroup = {
|
|
219
|
-
/** The owning layer, null after removeGroup. */
|
|
220
|
-
layer: SpriteLayer | null
|
|
384
|
+
/** The owning layer, null after removeGroup (which owns the write). */
|
|
385
|
+
readonly layer: SpriteLayer | null
|
|
221
386
|
/** The group's spatial arena node. */
|
|
222
|
-
node: NodeId
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
387
|
+
readonly node: NodeId
|
|
388
|
+
/** Pose mirror (readonly, like Sprite's underscore fields: cheap reads);
|
|
389
|
+
* writes go through setGroup. */
|
|
390
|
+
readonly _x: number
|
|
391
|
+
readonly _y: number
|
|
392
|
+
readonly _rot: number
|
|
393
|
+
readonly _scale: number
|
|
394
|
+
/** Visibility mirror; writes go through setGroup. */
|
|
395
|
+
readonly _visible: boolean
|
|
396
|
+
/** See Sprite._parent. */
|
|
397
|
+
readonly _parent: SpriteGroup | null
|
|
398
|
+
/** The handles parented here (removeGroup removes them with it);
|
|
399
|
+
* internal -
|
|
400
|
+
* membership writes go through addSprite/addGroup/setSpriteParent/
|
|
401
|
+
* setGroup. */
|
|
402
|
+
readonly _children: Set<SpriteState | GroupState>
|
|
403
|
+
/** Bubbled from a hit child sprite; see SpritePointerEvent. A group
|
|
404
|
+
* never receives enter/leave (hover pairs on the sprite alone). */
|
|
405
|
+
onPointerDown?: (event: SpritePointerEvent) => void
|
|
406
|
+
onPointerMove?: (event: SpritePointerEvent) => void
|
|
407
|
+
onPointerUp?: (event: SpritePointerEvent) => void
|
|
408
|
+
onWheel?: (event: SpriteWheelEvent) => void
|
|
409
|
+
onTap?: (event: SpriteTapEvent) => void
|
|
227
410
|
/** See Sprite.onTransitionEnd. */
|
|
228
411
|
onTransitionEnd?: (event: TransitionEndEvent) => void
|
|
229
412
|
}
|
|
230
413
|
|
|
414
|
+
/** The layer-internal mutable view of a SpriteGroup (see SpriteState). */
|
|
415
|
+
export type GroupState = { -readonly [K in keyof SpriteGroup]: SpriteGroup[K] }
|
|
416
|
+
|
|
231
417
|
/** Group fields, all optional: absent keys keep values. */
|
|
232
418
|
export type GroupOptions = {
|
|
233
419
|
/** Position in the parent frame (layer pixels at the root). */
|
|
@@ -238,142 +424,185 @@ export type GroupOptions = {
|
|
|
238
424
|
/** Uniform scale on the whole subtree (this one scales child sprites -
|
|
239
425
|
* a group is a frame, not a sprite size). */
|
|
240
426
|
scale?: number
|
|
427
|
+
/** Show or hide the group's WHOLE subtree (default true); each child
|
|
428
|
+
* keeps its own `visible`, so showing the group again restores the
|
|
429
|
+
* subtree as it was. See SpriteOptions.visible. */
|
|
430
|
+
visible?: boolean
|
|
241
431
|
/** Reparent (null = make the group a root). */
|
|
242
432
|
parent?: SpriteGroup | null
|
|
243
433
|
}
|
|
244
434
|
|
|
245
435
|
/** What both layer kinds share; the free sprite functions dispatch on it. */
|
|
436
|
+
/**
|
|
437
|
+
* What both layer kinds share: the sprites and what applies to all of
|
|
438
|
+
* them. A layer has no output of its own - it shows through its views
|
|
439
|
+
* (createView), each a target with a camera, a size and pointer dispatch
|
|
440
|
+
* of its own (ViewHandle), as a Unity scene shows only through Cameras
|
|
441
|
+
* and a Godot World2D only through Viewports.
|
|
442
|
+
*/
|
|
246
443
|
export type LayerBase = {
|
|
247
|
-
/** The layer's output: an ordinary texture id (`<texture src>`). */
|
|
248
|
-
texture: TextureId
|
|
249
|
-
/** Element handlers wiring sprite pointer events; see handlersFor. */
|
|
250
|
-
handlers: SpriteHandlers
|
|
251
444
|
/** Live sprite count. */
|
|
252
445
|
readonly count: number
|
|
253
|
-
setSize(width: number, height: number): void
|
|
254
|
-
/** Target texels per layer pixel; see setOversample. */
|
|
255
|
-
readonly oversample: number
|
|
256
446
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
447
|
+
* Tint the whole layer, [r, g, b, a] in 0..1: a uniform multiplied over
|
|
448
|
+
* every sprite's own tint (day/night, a dimmed parallax plane, a
|
|
449
|
+
* fade-in) in every view. One shared-params write per view - no record
|
|
450
|
+
* touches, cheap to animate - and the same contract as
|
|
451
|
+
* TileLayer.setTint, so one signal drives a whole scene across layer
|
|
452
|
+
* kinds.
|
|
262
453
|
*/
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
454
|
+
setTint(tint: [number, number, number, number]): void
|
|
455
|
+
/** Every shown sprite whose rotated rect contains the layer-pixel point
|
|
456
|
+
* (a hidden sprite or subtree is never hit), topmost first - the
|
|
457
|
+
* all-hits shape of @solidrt/3d's pick; `pick(x, y)[0]` is the topmost.
|
|
458
|
+
* Topmost means draw order (highest slot on the node layer, last added
|
|
459
|
+
* on a record layer); an `orderBy` key is not consulted (see
|
|
460
|
+
* SpriteLayerOptions.orderBy's known limitation). World space: a view
|
|
461
|
+
* undoes its camera before asking. */
|
|
462
|
+
pick(x: number, y: number): Sprite[]
|
|
267
463
|
/**
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
464
|
+
* A rendering of the layer's world from a camera of its own - the main
|
|
465
|
+
* view, a minimap, a zoomed inset, one split-screen pane -
|
|
466
|
+
* @solidrt/3d's scene.createView one dimension down. One more target
|
|
467
|
+
* drawing the SAME instance buffers (no sprite is mirrored, no per-frame
|
|
468
|
+
* JS beyond the view's camera writes), key order included; the layer's
|
|
469
|
+
* tint fans out to it. The view carries the viewport contract (texture,
|
|
470
|
+
* setSize, setOversample, setCamera, listen and handlers with sprite
|
|
471
|
+
* events walking to the view as root); sprites stay the layer's. Views
|
|
472
|
+
* die with the layer.
|
|
271
473
|
*/
|
|
272
|
-
|
|
474
|
+
createView(opts: ViewOptions): ViewHandle
|
|
273
475
|
dispose(): void
|
|
274
476
|
_add(opts?: AddSpriteOptions): Sprite
|
|
275
|
-
_write(sprite:
|
|
477
|
+
_write(sprite: SpriteState, opts: SpriteOptions): void
|
|
276
478
|
_read(sprite: Sprite): Required<SpriteOptions>
|
|
277
|
-
_remove(sprite:
|
|
479
|
+
_remove(sprite: SpriteState): void
|
|
278
480
|
_schedule(): void
|
|
279
481
|
}
|
|
280
482
|
|
|
483
|
+
/** A query volume for overlap/sweep/moveAndSlide, in layer pixels: a
|
|
484
|
+
* circle, a capsule (the radius swept along the segment a-b: a character)
|
|
485
|
+
* or a rect (`rotation` radians about its center, 0 when absent) - Godot's
|
|
486
|
+
* CircleShape2D/CapsuleShape2D/RectangleShape2D, @solidrt/3d's Volume one
|
|
487
|
+
* dimension down. */
|
|
488
|
+
export type Circle = { x: number; y: number; radius: number }
|
|
489
|
+
export type Capsule = { ax: number; ay: number; bx: number; by: number; radius: number }
|
|
490
|
+
export type Rect = { x: number; y: number; width: number; height: number; rotation?: number }
|
|
491
|
+
export type Volume = Circle | Capsule | Rect
|
|
492
|
+
|
|
493
|
+
/** Filters for one raycast/overlap/sweep/moveAndSlide query. */
|
|
494
|
+
export type QueryOptions = {
|
|
495
|
+
/** Only these sprites report hits (an include-list); every shown sprite
|
|
496
|
+
* of the layer otherwise. */
|
|
497
|
+
sprites?: Sprite[]
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** One sprite a raycast() ray strikes: the distance along the ray, the
|
|
501
|
+
* point on the sprite's edge and the unit edge normal facing the ray. */
|
|
502
|
+
export type RayHit = { sprite: Sprite; distance: number; point: [number, number]; normal: [number, number] }
|
|
503
|
+
|
|
504
|
+
/** One sprite an overlap() volume touches: its deepest contact - the
|
|
505
|
+
* point on the sprite's edge, the unit direction out of the sprite, and
|
|
506
|
+
* the depth along that direction that clears the contact. */
|
|
507
|
+
export type Overlap = { sprite: Sprite; point: [number, number]; normal: [number, number]; depth: number }
|
|
508
|
+
|
|
509
|
+
/** One sprite a sweep() volume touches on its way: `time` is the fraction
|
|
510
|
+
* of the motion at first touch (0 for a volume already in contact and
|
|
511
|
+
* moving in), `point` the touch point on the sprite's edge, `normal` the
|
|
512
|
+
* unit normal there facing the volume. */
|
|
513
|
+
export type Impact = { sprite: Sprite; time: number; point: [number, number]; normal: [number, number] }
|
|
514
|
+
|
|
515
|
+
export type MoveOptions = QueryOptions & {
|
|
516
|
+
/** The direction floors face (default [0, -1]: y-down, floors face up
|
|
517
|
+
* the screen). */
|
|
518
|
+
up?: [number, number]
|
|
519
|
+
/** Largest angle (radians) between a contact normal and `up` that still
|
|
520
|
+
* counts as floor (default 45 degrees). Steeper contacts are walls, and
|
|
521
|
+
* the body slides down them. */
|
|
522
|
+
floorMaxAngle?: number
|
|
523
|
+
/** Most contacts one call slides along (default 6). */
|
|
524
|
+
maxSlides?: number
|
|
525
|
+
/** Gap kept from every surface, layer pixels (default 0.01). */
|
|
526
|
+
skin?: number
|
|
527
|
+
/** How far below the body a floor is pulled to when the motion does not
|
|
528
|
+
* rise (default 0.1; 0 disables): what keeps a walker on a ramp going
|
|
529
|
+
* down, and what decides `floor` at the end of the move - a body that
|
|
530
|
+
* ends higher than this above its floor is airborne, whatever it
|
|
531
|
+
* touched on the way. A rising motion (a jump) never snaps. */
|
|
532
|
+
floorSnap?: number
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export type MoveResult = {
|
|
536
|
+
/** The displacement the body gets: add it to the body's position. */
|
|
537
|
+
motion: [number, number]
|
|
538
|
+
/** The unit normal of the floor the body ends the move on - within
|
|
539
|
+
* `floorSnap` below it, snapped onto - else null (airborne, or on a
|
|
540
|
+
* slope too steep to stand on). With `floorSnap: 0` it is a floor met
|
|
541
|
+
* during the move instead. */
|
|
542
|
+
floor: [number, number] | null
|
|
543
|
+
/** Whether a wall (a contact steeper than a floor and flatter than a
|
|
544
|
+
* ceiling) or a ceiling was met. */
|
|
545
|
+
wall: boolean
|
|
546
|
+
ceiling: boolean
|
|
547
|
+
/** Every contact met, in order, the floor snap's last. */
|
|
548
|
+
hits: Impact[]
|
|
549
|
+
}
|
|
550
|
+
|
|
281
551
|
export type SpriteLayer = LayerBase & {
|
|
282
552
|
/**
|
|
283
|
-
* Every sprite whose rotated rect overlaps the layer-pixel rect
|
|
284
|
-
* core BVH overlap query, exact for rotated sprites), unordered -
|
|
285
|
-
* marquee query
|
|
553
|
+
* Every shown sprite whose rotated rect overlaps the layer-pixel rect
|
|
554
|
+
* (the core BVH overlap query, exact for rotated sprites), unordered -
|
|
555
|
+
* the marquee query: `overlap` over an unrotated rect, sprites only.
|
|
556
|
+
* Node layer only.
|
|
286
557
|
*/
|
|
287
|
-
pickRect(x: number, y: number,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
}
|
|
558
|
+
pickRect(x: number, y: number, width: number, height: number): Sprite[]
|
|
559
|
+
/**
|
|
560
|
+
* Every shown sprite the ray from (x, y) along (dx, dy) strikes, nearest
|
|
561
|
+
* first, with the distance (layer pixels along the normalized
|
|
562
|
+
* direction), the point on the sprite's edge and the edge normal.
|
|
563
|
+
* `pick` is the point form; this is the shot and the line of sight.
|
|
564
|
+
* Reads the index as of the last flush, the pending batch run first,
|
|
565
|
+
* like pick.
|
|
566
|
+
*/
|
|
567
|
+
raycast(x: number, y: number, dx: number, dy: number, opts?: QueryOptions): RayHit[]
|
|
568
|
+
/**
|
|
569
|
+
* Every shown sprite the volume touches, each with its deepest contact
|
|
570
|
+
* (Godot's intersect_shape, Unity's OverlapCircle/Box/Capsule: a blast
|
|
571
|
+
* radius, a pickup range, a melee arc), unordered. Sprites are tested
|
|
572
|
+
* as their rotated rects, so any rotation holds; a volume wholly inside
|
|
573
|
+
* a sprite touches it (a sprite is a solid rect to a volume, its
|
|
574
|
+
* push-out the nearest edge's). Same index contract as raycast.
|
|
575
|
+
*/
|
|
576
|
+
overlap(volume: Volume, opts?: QueryOptions): Overlap[]
|
|
577
|
+
/**
|
|
578
|
+
* The volume moved by (dx, dy): every shown sprite it touches on the
|
|
579
|
+
* way, at its first touch, earliest first (Godot's cast_motion, Unity's
|
|
580
|
+
* CircleCast/BoxCast: a bullet, a dash). A volume already in contact
|
|
581
|
+
* reports time 0 while the motion closes in, and nothing while it leaves
|
|
582
|
+
* or slides along the contact, which is what lets a slide along a wall
|
|
583
|
+
* proceed. A zero motion touches nothing.
|
|
584
|
+
*/
|
|
585
|
+
sweep(volume: Volume, dx: number, dy: number, opts?: QueryOptions): Impact[]
|
|
586
|
+
/**
|
|
587
|
+
* Move a body by (dx, dy) through the layer's sprites, sliding along
|
|
588
|
+
* what it hits (Godot's CharacterBody2D.move_and_slide, the platformer
|
|
589
|
+
* and top-down character mover), as one pure call: no sprite, no
|
|
590
|
+
* velocity state - it takes a volume where the body IS and the motion
|
|
591
|
+
* it WANTS, and returns the motion it gets plus what it touched. The
|
|
592
|
+
* body first pushes out of anything it starts inside, then sweeps and
|
|
593
|
+
* slides up to `maxSlides` times, then, unless the motion rises, snaps
|
|
594
|
+
* down onto a floor within `floorSnap` - the floor it reports is the
|
|
595
|
+
* one it ends on. The loop runs in the spatial core: one call per body
|
|
596
|
+
* per frame. Gravity is the caller's: fold the fall into the motion
|
|
597
|
+
* each frame and zero it while `floor` is set; a walkable floor absorbs
|
|
598
|
+
* the vertical part, so a body never creeps down a slope it can stand
|
|
599
|
+
* on. Colliders are the layer's shown sprites, or `opts.sprites`.
|
|
600
|
+
*/
|
|
601
|
+
moveAndSlide(volume: Volume, dx: number, dy: number, opts?: MoveOptions): MoveResult
|
|
602
|
+
_groups: Set<GroupState>
|
|
603
|
+
/** The core node every sprite and group of the layer sits under: what
|
|
604
|
+
* scopes the layer's queries in the arena shared with 3d scenes. */
|
|
605
|
+
_root: NodeId
|
|
377
606
|
}
|
|
378
607
|
|
|
379
608
|
/** The stored UVs at `at` un-mirrored by the sprite's flags: the frame as
|
|
@@ -401,6 +630,48 @@ function fillTransform(x: number, y: number, rot: number, sx: number, sy: number
|
|
|
401
630
|
TRANSFORM[9] = 1
|
|
402
631
|
}
|
|
403
632
|
|
|
633
|
+
// The core mover's options, refilled per call (the binding reads it
|
|
634
|
+
// synchronously); `up` is the 2d direction lifted into the plane.
|
|
635
|
+
const MOVE_OPTIONS: CoreMoveOptions & { up: [number, number, number] } = { up: [0, -1, 0] }
|
|
636
|
+
|
|
637
|
+
/** Pack a 2d volume into the core's scratch at z = 0: a circle is a
|
|
638
|
+
* capsule with a == b, a rect a "box" turned about z. Returns the kind. */
|
|
639
|
+
function packVolume(volume: Volume, site: string): "capsule" | "box" {
|
|
640
|
+
if ("width" in volume) {
|
|
641
|
+
if (!(volume.width >= 0 && volume.height >= 0)) {
|
|
642
|
+
throw new Error(site + ": width and height must be >= 0, got " + volume.width + " x " + volume.height)
|
|
643
|
+
}
|
|
644
|
+
let half = (volume.rotation ?? 0) / 2
|
|
645
|
+
BOX[0] = volume.x + volume.width / 2
|
|
646
|
+
BOX[1] = volume.y + volume.height / 2
|
|
647
|
+
BOX[2] = 0
|
|
648
|
+
BOX[3] = volume.width / 2
|
|
649
|
+
BOX[4] = volume.height / 2
|
|
650
|
+
BOX[5] = BOX_HALF_DEPTH
|
|
651
|
+
BOX[6] = 0
|
|
652
|
+
BOX[7] = 0
|
|
653
|
+
BOX[8] = Math.sin(half)
|
|
654
|
+
BOX[9] = Math.cos(half)
|
|
655
|
+
return "box"
|
|
656
|
+
}
|
|
657
|
+
if (!(volume.radius >= 0)) throw new Error(site + ": radius must be >= 0, got " + volume.radius)
|
|
658
|
+
if ("x" in volume) {
|
|
659
|
+
CAPSULE[0] = volume.x
|
|
660
|
+
CAPSULE[1] = volume.y
|
|
661
|
+
CAPSULE[3] = volume.x
|
|
662
|
+
CAPSULE[4] = volume.y
|
|
663
|
+
} else {
|
|
664
|
+
CAPSULE[0] = volume.ax
|
|
665
|
+
CAPSULE[1] = volume.ay
|
|
666
|
+
CAPSULE[3] = volume.bx
|
|
667
|
+
CAPSULE[4] = volume.by
|
|
668
|
+
}
|
|
669
|
+
CAPSULE[2] = 0
|
|
670
|
+
CAPSULE[5] = 0
|
|
671
|
+
CAPSULE[6] = volume.radius
|
|
672
|
+
return "capsule"
|
|
673
|
+
}
|
|
674
|
+
|
|
404
675
|
/** Compose and push a node-backed sprite's local transform - through the
|
|
405
676
|
* node's transition declaration, so with one set (setSpriteTransition)
|
|
406
677
|
* the write is a target the core animates toward. */
|
|
@@ -410,57 +681,25 @@ function writeTransform(sprite: Sprite): void {
|
|
|
410
681
|
}
|
|
411
682
|
|
|
412
683
|
/**
|
|
413
|
-
* Create a sprite layer
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
684
|
+
* Create a sprite layer over one atlas texture. It renders nothing by
|
|
685
|
+
* itself: `layer.createView({ width, height })` (or a `<SpriteLayer>` /
|
|
686
|
+
* `<View2d>`) is where it shows, once or many times. Disposed
|
|
687
|
+
* automatically with the owning reactive scope (opt out with
|
|
688
|
+
* `{ autoFree: false }`); the atlas is NOT owned - dispose it yourself
|
|
689
|
+
* (it commonly outlives layers).
|
|
417
690
|
*/
|
|
418
|
-
export function createSpriteLayer(
|
|
419
|
-
width: number,
|
|
420
|
-
height: number,
|
|
421
|
-
atlas: TextureId,
|
|
422
|
-
opts?: SpriteLayerOptions,
|
|
423
|
-
): SpriteLayer {
|
|
691
|
+
export function createSpriteLayer(atlas: TextureId, opts?: SpriteLayerOptions): SpriteLayer {
|
|
424
692
|
let capacity = opts?.capacity ?? 1024
|
|
425
693
|
if (!(capacity > 0 && Number.isInteger(capacity))) {
|
|
426
694
|
throw new Error(`createSpriteLayer: capacity must be a positive integer, got ${capacity}`)
|
|
427
695
|
}
|
|
428
696
|
let label = opts?.label ?? "sprites"
|
|
429
|
-
let
|
|
430
|
-
|
|
431
|
-
// One unit quad (triangle strip), reused by every instance.
|
|
432
|
-
let quad = createBuffer(new Float32Array([-0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5]), {
|
|
433
|
-
label: `${label}-quad`,
|
|
434
|
-
autoFree: false,
|
|
435
|
-
})
|
|
697
|
+
let tint = opts?.tint ?? [1, 1, 1, 1]
|
|
698
|
+
checkTint("createSpriteLayer", tint)
|
|
436
699
|
let pose: BufferId = createBuffer(capacity * POSE_FLOATS * 4, { label: `${label}-pose`, autoFree: false })
|
|
437
700
|
let style: BufferId = createBuffer(capacity * STYLE_FLOATS * 4, { label: `${label}-style`, autoFree: false })
|
|
438
|
-
let
|
|
439
|
-
VERTEX_SPLIT,
|
|
440
|
-
FRAGMENT,
|
|
441
|
-
width * oversample,
|
|
442
|
-
height * oversample,
|
|
443
|
-
{ uViewport: [width, height], uCamera: [0, 0, 1, 1] },
|
|
444
|
-
{
|
|
445
|
-
label,
|
|
446
|
-
topology: "triangle-strip",
|
|
447
|
-
vertexCount: 4,
|
|
448
|
-
attributes: [{ name: "aPos", format: "vec2" }],
|
|
449
|
-
buffer: quad,
|
|
450
|
-
instanceAttributes: INSTANCE_ATTRIBUTES_SPLIT,
|
|
451
|
-
instanceBuffers: [pose, style],
|
|
452
|
-
instanceCount: 0,
|
|
453
|
-
blend: "alpha",
|
|
454
|
-
textures: { uAtlas: atlas },
|
|
455
|
-
clearColor: opts?.clearColor ?? [0, 0, 0, 0],
|
|
456
|
-
autoFree: false,
|
|
457
|
-
},
|
|
458
|
-
)
|
|
701
|
+
let gpu = createSpritePipeline(label, VERTEX_SPLIT, INSTANCE_ATTRIBUTES_SPLIT)
|
|
459
702
|
|
|
460
|
-
// Camera state, mirrored for picking (the inverse mapping).
|
|
461
|
-
let camX = 0
|
|
462
|
-
let camY = 0
|
|
463
|
-
let camZoom = 1
|
|
464
703
|
let disposed = false
|
|
465
704
|
let scheduled = false
|
|
466
705
|
let styleDirty = false
|
|
@@ -473,7 +712,31 @@ export function createSpriteLayer(
|
|
|
473
712
|
let freeSlots: number[] = []
|
|
474
713
|
let gpuCapacity = capacity
|
|
475
714
|
let styleData = new Float32Array(capacity * STYLE_FLOATS)
|
|
476
|
-
let byNode = new Map<NodeId,
|
|
715
|
+
let byNode = new Map<NodeId, SpriteState>()
|
|
716
|
+
// The layer's root node (identity): parentless sprites and groups hang
|
|
717
|
+
// off it, so a query scoped to it sees exactly this layer.
|
|
718
|
+
fillTransform(0, 0, 0, 1, 1)
|
|
719
|
+
let root = spatial.createNode(TRANSFORM, true)
|
|
720
|
+
let filter: QueryFilter = { root }
|
|
721
|
+
// Refill the layer's one filter for a query: the root always, the
|
|
722
|
+
// include-list as the sprites' nodes when given.
|
|
723
|
+
let queryFilter = (opts: QueryOptions | undefined, site: string): void => {
|
|
724
|
+
if (opts?.sprites === undefined) {
|
|
725
|
+
filter.nodes = undefined
|
|
726
|
+
return
|
|
727
|
+
}
|
|
728
|
+
let nodes: NodeId[] = []
|
|
729
|
+
for (let sprite of opts.sprites) {
|
|
730
|
+
if (sprite.layer !== layer) throw new Error(site + ": a sprite in `sprites` belongs to another layer")
|
|
731
|
+
nodes.push(sprite.node!)
|
|
732
|
+
}
|
|
733
|
+
filter.nodes = nodes
|
|
734
|
+
}
|
|
735
|
+
let impactOf = (h: CoreImpact): Impact | null => {
|
|
736
|
+
let sprite = byNode.get(h.node)
|
|
737
|
+
if (!sprite) return null
|
|
738
|
+
return { sprite, time: h.time, point: [h.point[0], h.point[1]], normal: [h.normal[0], h.normal[1]] }
|
|
739
|
+
}
|
|
477
740
|
|
|
478
741
|
let flush = () => {
|
|
479
742
|
scheduled = false
|
|
@@ -485,7 +748,7 @@ export function createSpriteLayer(
|
|
|
485
748
|
endBufferWrite(style, highWater * STYLE_FLOATS * 4)
|
|
486
749
|
}
|
|
487
750
|
if (published !== highWater) {
|
|
488
|
-
|
|
751
|
+
views.setCount(highWater)
|
|
489
752
|
published = highWater
|
|
490
753
|
}
|
|
491
754
|
// The core recomputes moved subtrees and publishes every dirty pose
|
|
@@ -496,7 +759,7 @@ export function createSpriteLayer(
|
|
|
496
759
|
// Grow both instance buffers to `next` slots: the pose sinks move in one
|
|
497
760
|
// retargetRecords call (the whole used range republishes at the next
|
|
498
761
|
// flush), the style mirror grows in JS and republishes through the lease.
|
|
499
|
-
// The
|
|
762
|
+
// The entries hold the old buffers alive until the swaps land, so the
|
|
500
763
|
// destroys are safe to issue right after.
|
|
501
764
|
let grow = (next: number) => {
|
|
502
765
|
let newPose = createBuffer(next * POSE_FLOATS * 4, { label: `${label}-pose`, autoFree: false })
|
|
@@ -505,7 +768,7 @@ export function createSpriteLayer(
|
|
|
505
768
|
let grownStyle = new Float32Array(next * STYLE_FLOATS)
|
|
506
769
|
grownStyle.set(styleData)
|
|
507
770
|
styleData = grownStyle
|
|
508
|
-
|
|
771
|
+
views.setBuffers({ instanceBuffers: [newPose, newStyle] })
|
|
509
772
|
destroyBuffer(pose)
|
|
510
773
|
destroyBuffer(style)
|
|
511
774
|
pose = newPose
|
|
@@ -514,7 +777,7 @@ export function createSpriteLayer(
|
|
|
514
777
|
styleDirty = true
|
|
515
778
|
}
|
|
516
779
|
|
|
517
|
-
let writeStyle = (sprite:
|
|
780
|
+
let writeStyle = (sprite: SpriteState, opts: SpriteOptions) => {
|
|
518
781
|
let at = sprite._slot * STYLE_FLOATS
|
|
519
782
|
let flipX = opts.flipX !== undefined && opts.flipX !== sprite._flipX
|
|
520
783
|
let flipY = opts.flipY !== undefined && opts.flipY !== sprite._flipY
|
|
@@ -536,46 +799,47 @@ export function createSpriteLayer(
|
|
|
536
799
|
styleData[at + 7] = opts.tint[3]
|
|
537
800
|
styleDirty = true
|
|
538
801
|
}
|
|
802
|
+
if (opts.renderOrder !== undefined) {
|
|
803
|
+
styleData[at + STYLE_KEY_FIELD] = opts.renderOrder
|
|
804
|
+
styleDirty = true
|
|
805
|
+
}
|
|
539
806
|
}
|
|
540
807
|
|
|
541
|
-
let
|
|
542
|
-
|
|
543
|
-
|
|
808
|
+
let views = createViews({
|
|
809
|
+
label,
|
|
810
|
+
pipeline: gpu.pipeline,
|
|
811
|
+
quad: gpu.quad,
|
|
812
|
+
atlas,
|
|
813
|
+
buffers: () => ({ instanceBuffers: [pose, style] }),
|
|
814
|
+
count: () => published,
|
|
815
|
+
tint: () => tint,
|
|
544
816
|
pick: (x, y) => layer.pick(x, y),
|
|
817
|
+
// The order key one view entry declares: "y" on world y in the pose
|
|
818
|
+
// record (slot 0), "renderOrder" on the app-owned key in the style
|
|
819
|
+
// record (slot 1); either way the core gathers BOTH buffers under the
|
|
820
|
+
// one permutation at every publish and republishes the sibling itself
|
|
821
|
+
// when the key buffer re-orders.
|
|
822
|
+
order:
|
|
823
|
+
opts?.orderBy === "y"
|
|
824
|
+
? { field: POSE_Y_FIELD }
|
|
825
|
+
: opts?.orderBy === "renderOrder"
|
|
826
|
+
? { field: STYLE_KEY_FIELD, slot: 1 }
|
|
827
|
+
: undefined,
|
|
545
828
|
})
|
|
546
829
|
|
|
547
830
|
let layer: SpriteLayer = {
|
|
548
|
-
texture,
|
|
549
|
-
handlers: undefined as unknown as SpriteHandlers,
|
|
550
831
|
get count() {
|
|
551
832
|
return byNode.size
|
|
552
833
|
},
|
|
553
|
-
|
|
554
|
-
if (disposed || (w === width && h === height)) return
|
|
555
|
-
checkOversample("setSize", oversample, w, h)
|
|
556
|
-
width = w
|
|
557
|
-
height = h
|
|
558
|
-
setTargetSize(texture, w * oversample, h * oversample)
|
|
559
|
-
setTargetParams(texture, { uViewport: [w, h] })
|
|
560
|
-
},
|
|
561
|
-
get oversample() {
|
|
562
|
-
return oversample
|
|
563
|
-
},
|
|
564
|
-
setOversample(n) {
|
|
565
|
-
if (disposed || n === oversample) return
|
|
566
|
-
checkOversample("setOversample", n, width, height)
|
|
567
|
-
oversample = n
|
|
568
|
-
setTargetSize(texture, width * n, height * n)
|
|
569
|
-
},
|
|
570
|
-
setCamera(update) {
|
|
834
|
+
setTint(next) {
|
|
571
835
|
if (disposed) return
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
836
|
+
checkTint("setTint", next)
|
|
837
|
+
tint = next
|
|
838
|
+
views.setTint(next)
|
|
839
|
+
},
|
|
840
|
+
createView(vopts) {
|
|
841
|
+
if (disposed) throw new Error("createView: layer is disposed")
|
|
842
|
+
return views.create(vopts)
|
|
579
843
|
},
|
|
580
844
|
pick(x, y) {
|
|
581
845
|
// The index reads as of the last core flush; run any pending batch
|
|
@@ -583,33 +847,92 @@ export function createSpriteLayer(
|
|
|
583
847
|
if (scheduled) flush()
|
|
584
848
|
RAY_ORIGIN[0] = x
|
|
585
849
|
RAY_ORIGIN[1] = y
|
|
586
|
-
RAY_ORIGIN[2] =
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
850
|
+
RAY_ORIGIN[2] = PICK_RAY_START
|
|
851
|
+
RAY_DIR[0] = 0
|
|
852
|
+
RAY_DIR[1] = 0
|
|
853
|
+
RAY_DIR[2] = 1
|
|
854
|
+
filter.nodes = undefined
|
|
855
|
+
let out: Sprite[] = []
|
|
856
|
+
for (let hit of spatial.raycast(RAY_ORIGIN, RAY_DIR, filter)) {
|
|
591
857
|
let sprite = byNode.get(hit.node)
|
|
592
|
-
if (sprite
|
|
858
|
+
if (sprite) out.push(sprite)
|
|
593
859
|
}
|
|
594
|
-
|
|
860
|
+
// Topmost first: higher slot = drawn later = on top.
|
|
861
|
+
return out.sort((a, b) => b._slot - a._slot)
|
|
595
862
|
},
|
|
596
|
-
pickRect(x, y,
|
|
597
|
-
if (scheduled) flush()
|
|
598
|
-
BOX[0] = x
|
|
599
|
-
BOX[1] = y
|
|
600
|
-
BOX[2] = -1
|
|
601
|
-
BOX[3] = x + w
|
|
602
|
-
BOX[4] = y + h
|
|
603
|
-
BOX[5] = 1
|
|
863
|
+
pickRect(x, y, width, height) {
|
|
604
864
|
let out: Sprite[] = []
|
|
605
|
-
for (let
|
|
606
|
-
|
|
607
|
-
|
|
865
|
+
for (let hit of layer.overlap({ x, y, width, height })) out.push(hit.sprite)
|
|
866
|
+
return out
|
|
867
|
+
},
|
|
868
|
+
raycast(x, y, dx, dy, opts) {
|
|
869
|
+
if (scheduled) flush()
|
|
870
|
+
RAY_ORIGIN[0] = x
|
|
871
|
+
RAY_ORIGIN[1] = y
|
|
872
|
+
RAY_ORIGIN[2] = 0
|
|
873
|
+
RAY_DIR[0] = dx
|
|
874
|
+
RAY_DIR[1] = dy
|
|
875
|
+
RAY_DIR[2] = 0
|
|
876
|
+
queryFilter(opts, "raycast")
|
|
877
|
+
let out: RayHit[] = []
|
|
878
|
+
for (let h of spatial.raycast(RAY_ORIGIN, RAY_DIR, filter)) {
|
|
879
|
+
let sprite = byNode.get(h.node)
|
|
880
|
+
if (sprite) out.push({ sprite, distance: h.distance, point: [h.point[0], h.point[1]], normal: [h.normal[0], h.normal[1]] })
|
|
608
881
|
}
|
|
609
882
|
return out
|
|
610
883
|
},
|
|
611
|
-
|
|
612
|
-
|
|
884
|
+
overlap(volume, opts) {
|
|
885
|
+
if (scheduled) flush()
|
|
886
|
+
let kind = packVolume(volume, "overlap")
|
|
887
|
+
queryFilter(opts, "overlap")
|
|
888
|
+
let out: Overlap[] = []
|
|
889
|
+
for (let h of spatial.overlap(kind, kind === "box" ? BOX : CAPSULE, filter)) {
|
|
890
|
+
let sprite = byNode.get(h.node)
|
|
891
|
+
if (sprite) out.push({ sprite, point: [h.point[0], h.point[1]], normal: [h.normal[0], h.normal[1]], depth: h.depth })
|
|
892
|
+
}
|
|
893
|
+
return out
|
|
894
|
+
},
|
|
895
|
+
sweep(volume, dx, dy, opts) {
|
|
896
|
+
if (scheduled) flush()
|
|
897
|
+
let kind = packVolume(volume, "sweep")
|
|
898
|
+
MOTION[0] = dx
|
|
899
|
+
MOTION[1] = dy
|
|
900
|
+
MOTION[2] = 0
|
|
901
|
+
queryFilter(opts, "sweep")
|
|
902
|
+
let out: Impact[] = []
|
|
903
|
+
for (let h of spatial.sweep(kind, kind === "box" ? BOX : CAPSULE, MOTION, filter)) {
|
|
904
|
+
let impact = impactOf(h)
|
|
905
|
+
if (impact) out.push(impact)
|
|
906
|
+
}
|
|
907
|
+
return out
|
|
908
|
+
},
|
|
909
|
+
moveAndSlide(volume, dx, dy, opts) {
|
|
910
|
+
if (scheduled) flush()
|
|
911
|
+
let kind = packVolume(volume, "moveAndSlide")
|
|
912
|
+
MOTION[0] = dx
|
|
913
|
+
MOTION[1] = dy
|
|
914
|
+
MOTION[2] = 0
|
|
915
|
+
queryFilter(opts, "moveAndSlide")
|
|
916
|
+
let up = opts?.up ?? UP_2D
|
|
917
|
+
MOVE_OPTIONS.up[0] = up[0]
|
|
918
|
+
MOVE_OPTIONS.up[1] = up[1]
|
|
919
|
+
MOVE_OPTIONS.floorMaxAngle = opts?.floorMaxAngle
|
|
920
|
+
MOVE_OPTIONS.maxSlides = opts?.maxSlides
|
|
921
|
+
MOVE_OPTIONS.skin = opts?.skin
|
|
922
|
+
MOVE_OPTIONS.floorSnap = opts?.floorSnap
|
|
923
|
+
let r = spatial.moveAndSlide(kind, kind === "box" ? BOX : CAPSULE, MOTION, MOVE_OPTIONS, filter)
|
|
924
|
+
let hits: Impact[] = []
|
|
925
|
+
for (let h of r.hits) {
|
|
926
|
+
let impact = impactOf(h)
|
|
927
|
+
if (impact) hits.push(impact)
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
motion: [r.motion[0], r.motion[1]],
|
|
931
|
+
floor: r.floor === null ? null : [r.floor[0], r.floor[1]],
|
|
932
|
+
wall: r.wall,
|
|
933
|
+
ceiling: r.ceiling,
|
|
934
|
+
hits,
|
|
935
|
+
}
|
|
613
936
|
},
|
|
614
937
|
dispose() {
|
|
615
938
|
if (disposed) return
|
|
@@ -626,19 +949,20 @@ export function createSpriteLayer(
|
|
|
626
949
|
spatial.destroyNode(group.node)
|
|
627
950
|
}
|
|
628
951
|
layer._groups.clear()
|
|
952
|
+
spatial.destroyNode(root)
|
|
629
953
|
// Let the core emit its final slot-zeroing writes while the pose
|
|
630
954
|
// buffer still exists, then free everything.
|
|
631
955
|
spatial.flush()
|
|
632
|
-
|
|
956
|
+
views.dispose()
|
|
633
957
|
destroyBuffer(pose)
|
|
634
958
|
destroyBuffer(style)
|
|
635
|
-
|
|
959
|
+
gpu.dispose()
|
|
636
960
|
},
|
|
637
961
|
_add(opts) {
|
|
638
962
|
if (disposed) throw new Error("addSprite: layer is disposed")
|
|
639
963
|
let slot = freeSlots.pop() ?? highWater++
|
|
640
964
|
if (slot >= gpuCapacity) grow(gpuCapacity * 2)
|
|
641
|
-
let sprite:
|
|
965
|
+
let sprite: SpriteState = {
|
|
642
966
|
layer,
|
|
643
967
|
node: null,
|
|
644
968
|
_slot: slot,
|
|
@@ -649,18 +973,25 @@ export function createSpriteLayer(
|
|
|
649
973
|
_rot: opts?.rotation ?? 0,
|
|
650
974
|
_flipX: false,
|
|
651
975
|
_flipY: false,
|
|
976
|
+
_visible: opts?.visible ?? true,
|
|
977
|
+
_parent: opts?.parent ?? null,
|
|
652
978
|
}
|
|
653
979
|
fillTransform(sprite._x, sprite._y, sprite._rot, sprite._w, sprite._h)
|
|
654
|
-
let node = spatial.createNode(TRANSFORM,
|
|
980
|
+
let node = spatial.createNode(TRANSFORM, sprite._visible)
|
|
655
981
|
sprite.node = node
|
|
656
982
|
if (opts?.parent) {
|
|
657
983
|
if (opts.parent.layer !== layer) throw new Error("addSprite: parent group belongs to another layer")
|
|
658
984
|
spatial.setParent(node, opts.parent.node)
|
|
985
|
+
opts.parent._children.add(sprite)
|
|
986
|
+
} else {
|
|
987
|
+
spatial.setParent(node, root)
|
|
659
988
|
}
|
|
660
|
-
spatial.setBounds(node,
|
|
989
|
+
spatial.setBounds(node, COLUMN_BOUNDS)
|
|
661
990
|
spatial.bindPoseRecord(node, pose, slot)
|
|
662
991
|
byNode.set(node, sprite)
|
|
663
|
-
|
|
992
|
+
// renderOrder defaults to 0 explicitly: a recycled slot holds the
|
|
993
|
+
// previous occupant's key otherwise.
|
|
994
|
+
writeStyle(sprite, { frame: FULL_FRAME, tint: [1, 1, 1, 1], renderOrder: 0, ...opts })
|
|
664
995
|
layer._schedule()
|
|
665
996
|
return sprite
|
|
666
997
|
},
|
|
@@ -672,7 +1003,18 @@ export function createSpriteLayer(
|
|
|
672
1003
|
if (opts.h !== undefined && opts.h !== sprite._h) (sprite._h = opts.h), (moved = true)
|
|
673
1004
|
if (opts.rotation !== undefined && opts.rotation !== sprite._rot) (sprite._rot = opts.rotation), (moved = true)
|
|
674
1005
|
if (moved) writeTransform(sprite)
|
|
675
|
-
if (opts.
|
|
1006
|
+
if (opts.visible !== undefined && opts.visible !== sprite._visible) {
|
|
1007
|
+
sprite._visible = opts.visible
|
|
1008
|
+
spatial.setVisible(sprite.node!, opts.visible)
|
|
1009
|
+
moved = true
|
|
1010
|
+
}
|
|
1011
|
+
if (
|
|
1012
|
+
opts.frame !== undefined ||
|
|
1013
|
+
opts.tint !== undefined ||
|
|
1014
|
+
opts.flipX !== undefined ||
|
|
1015
|
+
opts.flipY !== undefined ||
|
|
1016
|
+
opts.renderOrder !== undefined
|
|
1017
|
+
) {
|
|
676
1018
|
writeStyle(sprite, opts)
|
|
677
1019
|
}
|
|
678
1020
|
if (moved || styleDirty) layer._schedule()
|
|
@@ -689,10 +1031,16 @@ export function createSpriteLayer(
|
|
|
689
1031
|
flipY: sprite._flipY,
|
|
690
1032
|
rotation: sprite._rot,
|
|
691
1033
|
tint: [styleData[at + 4]!, styleData[at + 5]!, styleData[at + 6]!, styleData[at + 7]!],
|
|
1034
|
+
renderOrder: styleData[at + STYLE_KEY_FIELD]!,
|
|
1035
|
+
visible: sprite._visible,
|
|
692
1036
|
}
|
|
693
1037
|
},
|
|
694
1038
|
_remove(sprite) {
|
|
695
1039
|
sprite.layer = null
|
|
1040
|
+
if (sprite._parent) {
|
|
1041
|
+
sprite._parent._children.delete(sprite)
|
|
1042
|
+
sprite._parent = null
|
|
1043
|
+
}
|
|
696
1044
|
// Destroying the node zeroes its pose slot at the next core flush
|
|
697
1045
|
// (zero scale = nothing drawn); the slot then recycles.
|
|
698
1046
|
byNode.delete(sprite.node!)
|
|
@@ -707,9 +1055,8 @@ export function createSpriteLayer(
|
|
|
707
1055
|
RESOLVED.then(flush)
|
|
708
1056
|
},
|
|
709
1057
|
_groups: new Set(),
|
|
1058
|
+
_root: root,
|
|
710
1059
|
}
|
|
711
|
-
layer.handlers = dispatch(null)
|
|
712
|
-
|
|
713
1060
|
if (opts?.autoFree !== false && getOwner()) onCleanup(() => layer.dispose())
|
|
714
1061
|
return layer
|
|
715
1062
|
}
|
|
@@ -719,7 +1066,8 @@ export function createSpriteLayer(
|
|
|
719
1066
|
* order, and a removed sprite's slot recycles to the next add - so unlike
|
|
720
1067
|
* the record layer there is no painter's-insertion-order guarantee across
|
|
721
1068
|
* removals. Opaque-or-transparent pixel art (the overwhelming case) never
|
|
722
|
-
* notices;
|
|
1069
|
+
* notices; a scene that needs depth order sorts by world y with the
|
|
1070
|
+
* layer's `orderBy: "y"` (core-produced, zero JS per frame). Past the
|
|
723
1071
|
* layer's reservation both instance buffers double (pose sinks move in one
|
|
724
1072
|
* core retarget); reserve with `capacity` to avoid the copies.
|
|
725
1073
|
*/
|
|
@@ -745,17 +1093,46 @@ export function removeSprite(sprite: Sprite): void {
|
|
|
745
1093
|
sprite.layer?._remove(sprite)
|
|
746
1094
|
}
|
|
747
1095
|
|
|
1096
|
+
/**
|
|
1097
|
+
* A sprite's or group's position in LAYER pixels: its local x/y composed
|
|
1098
|
+
* through every enclosing group (@solidrt/3d's worldPosition, Three's
|
|
1099
|
+
* getWorldPosition), read from the core's world matrix as the tree stands
|
|
1100
|
+
* now, pending writes included. Under a transition this is the
|
|
1101
|
+
* mid-flight pose - what picking and the screen show - where the handle's
|
|
1102
|
+
* own fields hold the target. A record layer's sprite has no groups, so
|
|
1103
|
+
* its world position is its own, read as getSprite reads it (the records
|
|
1104
|
+
* array). A fresh pair per call; null once the handle is inert.
|
|
1105
|
+
*/
|
|
1106
|
+
export function worldPosition(target: Sprite | SpriteGroup): [number, number] | null {
|
|
1107
|
+
if (target.layer === null) return null
|
|
1108
|
+
// The set only groups carry tells the two handle kinds apart (as in
|
|
1109
|
+
// removeGroup); a node-less sprite is a record layer's.
|
|
1110
|
+
if (!("_children" in target) && target.node === null) {
|
|
1111
|
+
let s = target.layer._read(target)
|
|
1112
|
+
return [s.x, s.y]
|
|
1113
|
+
}
|
|
1114
|
+
spatial.worldMatrix(target.node!, WORLD)
|
|
1115
|
+
return [WORLD[12]!, WORLD[13]!]
|
|
1116
|
+
}
|
|
1117
|
+
|
|
748
1118
|
/**
|
|
749
1119
|
* Re-parent a sprite under a group (null = back to the layer root); its
|
|
750
1120
|
* 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
|
|
1121
|
+
* them (it holds its local pose, not its world pose; worldPosition reads
|
|
1122
|
+
* the composed one). Node layer only.
|
|
752
1123
|
*/
|
|
753
1124
|
export function setSpriteParent(sprite: Sprite, parent: SpriteGroup | null): void {
|
|
754
1125
|
let layer = sprite.layer
|
|
755
1126
|
if (!layer) return
|
|
756
1127
|
if (sprite.node === null) throw new Error("setSpriteParent: record layers have no groups")
|
|
757
1128
|
if (parent && parent.layer !== layer) throw new Error("setSpriteParent: group belongs to another layer")
|
|
758
|
-
|
|
1129
|
+
// A sprite with a node is on a node layer (the throw above), which TS
|
|
1130
|
+
// cannot narrow through sprite.layer.
|
|
1131
|
+
spatial.setParent(sprite.node, parent ? parent.node : (layer as SpriteLayer)._root)
|
|
1132
|
+
let s: SpriteState = sprite
|
|
1133
|
+
if (s._parent) s._parent._children.delete(s)
|
|
1134
|
+
s._parent = parent
|
|
1135
|
+
if (parent) parent._children.add(s)
|
|
759
1136
|
layer._schedule()
|
|
760
1137
|
}
|
|
761
1138
|
|
|
@@ -796,12 +1173,18 @@ export function addGroup(layer: SpriteLayer, opts?: GroupOptions): SpriteGroup {
|
|
|
796
1173
|
_y: opts?.y ?? 0,
|
|
797
1174
|
_rot: opts?.rotation ?? 0,
|
|
798
1175
|
_scale: opts?.scale ?? 1,
|
|
799
|
-
|
|
1176
|
+
_visible: opts?.visible ?? true,
|
|
1177
|
+
_parent: opts?.parent ?? null,
|
|
1178
|
+
_children: new Set(),
|
|
1179
|
+
} as GroupState
|
|
800
1180
|
writeGroupTransform(group)
|
|
801
|
-
group.node = spatial.createNode(TRANSFORM,
|
|
1181
|
+
group.node = spatial.createNode(TRANSFORM, group._visible)
|
|
802
1182
|
if (opts?.parent) {
|
|
803
1183
|
if (opts.parent.layer !== layer) throw new Error("addGroup: parent group belongs to another layer")
|
|
804
1184
|
spatial.setParent(group.node, opts.parent.node)
|
|
1185
|
+
opts.parent._children.add(group)
|
|
1186
|
+
} else {
|
|
1187
|
+
spatial.setParent(group.node, layer._root)
|
|
805
1188
|
}
|
|
806
1189
|
layer._groups.add(group)
|
|
807
1190
|
layer._schedule()
|
|
@@ -816,33 +1199,57 @@ function writeGroupTransform(group: SpriteGroup): void {
|
|
|
816
1199
|
export function setGroup(group: SpriteGroup, opts: GroupOptions): void {
|
|
817
1200
|
let layer = group.layer
|
|
818
1201
|
if (!layer) return
|
|
1202
|
+
let g: GroupState = group
|
|
819
1203
|
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 !==
|
|
1204
|
+
if (opts.x !== undefined && opts.x !== g._x) (g._x = opts.x), (moved = true)
|
|
1205
|
+
if (opts.y !== undefined && opts.y !== g._y) (g._y = opts.y), (moved = true)
|
|
1206
|
+
if (opts.rotation !== undefined && opts.rotation !== g._rot) (g._rot = opts.rotation), (moved = true)
|
|
1207
|
+
if (opts.scale !== undefined && opts.scale !== g._scale) (g._scale = opts.scale), (moved = true)
|
|
824
1208
|
if (moved) {
|
|
825
1209
|
writeGroupTransform(group)
|
|
826
1210
|
spatial.writeTransform(group.node, TRANSFORM)
|
|
827
1211
|
}
|
|
1212
|
+
if (opts.visible !== undefined && opts.visible !== g._visible) {
|
|
1213
|
+
g._visible = opts.visible
|
|
1214
|
+
spatial.setVisible(group.node, opts.visible)
|
|
1215
|
+
moved = true
|
|
1216
|
+
}
|
|
828
1217
|
if (opts.parent !== undefined) {
|
|
829
1218
|
if (opts.parent && opts.parent.layer !== layer) throw new Error("setGroup: parent group belongs to another layer")
|
|
830
|
-
spatial.setParent(group.node, opts.parent ? opts.parent.node :
|
|
1219
|
+
spatial.setParent(group.node, opts.parent ? opts.parent.node : layer._root)
|
|
1220
|
+
if (g._parent) g._parent._children.delete(g)
|
|
1221
|
+
g._parent = opts.parent
|
|
1222
|
+
if (opts.parent) opts.parent._children.add(g)
|
|
831
1223
|
moved = true
|
|
832
1224
|
}
|
|
833
1225
|
if (moved) layer._schedule()
|
|
834
1226
|
}
|
|
835
1227
|
|
|
836
1228
|
/**
|
|
837
|
-
* Remove a group:
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
1229
|
+
* Remove a group AND everything under it: child sprites and groups are
|
|
1230
|
+
* removed with it - Unity's Destroy, Godot's free, the subtree form of
|
|
1231
|
+
* removeSprite. To keep a child, re-parent it out first (setSpriteParent /
|
|
1232
|
+
* setGroup's `parent`). Every removed handle goes inert. A component tree
|
|
1233
|
+
* unmounts children first and never sees the recursion. (@solidrt/3d's
|
|
1234
|
+
* `remove` DETACHES a re-addable subtree instead - its nodes exist outside
|
|
1235
|
+
* a scene; a sprite cannot exist outside its layer, so here remove means
|
|
1236
|
+
* destroy, exactly as it does for removeSprite.)
|
|
841
1237
|
*/
|
|
842
1238
|
export function removeGroup(group: SpriteGroup): void {
|
|
843
1239
|
let layer = group.layer
|
|
844
1240
|
if (!layer) return
|
|
845
|
-
|
|
1241
|
+
let g: GroupState = group
|
|
1242
|
+
// Children first, over a snapshot (each removal edits _children); the
|
|
1243
|
+
// set only groups carry tells the two handle kinds apart.
|
|
1244
|
+
for (let child of [...g._children]) {
|
|
1245
|
+
if ("_children" in child) removeGroup(child)
|
|
1246
|
+
else removeSprite(child)
|
|
1247
|
+
}
|
|
1248
|
+
g.layer = null
|
|
1249
|
+
if (g._parent) {
|
|
1250
|
+
g._parent._children.delete(g)
|
|
1251
|
+
g._parent = null
|
|
1252
|
+
}
|
|
846
1253
|
layer._groups.delete(group)
|
|
847
1254
|
declared.delete(group.node)
|
|
848
1255
|
spatial.destroyNode(group.node)
|