@threenative/terrain 0.0.0-stage → 0.1.2

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 (125) hide show
  1. package/AGENT_GUIDE.md +194 -0
  2. package/LICENSE +21 -0
  3. package/README.md +71 -2
  4. package/THIRD_PARTY_NOTICES.md +38 -0
  5. package/dist/editor/index.d.ts +231 -0
  6. package/dist/editor/index.js +5624 -0
  7. package/dist/editor/server.d.ts +3 -0
  8. package/dist/editor/server.js +3688 -0
  9. package/dist/editor/worker.d.ts +2 -0
  10. package/dist/editor/worker.js +2212 -0
  11. package/dist/export.d.ts +54 -0
  12. package/dist/export.js +297 -0
  13. package/dist/index.d.ts +335 -0
  14. package/dist/index.js +2927 -0
  15. package/dist/server-ByM2hmYJ.d.ts +630 -0
  16. package/dist/terrain-gPnkCU6S.d.ts +141 -0
  17. package/dist/three.d.ts +14 -0
  18. package/dist/three.js +40 -0
  19. package/dist/types-Ant8-Axc.d.ts +486 -0
  20. package/package.json +86 -3
  21. package/starter/alpine/README.md +29 -0
  22. package/starter/alpine/assets.json +12 -0
  23. package/starter/alpine/bake.mjs +118 -0
  24. package/starter/alpine/recipe.json +66167 -0
  25. package/starter/alpine/sky.ts +23 -0
  26. package/starter/alpine/surface.json +53 -0
  27. package/starter/alpine/world.ts +335 -0
  28. package/starter/coastal/README.md +37 -0
  29. package/starter/coastal/assets.json +10 -0
  30. package/starter/coastal/bake.mjs +118 -0
  31. package/starter/coastal/recipe.json +66161 -0
  32. package/starter/coastal/sea.ts +327 -0
  33. package/starter/coastal/sky.ts +24 -0
  34. package/starter/coastal/surface.json +41 -0
  35. package/starter/coastal/world.ts +386 -0
  36. package/starter/desert/README.md +35 -0
  37. package/starter/desert/assets.json +12 -0
  38. package/starter/desert/bake.mjs +118 -0
  39. package/starter/desert/recipe.json +66156 -0
  40. package/starter/desert/sky.ts +23 -0
  41. package/starter/desert/surface.json +54 -0
  42. package/starter/desert/world.ts +335 -0
  43. package/starter/forest/README.md +46 -0
  44. package/starter/forest/assets.json +32 -0
  45. package/starter/forest/bake.mjs +118 -0
  46. package/starter/forest/recipe.json +4586 -0
  47. package/starter/forest/sky.ts +27 -0
  48. package/starter/forest/surface.json +79 -0
  49. package/starter/forest/water.ts +266 -0
  50. package/starter/forest/world.ts +413 -0
  51. package/starter/tundra/README.md +29 -0
  52. package/starter/tundra/assets.json +12 -0
  53. package/starter/tundra/bake.mjs +118 -0
  54. package/starter/tundra/recipe.json +66325 -0
  55. package/starter/tundra/sky.ts +27 -0
  56. package/starter/tundra/surface.json +54 -0
  57. package/starter/tundra/water.ts +313 -0
  58. package/starter/tundra/world.ts +366 -0
  59. package/starter-assets/bark_brown_02/bark_brown_02_diff_1k.jpg +0 -0
  60. package/starter-assets/bark_brown_02/bark_brown_02_nor_gl_1k.jpg +0 -0
  61. package/starter-assets/bark_brown_02/bark_brown_02_rough_1k.jpg +0 -0
  62. package/starter-assets/boulder_01/boulder-mid.glb +0 -0
  63. package/starter-assets/boulder_01/boulder-near.glb +0 -0
  64. package/starter-assets/cliff_side/cliff_side_ao_1k.jpg +0 -0
  65. package/starter-assets/cliff_side/cliff_side_diff_1k.jpg +0 -0
  66. package/starter-assets/cliff_side/cliff_side_nor_gl_1k.jpg +0 -0
  67. package/starter-assets/cliff_side/cliff_side_rough_1k.jpg +0 -0
  68. package/starter-assets/credits.json +1022 -0
  69. package/starter-assets/fern_02/fern_02_alpha_512.png +0 -0
  70. package/starter-assets/fern_02/fern_02_diff_512.jpg +0 -0
  71. package/starter-assets/fern_02/fern_02_nor_gl_512.jpg +0 -0
  72. package/starter-assets/fir_tree_01/fir-b-lod1.glb +0 -0
  73. package/starter-assets/fir_tree_01/fir-b-lod2.glb +0 -0
  74. package/starter-assets/fir_tree_01/fir-c-lod1.glb +0 -0
  75. package/starter-assets/fir_tree_01/fir-c-lod2.glb +0 -0
  76. package/starter-assets/fir_tree_01/fir_tree_01_twig_arm_1k.jpg +0 -0
  77. package/starter-assets/fir_tree_01/fir_tree_01_twig_diff_1k.jpg +0 -0
  78. package/starter-assets/fir_tree_01/fir_tree_01_twig_nor_gl_1k.jpg +0 -0
  79. package/starter-assets/forest_ground_04/forest_ground_04_ao_1k.jpg +0 -0
  80. package/starter-assets/forest_ground_04/forest_ground_04_diff_1k.jpg +0 -0
  81. package/starter-assets/forest_ground_04/forest_ground_04_nor_gl_1k.jpg +0 -0
  82. package/starter-assets/forest_ground_04/forest_ground_04_rough_1k.jpg +0 -0
  83. package/starter-assets/forest_leaves_02/forest_leaves_02_arm_512.jpg +0 -0
  84. package/starter-assets/forest_leaves_02/forest_leaves_02_diff_512.jpg +0 -0
  85. package/starter-assets/forest_leaves_02/forest_leaves_02_nor_gl_512.jpg +0 -0
  86. package/starter-assets/grass_medium_02/grass_medium_02_alpha_512.png +0 -0
  87. package/starter-assets/grass_medium_02/grass_medium_02_diff_512.jpg +0 -0
  88. package/starter-assets/grass_medium_02/grass_medium_02_nor_gl_512.jpg +0 -0
  89. package/starter-assets/hdri/kloofendal_48d_partly_cloudy_1k.hdr +0 -0
  90. package/starter-assets/leafy_grass/leafy_grass_ao_1k.jpg +0 -0
  91. package/starter-assets/leafy_grass/leafy_grass_diff_1k.jpg +0 -0
  92. package/starter-assets/leafy_grass/leafy_grass_nor_gl_1k.jpg +0 -0
  93. package/starter-assets/leafy_grass/leafy_grass_rough_1k.jpg +0 -0
  94. package/starter-assets/lichen_rock/lichen_rock_arm_512.jpg +0 -0
  95. package/starter-assets/lichen_rock/lichen_rock_diff_512.jpg +0 -0
  96. package/starter-assets/lichen_rock/lichen_rock_nor_gl_512.jpg +0 -0
  97. package/starter-assets/mossy_rock/mossy_rock_ao_1k.jpg +0 -0
  98. package/starter-assets/mossy_rock/mossy_rock_diff_1k.jpg +0 -0
  99. package/starter-assets/mossy_rock/mossy_rock_nor_gl_1k.jpg +0 -0
  100. package/starter-assets/mossy_rock/mossy_rock_rough_1k.jpg +0 -0
  101. package/starter-assets/needle-atlas.png +0 -0
  102. package/starter-assets/needle-surface.png +0 -0
  103. package/starter-assets/river_small_rocks/river_small_rocks_arm_512.jpg +0 -0
  104. package/starter-assets/river_small_rocks/river_small_rocks_diff_512.jpg +0 -0
  105. package/starter-assets/river_small_rocks/river_small_rocks_nor_gl_512.jpg +0 -0
  106. package/starter-assets/rock_boulder_dry/rock_boulder_dry_arm_512.jpg +0 -0
  107. package/starter-assets/rock_boulder_dry/rock_boulder_dry_diff_512.jpg +0 -0
  108. package/starter-assets/rock_boulder_dry/rock_boulder_dry_nor_gl_512.jpg +0 -0
  109. package/starter-assets/rocks/boulder-mid.glb +0 -0
  110. package/starter-assets/rocks/boulder-near.glb +0 -0
  111. package/starter-assets/rocks/boulder-prep.json +37 -0
  112. package/starter-assets/rocks/rock01-mid.glb +0 -0
  113. package/starter-assets/rocks/rock01-near.glb +0 -0
  114. package/starter-assets/rocks/rock01-prep.json +37 -0
  115. package/starter-assets/rocks/rock04-mid.glb +0 -0
  116. package/starter-assets/rocks/rock04-near.glb +0 -0
  117. package/starter-assets/rocks/rock04-prep.json +37 -0
  118. package/starter-assets/sand_01/sand_01_ao_1k.jpg +0 -0
  119. package/starter-assets/sand_01/sand_01_diff_1k.jpg +0 -0
  120. package/starter-assets/sand_01/sand_01_nor_gl_1k.jpg +0 -0
  121. package/starter-assets/sand_01/sand_01_rough_1k.jpg +0 -0
  122. package/starter-assets/snow_02/snow_02_ao_1k.jpg +0 -0
  123. package/starter-assets/snow_02/snow_02_diff_1k.jpg +0 -0
  124. package/starter-assets/snow_02/snow_02_nor_gl_1k.jpg +0 -0
  125. package/starter-assets/snow_02/snow_02_rough_1k.jpg +0 -0
