@aethervtt/shard-terrain 0.0.1 → 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 +18 -8
  72. package/dist/test-planet.d.ts +39 -0
  73. package/dist/test-planet.js +41 -5
  74. package/dist/testing.d.ts +2 -0
  75. package/dist/testing.js +43 -3
  76. package/package.json +16 -16
@@ -0,0 +1,224 @@
1
+ import { faceToDirection, neighborNode, nodeExtent } from "./cube.js";
2
+ import { QuadTree } from "./quadtree.js";
3
+ const POINTS = 9;
4
+ class CubeSphere {
5
+ roots = 6;
6
+ radius = 1;
7
+ shape = new Float64Array([1, 1, 1]);
8
+ /** Horizon test directions (unit, 9 per node). */
9
+ dirs = new Float64Array(0);
10
+ /**
11
+ * 1 / cos(half the widest angle between neighboring test points, diagonals included): lifting
12
+ * the points by it puts the surface between them inside their hull (horizon test, bulge).
13
+ */
14
+ lift = new Float64Array(0);
15
+ grow(capacity) {
16
+ const dirs = new Float64Array(capacity * POINTS * 3);
17
+ dirs.set(this.dirs);
18
+ this.dirs = dirs;
19
+ const lift = new Float64Array(capacity);
20
+ lift.set(this.lift);
21
+ this.lift = lift;
22
+ }
23
+ initNode(tree, n) {
24
+ const face = tree.root[n];
25
+ const ext = nodeExtent(tree.depth[n]);
26
+ const u0 = -1 + tree.x[n] * ext;
27
+ const v0 = -1 + tree.y[n] * ext;
28
+ let k = n * POINTS * 3;
29
+ for (let j = 0; j < 3; j++) {
30
+ for (let i = 0; i < 3; i++) {
31
+ faceToDirection(face, u0 + ext * i / 2, v0 + ext * j / 2, this.dirs, k);
32
+ k += 3;
33
+ }
34
+ }
35
+ let widest = 0;
36
+ const base = n * POINTS * 3;
37
+ for (let a = 0; a < POINTS; a++) {
38
+ for (let b = a + 1; b < POINTS; b++) {
39
+ if (Math.abs(a % 3 - b % 3) > 1 || Math.abs(Math.floor(a / 3) - Math.floor(b / 3)) > 1)
40
+ continue;
41
+ const d = this.dirs;
42
+ const cos = d[base + a * 3] * d[base + b * 3] + d[base + a * 3 + 1] * d[base + b * 3 + 1] + d[base + a * 3 + 2] * d[base + b * 3 + 2];
43
+ widest = Math.max(widest, Math.acos(Math.min(1, cos)));
44
+ }
45
+ }
46
+ this.lift[n] = 1 / Math.cos(widest / 2) + 1e-9;
47
+ }
48
+ /** Bounding sphere of the node's 9 test points at its lowest and highest heights, plus bulge. */
49
+ updateSphere(tree, n) {
50
+ const d = this.dirs;
51
+ const s = this.shape;
52
+ const lo = this.radius + tree.minH[n];
53
+ const hi = this.radius + tree.maxH[n];
54
+ let minX = Infinity;
55
+ let minY = Infinity;
56
+ let minZ = Infinity;
57
+ let maxX = -Infinity;
58
+ let maxY = -Infinity;
59
+ let maxZ = -Infinity;
60
+ for (let p = 0; p < POINTS; p++) {
61
+ const o2 = (n * POINTS + p) * 3;
62
+ for (let k = 0; k < 2; k++) {
63
+ const r = k === 0 ? lo : hi;
64
+ const x = d[o2] * s[0] * r;
65
+ const y = d[o2 + 1] * s[1] * r;
66
+ const z = d[o2 + 2] * s[2] * r;
67
+ if (x < minX) minX = x;
68
+ if (y < minY) minY = y;
69
+ if (z < minZ) minZ = z;
70
+ if (x > maxX) maxX = x;
71
+ if (y > maxY) maxY = y;
72
+ if (z > maxZ) maxZ = z;
73
+ }
74
+ }
75
+ const cx = (minX + maxX) / 2;
76
+ const cy = (minY + maxY) / 2;
77
+ const cz = (minZ + maxZ) / 2;
78
+ let r2 = 0;
79
+ for (let p = 0; p < POINTS; p++) {
80
+ const o2 = (n * POINTS + p) * 3;
81
+ for (let k = 0; k < 2; k++) {
82
+ const r = k === 0 ? lo : hi;
83
+ const x = d[o2] * s[0] * r - cx;
84
+ const y = d[o2 + 1] * s[1] * r - cy;
85
+ const z = d[o2 + 2] * s[2] * r - cz;
86
+ const q = x * x + y * y + z * z;
87
+ if (q > r2) r2 = q;
88
+ }
89
+ }
90
+ const bulge = hi * (this.lift[n] - 1) + 1e-3;
91
+ const o = n * 4;
92
+ tree.sphere[o] = cx;
93
+ tree.sphere[o + 1] = cy;
94
+ tree.sphere[o + 2] = cz;
95
+ tree.sphere[o + 3] = Math.sqrt(r2) + bulge;
96
+ }
97
+ /**
98
+ * Axis-aligned bounds of the node relative to `center`, from its test points at its lowest and
99
+ * highest heights plus the bulge between them.
100
+ */
101
+ bounds(tree, n, center, out) {
102
+ const d = this.dirs;
103
+ const s = this.shape;
104
+ const lo = this.radius + tree.minH[n];
105
+ const hi = this.radius + tree.maxH[n];
106
+ let minX = Infinity;
107
+ let minY = Infinity;
108
+ let minZ = Infinity;
109
+ let maxX = -Infinity;
110
+ let maxY = -Infinity;
111
+ let maxZ = -Infinity;
112
+ for (let p = 0; p < POINTS; p++) {
113
+ const o = (n * POINTS + p) * 3;
114
+ for (let k = 0; k < 2; k++) {
115
+ const r = k === 0 ? lo : hi;
116
+ const x = d[o] * s[0] * r - center[0];
117
+ const y = d[o + 1] * s[1] * r - center[1];
118
+ const z = d[o + 2] * s[2] * r - center[2];
119
+ if (x < minX) minX = x;
120
+ if (y < minY) minY = y;
121
+ if (z < minZ) minZ = z;
122
+ if (x > maxX) maxX = x;
123
+ if (y > maxY) maxY = y;
124
+ if (z > maxZ) maxZ = z;
125
+ }
126
+ }
127
+ const bulge = hi * (this.lift[n] - 1) + 1e-3;
128
+ out[0] = minX - bulge;
129
+ out[1] = minY - bulge;
130
+ out[2] = minZ - bulge;
131
+ out[3] = maxX + bulge;
132
+ out[4] = maxY + bulge;
133
+ out[5] = maxZ + bulge;
134
+ }
135
+ culled(tree, n, view, params) {
136
+ const s = tree.sphere;
137
+ const o = n * 4;
138
+ const cx = s[o];
139
+ const cy = s[o + 1];
140
+ const cz = s[o + 2];
141
+ const r = s[o + 3];
142
+ const f = view.frustum;
143
+ const d = this.dirs;
144
+ const sh = this.shape;
145
+ const lo = this.radius + tree.minH[n];
146
+ const hi = (this.radius + tree.maxH[n]) * this.lift[n];
147
+ for (let p = 0; p < view.planes; p++) {
148
+ const q = p * 4;
149
+ const nx = f[q];
150
+ const ny = f[q + 1];
151
+ const nz = f[q + 2];
152
+ const dist = nx * cx + ny * cy + nz * cz + f[q + 3];
153
+ if (dist < -r) return true;
154
+ if (dist > r) continue;
155
+ let inside = false;
156
+ for (let k = 0; k < POINTS && !inside; k++) {
157
+ const i = (n * POINTS + k) * 3;
158
+ const dx = d[i] * sh[0];
159
+ const dy = d[i + 1] * sh[1];
160
+ const dz = d[i + 2] * sh[2];
161
+ const along = nx * dx + ny * dy + nz * dz;
162
+ if (along * lo + f[q + 3] >= 0 || along * hi + f[q + 3] >= 0) inside = true;
163
+ }
164
+ if (!inside) return true;
165
+ }
166
+ if (params.occluder <= 0) return false;
167
+ const inv = 1 / params.occluder;
168
+ const px = view.position[0] / sh[0] * inv;
169
+ const py = view.position[1] / sh[1] * inv;
170
+ const pz = view.position[2] / sh[2] * inv;
171
+ const vh2 = px * px + py * py + pz * pz - 1;
172
+ if (vh2 <= 0) return false;
173
+ const top = hi * inv;
174
+ for (let k = 0; k < POINTS; k++) {
175
+ const i = (n * POINTS + k) * 3;
176
+ const tx = d[i] * top - px;
177
+ const ty = d[i + 1] * top - py;
178
+ const tz = d[i + 2] * top - pz;
179
+ const dot = -(tx * px + ty * py + tz * pz);
180
+ if (dot <= vh2) return false;
181
+ if (dot * dot / (tx * tx + ty * ty + tz * tz) <= vh2) return false;
182
+ }
183
+ return true;
184
+ }
185
+ /** Across a cube edge the neighbor is on another face: there always is one. */
186
+ neighbor(root, depth, x, y, edge, out) {
187
+ neighborNode(root, depth, x, y, edge, out);
188
+ return true;
189
+ }
190
+ }
191
+ class NodeTree extends QuadTree {
192
+ cube;
193
+ constructor(capacity = 1024) {
194
+ const cube = new CubeSphere();
195
+ super(cube, capacity);
196
+ this.cube = cube;
197
+ }
198
+ /** Each node's cube face (its root). */
199
+ get face() {
200
+ return this.root;
201
+ }
202
+ get radius() {
203
+ return this.cube.radius;
204
+ }
205
+ get shape() {
206
+ return this.cube.shape;
207
+ }
208
+ /** Clears every node and makes the six roots (after a planet's shape or graphs change). */
209
+ reset(radius, shape, lowest, highest) {
210
+ this.cube.radius = radius;
211
+ this.cube.shape[0] = shape[0];
212
+ this.cube.shape[1] = shape[1];
213
+ this.cube.shape[2] = shape[2];
214
+ this.resetRoots(lowest, highest);
215
+ }
216
+ /** Root of a face (nodes 0–5). */
217
+ rootOf(face) {
218
+ return face;
219
+ }
220
+ }
221
+ export {
222
+ CubeSphere,
223
+ NodeTree
224
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * How a chunk's vertices are laid out, per resolution `n` (vertices per edge, 2^k + 1). The border
3
+ * ring comes first (so edge locks rewrite one contiguous block), then the interior row by row, then
4
+ * one skirt vertex under each ring vertex. Every chunk of a resolution shares one index buffer.
5
+ */
6
+ export interface ChunkLayout {
7
+ resolution: number;
8
+ /** Ring vertices: 4 (n − 1), counter-clockwise from (0, 0) seen from outside. */
9
+ ring: number;
10
+ /** Surface vertices: n². */
11
+ surface: number;
12
+ /** Surface plus skirt: n² + 4 (n − 1). */
13
+ vertexCount: number;
14
+ /** Vertex index of grid point (i, j), at `i + j * n`. */
15
+ index: Uint16Array;
16
+ /** Grid point of each ring vertex: i, j pairs. */
17
+ ringPoints: Uint16Array;
18
+ /** Triangles: the surface, then the skirts (`chunkIndices` with every quadrant, no stitching). */
19
+ indices: Uint16Array;
20
+ /** Triangles without skirts (the surface only): colliders, and planets with skirts off. */
21
+ surfaceIndices: Uint16Array;
22
+ }
23
+ /**
24
+ * A chunk's triangles (cached per combination, so chunks share the array and its GPU index buffer).
25
+ * `mask` picks quadrants (bit q: quadrant q, in child order (x, y), (x+1, y), (x, y+1),
26
+ * (x+1, y+1)): a split waiting for children draws its own quadrants where they're missing and the
27
+ * children elsewhere. `stitch` bit e marks edge e (0 bottom, 1 right, 2 top, 3 left) as meeting a
28
+ * coarser neighbor: that edge's triangles use only its even vertices, the neighbor's, so there's no
29
+ * T-junction (a vertex on the neighbor's edge only up to rounding shows pixel-sized holes).
30
+ *
31
+ * Quads fill the interior; each edge's strip (between the border and the first inner row) is
32
+ * zipped per half, so no triangle crosses a quadrant boundary. Skirts follow the drawn border.
33
+ *
34
+ * `diagonal: 'anti'` (heightfields, 0071) splits every quad along (i+1, j)–(i, j+1), Rapier's
35
+ * heightfield split, border cells included: only a stitched edge's strip is zipped. What's drawn is
36
+ * then exactly the collider's triangles wherever no edge is stitched. Its triangles face +Y with
37
+ * i along +X and j along +Z (clockwise in (i, j)).
38
+ */
39
+ export declare function chunkIndices(n: number, mask: number, stitch: number, skirts: boolean, diagonal?: 'main' | 'anti'): Uint16Array;
40
+ /** Throws `terrain/bad-resolution` unless `n` is 2^k + 1 (5 to 129). */
41
+ export declare function checkResolution(n: number): void;
42
+ export declare function chunkLayout(n: number): ChunkLayout;
43
+ /**
44
+ * A vertex's lock code (uv1.x): its edge for ring vertices (0 bottom, 1 right, 2 top, 3 left, where
45
+ * the instance's lock bits apply), the quadrant boundary for vertices on the center lines (4 and 5
46
+ * the vertical line's lower and upper half, 6 and 7 the horizontal line's left and right half, 8
47
+ * the center; locked to this level where a partial draw meets a child), else −1.
48
+ */
49
+ export declare function lockCode(i: number, j: number, n: number, vi: number, ring: number): number;
@@ -0,0 +1,208 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ const layouts = /* @__PURE__ */ new Map();
3
+ const sets = /* @__PURE__ */ new Map();
4
+ function chunkIndices(n, mask, stitch, skirts, diagonal = "main") {
5
+ if (diagonal === "anti") return antiIndices(n, mask, stitch, skirts);
6
+ const key = `${n}:${mask}:${stitch}:${skirts}`;
7
+ let out = sets.get(key);
8
+ if (out) return out;
9
+ const layout = chunkLayout(n);
10
+ const index = layout.index;
11
+ const side = n - 1;
12
+ const h = side / 2;
13
+ const parts = [];
14
+ const quadrant = (i, j) => (i >= h ? 1 : 0) + (j >= h ? 2 : 0);
15
+ for (let j = 1; j < side - 1; j++) {
16
+ for (let i = 1; i < side - 1; i++) {
17
+ if (!(mask & 1 << quadrant(i, j))) continue;
18
+ const a = index[i + j * n];
19
+ const b = index[i + 1 + j * n];
20
+ const c = index[i + 1 + (j + 1) * n];
21
+ const d = index[i + (j + 1) * n];
22
+ parts.push(a, b, c, a, c, d);
23
+ }
24
+ }
25
+ const at = (e, s, depth) => {
26
+ const i = e === 0 ? s : e === 1 ? side - depth : e === 2 ? side - s : depth;
27
+ const j = e === 0 ? depth : e === 1 ? s : e === 2 ? side - depth : side - s;
28
+ return index[i + j * n];
29
+ };
30
+ const skirt = [];
31
+ const outer = [];
32
+ const inner = [];
33
+ for (let e = 0; e < 4; e++) {
34
+ const step = stitch & 1 << e ? 2 : 1;
35
+ for (let half = 0; half < 2; half++) {
36
+ const s0 = half * h;
37
+ const s1 = s0 + h;
38
+ const m = s0 + h / 2;
39
+ const q = e === 0 ? quadrant(m, 0) : e === 1 ? quadrant(side, m) : e === 2 ? quadrant(side - m, side) : quadrant(0, side - m);
40
+ if (!(mask & 1 << q)) continue;
41
+ outer.length = 0;
42
+ inner.length = 0;
43
+ for (let s = s0; s <= s1; s += step) outer.push(s);
44
+ for (let s = Math.max(1, s0); s <= Math.min(side - 1, s1); s++) inner.push(s);
45
+ let a = 0;
46
+ let b = 0;
47
+ while (a < outer.length - 1 || b < inner.length - 1) {
48
+ const advanceOuter = b === inner.length - 1 || a < outer.length - 1 && outer[a + 1] <= inner[b + 1];
49
+ if (advanceOuter) {
50
+ parts.push(at(e, outer[a], 0), at(e, outer[a + 1], 0), at(e, inner[b], 1));
51
+ a++;
52
+ } else {
53
+ parts.push(at(e, outer[a], 0), at(e, inner[b + 1], 1), at(e, inner[b], 1));
54
+ b++;
55
+ }
56
+ }
57
+ if (!skirts) continue;
58
+ for (let k = 0; k + 1 < outer.length; k++) {
59
+ const va = at(e, outer[k], 0);
60
+ const vb = at(e, outer[k + 1], 0);
61
+ const sa = layout.surface + va;
62
+ const sb = layout.surface + vb;
63
+ skirt.push(va, sa, vb, vb, sa, sb, va, vb, sa, vb, sb, sa);
64
+ }
65
+ }
66
+ }
67
+ for (const v of skirt) parts.push(v);
68
+ out = Uint16Array.from(parts);
69
+ sets.set(key, out);
70
+ return out;
71
+ }
72
+ function antiIndices(n, mask, stitch, skirts) {
73
+ const key = `${n}:${mask}:${stitch}:${skirts}:anti`;
74
+ let out = sets.get(key);
75
+ if (out) return out;
76
+ const layout = chunkLayout(n);
77
+ const index = layout.index;
78
+ const side = n - 1;
79
+ const h = side / 2;
80
+ const parts = [];
81
+ const quadrant = (i, j) => (i >= h ? 1 : 0) + (j >= h ? 2 : 0);
82
+ const stitched = (e) => (stitch & 1 << e) !== 0;
83
+ for (let j = 0; j < side; j++) {
84
+ for (let i = 0; i < side; i++) {
85
+ if (j === 0 && stitched(0) || i === side - 1 && stitched(1) || j === side - 1 && stitched(2) || i === 0 && stitched(3))
86
+ continue;
87
+ if (!(mask & 1 << quadrant(i, j))) continue;
88
+ const a = index[i + j * n];
89
+ const b = index[i + 1 + j * n];
90
+ const c = index[i + 1 + (j + 1) * n];
91
+ const d = index[i + (j + 1) * n];
92
+ parts.push(a, b, d, b, c, d);
93
+ }
94
+ }
95
+ const at = (e, s, depth) => {
96
+ const i = e === 0 ? s : e === 1 ? side - depth : e === 2 ? side - s : depth;
97
+ const j = e === 0 ? depth : e === 1 ? s : e === 2 ? side - depth : side - s;
98
+ return index[i + j * n];
99
+ };
100
+ const skirt = [];
101
+ const outer = [];
102
+ const inner = [];
103
+ for (let e = 0; e < 4; e++) {
104
+ const step = stitched(e) ? 2 : 1;
105
+ const lo = stitched((e + 3) % 4) ? 1 : 0;
106
+ const hi = stitched((e + 1) % 4) ? side - 1 : side;
107
+ for (let half = 0; half < 2; half++) {
108
+ const s0 = half * h;
109
+ const s1 = s0 + h;
110
+ const m = s0 + h / 2;
111
+ const q = e === 0 ? quadrant(m, 0) : e === 1 ? quadrant(side, m) : e === 2 ? quadrant(side - m, side) : quadrant(0, side - m);
112
+ if (!(mask & 1 << q)) continue;
113
+ outer.length = 0;
114
+ for (let s = s0; s <= s1; s += step) outer.push(s);
115
+ if (stitched(e)) {
116
+ inner.length = 0;
117
+ for (let s = Math.max(lo, s0); s <= Math.min(hi, s1); s++) inner.push(s);
118
+ let a = 0;
119
+ let b = 0;
120
+ while (a < outer.length - 1 || b < inner.length - 1) {
121
+ const advanceOuter = b === inner.length - 1 || a < outer.length - 1 && outer[a + 1] <= inner[b + 1];
122
+ if (advanceOuter) {
123
+ parts.push(at(e, outer[a], 0), at(e, outer[a + 1], 0), at(e, inner[b], 1));
124
+ a++;
125
+ } else {
126
+ parts.push(at(e, outer[a], 0), at(e, inner[b + 1], 1), at(e, inner[b], 1));
127
+ b++;
128
+ }
129
+ }
130
+ }
131
+ if (!skirts) continue;
132
+ for (let k = 0; k + 1 < outer.length; k++) {
133
+ const va = at(e, outer[k], 0);
134
+ const vb = at(e, outer[k + 1], 0);
135
+ const sa = layout.surface + va;
136
+ const sb = layout.surface + vb;
137
+ skirt.push(va, sa, vb, vb, sa, sb, va, vb, sa, vb, sb, sa);
138
+ }
139
+ }
140
+ }
141
+ for (const v of skirt) parts.push(v);
142
+ for (let t = 0; t < parts.length; t += 3) {
143
+ const b = parts[t + 1];
144
+ parts[t + 1] = parts[t + 2];
145
+ parts[t + 2] = b;
146
+ }
147
+ out = Uint16Array.from(parts);
148
+ sets.set(key, out);
149
+ return out;
150
+ }
151
+ function checkResolution(n) {
152
+ const k = Math.log2(n - 1);
153
+ if (!Number.isInteger(k) || k < 2 || k > 7) {
154
+ throw new ShardError("terrain/bad-resolution", `Planet resolution ${n} isn't 2^n + 1`, {
155
+ hint: "Use 17, 33 (the default), 65, or 129 vertices per chunk edge."
156
+ });
157
+ }
158
+ }
159
+ function chunkLayout(n) {
160
+ let layout = layouts.get(n);
161
+ if (layout) return layout;
162
+ checkResolution(n);
163
+ const ring = 4 * (n - 1);
164
+ const surface = n * n;
165
+ const index = new Uint16Array(surface);
166
+ const ringPoints = new Uint16Array(ring * 2);
167
+ let r = 0;
168
+ const put = (i, j) => {
169
+ index[i + j * n] = r;
170
+ ringPoints[r * 2] = i;
171
+ ringPoints[r * 2 + 1] = j;
172
+ r++;
173
+ };
174
+ for (let i = 0; i < n - 1; i++) put(i, 0);
175
+ for (let j = 0; j < n - 1; j++) put(n - 1, j);
176
+ for (let i = n - 1; i > 0; i--) put(i, n - 1);
177
+ for (let j = n - 1; j > 0; j--) put(0, j);
178
+ let v = ring;
179
+ for (let j = 1; j < n - 1; j++) for (let i = 1; i < n - 1; i++) index[i + j * n] = v++;
180
+ layout = {
181
+ resolution: n,
182
+ ring,
183
+ surface,
184
+ vertexCount: surface + ring,
185
+ index,
186
+ ringPoints,
187
+ indices: new Uint16Array(0),
188
+ surfaceIndices: new Uint16Array(0)
189
+ };
190
+ layouts.set(n, layout);
191
+ layout.indices = chunkIndices(n, 15, 0, true);
192
+ layout.surfaceIndices = chunkIndices(n, 15, 0, false);
193
+ return layout;
194
+ }
195
+ function lockCode(i, j, n, vi, ring) {
196
+ if (vi < ring) return Math.floor(vi / (n - 1));
197
+ const h = (n - 1) / 2;
198
+ if (i === h && j === h) return 8;
199
+ if (i === h) return j < h ? 4 : 5;
200
+ if (j === h) return i < h ? 6 : 7;
201
+ return -1;
202
+ }
203
+ export {
204
+ checkResolution,
205
+ chunkIndices,
206
+ chunkLayout,
207
+ lockCode
208
+ };
@@ -0,0 +1,120 @@
1
+ import { ShardError } from '@aethervtt/shard-core';
2
+ import type { Workers } from '@aethervtt/shard-platform';
3
+ import { type Stack } from './kernel.js';
4
+ import { type PackStore } from './pack.js';
5
+ import type { TerrainLayout, TerrainSource } from './source.js';
6
+ /** What a bake remembers per block: its key, errors per level above the leaves, its height range. */
7
+ export interface BlockRecord {
8
+ key: string;
9
+ /** Index L: the largest error of this block's level-L nodes (its share, above the block). 0 for leaves. */
10
+ errors: number[];
11
+ /** Quantized height range. */
12
+ range: [number, number];
13
+ /** Samples clipped by heightRange, and the unclipped extremes. */
14
+ clipped: number;
15
+ lowest: number;
16
+ highest: number;
17
+ /** Milliseconds its last bake took. */
18
+ ms: number;
19
+ }
20
+ /** `manifest.json` next to the packs: enough to rebake incrementally and to stream. */
21
+ export interface BakeManifest {
22
+ format: 1;
23
+ bakeVersion: number;
24
+ /** The grid and stored range the packs were made for; a change rebakes everything. */
25
+ grid: {
26
+ sizeX: number;
27
+ sizeZ: number;
28
+ spacing: number;
29
+ paintSpacing: number;
30
+ depth: number;
31
+ block: number;
32
+ heightRange: [number, number];
33
+ };
34
+ /** "bx,bz" → record. */
35
+ blocks: Record<string, BlockRecord>;
36
+ /** Per depth (0 roots … depth leaves): the largest node error there. */
37
+ depthErrors: number[];
38
+ /** The source hash the packs match. */
39
+ sourceHash: string;
40
+ }
41
+ export interface OutOfRange {
42
+ /** [x0, z0, x1, z1] metres. */
43
+ rect: [number, number, number, number];
44
+ samples: number;
45
+ lowest: number;
46
+ highest: number;
47
+ }
48
+ export interface BakeReport {
49
+ blocks: number;
50
+ rebaked: number;
51
+ /** Blocks this bake rebaked, as "bx,bz" (terrain.map's bake mode shades them). */
52
+ dirtied: string[];
53
+ /** Pages written (leaves and every rebuilt ancestor). */
54
+ pages: number;
55
+ /** Pages above the blocks rebuilt from their children. */
56
+ ancestors: number;
57
+ /** Pack bytes written. */
58
+ bytes: number;
59
+ ms: number;
60
+ /** Mean milliseconds per rebaked block (pool time, not wall time). */
61
+ msPerBlock: number;
62
+ outOfRange: OutOfRange[];
63
+ }
64
+ export interface BakeOptions {
65
+ /** Blocks bake here; inline (on the calling thread) without one. */
66
+ workers?: Workers;
67
+ /** Rebake every block. */
68
+ force?: boolean;
69
+ /** Problems worth a warning (terrain/out-of-range, once per block). */
70
+ warn?: (problem: ShardError) => void;
71
+ onProgress?: (done: number, total: number) => void;
72
+ }
73
+ /** What a bake reads of a terrain source asset (TerrainSourceAsset). */
74
+ export interface BakeInput {
75
+ source: TerrainSource;
76
+ layout: TerrainLayout;
77
+ deps: Record<string, {
78
+ hash: string;
79
+ }>;
80
+ hash: string;
81
+ }
82
+ export declare function readManifest(store: PackStore): Promise<BakeManifest | undefined>;
83
+ /**
84
+ * Each block's key (spec 0071): SHA-256 of the bake version, the grid, the seed, and every layer
85
+ * (height and paint) whose region plus falloff touches the block's samples, with its dependency's
86
+ * hash and, for splines, its resolved centerline. Layers elsewhere don't change it.
87
+ */
88
+ export declare function blockKeys(input: BakeInput, stack: Stack): Promise<string[]>;
89
+ /**
90
+ * Bakes a terrain source into page packs (spec 0071), incrementally: blocks whose key changed are
91
+ * rebaked (on the pool when there is one), every ancestor of a rebaked block is rebuilt from its
92
+ * children, and only the packs holding changed pages are rewritten. A changed grid rebakes it all.
93
+ */
94
+ export declare function bakeTerrain(input: BakeInput, stack: Stack, store: PackStore, options?: BakeOptions): Promise<BakeReport>;
95
+ export interface TerrainStats {
96
+ baked: boolean;
97
+ /** The packs match the source. */
98
+ current: boolean;
99
+ size: [number, number];
100
+ spacing: number;
101
+ roots: number;
102
+ depth: number;
103
+ blocks: number;
104
+ pages: number;
105
+ /** Page bytes before deflate, and every file's bytes on disk. */
106
+ rawBytes: number;
107
+ bytesOnDisk: number;
108
+ /** Per leaf sample. */
109
+ bytesPerSample: number;
110
+ diskBytesPerSample: number;
111
+ msPerBlock: number;
112
+ slowestBlockMs: number;
113
+ }
114
+ /** What `shard terrain stats` prints: size on disk, pages, bytes per sample, time per block. */
115
+ export declare function terrainStats(input: BakeInput, store: PackStore, manifest: BakeManifest | undefined): Promise<TerrainStats>;
116
+ /**
117
+ * SHA-256 over every pack (path and bytes, sorted by path; not the manifest, which records
118
+ * timings): equal hashes mean byte-identical bakes, as Node's and Chrome's must be (0071).
119
+ */
120
+ export declare function packHash(files: ReadonlyMap<string, Uint8Array>): Promise<string>;