@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/tiles.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// never a quad per tile. On tiled GPUs the budget is primitive count, so a
|
|
4
4
|
// 100x100 world must not be 10,000 quads per frame; baked, it is a few
|
|
5
5
|
// chunk textures, and scrolling is a transform on the composited world
|
|
6
|
-
// (see <TileLayer> in components.tsx), never a repaint.
|
|
6
|
+
// (see <TileLayer> in components/tile-layer.tsx), never a repaint.
|
|
7
7
|
//
|
|
8
8
|
// Each chunk is a small copy of the sprite pipeline (shaders.ts) with fixed
|
|
9
9
|
// record slots - record localRow * chunkTiles + localCol IS that tile, an
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
// Chunks allocate lazily on the first setTile that gives them content: an
|
|
16
16
|
// empty chunk costs nothing - no records, no buffer, no texture - so a
|
|
17
17
|
// sparse world is bounded by its content, and world size is bounded by
|
|
18
|
-
// memory, not maxTextureSize. setTile
|
|
19
|
-
// publishes and re-bakes ONLY dirty chunks. A layer nobody edits
|
|
20
|
-
// nothing, renders nothing, and costs nothing per frame.
|
|
21
|
-
// residency (bake far chunks on approach, evict them) is
|
|
22
|
-
// here yet - see okf/backlog/2d-baked-layers.md.
|
|
18
|
+
// memory, not maxTextureSize. setTile and setTiles batch to a microtask;
|
|
19
|
+
// the flush publishes and re-bakes ONLY dirty chunks. A layer nobody edits
|
|
20
|
+
// publishes nothing, renders nothing, and costs nothing per frame.
|
|
21
|
+
// Camera-driven residency (bake far chunks on approach, evict them) is
|
|
22
|
+
// deliberately not here yet - see okf/backlog/2d-baked-layers.md.
|
|
23
23
|
import { getOwner, onCleanup } from "@solidrt/core"
|
|
24
24
|
import {
|
|
25
25
|
beginBufferWrite,
|
|
@@ -30,11 +30,13 @@ import {
|
|
|
30
30
|
endBufferWrite,
|
|
31
31
|
limits,
|
|
32
32
|
renderTarget,
|
|
33
|
+
setTargetParams,
|
|
33
34
|
setTargetSize,
|
|
34
35
|
} from "@solidrt/core/gpu"
|
|
35
36
|
import type { BufferId, FilterMode, TextureId } from "@solidrt/core/gpu"
|
|
36
37
|
import type { Frame } from "./frames.ts"
|
|
37
|
-
import {
|
|
38
|
+
import { checkTint } from "./layer.ts"
|
|
39
|
+
import { checkOversample, thrashSentinel } from "./oversample.ts"
|
|
38
40
|
import { FLOATS_PER_SPRITE } from "./records.ts"
|
|
39
41
|
import { FRAGMENT, INSTANCE_ATTRIBUTES, VERTEX } from "./shaders.ts"
|
|
40
42
|
|
|
@@ -42,10 +44,24 @@ const RESOLVED = Promise.resolve()
|
|
|
42
44
|
|
|
43
45
|
// Default chunk edge in pixels; the tile count per chunk derives from it.
|
|
44
46
|
const CHUNK_TARGET_PX = 512
|
|
47
|
+
// The index that clears a cell in setTiles' index form: -1, which a
|
|
48
|
+
// Uint16Array holds as 0xffff (the same bits), so both spellings mean it
|
|
49
|
+
// and a frames table stops one short of it.
|
|
50
|
+
const CLEAR_INDEX = 0xffff
|
|
51
|
+
|
|
52
|
+
export type Tint = [number, number, number, number]
|
|
53
|
+
|
|
54
|
+
// Whether a value is a frame: four numeric UVs (the validation both write
|
|
55
|
+
// paths run before touching a record).
|
|
56
|
+
let isFrame = (f: unknown): f is Frame => {
|
|
57
|
+
let o = f as Frame | null
|
|
58
|
+
return typeof o === "object" && o !== null && typeof o.u0 === "number" && typeof o.v0 === "number" && typeof o.u1 === "number" && typeof o.v1 === "number"
|
|
59
|
+
}
|
|
45
60
|
|
|
46
61
|
export type TileLayerOptions = {
|
|
47
|
-
/** Per-chunk clear color
|
|
48
|
-
|
|
62
|
+
/** Per-chunk clear color (the name says the scope: chunks that never
|
|
63
|
+
* allocate render nothing, so this is not a layer background). */
|
|
64
|
+
chunkClearColor?: [number, number, number, number]
|
|
49
65
|
/**
|
|
50
66
|
* Sampler filter for the baked chunk textures at composite time; default
|
|
51
67
|
* "linear", which with `oversample` is the proper resample at any scale.
|
|
@@ -66,6 +82,15 @@ export type TileLayerOptions = {
|
|
|
66
82
|
* chunk count (larger = fewer textures and leaves).
|
|
67
83
|
*/
|
|
68
84
|
chunkTiles?: number
|
|
85
|
+
/** Layer tint, [r, g, b, a] in 0..1; see TileLayer.setTint. */
|
|
86
|
+
tint?: [number, number, number, number]
|
|
87
|
+
/**
|
|
88
|
+
* The frames table setTiles' index cells name - a tileset: `grid()`'s
|
|
89
|
+
* array, or any Frame[] (up to 65535 entries), so a generated world is
|
|
90
|
+
* a Uint16Array of indices and never an array of frame objects. Fixed
|
|
91
|
+
* at creation; setTile takes frames directly either way.
|
|
92
|
+
*/
|
|
93
|
+
frames?: Frame[]
|
|
69
94
|
label?: string
|
|
70
95
|
/** Skip the owner-scoped auto-dispose (see createTileLayer). */
|
|
71
96
|
autoFree?: boolean
|
|
@@ -114,13 +139,40 @@ export type TileLayer = {
|
|
|
114
139
|
/** Called after a chunk allocates - the composition hook. Assignable. */
|
|
115
140
|
onChunk?: (chunk: TileChunk) => void
|
|
116
141
|
/**
|
|
117
|
-
* Set one cell: a frame draws it, null clears it
|
|
118
|
-
*
|
|
119
|
-
*
|
|
142
|
+
* Set one cell: a frame draws it, null clears it (a clear ignores `opts` -
|
|
143
|
+
* an absent cell has no tint, it draws nothing). `tint` multiplies the
|
|
144
|
+
* cell's sampled texels like a sprite's tint; absent it keeps the cell's
|
|
145
|
+
* current tint (a cell being set from empty starts at [1, 1, 1, 1]).
|
|
146
|
+
* Batched; the microtask flush publishes and re-bakes ONLY the chunks
|
|
147
|
+
* that changed, however many tiles did.
|
|
148
|
+
*/
|
|
149
|
+
setTile(col: number, row: number, frame: Frame | null, opts?: { tint?: Tint }): void
|
|
150
|
+
/**
|
|
151
|
+
* Set a rect of cells at once: `cols` x `rows` cells from `col`, `row`,
|
|
152
|
+
* `cells` in row-major order (`cols * rows` long) - frames (null clears)
|
|
153
|
+
* or, with a `frames` table on the layer, indices into it (-1 clears; a
|
|
154
|
+
* Uint16Array holds -1 as 0xffff, the same bits). Unity's SetTilesBlock,
|
|
155
|
+
* Phaser's putTilesAt: one locate and one dirty mark per chunk the rect
|
|
156
|
+
* touches, the per-cell loop inside the layer over its own records, the
|
|
157
|
+
* same batching and flush. A chunk the rect only clears never allocates,
|
|
158
|
+
* so a whole-world write of a sparse world stays sparse. `tint` applies
|
|
159
|
+
* to every cell set, as setTile's; absent, each cell keeps its own. A
|
|
160
|
+
* bad entry throws before any cell changes.
|
|
120
161
|
*/
|
|
121
|
-
|
|
162
|
+
setTiles(col: number, row: number, cols: number, rows: number, cells: ArrayLike<Frame | null> | ArrayLike<number>, opts?: { tint?: Tint }): void
|
|
122
163
|
/** The frame at a cell, or null when empty. */
|
|
123
164
|
getTile(col: number, row: number): Frame | null
|
|
165
|
+
/** The frames table given at creation (what setTiles' indices name), or
|
|
166
|
+
* null. */
|
|
167
|
+
readonly frames: readonly Frame[] | null
|
|
168
|
+
/**
|
|
169
|
+
* Tint the whole layer, [r, g, b, a] in 0..1: a uniform multiplied over
|
|
170
|
+
* every cell's own tint (day/night, a dimmed parallax plane). Not a
|
|
171
|
+
* record write - resident chunks re-render GPU-side with nothing
|
|
172
|
+
* re-uploaded - but each write does re-render every resident chunk, so
|
|
173
|
+
* animate it coarsely, not per frame.
|
|
174
|
+
*/
|
|
175
|
+
setTint(tint: [number, number, number, number]): void
|
|
124
176
|
dispose(): void
|
|
125
177
|
}
|
|
126
178
|
|
|
@@ -128,15 +180,21 @@ type Chunk = TileChunk & {
|
|
|
128
180
|
records: Float32Array
|
|
129
181
|
buffer: BufferId
|
|
130
182
|
dirty: boolean
|
|
183
|
+
/** Records changed since the last flush (a tint-only or resize re-bake
|
|
184
|
+
* renders without re-uploading them). */
|
|
185
|
+
wrote: boolean
|
|
131
186
|
}
|
|
132
187
|
|
|
133
188
|
/**
|
|
134
189
|
* Create a baked tile layer: `cols` x `rows` cells of `tileW` x `tileH`
|
|
135
190
|
* pixels, every tile drawing one atlas frame, baked into lazily-allocated
|
|
136
|
-
* chunk textures and composited as a few quads.
|
|
137
|
-
* creation -
|
|
138
|
-
*
|
|
139
|
-
*
|
|
191
|
+
* chunk textures and composited as a few quads. A tile world is FINITE and
|
|
192
|
+
* sized at creation - that is the contract, not a provisional limit:
|
|
193
|
+
* recreate the layer to resize, and for a huge sparse world just pick big
|
|
194
|
+
* numbers, since a chunk nobody writes costs nothing (no records, no
|
|
195
|
+
* texture) and the grid bound is bookkeeping, not allocation. Disposed
|
|
196
|
+
* automatically with the owning reactive scope (opt out with `{ autoFree:
|
|
197
|
+
* false }`); the atlas is NOT owned - dispose it yourself.
|
|
140
198
|
*/
|
|
141
199
|
export function createTileLayer(
|
|
142
200
|
cols: number,
|
|
@@ -164,8 +222,30 @@ export function createTileLayer(
|
|
|
164
222
|
)
|
|
165
223
|
}
|
|
166
224
|
let label = opts?.label ?? "tiles"
|
|
225
|
+
let tint: Tint = opts?.tint ?? [1, 1, 1, 1]
|
|
226
|
+
checkTint("createTileLayer", tint)
|
|
227
|
+
// The frames table, copied (the caller's array may move on) and checked
|
|
228
|
+
// once, plus its UVs as four floats per entry: an index write copies by
|
|
229
|
+
// offset instead of reading a frame object per cell.
|
|
230
|
+
let frames: Frame[] | null = opts?.frames ? opts.frames.slice() : null
|
|
231
|
+
let tableUv: Float32Array | null = null
|
|
232
|
+
if (frames !== null) {
|
|
233
|
+
if (frames.length >= CLEAR_INDEX) {
|
|
234
|
+
throw new Error(`createTileLayer: a frames table holds at most ${CLEAR_INDEX - 1} frames, got ${frames.length}`)
|
|
235
|
+
}
|
|
236
|
+
tableUv = new Float32Array(frames.length * 4)
|
|
237
|
+
for (let i = 0; i < frames.length; i++) {
|
|
238
|
+
let f = frames[i]!
|
|
239
|
+
if (!isFrame(f)) throw new Error(`createTileLayer: frames[${i}] is not a frame, got ${JSON.stringify(f)}`)
|
|
240
|
+
tableUv[i * 4] = f.u0
|
|
241
|
+
tableUv[i * 4 + 1] = f.v0
|
|
242
|
+
tableUv[i * 4 + 2] = f.u1
|
|
243
|
+
tableUv[i * 4 + 3] = f.v1
|
|
244
|
+
}
|
|
245
|
+
}
|
|
167
246
|
let oversample = opts?.oversample ?? 1
|
|
168
247
|
checkOversample("createTileLayer", oversample, chunkW, chunkH)
|
|
248
|
+
let thrash = thrashSentinel(`tile layer "${label}"`)
|
|
169
249
|
let chunkCols = Math.ceil(cols / chunkTiles)
|
|
170
250
|
let perChunk = chunkTiles * chunkTiles
|
|
171
251
|
// One unit quad (triangle strip), shared by every chunk's pipeline.
|
|
@@ -188,13 +268,17 @@ export function createTileLayer(
|
|
|
188
268
|
for (let chunk of baked) {
|
|
189
269
|
if (!chunk.dirty) continue
|
|
190
270
|
chunk.dirty = false
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
271
|
+
if (chunk.wrote) {
|
|
272
|
+
chunk.wrote = false
|
|
273
|
+
let out = beginBufferWrite(chunk.buffer)
|
|
274
|
+
out.set(chunk.records)
|
|
275
|
+
endBufferWrite(chunk.buffer, chunk.records.byteLength)
|
|
276
|
+
}
|
|
194
277
|
renderTarget(chunk.texture)
|
|
195
278
|
}
|
|
196
279
|
}
|
|
197
|
-
let touch = (chunk: Chunk) => {
|
|
280
|
+
let touch = (chunk: Chunk, wroteRecords = true) => {
|
|
281
|
+
if (wroteRecords) chunk.wrote = true
|
|
198
282
|
if (chunk.dirty) return
|
|
199
283
|
chunk.dirty = true
|
|
200
284
|
dirtyChunks.push(chunk)
|
|
@@ -213,7 +297,9 @@ export function createTileLayer(
|
|
|
213
297
|
FRAGMENT,
|
|
214
298
|
chunkW * oversample,
|
|
215
299
|
chunkH * oversample,
|
|
216
|
-
|
|
300
|
+
// Bake targets pin the camera rotation to identity: the tile camera
|
|
301
|
+
// rotates the COMPOSITED world (<TileLayer>'s view), never the bake.
|
|
302
|
+
{ uViewport: [chunkW, chunkH], uCamera: [x, y, 1, 1], uCameraRot: [1, 0, 0, 0], uTint: tint },
|
|
217
303
|
{
|
|
218
304
|
label: `${label}-chunk`,
|
|
219
305
|
topology: "triangle-strip",
|
|
@@ -225,26 +311,58 @@ export function createTileLayer(
|
|
|
225
311
|
instanceCount: perChunk,
|
|
226
312
|
blend: "alpha",
|
|
227
313
|
textures: { uAtlas: atlas },
|
|
228
|
-
clearColor: opts?.
|
|
314
|
+
clearColor: opts?.chunkClearColor ?? [0, 0, 0, 0],
|
|
229
315
|
filter: opts?.filter,
|
|
230
316
|
render: "manual",
|
|
231
317
|
autoFree: false,
|
|
232
318
|
},
|
|
233
319
|
)
|
|
234
|
-
let chunk: Chunk = { texture, x, y, width: chunkW, height: chunkH, records, buffer, dirty: false }
|
|
320
|
+
let chunk: Chunk = { texture, x, y, width: chunkW, height: chunkH, records, buffer, dirty: false, wrote: false }
|
|
235
321
|
resident.set(index, chunk)
|
|
236
322
|
layer.chunks.push(chunk)
|
|
237
323
|
layer.onChunk?.(chunk)
|
|
238
324
|
return chunk
|
|
239
325
|
}
|
|
240
326
|
|
|
241
|
-
let
|
|
327
|
+
let checkCell = (col: number, row: number, verb: string): void => {
|
|
242
328
|
if (!(Number.isInteger(col) && Number.isInteger(row) && col >= 0 && col < cols && row >= 0 && row < rows)) {
|
|
243
329
|
throw new Error(`${verb}: cell ${col}, ${row} outside the ${cols} x ${rows} grid`)
|
|
244
330
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
331
|
+
}
|
|
332
|
+
// A cell's chunk index, and its record offset inside that chunk.
|
|
333
|
+
let chunkOf = (col: number, row: number): number => Math.floor(row / chunkTiles) * chunkCols + Math.floor(col / chunkTiles)
|
|
334
|
+
let slot = (col: number, row: number): number => ((row % chunkTiles) * chunkTiles + (col % chunkTiles)) * FLOATS_PER_SPRITE
|
|
335
|
+
// Write one cell's record: the quad at the cell, the frame's UVs, and
|
|
336
|
+
// the tint - the one given, else the default when the cell comes up from
|
|
337
|
+
// empty (a re-set keeps its tint: absent keys keep their values, like
|
|
338
|
+
// every options object here).
|
|
339
|
+
let writeCell = (r: Float32Array, at: number, col: number, row: number, u0: number, v0: number, u1: number, v1: number, cellTint: Tint | undefined): void => {
|
|
340
|
+
let fresh = r[at + 2] === 0
|
|
341
|
+
r[at] = (col + 0.5) * tileW
|
|
342
|
+
r[at + 1] = (row + 0.5) * tileH
|
|
343
|
+
r[at + 2] = tileW
|
|
344
|
+
r[at + 3] = tileH
|
|
345
|
+
r[at + 4] = u0
|
|
346
|
+
r[at + 5] = v0
|
|
347
|
+
r[at + 6] = u1
|
|
348
|
+
r[at + 7] = v1
|
|
349
|
+
if (cellTint !== undefined) {
|
|
350
|
+
r[at + 9] = cellTint[0]
|
|
351
|
+
r[at + 10] = cellTint[1]
|
|
352
|
+
r[at + 11] = cellTint[2]
|
|
353
|
+
r[at + 12] = cellTint[3]
|
|
354
|
+
} else if (fresh) {
|
|
355
|
+
r[at + 9] = 1
|
|
356
|
+
r[at + 10] = 1
|
|
357
|
+
r[at + 11] = 1
|
|
358
|
+
r[at + 12] = 1
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Clear one cell: a zero-size quad draws nothing (the tint stays, and
|
|
362
|
+
// a later set from empty resets it).
|
|
363
|
+
let clearCell = (r: Float32Array, at: number): void => {
|
|
364
|
+
r[at + 2] = 0
|
|
365
|
+
r[at + 3] = 0
|
|
248
366
|
}
|
|
249
367
|
|
|
250
368
|
let layer: TileLayer = {
|
|
@@ -263,48 +381,138 @@ export function createTileLayer(
|
|
|
263
381
|
setOversample(n) {
|
|
264
382
|
if (disposed || n === oversample) return
|
|
265
383
|
checkOversample("setOversample", n, chunkW, chunkH)
|
|
384
|
+
thrash()
|
|
266
385
|
oversample = n
|
|
267
386
|
// Resizing a target re-renders it, but a manual target's content is
|
|
268
387
|
// its last bake: mark every chunk so the flush bakes it at the new size.
|
|
269
388
|
for (let chunk of resident.values()) {
|
|
270
389
|
setTargetSize(chunk.texture, chunkW * n, chunkH * n)
|
|
271
|
-
|
|
390
|
+
// The records are unchanged; the resized target just needs a bake.
|
|
391
|
+
touch(chunk, false)
|
|
272
392
|
}
|
|
273
393
|
},
|
|
274
|
-
setTile(col, row, frame) {
|
|
394
|
+
setTile(col, row, frame, opts) {
|
|
275
395
|
if (disposed) return
|
|
276
|
-
|
|
396
|
+
checkCell(col, row, "setTile")
|
|
397
|
+
let index = chunkOf(col, row)
|
|
398
|
+
let at = slot(col, row)
|
|
277
399
|
let chunk = resident.get(index)
|
|
278
400
|
if (frame === null) {
|
|
279
401
|
// Clearing a cell no chunk holds is a no-op, not an allocation.
|
|
280
402
|
if (!chunk) return
|
|
281
|
-
chunk.records
|
|
282
|
-
chunk.records[at + 3] = 0
|
|
403
|
+
clearCell(chunk.records, at)
|
|
283
404
|
} else {
|
|
405
|
+
if (!isFrame(frame)) throw new Error(`setTile: not a frame, got ${JSON.stringify(frame)}`)
|
|
406
|
+
if (opts?.tint !== undefined) checkTint("setTile", opts.tint)
|
|
284
407
|
chunk ??= allocate(index)
|
|
285
|
-
|
|
286
|
-
r[at] = (col + 0.5) * tileW
|
|
287
|
-
r[at + 1] = (row + 0.5) * tileH
|
|
288
|
-
r[at + 2] = tileW
|
|
289
|
-
r[at + 3] = tileH
|
|
290
|
-
r[at + 4] = frame.u0
|
|
291
|
-
r[at + 5] = frame.v0
|
|
292
|
-
r[at + 6] = frame.u1
|
|
293
|
-
r[at + 7] = frame.v1
|
|
294
|
-
r[at + 9] = 1
|
|
295
|
-
r[at + 10] = 1
|
|
296
|
-
r[at + 11] = 1
|
|
297
|
-
r[at + 12] = 1
|
|
408
|
+
writeCell(chunk.records, at, col, row, frame.u0, frame.v0, frame.u1, frame.v1, opts?.tint)
|
|
298
409
|
}
|
|
299
410
|
touch(chunk)
|
|
300
411
|
},
|
|
412
|
+
setTiles(col, row, w, h, cells, opts) {
|
|
413
|
+
if (disposed) return
|
|
414
|
+
if (!(Number.isInteger(col) && Number.isInteger(row) && Number.isInteger(w) && Number.isInteger(h) && w > 0 && h > 0 && col >= 0 && row >= 0 && col + w <= cols && row + h <= rows)) {
|
|
415
|
+
throw new Error(`setTiles: rect ${col}, ${row} of ${w} x ${h} outside the ${cols} x ${rows} grid`)
|
|
416
|
+
}
|
|
417
|
+
if (cells.length !== w * h) throw new Error(`setTiles: a ${w} x ${h} rect takes ${w * h} cells, got ${cells.length}`)
|
|
418
|
+
if (opts?.tint !== undefined) checkTint("setTiles", opts.tint)
|
|
419
|
+
let cellTint = opts?.tint
|
|
420
|
+
// The two forms: indices into the table, or frames. Every entry is
|
|
421
|
+
// checked before the first write, so a bad one changes nothing.
|
|
422
|
+
let idx = typeof cells[0] === "number" ? (cells as ArrayLike<number>) : null
|
|
423
|
+
let objs = idx === null ? (cells as ArrayLike<Frame | null>) : null
|
|
424
|
+
if (idx !== null) {
|
|
425
|
+
if (frames === null) throw new Error("setTiles: index cells need a frames table (createTileLayer's frames option)")
|
|
426
|
+
let count = frames.length
|
|
427
|
+
for (let i = 0; i < idx.length; i++) {
|
|
428
|
+
let k = idx[i]!
|
|
429
|
+
if (k === -1 || k === CLEAR_INDEX) continue
|
|
430
|
+
if (!(Number.isInteger(k) && k >= 0 && k < count)) throw new Error(`setTiles: index ${k} at cell ${i} outside the ${count}-frame table`)
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
for (let i = 0; i < objs!.length; i++) {
|
|
434
|
+
let f = objs![i]
|
|
435
|
+
if (f !== null && f !== undefined && !isFrame(f)) throw new Error(`setTiles: cell ${i} is neither a frame nor null, got ${JSON.stringify(f)}`)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
let uv = tableUv
|
|
439
|
+
// Chunk by chunk over the rect: each chunk's slice of the rect is a
|
|
440
|
+
// sub-rect walked row by row with the record offset advancing, one
|
|
441
|
+
// dirty mark at the end. A chunk the slice only clears is never
|
|
442
|
+
// allocated.
|
|
443
|
+
let cr0 = Math.floor(row / chunkTiles)
|
|
444
|
+
let cr1 = Math.floor((row + h - 1) / chunkTiles)
|
|
445
|
+
let cc0 = Math.floor(col / chunkTiles)
|
|
446
|
+
let cc1 = Math.floor((col + w - 1) / chunkTiles)
|
|
447
|
+
for (let cr = cr0; cr <= cr1; cr++) {
|
|
448
|
+
let rowA = Math.max(row, cr * chunkTiles)
|
|
449
|
+
let rowB = Math.min(row + h, (cr + 1) * chunkTiles)
|
|
450
|
+
for (let cc = cc0; cc <= cc1; cc++) {
|
|
451
|
+
let colA = Math.max(col, cc * chunkTiles)
|
|
452
|
+
let colB = Math.min(col + w, (cc + 1) * chunkTiles)
|
|
453
|
+
let index = cr * chunkCols + cc
|
|
454
|
+
let chunk = resident.get(index)
|
|
455
|
+
let r = chunk ? chunk.records : null
|
|
456
|
+
for (let y = rowA; y < rowB; y++) {
|
|
457
|
+
let at = slot(colA, y)
|
|
458
|
+
let i = (y - row) * w + (colA - col)
|
|
459
|
+
for (let x = colA; x < colB; x++, at += FLOATS_PER_SPRITE, i++) {
|
|
460
|
+
let u0: number
|
|
461
|
+
let v0: number
|
|
462
|
+
let u1: number
|
|
463
|
+
let v1: number
|
|
464
|
+
if (idx !== null) {
|
|
465
|
+
let k = idx[i]!
|
|
466
|
+
if (k === -1 || k === CLEAR_INDEX) {
|
|
467
|
+
if (r !== null) clearCell(r, at)
|
|
468
|
+
continue
|
|
469
|
+
}
|
|
470
|
+
let b = k * 4
|
|
471
|
+
u0 = uv![b]!
|
|
472
|
+
v0 = uv![b + 1]!
|
|
473
|
+
u1 = uv![b + 2]!
|
|
474
|
+
v1 = uv![b + 3]!
|
|
475
|
+
} else {
|
|
476
|
+
let f = objs![i]
|
|
477
|
+
if (f === null || f === undefined) {
|
|
478
|
+
if (r !== null) clearCell(r, at)
|
|
479
|
+
continue
|
|
480
|
+
}
|
|
481
|
+
u0 = f.u0
|
|
482
|
+
v0 = f.v0
|
|
483
|
+
u1 = f.u1
|
|
484
|
+
v1 = f.v1
|
|
485
|
+
}
|
|
486
|
+
if (r === null) {
|
|
487
|
+
chunk = allocate(index)
|
|
488
|
+
r = chunk.records
|
|
489
|
+
}
|
|
490
|
+
writeCell(r, at, x, y, u0, v0, u1, v1, cellTint)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (chunk) touch(chunk)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
},
|
|
301
497
|
getTile(col, row) {
|
|
302
|
-
|
|
303
|
-
let chunk = resident.get(
|
|
498
|
+
checkCell(col, row, "getTile")
|
|
499
|
+
let chunk = resident.get(chunkOf(col, row))
|
|
500
|
+
let at = slot(col, row)
|
|
304
501
|
if (!chunk || chunk.records[at + 2] === 0) return null
|
|
305
502
|
let r = chunk.records
|
|
306
503
|
return { u0: r[at + 4]!, v0: r[at + 5]!, u1: r[at + 6]!, v1: r[at + 7]! }
|
|
307
504
|
},
|
|
505
|
+
frames,
|
|
506
|
+
setTint(next) {
|
|
507
|
+
if (disposed) return
|
|
508
|
+
checkTint("setTint", next)
|
|
509
|
+
if (next[0] === tint[0] && next[1] === tint[1] && next[2] === tint[2] && next[3] === tint[3]) return
|
|
510
|
+
tint = next
|
|
511
|
+
for (let chunk of resident.values()) {
|
|
512
|
+
setTargetParams(chunk.texture, { uTint: tint })
|
|
513
|
+
touch(chunk, false)
|
|
514
|
+
}
|
|
515
|
+
},
|
|
308
516
|
dispose() {
|
|
309
517
|
if (disposed) return
|
|
310
518
|
disposed = true
|