@aethervtt/shard-terrain 0.1.0 → 0.2.0

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.
Files changed (76) hide show
  1. package/dist/biomes.d.ts +1 -0
  2. package/dist/biomes.js +3 -0
  3. package/dist/chunk.d.ts +0 -44
  4. package/dist/chunk.js +2 -127
  5. package/dist/colliders.d.ts +16 -1
  6. package/dist/colliders.js +39 -24
  7. package/dist/components.d.ts +5 -0
  8. package/dist/components.js +7 -2
  9. package/dist/cube-sphere.d.ts +50 -0
  10. package/dist/cube-sphere.js +224 -0
  11. package/dist/grid-mesh.d.ts +49 -0
  12. package/dist/grid-mesh.js +208 -0
  13. package/dist/heightfield/bake.d.ts +120 -0
  14. package/dist/heightfield/bake.js +494 -0
  15. package/dist/heightfield/colliders.d.ts +39 -0
  16. package/dist/heightfield/colliders.js +152 -0
  17. package/dist/heightfield/component.d.ts +14 -0
  18. package/dist/heightfield/component.js +59 -0
  19. package/dist/heightfield/deflate.d.ts +11 -0
  20. package/dist/heightfield/deflate.js +484 -0
  21. package/dist/heightfield/heightmap.d.ts +27 -0
  22. package/dist/heightfield/heightmap.js +160 -0
  23. package/dist/heightfield/kernel.d.ts +277 -0
  24. package/dist/heightfield/kernel.js +1033 -0
  25. package/dist/heightfield/material.d.ts +13 -0
  26. package/dist/heightfield/material.js +394 -0
  27. package/dist/heightfield/methods.d.ts +104 -0
  28. package/dist/heightfield/methods.js +218 -0
  29. package/dist/heightfield/pack.d.ts +59 -0
  30. package/dist/heightfield/pack.js +127 -0
  31. package/dist/heightfield/pages.d.ts +91 -0
  32. package/dist/heightfield/pages.js +244 -0
  33. package/dist/heightfield/project.d.ts +40 -0
  34. package/dist/heightfield/project.js +55 -0
  35. package/dist/heightfield/queries.d.ts +48 -0
  36. package/dist/heightfield/queries.js +150 -0
  37. package/dist/heightfield/render.d.ts +108 -0
  38. package/dist/heightfield/render.js +727 -0
  39. package/dist/heightfield/root-grid.d.ts +38 -0
  40. package/dist/heightfield/root-grid.js +109 -0
  41. package/dist/heightfield/runtime.d.ts +105 -0
  42. package/dist/heightfield/runtime.js +312 -0
  43. package/dist/heightfield/source-asset.d.ts +39 -0
  44. package/dist/heightfield/source-asset.js +372 -0
  45. package/dist/heightfield/source.d.ts +151 -0
  46. package/dist/heightfield/source.js +402 -0
  47. package/dist/heightfield/stack.d.ts +29 -0
  48. package/dist/heightfield/stack.js +158 -0
  49. package/dist/heightfield/system.d.ts +22 -0
  50. package/dist/heightfield/system.js +165 -0
  51. package/dist/heightfield/testing.d.ts +317 -0
  52. package/dist/heightfield/testing.js +441 -0
  53. package/dist/heightfield/worker.js +47 -0
  54. package/dist/heights.d.ts +8 -3
  55. package/dist/heights.js +8 -4
  56. package/dist/index.d.ts +24 -7
  57. package/dist/index.js +158 -8
  58. package/dist/lod.d.ts +27 -0
  59. package/dist/lod.js +23 -1
  60. package/dist/material.js +2 -1
  61. package/dist/methods.js +64 -16
  62. package/dist/overlays.d.ts +2 -0
  63. package/dist/overlays.js +48 -6
  64. package/dist/planet.d.ts +2 -7
  65. package/dist/planet.js +8 -18
  66. package/dist/plugin.d.ts +10 -3
  67. package/dist/plugin.js +48 -8
  68. package/dist/quadtree.d.ts +61 -39
  69. package/dist/quadtree.js +44 -208
  70. package/dist/render.d.ts +7 -1
  71. package/dist/render.js +8 -6
  72. package/dist/test-planet.d.ts +39 -0
  73. package/dist/test-planet.js +40 -4
  74. package/dist/testing.d.ts +2 -0
  75. package/dist/testing.js +42 -2
  76. package/package.json +16 -16