package/dist/index.js ADDED
@@ -0,0 +1,2927 @@
1
+ // src/core/math.ts
2
+ var clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
3
+ var lerp = (a, b, t) => a + (b - a) * t;
4
+ var smoothstep = (a, b, x) => {
5
+ if (a === b) return x >= b ? 1 : 0;
6
+ const t = clamp((x - a) / (b - a));
7
+ return t * t * (3 - 2 * t);
8
+ };
9
+ var hashString = (text) => {
10
+ let h = 2166136261;
11
+ for (const c of text) h = Math.imul(h ^ c.charCodeAt(0), 16777619);
12
+ return h >>> 0;
13
+ };
14
+ function random(seed) {
15
+ let a = seed >>> 0;
16
+ return () => {
17
+ a |= 0;
18
+ a = a + 1831565813 | 0;
19
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
20
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
21
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
22
+ };
23
+ }
24
+ var hash2 = (x, z, seed) => {
25
+ let n = Math.imul(x | 0, 374761393) ^ Math.imul(z | 0, 668265263) ^ seed;
26
+ n = Math.imul(n ^ n >>> 13, 1274126177);
27
+ return (n ^ n >>> 16) >>> 0;
28
+ };
29
+ var diagonalGradient = 0.7071;
30
+ var grad = (h, x, z) => {
31
+ switch (h & 7) {
32
+ case 0:
33
+ return x;
34
+ case 1:
35
+ return -x;
36
+ case 2:
37
+ return z;
38
+ case 3:
39
+ return -z;
40
+ case 4:
41
+ return (x + z) * diagonalGradient;
42
+ case 5:
43
+ return (x - z) * diagonalGradient;
44
+ case 6:
45
+ return (-x + z) * diagonalGradient;
46
+ default:
47
+ return (-x - z) * diagonalGradient;
48
+ }
49
+ };
50
+ function noise2(x, z, seed = 1) {
51
+ const ix = Math.floor(x);
52
+ const iz = Math.floor(z);
53
+ const fx = x - ix;
54
+ const fz = z - iz;
55
+ const u = fx * fx * fx * (fx * (fx * 6 - 15) + 10);
56
+ const v = fz * fz * fz * (fz * (fz * 6 - 15) + 10);
57
+ return lerp(
58
+ lerp(grad(hash2(ix, iz, seed), fx, fz), grad(hash2(ix + 1, iz, seed), fx - 1, fz), u),
59
+ lerp(
60
+ grad(hash2(ix, iz + 1, seed), fx, fz - 1),
61
+ grad(hash2(ix + 1, iz + 1, seed), fx - 1, fz - 1),
62
+ u
63
+ ),
64
+ v
65
+ ) * 1.65;
66
+ }
67
+ function fbm(inputX, inputZ, { seed = 1, octaves = 5, persistence = 0.5, lacunarity = 2, mode = "fbm" } = {}) {
68
+ let x = inputX;
69
+ let z = inputZ;
70
+ let value = 0;
71
+ let amplitude = 1;
72
+ let total = 0;
73
+ for (let i = 0; i < octaves; i += 1) {
74
+ let n = noise2(x + 13.17, z - 8.71, seed + i * 71);
75
+ if (mode === "ridged") n = (1 - Math.abs(n)) ** 2 * 2 - 1;
76
+ if (mode === "billow") n = Math.abs(n) * 2 - 1;
77
+ value += n * amplitude;
78
+ total += amplitude;
79
+ amplitude *= persistence;
80
+ x *= lacunarity;
81
+ z *= lacunarity;
82
+ }
83
+ return total ? value / total : 0;
84
+ }
85
+ var world = (grid, x) => x / (grid.resolution - 1) * grid.size - grid.size / 2;
86
+ function sampleGrid(a, width, height, u, v) {
87
+ const gx = clamp(u, 0, 1) * (width - 1);
88
+ const gz = clamp(v, 0, 1) * (height - 1);
89
+ const x = Math.min(width - 2, Math.floor(gx));
90
+ const z = Math.min(height - 2, Math.floor(gz));
91
+ const tx = gx - x;
92
+ const tz = gz - z;
93
+ return lerp(
94
+ lerp(a[z * width + x], a[z * width + x + 1], tx),
95
+ lerp(a[(z + 1) * width + x], a[(z + 1) * width + x + 1], tx),
96
+ tz
97
+ );
98
+ }
99
+ var sampleHeight = (grid, x, z) => sampleGrid(
100
+ grid.height,
101
+ grid.resolution,
102
+ grid.resolution,
103
+ x / grid.size + 0.5,
104
+ z / grid.size + 0.5
105
+ );
106
+ function gradientAt(grid, x, z) {
107
+ const d = grid.size / (grid.resolution - 1);
108
+ return [
109
+ (sampleHeight(grid, x + d, z) - sampleHeight(grid, x - d, z)) / (Math.min(grid.size / 2, x + d) - Math.max(-grid.size / 2, x - d) || d),
110
+ (sampleHeight(grid, x, z + d) - sampleHeight(grid, x, z - d)) / (Math.min(grid.size / 2, z + d) - Math.max(-grid.size / 2, z - d) || d)
111
+ ];
112
+ }
113
+ function slopeAtIndex(grid, i) {
114
+ const n = grid.resolution;
115
+ const x = i % n;
116
+ const z = Math.floor(i / n);
117
+ const dx = x > 0 && x < n - 1 ? 2 : 1;
118
+ const dz = z > 0 && z < n - 1 ? 2 : 1;
119
+ const cell = grid.size / (n - 1);
120
+ const gx = (grid.height[z * n + Math.min(x + 1, n - 1)] - grid.height[z * n + Math.max(x - 1, 0)]) / (dx * cell);
121
+ const gz = (grid.height[Math.min(z + 1, n - 1) * n + x] - grid.height[Math.max(z - 1, 0) * n + x]) / (dz * cell);
122
+ return Math.atan(Math.hypot(gx, gz)) * (180 / Math.PI);
123
+ }
124
+ function slopeQuantile(grid, quantile) {
125
+ if (!(quantile >= 0 && quantile <= 1))
126
+ throw new Error(
127
+ `TN_TERRAIN_SLOPE_QUANTILE_INVALID: quantile must be in [0, 1], received ${String(quantile)}.`
128
+ );
129
+ const n = grid.resolution;
130
+ const slopes = [];
131
+ for (let z = 1; z < n - 1; z++)
132
+ for (let x = 1; x < n - 1; x++) slopes.push(slopeAtIndex(grid, z * n + x));
133
+ if (slopes.length === 0) return 0;
134
+ slopes.sort((a, b) => a - b);
135
+ return slopes[Math.min(slopes.length - 1, Math.floor(slopes.length * quantile))];
136
+ }
137
+ function falloff(distance, radius, softness = 0.6) {
138
+ const d = distance / radius;
139
+ return d >= 1 ? 0 : 1 - smoothstep(1 - clamp(softness, 1e-5, 1), 1, d);
140
+ }
141
+ function splinePoints(points, step = 3, smooth = true) {
142
+ const out = [];
143
+ for (let k = 0; k < points.length - 1; k += 1) {
144
+ const a = points[Math.max(0, k - 1)];
145
+ const b = points[k];
146
+ const c = points[k + 1];
147
+ const d = points[Math.min(points.length - 1, k + 2)];
148
+ const count = Math.max(
149
+ 1,
150
+ Math.min(512, Math.ceil(Math.hypot(c[0] - b[0], c[2] - b[2]) / step))
151
+ );
152
+ for (let j = 0; j < count; j += 1) {
153
+ const t = j / count;
154
+ const t2 = t * t;
155
+ const t3 = t2 * t;
156
+ const cat = (dim) => 0.5 * (2 * b[dim] + (-a[dim] + c[dim]) * t + (2 * a[dim] - 5 * b[dim] + 4 * c[dim] - d[dim]) * t2 + (-a[dim] + 3 * b[dim] - 3 * c[dim] + d[dim]) * t3);
157
+ out.push([
158
+ smooth ? cat(0) : lerp(b[0], c[0], t),
159
+ lerp(b[1], c[1], t),
160
+ smooth ? cat(2) : lerp(b[2], c[2], t)
161
+ ]);
162
+ }
163
+ }
164
+ const last = points[points.length - 1];
165
+ out.push([last[0], last[1], last[2]]);
166
+ return out;
167
+ }
168
+
169
+ // src/core/erosion.ts
170
+ function thermal(height, n, size, { iterations = 20, talus = 32, rate = 0.22 } = {}, observations) {
171
+ const h = height.slice();
172
+ const delta = new Float64Array(h.length);
173
+ const limit = Math.tan(talus * Math.PI / 180) * (size / (n - 1));
174
+ for (let k = 0; k < iterations; k += 1) {
175
+ delta.fill(0);
176
+ for (let z = 0; z < n; z += 1) {
177
+ for (let x = 0; x < n; x += 1) {
178
+ const i = z * n + x;
179
+ let best = -1;
180
+ let diff = limit;
181
+ for (const j of [
182
+ x > 0 ? i - 1 : -1,
183
+ x < n - 1 ? i + 1 : -1,
184
+ z > 0 ? i - n : -1,
185
+ z < n - 1 ? i + n : -1
186
+ ]) {
187
+ if (j >= 0 && h[i] - h[j] > diff) {
188
+ diff = h[i] - h[j];
189
+ best = j;
190
+ }
191
+ }
192
+ if (best >= 0) {
193
+ const transfer = (diff - limit) * rate;
194
+ delta[i] = delta[i] - transfer;
195
+ delta[best] = delta[best] + transfer;
196
+ if (observations)
197
+ observations.talus[best] = observations.talus[best] + transfer;
198
+ }
199
+ }
200
+ }
201
+ for (let i = 0; i < h.length; i += 1) h[i] = h[i] + delta[i];
202
+ }
203
+ return h;
204
+ }
205
+ var brushes = /* @__PURE__ */ new Map();
206
+ function brush(radius) {
207
+ let cells = brushes.get(radius);
208
+ if (!cells) {
209
+ cells = [];
210
+ let total = 0;
211
+ for (let dz = -radius; dz <= radius; dz += 1) {
212
+ for (let dx = -radius; dx <= radius; dx += 1) {
213
+ const d = Math.hypot(dx, dz);
214
+ if (d > radius) continue;
215
+ const weight = 1 - d / (radius + 1);
216
+ cells.push({ dx, dz, weight });
217
+ total += weight;
218
+ }
219
+ }
220
+ for (const c of cells) c.weight /= total;
221
+ brushes.set(radius, cells);
222
+ }
223
+ return cells;
224
+ }
225
+ function hydraulic(height, n, size, {
226
+ // Roughly one droplet per cell. Incision saturates near here: more droplets keep cutting the
227
+ // same drainage lines over, which deepens nothing and spends the bake time.
228
+ droplets = n * n,
229
+ // A droplet has to be able to cross the world, so its step budget scales with the grid: a
230
+ // fixed 40 barely leaves a 257 world while a 65 world would walk off it in a few steps.
231
+ maxSteps = Math.round(Math.min(64, Math.max(24, n / 4))),
232
+ inertia = 0.2,
233
+ capacity = 4,
234
+ // Each step takes a small bite of the sediment the droplet can carry. A big bite carves
235
+ // fewer, deeper dimples that stand proud of their neighbours as spikes; many shallow bites
236
+ // over the same lines incise the same drainage with a smooth surface.
237
+ erosion = 0.03,
238
+ deposition = 0.3,
239
+ evaporation = 0.025,
240
+ seed = 1,
241
+ brushRadius = 3
242
+ } = {}, observations) {
243
+ const h = Float64Array.from(height);
244
+ const rnd = random(seed);
245
+ const cell = size / (n - 1);
246
+ const cells = brush(brushRadius);
247
+ const spread = (x, z, amount, floor = Number.NEGATIVE_INFINITY) => {
248
+ let total = 0;
249
+ for (const c of cells) {
250
+ const ix = x + c.dx;
251
+ const iz = z + c.dz;
252
+ if (ix >= 0 && ix < n && iz >= 0 && iz < n) total += c.weight;
253
+ }
254
+ if (total <= 0) return 0;
255
+ let moved = 0;
256
+ for (const c of cells) {
257
+ const ix = x + c.dx;
258
+ const iz = z + c.dz;
259
+ if (ix < 0 || ix >= n || iz < 0 || iz >= n) continue;
260
+ const i = iz * n + ix;
261
+ const share = amount * c.weight / total;
262
+ const change = amount < 0 ? -Math.min(-share, Math.max(0, h[i] - floor)) : share;
263
+ h[i] = h[i] + change;
264
+ moved += change;
265
+ if (observations && amount > 0)
266
+ observations.deposition[i] = observations.deposition[i] + amount * c.weight / total;
267
+ }
268
+ return moved;
269
+ };
270
+ const get = (x, z) => {
271
+ const ix = Math.min(n - 2, Math.floor(x));
272
+ const iz = Math.min(n - 2, Math.floor(z));
273
+ const tx = x - ix;
274
+ const tz = z - iz;
275
+ const i = iz * n + ix;
276
+ return {
277
+ value: lerp(
278
+ lerp(h[i], h[i + 1], tx),
279
+ lerp(h[i + n], h[i + n + 1], tx),
280
+ tz
281
+ ),
282
+ dx: lerp(
283
+ h[i + 1] - h[i],
284
+ h[i + n + 1] - h[i + n],
285
+ tz
286
+ ) / cell,
287
+ dz: lerp(
288
+ h[i + n] - h[i],
289
+ h[i + n + 1] - h[i + 1],
290
+ tx
291
+ ) / cell,
292
+ ix,
293
+ iz
294
+ };
295
+ };
296
+ for (let k = 0; k < droplets; k += 1) {
297
+ let x = rnd() * (n - 1 - 1e-3);
298
+ let z = rnd() * (n - 1 - 1e-3);
299
+ let dx = 0;
300
+ let dz = 0;
301
+ let water = 1;
302
+ let speed = 1;
303
+ let sediment = 0;
304
+ for (let step = 0; step < maxSteps; step += 1) {
305
+ const old = get(x, z);
306
+ if (observations) {
307
+ const tx = x - old.ix;
308
+ const tz = z - old.iz;
309
+ const i = old.iz * n + old.ix;
310
+ for (let oz = 0; oz < 2; oz++) {
311
+ for (let ox = 0; ox < 2; ox++) {
312
+ const index = i + oz * n + ox;
313
+ const weight = (ox ? tx : 1 - tx) * (oz ? tz : 1 - tz);
314
+ observations.flow[index] = observations.flow[index] + water * weight;
315
+ observations.sediment[index] = observations.sediment[index] + sediment * weight;
316
+ }
317
+ }
318
+ }
319
+ dx = dx * inertia - old.dx * (1 - inertia);
320
+ dz = dz * inertia - old.dz * (1 - inertia);
321
+ let length = Math.hypot(dx, dz);
322
+ if (length < 1e-9) {
323
+ const angle = rnd() * Math.PI * 2;
324
+ dx = Math.cos(angle);
325
+ dz = Math.sin(angle);
326
+ length = 1;
327
+ }
328
+ dx /= length;
329
+ dz /= length;
330
+ const nx = x + dx;
331
+ const nz = z + dz;
332
+ if (nx < 0 || nx >= n - 1 || nz < 0 || nz >= n - 1) {
333
+ sediment = 0;
334
+ break;
335
+ }
336
+ const next = get(nx, nz);
337
+ const dh = next.value - old.value;
338
+ const cap = Math.max(-dh, 5e-3 * cell) * speed * water * capacity;
339
+ if (dh > 0 || sediment > cap) {
340
+ const amount = dh > 0 ? Math.min(dh, sediment) : (sediment - cap) * deposition;
341
+ spread(old.ix, old.iz, amount);
342
+ sediment -= amount;
343
+ } else {
344
+ const amount = Math.max(0, Math.min((cap - sediment) * erosion, -dh));
345
+ sediment -= spread(old.ix, old.iz, -amount, next.value);
346
+ }
347
+ speed = Math.sqrt(Math.max(0.01, speed * speed - dh * 3));
348
+ water *= 1 - evaporation;
349
+ x = nx;
350
+ z = nz;
351
+ if (water < 0.02) break;
352
+ }
353
+ if (sediment > 0 && water < 0.02) {
354
+ const end = get(clamp(x, 0, n - 1.001), clamp(z, 0, n - 1.001));
355
+ spread(end.ix, end.iz, sediment);
356
+ }
357
+ }
358
+ return Float32Array.from(h);
359
+ }
360
+
361
+ // src/core/masks.ts
362
+ var MATERIAL_IDS = Object.freeze([
363
+ "grass",
364
+ "dirt",
365
+ "rock",
366
+ "snow",
367
+ "sand",
368
+ "mud",
369
+ "road",
370
+ "moss"
371
+ ]);
372
+ var Mask = Object.freeze({
373
+ all: () => ({ type: "all" }),
374
+ none: () => ({ type: "none" }),
375
+ circle: (at, radius, softness = 0.6) => ({
376
+ type: "circle",
377
+ at,
378
+ radius,
379
+ falloff: softness
380
+ }),
381
+ rectangle: (at, size, rotation = 0, softness = 0.2) => ({ type: "rectangle", at, size, rotation, falloff: softness }),
382
+ height: (min = -1e9, max = 1e9, fade = 0) => ({ type: "height", min, max, fade }),
383
+ slope: (min = 0, max = 90, fade = 0) => ({ type: "slope", min, max, fade }),
384
+ noise: (scale = 80, threshold = 0.5, seed = 1, fade = 0.15) => ({
385
+ type: "noise",
386
+ scale,
387
+ threshold,
388
+ seed,
389
+ fade
390
+ }),
391
+ biome: (name) => ({ type: "biome", name }),
392
+ material: (name) => ({ type: "material", name }),
393
+ and: (...masks) => ({ type: "and", masks }),
394
+ or: (...masks) => ({ type: "or", masks }),
395
+ not: (mask) => ({ type: "not", mask })
396
+ });
397
+ var range = (v, min, max, fade = 0) => fade > 0 ? smoothstep(min - fade, min, v) * (1 - smoothstep(max, max + fade, v)) : Number(v >= min && v <= max);
398
+ function compileMask(mask, s) {
399
+ if (!mask || mask.type === "all") return () => 1;
400
+ switch (mask.type) {
401
+ case "none":
402
+ return () => 0;
403
+ case "circle": {
404
+ const centre = mask.at;
405
+ return (i, x, z) => falloff(
406
+ Math.hypot(x - centre[0], z - centre[1]),
407
+ mask.radius,
408
+ mask.falloff ?? 0.6
409
+ );
410
+ }
411
+ case "rectangle": {
412
+ const centre = mask.at;
413
+ const rad = (mask.rotation ?? 0) * Math.PI / 180;
414
+ const c = Math.cos(rad);
415
+ const sn = Math.sin(rad);
416
+ const size = Array.isArray(mask.size) ? mask.size : [mask.size, mask.size];
417
+ return (i, x, z) => {
418
+ const dx = x - centre[0];
419
+ const dz = z - centre[1];
420
+ return falloff(
421
+ Math.max(
422
+ Math.abs(dx * c + dz * sn) / (size[0] / 2),
423
+ Math.abs(-dx * sn + dz * c) / (size[1] / 2)
424
+ ),
425
+ 1,
426
+ mask.falloff ?? 0.2
427
+ );
428
+ };
429
+ }
430
+ case "height":
431
+ return (i) => range(s.height[i], mask.min ?? -1e9, mask.max ?? 1e9, mask.fade);
432
+ case "slope":
433
+ return (i) => range(slopeAtIndex(s, i), mask.min ?? 0, mask.max ?? 90, mask.fade);
434
+ case "noise":
435
+ return (i, x, z) => smoothstep(
436
+ (mask.threshold ?? 0.5) - (mask.fade ?? 0.15),
437
+ (mask.threshold ?? 0.5) + (mask.fade ?? 0.15),
438
+ noise2(x / mask.scale, z / mask.scale, mask.seed ?? s.seed) * 0.5 + 0.5
439
+ );
440
+ case "biome": {
441
+ const field = s.biomes[mask.name];
442
+ return (i) => field?.[i] ?? 0;
443
+ }
444
+ case "material": {
445
+ const channel = MATERIAL_IDS.indexOf(mask.name);
446
+ if (channel < 0) throw Error(`Unknown material: ${String(mask.name)}`);
447
+ return (i) => s.splat[i * 8 + channel];
448
+ }
449
+ case "not": {
450
+ const inner = compileMask(mask.mask, s);
451
+ return (i, x, z) => 1 - inner(i, x, z);
452
+ }
453
+ case "and": {
454
+ const inner = (mask.masks ?? []).map((entry) => compileMask(entry, s));
455
+ return (i, x, z) => inner.reduce((value, sample) => value * sample(i, x, z), 1);
456
+ }
457
+ case "or": {
458
+ const inner = (mask.masks ?? []).map((entry) => compileMask(entry, s));
459
+ return (i, x, z) => 1 - inner.reduce((value, sample) => value * (1 - sample(i, x, z)), 1);
460
+ }
461
+ default:
462
+ throw Error(`Unknown mask: ${String(mask.type)}`);
463
+ }
464
+ }
465
+ function rasterMask(mask, s) {
466
+ if (!mask || mask.type === "all") return null;
467
+ const out = new Float32Array(s.height.length);
468
+ const sample = compileMask(mask, s);
469
+ for (let z = 0; z < s.resolution; z += 1) {
470
+ for (let x = 0; x < s.resolution; x += 1) {
471
+ const i = z * s.resolution + x;
472
+ out[i] = clamp(sample(i, world(s, x), world(s, z)));
473
+ }
474
+ }
475
+ return out;
476
+ }
477
+ function paintWeights(s, i, channel, weight) {
478
+ const w = clamp(weight);
479
+ const base = i * 8;
480
+ for (let c = 0; c < 8; c += 1)
481
+ s.splat[base + c] = s.splat[base + c] * (1 - w) + (c === channel ? w : 0);
482
+ }
483
+
484
+ // src/core/operations.ts
485
+ var OPERATION_TYPES = Object.freeze([
486
+ "noise",
487
+ "sculpt",
488
+ "smooth",
489
+ "flatten",
490
+ "ramp",
491
+ "stamp",
492
+ "erode",
493
+ "terrace",
494
+ "materials",
495
+ "paint",
496
+ "biome",
497
+ "scatter",
498
+ "clear",
499
+ "road",
500
+ "river",
501
+ "water",
502
+ "heightmap",
503
+ "paste"
504
+ ]);
505
+ function createState(config, resolution = config.resolution) {
506
+ const height = new Float32Array(resolution * resolution);
507
+ const splat = new Float32Array(height.length * 8);
508
+ for (let i = 0; i < height.length; i += 1) splat[i * 8] = 1;
509
+ return {
510
+ size: config.size,
511
+ resolution,
512
+ seed: config.seed,
513
+ height,
514
+ splat,
515
+ biomes: {},
516
+ scatterRules: [],
517
+ waterRules: [],
518
+ rivers: [],
519
+ instances: [],
520
+ waters: [],
521
+ diagnostics: []
522
+ };
523
+ }
524
+ function brushCenters(p, seed) {
525
+ const source = p.points ?? (p.at ? [p.at] : null);
526
+ if (!source) return null;
527
+ const radius = p.radius ?? 30;
528
+ const first = source[0];
529
+ if (!first) return null;
530
+ const out = [[first[0], first[1]]];
531
+ const step = Math.max(0.1, radius * 2 * (p.spacing ?? 0.15));
532
+ for (let k = 1; k < source.length; k += 1) {
533
+ const a = source[k - 1];
534
+ const b = source[k];
535
+ const count = Math.max(1, Math.ceil(Math.hypot(b[0] - a[0], b[1] - a[1]) / step));
536
+ for (let j = 1; j <= count; j += 1)
537
+ out.push([lerp(a[0], b[0], j / count), lerp(a[1], b[1], j / count)]);
538
+ }
539
+ if (p.jitter) {
540
+ const rnd = random(seed);
541
+ for (const centre of out) {
542
+ const angle = rnd() * Math.PI * 2;
543
+ const offset = Math.sqrt(rnd()) * p.jitter * radius;
544
+ centre[0] += Math.cos(angle) * offset;
545
+ centre[1] += Math.sin(angle) * offset;
546
+ }
547
+ }
548
+ return out;
549
+ }
550
+ function brushWeights(s, p, mask, seed) {
551
+ const centers = brushCenters(p, seed);
552
+ if (!centers) return mask ?? new Float32Array(s.height.length).fill(1);
553
+ const n = s.resolution;
554
+ const r = p.radius ?? 30;
555
+ const cell = s.size / (n - 1);
556
+ const weights = new Float32Array(n * n);
557
+ const angle = (p.rotation ?? 0) * Math.PI / 180;
558
+ const cos = Math.cos(angle);
559
+ const sin = Math.sin(angle);
560
+ const bound = r * (p.shape === "square" ? Math.SQRT2 : 1);
561
+ for (const [cx, cz] of centers) {
562
+ const x0 = clamp(Math.floor((cx - bound + s.size / 2) / cell), 0, n - 1);
563
+ const x1 = clamp(Math.ceil((cx + bound + s.size / 2) / cell), 0, n - 1);
564
+ const z0 = clamp(Math.floor((cz - bound + s.size / 2) / cell), 0, n - 1);
565
+ const z1 = clamp(Math.ceil((cz + bound + s.size / 2) / cell), 0, n - 1);
566
+ for (let z = z0; z <= z1; z += 1) {
567
+ for (let x = x0; x <= x1; x += 1) {
568
+ const dx = world(s, x) - cx;
569
+ const dz = world(s, z) - cz;
570
+ const distance = p.shape === "square" ? Math.max(Math.abs(dx * cos + dz * sin), Math.abs(-dx * sin + dz * cos)) : Math.hypot(dx, dz);
571
+ const i = z * n + x;
572
+ weights[i] = Math.max(
573
+ weights[i],
574
+ falloff(distance, r, p.falloff ?? 0.6) * (mask?.[i] ?? 1)
575
+ );
576
+ }
577
+ }
578
+ }
579
+ return weights;
580
+ }
581
+ function followsTerrain(source, followTerrain) {
582
+ return followTerrain ?? source.some((point) => !Number.isFinite(point[1]));
583
+ }
584
+ function gradeToGround(s, points, p) {
585
+ const at = (k) => points[k];
586
+ const run = (k) => {
587
+ const a = at(k - 1);
588
+ const b = at(k);
589
+ return Math.hypot(b[0] - a[0], b[2] - a[2]);
590
+ };
591
+ const bed = points.map(([x, , z]) => sampleHeight(s, x, z));
592
+ let spacing = 0;
593
+ for (let k = 1; k < points.length && spacing === 0; k += 1) spacing = run(k);
594
+ spacing ||= 3;
595
+ const window = Math.max(1, Math.round(Math.max(8, p.width ?? 12) / spacing));
596
+ const profile = bed.slice();
597
+ for (let pass = 0; pass < 2; pass += 1) {
598
+ const next = profile.slice();
599
+ for (let k = 0; k < profile.length; k += 1) {
600
+ let sum = 0;
601
+ let count = 0;
602
+ for (let j = Math.max(0, k - window); j <= Math.min(profile.length - 1, k + window); j += 1) {
603
+ sum += profile[j];
604
+ count += 1;
605
+ }
606
+ next[k] = sum / count;
607
+ }
608
+ for (let k = 0; k < profile.length; k += 1) profile[k] = next[k];
609
+ }
610
+ const maxCut = p.maxCut ?? 2.5;
611
+ const maxFill = p.maxFill ?? 1.5;
612
+ const grade2 = p.maxGrade ?? 0.12;
613
+ const limit = (k) => grade2 * Math.max(0.5, run(k));
614
+ for (let round = 0; round < 16; round += 1) {
615
+ let moved = 0;
616
+ for (let k = 1; k < profile.length; k += 1) {
617
+ const y = clamp(
618
+ profile[k],
619
+ profile[k - 1] - limit(k),
620
+ profile[k - 1] + limit(k)
621
+ );
622
+ moved += Math.abs(y - profile[k]);
623
+ profile[k] = y;
624
+ }
625
+ for (let k = profile.length - 2; k >= 0; k -= 1) {
626
+ const y = clamp(
627
+ profile[k],
628
+ profile[k + 1] - limit(k + 1),
629
+ profile[k + 1] + limit(k + 1)
630
+ );
631
+ moved += Math.abs(y - profile[k]);
632
+ profile[k] = y;
633
+ }
634
+ if (moved < 1e-4) break;
635
+ }
636
+ for (let k = 0; k < profile.length; k += 1)
637
+ profile[k] = clamp(
638
+ profile[k],
639
+ bed[k] - maxCut,
640
+ bed[k] + maxFill
641
+ );
642
+ for (let k = 0; k < points.length; k += 1) at(k)[1] = profile[k];
643
+ }
644
+ function applySpline(s, layer, mask) {
645
+ const p = layer.params;
646
+ const river = layer.type === "river";
647
+ const ramp = layer.type === "ramp";
648
+ const seed = p.seed ?? s.seed;
649
+ const raw = ramp ? [p.from, p.to] : p.points ?? [];
650
+ const follows = followsTerrain(raw, p.followTerrain);
651
+ const source = raw.map(
652
+ ([x, y, z]) => [x, Number.isFinite(y) ? y : 0, z]
653
+ );
654
+ const points = splinePoints(
655
+ source,
656
+ Math.max(1, (p.width ?? 12) * 0.22),
657
+ !ramp && p.smooth !== false
658
+ );
659
+ if (follows) gradeToGround(s, points, p);
660
+ const half = (p.width ?? 12) / 2;
661
+ const shoulder = p.shoulder ?? half;
662
+ const extent = half + shoulder;
663
+ const depth = p.depth ?? 4;
664
+ const alpha = layer.opacity ?? 1;
665
+ const n = s.resolution;
666
+ const cell = s.size / (n - 1);
667
+ const dist = new Float32Array(n * n).fill(Number.POSITIVE_INFINITY);
668
+ const target = new Float32Array(n * n);
669
+ const bed = new Float32Array(n * n);
670
+ const floor = river ? 1.5 * depth : 0;
671
+ let reach = extent;
672
+ const raster = () => {
673
+ dist.fill(Number.POSITIVE_INFINITY);
674
+ for (let k = 1; k < points.length; k += 1) {
675
+ const a = points[k - 1];
676
+ const b = points[k];
677
+ const dx = b[0] - a[0];
678
+ const dz = b[2] - a[2];
679
+ const den = dx * dx + dz * dz;
680
+ if (den < 1e-12) continue;
681
+ const xmin = clamp(Math.floor((Math.min(a[0], b[0]) - reach + s.size / 2) / cell), 0, n - 1);
682
+ const xmax = clamp(Math.ceil((Math.max(a[0], b[0]) + reach + s.size / 2) / cell), 0, n - 1);
683
+ const zmin = clamp(Math.floor((Math.min(a[2], b[2]) - reach + s.size / 2) / cell), 0, n - 1);
684
+ const zmax = clamp(Math.ceil((Math.max(a[2], b[2]) + reach + s.size / 2) / cell), 0, n - 1);
685
+ for (let z = zmin; z <= zmax; z += 1) {
686
+ for (let x = xmin; x <= xmax; x += 1) {
687
+ const wx = world(s, x);
688
+ const wz = world(s, z);
689
+ const t = clamp(((wx - a[0]) * dx + (wz - a[2]) * dz) / den);
690
+ const d = Math.hypot(wx - a[0] - t * dx, wz - a[2] - t * dz);
691
+ const i = z * n + x;
692
+ if (d < dist[i]) {
693
+ dist[i] = d;
694
+ target[i] = lerp(a[1], b[1], t);
695
+ bed[i] = s.height[i];
696
+ }
697
+ }
698
+ }
699
+ }
700
+ };
701
+ raster();
702
+ let worst = 0;
703
+ for (let i = 0; i < dist.length; i += 1)
704
+ if (dist[i] <= extent)
705
+ worst = Math.max(worst, Math.abs(target[i] - bed[i]));
706
+ if (floor + 1.5 * worst > 0.05) {
707
+ reach = extent + floor + 1.5 * worst;
708
+ raster();
709
+ }
710
+ const material = MATERIAL_IDS.indexOf(p.material ?? (river ? "mud" : "road"));
711
+ for (let i = 0; i < s.height.length; i += 1) {
712
+ const d = dist[i];
713
+ if (d > reach) continue;
714
+ const pre = bed[i];
715
+ const edge = Math.min(reach, extent + floor + 1.5 * Math.abs(target[i] - pre));
716
+ const blend = 1 - smoothstep(half, edge, d);
717
+ const w = blend * blend * (mask?.[i] ?? 1) * alpha;
718
+ let y = target[i];
719
+ if (river) {
720
+ const bowl = 1 - 0.35 * clamp(d / half) ** 2;
721
+ const bars = 1 + 0.22 * noise2(world(s, i % n) / (half * 8), world(s, Math.floor(i / n)) / (half * 8), seed + 7);
722
+ y = Math.min(pre, y - depth * bowl * bars * blend);
723
+ }
724
+ s.height[i] = lerp(pre, y, w);
725
+ if (material >= 0) paintWeights(s, i, material, w * (river ? 0.85 : 1));
726
+ }
727
+ if (river && p.water !== false)
728
+ s.rivers.push({
729
+ id: layer.id,
730
+ points,
731
+ width: (p.width ?? 12) * (p.waterWidth ?? 0.76),
732
+ color: p.color,
733
+ opacity: alpha
734
+ });
735
+ }
736
+ function applyOperation(s, layer) {
737
+ const { type, params: p } = layer;
738
+ const opacity = layer.opacity ?? 1;
739
+ if (layer.enabled === false || opacity <= 0) return;
740
+ const mask = rasterMask(layer.mask, s);
741
+ const weight = (i) => (mask?.[i] ?? 1) * opacity;
742
+ const n = s.resolution;
743
+ const seed = p.seed ?? s.seed;
744
+ switch (type) {
745
+ case "noise": {
746
+ const amplitude = p.amplitude ?? 35;
747
+ const scale = p.scale ?? 100;
748
+ const warp = p.warp ?? 20;
749
+ for (let z = 0; z < n; z += 1) {
750
+ for (let x = 0; x < n; x += 1) {
751
+ const i = z * n + x;
752
+ const wx = world(s, x);
753
+ const wz = world(s, z);
754
+ const ox = noise2(wx / (scale * 1.7) + 17, wz / (scale * 1.7) - 19, seed + 29) * warp;
755
+ const oz = noise2(wx / (scale * 1.7) - 27, wz / (scale * 1.7) + 12, seed + 31) * warp;
756
+ let value = (p.base ?? 0) + fbm((wx + ox) / scale, (wz + oz) / scale, {
757
+ seed,
758
+ octaves: p.octaves,
759
+ persistence: p.persistence,
760
+ lacunarity: p.lacunarity,
761
+ mode: p.mode
762
+ }) * amplitude;
763
+ if (p.island) {
764
+ const r = Math.hypot(wx / (s.size * 0.59), wz / (s.size * 0.59));
765
+ value = value * (1 - smoothstep(0.45, 1, r)) - (p.coastDepth ?? 15) * smoothstep(0.6, 1, r);
766
+ }
767
+ s.height[i] = p.blend === "replace" ? lerp(s.height[i], value, weight(i)) : s.height[i] + value * weight(i);
768
+ }
769
+ }
770
+ break;
771
+ }
772
+ case "sculpt":
773
+ case "flatten":
774
+ case "smooth":
775
+ case "paint": {
776
+ const weights = brushWeights(s, p, mask, seed);
777
+ const w = (i) => (weights?.[i] ?? 1) * opacity;
778
+ if (type === "sculpt")
779
+ for (let i = 0; i < s.height.length; i += 1)
780
+ s.height[i] = s.height[i] + (p.strength ?? 5) * w(i);
781
+ if (type === "flatten") {
782
+ const level = "height" in p ? p.height : void 0;
783
+ if (typeof level !== "number" || !Number.isFinite(level)) {
784
+ const covered = brushWeights(s, p, mask, seed);
785
+ let sum = 0;
786
+ let total = 0;
787
+ for (let i = 0; i < s.height.length; i += 1) {
788
+ const share = covered[i] * opacity;
789
+ sum += s.height[i] * share;
790
+ total += share;
791
+ }
792
+ const target = total > 0 ? sum / total : 0;
793
+ let delta = 0;
794
+ for (let i = 0; i < s.height.length; i += 1)
795
+ if (covered[i] > 0)
796
+ delta = Math.max(delta, Math.abs(s.height[i] - target));
797
+ const graded = brushWeights(
798
+ s,
799
+ { ...p, radius: (p.radius ?? 30) + 1.5 * delta },
800
+ mask,
801
+ seed
802
+ );
803
+ for (let i = 0; i < s.height.length; i += 1)
804
+ s.height[i] = lerp(
805
+ s.height[i],
806
+ target,
807
+ graded[i] * opacity * clamp(p.strength ?? 1)
808
+ );
809
+ } else
810
+ for (let i = 0; i < s.height.length; i += 1)
811
+ s.height[i] = lerp(s.height[i], level, w(i) * clamp(p.strength ?? 1));
812
+ }
813
+ if (type === "paint") {
814
+ const channel = MATERIAL_IDS.indexOf(("material" in p ? p.material : void 0) ?? "dirt");
815
+ for (let i = 0; i < s.height.length; i += 1)
816
+ paintWeights(s, i, channel, w(i) * clamp(p.strength ?? 1));
817
+ }
818
+ if (type === "smooth")
819
+ for (let k = 0; k < (("iterations" in p ? p.iterations : void 0) ?? 3); k += 1) {
820
+ const old = s.height.slice();
821
+ for (let z = 0; z < n; z += 1) {
822
+ for (let x = 0; x < n; x += 1) {
823
+ const i = z * n + x;
824
+ if (w(i) === 0) continue;
825
+ let total = 0;
826
+ let count = 0;
827
+ for (let dz = -1; dz <= 1; dz += 1) {
828
+ for (let dx = -1; dx <= 1; dx += 1) {
829
+ const xx = x + dx;
830
+ const zz = z + dz;
831
+ if (xx >= 0 && xx < n && zz >= 0 && zz < n) {
832
+ total += old[zz * n + xx];
833
+ count += 1;
834
+ }
835
+ }
836
+ }
837
+ s.height[i] = lerp(old[i], total / count, w(i) * clamp(p.strength ?? 0.65));
838
+ }
839
+ }
840
+ }
841
+ break;
842
+ }
843
+ case "stamp":
844
+ case "paste": {
845
+ const at = p.at ?? [0, 0];
846
+ const radius = p.radius ?? (p.size ? typeof p.size === "number" ? p.size / 2 : p.size.map((v) => v / 2) : 60);
847
+ const extents = Array.isArray(radius) ? [radius[0] / 1, radius[1] / 1] : [radius / 1, radius / 1];
848
+ const rot = (p.rotation ?? 0) * Math.PI / 180;
849
+ const c = Math.cos(rot);
850
+ const sn = Math.sin(rot);
851
+ const shape = ("shape" in p ? p.shape : void 0) ?? "mountain";
852
+ for (let z = 0; z < n; z += 1) {
853
+ for (let x = 0; x < n; x += 1) {
854
+ const i = z * n + x;
855
+ const dx = world(s, x) - at[0];
856
+ const dz = world(s, z) - at[1];
857
+ const u = (dx * c + dz * sn) / extents[0] * (p.mirrorX ? -1 : 1);
858
+ const v = (-dx * sn + dz * c) / extents[1] * (p.mirrorZ ? -1 : 1);
859
+ const d = Math.hypot(u, v);
860
+ let profile;
861
+ let w = 1;
862
+ if (p.data) {
863
+ if (Math.abs(u) > 1 || Math.abs(v) > 1) continue;
864
+ const source = p.data;
865
+ profile = sampleGrid(
866
+ source.values,
867
+ source.width,
868
+ source.height,
869
+ u * 0.5 + 0.5,
870
+ v * 0.5 + 0.5
871
+ );
872
+ w = falloff(Math.max(Math.abs(u), Math.abs(v)), 1, p.falloff ?? 0.12);
873
+ } else {
874
+ if (d >= 1) continue;
875
+ const rough = 1 + (("roughness" in p ? p.roughness : void 0) ?? 0.13) * fbm(u * 5, v * 5, { seed, octaves: 3 });
876
+ if (shape === "crater")
877
+ profile = (Math.exp(-(((d - 0.67) / 0.14) ** 2)) * 0.48 - Math.exp(-((d / 0.49) ** 4)) * 0.6) * (1 - smoothstep(0.85, 1, d));
878
+ else if (shape === "ridge")
879
+ profile = Math.max(0, 1 - Math.abs(v)) ** 1.7 * (1 - smoothstep(0.35, 1, Math.abs(u))) * rough;
880
+ else if (shape === "mesa") profile = (1 - smoothstep(0.5, 1, d)) * rough;
881
+ else if (shape === "valley") profile = -(1 - smoothstep(0, 1, d)) * rough;
882
+ else if (shape === "dune") profile = Math.max(0, 1 - d) ** 1.2 * (1 + 0.25 * u) * rough;
883
+ else profile = Math.max(0, 1 - d) ** 1.65 * rough;
884
+ profile *= ("amplitude" in p ? p.amplitude : void 0) ?? 45;
885
+ w = falloff(d, 1, p.falloff ?? 0);
886
+ }
887
+ const alpha = weight(i) * w;
888
+ const blend = p.blend ?? (type === "paste" ? "replace" : "add");
889
+ const height = profile * (p.scale ?? 1) + (p.offset ?? 0);
890
+ const value = blend === "replace" ? height : blend === "max" ? Math.max(s.height[i], height) : blend === "min" ? Math.min(s.height[i], height) : s.height[i] + height;
891
+ s.height[i] = lerp(s.height[i], value, alpha);
892
+ }
893
+ }
894
+ break;
895
+ }
896
+ case "erode": {
897
+ const observations = {
898
+ flow: new Float32Array(s.height.length),
899
+ sediment: new Float32Array(s.height.length),
900
+ deposition: new Float32Array(s.height.length),
901
+ talus: new Float32Array(s.height.length)
902
+ };
903
+ s.erosion ??= structuredClone(observations);
904
+ const eroded = p.method === "hydraulic" ? hydraulic(
905
+ s.height,
906
+ n,
907
+ s.size,
908
+ {
909
+ seed,
910
+ droplets: p.droplets,
911
+ maxSteps: p.maxSteps,
912
+ inertia: p.inertia,
913
+ capacity: p.capacity,
914
+ erosion: p.erosion,
915
+ deposition: p.deposition,
916
+ evaporation: p.evaporation
917
+ },
918
+ observations
919
+ ) : thermal(s.height, n, s.size, p, observations);
920
+ let influence = mask;
921
+ if (p.at || p.points) influence = brushWeights(s, p, mask, seed);
922
+ for (let i = 0; i < s.height.length; i += 1) {
923
+ const alpha = (influence?.[i] ?? 1) * opacity * (p.strength ?? 1);
924
+ s.height[i] = lerp(s.height[i], eroded[i], alpha);
925
+ for (const key of ["flow", "sediment", "deposition", "talus"])
926
+ s.erosion[key][i] = s.erosion[key][i] + observations[key][i] * alpha;
927
+ }
928
+ break;
929
+ }
930
+ case "terrace": {
931
+ const step = p.step ?? 8;
932
+ const softness = p.softness ?? 0.18;
933
+ for (let i = 0; i < s.height.length; i += 1) {
934
+ const v = (s.height[i] - (p.offset ?? 0)) / step;
935
+ const base = Math.floor(v);
936
+ const f = v - base;
937
+ const target = (base + smoothstep(0.5 - softness * 0.5, 0.5 + softness * 0.5, f)) * step + (p.offset ?? 0);
938
+ s.height[i] = lerp(s.height[i], target, weight(i) * (p.strength ?? 1));
939
+ }
940
+ break;
941
+ }
942
+ case "materials": {
943
+ if (p.reset !== false)
944
+ for (let i = 0; i < s.height.length; i += 1)
945
+ paintWeights(s, i, MATERIAL_IDS.indexOf(p.base ?? "grass"), weight(i));
946
+ for (const rule of p.rules ?? []) {
947
+ const ruleMask = rasterMask(rule.mask, s);
948
+ const channel = MATERIAL_IDS.indexOf(rule.material);
949
+ for (let i = 0; i < s.height.length; i += 1)
950
+ paintWeights(s, i, channel, (ruleMask?.[i] ?? 1) * weight(i) * (rule.strength ?? 1));
951
+ }
952
+ break;
953
+ }
954
+ case "biome": {
955
+ const name = p.name ?? "forest";
956
+ s.biomes[name] ??= new Float32Array(s.height.length);
957
+ const values = s.biomes[name];
958
+ const weights = brushWeights(s, p, mask, seed);
959
+ for (let i = 0; i < values.length; i += 1)
960
+ values[i] = lerp(
961
+ values[i],
962
+ p.value ?? 1,
963
+ (weights?.[i] ?? 1) * opacity * (p.strength ?? 1)
964
+ );
965
+ break;
966
+ }
967
+ case "scatter":
968
+ s.scatterRules.push({ id: layer.id, ...p, mask: layer.mask, opacity, clear: [] });
969
+ break;
970
+ case "clear": {
971
+ if (p.target === "scatter" || p.target === "all") {
972
+ for (const rule of s.scatterRules)
973
+ if (!p.asset || rule.asset === p.asset)
974
+ rule.clear.push({ mask: layer.mask ?? { type: "all" }, opacity });
975
+ }
976
+ if (p.target === "biome" || p.target === "all")
977
+ for (const [name, values] of Object.entries(s.biomes)) {
978
+ if (p.name && p.name !== name) continue;
979
+ for (let i = 0; i < values.length; i += 1)
980
+ values[i] = values[i] * (1 - weight(i));
981
+ }
982
+ if (p.target === "paint" || p.target === "all")
983
+ for (let i = 0; i < s.height.length; i += 1) paintWeights(s, i, 0, weight(i));
984
+ break;
985
+ }
986
+ case "road":
987
+ case "river":
988
+ case "ramp":
989
+ applySpline(s, layer, mask);
990
+ break;
991
+ case "water":
992
+ s.waterRules.push({ id: layer.id, ...p, mask: layer.mask, opacity });
993
+ break;
994
+ case "heightmap": {
995
+ if (p.at !== void 0 || p.size !== void 0 || p.rotation !== void 0 || p.falloff !== void 0) {
996
+ applyOperation(s, {
997
+ ...layer,
998
+ type: "paste",
999
+ params: { ...p, size: p.size ?? s.size, falloff: p.falloff ?? 0 }
1000
+ });
1001
+ break;
1002
+ }
1003
+ const data = p.data;
1004
+ for (let z = 0; z < n; z += 1) {
1005
+ for (let x = 0; x < n; x += 1) {
1006
+ const i = z * n + x;
1007
+ const value = sampleGrid(data.values, data.width, data.height, x / (n - 1), z / (n - 1)) * (p.scale ?? 1) + (p.offset ?? 0);
1008
+ s.height[i] = p.blend === "add" ? s.height[i] + value * weight(i) : lerp(s.height[i], value, weight(i));
1009
+ }
1010
+ }
1011
+ break;
1012
+ }
1013
+ default:
1014
+ throw Error(`Unsupported operation: ${String(layer.type)}`);
1015
+ }
1016
+ }
1017
+ function finalizeState(s) {
1018
+ const n = s.resolution;
1019
+ const cell = s.size / (n - 1);
1020
+ s.waters = [];
1021
+ s.instances = [];
1022
+ for (const water of s.waterRules) {
1023
+ const mask = new Uint8Array(n * n);
1024
+ const queue = new Int32Array(n * n);
1025
+ const allowed = compileMask(water.mask, s);
1026
+ const level = water.level ?? 0;
1027
+ const at = water.at ?? [0, 0];
1028
+ const radius = water.radius ?? s.size;
1029
+ let front = 0;
1030
+ let back = 0;
1031
+ const eligible = (i) => {
1032
+ const x = world(s, i % n);
1033
+ const z = world(s, Math.floor(i / n));
1034
+ return s.height[i] < level && Math.hypot(x - at[0], z - at[1]) <= radius && allowed(i, x, z) > 0.05;
1035
+ };
1036
+ const push = (i) => {
1037
+ if (i >= 0 && i < n * n && !mask[i] && eligible(i)) {
1038
+ mask[i] = 1;
1039
+ queue[back] = i;
1040
+ back += 1;
1041
+ }
1042
+ };
1043
+ if (water.kind === "ocean") {
1044
+ for (let k = 0; k < n; k += 1) {
1045
+ push(k);
1046
+ push((n - 1) * n + k);
1047
+ push(k * n);
1048
+ push(k * n + n - 1);
1049
+ }
1050
+ } else {
1051
+ const x = clamp(Math.round((at[0] + s.size / 2) / cell), 0, n - 1);
1052
+ const z = clamp(Math.round((at[1] + s.size / 2) / cell), 0, n - 1);
1053
+ push(z * n + x);
1054
+ }
1055
+ while (front < back) {
1056
+ const i = queue[front];
1057
+ front += 1;
1058
+ const x = i % n;
1059
+ const z = Math.floor(i / n);
1060
+ if (x > 0) push(i - 1);
1061
+ if (x < n - 1) push(i + 1);
1062
+ if (z > 0) push(i - n);
1063
+ if (z < n - 1) push(i + n);
1064
+ }
1065
+ s.waters.push({ ...water, level, mask });
1066
+ if (!back)
1067
+ s.diagnostics.push(
1068
+ `Water '${water.id}' has no flooded cells; its seed may be above the water level.`
1069
+ );
1070
+ }
1071
+ const wet = (i, x, z, h) => {
1072
+ if (s.waters.some((water) => water.mask[i])) return true;
1073
+ for (const river of s.rivers) {
1074
+ const half = river.width / 2;
1075
+ for (let k = 1; k < river.points.length; k += 1) {
1076
+ const a = river.points[k - 1];
1077
+ const b = river.points[k];
1078
+ if (x < Math.min(a[0], b[0]) - half || x > Math.max(a[0], b[0]) + half || z < Math.min(a[2], b[2]) - half || z > Math.max(a[2], b[2]) + half)
1079
+ continue;
1080
+ const dx = b[0] - a[0];
1081
+ const dz = b[2] - a[2];
1082
+ const t = clamp(((x - a[0]) * dx + (z - a[2]) * dz) / (dx * dx + dz * dz || 1));
1083
+ if (Math.hypot(x - a[0] - t * dx, z - a[2] - t * dz) < half && h <= lerp(a[1], b[1], t) + 0.2)
1084
+ return true;
1085
+ }
1086
+ }
1087
+ return false;
1088
+ };
1089
+ for (const rule of s.scatterRules) {
1090
+ const count = Math.round((rule.count ?? 300) * (rule.opacity ?? 1));
1091
+ const seed = (rule.seed ?? s.seed ^ hashString(rule.id)) >>> 0;
1092
+ const rnd = random(seed);
1093
+ const accept = compileMask(rule.mask, s);
1094
+ const clears = rule.clear.map((entry) => ({
1095
+ fn: compileMask(entry.mask, s),
1096
+ opacity: entry.opacity
1097
+ }));
1098
+ const distance = rule.minDistance ?? 4;
1099
+ const gridSize = Math.max(distance, 0.1);
1100
+ const buckets = /* @__PURE__ */ new Map();
1101
+ const accepted = [];
1102
+ const maxAttempts = count * 40;
1103
+ const scale = rule.scale ?? [0.8, 1.3];
1104
+ for (let attempt = 0; attempt < maxAttempts && accepted.length < count; attempt += 1) {
1105
+ const x = (rnd() - 0.5) * s.size;
1106
+ const z = (rnd() - 0.5) * s.size;
1107
+ const acceptance = rnd();
1108
+ const scaleRandom = rnd();
1109
+ const yaw = rnd() * Math.PI * 2;
1110
+ const ix = clamp(Math.round((x / s.size + 0.5) * (n - 1)), 0, n - 1);
1111
+ const iz = clamp(Math.round((z / s.size + 0.5) * (n - 1)), 0, n - 1);
1112
+ const i = iz * n + ix;
1113
+ let w = accept(i, x, z);
1114
+ for (const clear of clears) w *= 1 - clear.fn(i, x, z) * clear.opacity;
1115
+ if (acceptance > w) continue;
1116
+ const h = sampleHeight(s, x, z);
1117
+ if (rule.avoidWater !== false && wet(i, x, z, h)) continue;
1118
+ if (h < (rule.minHeight ?? -1e9) || h > (rule.maxHeight ?? 1e9)) continue;
1119
+ const g = gradientAt(s, x, z);
1120
+ const slope = Math.atan(Math.hypot(g[0], g[1])) * (180 / Math.PI);
1121
+ if (slope < (rule.minSlope ?? 0) || slope > (rule.maxSlope ?? 90)) continue;
1122
+ const gx = Math.floor(x / gridSize);
1123
+ const gz = Math.floor(z / gridSize);
1124
+ let blocked = false;
1125
+ if (distance > 0)
1126
+ for (let dz = -1; dz <= 1 && !blocked; dz += 1) {
1127
+ for (let dx = -1; dx <= 1 && !blocked; dx += 1) {
1128
+ for (const j of buckets.get(`${gx + dx},${gz + dz}`) ?? []) {
1129
+ const other = accepted[j];
1130
+ if (Math.hypot(other.position[0] - x, other.position[2] - z) < distance) {
1131
+ blocked = true;
1132
+ break;
1133
+ }
1134
+ }
1135
+ }
1136
+ }
1137
+ if (blocked) continue;
1138
+ const index = accepted.length;
1139
+ const itemScale = Array.isArray(scale) ? lerp(scale[0], scale[1], scaleRandom) : scale;
1140
+ const normal = [-g[0], 1, -g[1]];
1141
+ const length = Math.hypot(normal[0], normal[1], normal[2]);
1142
+ accepted.push({
1143
+ id: `${rule.id}:candidate:${seed}:${attempt}`,
1144
+ layer: rule.id,
1145
+ asset: rule.asset ?? "pine",
1146
+ position: [x, h + (rule.offsetY ?? 0), z],
1147
+ rotation: yaw,
1148
+ scale: itemScale,
1149
+ normal: [normal[0] / length, normal[1] / length, normal[2] / length],
1150
+ alignToNormal: rule.alignToNormal ?? false
1151
+ });
1152
+ const key = `${gx},${gz}`;
1153
+ const bucket = buckets.get(key) ?? [];
1154
+ bucket.push(index);
1155
+ buckets.set(key, bucket);
1156
+ }
1157
+ s.instances.push(...accepted);
1158
+ if (accepted.length < count)
1159
+ s.diagnostics.push(
1160
+ `Scatter '${rule.id}': placed ${accepted.length}/${count}; masks or spacing exhausted the attempt budget.`
1161
+ );
1162
+ }
1163
+ return s;
1164
+ }
1165
+
1166
+ // src/core/validation.ts
1167
+ var RESOLUTIONS = Object.freeze([17, 33, 65, 129, 257, 513, 1025]);
1168
+ var brush2 = [
1169
+ "at",
1170
+ "points",
1171
+ "radius",
1172
+ "strength",
1173
+ "falloff",
1174
+ "shape",
1175
+ "rotation",
1176
+ "spacing",
1177
+ "jitter"
1178
+ ];
1179
+ var grade = ["followTerrain", "maxGrade", "maxCut", "maxFill"];
1180
+ var PARAMS = Object.freeze({
1181
+ noise: [
1182
+ "amplitude",
1183
+ "scale",
1184
+ "octaves",
1185
+ "persistence",
1186
+ "lacunarity",
1187
+ "mode",
1188
+ "warp",
1189
+ "base",
1190
+ "blend",
1191
+ "island",
1192
+ "coastDepth"
1193
+ ],
1194
+ sculpt: brush2,
1195
+ smooth: [...brush2, "iterations"],
1196
+ flatten: [...brush2, "height"],
1197
+ paint: [...brush2, "material"],
1198
+ stamp: [
1199
+ "at",
1200
+ "radius",
1201
+ "size",
1202
+ "amplitude",
1203
+ "shape",
1204
+ "rotation",
1205
+ "mirrorX",
1206
+ "mirrorZ",
1207
+ "blend",
1208
+ "roughness",
1209
+ "scale",
1210
+ "offset",
1211
+ "data",
1212
+ "falloff"
1213
+ ],
1214
+ paste: [
1215
+ "at",
1216
+ "size",
1217
+ "radius",
1218
+ "rotation",
1219
+ "mirrorX",
1220
+ "mirrorZ",
1221
+ "blend",
1222
+ "offset",
1223
+ "scale",
1224
+ "data",
1225
+ "falloff"
1226
+ ],
1227
+ erode: [
1228
+ ...brush2,
1229
+ "method",
1230
+ "iterations",
1231
+ "talus",
1232
+ "rate",
1233
+ "droplets",
1234
+ "maxSteps",
1235
+ "inertia",
1236
+ "capacity",
1237
+ "erosion",
1238
+ "deposition",
1239
+ "evaporation"
1240
+ ],
1241
+ terrace: ["step", "softness", "strength", "offset"],
1242
+ materials: ["base", "rules", "reset"],
1243
+ biome: [...brush2, "name", "value"],
1244
+ scatter: [
1245
+ "asset",
1246
+ "count",
1247
+ "minDistance",
1248
+ "scale",
1249
+ "minHeight",
1250
+ "maxHeight",
1251
+ "minSlope",
1252
+ "maxSlope",
1253
+ "avoidWater",
1254
+ "offsetY",
1255
+ "alignToNormal"
1256
+ ],
1257
+ clear: ["target", "asset", "name"],
1258
+ road: ["points", "width", "shoulder", "material", "smooth", ...grade],
1259
+ river: [
1260
+ "points",
1261
+ "width",
1262
+ "shoulder",
1263
+ "depth",
1264
+ "material",
1265
+ "smooth",
1266
+ "water",
1267
+ "waterWidth",
1268
+ "color",
1269
+ "enforceDownhill",
1270
+ ...grade
1271
+ ],
1272
+ ramp: ["from", "to", "width", "shoulder", "material", ...grade],
1273
+ water: ["kind", "at", "radius", "level", "color"],
1274
+ heightmap: ["data", "scale", "offset", "blend", "at", "size", "rotation", "falloff"]
1275
+ });
1276
+ function fail(message) {
1277
+ throw new TypeError(message);
1278
+ }
1279
+ function finiteTree(value, path = "value", depth = 0) {
1280
+ if (depth > 32) fail(`${path}: nesting is too deep`);
1281
+ if (typeof value === "number" && !Number.isFinite(value)) fail(`${path} must be finite`);
1282
+ if (value === void 0 || typeof value === "function" || typeof value === "bigint" || typeof value === "symbol")
1283
+ fail(`${path} is not JSON-serializable`);
1284
+ if (value && typeof value === "object") {
1285
+ if (ArrayBuffer.isView(value)) fail(`${path}: use a plain array in recipes`);
1286
+ for (const [key, entry] of Object.entries(value)) {
1287
+ if (["__proto__", "prototype", "constructor"].includes(key))
1288
+ fail(`${path}: unsafe object key`);
1289
+ finiteTree(entry, `${path}.${key}`, depth + 1);
1290
+ }
1291
+ }
1292
+ }
1293
+ var num = (v, name, min = -1e9, max = 1e9, integer = false) => {
1294
+ if (typeof v !== "number" || !Number.isFinite(v) || v < min || v > max || integer && !Number.isInteger(v))
1295
+ fail(`${name} must be ${integer ? "an integer" : "finite"} between ${min} and ${max}`);
1296
+ };
1297
+ var vec = (v, name, n) => {
1298
+ if (!Array.isArray(v) || v.length !== n) fail(`${name} must contain ${n} numbers`);
1299
+ for (const x of v) num(x, name);
1300
+ };
1301
+ var identifier = (v, name) => {
1302
+ if (typeof v !== "string" || v.length === 0 || v.length > 100 || ["__proto__", "prototype", "constructor"].includes(v))
1303
+ fail(`${name} must be a nonempty safe string, at most 100 characters`);
1304
+ };
1305
+ var enumeration = (v, name, choices) => {
1306
+ if (!choices.includes(v)) fail(`${name}: expected ${choices.join(", ")}`);
1307
+ };
1308
+ function validateConfig(config) {
1309
+ num(config.size, "size", 1, 1e5);
1310
+ if (!RESOLUTIONS.includes(config.resolution))
1311
+ fail(`resolution must be one of ${RESOLUTIONS.join(", ")}`);
1312
+ num(config.seed, "seed", 0, 4294967295, true);
1313
+ }
1314
+ function validateMask(m, depth = 0) {
1315
+ if (!m || typeof m !== "object" || depth > 20) fail("Invalid mask");
1316
+ enumeration(m.type, "mask.type", [
1317
+ "all",
1318
+ "none",
1319
+ "circle",
1320
+ "rectangle",
1321
+ "height",
1322
+ "slope",
1323
+ "noise",
1324
+ "biome",
1325
+ "material",
1326
+ "and",
1327
+ "or",
1328
+ "not"
1329
+ ]);
1330
+ const allowed = {
1331
+ all: [],
1332
+ none: [],
1333
+ circle: ["at", "radius", "falloff"],
1334
+ rectangle: ["at", "size", "rotation", "falloff"],
1335
+ height: ["min", "max", "fade"],
1336
+ slope: ["min", "max", "fade"],
1337
+ noise: ["scale", "threshold", "seed", "fade"],
1338
+ biome: ["name"],
1339
+ material: ["name"],
1340
+ and: ["masks"],
1341
+ or: ["masks"],
1342
+ not: ["mask"]
1343
+ };
1344
+ for (const key of Object.keys(m))
1345
+ if (key !== "type" && !allowed[m.type].includes(key)) fail(`Unknown mask field '${key}'`);
1346
+ if (m.rotation !== void 0) num(m.rotation, "mask.rotation");
1347
+ if (m.seed !== void 0) num(m.seed, "mask.seed", 0, 4294967295, true);
1348
+ if (m.type === "circle") {
1349
+ vec(m.at, "mask.at", 2);
1350
+ num(m.radius, "mask.radius", 1e-3, 1e6);
1351
+ }
1352
+ if (m.type === "rectangle") {
1353
+ vec(m.at, "mask.at", 2);
1354
+ const size = m.size;
1355
+ if (Array.isArray(size)) {
1356
+ vec(size, "mask.size", 2);
1357
+ for (const value of size) num(value, "mask.size", 1e-3, 1e6);
1358
+ } else num(size, "mask.size", 1e-3, 1e6);
1359
+ }
1360
+ if (["height", "slope"].includes(m.type)) {
1361
+ if (m.min !== void 0) num(m.min, "mask.min");
1362
+ if (m.max !== void 0) num(m.max, "mask.max");
1363
+ if ((m.min ?? -1e9) > (m.max ?? 1e9)) fail("mask.min must not exceed mask.max");
1364
+ if (m.fade !== void 0) num(m.fade, "mask.fade", 0, 1e6);
1365
+ }
1366
+ if (m.type === "noise") {
1367
+ num(m.scale, "mask.scale", 1e-3, 1e6);
1368
+ if (m.threshold !== void 0) num(m.threshold, "mask.threshold", 0, 1);
1369
+ if (m.fade !== void 0) num(m.fade, "mask.fade", 1e-6, 1);
1370
+ }
1371
+ if (m.type === "biome") identifier(m.name, "mask.name");
1372
+ if (m.type === "material") enumeration(m.name, "mask.material", MATERIAL_IDS);
1373
+ if (m.type === "not") validateMask(m.mask, depth + 1);
1374
+ if (["and", "or"].includes(m.type)) {
1375
+ if (!Array.isArray(m.masks) || m.masks.length > 32)
1376
+ fail("mask.masks must be an array of at most 32 masks");
1377
+ for (const entry of m.masks) validateMask(entry, depth + 1);
1378
+ }
1379
+ if (m.falloff !== void 0) num(m.falloff, "mask.falloff", 0, 1);
1380
+ }
1381
+ function splinePoint(v, name, follows) {
1382
+ if (!Array.isArray(v) || v.length !== 3) fail(`${name} must contain 3 numbers`);
1383
+ num(v[0], name);
1384
+ num(v[2], name);
1385
+ const y = v[1];
1386
+ if (y === null || y === void 0) {
1387
+ if (follows) return;
1388
+ fail(`${name} must contain 3 numbers`);
1389
+ }
1390
+ if (typeof y !== "number") fail(`${name} must contain 3 numbers`);
1391
+ if (!Number.isFinite(y) && !follows) fail(`${name} elevation must be finite`);
1392
+ }
1393
+ function layerFollowsTerrain(layer) {
1394
+ if (!layer || typeof layer !== "object" || !["road", "river", "ramp"].includes(layer.type))
1395
+ return false;
1396
+ const params = layer.params ?? {};
1397
+ const source = layer.type === "ramp" ? [params.from, params.to] : params.points;
1398
+ if (!Array.isArray(source)) return false;
1399
+ return followsTerrain(source, params.followTerrain);
1400
+ }
1401
+ function omittedElevations(layer, follows) {
1402
+ if (!follows) return layer;
1403
+ const params = layer.params ?? {};
1404
+ const zero = (p) => Array.isArray(p) && p.length === 3 ? [p[0], 0, p[2]] : p;
1405
+ const points = params.points;
1406
+ const fixes = {
1407
+ ...Array.isArray(points) ? { points: points.map(zero) } : {},
1408
+ ...Array.isArray(params.from) ? { from: zero(params.from) } : {},
1409
+ ...Array.isArray(params.to) ? { to: zero(params.to) } : {}
1410
+ };
1411
+ return Object.keys(fixes).length ? { ...layer, params: { ...params, ...fixes } } : layer;
1412
+ }
1413
+ function validateLayer(layer) {
1414
+ const follows = layerFollowsTerrain(layer);
1415
+ finiteTree(omittedElevations(layer, follows), "layer");
1416
+ if (!layer || typeof layer !== "object") fail("layer must be an object");
1417
+ for (const k of Object.keys(layer))
1418
+ if (!["id", "name", "type", "params", "enabled", "opacity", "mask"].includes(k))
1419
+ fail(`Unknown layer field '${k}'`);
1420
+ identifier(layer.id, "layer.id");
1421
+ enumeration(layer.type, "operation", OPERATION_TYPES);
1422
+ if (layer.name !== void 0) identifier(layer.name, "layer.name");
1423
+ if (layer.enabled !== void 0 && typeof layer.enabled !== "boolean")
1424
+ fail("enabled must be boolean");
1425
+ if (layer.opacity !== void 0) num(layer.opacity, "opacity", 0, 1);
1426
+ if (layer.mask) validateMask(layer.mask);
1427
+ const params = layer.params ?? {};
1428
+ if (!params || Array.isArray(params) || typeof params !== "object")
1429
+ fail("params must be an object");
1430
+ for (const k of Object.keys(params))
1431
+ if (!PARAMS[layer.type].includes(k) && k !== "seed")
1432
+ fail(`Unknown ${layer.type} parameter '${k}'`);
1433
+ for (const key of [
1434
+ "island",
1435
+ "mirrorX",
1436
+ "mirrorZ",
1437
+ "reset",
1438
+ "avoidWater",
1439
+ "alignToNormal",
1440
+ "smooth",
1441
+ "water",
1442
+ "enforceDownhill",
1443
+ "followTerrain"
1444
+ ])
1445
+ if (params[key] !== void 0 && typeof params[key] !== "boolean")
1446
+ fail(`${key} must be boolean`);
1447
+ if (params.seed !== void 0) num(params.seed, "seed", 0, 4294967295, true);
1448
+ if (params.at !== void 0) vec(params.at, "at", 2);
1449
+ if (params.from !== void 0) splinePoint(params.from, "from", follows);
1450
+ if (params.to !== void 0) splinePoint(params.to, "to", follows);
1451
+ const points = params.points;
1452
+ const isSpline = ["road", "river"].includes(layer.type);
1453
+ if (points) {
1454
+ if (!Array.isArray(points) || !points.length || points.length > 8192)
1455
+ fail("points must have 1..8192 entries");
1456
+ const dimensions = isSpline ? 3 : 2;
1457
+ for (const point of points)
1458
+ if (isSpline) splinePoint(point, "point", follows);
1459
+ else vec(point, "point", dimensions);
1460
+ }
1461
+ if (Array.isArray(points) && !isSpline) {
1462
+ const stroke = points;
1463
+ const spacing = Math.max(
1464
+ 0.1,
1465
+ (params.radius ?? 30) * 2 * (params.spacing ?? 0.15)
1466
+ );
1467
+ let samples = 1;
1468
+ for (let k = 1; k < stroke.length; k += 1) {
1469
+ const next = stroke[k];
1470
+ const previous = stroke[k - 1];
1471
+ samples += Math.max(
1472
+ 1,
1473
+ Math.ceil(
1474
+ Math.hypot(
1475
+ next[0] - previous[0],
1476
+ next[1] - previous[1]
1477
+ ) / spacing
1478
+ )
1479
+ );
1480
+ }
1481
+ if (samples > 65536)
1482
+ fail(
1483
+ "Brush interpolation exceeds 65536 samples; increase radius/spacing or shorten the stroke"
1484
+ );
1485
+ }
1486
+ if (isSpline && (!Array.isArray(points) || points.length < 2))
1487
+ fail(`${layer.type} requires at least two [x,y,z] points`);
1488
+ if (layer.type === "ramp" && (!params.from || !params.to)) fail("ramp requires from and to");
1489
+ for (const key of ["radius", "size"]) {
1490
+ const value = params[key];
1491
+ if (value === void 0) continue;
1492
+ if (Array.isArray(value)) {
1493
+ vec(value, key, 2);
1494
+ for (const entry of value) num(entry, key, 1e-3, 1e6);
1495
+ } else num(value, key, 1e-3, 1e6);
1496
+ }
1497
+ if (Array.isArray(params.radius) && !["stamp", "paste"].includes(layer.type))
1498
+ fail("Brush radius must be one number");
1499
+ for (const key of [
1500
+ "amplitude",
1501
+ "height",
1502
+ "base",
1503
+ "offset",
1504
+ "offsetY",
1505
+ "minHeight",
1506
+ "maxHeight",
1507
+ "level",
1508
+ "rotation"
1509
+ ])
1510
+ if (params[key] !== void 0 && !(key === "base" && layer.type === "materials")) {
1511
+ if (params[key] !== null || key !== "height") num(params[key], key);
1512
+ }
1513
+ for (const key of ["width", "step"])
1514
+ if (params[key] !== void 0) num(params[key], key, 1e-3, 1e6);
1515
+ for (const key of [
1516
+ "warp",
1517
+ "shoulder",
1518
+ "roughness",
1519
+ "coastDepth",
1520
+ "minDistance",
1521
+ "depth",
1522
+ "capacity",
1523
+ "maxCut",
1524
+ "maxFill"
1525
+ ])
1526
+ if (params[key] !== void 0) num(params[key], key, 0, 1e6);
1527
+ for (const key of [
1528
+ "falloff",
1529
+ "jitter",
1530
+ "softness",
1531
+ "inertia",
1532
+ "erosion",
1533
+ "deposition",
1534
+ "evaporation",
1535
+ "rate",
1536
+ "value",
1537
+ "waterWidth",
1538
+ "maxGrade"
1539
+ ])
1540
+ if (params[key] !== void 0) num(params[key], key, 0, 1);
1541
+ if (params.spacing !== void 0) num(params.spacing, "spacing", 0.01, 2);
1542
+ if (params.strength !== void 0)
1543
+ num(
1544
+ params.strength,
1545
+ "strength",
1546
+ layer.type === "sculpt" ? -1e4 : 0,
1547
+ layer.type === "sculpt" ? 1e4 : 1
1548
+ );
1549
+ if (params.scale !== void 0) {
1550
+ const scale = params.scale;
1551
+ if (layer.type === "scatter") {
1552
+ if (Array.isArray(scale)) {
1553
+ vec(scale, "scale", 2);
1554
+ if (scale[0] > scale[1]) fail("scale range is inverted");
1555
+ for (const entry of scale) num(entry, "scale", 1e-3, 1e4);
1556
+ } else num(scale, "scale", 1e-3, 1e4);
1557
+ } else num(scale, "scale", 1e-3, 1e6);
1558
+ }
1559
+ for (const [key, max] of [
1560
+ ["octaves", 10],
1561
+ ["iterations", 200],
1562
+ ["droplets", 2e5],
1563
+ ["maxSteps", 128],
1564
+ ["count", 2e4]
1565
+ ])
1566
+ if (params[key] !== void 0) num(params[key], key, key === "count" ? 0 : 1, max, true);
1567
+ if (params.persistence !== void 0) num(params.persistence, "persistence", 0.01, 1);
1568
+ if (params.lacunarity !== void 0) num(params.lacunarity, "lacunarity", 1, 4);
1569
+ for (const key of ["minSlope", "maxSlope", "talus"])
1570
+ if (params[key] !== void 0) num(params[key], key, 0, key === "talus" ? 89.9 : 90);
1571
+ if (params.minHeight !== void 0 && params.maxHeight !== void 0 && params.minHeight > params.maxHeight)
1572
+ fail("height range is inverted");
1573
+ if (params.minSlope !== void 0 && params.maxSlope !== void 0 && params.minSlope > params.maxSlope)
1574
+ fail("slope range is inverted");
1575
+ if (params.material !== void 0) enumeration(params.material, "material", MATERIAL_IDS);
1576
+ if (layer.type === "materials") {
1577
+ if (params.base !== void 0) enumeration(params.base, "base", MATERIAL_IDS);
1578
+ const rules = params.rules;
1579
+ if (!Array.isArray(rules) || rules.length > 64) fail("materials requires a rules array");
1580
+ for (const rule of rules) {
1581
+ enumeration(rule.material, "rule.material", MATERIAL_IDS);
1582
+ if (rule.mask) validateMask(rule.mask);
1583
+ if (rule.strength !== void 0) num(rule.strength, "rule.strength", 0, 1);
1584
+ }
1585
+ }
1586
+ if (params.method !== void 0) enumeration(params.method, "method", ["thermal", "hydraulic"]);
1587
+ if (params.mode !== void 0) enumeration(params.mode, "mode", ["fbm", "ridged", "billow"]);
1588
+ if (params.shape !== void 0)
1589
+ enumeration(
1590
+ params.shape,
1591
+ "shape",
1592
+ ["stamp"].includes(layer.type) ? ["mountain", "crater", "ridge", "valley", "mesa", "dune"] : ["circle", "square"]
1593
+ );
1594
+ if (params.blend !== void 0)
1595
+ enumeration(
1596
+ params.blend,
1597
+ "blend",
1598
+ ["noise", "heightmap"].includes(layer.type) ? ["add", "replace"] : ["add", "replace", "min", "max"]
1599
+ );
1600
+ if (layer.type === "clear")
1601
+ enumeration(params.target, "clear.target", ["scatter", "paint", "biome", "all"]);
1602
+ if (layer.type === "water") enumeration(params.kind ?? "lake", "water.kind", ["lake", "ocean"]);
1603
+ if (layer.type === "river" && params.enforceDownhill && Array.isArray(points)) {
1604
+ const river = points;
1605
+ for (let i = 1; i < river.length; i += 1) {
1606
+ const next = river[i];
1607
+ const previous = river[i - 1];
1608
+ if (next[1] > previous[1])
1609
+ fail("River control-point elevations must be non-increasing");
1610
+ }
1611
+ }
1612
+ if (params.name !== void 0) identifier(params.name, "name");
1613
+ if (params.asset !== void 0) identifier(params.asset, "asset");
1614
+ if (["heightmap", "paste"].includes(layer.type) && !params.data)
1615
+ fail(`${layer.type} requires heightmap data`);
1616
+ const data = params.data;
1617
+ if (data) {
1618
+ num(data.width, "data.width", 2, 4097, true);
1619
+ num(data.height, "data.height", 2, 4097, true);
1620
+ if (!Array.isArray(data.values) || data.values.length !== data.width * data.height)
1621
+ fail("heightmap dimensions do not match values");
1622
+ for (const value of data.values)
1623
+ if (typeof value !== "number" || !Number.isFinite(value))
1624
+ fail("data.values must contain only finite numbers");
1625
+ }
1626
+ }
1627
+ function validateDocument(doc) {
1628
+ finiteTree(
1629
+ Array.isArray(doc.layers) ? {
1630
+ ...doc,
1631
+ layers: doc.layers.map((layer) => omittedElevations(layer, layerFollowsTerrain(layer)))
1632
+ } : doc,
1633
+ "recipe"
1634
+ );
1635
+ if (doc.version !== 1) fail("Unsupported recipe version; expected 1");
1636
+ validateConfig(doc.config);
1637
+ if (!Array.isArray(doc.layers) || doc.layers.length > 512)
1638
+ fail("layers must be an array of at most 512 layers");
1639
+ const ids = /* @__PURE__ */ new Set();
1640
+ for (const layer of doc.layers) {
1641
+ validateLayer(layer);
1642
+ if (ids.has(layer.id)) fail(`Duplicate layer ID: ${layer.id}`);
1643
+ ids.add(layer.id);
1644
+ }
1645
+ }
1646
+
1647
+ // src/core/terrain.ts
1648
+ var TerrainEvaluator = class {
1649
+ cache = /* @__PURE__ */ new Map();
1650
+ bytes = 0;
1651
+ limit;
1652
+ constructor({ cacheMB = 48 } = {}) {
1653
+ if (!Number.isFinite(cacheMB) || cacheMB < 0)
1654
+ throw new RangeError("cacheMB must be finite and nonnegative");
1655
+ this.limit = cacheMB * 1024 * 1024;
1656
+ }
1657
+ clear() {
1658
+ this.cache.clear();
1659
+ this.bytes = 0;
1660
+ }
1661
+ evaluate(doc, { resolution = doc.config.resolution, signal, onProgress } = {}) {
1662
+ validateDocument(doc);
1663
+ validateConfig({ ...doc.config, resolution });
1664
+ const keys = [];
1665
+ let key = JSON.stringify({ ...doc.config, resolution });
1666
+ for (const layer of doc.layers) {
1667
+ key += `|${JSON.stringify(layer)}`;
1668
+ keys.push(key);
1669
+ }
1670
+ let state;
1671
+ let start = 0;
1672
+ for (let i = keys.length - 1; i >= 0; i--) {
1673
+ const k = keys[i];
1674
+ const hit = this.cache.get(k);
1675
+ if (hit) {
1676
+ state = structuredClone(hit.state);
1677
+ start = i + 1;
1678
+ this.cache.delete(k);
1679
+ this.cache.set(k, hit);
1680
+ break;
1681
+ }
1682
+ }
1683
+ state ??= createState(doc.config, resolution);
1684
+ for (let i = start; i < doc.layers.length; i++) {
1685
+ signal?.throwIfAborted();
1686
+ const layer = doc.layers[i];
1687
+ applyOperation(state, layer);
1688
+ onProgress?.({ index: i, total: doc.layers.length, id: layer.id });
1689
+ const bytes = state.height.byteLength + state.splat.byteLength + Object.values(state.erosion ?? {}).reduce((n, a) => n + a.byteLength, 0) + Object.values(state.biomes).reduce((n, a) => n + a.byteLength, 0);
1690
+ if (bytes <= this.limit) {
1691
+ while (this.bytes + bytes > this.limit && this.cache.size) {
1692
+ const first = this.cache.entries().next().value;
1693
+ if (!first) break;
1694
+ this.bytes -= first[1].bytes;
1695
+ this.cache.delete(first[0]);
1696
+ }
1697
+ this.cache.set(keys[i], { state: structuredClone(state), bytes });
1698
+ this.bytes += bytes;
1699
+ }
1700
+ }
1701
+ signal?.throwIfAborted();
1702
+ if (!state.height.every(Number.isFinite))
1703
+ throw new Error("A terrain operation produced a non-finite height");
1704
+ return finalizeState(state);
1705
+ }
1706
+ };
1707
+ var Terrain = class _Terrain {
1708
+ _doc;
1709
+ _undo = [];
1710
+ _redo = [];
1711
+ _listeners = /* @__PURE__ */ new Set();
1712
+ _historyLimit;
1713
+ _transactionDepth = 0;
1714
+ _evaluator = new TerrainEvaluator();
1715
+ constructor({
1716
+ size = 512,
1717
+ resolution = 257,
1718
+ seed = 73,
1719
+ historyLimit = 60
1720
+ } = {}) {
1721
+ const config = { size, resolution, seed };
1722
+ validateConfig(config);
1723
+ if (!Number.isInteger(historyLimit) || historyLimit < 0)
1724
+ throw new RangeError("historyLimit must be a nonnegative integer");
1725
+ this._doc = { version: 1, config, layers: [] };
1726
+ this._historyLimit = historyLimit;
1727
+ }
1728
+ static fromJSON(json2) {
1729
+ const doc = typeof json2 === "string" ? JSON.parse(json2) : structuredClone(json2);
1730
+ validateDocument(doc);
1731
+ const terrain = new _Terrain(doc.config);
1732
+ terrain._doc = doc;
1733
+ return terrain;
1734
+ }
1735
+ get layers() {
1736
+ return structuredClone(this._doc.layers);
1737
+ }
1738
+ get config() {
1739
+ return structuredClone(this._doc.config);
1740
+ }
1741
+ get canUndo() {
1742
+ return this._undo.length > 0;
1743
+ }
1744
+ get canRedo() {
1745
+ return this._redo.length > 0;
1746
+ }
1747
+ toJSON() {
1748
+ return structuredClone(this._doc);
1749
+ }
1750
+ subscribe(fn) {
1751
+ this._listeners.add(fn);
1752
+ return () => this._listeners.delete(fn);
1753
+ }
1754
+ _emit() {
1755
+ for (const fn of this._listeners) {
1756
+ try {
1757
+ fn(this);
1758
+ } catch (error) {
1759
+ console.error("Terrain subscriber failed:", error);
1760
+ }
1761
+ }
1762
+ }
1763
+ _change(fn) {
1764
+ const before = structuredClone(this._doc);
1765
+ try {
1766
+ fn();
1767
+ validateDocument(this._doc);
1768
+ } catch (error) {
1769
+ this._doc = before;
1770
+ throw error;
1771
+ }
1772
+ if (!this._transactionDepth) {
1773
+ this._undo.push(before);
1774
+ if (this._undo.length > this._historyLimit) this._undo.shift();
1775
+ this._redo = [];
1776
+ this._emit();
1777
+ }
1778
+ return this;
1779
+ }
1780
+ _id(type) {
1781
+ let i = 1;
1782
+ while (this._doc.layers.some((layer) => layer.id === `${type}-${i}`)) i++;
1783
+ return `${type}-${i}`;
1784
+ }
1785
+ add(layer) {
1786
+ const next = { enabled: true, opacity: 1, ...structuredClone(layer) };
1787
+ next.id ??= this._id(next.type);
1788
+ validateLayer(next);
1789
+ return this._change(() => {
1790
+ const i = this._doc.layers.findIndex((layer2) => layer2.id === next.id);
1791
+ if (i < 0) this._doc.layers.push(next);
1792
+ else this._doc.layers[i] = next;
1793
+ });
1794
+ }
1795
+ _operation(type, options) {
1796
+ const { id, label, enabled, opacity, mask, ...params } = options;
1797
+ if ("data" in params && params.data && typeof params.data === "object" && "values" in params.data && ArrayBuffer.isView(params.data.values)) {
1798
+ const data = params.data;
1799
+ Object.assign(params, { data: { ...data, values: Array.from(data.values) } });
1800
+ }
1801
+ const layer = { id: id ?? this._id(type), type, params };
1802
+ const optional = Object.fromEntries(
1803
+ Object.entries({ name: label, enabled, opacity, mask }).filter(
1804
+ ([, value]) => value !== void 0
1805
+ )
1806
+ );
1807
+ return this.add(Object.assign(layer, optional));
1808
+ }
1809
+ layer(id) {
1810
+ const layer = this._doc.layers.find((layer2) => layer2.id === id);
1811
+ if (!layer) throw new Error(`Unknown layer '${id}'`);
1812
+ return structuredClone(layer);
1813
+ }
1814
+ update(id, patch) {
1815
+ const old = this.layer(id);
1816
+ const next = {
1817
+ ...old,
1818
+ ...structuredClone(patch),
1819
+ id,
1820
+ params: { ...old.params, ...structuredClone(patch.params ?? {}) }
1821
+ };
1822
+ const { mask: _mask, ...unmasked } = next;
1823
+ return this.add(patch.mask === null ? unmasked : next);
1824
+ }
1825
+ toggle(id, enabled = this.layer(id).enabled === false) {
1826
+ return this.update(id, { enabled });
1827
+ }
1828
+ move(id, index) {
1829
+ if (!Number.isInteger(index) || index < 0 || index >= this._doc.layers.length)
1830
+ throw new RangeError("Layer index out of bounds");
1831
+ return this._change(() => {
1832
+ const i = this._doc.layers.findIndex((layer2) => layer2.id === id);
1833
+ if (i < 0) throw new Error(`Unknown layer '${id}'`);
1834
+ const layer = this._doc.layers.splice(i, 1)[0];
1835
+ this._doc.layers.splice(index, 0, layer);
1836
+ });
1837
+ }
1838
+ remove(id) {
1839
+ this.layer(id);
1840
+ return this._change(() => {
1841
+ this._doc.layers = this._doc.layers.filter((layer) => layer.id !== id);
1842
+ });
1843
+ }
1844
+ setConfig(patch) {
1845
+ const config = { ...this._doc.config, ...patch };
1846
+ validateConfig(config);
1847
+ return this._change(() => {
1848
+ this._doc.config = config;
1849
+ });
1850
+ }
1851
+ loadJSON(json2) {
1852
+ const next = _Terrain.fromJSON(json2).toJSON();
1853
+ return this._change(() => {
1854
+ this._doc = next;
1855
+ });
1856
+ }
1857
+ transaction(fn) {
1858
+ if (fn.constructor.name === "AsyncFunction")
1859
+ throw new TypeError("Transactions must be synchronous");
1860
+ const before = structuredClone(this._doc);
1861
+ const undo = this._undo.slice();
1862
+ const redo = this._redo.slice();
1863
+ this._transactionDepth++;
1864
+ try {
1865
+ const result = fn(this);
1866
+ if (result && (typeof result === "object" || typeof result === "function") && "then" in result && typeof result.then === "function")
1867
+ throw new TypeError("Transactions must be synchronous");
1868
+ validateDocument(this._doc);
1869
+ this._transactionDepth--;
1870
+ if (!this._transactionDepth) {
1871
+ this._undo.push(before);
1872
+ if (this._undo.length > this._historyLimit) this._undo.shift();
1873
+ this._redo = [];
1874
+ this._emit();
1875
+ }
1876
+ return this;
1877
+ } catch (error) {
1878
+ this._transactionDepth--;
1879
+ this._doc = before;
1880
+ this._undo = undo;
1881
+ this._redo = redo;
1882
+ throw error;
1883
+ }
1884
+ }
1885
+ applyPatch(commands) {
1886
+ return this.transaction(() => {
1887
+ for (const command of commands) {
1888
+ switch (command.op) {
1889
+ case "upsert":
1890
+ this.add(command.layer);
1891
+ break;
1892
+ case "update":
1893
+ this.update(command.id, command.patch);
1894
+ break;
1895
+ case "remove":
1896
+ this.remove(command.id);
1897
+ break;
1898
+ case "move":
1899
+ this.move(command.id, command.index);
1900
+ break;
1901
+ case "toggle":
1902
+ this.toggle(command.id, command.enabled);
1903
+ break;
1904
+ default:
1905
+ throw new Error("Unknown patch command");
1906
+ }
1907
+ }
1908
+ });
1909
+ }
1910
+ undo() {
1911
+ const prior = this._undo.pop();
1912
+ if (prior) {
1913
+ this._redo.push(structuredClone(this._doc));
1914
+ this._doc = prior;
1915
+ this._emit();
1916
+ }
1917
+ return this;
1918
+ }
1919
+ redo() {
1920
+ const next = this._redo.pop();
1921
+ if (next) {
1922
+ this._undo.push(structuredClone(this._doc));
1923
+ this._doc = next;
1924
+ this._emit();
1925
+ }
1926
+ return this;
1927
+ }
1928
+ evaluate(options) {
1929
+ return this._evaluator.evaluate(this._doc, options);
1930
+ }
1931
+ clearCache() {
1932
+ this._evaluator.clear();
1933
+ }
1934
+ copyRegion({ at = [0, 0], size = 64, resolution = 33 } = {}) {
1935
+ if (!RESOLUTIONS.includes(resolution)) throw new RangeError("Unsupported copy resolution");
1936
+ if (!(size > 0 && Number.isFinite(size))) throw new RangeError("Copy size must be positive");
1937
+ const state = this.evaluate();
1938
+ const values = [];
1939
+ for (let z = 0; z < resolution; z++)
1940
+ for (let x = 0; x < resolution; x++)
1941
+ values.push(
1942
+ sampleHeight(
1943
+ state,
1944
+ at[0] + (x / (resolution - 1) - 0.5) * size,
1945
+ at[1] + (z / (resolution - 1) - 0.5) * size
1946
+ )
1947
+ );
1948
+ return { width: resolution, height: resolution, values };
1949
+ }
1950
+ inspect(options) {
1951
+ const state = this.evaluate(options);
1952
+ let min = Number.POSITIVE_INFINITY;
1953
+ let max = Number.NEGATIVE_INFINITY;
1954
+ for (const height of state.height) {
1955
+ min = Math.min(min, height);
1956
+ max = Math.max(max, height);
1957
+ }
1958
+ return {
1959
+ size: state.size,
1960
+ resolution: state.resolution,
1961
+ layers: this._doc.layers.length,
1962
+ vertices: state.height.length,
1963
+ triangles: 2 * (state.resolution - 1) ** 2,
1964
+ minHeight: min,
1965
+ maxHeight: max,
1966
+ instances: state.instances.length,
1967
+ diagnostics: state.diagnostics
1968
+ };
1969
+ }
1970
+ noise(options = {}) {
1971
+ return this._operation("noise", options);
1972
+ }
1973
+ sculpt(options = {}) {
1974
+ return this._operation("sculpt", options);
1975
+ }
1976
+ smooth(options = {}) {
1977
+ return this._operation("smooth", options);
1978
+ }
1979
+ flatten(options = {}) {
1980
+ return this._operation("flatten", options);
1981
+ }
1982
+ ramp(options) {
1983
+ return this._operation("ramp", options);
1984
+ }
1985
+ stamp(options = {}) {
1986
+ return this._operation("stamp", options);
1987
+ }
1988
+ erode(options = {}) {
1989
+ return this._operation("erode", options);
1990
+ }
1991
+ terrace(options = {}) {
1992
+ return this._operation("terrace", options);
1993
+ }
1994
+ materials(options) {
1995
+ return this._operation("materials", options);
1996
+ }
1997
+ paint(options = {}) {
1998
+ return this._operation("paint", options);
1999
+ }
2000
+ biome(options = {}) {
2001
+ return this._operation("biome", options);
2002
+ }
2003
+ scatter(options = {}) {
2004
+ return this._operation("scatter", options);
2005
+ }
2006
+ clear(options) {
2007
+ return this._operation("clear", options);
2008
+ }
2009
+ road(options) {
2010
+ return this._operation("road", options);
2011
+ }
2012
+ river(options) {
2013
+ return this._operation("river", options);
2014
+ }
2015
+ water(options = {}) {
2016
+ return this._operation("water", options);
2017
+ }
2018
+ heightmap(options) {
2019
+ return this._operation("heightmap", options);
2020
+ }
2021
+ paste(options) {
2022
+ return this._operation("paste", options);
2023
+ }
2024
+ };
2025
+
2026
+ // src/core/bake.ts
2027
+ var toLinear = (color) => color <= 0.04045 ? color / 12.92 : ((color + 0.055) / 1.055) ** 2.4;
2028
+ function bakeMesh(state, {
2029
+ step = 1,
2030
+ x = 0,
2031
+ z = 0,
2032
+ cells = state.resolution - 1,
2033
+ skirtDepth = 0,
2034
+ palette
2035
+ } = {}) {
2036
+ if (!Number.isFinite(skirtDepth) || skirtDepth < 0)
2037
+ throw new RangeError("skirtDepth must be finite and nonnegative");
2038
+ const n = state.resolution;
2039
+ if (!Number.isInteger(n) || n < 2 || !Number.isFinite(state.size) || state.size <= 0)
2040
+ throw new RangeError("Invalid heightfield extent");
2041
+ if (!Number.isInteger(step) || step < 1 || (step & step - 1) !== 0 || cells % step !== 0)
2042
+ throw new RangeError("step must be a power of two dividing chunk cells");
2043
+ if (![x, z, cells].every(Number.isInteger) || x < 0 || z < 0 || cells < 1 || x + cells >= n || z + cells >= n)
2044
+ throw new RangeError("Chunk outside heightfield");
2045
+ if (state.height.length !== n * n || !state.height.every(Number.isFinite))
2046
+ throw new RangeError("Invalid heightfield samples");
2047
+ if (palette && (palette.length !== 8 || Array.from(palette).some(
2048
+ (rgb) => !Array.isArray(rgb) || rgb.length !== 3 || Array.from(rgb).some((value) => !Number.isFinite(value) || value < 0 || value > 1)
2049
+ )))
2050
+ throw new RangeError("palette requires eight finite sRGB triples in 0..1");
2051
+ if (palette && (state.splat.length !== n * n * 8 || !state.splat.every(Number.isFinite)))
2052
+ throw new RangeError("Invalid splat samples");
2053
+ const side = cells / step + 1;
2054
+ const base = side * side;
2055
+ const ring = [];
2056
+ if (skirtDepth > 0) {
2057
+ for (let k2 = 0; k2 < side - 1; k2++) ring.push(k2);
2058
+ for (let k2 = 0; k2 < side - 1; k2++) ring.push(k2 * side + side - 1);
2059
+ for (let k2 = side - 1; k2 > 0; k2--) ring.push((side - 1) * side + k2);
2060
+ for (let k2 = side - 1; k2 > 0; k2--) ring.push(k2 * side);
2061
+ }
2062
+ const count = base + ring.length;
2063
+ const positions = new Float32Array(count * 3);
2064
+ const normals = new Float32Array(count * 3);
2065
+ const uvs = new Float32Array(count * 2);
2066
+ const indices = new Uint32Array((side - 1) ** 2 * 6 + ring.length * 6);
2067
+ const colors = palette ? new Float32Array(count * 3) : void 0;
2068
+ const cell = state.size / (n - 1);
2069
+ for (let zz = 0; zz < side; zz++)
2070
+ for (let xx = 0; xx < side; xx++) {
2071
+ const gx = x + xx * step;
2072
+ const gz = z + zz * step;
2073
+ const i = gz * n + gx;
2074
+ const j = zz * side + xx;
2075
+ const dx = (state.height[gz * n + Math.min(n - 1, gx + 1)] - state.height[gz * n + Math.max(0, gx - 1)]) / ((gx > 0 && gx < n - 1 ? 2 : 1) * cell);
2076
+ const dz = (state.height[Math.min(n - 1, gz + 1) * n + gx] - state.height[Math.max(0, gz - 1) * n + gx]) / ((gz > 0 && gz < n - 1 ? 2 : 1) * cell);
2077
+ const length = Math.hypot(dx, 1, dz);
2078
+ positions.set([world(state, gx), state.height[i], world(state, gz)], j * 3);
2079
+ normals.set([-dx / length, 1 / length, -dz / length], j * 3);
2080
+ if (colors && palette)
2081
+ for (let k2 = 0; k2 < 3; k2++) {
2082
+ let color = 0;
2083
+ for (let c = 0; c < 8; c++)
2084
+ color += toLinear(palette[c]?.[k2]) * state.splat[i * 8 + c];
2085
+ colors[j * 3 + k2] = color;
2086
+ }
2087
+ uvs.set([gx / (n - 1), 1 - gz / (n - 1)], j * 2);
2088
+ }
2089
+ let k = 0;
2090
+ for (let zz = 0; zz < side - 1; zz++)
2091
+ for (let xx = 0; xx < side - 1; xx++) {
2092
+ const a = zz * side + xx;
2093
+ const b = a + 1;
2094
+ const c = a + side;
2095
+ const d = c + 1;
2096
+ indices.set([a, c, b, b, c, d], k);
2097
+ k += 6;
2098
+ }
2099
+ for (let j = 0; j < ring.length; j++) {
2100
+ const top = ring[j];
2101
+ const bottom = base + j;
2102
+ positions.set(positions.subarray(top * 3, top * 3 + 3), bottom * 3);
2103
+ positions[bottom * 3 + 1] = positions[bottom * 3 + 1] - skirtDepth;
2104
+ normals.set(normals.subarray(top * 3, top * 3 + 3), bottom * 3);
2105
+ colors?.set(colors.subarray(top * 3, top * 3 + 3), bottom * 3);
2106
+ uvs.set(uvs.subarray(top * 2, top * 2 + 2), bottom * 2);
2107
+ const next = (j + 1) % ring.length;
2108
+ indices.set([top, ring[next], bottom, ring[next], base + next, bottom], k);
2109
+ k += 6;
2110
+ }
2111
+ return {
2112
+ name: `terrain_${x}_${z}_step${step}`,
2113
+ positions,
2114
+ normals,
2115
+ colors,
2116
+ uvs,
2117
+ indices,
2118
+ side,
2119
+ topVertexCount: base,
2120
+ bounds: { x, z, cells, step },
2121
+ skirtDepth
2122
+ };
2123
+ }
2124
+ function bakeTerrain(state, {
2125
+ chunkCells = Math.min(64, state.resolution - 1),
2126
+ lodSteps = [1, 2, 4],
2127
+ skirtDepth = 6,
2128
+ palette
2129
+ } = {}) {
2130
+ if (!Number.isInteger(chunkCells) || chunkCells < 1 || (state.resolution - 1) % chunkCells !== 0)
2131
+ throw new RangeError("chunkCells must divide resolution - 1");
2132
+ if (!lodSteps.length) throw new RangeError("lodSteps must not be empty");
2133
+ const lods = lodSteps.map((step) => {
2134
+ const chunks = [];
2135
+ for (let z = 0; z < state.resolution - 1; z += chunkCells)
2136
+ for (let x = 0; x < state.resolution - 1; x += chunkCells)
2137
+ chunks.push(bakeMesh(state, { x, z, cells: chunkCells, step, skirtDepth, palette }));
2138
+ return { step, chunks };
2139
+ });
2140
+ return {
2141
+ version: 1,
2142
+ size: state.size,
2143
+ resolution: state.resolution,
2144
+ lods,
2145
+ collision: {
2146
+ type: "heightfield",
2147
+ size: state.size,
2148
+ resolution: state.resolution,
2149
+ cellSize: state.size / (state.resolution - 1),
2150
+ origin: [-state.size / 2, 0, -state.size / 2],
2151
+ heights: state.height.slice()
2152
+ },
2153
+ instances: structuredClone(state.instances),
2154
+ materialChannels: [...MATERIAL_IDS]
2155
+ };
2156
+ }
2157
+ function validateBakedMesh(mesh) {
2158
+ if (![mesh.positions, mesh.normals, mesh.uvs, ...mesh.colors ? [mesh.colors] : []].every(
2159
+ (array) => array instanceof Float32Array
2160
+ ) || !(mesh.indices instanceof Uint32Array))
2161
+ throw new TypeError("Baked mesh requires Float32 attributes and Uint32 indices");
2162
+ const count = mesh.positions.length / 3;
2163
+ if (!count || !Number.isInteger(count) || mesh.normals.length !== count * 3 || mesh.uvs.length !== count * 2 || mesh.colors && mesh.colors.length !== count * 3 || !mesh.indices.length || mesh.indices.length % 3)
2164
+ throw new RangeError("Baked mesh attribute counts do not agree");
2165
+ for (const array of [
2166
+ mesh.positions,
2167
+ mesh.normals,
2168
+ mesh.uvs,
2169
+ ...mesh.colors ? [mesh.colors] : []
2170
+ ])
2171
+ if (!array.every(Number.isFinite)) throw new RangeError("Baked mesh attributes must be finite");
2172
+ if (mesh.indices.some((index) => index >= count))
2173
+ throw new RangeError("Baked mesh index outside vertex range");
2174
+ }
2175
+
2176
+ // src/core/placements.ts
2177
+ function validatePlacementOverrides(value = {}) {
2178
+ finiteTree(value, "placementOverrides");
2179
+ if (!value || typeof value !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
2180
+ throw new Error("Expected placementOverrides object");
2181
+ return Object.fromEntries(
2182
+ Object.entries(value).map(([key, entry]) => {
2183
+ if (!key.length || key.length > 256)
2184
+ throw new Error("Placement key must contain 1\u2013256 characters");
2185
+ if (!entry || typeof entry !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(entry)))
2186
+ throw new Error(`Invalid placement override '${key}'`);
2187
+ const input = entry;
2188
+ if (Object.keys(input).some(
2189
+ (field) => !["position", "quaternion", "scale", "grounding"].includes(field)
2190
+ ))
2191
+ throw new Error(`Unknown placement override fields '${key}'`);
2192
+ function vector(field, length) {
2193
+ const values = input[field];
2194
+ if (!Array.isArray(values) || values.length !== length || !Array.from(values).every(Number.isFinite))
2195
+ throw new Error(`${key}.${field} must contain ${length} finite numbers`);
2196
+ return Array.from(values);
2197
+ }
2198
+ const position = vector("position", 3);
2199
+ const quaternion = vector("quaternion", 4);
2200
+ const scale = vector("scale", 3);
2201
+ if (scale.some((item) => item <= 0)) throw new Error(`${key}.scale must be positive`);
2202
+ if (Math.abs(Math.hypot(...quaternion) - 1) > 1e-6)
2203
+ throw new Error(`${key}.quaternion must be a unit quaternion`);
2204
+ const grounding = input.grounding === void 0 ? true : input.grounding;
2205
+ if (typeof grounding !== "boolean") throw new Error(`${key}.grounding must be boolean`);
2206
+ return [key, { position, quaternion, scale, grounding }];
2207
+ })
2208
+ );
2209
+ }
2210
+ function applyPlacementOverrides(state, value) {
2211
+ const overrides = validatePlacementOverrides(value);
2212
+ const keys = new Set(state.instances.map((item) => item.id));
2213
+ return {
2214
+ ...state,
2215
+ instances: state.instances.map((item) => {
2216
+ const { transform: _, ...base } = item;
2217
+ return Object.hasOwn(overrides, item.id) ? { ...base, transform: overrides[item.id] } : base;
2218
+ }),
2219
+ diagnostics: [
2220
+ ...state.diagnostics.filter(
2221
+ (message) => !message.startsWith("Unmatched placement override '")
2222
+ ),
2223
+ ...Object.keys(overrides).filter((key) => !keys.has(key)).map((key) => `Unmatched placement override '${key}'`)
2224
+ ]
2225
+ };
2226
+ }
2227
+
2228
+ // src/core/io.ts
2229
+ var encoder = new TextEncoder();
2230
+ var decoder = new TextDecoder();
2231
+ var crcTable = Uint32Array.from({ length: 256 }, (_, initial) => {
2232
+ let n = initial;
2233
+ for (let k = 0; k < 8; k++) n = n & 1 ? 3988292384 ^ n >>> 1 : n >>> 1;
2234
+ return n >>> 0;
2235
+ });
2236
+ var crc32 = (bytes) => {
2237
+ let c = 4294967295;
2238
+ for (const b of bytes) c = crcTable[(c ^ b) & 255] ^ c >>> 8;
2239
+ return (c ^ 4294967295) >>> 0;
2240
+ };
2241
+ var join = (arrays) => {
2242
+ const out = new Uint8Array(arrays.reduce((n, a) => n + a.length, 0));
2243
+ let offset = 0;
2244
+ for (const a of arrays) {
2245
+ out.set(a, offset);
2246
+ offset += a.length;
2247
+ }
2248
+ return out;
2249
+ };
2250
+ function heightRange(values, { min, max } = {}) {
2251
+ for (const v of values) if (!Number.isFinite(v)) throw TypeError("Height data must be finite");
2252
+ if (min === void 0 || max === void 0) {
2253
+ let lo = Number.POSITIVE_INFINITY;
2254
+ let hi = Number.NEGATIVE_INFINITY;
2255
+ for (const v of values) {
2256
+ if (!Number.isFinite(v)) throw TypeError("Height data must be finite");
2257
+ lo = Math.min(lo, v);
2258
+ hi = Math.max(hi, v);
2259
+ }
2260
+ min ??= lo;
2261
+ max ??= hi > lo ? hi : lo + 1;
2262
+ }
2263
+ if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min)
2264
+ throw RangeError("Height range must be finite and max > min");
2265
+ return { min, max };
2266
+ }
2267
+ function encodeRAW16(values, options = {}) {
2268
+ const { min, max } = heightRange(values, options);
2269
+ const bytes = new Uint8Array(values.length * 2);
2270
+ const view = new DataView(bytes.buffer);
2271
+ for (let i = 0; i < values.length; i++)
2272
+ view.setUint16(
2273
+ i * 2,
2274
+ Math.round(clamp((values[i] - min) / (max - min)) * 65535),
2275
+ options.littleEndian !== false
2276
+ );
2277
+ return bytes;
2278
+ }
2279
+ function decodeRAW16(input, { width, height, min = 0, max = 1, littleEndian = true }) {
2280
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
2281
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width * height > 4097 ** 2 || bytes.length !== width * height * 2)
2282
+ throw RangeError("RAW byte length must match width \xD7 height \xD7 2");
2283
+ heightRange([min, max], { min, max });
2284
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2285
+ const values = new Float32Array(width * height);
2286
+ for (let i = 0; i < values.length; i++)
2287
+ values[i] = min + view.getUint16(i * 2, littleEndian) / 65535 * (max - min);
2288
+ return { width, height, values, min, max };
2289
+ }
2290
+ function chunk(type, content) {
2291
+ const bytes = new Uint8Array(content.length + 12);
2292
+ const view = new DataView(bytes.buffer);
2293
+ view.setUint32(0, content.length);
2294
+ bytes.set(encoder.encode(type), 4);
2295
+ bytes.set(content, 8);
2296
+ view.setUint32(bytes.length - 4, crc32(bytes.subarray(4, bytes.length - 4)));
2297
+ return bytes;
2298
+ }
2299
+ async function compress(bytes) {
2300
+ if (typeof CompressionStream === "undefined")
2301
+ throw Error("PNG export requires CompressionStream (modern browser or Node 22+)");
2302
+ return new Uint8Array(
2303
+ await new Response(
2304
+ new Blob([new Uint8Array(bytes)]).stream().pipeThrough(new CompressionStream("deflate"))
2305
+ ).arrayBuffer()
2306
+ );
2307
+ }
2308
+ async function inflate(bytes, expected) {
2309
+ if (typeof DecompressionStream === "undefined")
2310
+ throw Error("PNG import requires DecompressionStream");
2311
+ const reader = new Blob([new Uint8Array(bytes)]).stream().pipeThrough(new DecompressionStream("deflate")).getReader();
2312
+ const parts = [];
2313
+ let length = 0;
2314
+ try {
2315
+ while (true) {
2316
+ const { done, value } = await reader.read();
2317
+ if (done) break;
2318
+ length += value.length;
2319
+ if (length > expected) {
2320
+ await reader.cancel();
2321
+ throw Error("PNG decompressed size exceeds declared dimensions");
2322
+ }
2323
+ parts.push(value);
2324
+ }
2325
+ } finally {
2326
+ reader.releaseLock();
2327
+ }
2328
+ if (length !== expected) throw Error("PNG decompressed byte length mismatch");
2329
+ return join(parts);
2330
+ }
2331
+ async function encodePNG(width, height, bytes, bitDepth, colorType, metadata) {
2332
+ const channels = colorType === 6 ? 4 : 1;
2333
+ const rowBytes = width * channels * (bitDepth / 8);
2334
+ if (bytes.length !== rowBytes * height) throw RangeError("PNG pixel byte count mismatch");
2335
+ const raw = new Uint8Array((rowBytes + 1) * height);
2336
+ for (let y = 0; y < height; y++)
2337
+ raw.set(bytes.subarray(y * rowBytes, (y + 1) * rowBytes), y * (rowBytes + 1) + 1);
2338
+ const ihdr = new Uint8Array(13);
2339
+ const v = new DataView(ihdr.buffer);
2340
+ v.setUint32(0, width);
2341
+ v.setUint32(4, height);
2342
+ ihdr[8] = bitDepth;
2343
+ ihdr[9] = colorType;
2344
+ const parts = [new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]), chunk("IHDR", ihdr)];
2345
+ if (metadata)
2346
+ parts.push(chunk("tEXt", encoder.encode(`StrataHeightRange\0${JSON.stringify(metadata)}`)));
2347
+ parts.push(chunk("IDAT", await compress(raw)), chunk("IEND", new Uint8Array()));
2348
+ return join(parts);
2349
+ }
2350
+ async function encodeHeightPNG(state, options = {}) {
2351
+ const values = state.height;
2352
+ const range2 = heightRange(values, options);
2353
+ const bytes = encodeRAW16(values, { ...range2, littleEndian: false });
2354
+ return encodePNG(state.resolution, state.resolution, bytes, 16, 0, range2);
2355
+ }
2356
+ async function encodeSplatPNGs(state) {
2357
+ const maps = [];
2358
+ for (let pack = 0; pack < 2; pack++) {
2359
+ const bytes = new Uint8Array(state.height.length * 4);
2360
+ for (let i = 0; i < state.height.length; i++)
2361
+ for (let c = 0; c < 4; c++)
2362
+ bytes[i * 4 + c] = Math.round(clamp(state.splat[i * 8 + pack * 4 + c]) * 255);
2363
+ maps.push({
2364
+ channels: MATERIAL_IDS.slice(pack * 4, pack * 4 + 4),
2365
+ bytes: await encodePNG(state.resolution, state.resolution, bytes, 8, 6)
2366
+ });
2367
+ }
2368
+ return maps;
2369
+ }
2370
+ var paeth = (a, b, c) => {
2371
+ const p = a + b - c;
2372
+ const pa = Math.abs(p - a);
2373
+ const pb = Math.abs(p - b);
2374
+ const pc = Math.abs(p - c);
2375
+ return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
2376
+ };
2377
+ async function decodeHeightPNG(input, options = {}) {
2378
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
2379
+ const signature = [137, 80, 78, 71, 13, 10, 26, 10];
2380
+ if (bytes.length < 33 || !signature.every((v, i) => bytes[i] === v))
2381
+ throw Error("Not a PNG file");
2382
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2383
+ const idat = [];
2384
+ let offset = 8;
2385
+ let width = 0;
2386
+ let height = 0;
2387
+ let bitDepth = 0;
2388
+ let colorType = 0;
2389
+ let metadata = {};
2390
+ let ended = false;
2391
+ let seenHeader = false;
2392
+ while (offset + 12 <= bytes.length) {
2393
+ const length = view.getUint32(offset);
2394
+ const type = decoder.decode(bytes.subarray(offset + 4, offset + 8));
2395
+ const end = offset + 12 + length;
2396
+ if (length > 64 * 1024 * 1024 || end > bytes.length)
2397
+ throw Error("Truncated or oversized PNG chunk");
2398
+ if (crc32(bytes.subarray(offset + 4, end - 4)) !== view.getUint32(end - 4))
2399
+ throw Error(`PNG CRC mismatch in ${type}`);
2400
+ const data = bytes.subarray(offset + 8, end - 4);
2401
+ if (!seenHeader && type !== "IHDR") throw Error("PNG IHDR must be first");
2402
+ if (type === "IHDR") {
2403
+ if (seenHeader || length !== 13) throw Error("Invalid PNG IHDR");
2404
+ seenHeader = true;
2405
+ width = view.getUint32(offset + 8);
2406
+ height = view.getUint32(offset + 12);
2407
+ bitDepth = data[8];
2408
+ colorType = data[9];
2409
+ if (!width || !height || width > 4097 || height > 4097)
2410
+ throw Error("PNG dimensions must be 1..4097");
2411
+ if (colorType !== 0 || ![8, 16].includes(bitDepth) || data[12] !== 0 || data[10] !== 0 || data[11] !== 0)
2412
+ throw Error("Heightmap import supports non-interlaced 8/16-bit grayscale PNG only");
2413
+ }
2414
+ if (type === "tEXt") {
2415
+ const text = decoder.decode(data);
2416
+ if (text.startsWith("StrataHeightRange\0")) {
2417
+ try {
2418
+ metadata = JSON.parse(text.slice(18));
2419
+ } catch {
2420
+ throw Error("Invalid PNG height range metadata");
2421
+ }
2422
+ }
2423
+ }
2424
+ if (type === "IDAT") idat.push(data);
2425
+ if (type === "IEND") {
2426
+ ended = true;
2427
+ break;
2428
+ }
2429
+ offset = end;
2430
+ }
2431
+ if (!ended || !idat.length) throw Error("Incomplete PNG chunks");
2432
+ const bpp = bitDepth / 8;
2433
+ const row = width * bpp;
2434
+ const raw = await inflate(join(idat), (row + 1) * height);
2435
+ const pixels = new Uint8Array(row * height);
2436
+ for (let y = 0; y < height; y++) {
2437
+ const filter = raw[y * (row + 1)];
2438
+ if (filter > 4) throw Error("Unsupported PNG row filter");
2439
+ for (let x = 0; x < row; x++) {
2440
+ const v = raw[y * (row + 1) + 1 + x];
2441
+ const i = y * row + x;
2442
+ const a = x >= bpp ? pixels[i - bpp] : 0;
2443
+ const b = y > 0 ? pixels[i - row] : 0;
2444
+ const c = y > 0 && x >= bpp ? pixels[i - row - bpp] : 0;
2445
+ pixels[i] = v + (filter === 1 ? a : filter === 2 ? b : filter === 3 ? Math.floor((a + b) / 2) : filter === 4 ? paeth(a, b, c) : 0) & 255;
2446
+ }
2447
+ }
2448
+ const { min, max } = heightRange([0, 1], {
2449
+ min: options.min ?? metadata.min ?? 0,
2450
+ max: options.max ?? metadata.max ?? 1
2451
+ });
2452
+ const values = new Float32Array(width * height);
2453
+ for (let i = 0; i < values.length; i++) {
2454
+ const v = bpp === 2 ? pixels[i * 2] << 8 | pixels[i * 2 + 1] : pixels[i];
2455
+ values[i] = min + v / (bpp === 2 ? 65535 : 255) * (max - min);
2456
+ }
2457
+ return {
2458
+ width,
2459
+ height,
2460
+ values,
2461
+ min,
2462
+ max,
2463
+ bitDepth,
2464
+ hasEmbeddedRange: Number.isFinite(metadata.min) && Number.isFinite(metadata.max)
2465
+ };
2466
+ }
2467
+ function encodeGLB(input) {
2468
+ const meshes = Array.isArray(input) ? input : [input];
2469
+ if (!meshes.length) throw new RangeError("GLB requires at least one baked mesh");
2470
+ for (const mesh of meshes) validateBakedMesh(mesh);
2471
+ const chunks = [];
2472
+ const bufferViews = [];
2473
+ const accessors = [];
2474
+ const gltfMeshes = [];
2475
+ let offset = 0;
2476
+ const add = (array, type, componentType, target, minMax = false) => {
2477
+ const aligned = offset + 3 & -4;
2478
+ if (aligned > offset) {
2479
+ chunks.push(new Uint8Array(aligned - offset));
2480
+ offset = aligned;
2481
+ }
2482
+ const bytes = new Uint8Array(array.buffer, array.byteOffset, array.byteLength).slice();
2483
+ const view = bufferViews.length;
2484
+ bufferViews.push({ buffer: 0, byteOffset: offset, byteLength: bytes.length, target });
2485
+ chunks.push(bytes);
2486
+ offset += bytes.length;
2487
+ const size = type === "VEC3" ? 3 : type === "VEC2" ? 2 : 1;
2488
+ const accessor = {
2489
+ bufferView: view,
2490
+ componentType,
2491
+ count: array.length / size,
2492
+ type
2493
+ };
2494
+ if (minMax) {
2495
+ accessor.min = Array(size).fill(Number.POSITIVE_INFINITY);
2496
+ accessor.max = Array(size).fill(Number.NEGATIVE_INFINITY);
2497
+ for (let i = 0; i < array.length; i++) {
2498
+ const c = i % size;
2499
+ accessor.min[c] = Math.min(accessor.min[c], array[i]);
2500
+ accessor.max[c] = Math.max(accessor.max[c], array[i]);
2501
+ }
2502
+ }
2503
+ accessors.push(accessor);
2504
+ return accessors.length - 1;
2505
+ };
2506
+ for (const m of meshes) {
2507
+ const attributes = {
2508
+ POSITION: add(m.positions, "VEC3", 5126, 34962, true),
2509
+ NORMAL: add(m.normals, "VEC3", 5126, 34962),
2510
+ TEXCOORD_0: add(m.uvs, "VEC2", 5126, 34962)
2511
+ };
2512
+ if (m.colors) attributes.COLOR_0 = add(m.colors, "VEC3", 5126, 34962);
2513
+ gltfMeshes.push({
2514
+ name: m.name ?? "Terrain",
2515
+ primitives: [{ attributes, indices: add(m.indices, "SCALAR", 5125, 34963), mode: 4 }]
2516
+ });
2517
+ }
2518
+ const binary = join(chunks);
2519
+ const doc = {
2520
+ asset: { version: "2.0", generator: "Strata Terrain 0.1.0" },
2521
+ scene: 0,
2522
+ scenes: [{ nodes: meshes.map((_, i) => i) }],
2523
+ nodes: meshes.map((m, i) => ({ mesh: i, name: m.name ?? `Terrain ${i}` })),
2524
+ meshes: gltfMeshes,
2525
+ buffers: [{ byteLength: binary.length }],
2526
+ bufferViews,
2527
+ accessors
2528
+ };
2529
+ const json2 = encoder.encode(JSON.stringify(doc));
2530
+ const jsonLength = json2.length + 3 & -4;
2531
+ const binLength = binary.length + 3 & -4;
2532
+ const out = new Uint8Array(12 + 8 + jsonLength + 8 + binLength);
2533
+ const v = new DataView(out.buffer);
2534
+ v.setUint32(0, 1179937895, true);
2535
+ v.setUint32(4, 2, true);
2536
+ v.setUint32(8, out.length, true);
2537
+ v.setUint32(12, jsonLength, true);
2538
+ v.setUint32(16, 1313821514, true);
2539
+ out.fill(32, 20, 20 + jsonLength);
2540
+ out.set(json2, 20);
2541
+ const start = 20 + jsonLength;
2542
+ v.setUint32(start, binLength, true);
2543
+ v.setUint32(start + 4, 5130562, true);
2544
+ out.set(binary, start + 8);
2545
+ return out;
2546
+ }
2547
+
2548
+ // src/core/export.ts
2549
+ var enc = new TextEncoder();
2550
+ var table = Uint32Array.from({ length: 256 }, (_, initial) => {
2551
+ let n = initial;
2552
+ for (let k = 0; k < 8; k++) n = n & 1 ? 3988292384 ^ n >>> 1 : n >>> 1;
2553
+ return n >>> 0;
2554
+ });
2555
+ function crc322(b) {
2556
+ let c = 4294967295;
2557
+ for (const v of b) c = table[(c ^ v) & 255] ^ c >>> 8;
2558
+ return (c ^ 4294967295) >>> 0;
2559
+ }
2560
+ function concat(parts) {
2561
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
2562
+ let o = 0;
2563
+ for (const p of parts) {
2564
+ out.set(p, o);
2565
+ o += p.length;
2566
+ }
2567
+ return out;
2568
+ }
2569
+ function encodeZIP(files) {
2570
+ if (files.length > 65535) throw RangeError("Too many ZIP entries");
2571
+ const parts = [];
2572
+ const central = [];
2573
+ const names = /* @__PURE__ */ new Set();
2574
+ let offset = 0;
2575
+ for (const { name, bytes } of files) {
2576
+ if (!/^[a-zA-Z0-9_.\/-]+$/.test(name) || name.startsWith("/") || name.split("/").includes("..") || names.has(name))
2577
+ throw Error("Unsafe or duplicate ZIP filename");
2578
+ if (!(bytes instanceof Uint8Array)) throw TypeError("ZIP bytes must be Uint8Array");
2579
+ names.add(name);
2580
+ const nb = enc.encode(name);
2581
+ const crc = crc322(bytes);
2582
+ const h = new Uint8Array(30 + nb.length);
2583
+ const v = new DataView(h.buffer);
2584
+ v.setUint32(0, 67324752, true);
2585
+ v.setUint16(4, 20, true);
2586
+ v.setUint16(12, 33, true);
2587
+ v.setUint32(14, crc, true);
2588
+ v.setUint32(18, bytes.length, true);
2589
+ v.setUint32(22, bytes.length, true);
2590
+ v.setUint16(26, nb.length, true);
2591
+ h.set(nb, 30);
2592
+ parts.push(h, bytes);
2593
+ const c = new Uint8Array(46 + nb.length);
2594
+ const cv = new DataView(c.buffer);
2595
+ cv.setUint32(0, 33639248, true);
2596
+ cv.setUint16(4, 20, true);
2597
+ cv.setUint16(6, 20, true);
2598
+ cv.setUint16(14, 33, true);
2599
+ cv.setUint32(16, crc, true);
2600
+ cv.setUint32(20, bytes.length, true);
2601
+ cv.setUint32(24, bytes.length, true);
2602
+ cv.setUint16(28, nb.length, true);
2603
+ cv.setUint32(42, offset, true);
2604
+ c.set(nb, 46);
2605
+ central.push(c);
2606
+ offset += h.length + bytes.length;
2607
+ if (offset > 4294967295) throw RangeError("ZIP64 is not supported");
2608
+ }
2609
+ const directory = concat(central);
2610
+ const end = new Uint8Array(22);
2611
+ const e = new DataView(end.buffer);
2612
+ e.setUint32(0, 101010256, true);
2613
+ e.setUint16(8, files.length, true);
2614
+ e.setUint16(10, files.length, true);
2615
+ e.setUint32(12, directory.length, true);
2616
+ e.setUint32(16, offset, true);
2617
+ return concat([...parts, directory, end]);
2618
+ }
2619
+ var json = (name, data) => ({
2620
+ name,
2621
+ bytes: enc.encode(JSON.stringify(data, null, 2))
2622
+ });
2623
+ async function makeExport(state, recipe, kind) {
2624
+ const result = (name, type, bytes) => ({
2625
+ name,
2626
+ type,
2627
+ bytes
2628
+ });
2629
+ if (kind === "project")
2630
+ return result("terrain-recipe.json", "application/json", json("", recipe).bytes);
2631
+ if (kind === "png")
2632
+ return result("terrain-height16.png", "image/png", await encodeHeightPNG(state));
2633
+ if (kind === "glb") return result("terrain.glb", "model/gltf-binary", encodeGLB(bakeMesh(state)));
2634
+ let min = Number.POSITIVE_INFINITY;
2635
+ let max = Number.NEGATIVE_INFINITY;
2636
+ for (const h of state.height) {
2637
+ min = Math.min(min, h);
2638
+ max = Math.max(max, h);
2639
+ }
2640
+ if (max === min) max = min + 1;
2641
+ const range2 = {
2642
+ resolution: state.resolution,
2643
+ size: state.size,
2644
+ min,
2645
+ max,
2646
+ littleEndian: true,
2647
+ units: "metres",
2648
+ rowOrder: "z increases from -size/2 to +size/2",
2649
+ columnOrder: "x increases from -size/2 to +size/2"
2650
+ };
2651
+ if (kind === "raw")
2652
+ return result(
2653
+ "terrain-raw16.zip",
2654
+ "application/zip",
2655
+ encodeZIP([
2656
+ { name: "height.raw", bytes: encodeRAW16(state.height, { min, max }) },
2657
+ json("heightfield.json", range2)
2658
+ ])
2659
+ );
2660
+ const splats = await encodeSplatPNGs(state);
2661
+ const splatFiles = splats.map((s, i) => ({ name: `splat-${i}.png`, bytes: s.bytes }));
2662
+ if (kind === "splat")
2663
+ return result(
2664
+ "terrain-splatmaps.zip",
2665
+ "application/zip",
2666
+ encodeZIP([
2667
+ ...splatFiles,
2668
+ json(
2669
+ "channels.json",
2670
+ splats.map((s, i) => ({ file: `splat-${i}.png`, rgba: s.channels }))
2671
+ )
2672
+ ])
2673
+ );
2674
+ if (kind !== "runtime") throw Error(`Unknown export '${kind}'`);
2675
+ const baked = bakeTerrain(state);
2676
+ const collision = new Uint8Array(state.height.length * 4);
2677
+ const dv = new DataView(collision.buffer);
2678
+ state.height.forEach((h, i) => dv.setFloat32(i * 4, h, true));
2679
+ const manifest = {
2680
+ version: 1,
2681
+ size: state.size,
2682
+ resolution: state.resolution,
2683
+ units: "metres",
2684
+ axis: "Y up",
2685
+ lods: baked.lods.map((l) => ({
2686
+ step: l.step,
2687
+ file: `lod-${l.step}.glb`,
2688
+ chunks: l.chunks.map((c) => ({ name: c.name, ...c.bounds, skirtDepth: c.skirtDepth }))
2689
+ })),
2690
+ collision: {
2691
+ format: "float32-le",
2692
+ file: "collision.f32le",
2693
+ resolution: state.resolution,
2694
+ cellSize: baked.collision.cellSize,
2695
+ origin: baked.collision.origin
2696
+ },
2697
+ heightRange: range2,
2698
+ splats: splats.map((s, i) => ({ file: `splat-${i}.png`, rgba: s.channels })),
2699
+ placements: "placements.json",
2700
+ water: "water.json",
2701
+ limitations: [
2702
+ "Static LODs; runtime selection and streaming not provided",
2703
+ "GLBs contain terrain only; supply assets for placement IDs",
2704
+ "No navigation mesh"
2705
+ ]
2706
+ };
2707
+ const water = state.waters.map((w, i) => {
2708
+ const { mask, ...rest } = w;
2709
+ return { ...rest, maskFile: `water-${i}.mask8` };
2710
+ });
2711
+ const files = [
2712
+ json("manifest.json", manifest),
2713
+ json("recipe.json", recipe),
2714
+ json("placements.json", baked.instances),
2715
+ json("water.json", { bodies: water, rivers: state.rivers }),
2716
+ { name: "collision.f32le", bytes: collision },
2717
+ { name: "height16.png", bytes: await encodeHeightPNG(state) },
2718
+ ...splatFiles,
2719
+ ...baked.lods.map((l) => ({ name: `lod-${l.step}.glb`, bytes: encodeGLB(l.chunks) }))
2720
+ ];
2721
+ for (let i = 0; i < water.length; i++)
2722
+ files.push({
2723
+ name: water[i]?.maskFile,
2724
+ bytes: new Uint8Array(state.waters[i]?.mask ?? [])
2725
+ });
2726
+ return result("terrain-runtime.zip", "application/zip", encodeZIP(files));
2727
+ }
2728
+
2729
+ // src/core/world-package.ts
2730
+ var HEIGHTMAP_MAX = 65535;
2731
+ var RECORD_VALUES = 8;
2732
+ var COMPONENTS = ["r", "g", "b", "a"];
2733
+ var PLANES = MATERIAL_IDS.length / COMPONENTS.length;
2734
+ var SCALE_TOLERANCE = 1e-3;
2735
+ var SLOTS = MATERIAL_IDS.map((id, channel) => ({
2736
+ channel,
2737
+ component: COMPONENTS[channel % COMPONENTS.length],
2738
+ id,
2739
+ plane: Math.floor(channel / COMPONENTS.length)
2740
+ }));
2741
+ function poseOf(item) {
2742
+ if (item.transform === void 0)
2743
+ return {
2744
+ position: item.position,
2745
+ quaternion: [0, Math.sin(item.rotation / 2), 0, Math.cos(item.rotation / 2)],
2746
+ scale: [item.scale, item.scale, item.scale]
2747
+ };
2748
+ const { position, quaternion, scale } = item.transform;
2749
+ return { position, quaternion, scale };
2750
+ }
2751
+ function uniformScale(asset, id, scale) {
2752
+ const mean = (scale[0] + scale[1] + scale[2]) / 3;
2753
+ const deviation = Math.max(
2754
+ Math.abs(scale[0] - mean),
2755
+ Math.abs(scale[1] - mean),
2756
+ Math.abs(scale[2] - mean)
2757
+ );
2758
+ if (!(mean > 0) || deviation > mean * SCALE_TOLERANCE)
2759
+ throw Error(
2760
+ `World package: placement '${id}' of asset '${asset}' is scaled ${JSON.stringify(scale)}; a placement record carries one uniform scale.`
2761
+ );
2762
+ return mean;
2763
+ }
2764
+ function placementRecord(item) {
2765
+ if (item.transform === void 0 && !Number.isFinite(item.rotation))
2766
+ throw Error(`World package: placement '${item.id}' has a non-finite rotation.`);
2767
+ const { position, quaternion, scale } = poseOf(item);
2768
+ const length = Math.hypot(...quaternion);
2769
+ if (!(length > 0)) throw Error(`World package: placement '${item.id}' has a zero quaternion.`);
2770
+ return [
2771
+ position[0],
2772
+ position[1],
2773
+ position[2],
2774
+ quaternion[0] / length,
2775
+ quaternion[1] / length,
2776
+ quaternion[2] / length,
2777
+ quaternion[3] / length,
2778
+ uniformScale(item.asset, item.id, scale)
2779
+ ];
2780
+ }
2781
+ function placementCells(state, assets, cellSize, extent) {
2782
+ const cells = /* @__PURE__ */ new Map();
2783
+ const across = Math.max(1, Math.ceil(extent.sizeX / cellSize));
2784
+ const cellOf = (value, min) => Math.min(across - 1, Math.max(0, Math.floor((value - min) / cellSize)));
2785
+ for (const item of state.instances) {
2786
+ if (assets[item.asset] === void 0)
2787
+ throw Error(
2788
+ `World package: placement '${item.id}' names asset '${item.asset}', which options.assets does not.`
2789
+ );
2790
+ const x = cellOf(item.position[0], extent.minX);
2791
+ const z = cellOf(item.position[2], extent.minZ);
2792
+ const key = `${String(x)},${String(z)}`;
2793
+ let cell = cells.get(key);
2794
+ if (cell === void 0) {
2795
+ cell = { items: /* @__PURE__ */ new Map(), x, z };
2796
+ cells.set(key, cell);
2797
+ }
2798
+ const bucket = cell.items.get(item.asset);
2799
+ if (bucket === void 0) cell.items.set(item.asset, [item]);
2800
+ else bucket.push(item);
2801
+ }
2802
+ const buffer = new ArrayBuffer(state.instances.length * RECORD_VALUES * 4);
2803
+ const records = new Float32Array(buffer);
2804
+ const ordered = [];
2805
+ let written = 0;
2806
+ const byId = (left, right) => left[0] < right[0] ? -1 : 1;
2807
+ for (const cell of [...cells.values()].sort((a, b) => a.x - b.x || a.z - b.z)) {
2808
+ const runs = [];
2809
+ for (const [asset, items] of [...cell.items.entries()].sort(byId)) {
2810
+ runs.push({ asset, count: items.length, offset: written });
2811
+ for (const item of items) {
2812
+ records.set(placementRecord(item), written * RECORD_VALUES);
2813
+ written += 1;
2814
+ }
2815
+ }
2816
+ ordered.push({ runs, x: cell.x, z: cell.z });
2817
+ }
2818
+ return { bytes: new Uint8Array(buffer), cells: ordered };
2819
+ }
2820
+ function bakeWorldPackage(state, options) {
2821
+ const { size, resolution } = state;
2822
+ const { assets, cellSize = 64, layers, names } = options;
2823
+ const heightmapName = names?.heightmap ?? "heightmap.u16";
2824
+ const placementsName = names?.placements ?? "placements.bin";
2825
+ const splatName = names?.splat ?? "splat.rgba";
2826
+ const extent = { minX: -size / 2, minZ: -size / 2, sizeX: size, sizeZ: size };
2827
+ let heightMin = Number.POSITIVE_INFINITY;
2828
+ let heightMax = Number.NEGATIVE_INFINITY;
2829
+ for (const height of state.height) {
2830
+ if (!Number.isFinite(height)) throw Error("World package: terrain heights must be finite.");
2831
+ heightMin = Math.min(heightMin, height);
2832
+ heightMax = Math.max(heightMax, height);
2833
+ }
2834
+ if (heightMax - heightMin < 1e-6) heightMax = heightMin + 1;
2835
+ const heightmap = new Uint8Array(resolution * resolution * 2);
2836
+ const heights = new DataView(heightmap.buffer);
2837
+ for (let i = 0; i < state.height.length; i += 1)
2838
+ heights.setUint16(
2839
+ i * 2,
2840
+ Math.round(
2841
+ (state.height[i] - heightMin) / (heightMax - heightMin) * HEIGHTMAP_MAX
2842
+ ),
2843
+ true
2844
+ );
2845
+ const placements = placementCells(state, assets, cellSize, extent);
2846
+ return {
2847
+ files: {
2848
+ [heightmapName]: heightmap,
2849
+ [placementsName]: placements.bytes,
2850
+ [splatName]: splatBytes(state, resolution)
2851
+ },
2852
+ manifest: {
2853
+ assets,
2854
+ cellSize,
2855
+ cells: placements.cells,
2856
+ extent,
2857
+ placements: placementsName,
2858
+ terrain: {
2859
+ columns: resolution,
2860
+ heightMax,
2861
+ heightMin,
2862
+ heightmap: heightmapName,
2863
+ layers: { splat: splatName, ...layers },
2864
+ rows: resolution,
2865
+ spacing: size / (resolution - 1)
2866
+ },
2867
+ version: 1
2868
+ },
2869
+ splat: {
2870
+ masks: Object.fromEntries(SLOTS.map(({ component, id, plane }) => [id, [plane, component]])),
2871
+ planes: PLANES,
2872
+ size: resolution
2873
+ }
2874
+ };
2875
+ }
2876
+ function splatBytes(state, resolution) {
2877
+ const planeBytes = resolution * resolution * 4;
2878
+ if (state.splat.length !== resolution * resolution * MATERIAL_IDS.length)
2879
+ throw Error(
2880
+ `World package: splat has ${String(state.splat.length)} weights, expected ${String(resolution * resolution * MATERIAL_IDS.length)}.`
2881
+ );
2882
+ const bytes = new Uint8Array(planeBytes * PLANES);
2883
+ for (let row = 0; row < resolution; row += 1) {
2884
+ const source = (resolution - 1 - row) * resolution;
2885
+ for (let column = 0; column < resolution; column += 1) {
2886
+ const sample = (source + column) * MATERIAL_IDS.length;
2887
+ for (const slot of SLOTS) {
2888
+ const weight = state.splat[sample + slot.channel];
2889
+ bytes[slot.plane * planeBytes + row * resolution * 4 + column * 4 + COMPONENTS.indexOf(slot.component)] = Math.round(Math.min(1, Math.max(0, weight)) * 255);
2890
+ }
2891
+ }
2892
+ }
2893
+ return bytes;
2894
+ }
2895
+
2896
+ // src/core/water.ts
2897
+ var EMPTY = [];
2898
+ function createSegmentIndex(segments, cellSize) {
2899
+ if (!(cellSize > 0) || !Number.isFinite(cellSize))
2900
+ throw new Error(
2901
+ `TN_TERRAIN_WATER_CELL_INVALID: cellSize must be a positive finite number, received ${String(cellSize)}.`
2902
+ );
2903
+ const cells = /* @__PURE__ */ new Map();
2904
+ for (const segment of segments) {
2905
+ const { a, b, reach } = segment;
2906
+ if (!(reach >= 0) || !Number.isFinite(reach))
2907
+ throw new Error(
2908
+ `TN_TERRAIN_WATER_REACH_INVALID: reach must be a non-negative finite number, received ${String(reach)}.`
2909
+ );
2910
+ const fromX = Math.floor((Math.min(a[0], b[0]) - reach) / cellSize);
2911
+ const toX = Math.floor((Math.max(a[0], b[0]) + reach) / cellSize);
2912
+ const fromZ = Math.floor((Math.min(a[1], b[1]) - reach) / cellSize);
2913
+ const toZ = Math.floor((Math.max(a[1], b[1]) + reach) / cellSize);
2914
+ for (let cx = fromX; cx <= toX; cx++)
2915
+ for (let cz = fromZ; cz <= toZ; cz++) {
2916
+ const key = `${cx}:${cz}`;
2917
+ const list = cells.get(key);
2918
+ if (list) list.push(segment.data);
2919
+ else cells.set(key, [segment.data]);
2920
+ }
2921
+ }
2922
+ return {
2923
+ nearby: (x, z) => cells.get(`${Math.floor(x / cellSize)}:${Math.floor(z / cellSize)}`) ?? EMPTY
2924
+ };
2925
+ }
2926
+
2927
+ export { MATERIAL_IDS, Mask, PARAMS, RESOLUTIONS, Terrain, TerrainEvaluator, applyPlacementOverrides, bakeMesh, bakeTerrain, bakeWorldPackage, createSegmentIndex, decodeHeightPNG, decodeRAW16, encodeGLB, encodeHeightPNG, encodeRAW16, encodeSplatPNGs, encodeZIP, gradientAt, makeExport, sampleHeight, slopeAtIndex, slopeQuantile, splinePoints, validateDocument, validatePlacementOverrides };