@solidrt/2d 0.0.56 → 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 +248 -85
- package/README.md +3 -3
- package/examples/README.md +38 -13
- package/examples/anim.tsx +6 -8
- package/examples/camera-probe.tsx +16 -14
- package/examples/camera.tsx +21 -15
- package/examples/parity.tsx +11 -9
- package/examples/pick.tsx +43 -7
- package/examples/split-screen.tsx +111 -0
- package/examples/springs.tsx +6 -9
- package/examples/sprites.tsx +9 -12
- package/examples/tiles.tsx +24 -9
- package/examples/views.tsx +177 -0
- package/package.json +2 -2
- package/src/atlas.ts +35 -33
- package/src/camera.ts +26 -1
- package/src/camera2d.ts +692 -119
- 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 +7 -7
- package/src/frames.ts +63 -14
- package/src/index.ts +27 -11
- package/src/input.ts +50 -0
- package/src/layer.ts +384 -245
- package/src/oversample-math.ts +1 -1
- package/src/records.ts +33 -138
- package/src/shaders.ts +65 -3
- package/src/tiles.ts +195 -42
- package/src/views.ts +294 -0
- package/src/camera-motion.ts +0 -532
- package/src/components.tsx +0 -611
package/src/layer.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
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
|
|
@@ -20,35 +21,24 @@
|
|
|
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
|
-
// in camera.ts) is a shared-params write (uCamera +
|
|
25
|
-
// per-sprite; pointer dispatch
|
|
26
|
-
// unprojectCamera.
|
|
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.
|
|
27
28
|
import { getOwner, onCleanup } from "@solidrt/core"
|
|
28
29
|
import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEvent } from "@solidrt/core"
|
|
29
|
-
import {
|
|
30
|
-
beginBufferWrite,
|
|
31
|
-
createBuffer,
|
|
32
|
-
createPipelineTexture,
|
|
33
|
-
destroyBuffer,
|
|
34
|
-
destroyTexture,
|
|
35
|
-
endBufferWrite,
|
|
36
|
-
setDraw,
|
|
37
|
-
setTargetParams,
|
|
38
|
-
setTargetSize,
|
|
39
|
-
} from "@solidrt/core/gpu"
|
|
30
|
+
import { beginBufferWrite, createBuffer, destroyBuffer, endBufferWrite } from "@solidrt/core/gpu"
|
|
40
31
|
import type { BufferId, TextureId } from "@solidrt/core/gpu"
|
|
41
32
|
import * as spatial from "flux:spatial"
|
|
42
|
-
import type { NodeId, NodeTransition } from "flux:spatial"
|
|
33
|
+
import type { Impact as CoreImpact, MoveOptions as CoreMoveOptions, NodeId, NodeTransition, QueryFilter } from "flux:spatial"
|
|
43
34
|
import { on } from "srt:events"
|
|
44
|
-
import { checkCamera, projectCamera, unprojectCamera } from "./camera.ts"
|
|
45
35
|
import type { CameraState, CameraUpdate } from "./camera.ts"
|
|
46
|
-
import { spriteDispatch } from "./dispatch.ts"
|
|
47
|
-
import { checkOversample, thrashSentinel } from "./oversample.ts"
|
|
48
36
|
import type { Frame } from "./frames.ts"
|
|
49
37
|
import { FULL_FRAME, writeFrame } from "./frames.ts"
|
|
50
38
|
import type { RecordLayer } from "./records.ts"
|
|
51
|
-
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"
|
|
52
42
|
|
|
53
43
|
/** Floats per pose record (the core's Pose2D projection). */
|
|
54
44
|
export const POSE_FLOATS = 5
|
|
@@ -67,10 +57,31 @@ const RESOLVED = Promise.resolve()
|
|
|
67
57
|
|
|
68
58
|
// Shared marshalling scratch (the bindings copy synchronously).
|
|
69
59
|
const TRANSFORM = new Float32Array(10)
|
|
70
|
-
|
|
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])
|
|
71
69
|
const RAY_ORIGIN = new Float32Array(3)
|
|
72
|
-
const RAY_DIR = new Float32Array(
|
|
73
|
-
|
|
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]
|
|
74
85
|
// worldPosition's world-matrix scratch (column-major; translation at 12, 13).
|
|
75
86
|
const WORLD = new Float32Array(16)
|
|
76
87
|
|
|
@@ -207,6 +218,12 @@ export type SpriteOptions = {
|
|
|
207
218
|
visible?: boolean
|
|
208
219
|
}
|
|
209
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
|
+
*/
|
|
210
227
|
export type AddSpriteOptions = SpriteOptions & {
|
|
211
228
|
/** Mount under this group (node layer only; null = the layer root, the
|
|
212
229
|
* default); pose fields are then local to it. Reparent later with
|
|
@@ -254,13 +271,13 @@ export type SpritePointerEvent = LayerEventBase & {
|
|
|
254
271
|
}
|
|
255
272
|
|
|
256
273
|
/**
|
|
257
|
-
* The event as the
|
|
274
|
+
* The event as the VIEW's listeners see it (ViewHandle.listen), the last
|
|
258
275
|
* stop of the walk: `sprite` is the hit sprite it bubbled from, or null
|
|
259
|
-
* over empty space, where the
|
|
276
|
+
* over empty space, where the view is the only target.
|
|
260
277
|
*/
|
|
261
278
|
export type LayerPointerEvent = LayerEventBase & {
|
|
262
279
|
sprite: Sprite | null
|
|
263
|
-
currentTarget:
|
|
280
|
+
currentTarget: ViewHandle
|
|
264
281
|
}
|
|
265
282
|
|
|
266
283
|
type WheelFields = {
|
|
@@ -287,7 +304,7 @@ export type LayerWheelEvent = LayerPointerEvent & WheelFields
|
|
|
287
304
|
export type SpriteTapEvent = SpritePointerEvent & TapFields
|
|
288
305
|
export type LayerTapEvent = LayerPointerEvent & TapFields
|
|
289
306
|
|
|
290
|
-
/** A listener at
|
|
307
|
+
/** A listener at a view's root; see ViewHandle.listen. */
|
|
291
308
|
export type LayerPointerListener = {
|
|
292
309
|
onPointerDown?: (event: LayerPointerEvent) => void
|
|
293
310
|
onPointerMove?: (event: LayerPointerEvent) => void
|
|
@@ -319,20 +336,13 @@ export type SpriteLayerOptions = {
|
|
|
319
336
|
* limit.
|
|
320
337
|
*/
|
|
321
338
|
capacity?: number
|
|
322
|
-
clearColor?: [number, number, number, number]
|
|
323
339
|
/** Layer tint, [r, g, b, a] in 0..1, multiplied over every sprite's own
|
|
324
|
-
* tint; default opaque white (sprites as-is). See
|
|
340
|
+
* tint in every view; default opaque white (sprites as-is). See
|
|
341
|
+
* setTint. */
|
|
325
342
|
tint?: [number, number, number, number]
|
|
343
|
+
/** Names the GPU resources (buffers, pipeline; views default to
|
|
344
|
+
* `<label>-view`); default "sprites". */
|
|
326
345
|
label?: string
|
|
327
|
-
/**
|
|
328
|
-
* Target texels per layer pixel (positive integer, default 1). The layer
|
|
329
|
-
* renders at `oversample` times its size and is composited down, so a
|
|
330
|
-
* fractional or HiDPI display scale resamples properly instead of snapping
|
|
331
|
-
* (nearest) or smearing (linear); see setOversample. The components pick
|
|
332
|
-
* it from the leaf's on-screen size - set it here when composing the
|
|
333
|
-
* output yourself.
|
|
334
|
-
*/
|
|
335
|
-
oversample?: number
|
|
336
346
|
/** Skip the owner-scoped auto-dispose (see createSpriteLayer). */
|
|
337
347
|
autoFree?: boolean
|
|
338
348
|
/**
|
|
@@ -423,57 +433,23 @@ export type GroupOptions = {
|
|
|
423
433
|
}
|
|
424
434
|
|
|
425
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
|
+
*/
|
|
426
443
|
export type LayerBase = {
|
|
427
|
-
/** The layer's output: an ordinary texture id (`<texture src>`). */
|
|
428
|
-
texture: TextureId
|
|
429
|
-
/** Element handlers wiring the layer's pointer events (sprites, groups
|
|
430
|
-
* and the root listeners); see handlersFor. */
|
|
431
|
-
handlers: SpriteHandlers
|
|
432
444
|
/** Live sprite count. */
|
|
433
445
|
readonly count: number
|
|
434
|
-
/** Layer pixels, as created or last set by setSize. */
|
|
435
|
-
readonly width: number
|
|
436
|
-
readonly height: number
|
|
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
|
|
450
|
-
/** Target texels per layer pixel; see setOversample. */
|
|
451
|
-
readonly oversample: number
|
|
452
|
-
/**
|
|
453
|
-
* Re-render at `n` target texels per layer pixel (positive integer): the
|
|
454
|
-
* target resizes in place at its stable id, layer pixels, records, camera
|
|
455
|
-
* and picking are untouched. Pick `n` as the ceiling of the device pixels
|
|
456
|
-
* one layer pixel covers on screen (display scale times any designSize
|
|
457
|
-
* fit or layout scaling), which the components do in onLayout.
|
|
458
|
-
*/
|
|
459
|
-
setOversample(n: number): void
|
|
460
|
-
setCamera(update: CameraUpdate): void
|
|
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
446
|
/**
|
|
472
447
|
* Tint the whole layer, [r, g, b, a] in 0..1: a uniform multiplied over
|
|
473
448
|
* every sprite's own tint (day/night, a dimmed parallax plane, a
|
|
474
|
-
* fade-in). One shared-params write - no record
|
|
475
|
-
* animate - and the same contract as
|
|
476
|
-
* drives a whole scene across layer
|
|
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.
|
|
477
453
|
*/
|
|
478
454
|
setTint(tint: [number, number, number, number]): void
|
|
479
455
|
/** Every shown sprite whose rotated rect contains the layer-pixel point
|
|
@@ -481,15 +457,21 @@ export type LayerBase = {
|
|
|
481
457
|
* all-hits shape of @solidrt/3d's pick; `pick(x, y)[0]` is the topmost.
|
|
482
458
|
* Topmost means draw order (highest slot on the node layer, last added
|
|
483
459
|
* on a record layer); an `orderBy` key is not consulted (see
|
|
484
|
-
* SpriteLayerOptions.orderBy's known limitation).
|
|
460
|
+
* SpriteLayerOptions.orderBy's known limitation). World space: a view
|
|
461
|
+
* undoes its camera before asking. */
|
|
485
462
|
pick(x: number, y: number): Sprite[]
|
|
486
463
|
/**
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
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.
|
|
491
473
|
*/
|
|
492
|
-
|
|
474
|
+
createView(opts: ViewOptions): ViewHandle
|
|
493
475
|
dispose(): void
|
|
494
476
|
_add(opts?: AddSpriteOptions): Sprite
|
|
495
477
|
_write(sprite: SpriteState, opts: SpriteOptions): void
|
|
@@ -498,14 +480,129 @@ export type LayerBase = {
|
|
|
498
480
|
_schedule(): void
|
|
499
481
|
}
|
|
500
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
|
+
|
|
501
551
|
export type SpriteLayer = LayerBase & {
|
|
502
552
|
/**
|
|
503
553
|
* Every shown sprite whose rotated rect overlaps the layer-pixel rect
|
|
504
554
|
* (the core BVH overlap query, exact for rotated sprites), unordered -
|
|
505
|
-
* the marquee query
|
|
555
|
+
* the marquee query: `overlap` over an unrotated rect, sprites only.
|
|
556
|
+
* Node layer only.
|
|
506
557
|
*/
|
|
507
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
|
|
508
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
|
|
509
606
|
}
|
|
510
607
|
|
|
511
608
|
/** The stored UVs at `at` un-mirrored by the sprite's flags: the frame as
|
|
@@ -533,6 +630,48 @@ function fillTransform(x: number, y: number, rot: number, sx: number, sy: number
|
|
|
533
630
|
TRANSFORM[9] = 1
|
|
534
631
|
}
|
|
535
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
|
+
|
|
536
675
|
/** Compose and push a node-backed sprite's local transform - through the
|
|
537
676
|
* node's transition declaration, so with one set (setSpriteTransition)
|
|
538
677
|
* the write is a target the core animates toward. */
|
|
@@ -542,73 +681,25 @@ function writeTransform(sprite: Sprite): void {
|
|
|
542
681
|
}
|
|
543
682
|
|
|
544
683
|
/**
|
|
545
|
-
* Create a sprite layer
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
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).
|
|
549
690
|
*/
|
|
550
|
-
export function createSpriteLayer(
|
|
551
|
-
width: number,
|
|
552
|
-
height: number,
|
|
553
|
-
atlas: TextureId,
|
|
554
|
-
opts?: SpriteLayerOptions,
|
|
555
|
-
): SpriteLayer {
|
|
691
|
+
export function createSpriteLayer(atlas: TextureId, opts?: SpriteLayerOptions): SpriteLayer {
|
|
556
692
|
let capacity = opts?.capacity ?? 1024
|
|
557
693
|
if (!(capacity > 0 && Number.isInteger(capacity))) {
|
|
558
694
|
throw new Error(`createSpriteLayer: capacity must be a positive integer, got ${capacity}`)
|
|
559
695
|
}
|
|
560
696
|
let label = opts?.label ?? "sprites"
|
|
561
|
-
let oversample = opts?.oversample ?? 1
|
|
562
|
-
checkOversample("createSpriteLayer", oversample, width, height)
|
|
563
697
|
let tint = opts?.tint ?? [1, 1, 1, 1]
|
|
564
698
|
checkTint("createSpriteLayer", tint)
|
|
565
|
-
let thrash = thrashSentinel(`sprite layer "${label}"`)
|
|
566
|
-
// One unit quad (triangle strip), reused by every instance.
|
|
567
|
-
let quad = createBuffer(new Float32Array([-0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5]), {
|
|
568
|
-
label: `${label}-quad`,
|
|
569
|
-
autoFree: false,
|
|
570
|
-
})
|
|
571
699
|
let pose: BufferId = createBuffer(capacity * POSE_FLOATS * 4, { label: `${label}-pose`, autoFree: false })
|
|
572
700
|
let style: BufferId = createBuffer(capacity * STYLE_FLOATS * 4, { label: `${label}-style`, autoFree: false })
|
|
573
|
-
let
|
|
574
|
-
VERTEX_SPLIT,
|
|
575
|
-
FRAGMENT,
|
|
576
|
-
width * oversample,
|
|
577
|
-
height * oversample,
|
|
578
|
-
{ uViewport: [width, height], uCamera: [0, 0, 1, 1], uCameraRot: [1, 0, 0, 0], uTint: tint },
|
|
579
|
-
{
|
|
580
|
-
label,
|
|
581
|
-
topology: "triangle-strip",
|
|
582
|
-
vertexCount: 4,
|
|
583
|
-
attributes: [{ name: "aPos", format: "vec2" }],
|
|
584
|
-
buffer: quad,
|
|
585
|
-
instanceAttributes: INSTANCE_ATTRIBUTES_SPLIT,
|
|
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,
|
|
597
|
-
instanceCount: 0,
|
|
598
|
-
blend: "alpha",
|
|
599
|
-
textures: { uAtlas: atlas },
|
|
600
|
-
clearColor: opts?.clearColor ?? [0, 0, 0, 0],
|
|
601
|
-
autoFree: false,
|
|
602
|
-
},
|
|
603
|
-
)
|
|
701
|
+
let gpu = createSpritePipeline(label, VERTEX_SPLIT, INSTANCE_ATTRIBUTES_SPLIT)
|
|
604
702
|
|
|
605
|
-
// Camera state, mirrored for picking (the inverse mapping).
|
|
606
|
-
let camX = 0
|
|
607
|
-
let camY = 0
|
|
608
|
-
let camZoom = 1
|
|
609
|
-
let camRot = 0
|
|
610
|
-
let camPivotX = 0
|
|
611
|
-
let camPivotY = 0
|
|
612
703
|
let disposed = false
|
|
613
704
|
let scheduled = false
|
|
614
705
|
let styleDirty = false
|
|
@@ -622,6 +713,30 @@ export function createSpriteLayer(
|
|
|
622
713
|
let gpuCapacity = capacity
|
|
623
714
|
let styleData = new Float32Array(capacity * STYLE_FLOATS)
|
|
624
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
|
+
}
|
|
625
740
|
|
|
626
741
|
let flush = () => {
|
|
627
742
|
scheduled = false
|
|
@@ -633,7 +748,7 @@ export function createSpriteLayer(
|
|
|
633
748
|
endBufferWrite(style, highWater * STYLE_FLOATS * 4)
|
|
634
749
|
}
|
|
635
750
|
if (published !== highWater) {
|
|
636
|
-
|
|
751
|
+
views.setCount(highWater)
|
|
637
752
|
published = highWater
|
|
638
753
|
}
|
|
639
754
|
// The core recomputes moved subtrees and publishes every dirty pose
|
|
@@ -644,7 +759,7 @@ export function createSpriteLayer(
|
|
|
644
759
|
// Grow both instance buffers to `next` slots: the pose sinks move in one
|
|
645
760
|
// retargetRecords call (the whole used range republishes at the next
|
|
646
761
|
// flush), the style mirror grows in JS and republishes through the lease.
|
|
647
|
-
// The
|
|
762
|
+
// The entries hold the old buffers alive until the swaps land, so the
|
|
648
763
|
// destroys are safe to issue right after.
|
|
649
764
|
let grow = (next: number) => {
|
|
650
765
|
let newPose = createBuffer(next * POSE_FLOATS * 4, { label: `${label}-pose`, autoFree: false })
|
|
@@ -653,7 +768,7 @@ export function createSpriteLayer(
|
|
|
653
768
|
let grownStyle = new Float32Array(next * STYLE_FLOATS)
|
|
654
769
|
grownStyle.set(styleData)
|
|
655
770
|
styleData = grownStyle
|
|
656
|
-
|
|
771
|
+
views.setBuffers({ instanceBuffers: [newPose, newStyle] })
|
|
657
772
|
destroyBuffer(pose)
|
|
658
773
|
destroyBuffer(style)
|
|
659
774
|
pose = newPose
|
|
@@ -690,71 +805,41 @@ export function createSpriteLayer(
|
|
|
690
805
|
}
|
|
691
806
|
}
|
|
692
807
|
|
|
693
|
-
let
|
|
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,
|
|
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,
|
|
828
|
+
})
|
|
694
829
|
|
|
695
830
|
let layer: SpriteLayer = {
|
|
696
|
-
texture,
|
|
697
|
-
handlers: undefined as unknown as SpriteHandlers,
|
|
698
831
|
get count() {
|
|
699
832
|
return byNode.size
|
|
700
833
|
},
|
|
701
|
-
get width() {
|
|
702
|
-
return width
|
|
703
|
-
},
|
|
704
|
-
get height() {
|
|
705
|
-
return height
|
|
706
|
-
},
|
|
707
|
-
setSize(w, h) {
|
|
708
|
-
if (disposed || (w === width && h === height)) return
|
|
709
|
-
checkOversample("setSize", oversample, w, h)
|
|
710
|
-
width = w
|
|
711
|
-
height = h
|
|
712
|
-
setTargetSize(texture, w * oversample, h * oversample)
|
|
713
|
-
setTargetParams(texture, { uViewport: [w, h] })
|
|
714
|
-
},
|
|
715
|
-
listen(listener) {
|
|
716
|
-
listeners.add(listener)
|
|
717
|
-
return () => {
|
|
718
|
-
listeners.delete(listener)
|
|
719
|
-
}
|
|
720
|
-
},
|
|
721
|
-
get oversample() {
|
|
722
|
-
return oversample
|
|
723
|
-
},
|
|
724
|
-
setOversample(n) {
|
|
725
|
-
if (disposed || n === oversample) return
|
|
726
|
-
checkOversample("setOversample", n, width, height)
|
|
727
|
-
thrash()
|
|
728
|
-
oversample = n
|
|
729
|
-
setTargetSize(texture, width * n, height * n)
|
|
730
|
-
},
|
|
731
|
-
setCamera(update) {
|
|
732
|
-
if (disposed) return
|
|
733
|
-
checkCamera(update)
|
|
734
|
-
if (update.x !== undefined) camX = update.x
|
|
735
|
-
if (update.y !== undefined) camY = update.y
|
|
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
834
|
setTint(next) {
|
|
755
835
|
if (disposed) return
|
|
756
836
|
checkTint("setTint", next)
|
|
757
|
-
|
|
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)
|
|
758
843
|
},
|
|
759
844
|
pick(x, y) {
|
|
760
845
|
// The index reads as of the last core flush; run any pending batch
|
|
@@ -762,11 +847,13 @@ export function createSpriteLayer(
|
|
|
762
847
|
if (scheduled) flush()
|
|
763
848
|
RAY_ORIGIN[0] = x
|
|
764
849
|
RAY_ORIGIN[1] = y
|
|
765
|
-
RAY_ORIGIN[2] =
|
|
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
|
|
766
855
|
let out: Sprite[] = []
|
|
767
|
-
for (let hit of spatial.raycast(RAY_ORIGIN, RAY_DIR)) {
|
|
768
|
-
// The arena is shared (a 3d scene lives in the same index): only
|
|
769
|
-
// this layer's nodes count.
|
|
856
|
+
for (let hit of spatial.raycast(RAY_ORIGIN, RAY_DIR, filter)) {
|
|
770
857
|
let sprite = byNode.get(hit.node)
|
|
771
858
|
if (sprite) out.push(sprite)
|
|
772
859
|
}
|
|
@@ -774,29 +861,82 @@ export function createSpriteLayer(
|
|
|
774
861
|
return out.sort((a, b) => b._slot - a._slot)
|
|
775
862
|
},
|
|
776
863
|
pickRect(x, y, width, height) {
|
|
777
|
-
if (scheduled) flush()
|
|
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
|
|
785
|
-
BOX[5] = 1
|
|
786
864
|
let out: Sprite[] = []
|
|
787
|
-
for (let hit of
|
|
788
|
-
|
|
789
|
-
|
|
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]] })
|
|
790
881
|
}
|
|
791
882
|
return out
|
|
792
883
|
},
|
|
793
|
-
|
|
794
|
-
|
|
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
|
+
}
|
|
795
936
|
},
|
|
796
937
|
dispose() {
|
|
797
938
|
if (disposed) return
|
|
798
939
|
disposed = true
|
|
799
|
-
listeners.clear()
|
|
800
940
|
for (let sprite of byNode.values()) {
|
|
801
941
|
sprite.layer = null
|
|
802
942
|
declared.delete(sprite.node!)
|
|
@@ -809,13 +949,14 @@ export function createSpriteLayer(
|
|
|
809
949
|
spatial.destroyNode(group.node)
|
|
810
950
|
}
|
|
811
951
|
layer._groups.clear()
|
|
952
|
+
spatial.destroyNode(root)
|
|
812
953
|
// Let the core emit its final slot-zeroing writes while the pose
|
|
813
954
|
// buffer still exists, then free everything.
|
|
814
955
|
spatial.flush()
|
|
815
|
-
|
|
956
|
+
views.dispose()
|
|
816
957
|
destroyBuffer(pose)
|
|
817
958
|
destroyBuffer(style)
|
|
818
|
-
|
|
959
|
+
gpu.dispose()
|
|
819
960
|
},
|
|
820
961
|
_add(opts) {
|
|
821
962
|
if (disposed) throw new Error("addSprite: layer is disposed")
|
|
@@ -842,8 +983,10 @@ export function createSpriteLayer(
|
|
|
842
983
|
if (opts.parent.layer !== layer) throw new Error("addSprite: parent group belongs to another layer")
|
|
843
984
|
spatial.setParent(node, opts.parent.node)
|
|
844
985
|
opts.parent._children.add(sprite)
|
|
986
|
+
} else {
|
|
987
|
+
spatial.setParent(node, root)
|
|
845
988
|
}
|
|
846
|
-
spatial.setBounds(node,
|
|
989
|
+
spatial.setBounds(node, COLUMN_BOUNDS)
|
|
847
990
|
spatial.bindPoseRecord(node, pose, slot)
|
|
848
991
|
byNode.set(node, sprite)
|
|
849
992
|
// renderOrder defaults to 0 explicitly: a recycled slot holds the
|
|
@@ -912,16 +1055,8 @@ export function createSpriteLayer(
|
|
|
912
1055
|
RESOLVED.then(flush)
|
|
913
1056
|
},
|
|
914
1057
|
_groups: new Set(),
|
|
1058
|
+
_root: root,
|
|
915
1059
|
}
|
|
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
|
-
})
|
|
923
|
-
layer.handlers = dispatch(null)
|
|
924
|
-
|
|
925
1060
|
if (opts?.autoFree !== false && getOwner()) onCleanup(() => layer.dispose())
|
|
926
1061
|
return layer
|
|
927
1062
|
}
|
|
@@ -991,7 +1126,9 @@ export function setSpriteParent(sprite: Sprite, parent: SpriteGroup | null): voi
|
|
|
991
1126
|
if (!layer) return
|
|
992
1127
|
if (sprite.node === null) throw new Error("setSpriteParent: record layers have no groups")
|
|
993
1128
|
if (parent && parent.layer !== layer) throw new Error("setSpriteParent: group belongs to another layer")
|
|
994
|
-
|
|
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)
|
|
995
1132
|
let s: SpriteState = sprite
|
|
996
1133
|
if (s._parent) s._parent._children.delete(s)
|
|
997
1134
|
s._parent = parent
|
|
@@ -1046,6 +1183,8 @@ export function addGroup(layer: SpriteLayer, opts?: GroupOptions): SpriteGroup {
|
|
|
1046
1183
|
if (opts.parent.layer !== layer) throw new Error("addGroup: parent group belongs to another layer")
|
|
1047
1184
|
spatial.setParent(group.node, opts.parent.node)
|
|
1048
1185
|
opts.parent._children.add(group)
|
|
1186
|
+
} else {
|
|
1187
|
+
spatial.setParent(group.node, layer._root)
|
|
1049
1188
|
}
|
|
1050
1189
|
layer._groups.add(group)
|
|
1051
1190
|
layer._schedule()
|
|
@@ -1077,7 +1216,7 @@ export function setGroup(group: SpriteGroup, opts: GroupOptions): void {
|
|
|
1077
1216
|
}
|
|
1078
1217
|
if (opts.parent !== undefined) {
|
|
1079
1218
|
if (opts.parent && opts.parent.layer !== layer) throw new Error("setGroup: parent group belongs to another layer")
|
|
1080
|
-
spatial.setParent(group.node, opts.parent ? opts.parent.node :
|
|
1219
|
+
spatial.setParent(group.node, opts.parent ? opts.parent.node : layer._root)
|
|
1081
1220
|
if (g._parent) g._parent._children.delete(g)
|
|
1082
1221
|
g._parent = opts.parent
|
|
1083
1222
|
if (opts.parent) opts.parent._children.add(g)
|