@@ -0,0 +1,59 @@
1
+ import { defineComponent, t } from "@aethervtt/shard-core";
2
+ import { Visibility } from "@aethervtt/shard-render";
3
+ import { Grid, Transform } from "@aethervtt/shard-transform";
4
+ const Terrain = defineComponent(
5
+ "terrain/Terrain",
6
+ {
7
+ source: t.handle("terrain/TerrainSource", {
8
+ description: "The layer stack (*.terrain.json): noise, heightmap images, splines that flatten, raise or carve, and paint layers. Baked into page packs in .shard/cache/terrain by `shard import` (or on first use)."
9
+ }),
10
+ errorPixels: t.f32({
11
+ default: 2,
12
+ min: 0.1,
13
+ description: "A node splits when its geometric error covers more pixels than this."
14
+ }),
15
+ vertexPixels: t.f32({
16
+ default: 4,
17
+ min: 0,
18
+ description: "Finest on-screen vertex spacing: however rough the ground, nodes stop splitting once their vertices are this many pixels apart (normals keep the finer relief in the shading). 0: no limit."
19
+ }),
20
+ colliderRadius: t.f32({
21
+ default: 96,
22
+ min: 0,
23
+ unit: "m",
24
+ description: "Heightfield collider tiles (and leaf-detail rendering) within this distance of every TerrainAnchor, character, dynamic body and NavAgent."
25
+ }),
26
+ skirts: t.bool({
27
+ default: true,
28
+ description: "Walls under chunk edges that hide cracks while neighbors change level."
29
+ }),
30
+ residentDepth: t.i8({
31
+ default: -1,
32
+ min: -1,
33
+ description: "Depths up to this stay loaded for the terrain\u2019s life, so streaming that falls behind shows coarse ground, never a hole. \u22121: the deepest level whose pages fit in 8 MB."
34
+ }),
35
+ scatter: t.handle("scatter/ScatterSet", {
36
+ description: "Props and foliage on the terrain (*.scatter.json; needs the scatter plugin), placed per block from its heights and paint."
37
+ })
38
+ },
39
+ {
40
+ description: "A heightfield terrain (spec 0071): a bounded landscape from 256 m to 64 km a side, streamed from its baked pages through a quadtree with no cracks or popping, with Rapier heightfield colliders near bodies. Needs a Grid (its chunks are grid children); its local origin is the terrain\u2019s corner, x and z along its sides.",
41
+ requires: [Grid, Transform, Visibility]
42
+ }
43
+ );
44
+ const TerrainChunk = defineComponent(
45
+ "terrain/TerrainChunk",
46
+ {
47
+ terrain: t.entity({ readonly: true }),
48
+ key: t.string({ readonly: true, description: "depth/x/z" }),
49
+ kind: t.enum(["render", "collider"], { readonly: true })
50
+ },
51
+ {
52
+ description: "A heightfield chunk or collider tile the terrain spawned. Rebuilt from it; never saved.",
53
+ serialize: false
54
+ }
55
+ );
56
+ export {
57
+ Terrain,
58
+ TerrainChunk
59
+ };
@@ -0,0 +1,11 @@
1
+ // Types for deflate.js (plain JavaScript so worker threads can import it without a loader).
2
+
3
+ /** Compresses `input` to a raw DEFLATE stream (one final block). Same bytes on every host. */
4
+ export declare function deflate(input: Uint8Array): Uint8Array
5
+ /**
6
+ * Decompresses a raw DEFLATE stream into exactly `size` bytes. Throws an error with code
7
+ * `terrain/corrupt-pack` when the stream is damaged or doesn't hold `size` bytes.
8
+ */
9
+ export declare function inflate(input: Uint8Array, size: number): Uint8Array
10
+ /** Huffman code lengths for `freqs`, at most `limit` bits, the same on every host. */
11
+ export declare function codeLengths(freqs: ArrayLike<number>, limit: number): Uint8Array
@@ -0,0 +1,484 @@
1
+ // Raw DEFLATE (RFC 1951) for terrain packs (spec 0071). Plain JavaScript with no imports, so pool
2
+ // workers load it directly (no bundler, no TypeScript loader); types are in deflate.d.ts. Written
3
+ // here so a bake writes the same pack bytes on every host: Node's zlib and Chrome's
4
+ // CompressionStream are free to emit different streams for the same input, and pages are compared
5
+ // byte for byte. One block per call: LZ77 over a 32 KB window with hash chains of fixed length and
6
+ // one step of lazy matching, then dynamic Huffman codes limited to 15 bits. Every choice is fixed,
7
+ // so the output depends on the input alone.
8
+
9
+ const WINDOW = 32768
10
+ const MIN_MATCH = 3
11
+ const MAX_MATCH = 258
12
+ const HASH_BITS = 15
13
+ const HASH_SIZE = 1 << HASH_BITS
14
+ /** Candidates tried per position: deterministic, and enough for terrain pages. */
15
+ const CHAIN = 48
16
+ /** A match this long is taken without looking for a better one at the next byte. */
17
+ const GOOD = 32
18
+
19
+ const LENGTH_BASE = [
20
+ 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131,
21
+ 163, 195, 227, 258,
22
+ ]
23
+ const LENGTH_EXTRA = [
24
+ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0,
25
+ ]
26
+ const DIST_BASE = [
27
+ 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049,
28
+ 3073, 4097, 6145, 8193, 12289, 16385, 24577,
29
+ ]
30
+ const DIST_EXTRA = [
31
+ 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13,
32
+ ]
33
+ /** Order code length code lengths are sent in. */
34
+ const CL_ORDER = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]
35
+
36
+ /** Length code (257–285) for a match length. */
37
+ const lengthCode = new Uint16Array(MAX_MATCH + 1)
38
+ for (let c = 0; c < LENGTH_BASE.length; c++) {
39
+ const end = c + 1 < LENGTH_BASE.length ? LENGTH_BASE[c + 1] : MAX_MATCH + 1
40
+ for (let l = LENGTH_BASE[c]; l < end; l++) lengthCode[l] = c
41
+ }
42
+ lengthCode[MAX_MATCH] = 28
43
+
44
+ /** Distance code (0–29) per distance (1–32768). */
45
+ const distCodes = new Uint8Array(WINDOW + 1)
46
+ for (let c = 0; c < DIST_BASE.length; c++) {
47
+ const end = c + 1 < DIST_BASE.length ? DIST_BASE[c + 1] : WINDOW + 1
48
+ for (let d = DIST_BASE[c]; d < end; d++) distCodes[d] = c
49
+ }
50
+
51
+ function distCode(d) {
52
+ return distCodes[d]
53
+ }
54
+
55
+ /** Huffman codes go out most significant bit first: reversed into the LSB-first stream. */
56
+ function reverse(code, length) {
57
+ let rev = 0
58
+ for (let i = 0; i < length; i++) rev |= ((code >>> i) & 1) << (length - 1 - i)
59
+ return rev
60
+ }
61
+
62
+ class BitWriter {
63
+ buf = new Uint8Array(1024)
64
+ len = 0
65
+ acc = 0
66
+ bits = 0
67
+
68
+ write(value, count) {
69
+ this.acc |= value << this.bits
70
+ this.bits += count
71
+ while (this.bits >= 8) {
72
+ this.byte(this.acc & 0xff)
73
+ this.acc >>>= 8
74
+ this.bits -= 8
75
+ }
76
+ }
77
+
78
+ byte(b) {
79
+ if (this.len === this.buf.length) {
80
+ const next = new Uint8Array(this.buf.length * 2)
81
+ next.set(this.buf)
82
+ this.buf = next
83
+ }
84
+ this.buf[this.len++] = b
85
+ }
86
+
87
+ finish() {
88
+ if (this.bits > 0) this.byte(this.acc & 0xff)
89
+ this.acc = 0
90
+ this.bits = 0
91
+ return this.buf.slice(0, this.len)
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Code lengths for `freqs`, at most `limit` bits: Huffman's by a sort with ties broken by symbol,
97
+ * then lengths over the limit moved up the tree, so the result is the same everywhere.
98
+ */
99
+ export function codeLengths(freqs, limit) {
100
+ const n = freqs.length
101
+ const lengths = new Uint8Array(n)
102
+ const symbols = []
103
+ for (let s = 0; s < n; s++) if (freqs[s] > 0) symbols.push(s)
104
+ if (symbols.length === 0) return lengths
105
+ if (symbols.length === 1) {
106
+ lengths[symbols[0]] = 1
107
+ return lengths
108
+ }
109
+ // Nodes: leaves first (weight, symbol order), then internal ones made in order.
110
+ const weight = []
111
+ const parent = []
112
+ const order = symbols.slice().sort((a, b) => freqs[a] - freqs[b] || a - b)
113
+ for (const s of order) {
114
+ weight.push(freqs[s])
115
+ parent.push(-1)
116
+ }
117
+ // Two queues: leaves sorted, internal nodes made in nondecreasing weight.
118
+ let leaf = 0
119
+ let inner = order.length
120
+ const pick = () => {
121
+ if (leaf < order.length && (inner >= weight.length || weight[leaf] <= weight[inner]))
122
+ return leaf++
123
+ return inner++
124
+ }
125
+ // Until one node is left: join the two lightest.
126
+ while (order.length - leaf + (weight.length - inner) >= 2) {
127
+ const a = pick()
128
+ const b = pick()
129
+ weight.push(weight[a] + weight[b])
130
+ parent.push(-1)
131
+ parent[a] = weight.length - 1
132
+ parent[b] = weight.length - 1
133
+ }
134
+ const depth = new Array(weight.length).fill(0)
135
+ for (let i = weight.length - 2; i >= 0; i--) depth[i] = depth[parent[i]] + 1
136
+ let deepest = 0
137
+ for (let i = 0; i < order.length; i++) deepest = Math.max(deepest, depth[i])
138
+ const count = new Array(Math.max(limit, deepest) + 1).fill(0)
139
+ for (let i = 0; i < order.length; i++) count[depth[i]]++
140
+ // Lengths over the limit move up the tree two leaves at a time (JPEG's Annex K.3): a pair at
141
+ // depth i becomes one leaf at i − 1, and a leaf at a shallower j splits to make room. The Kraft
142
+ // sum stays 1.
143
+ for (let i = count.length - 1; i > limit; i--) {
144
+ while (count[i] > 0) {
145
+ let j = i - 2
146
+ while (count[j] === 0) j--
147
+ count[i] -= 2
148
+ count[i - 1] += 1
149
+ count[j + 1] += 2
150
+ count[j] -= 1
151
+ }
152
+ }
153
+ // Longest codes to the least frequent symbols (the sorted order), as Huffman would.
154
+ let i = 0
155
+ for (let l = limit; l >= 1; l--) {
156
+ for (let k = 0; k < count[l]; k++) lengths[order[i++]] = l
157
+ }
158
+ return lengths
159
+ }
160
+
161
+ /** Canonical codes for code lengths (RFC 1951 3.2.2), bit-reversed for the stream. */
162
+ function canonical(lengths) {
163
+ const maxLen = 16
164
+ const blCount = new Uint16Array(maxLen)
165
+ for (const l of lengths) if (l > 0) blCount[l]++
166
+ const next = new Uint16Array(maxLen)
167
+ let code = 0
168
+ for (let bits = 1; bits < maxLen; bits++) {
169
+ code = (code + blCount[bits - 1]) << 1
170
+ next[bits] = code
171
+ }
172
+ const codes = new Uint16Array(lengths.length)
173
+ for (let s = 0; s < lengths.length; s++) {
174
+ const l = lengths[s]
175
+ if (l > 0) codes[s] = reverse(next[l]++, l)
176
+ }
177
+ return codes
178
+ }
179
+
180
+ /** LZ77 tokens: a literal is `byte`, a match is `(256 + length) | distance << 16`. */
181
+ function tokenize(input) {
182
+ const n = input.length
183
+ const out = new Uint32Array(n + 1)
184
+ let count = 0
185
+ const head = new Int32Array(HASH_SIZE).fill(-1)
186
+ const prev = new Int32Array(WINDOW).fill(-1)
187
+ const hash = (i) =>
188
+ (((input[i] << 10) ^ (input[i + 1] << 5) ^ input[i + 2]) * 2654435761) >>> (32 - HASH_BITS)
189
+ const insert = (i) => {
190
+ if (i + MIN_MATCH > n) return
191
+ const h = hash(i)
192
+ prev[i & (WINDOW - 1)] = head[h]
193
+ head[h] = i
194
+ }
195
+ /** Longest match at `i` among earlier positions: [length, distance]. */
196
+ let bestLen = 0
197
+ let bestDist = 0
198
+ const find = (i) => {
199
+ bestLen = 0
200
+ bestDist = 0
201
+ if (i + MIN_MATCH > n) return
202
+ let candidate = head[hash(i)]
203
+ const limit = Math.min(MAX_MATCH, n - i)
204
+ for (let chain = 0; chain < CHAIN && candidate >= 0 && i - candidate <= WINDOW; chain++) {
205
+ if (input[candidate + bestLen] === input[i + bestLen]) {
206
+ let l = 0
207
+ while (l < limit && input[candidate + l] === input[i + l]) l++
208
+ if (l > bestLen) {
209
+ bestLen = l
210
+ bestDist = i - candidate
211
+ if (l === limit) break
212
+ }
213
+ }
214
+ const p = prev[candidate & (WINDOW - 1)]
215
+ if (p >= candidate) break
216
+ candidate = p
217
+ }
218
+ if (bestLen < MIN_MATCH) bestLen = 0
219
+ }
220
+ let i = 0
221
+ while (i < n) {
222
+ find(i)
223
+ let len = bestLen
224
+ let dist = bestDist
225
+ if (len > 0 && len < GOOD && i + 1 < n) {
226
+ // Lazy: if the next byte starts a longer match, this one goes out as a literal.
227
+ insert(i)
228
+ find(i + 1)
229
+ if (bestLen > len) {
230
+ out[count++] = input[i]
231
+ i++
232
+ len = bestLen
233
+ dist = bestDist
234
+ for (let k = 0; k < len; k++) insert(i + k)
235
+ } else for (let k = 1; k < len; k++) insert(i + k)
236
+ out[count++] = (256 + len) | (dist << 16)
237
+ i += len
238
+ } else if (len > 0) {
239
+ for (let k = 0; k < len; k++) insert(i + k)
240
+ out[count++] = (256 + len) | (dist << 16)
241
+ i += len
242
+ } else {
243
+ insert(i)
244
+ out[count++] = input[i]
245
+ i++
246
+ }
247
+ }
248
+ return out.subarray(0, count)
249
+ }
250
+
251
+ /** Compresses `input` to a raw DEFLATE stream (one final block). Same bytes on every host. */
252
+ export function deflate(input) {
253
+ const tokens = tokenize(input)
254
+ const litFreq = new Uint32Array(286)
255
+ const distFreq = new Uint32Array(30)
256
+ for (const t of tokens) {
257
+ if (t < 256) litFreq[t]++
258
+ else {
259
+ const len = (t & 0xffff) - 256
260
+ litFreq[257 + lengthCode[len]]++
261
+ distFreq[distCode(t >>> 16)]++
262
+ }
263
+ }
264
+ litFreq[256] = 1
265
+ // At least one distance code (inflaters want a distance tree even with no matches).
266
+ if (distFreq.every((f) => f === 0)) distFreq[0] = 1
267
+ const litLen = codeLengths(litFreq, 15)
268
+ const distLen = codeLengths(distFreq, 15)
269
+ let hlit = 286
270
+ while (hlit > 257 && litLen[hlit - 1] === 0) hlit--
271
+ let hdist = 30
272
+ while (hdist > 1 && distLen[hdist - 1] === 0) hdist--
273
+ // Run-length code the two length tables together.
274
+ const all = new Uint8Array(hlit + hdist)
275
+ all.set(litLen.subarray(0, hlit))
276
+ all.set(distLen.subarray(0, hdist), hlit)
277
+ const cl = []
278
+ const clExtra = []
279
+ for (let i = 0; i < all.length; ) {
280
+ const v = all[i]
281
+ let run = 1
282
+ while (i + run < all.length && all[i + run] === v) run++
283
+ if (v === 0 && run >= 3) {
284
+ const r = Math.min(run, 138)
285
+ if (r >= 11) {
286
+ cl.push(18)
287
+ clExtra.push(r - 11)
288
+ } else {
289
+ cl.push(17)
290
+ clExtra.push(r - 3)
291
+ }
292
+ i += r
293
+ } else if (v !== 0 && run >= 4) {
294
+ cl.push(v)
295
+ clExtra.push(0)
296
+ const r = Math.min(run - 1, 6)
297
+ cl.push(16)
298
+ clExtra.push(r - 3)
299
+ i += 1 + r
300
+ } else {
301
+ cl.push(v)
302
+ clExtra.push(0)
303
+ i++
304
+ }
305
+ }
306
+ const clFreq = new Uint32Array(19)
307
+ for (const c of cl) clFreq[c]++
308
+ const clLen = codeLengths(clFreq, 7)
309
+ const clCodes = canonical(clLen)
310
+ let hclen = 19
311
+ while (hclen > 4 && clLen[CL_ORDER[hclen - 1]] === 0) hclen--
312
+ const litCodes = canonical(litLen)
313
+ const distCodes = canonical(distLen)
314
+ const w = new BitWriter()
315
+ w.write(1, 1) // final block
316
+ w.write(2, 2) // dynamic Huffman
317
+ w.write(hlit - 257, 5)
318
+ w.write(hdist - 1, 5)
319
+ w.write(hclen - 4, 4)
320
+ for (let i = 0; i < hclen; i++) w.write(clLen[CL_ORDER[i]], 3)
321
+ for (let i = 0; i < cl.length; i++) {
322
+ const c = cl[i]
323
+ w.write(clCodes[c], clLen[c])
324
+ if (c === 16) w.write(clExtra[i], 2)
325
+ else if (c === 17) w.write(clExtra[i], 3)
326
+ else if (c === 18) w.write(clExtra[i], 7)
327
+ }
328
+ for (const t of tokens) {
329
+ if (t < 256) {
330
+ w.write(litCodes[t], litLen[t])
331
+ continue
332
+ }
333
+ const len = (t & 0xffff) - 256
334
+ const lc = lengthCode[len]
335
+ w.write(litCodes[257 + lc], litLen[257 + lc])
336
+ if (LENGTH_EXTRA[lc] > 0) w.write(len - LENGTH_BASE[lc], LENGTH_EXTRA[lc])
337
+ const dist = t >>> 16
338
+ const dc = distCode(dist)
339
+ w.write(distCodes[dc], distLen[dc])
340
+ if (DIST_EXTRA[dc] > 0) w.write(dist - DIST_BASE[dc], DIST_EXTRA[dc])
341
+ }
342
+ w.write(litCodes[256], litLen[256])
343
+ return w.finish()
344
+ }
345
+
346
+ // --- inflate -----------------------------------------------------------------------------------
347
+
348
+ /** A Huffman decoding table: counts per length and symbols in canonical order. */
349
+ function decoder(lengths, n) {
350
+ const counts = new Uint16Array(16)
351
+ for (let i = 0; i < n; i++) counts[lengths[i]]++
352
+ counts[0] = 0
353
+ const offs = new Uint16Array(16)
354
+ for (let l = 1; l < 16; l++) offs[l] = offs[l - 1] + counts[l - 1]
355
+ const symbols = new Uint16Array(n)
356
+ for (let i = 0; i < n; i++) if (lengths[i] > 0) symbols[offs[lengths[i]]++] = i
357
+ return { counts, symbols }
358
+ }
359
+
360
+ const fixedLit = (() => {
361
+ const l = new Uint8Array(288)
362
+ l.fill(8, 0, 144)
363
+ l.fill(9, 144, 256)
364
+ l.fill(7, 256, 280)
365
+ l.fill(8, 280, 288)
366
+ return decoder(l, 288)
367
+ })()
368
+ const fixedDist = decoder(new Uint8Array(30).fill(5), 30)
369
+
370
+ function corrupt(why) {
371
+ return Object.assign(new Error(`A terrain page doesn't inflate: ${why}`), {
372
+ code: 'terrain/corrupt-pack',
373
+ hint: 'The pack is damaged or from another bake: `shard terrain bake --force` rewrites it.',
374
+ })
375
+ }
376
+
377
+ /**
378
+ * Decompresses a raw DEFLATE stream into exactly `size` bytes (a page's known length). Any
379
+ * conforming stream works; terrain packs hold `deflate`'s.
380
+ */
381
+ export function inflate(input, size) {
382
+ const out = new Uint8Array(size)
383
+ let o = 0
384
+ let pos = 0
385
+ let acc = 0
386
+ let bits = 0
387
+ const need = (n) => {
388
+ while (bits < n) {
389
+ if (pos >= input.length) throw corrupt('it ends early')
390
+ acc |= input[pos++] << bits
391
+ bits += 8
392
+ }
393
+ }
394
+ const take = (n) => {
395
+ if (n === 0) return 0
396
+ need(n)
397
+ const v = acc & ((1 << n) - 1)
398
+ acc >>>= n
399
+ bits -= n
400
+ return v
401
+ }
402
+ const decode = (d) => {
403
+ let code = 0
404
+ let first = 0
405
+ let index = 0
406
+ for (let len = 1; len < 16; len++) {
407
+ code |= take(1)
408
+ const count = d.counts[len]
409
+ if (code - count < first) return d.symbols[index + (code - first)]
410
+ index += count
411
+ first += count
412
+ first <<= 1
413
+ code <<= 1
414
+ }
415
+ throw corrupt('a code is invalid')
416
+ }
417
+ let final = 0
418
+ while (!final) {
419
+ final = take(1)
420
+ const type = take(2)
421
+ if (type === 0) {
422
+ acc = 0
423
+ bits = 0
424
+ if (pos + 4 > input.length) throw corrupt('it ends early')
425
+ const len = input[pos] | (input[pos + 1] << 8)
426
+ pos += 4
427
+ if (o + len > size || pos + len > input.length) throw corrupt('a stored block overruns')
428
+ out.set(input.subarray(pos, pos + len), o)
429
+ o += len
430
+ pos += len
431
+ continue
432
+ }
433
+ let lit = fixedLit
434
+ let dist = fixedDist
435
+ if (type === 2) {
436
+ const hlit = take(5) + 257
437
+ const hdist = take(5) + 1
438
+ const hclen = take(4) + 4
439
+ const clLen = new Uint8Array(19)
440
+ for (let i = 0; i < hclen; i++) clLen[CL_ORDER[i]] = take(3)
441
+ const cl = decoder(clLen, 19)
442
+ const lengths = new Uint8Array(hlit + hdist)
443
+ for (let i = 0; i < hlit + hdist; ) {
444
+ const sym = decode(cl)
445
+ if (sym < 16) lengths[i++] = sym
446
+ else {
447
+ let rep = 0
448
+ let v = 0
449
+ if (sym === 16) {
450
+ if (i === 0) throw corrupt('a repeat has nothing before it')
451
+ v = lengths[i - 1]
452
+ rep = 3 + take(2)
453
+ } else if (sym === 17) rep = 3 + take(3)
454
+ else rep = 11 + take(7)
455
+ if (i + rep > lengths.length) throw corrupt('code lengths overrun')
456
+ lengths.fill(v, i, i + rep)
457
+ i += rep
458
+ }
459
+ }
460
+ lit = decoder(lengths.subarray(0, hlit), hlit)
461
+ dist = decoder(lengths.subarray(hlit), hdist)
462
+ } else if (type !== 1) throw corrupt('a block has an unknown type')
463
+ for (;;) {
464
+ const sym = decode(lit)
465
+ if (sym < 256) {
466
+ if (o >= size) throw corrupt('it is longer than the page')
467
+ out[o++] = sym
468
+ } else if (sym === 256) break
469
+ else {
470
+ const lc = sym - 257
471
+ if (lc >= 29) throw corrupt('a length code is invalid')
472
+ const len = LENGTH_BASE[lc] + take(LENGTH_EXTRA[lc])
473
+ const dc = decode(dist)
474
+ if (dc >= 30) throw corrupt('a distance code is invalid')
475
+ const d = DIST_BASE[dc] + take(DIST_EXTRA[dc])
476
+ if (d > o || o + len > size) throw corrupt('a match reaches outside the page')
477
+ for (let k = 0; k < len; k++) out[o + k] = out[o + k - d]
478
+ o += len
479
+ }
480
+ }
481
+ }
482
+ if (o !== size) throw corrupt(`it holds ${o} bytes, not ${size}`)
483
+ return out
484
+ }
@@ -0,0 +1,27 @@
1
+ import { AssetStore } from '@aethervtt/shard-assets';
2
+ /** A heightmap image (spec 0071): samples 0–1, row by row, top row first. */
3
+ export interface Heightmap {
4
+ readonly width: number;
5
+ readonly height: number;
6
+ readonly data: Float32Array;
7
+ /** Bumps on hot reload. */
8
+ version: number;
9
+ }
10
+ export declare const Heightmaps: import("@aethervtt/shard-core").ResourceDef<AssetStore<Heightmap, "Heightmap">>;
11
+ /** What the importer stores: a small header and the samples (u16 or f32, little-endian). */
12
+ interface HeightmapArtifact {
13
+ width: number;
14
+ height: number;
15
+ bits: 16 | 32;
16
+ }
17
+ export declare const HeightmapAssetType: import("@aethervtt/shard-assets").AssetTypeDef<Heightmap>;
18
+ /** Decodes a 16-bit grayscale, non-interlaced PNG to samples 0–65535 (row by row). */
19
+ export declare function decodeHeightPng(path: string, png: Uint8Array): HeightmapArtifact & {
20
+ samples: Uint16Array;
21
+ };
22
+ /**
23
+ * `*.r16`, `*.r32` and `*.height.png` (or any 16-bit grayscale PNG whose `.meta` names this
24
+ * importer): a Heightmap for terrain image layers and paint masks (spec 0071).
25
+ */
26
+ export declare const HeightmapImporter: import("@aethervtt/shard-assets").ImporterDef;
27
+ export {};