@plasius/gpu-world-generator 0.0.4

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 (43) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +73 -0
  3. package/dist/field.wgsl +225 -0
  4. package/dist/fractal-prepass.wgsl +290 -0
  5. package/dist/index.cjs +1942 -0
  6. package/dist/index.cjs.map +1 -0
  7. package/dist/index.d.cts +447 -0
  8. package/dist/index.d.ts +447 -0
  9. package/dist/index.js +1848 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/terrain.wgsl +451 -0
  12. package/docs/adrs/adr-0001-package-scope.md +18 -0
  13. package/docs/adrs/adr-0002-world-tiling-lod-stitching.md +28 -0
  14. package/docs/adrs/adr-0003-terrain-generation-style-mixing.md +21 -0
  15. package/docs/adrs/index.md +5 -0
  16. package/docs/biomes.md +206 -0
  17. package/docs/lod-zoning.md +22 -0
  18. package/docs/plan.md +107 -0
  19. package/docs/procedural-surface.md +73 -0
  20. package/docs/resources.md +55 -0
  21. package/package.json +53 -0
  22. package/src/biomes/temperate.ts +387 -0
  23. package/src/field.wgsl +225 -0
  24. package/src/fields.ts +321 -0
  25. package/src/fractal-prepass.ts +237 -0
  26. package/src/fractal-prepass.wgsl +290 -0
  27. package/src/generator.ts +106 -0
  28. package/src/hex.ts +54 -0
  29. package/src/index.ts +11 -0
  30. package/src/mesh.ts +285 -0
  31. package/src/perf-monitor.ts +133 -0
  32. package/src/shaders/demo-terrain.wgsl +193 -0
  33. package/src/shaders/demo-trees.wgsl +172 -0
  34. package/src/shaders/demo-water-sim.wgsl +361 -0
  35. package/src/shaders/library/common.wgsl +38 -0
  36. package/src/shaders/library/materials.wgsl +175 -0
  37. package/src/terrain.wgsl +451 -0
  38. package/src/tile-cache.ts +274 -0
  39. package/src/tiles.ts +417 -0
  40. package/src/types.ts +220 -0
  41. package/src/wgsl/field.job.wgsl +225 -0
  42. package/src/wgsl/terrain.job.wgsl +451 -0
  43. package/src/wgsl.ts +77 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,1942 @@
1
+ const { pathToFileURL } = require("url"); const __PLASIUS_IMPORT_META_URL__ = pathToFileURL(__filename).href;
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
+ DEFAULT_TILE_SIZE_WORLD: () => DEFAULT_TILE_SIZE_WORLD,
35
+ FIELD_DOWNWARD_MAX: () => FIELD_DOWNWARD_MAX,
36
+ FIELD_UPWARD_MIN: () => FIELD_UPWARD_MIN,
37
+ FRACTAL_ASSET_VERSION: () => FRACTAL_ASSET_VERSION,
38
+ FRACTAL_SAMPLE_STRIDE: () => FRACTAL_SAMPLE_STRIDE,
39
+ MacroBiome: () => MacroBiome,
40
+ MacroBiomeLabel: () => MacroBiomeLabel,
41
+ MicroFeature: () => MicroFeature,
42
+ MicroFeatureLabel: () => MicroFeatureLabel,
43
+ SlopeBand: () => SlopeBand,
44
+ SlopeBandLabel: () => SlopeBandLabel,
45
+ SurfaceCover: () => SurfaceCover,
46
+ SurfaceCoverLabel: () => SurfaceCoverLabel,
47
+ TILE_ASSET_VERSION: () => TILE_ASSET_VERSION,
48
+ TerrainBiome: () => TerrainBiome,
49
+ TerrainBiomeLabel: () => TerrainBiomeLabel,
50
+ TileCache: () => TileCache,
51
+ assetMatches: () => assetMatches,
52
+ axialToWorld: () => axialToWorld,
53
+ bakeTileAsset: () => bakeTileAsset,
54
+ buildHexLevels: () => buildHexLevels,
55
+ classifySlopeBand: () => classifySlopeBand,
56
+ computeNormal: () => computeNormal,
57
+ createFractalPrepassRunner: () => createFractalPrepassRunner,
58
+ createMeshBuilder: () => createMeshBuilder,
59
+ createPerfMonitor: () => createPerfMonitor,
60
+ defaultFieldParams: () => defaultFieldParams,
61
+ defaultFractalMandelSettings: () => defaultFractalMandelSettings,
62
+ encodeTerrainParams: () => encodeTerrainParams,
63
+ fieldWgslUrl: () => fieldWgslUrl,
64
+ fractalPrepassWgslUrl: () => fractalPrepassWgslUrl,
65
+ generateHexGrid: () => generateHexGrid,
66
+ generateTemperateMixedForest: () => generateTemperateMixedForest,
67
+ hexAreaFromSide: () => hexAreaFromSide,
68
+ hexSideFromArea: () => hexSideFromArea,
69
+ loadFieldWgsl: () => loadFieldWgsl,
70
+ loadFractalPrepassWgsl: () => loadFractalPrepassWgsl,
71
+ loadTerrainWgsl: () => loadTerrainWgsl,
72
+ normalize: () => normalize,
73
+ normalizeTileKey: () => normalizeTileKey,
74
+ packHexCells: () => packHexCells,
75
+ parseFractalAsset: () => parseFractalAsset,
76
+ parseTileAssetBinary: () => parseTileAssetBinary,
77
+ parseTileAssetJson: () => parseTileAssetJson,
78
+ resolveTileSizeWorld: () => resolveTileSizeWorld,
79
+ sampleFieldStack: () => sampleFieldStack,
80
+ serializeFractalAsset: () => serializeFractalAsset,
81
+ serializeTileAssetBinary: () => serializeTileAssetBinary,
82
+ serializeTileAssetBinaryFromJson: () => serializeTileAssetBinaryFromJson,
83
+ serializeTileAssetJson: () => serializeTileAssetJson,
84
+ serializeTileAssetJsonFromBinary: () => serializeTileAssetJsonFromBinary,
85
+ shade: () => shade,
86
+ terrainWgslUrl: () => terrainWgslUrl,
87
+ tileAssetFileStem: () => tileAssetFileStem,
88
+ tileBoundsWorld: () => tileBoundsWorld,
89
+ tileKeyFromWorldPosition: () => tileKeyFromWorldPosition,
90
+ tileKeyToString: () => tileKeyToString,
91
+ unpackTerrain: () => unpackTerrain,
92
+ validateTileAssetPayload: () => validateTileAssetPayload
93
+ });
94
+ module.exports = __toCommonJS(index_exports);
95
+
96
+ // src/types.ts
97
+ var TerrainBiome = {
98
+ Plains: 0,
99
+ Tundra: 1,
100
+ Savanna: 2,
101
+ River: 3,
102
+ City: 4,
103
+ Village: 5,
104
+ Ice: 6,
105
+ Snow: 7,
106
+ Mountainous: 8,
107
+ Volcanic: 9,
108
+ Road: 10,
109
+ Town: 11,
110
+ Castle: 12,
111
+ MixedForest: 13
112
+ };
113
+ var TerrainBiomeLabel = {
114
+ [TerrainBiome.Plains]: "Plains",
115
+ [TerrainBiome.Tundra]: "Tundra",
116
+ [TerrainBiome.Savanna]: "Savanna",
117
+ [TerrainBiome.River]: "River",
118
+ [TerrainBiome.City]: "City",
119
+ [TerrainBiome.Village]: "Village",
120
+ [TerrainBiome.Ice]: "Ice",
121
+ [TerrainBiome.Snow]: "Snow",
122
+ [TerrainBiome.Mountainous]: "Mountainous",
123
+ [TerrainBiome.Volcanic]: "Volcanic",
124
+ [TerrainBiome.Road]: "Road",
125
+ [TerrainBiome.Town]: "Town",
126
+ [TerrainBiome.Castle]: "Castle",
127
+ [TerrainBiome.MixedForest]: "Mixed Forest"
128
+ };
129
+ var MacroBiome = {
130
+ Polar: 0,
131
+ ColdTemperate: 1,
132
+ Temperate: 2,
133
+ Arid: 3,
134
+ Tropical: 4,
135
+ Alpine: 5,
136
+ Volcanic: 6,
137
+ Freshwater: 7,
138
+ Coastal: 8,
139
+ Urban: 9,
140
+ Underground: 10
141
+ };
142
+ var MacroBiomeLabel = {
143
+ [MacroBiome.Polar]: "Polar",
144
+ [MacroBiome.ColdTemperate]: "Cold Temperate",
145
+ [MacroBiome.Temperate]: "Temperate",
146
+ [MacroBiome.Arid]: "Arid",
147
+ [MacroBiome.Tropical]: "Tropical",
148
+ [MacroBiome.Alpine]: "Alpine",
149
+ [MacroBiome.Volcanic]: "Volcanic",
150
+ [MacroBiome.Freshwater]: "Freshwater",
151
+ [MacroBiome.Coastal]: "Coastal",
152
+ [MacroBiome.Urban]: "Urban",
153
+ [MacroBiome.Underground]: "Underground"
154
+ };
155
+ var SurfaceCover = {
156
+ Grass: 0,
157
+ Dirt: 1,
158
+ Sand: 2,
159
+ Rock: 3,
160
+ Gravel: 4,
161
+ Snowpack: 5,
162
+ Ice: 6,
163
+ Mud: 7,
164
+ Ash: 8,
165
+ Cobble: 9,
166
+ Road: 10,
167
+ Water: 11,
168
+ Basalt: 12,
169
+ Crystal: 13,
170
+ Sludge: 14
171
+ };
172
+ var SurfaceCoverLabel = {
173
+ [SurfaceCover.Grass]: "Grass",
174
+ [SurfaceCover.Dirt]: "Dirt",
175
+ [SurfaceCover.Sand]: "Sand",
176
+ [SurfaceCover.Rock]: "Rock",
177
+ [SurfaceCover.Gravel]: "Gravel",
178
+ [SurfaceCover.Snowpack]: "Snowpack",
179
+ [SurfaceCover.Ice]: "Ice",
180
+ [SurfaceCover.Mud]: "Mud",
181
+ [SurfaceCover.Ash]: "Ash",
182
+ [SurfaceCover.Cobble]: "Cobble",
183
+ [SurfaceCover.Road]: "Road",
184
+ [SurfaceCover.Water]: "Water",
185
+ [SurfaceCover.Basalt]: "Basalt",
186
+ [SurfaceCover.Crystal]: "Crystal",
187
+ [SurfaceCover.Sludge]: "Sludge"
188
+ };
189
+ var MicroFeature = {
190
+ Tree: 0,
191
+ Bush: 1,
192
+ GrassTuft: 2,
193
+ Reed: 3,
194
+ Rock: 4,
195
+ Boulder: 5,
196
+ WaterRipple: 6,
197
+ IceSpike: 7,
198
+ Hut: 8,
199
+ Wall: 9,
200
+ Bridge: 10,
201
+ Gate: 11,
202
+ Tower: 12,
203
+ Ruin: 13,
204
+ Stalactite: 14,
205
+ Stalagmite: 15,
206
+ CrystalSpire: 16,
207
+ Mushroom: 17,
208
+ TimberSupport: 18,
209
+ Rail: 19,
210
+ Cart: 20,
211
+ Lantern: 21,
212
+ Grate: 22,
213
+ BrickTunnel: 23,
214
+ Flower: 24
215
+ };
216
+ var MicroFeatureLabel = {
217
+ [MicroFeature.Tree]: "Tree",
218
+ [MicroFeature.Bush]: "Bush",
219
+ [MicroFeature.GrassTuft]: "Grass Tuft",
220
+ [MicroFeature.Reed]: "Reed",
221
+ [MicroFeature.Rock]: "Rock",
222
+ [MicroFeature.Boulder]: "Boulder",
223
+ [MicroFeature.WaterRipple]: "Water Ripple",
224
+ [MicroFeature.IceSpike]: "Ice Spike",
225
+ [MicroFeature.Hut]: "Hut",
226
+ [MicroFeature.Wall]: "Wall",
227
+ [MicroFeature.Bridge]: "Bridge",
228
+ [MicroFeature.Gate]: "Gate",
229
+ [MicroFeature.Tower]: "Tower",
230
+ [MicroFeature.Ruin]: "Ruin",
231
+ [MicroFeature.Stalactite]: "Stalactite",
232
+ [MicroFeature.Stalagmite]: "Stalagmite",
233
+ [MicroFeature.CrystalSpire]: "Crystal Spire",
234
+ [MicroFeature.Mushroom]: "Mushroom",
235
+ [MicroFeature.TimberSupport]: "Timber Support",
236
+ [MicroFeature.Rail]: "Rail",
237
+ [MicroFeature.Cart]: "Cart",
238
+ [MicroFeature.Lantern]: "Lantern",
239
+ [MicroFeature.Grate]: "Grate",
240
+ [MicroFeature.BrickTunnel]: "Brick Tunnel",
241
+ [MicroFeature.Flower]: "Flower"
242
+ };
243
+ var SlopeBand = {
244
+ Downward: 0,
245
+ Flat: 1,
246
+ Upward: 2
247
+ };
248
+ var SlopeBandLabel = {
249
+ [SlopeBand.Downward]: "Downward",
250
+ [SlopeBand.Flat]: "Flat",
251
+ [SlopeBand.Upward]: "Upward"
252
+ };
253
+
254
+ // src/hex.ts
255
+ var HEX_AREA_FACTOR = 3 * Math.sqrt(3) / 2;
256
+ function hexAreaFromSide(sideMeters) {
257
+ return HEX_AREA_FACTOR * sideMeters * sideMeters;
258
+ }
259
+ function hexSideFromArea(areaM2) {
260
+ return Math.sqrt(areaM2 / HEX_AREA_FACTOR);
261
+ }
262
+ function axialToWorld(q, r, sizeMeters) {
263
+ const x = sizeMeters * (Math.sqrt(3) * q + Math.sqrt(3) / 2 * r);
264
+ const y = sizeMeters * (1.5 * r);
265
+ return { x, y };
266
+ }
267
+ function generateHexGrid(radius, level = 0) {
268
+ const cells = [];
269
+ for (let q = -radius; q <= radius; q += 1) {
270
+ const r1 = Math.max(-radius, -q - radius);
271
+ const r2 = Math.min(radius, -q + radius);
272
+ for (let r = r1; r <= r2; r += 1) {
273
+ cells.push({ q, r, level, flags: 0 });
274
+ }
275
+ }
276
+ return cells;
277
+ }
278
+ function buildHexLevels(options = {}) {
279
+ const topAreaKm2 = options.topAreaKm2 ?? 1e3;
280
+ const minAreaM2 = options.minAreaM2 ?? 10;
281
+ const levels = Math.max(2, options.levels ?? 6);
282
+ const topAreaM2 = topAreaKm2 * 1e6;
283
+ const ratio = Math.pow(topAreaM2 / minAreaM2, 1 / (levels - 1));
284
+ const specs = [];
285
+ for (let level = 0; level < levels; level += 1) {
286
+ const areaM2 = topAreaM2 / Math.pow(ratio, level);
287
+ const sideMeters = hexSideFromArea(areaM2);
288
+ specs.push({
289
+ level,
290
+ areaM2,
291
+ sideMeters,
292
+ acrossFlatsMeters: sideMeters * Math.sqrt(3)
293
+ });
294
+ }
295
+ return specs;
296
+ }
297
+
298
+ // src/generator.ts
299
+ function packHexCells(cells) {
300
+ const data = new Int32Array(cells.length * 4);
301
+ cells.forEach((cell, i) => {
302
+ const base = i * 4;
303
+ data[base] = cell.q | 0;
304
+ data[base + 1] = cell.r | 0;
305
+ data[base + 2] = cell.level | 0;
306
+ data[base + 3] = cell.flags ?? 0;
307
+ });
308
+ return data;
309
+ }
310
+ function encodeTerrainParams(params) {
311
+ const buffer = new ArrayBuffer(16 * 4);
312
+ const u32 = new Uint32Array(buffer);
313
+ const f32 = new Float32Array(buffer);
314
+ u32[0] = params.seed >>> 0;
315
+ u32[1] = params.cellCount >>> 0;
316
+ f32[2] = params.heatBias ?? 0;
317
+ f32[3] = params.heightScale ?? 1;
318
+ f32[4] = params.macroScale ?? 0.035;
319
+ f32[5] = params.macroWarpStrength ?? 0.18;
320
+ f32[6] = params.styleMixStrength ?? 1;
321
+ f32[7] = params.terraceSteps ?? 6;
322
+ f32[8] = params.terraceStrength ?? 0.35;
323
+ f32[9] = params.craterStrength ?? 0.25;
324
+ f32[10] = params.craterScale ?? 0.18;
325
+ f32[11] = params.heightMin ?? -0.35;
326
+ f32[12] = params.heightMax ?? 1.6;
327
+ f32[13] = params.slopeDownMax ?? 0.2;
328
+ f32[14] = params.slopeUpMin ?? 0.8;
329
+ f32[15] = 0;
330
+ return buffer;
331
+ }
332
+ function isExpandedTerrainLayout(u32) {
333
+ if (u32.length % 8 !== 0) {
334
+ return false;
335
+ }
336
+ const count = u32.length / 8;
337
+ if (count === 0) {
338
+ return false;
339
+ }
340
+ const sampleCount = Math.min(16, count);
341
+ let valid = 0;
342
+ for (let i = 0; i < sampleCount; i += 1) {
343
+ const base = i * 8;
344
+ const surface = u32[base + 4];
345
+ const slopeBand = u32[base + 7];
346
+ if (surface <= SurfaceCover.Sludge && slopeBand <= SlopeBand.Upward) {
347
+ valid += 1;
348
+ }
349
+ }
350
+ return valid >= Math.max(1, Math.floor(sampleCount * 0.8));
351
+ }
352
+ function unpackTerrain(buffer) {
353
+ const f32 = new Float32Array(buffer);
354
+ const u32 = new Uint32Array(buffer);
355
+ const expanded = isExpandedTerrainLayout(u32);
356
+ const stride = expanded ? 8 : 4;
357
+ const count = Math.floor(f32.length / stride);
358
+ const cells = [];
359
+ for (let i = 0; i < count; i += 1) {
360
+ const base = i * stride;
361
+ const cell = {
362
+ height: f32[base],
363
+ heat: f32[base + 1],
364
+ moisture: f32[base + 2],
365
+ biome: u32[base + 3]
366
+ };
367
+ if (expanded) {
368
+ const surface = u32[base + 4];
369
+ const feature = u32[base + 5];
370
+ const foliage = f32[base + 6];
371
+ const slopeBand = u32[base + 7];
372
+ if (surface <= SurfaceCover.Sludge) {
373
+ cell.surface = surface;
374
+ }
375
+ if (feature <= MicroFeature.Flower) {
376
+ cell.feature = feature;
377
+ }
378
+ if (Number.isFinite(foliage)) {
379
+ cell.foliage = foliage;
380
+ }
381
+ if (slopeBand <= SlopeBand.Upward) {
382
+ cell.slopeBand = slopeBand;
383
+ }
384
+ }
385
+ cells.push(cell);
386
+ }
387
+ return cells;
388
+ }
389
+
390
+ // src/wgsl.ts
391
+ var wgslBaseUrl = typeof __PLASIUS_IMPORT_META_URL__ === "string" && __PLASIUS_IMPORT_META_URL__ ? __PLASIUS_IMPORT_META_URL__ : typeof document !== "undefined" && document.baseURI ? document.baseURI : typeof location !== "undefined" ? location.href : "file:///";
392
+ var terrainWgslUrl = new URL("./terrain.wgsl", wgslBaseUrl);
393
+ var fieldWgslUrl = new URL("./field.wgsl", wgslBaseUrl);
394
+ var fractalPrepassWgslUrl = new URL("./fractal-prepass.wgsl", wgslBaseUrl);
395
+ async function loadTerrainWgsl(options = {}) {
396
+ const { url = terrainWgslUrl, fetcher = globalThis.fetch } = options;
397
+ const resolved = url instanceof URL ? url : new URL(url, terrainWgslUrl);
398
+ if (!fetcher || resolved.protocol === "file:") {
399
+ const { readFile } = await import("fs/promises");
400
+ const { fileURLToPath } = await import("url");
401
+ return readFile(fileURLToPath(resolved), "utf8");
402
+ }
403
+ const response = await fetcher(resolved);
404
+ if (!response.ok) {
405
+ const statusText = response.statusText ? ` ${response.statusText}` : "";
406
+ throw new Error(`Failed to load WGSL (${response.status}${statusText})`);
407
+ }
408
+ return response.text();
409
+ }
410
+ async function loadFieldWgsl(options = {}) {
411
+ const { url = fieldWgslUrl, fetcher = globalThis.fetch } = options;
412
+ const resolved = url instanceof URL ? url : new URL(url, fieldWgslUrl);
413
+ if (!fetcher || resolved.protocol === "file:") {
414
+ const { readFile } = await import("fs/promises");
415
+ const { fileURLToPath } = await import("url");
416
+ return readFile(fileURLToPath(resolved), "utf8");
417
+ }
418
+ const response = await fetcher(resolved);
419
+ if (!response.ok) {
420
+ const statusText = response.statusText ? ` ${response.statusText}` : "";
421
+ throw new Error(`Failed to load WGSL (${response.status}${statusText})`);
422
+ }
423
+ return response.text();
424
+ }
425
+ async function loadFractalPrepassWgsl(options = {}) {
426
+ const { url = fractalPrepassWgslUrl, fetcher = globalThis.fetch } = options;
427
+ const resolved = url instanceof URL ? url : new URL(url, fractalPrepassWgslUrl);
428
+ if (!fetcher || resolved.protocol === "file:") {
429
+ const { readFile } = await import("fs/promises");
430
+ const { fileURLToPath } = await import("url");
431
+ return readFile(fileURLToPath(resolved), "utf8");
432
+ }
433
+ const response = await fetcher(resolved);
434
+ if (!response.ok) {
435
+ const statusText = response.statusText ? ` ${response.statusText}` : "";
436
+ throw new Error(`Failed to load WGSL (${response.status}${statusText})`);
437
+ }
438
+ return response.text();
439
+ }
440
+
441
+ // src/fields.ts
442
+ var FIELD_DOWNWARD_MAX = 0.2;
443
+ var FIELD_UPWARD_MIN = 0.8;
444
+ function classifySlopeBand(cumulativeHeight) {
445
+ if (cumulativeHeight < FIELD_DOWNWARD_MAX) {
446
+ return SlopeBand.Downward;
447
+ }
448
+ if (cumulativeHeight >= FIELD_UPWARD_MIN) {
449
+ return SlopeBand.Upward;
450
+ }
451
+ return SlopeBand.Flat;
452
+ }
453
+ function defaultFieldParams(seed = 1337) {
454
+ return {
455
+ seed,
456
+ scale: 0.14,
457
+ warpScale: 0.5,
458
+ warpStrength: 0.75,
459
+ iterations: 64,
460
+ power: 2.2,
461
+ detailScale: 3.2,
462
+ detailIterations: 28,
463
+ detailPower: 2,
464
+ ridgePower: 1.25,
465
+ heatBias: 0,
466
+ moistureBias: 0,
467
+ macroScale: 0.035,
468
+ macroWarpStrength: 0.18,
469
+ styleMixStrength: 1,
470
+ terraceSteps: 6,
471
+ terraceStrength: 0.35,
472
+ craterStrength: 0.25,
473
+ craterScale: 0.18,
474
+ heightMin: -0.35,
475
+ heightMax: 1.6
476
+ };
477
+ }
478
+ function clamp(value, min, max) {
479
+ return Math.min(max, Math.max(min, value));
480
+ }
481
+ function smoothstep(edge0, edge1, x) {
482
+ const t = clamp((x - edge0) / (edge1 - edge0), 0, 1);
483
+ return t * t * (3 - 2 * t);
484
+ }
485
+ function hash01(seed) {
486
+ const s = Math.sin(seed) * 43758.5453123;
487
+ return s - Math.floor(s);
488
+ }
489
+ function smoothMandelbrot(cx, cy, iterations, power) {
490
+ let zx = 0;
491
+ let zy = 0;
492
+ let i = 0;
493
+ for (; i < iterations; i += 1) {
494
+ const r2 = zx * zx + zy * zy;
495
+ if (r2 > 4) {
496
+ break;
497
+ }
498
+ const r3 = Math.sqrt(r2);
499
+ const theta = Math.atan2(zy, zx);
500
+ const rp = Math.pow(r3, power);
501
+ zx = rp * Math.cos(theta * power) + cx;
502
+ zy = rp * Math.sin(theta * power) + cy;
503
+ }
504
+ if (i >= iterations) {
505
+ return 1;
506
+ }
507
+ const r = Math.max(Math.sqrt(zx * zx + zy * zy), 1e-6);
508
+ const nu = Math.log2(Math.log(r));
509
+ const smooth = (i + 1 - nu) / iterations;
510
+ return clamp(smooth, 0, 1);
511
+ }
512
+ function terrace(height, steps) {
513
+ const count = Math.max(1, Math.round(steps));
514
+ const step = 1 / count;
515
+ const h = clamp(height, 0, 1);
516
+ const band = Math.floor(h / step);
517
+ const t = (h - band * step) / step;
518
+ const smoothed = t * t * (3 - 2 * t);
519
+ return (band + smoothed) * step;
520
+ }
521
+ function craterField(x, z, scale, seed) {
522
+ const sx = x * scale;
523
+ const sz = z * scale;
524
+ const cellX = Math.floor(sx);
525
+ const cellZ = Math.floor(sz);
526
+ const fx = sx - cellX;
527
+ const fz = sz - cellZ;
528
+ const baseSeed = cellX * 374761393 + cellZ * 668265263 + seed * 1442695041;
529
+ const h0 = hash01(baseSeed * 0.17);
530
+ const h1 = hash01(baseSeed * 0.31 + 17.13);
531
+ const h2 = hash01(baseSeed * 0.47 + 9.2);
532
+ const cx = h0;
533
+ const cz = h1;
534
+ const radius = 0.22 + 0.25 * h2;
535
+ const dx = fx - cx;
536
+ const dz = fz - cz;
537
+ const dist = Math.hypot(dx, dz);
538
+ return smoothstep(radius, radius * 0.35, dist);
539
+ }
540
+ function sampleFieldStack(x, z, params) {
541
+ const seed = params.seed;
542
+ const offX = hash01(seed * 0.137 + 0.11) * 4 - 2;
543
+ const offZ = hash01(seed * 0.173 + 0.27) * 4 - 2;
544
+ const warpOffX = hash01(seed * 0.91 + 1.1) * 6 - 3;
545
+ const warpOffZ = hash01(seed * 1.07 + 2.2) * 6 - 3;
546
+ const warpA = smoothMandelbrot(
547
+ (x + warpOffX) * params.warpScale,
548
+ (z + warpOffZ) * params.warpScale,
549
+ Math.max(16, Math.floor(params.iterations * 0.6)),
550
+ params.power
551
+ );
552
+ const warpB = smoothMandelbrot(
553
+ (x - warpOffZ) * params.warpScale,
554
+ (z + warpOffX) * params.warpScale,
555
+ Math.max(16, Math.floor(params.iterations * 0.6)),
556
+ params.power
557
+ );
558
+ const warpedX = x + (warpA - 0.5) * params.warpStrength;
559
+ const warpedZ = z + (warpB - 0.5) * params.warpStrength;
560
+ const base = smoothMandelbrot(
561
+ warpedX * params.scale + offX,
562
+ warpedZ * params.scale + offZ,
563
+ params.iterations,
564
+ params.power
565
+ );
566
+ const mid = smoothMandelbrot(
567
+ warpedX * params.scale * 2.15 + offX * 0.6,
568
+ warpedZ * params.scale * 2.15 + offZ * 0.6,
569
+ Math.max(18, Math.floor(params.iterations * 0.7)),
570
+ params.power + 0.2
571
+ );
572
+ const detail = smoothMandelbrot(
573
+ warpedX * params.scale * params.detailScale + offX * 1.4,
574
+ warpedZ * params.scale * params.detailScale + offZ * 1.4,
575
+ params.detailIterations,
576
+ params.detailPower
577
+ );
578
+ const ridge = 1 - Math.abs(2 * mid - 1);
579
+ const baseHeight = Math.pow(base, 0.9) * Math.pow(mid, 1.05) * Math.pow(detail, 1.1);
580
+ const macroIter = Math.max(12, Math.floor(params.iterations * 0.35));
581
+ const macroA = smoothMandelbrot(
582
+ x * params.macroScale + offX * 0.2,
583
+ z * params.macroScale + offZ * 0.2,
584
+ macroIter,
585
+ params.power
586
+ );
587
+ const macroB = smoothMandelbrot(
588
+ (x + offZ) * params.macroScale,
589
+ (z - offX) * params.macroScale,
590
+ macroIter,
591
+ params.power + 0.35
592
+ );
593
+ const macroWarpX = (macroA - 0.5) * params.macroWarpStrength;
594
+ const macroWarpZ = (macroB - 0.5) * params.macroWarpStrength;
595
+ const macroMask = smoothMandelbrot(
596
+ (x + macroWarpX) * params.macroScale,
597
+ (z + macroWarpZ) * params.macroScale,
598
+ macroIter,
599
+ params.power
600
+ );
601
+ const styleMask = clamp((macroMask - 0.5) * params.styleMixStrength + 0.5, 0, 1);
602
+ const terraceHeight = terrace(baseHeight, params.terraceSteps);
603
+ const crater = craterField(x, z, params.craterScale, seed);
604
+ const styleA = clamp(Math.pow(baseHeight, 0.8) + Math.pow(ridge, 1.4) * 0.2, 0, 1);
605
+ const styleB = clamp(
606
+ baseHeight * (1 - params.terraceStrength) + terraceHeight * params.terraceStrength - crater * params.craterStrength + Math.pow(ridge, 1.6) * 0.12,
607
+ 0,
608
+ 1
609
+ );
610
+ const mixed = styleA * (1 - styleMask) + styleB * styleMask;
611
+ const cumulativeHeight = clamp(
612
+ base * 0.38 + mid * 0.33 + detail * 0.21 + styleMask * 0.08,
613
+ 0,
614
+ 1
615
+ );
616
+ const slopeBand = classifySlopeBand(cumulativeHeight);
617
+ const downwardStrength = 1 - smoothstep(0, FIELD_DOWNWARD_MAX, cumulativeHeight);
618
+ const upwardStrength = smoothstep(FIELD_UPWARD_MIN, 1, cumulativeHeight);
619
+ const flatStrength = clamp(1 - Math.max(downwardStrength, upwardStrength), 0, 1);
620
+ const ridgeBoost = Math.pow(ridge, 1.35) * 0.22;
621
+ const centered = (mixed - 0.5) * 2;
622
+ const shaped = Math.sign(centered) * Math.pow(Math.abs(centered), 0.75);
623
+ const macroOffset = (styleMask - 0.5) * 0.25;
624
+ const rawHeight = clamp(
625
+ 0.5 + shaped * 0.8 + macroOffset + ridgeBoost + upwardStrength * 0.22 - downwardStrength * 0.22,
626
+ params.heightMin,
627
+ params.heightMax
628
+ );
629
+ const height01 = clamp(rawHeight, 0, 1);
630
+ const roughness = clamp(
631
+ Math.pow(ridge, params.ridgePower) * 0.7 + detail * 0.3,
632
+ 0,
633
+ 1
634
+ );
635
+ const heat = clamp(0.55 * mid + 0.35 * (1 - height01) + params.heatBias, 0, 1);
636
+ const moisture = clamp(
637
+ 0.55 * detail + 0.35 * (1 - height01) - (heat - 0.5) * 0.1 + params.moistureBias,
638
+ 0,
639
+ 1
640
+ );
641
+ const rockiness = clamp(roughness * 0.6 + height01 * 0.4, 0, 1);
642
+ const water = clamp((0.32 - height01) * 3 + (moisture - 0.5) * 0.2, 0, 1);
643
+ const featureMask = smoothMandelbrot(
644
+ warpedX * params.scale * (params.detailScale + 1.25) - offX * 0.85,
645
+ warpedZ * params.scale * (params.detailScale + 1.25) - offZ * 0.85,
646
+ Math.max(14, Math.floor(params.detailIterations * 0.9)),
647
+ params.detailPower + 0.15
648
+ );
649
+ const obstacleMask = clamp(
650
+ featureMask * 0.58 + roughness * 0.25 + upwardStrength * 0.25 - moisture * 0.16 - water * 0.2,
651
+ 0,
652
+ 1
653
+ );
654
+ const foliageField = smoothMandelbrot(
655
+ warpedX * params.scale * (params.detailScale * 1.85 + 0.35) + offX * 1.9,
656
+ warpedZ * params.scale * (params.detailScale * 1.85 + 0.35) + offZ * 1.9,
657
+ Math.max(16, Math.floor(params.detailIterations * 1.1)),
658
+ Math.max(1.6, params.detailPower - 0.2)
659
+ );
660
+ const foliageMask = clamp(
661
+ foliageField * moisture * (1 - water) * (0.35 + flatStrength * 0.65) * (1 - obstacleMask * 0.82),
662
+ 0,
663
+ 1
664
+ );
665
+ return {
666
+ height: rawHeight,
667
+ cumulativeHeight,
668
+ slopeBand,
669
+ heat,
670
+ moisture,
671
+ roughness,
672
+ rockiness,
673
+ water,
674
+ featureMask,
675
+ obstacleMask,
676
+ foliageMask,
677
+ ridge,
678
+ base,
679
+ detail
680
+ };
681
+ }
682
+
683
+ // src/biomes/temperate.ts
684
+ function hash32(x) {
685
+ let v = x >>> 0;
686
+ v ^= v >>> 17;
687
+ v = Math.imul(v, 3982152891);
688
+ v ^= v >>> 11;
689
+ v = Math.imul(v, 2890668881);
690
+ v ^= v >>> 15;
691
+ v = Math.imul(v, 830770091);
692
+ v ^= v >>> 14;
693
+ return v >>> 0;
694
+ }
695
+ function hash012(x) {
696
+ return (hash32(x) & 16777215) / 16777216;
697
+ }
698
+ function hashCell(cell, seed, salt) {
699
+ const q = cell.q | 0;
700
+ const r = cell.r | 0;
701
+ const level = cell.level | 0;
702
+ const mixed = (Math.imul(q, 1664525) ^ Math.imul(r, 1013904223) ^ Math.imul(level, 747796405) ^ seed ^ salt) >>> 0;
703
+ return hash012(mixed);
704
+ }
705
+ function clamp01(value) {
706
+ return Math.min(1, Math.max(0, value));
707
+ }
708
+ function createBiomeFieldParams(seed) {
709
+ const params = defaultFieldParams(seed);
710
+ params.heatBias = -0.02;
711
+ params.moistureBias = 0.08;
712
+ return params;
713
+ }
714
+ function selectSurface(sample) {
715
+ if (sample.water > 0.58 || sample.cumulativeHeight < 0.14) {
716
+ return SurfaceCover.Water;
717
+ }
718
+ if (sample.heat < 0.18 && sample.water > 0.36) {
719
+ return SurfaceCover.Ice;
720
+ }
721
+ if (sample.heat < 0.3 && (sample.slopeBand === SlopeBand.Upward || sample.cumulativeHeight > 0.52)) {
722
+ return sample.obstacleMask > 0.58 ? SurfaceCover.Rock : SurfaceCover.Gravel;
723
+ }
724
+ if (sample.slopeBand === SlopeBand.Downward && sample.moisture > 0.52) {
725
+ return SurfaceCover.Mud;
726
+ }
727
+ if (sample.obstacleMask > 0.72 || sample.slopeBand === SlopeBand.Upward) {
728
+ return sample.heat > 0.72 ? SurfaceCover.Basalt : SurfaceCover.Rock;
729
+ }
730
+ if (sample.heat > 0.74 && sample.moisture < 0.32) {
731
+ return SurfaceCover.Sand;
732
+ }
733
+ const grassCapable = sample.heat >= 0.32 && sample.heat <= 0.82 && sample.moisture >= 0.34 && sample.moisture <= 0.88 && sample.slopeBand === SlopeBand.Flat;
734
+ if (grassCapable && sample.foliageMask > 0.4) {
735
+ return SurfaceCover.Grass;
736
+ }
737
+ if (sample.moisture > 0.6) {
738
+ return SurfaceCover.Dirt;
739
+ }
740
+ return sample.obstacleMask > 0.5 ? SurfaceCover.Gravel : SurfaceCover.Dirt;
741
+ }
742
+ function supportsGroundFoliage(surface) {
743
+ return surface === SurfaceCover.Grass || surface === SurfaceCover.Dirt || surface === SurfaceCover.Mud;
744
+ }
745
+ function supportsObstacleSurface(surface) {
746
+ return surface === SurfaceCover.Rock || surface === SurfaceCover.Gravel || surface === SurfaceCover.Basalt;
747
+ }
748
+ function axialDistance(a, b) {
749
+ const dq = a.q - b.q;
750
+ const dr = a.r - b.r;
751
+ return (Math.abs(dq) + Math.abs(dr) + Math.abs(dq + dr)) * 0.5;
752
+ }
753
+ function selectFeature(surface, sample, variation, nearestTreeDistance, nearestObstacleDistance, nearbyObstacleRatio, isTreeAnchor) {
754
+ if (isTreeAnchor) {
755
+ return MicroFeature.Tree;
756
+ }
757
+ if (surface === SurfaceCover.Water) {
758
+ return variation > 0.88 && sample.moisture > 0.64 ? MicroFeature.Reed : MicroFeature.WaterRipple;
759
+ }
760
+ if (surface === SurfaceCover.Ice && variation > 0.45) {
761
+ return MicroFeature.IceSpike;
762
+ }
763
+ if (surface === SurfaceCover.Rock || surface === SurfaceCover.Gravel || surface === SurfaceCover.Basalt) {
764
+ return sample.obstacleMask > 0.85 ? MicroFeature.Boulder : MicroFeature.Rock;
765
+ }
766
+ if (surface === SurfaceCover.Mud && variation > 0.78) {
767
+ return MicroFeature.Reed;
768
+ }
769
+ const nearTree = nearestTreeDistance <= 2;
770
+ const underCanopy = nearestTreeDistance <= 1.2;
771
+ if (nearTree && sample.foliageMask > 0.48 && sample.moisture > 0.42 && variation > 0.36) {
772
+ if (surface === SurfaceCover.Mud) {
773
+ return MicroFeature.Bush;
774
+ }
775
+ if (variation > 0.7 || underCanopy) {
776
+ return MicroFeature.Bush;
777
+ }
778
+ return MicroFeature.GrassTuft;
779
+ }
780
+ const openArea = nearestTreeDistance >= 3 && nearestObstacleDistance >= 2.2 && (surface === SurfaceCover.Grass || surface === SurfaceCover.Dirt);
781
+ if (openArea && sample.moisture > 0.32 && sample.moisture < 0.76 && sample.heat > 0.28 && sample.heat < 0.82 && variation > 0.9) {
782
+ return MicroFeature.Flower;
783
+ }
784
+ const obstacleFalloff = Number.isFinite(nearestObstacleDistance) ? clamp01((3.5 - nearestObstacleDistance) / 3.5) : 0;
785
+ const snowLikeGrassDeposit = clamp01(obstacleFalloff * 0.7 + nearbyObstacleRatio * 0.3);
786
+ if (supportsGroundFoliage(surface) && sample.heat >= 0.3 && sample.heat <= 0.84 && sample.moisture >= 0.28 && sample.moisture <= 0.9 && sample.water < 0.24 && variation < 0.96) {
787
+ const depositThreshold = clamp01(
788
+ 0.44 - snowLikeGrassDeposit * 0.24 - sample.foliageMask * 0.08 + Math.max(0, sample.moisture - 0.75) * 0.1
789
+ );
790
+ if (variation > depositThreshold) {
791
+ return MicroFeature.GrassTuft;
792
+ }
793
+ }
794
+ if (sample.slopeBand === SlopeBand.Upward && sample.obstacleMask > 0.6 && variation > 0.94) {
795
+ return MicroFeature.Ruin;
796
+ }
797
+ return void 0;
798
+ }
799
+ function selectBiome(surface, sample) {
800
+ if (surface === SurfaceCover.Water) {
801
+ return sample.heat < 0.25 ? TerrainBiome.Ice : TerrainBiome.River;
802
+ }
803
+ if (surface === SurfaceCover.Ice) {
804
+ return TerrainBiome.Ice;
805
+ }
806
+ if (sample.slopeBand === SlopeBand.Upward && sample.obstacleMask > 0.68) {
807
+ return sample.heat > 0.72 ? TerrainBiome.Volcanic : TerrainBiome.Mountainous;
808
+ }
809
+ if (sample.heat < 0.3) {
810
+ return TerrainBiome.Tundra;
811
+ }
812
+ if (sample.heat > 0.74 && sample.moisture < 0.33) {
813
+ return TerrainBiome.Savanna;
814
+ }
815
+ if (sample.foliageMask > 0.55 && sample.moisture > 0.45) {
816
+ return TerrainBiome.MixedForest;
817
+ }
818
+ return TerrainBiome.Plains;
819
+ }
820
+ function selectMacroBiome(sample) {
821
+ if (sample.water > 0.62) {
822
+ return MacroBiome.Freshwater;
823
+ }
824
+ if (sample.slopeBand === SlopeBand.Upward && sample.obstacleMask > 0.68) {
825
+ return MacroBiome.Alpine;
826
+ }
827
+ if (sample.heat > 0.74 && sample.moisture < 0.33) {
828
+ return MacroBiome.Arid;
829
+ }
830
+ if (sample.heat < 0.3) {
831
+ return MacroBiome.ColdTemperate;
832
+ }
833
+ return MacroBiome.Temperate;
834
+ }
835
+ function generateTemperateMixedForest(options) {
836
+ const terrainSeed = (options.terrainSeed ?? options.seed ?? 1337) >>> 0;
837
+ const featureSeed = (options.featureSeed ?? terrainSeed ^ 2654435769) >>> 0;
838
+ const foliageSeed = (options.foliageSeed ?? terrainSeed ^ 2246822507) >>> 0;
839
+ const levelSpecs = buildHexLevels({
840
+ topAreaKm2: options.topAreaKm2 ?? 1e3,
841
+ minAreaM2: options.minAreaM2 ?? 10,
842
+ levels: options.levels ?? 6
843
+ });
844
+ const levelSpec = levelSpecs[levelSpecs.length - 1];
845
+ const radius = options.radius ?? 6;
846
+ const cells = generateHexGrid(radius, levelSpec.level);
847
+ const terrainParams = createBiomeFieldParams(terrainSeed);
848
+ const featureParams = createBiomeFieldParams(featureSeed);
849
+ const foliageParams = createBiomeFieldParams(foliageSeed);
850
+ const candidates = cells.map((cell) => {
851
+ const world = axialToWorld(cell.q, cell.r, 1);
852
+ const terrainSample = sampleFieldStack(world.x, world.y, terrainParams);
853
+ const featureSample = sampleFieldStack(world.x, world.y, featureParams);
854
+ const foliageSample = sampleFieldStack(world.x, world.y, foliageParams);
855
+ const sample = {
856
+ ...terrainSample,
857
+ featureMask: featureSample.featureMask,
858
+ obstacleMask: featureSample.obstacleMask,
859
+ foliageMask: foliageSample.foliageMask
860
+ };
861
+ const featureNoise = hashCell(cell, featureSeed, 30635);
862
+ const foliageNoise = hashCell(cell, foliageSeed, 20908);
863
+ const variation = clamp01(featureNoise * 0.65 + foliageNoise * 0.35);
864
+ const surface = selectSurface(sample);
865
+ const biome = selectBiome(surface, sample);
866
+ return {
867
+ cell,
868
+ sample,
869
+ variation,
870
+ surface,
871
+ biome,
872
+ macroBiome: selectMacroBiome(sample)
873
+ };
874
+ });
875
+ const treeAnchorIndexes = [];
876
+ for (let i = 0; i < candidates.length; i += 1) {
877
+ const candidate = candidates[i];
878
+ if (!supportsGroundFoliage(candidate.surface)) {
879
+ continue;
880
+ }
881
+ const treeChance = clamp01(
882
+ 8e-3 + candidate.sample.foliageMask * 0.03 + Math.max(0, candidate.sample.moisture - 0.45) * 0.04 - candidate.sample.obstacleMask * 0.02
883
+ );
884
+ const treeRoll = hashCell(candidate.cell, foliageSeed, 41244);
885
+ if (candidate.sample.heat > 0.24 && candidate.sample.heat < 0.82 && candidate.sample.moisture > 0.42 && candidate.sample.water < 0.2 && treeRoll < treeChance) {
886
+ treeAnchorIndexes.push(i);
887
+ }
888
+ }
889
+ const treeAnchorSet = new Set(treeAnchorIndexes);
890
+ const obstacleAnchorIndexes = [];
891
+ for (let i = 0; i < candidates.length; i += 1) {
892
+ const candidate = candidates[i];
893
+ if (supportsObstacleSurface(candidate.surface) || candidate.sample.obstacleMask > 0.72) {
894
+ obstacleAnchorIndexes.push(i);
895
+ }
896
+ }
897
+ const obstacleRadius = 4;
898
+ const terrain = candidates.map((candidate, index) => {
899
+ const isTreeAnchor = treeAnchorSet.has(index);
900
+ let nearestTreeDistance = Number.POSITIVE_INFINITY;
901
+ if (isTreeAnchor) {
902
+ nearestTreeDistance = 0;
903
+ } else {
904
+ for (const treeIndex of treeAnchorIndexes) {
905
+ const distance = axialDistance(candidate.cell, candidates[treeIndex].cell);
906
+ if (distance < nearestTreeDistance) {
907
+ nearestTreeDistance = distance;
908
+ }
909
+ }
910
+ }
911
+ let nearestObstacleDistance = Number.POSITIVE_INFINITY;
912
+ let nearbyObstacleCount = 0;
913
+ for (const obstacleIndex of obstacleAnchorIndexes) {
914
+ const distance = axialDistance(candidate.cell, candidates[obstacleIndex].cell);
915
+ if (distance < nearestObstacleDistance) {
916
+ nearestObstacleDistance = distance;
917
+ }
918
+ if (distance <= obstacleRadius) {
919
+ nearbyObstacleCount += 1;
920
+ }
921
+ }
922
+ const nearbyObstacleRatio = clamp01(nearbyObstacleCount / 12);
923
+ const feature = selectFeature(
924
+ candidate.surface,
925
+ candidate.sample,
926
+ candidate.variation,
927
+ nearestTreeDistance,
928
+ nearestObstacleDistance,
929
+ nearbyObstacleRatio,
930
+ isTreeAnchor
931
+ );
932
+ const cellData = {
933
+ height: clamp01(candidate.sample.height),
934
+ heat: candidate.sample.heat,
935
+ moisture: candidate.sample.moisture,
936
+ biome: candidate.biome,
937
+ macroBiome: candidate.macroBiome,
938
+ surface: candidate.surface,
939
+ feature,
940
+ obstacle: candidate.sample.obstacleMask,
941
+ foliage: candidate.sample.foliageMask,
942
+ slopeBand: candidate.sample.slopeBand
943
+ };
944
+ return cellData;
945
+ });
946
+ return { levelSpec, cells, terrain };
947
+ }
948
+
949
+ // src/perf-monitor.ts
950
+ var defaultOptions = {
951
+ targetFps: 120,
952
+ tolerance: 6,
953
+ sampleSize: 90,
954
+ minSampleFraction: 0.6,
955
+ cooldownMs: 1200,
956
+ qualitySlew: 0.05,
957
+ initialBudget: 0.5,
958
+ auto: true
959
+ };
960
+ function clamp2(value, min, max) {
961
+ return Math.min(max, Math.max(min, value));
962
+ }
963
+ function clamp012(value) {
964
+ return clamp2(value, 0, 1);
965
+ }
966
+ function median(values) {
967
+ if (!values.length) return 0;
968
+ const sorted = values.slice().sort((a, b) => a - b);
969
+ const mid = Math.floor(sorted.length / 2);
970
+ if (sorted.length % 2 === 0) {
971
+ return (sorted[mid - 1] + sorted[mid]) * 0.5;
972
+ }
973
+ return sorted[mid];
974
+ }
975
+ function createPerfMonitor(options = {}) {
976
+ const config = { ...defaultOptions, ...options };
977
+ let budget = clamp012(config.initialBudget);
978
+ let lastAdjust = 0;
979
+ const samples = [];
980
+ const sampleFps = (fps) => {
981
+ if (!Number.isFinite(fps) || fps <= 0) return;
982
+ samples.push(fps);
983
+ if (samples.length > config.sampleSize) {
984
+ samples.shift();
985
+ }
986
+ };
987
+ const sampleFrame = (dtSeconds) => {
988
+ if (!Number.isFinite(dtSeconds) || dtSeconds <= 0) return;
989
+ sampleFps(1 / dtSeconds);
990
+ };
991
+ const update = (nowMs) => {
992
+ if (!config.auto) {
993
+ return { budget, medianFps: null, miss: null, adjusted: false, stable: true };
994
+ }
995
+ if (nowMs - lastAdjust < config.cooldownMs) {
996
+ return { budget, medianFps: null, miss: null, adjusted: false, stable: false };
997
+ }
998
+ if (samples.length < Math.floor(config.sampleSize * config.minSampleFraction)) {
999
+ return { budget, medianFps: null, miss: null, adjusted: false, stable: false };
1000
+ }
1001
+ const med = median(samples);
1002
+ const miss = config.targetFps - med;
1003
+ const tol = config.tolerance;
1004
+ if (Math.abs(miss) <= tol) {
1005
+ lastAdjust = nowMs;
1006
+ return { budget, medianFps: med, miss, adjusted: false, stable: true };
1007
+ }
1008
+ const magnitude = Math.min(1, (Math.abs(miss) - tol) / tol);
1009
+ const direction = miss > 0 ? -1 : 1;
1010
+ const next = clamp012(budget + direction * magnitude * config.qualitySlew);
1011
+ const adjusted = next !== budget;
1012
+ budget = next;
1013
+ lastAdjust = nowMs;
1014
+ return { budget, medianFps: med, miss, adjusted, stable: false };
1015
+ };
1016
+ const resetSamples = () => {
1017
+ samples.length = 0;
1018
+ };
1019
+ const setBudget = (next) => {
1020
+ budget = clamp012(next);
1021
+ };
1022
+ const getBudget = () => budget;
1023
+ const setAuto = (enabled) => {
1024
+ config.auto = enabled;
1025
+ };
1026
+ const getConfig = () => ({ ...config });
1027
+ return {
1028
+ sampleFrame,
1029
+ sampleFps,
1030
+ update,
1031
+ resetSamples,
1032
+ setBudget,
1033
+ getBudget,
1034
+ setAuto,
1035
+ getConfig
1036
+ };
1037
+ }
1038
+
1039
+ // src/fractal-prepass.ts
1040
+ var GPUBufferUsage = globalThis.GPUBufferUsage;
1041
+ var GPUMapMode = globalThis.GPUMapMode;
1042
+ var FRACTAL_ASSET_VERSION = 2;
1043
+ var FRACTAL_SAMPLE_STRIDE = 8;
1044
+ var defaultFractalMandelSettings = {
1045
+ scale: 0.16,
1046
+ strength: 0.85,
1047
+ rockBoost: 0.7
1048
+ };
1049
+ function serializeFractalAsset(asset) {
1050
+ return {
1051
+ version: FRACTAL_ASSET_VERSION,
1052
+ seed: asset.seed,
1053
+ extent: asset.extent,
1054
+ gridSize: asset.gridSize,
1055
+ heightScale: asset.heightScale,
1056
+ sampleStride: FRACTAL_SAMPLE_STRIDE,
1057
+ samples: Array.from(asset.samples)
1058
+ };
1059
+ }
1060
+ function parseFractalAsset(payload) {
1061
+ if (!payload || typeof payload !== "object") return null;
1062
+ const data = payload;
1063
+ if (data.version !== FRACTAL_ASSET_VERSION) return null;
1064
+ if (!Array.isArray(data.samples)) return null;
1065
+ if (data.sampleStride !== FRACTAL_SAMPLE_STRIDE) return null;
1066
+ const gridSize = Number(data.gridSize);
1067
+ const heightScale = Number(data.heightScale);
1068
+ if (!Number.isFinite(gridSize) || gridSize <= 0) return null;
1069
+ if (!Number.isFinite(heightScale)) return null;
1070
+ const expected = (gridSize + 1) * (gridSize + 1) * FRACTAL_SAMPLE_STRIDE;
1071
+ if (data.samples.length !== expected) return null;
1072
+ return {
1073
+ seed: Number(data.seed),
1074
+ extent: Number(data.extent),
1075
+ gridSize,
1076
+ heightScale,
1077
+ samples: new Float32Array(data.samples)
1078
+ };
1079
+ }
1080
+ function assetMatches(asset, config) {
1081
+ if (!asset) return false;
1082
+ if (asset.seed !== config.seed) return false;
1083
+ if (asset.gridSize !== config.gridSize) return false;
1084
+ if (Math.abs(asset.extent - config.extent) > 1e-3) return false;
1085
+ if (!Number.isFinite(asset.heightScale)) return false;
1086
+ const expected = (asset.gridSize + 1) * (asset.gridSize + 1) * FRACTAL_SAMPLE_STRIDE;
1087
+ if (asset.samples.length !== expected) return false;
1088
+ return true;
1089
+ }
1090
+ function createFractalPrepassRunner(options) {
1091
+ if (!GPUBufferUsage || !GPUMapMode) {
1092
+ throw new Error("WebGPU globals not available. Ensure this runs in a WebGPU context.");
1093
+ }
1094
+ const { device, wgsl, gridSize } = options;
1095
+ const gridPoints = gridSize + 1;
1096
+ const sampleCount = gridPoints * gridPoints;
1097
+ const byteSize = sampleCount * FRACTAL_SAMPLE_STRIDE * 4;
1098
+ const uniformBuffer = device.createBuffer({
1099
+ size: 7 * 16,
1100
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
1101
+ });
1102
+ const baseBuffer = device.createBuffer({
1103
+ size: byteSize,
1104
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
1105
+ });
1106
+ const accentBuffer = device.createBuffer({
1107
+ size: byteSize,
1108
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
1109
+ });
1110
+ const readbackBuffer = device.createBuffer({
1111
+ size: byteSize,
1112
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
1113
+ });
1114
+ const module2 = device.createShaderModule({ code: wgsl });
1115
+ const basePipeline = device.createComputePipeline({
1116
+ layout: "auto",
1117
+ compute: { module: module2, entryPoint: "main" }
1118
+ });
1119
+ const accentPipeline = device.createComputePipeline({
1120
+ layout: "auto",
1121
+ compute: { module: module2, entryPoint: "accent_heights" }
1122
+ });
1123
+ const baseBindGroup = device.createBindGroup({
1124
+ layout: basePipeline.getBindGroupLayout(0),
1125
+ entries: [
1126
+ { binding: 0, resource: { buffer: uniformBuffer } },
1127
+ { binding: 1, resource: { buffer: baseBuffer } }
1128
+ ]
1129
+ });
1130
+ const accentBindGroup = device.createBindGroup({
1131
+ layout: accentPipeline.getBindGroupLayout(0),
1132
+ entries: [
1133
+ { binding: 0, resource: { buffer: uniformBuffer } },
1134
+ { binding: 1, resource: { buffer: accentBuffer } },
1135
+ { binding: 2, resource: { buffer: baseBuffer } }
1136
+ ]
1137
+ });
1138
+ const workgroups = Math.ceil(gridPoints / 8);
1139
+ const run = async (runOptions) => {
1140
+ const fieldDefaults = defaultFieldParams(runOptions.seed);
1141
+ const params = {
1142
+ ...fieldDefaults,
1143
+ ...runOptions.fieldParams,
1144
+ seed: runOptions.seed
1145
+ };
1146
+ const mandel = { ...defaultFractalMandelSettings, ...runOptions.mandel };
1147
+ const step = runOptions.extent * 2 / gridSize;
1148
+ const uniformData = new Float32Array(28);
1149
+ uniformData.set([gridPoints, runOptions.extent, step, runOptions.seed], 0);
1150
+ uniformData.set(
1151
+ [params.scale, params.warpScale, params.warpStrength, params.power],
1152
+ 4
1153
+ );
1154
+ uniformData.set(
1155
+ [params.detailScale, params.detailPower, params.ridgePower, params.heatBias],
1156
+ 8
1157
+ );
1158
+ uniformData.set(
1159
+ [params.moistureBias, mandel.scale, mandel.strength, mandel.rockBoost],
1160
+ 12
1161
+ );
1162
+ uniformData.set(
1163
+ [params.iterations, params.detailIterations, params.macroScale, params.macroWarpStrength],
1164
+ 16
1165
+ );
1166
+ uniformData.set(
1167
+ [
1168
+ params.styleMixStrength,
1169
+ params.terraceSteps,
1170
+ params.terraceStrength,
1171
+ params.craterStrength
1172
+ ],
1173
+ 20
1174
+ );
1175
+ uniformData.set([params.craterScale, params.heightMin, params.heightMax, 0], 24);
1176
+ device.queue.writeBuffer(uniformBuffer, 0, uniformData);
1177
+ const encoder = device.createCommandEncoder();
1178
+ const pass = encoder.beginComputePass();
1179
+ pass.setPipeline(basePipeline);
1180
+ pass.setBindGroup(0, baseBindGroup);
1181
+ pass.dispatchWorkgroups(workgroups, workgroups);
1182
+ pass.setPipeline(accentPipeline);
1183
+ pass.setBindGroup(0, accentBindGroup);
1184
+ pass.dispatchWorkgroups(workgroups, workgroups);
1185
+ pass.end();
1186
+ encoder.copyBufferToBuffer(accentBuffer, 0, readbackBuffer, 0, byteSize);
1187
+ device.queue.submit([encoder.finish()]);
1188
+ await readbackBuffer.mapAsync(GPUMapMode.READ);
1189
+ const copy = readbackBuffer.getMappedRange();
1190
+ const data = new Float32Array(copy.slice(0));
1191
+ readbackBuffer.unmap();
1192
+ return {
1193
+ seed: runOptions.seed,
1194
+ extent: runOptions.extent,
1195
+ gridSize,
1196
+ heightScale: runOptions.heightScale,
1197
+ samples: data
1198
+ };
1199
+ };
1200
+ return {
1201
+ gridSize,
1202
+ gridPoints,
1203
+ sampleCount,
1204
+ run
1205
+ };
1206
+ }
1207
+
1208
+ // src/tiles.ts
1209
+ var TILE_ASSET_VERSION = 1;
1210
+ var MAGIC_BYTES = [84, 87, 76, 68];
1211
+ var HEADER_BYTES = 68;
1212
+ var FLAG_HAS_TILE_SIZE = 1 << 0;
1213
+ var FLAG_HAS_FIELDS = 1 << 1;
1214
+ var FLAG_HAS_MATERIALS = 1 << 2;
1215
+ var FLAG_HAS_FEATURES = 1 << 3;
1216
+ function align4(offset) {
1217
+ return offset + 3 & ~3;
1218
+ }
1219
+ function expectedHeightCount(gridSize) {
1220
+ const gridPoints = gridSize + 1;
1221
+ return gridPoints * gridPoints;
1222
+ }
1223
+ function validateTileAssetPayload(payload) {
1224
+ const errors = [];
1225
+ if (!payload || typeof payload !== "object") {
1226
+ return ["payload must be an object"];
1227
+ }
1228
+ const key = payload.key;
1229
+ if (!key || typeof key !== "object") {
1230
+ errors.push("key is required");
1231
+ } else {
1232
+ const keyFields = ["seed", "tx", "tz", "level"];
1233
+ for (const field of keyFields) {
1234
+ const value = key[field];
1235
+ if (!Number.isFinite(value)) {
1236
+ errors.push(`key.${field} must be a number`);
1237
+ }
1238
+ }
1239
+ if (key.tileSizeWorld !== void 0 && !Number.isFinite(key.tileSizeWorld)) {
1240
+ errors.push("key.tileSizeWorld must be a number when provided");
1241
+ }
1242
+ }
1243
+ if (!Number.isFinite(payload.gridSize) || payload.gridSize < 1) {
1244
+ errors.push("gridSize must be a positive number");
1245
+ }
1246
+ if (!Number.isFinite(payload.heightScale)) {
1247
+ errors.push("heightScale must be a number");
1248
+ }
1249
+ if (!Array.isArray(payload.height)) {
1250
+ errors.push("height must be an array");
1251
+ } else {
1252
+ const expected = expectedHeightCount(payload.gridSize);
1253
+ if (payload.height.length !== expected) {
1254
+ errors.push(`height length must be ${expected} for gridSize ${payload.gridSize}`);
1255
+ }
1256
+ }
1257
+ const materialStride = payload.materialStride ?? (payload.materials ? 1 : 0);
1258
+ if (payload.materials) {
1259
+ if (!Number.isFinite(materialStride) || materialStride <= 0) {
1260
+ errors.push("materialStride must be > 0 when materials are provided");
1261
+ } else if (payload.materials.length % materialStride !== 0) {
1262
+ errors.push("materials length must be divisible by materialStride");
1263
+ }
1264
+ }
1265
+ if (payload.fields) {
1266
+ if (!Number.isFinite(payload.fieldStride) || (payload.fieldStride ?? 0) <= 0) {
1267
+ errors.push("fieldStride must be > 0 when fields are provided");
1268
+ } else if (payload.fields.length % payload.fieldStride !== 0) {
1269
+ errors.push("fields length must be divisible by fieldStride");
1270
+ }
1271
+ }
1272
+ if (payload.features) {
1273
+ if (!Number.isFinite(payload.featureStride) || (payload.featureStride ?? 0) <= 0) {
1274
+ errors.push("featureStride must be > 0 when features are provided");
1275
+ } else if (payload.features.length % payload.featureStride !== 0) {
1276
+ errors.push("features length must be divisible by featureStride");
1277
+ }
1278
+ }
1279
+ return errors;
1280
+ }
1281
+ function serializeTileAssetJson(asset) {
1282
+ return {
1283
+ version: TILE_ASSET_VERSION,
1284
+ key: asset.key,
1285
+ gridSize: asset.gridSize,
1286
+ heightScale: asset.heightScale,
1287
+ height: Array.from(asset.height),
1288
+ fields: asset.fields ? Array.from(asset.fields) : void 0,
1289
+ fieldStride: asset.fieldStride,
1290
+ materials: asset.materials ? Array.from(asset.materials) : void 0,
1291
+ materialStride: asset.materialStride ?? (asset.materials ? 1 : void 0),
1292
+ features: asset.features ? Array.from(asset.features) : void 0,
1293
+ featureStride: asset.featureStride
1294
+ };
1295
+ }
1296
+ function parseTileAssetJson(payload) {
1297
+ const errors = validateTileAssetPayload(payload);
1298
+ if (errors.length) {
1299
+ throw new Error(`Invalid tile asset payload: ${errors.join("; ")}`);
1300
+ }
1301
+ const materialStride = payload.materialStride ?? (payload.materials ? 1 : void 0);
1302
+ return {
1303
+ key: payload.key,
1304
+ gridSize: payload.gridSize,
1305
+ heightScale: payload.heightScale,
1306
+ height: Float32Array.from(payload.height),
1307
+ fields: payload.fields ? Float32Array.from(payload.fields) : void 0,
1308
+ fieldStride: payload.fieldStride,
1309
+ materials: payload.materials ? Uint8Array.from(payload.materials) : void 0,
1310
+ materialStride,
1311
+ features: payload.features ? Float32Array.from(payload.features) : void 0,
1312
+ featureStride: payload.featureStride
1313
+ };
1314
+ }
1315
+ async function bakeTileAsset(asset, writer) {
1316
+ const payload = serializeTileAssetJson(asset);
1317
+ const errors = validateTileAssetPayload(payload);
1318
+ if (errors.length) {
1319
+ throw new Error(`Invalid tile asset payload: ${errors.join("; ")}`);
1320
+ }
1321
+ if (writer?.writeJson) {
1322
+ await writer.writeJson(payload, asset);
1323
+ }
1324
+ const binary = serializeTileAssetBinaryFromJson(payload);
1325
+ if (writer?.writeBinary) {
1326
+ await writer.writeBinary(binary, asset, payload);
1327
+ }
1328
+ return { asset, payload, binary };
1329
+ }
1330
+ function serializeTileAssetBinary(asset) {
1331
+ const heightCount = asset.height.length;
1332
+ const expected = expectedHeightCount(asset.gridSize);
1333
+ if (heightCount !== expected) {
1334
+ throw new Error(`height length must be ${expected} for gridSize ${asset.gridSize}`);
1335
+ }
1336
+ const fieldStride = asset.fieldStride ?? 0;
1337
+ if (asset.fields && fieldStride <= 0) {
1338
+ throw new Error("fieldStride must be provided when fields exist");
1339
+ }
1340
+ const materialStride = asset.materialStride ?? (asset.materials ? 1 : 0);
1341
+ if (asset.materials && materialStride <= 0) {
1342
+ throw new Error("materialStride must be provided when materials exist");
1343
+ }
1344
+ const featureStride = asset.featureStride ?? 0;
1345
+ if (asset.features && featureStride <= 0) {
1346
+ throw new Error("featureStride must be provided when features exist");
1347
+ }
1348
+ const fieldCount = asset.fields ? asset.fields.length : 0;
1349
+ const materialCount = asset.materials ? asset.materials.length : 0;
1350
+ const featureCount = asset.features ? asset.features.length : 0;
1351
+ const heightBytes = heightCount * 4;
1352
+ const fieldBytes = fieldCount * 4;
1353
+ const materialBytes = materialCount;
1354
+ const featureBytes = featureCount * 4;
1355
+ let offset = HEADER_BYTES + heightBytes + fieldBytes + materialBytes;
1356
+ offset = align4(offset);
1357
+ const totalBytes = offset + featureBytes;
1358
+ const buffer = new ArrayBuffer(totalBytes);
1359
+ const view = new DataView(buffer);
1360
+ const u8 = new Uint8Array(buffer);
1361
+ u8.set(MAGIC_BYTES, 0);
1362
+ let cursor = 4;
1363
+ view.setUint32(cursor, TILE_ASSET_VERSION, true);
1364
+ cursor += 4;
1365
+ let flags = 0;
1366
+ if (asset.key.tileSizeWorld !== void 0) flags |= FLAG_HAS_TILE_SIZE;
1367
+ if (fieldCount) flags |= FLAG_HAS_FIELDS;
1368
+ if (materialCount) flags |= FLAG_HAS_MATERIALS;
1369
+ if (featureCount) flags |= FLAG_HAS_FEATURES;
1370
+ view.setUint32(cursor, flags, true);
1371
+ cursor += 4;
1372
+ view.setInt32(cursor, asset.key.tx | 0, true);
1373
+ cursor += 4;
1374
+ view.setInt32(cursor, asset.key.tz | 0, true);
1375
+ cursor += 4;
1376
+ view.setUint32(cursor, asset.key.level >>> 0, true);
1377
+ cursor += 4;
1378
+ view.setUint32(cursor, asset.key.seed >>> 0, true);
1379
+ cursor += 4;
1380
+ view.setFloat32(cursor, asset.key.tileSizeWorld ?? 0, true);
1381
+ cursor += 4;
1382
+ view.setUint32(cursor, asset.gridSize >>> 0, true);
1383
+ cursor += 4;
1384
+ view.setFloat32(cursor, asset.heightScale, true);
1385
+ cursor += 4;
1386
+ view.setUint32(cursor, heightCount >>> 0, true);
1387
+ cursor += 4;
1388
+ view.setUint32(cursor, fieldCount >>> 0, true);
1389
+ cursor += 4;
1390
+ view.setUint32(cursor, materialCount >>> 0, true);
1391
+ cursor += 4;
1392
+ view.setUint32(cursor, featureCount >>> 0, true);
1393
+ cursor += 4;
1394
+ view.setUint32(cursor, fieldStride >>> 0, true);
1395
+ cursor += 4;
1396
+ view.setUint32(cursor, materialStride >>> 0, true);
1397
+ cursor += 4;
1398
+ view.setUint32(cursor, featureStride >>> 0, true);
1399
+ cursor += 4;
1400
+ let writeOffset = HEADER_BYTES;
1401
+ new Float32Array(buffer, writeOffset, heightCount).set(asset.height);
1402
+ writeOffset += heightBytes;
1403
+ if (fieldCount) {
1404
+ new Float32Array(buffer, writeOffset, fieldCount).set(asset.fields);
1405
+ writeOffset += fieldBytes;
1406
+ }
1407
+ if (materialCount) {
1408
+ new Uint8Array(buffer, writeOffset, materialCount).set(asset.materials);
1409
+ writeOffset += materialBytes;
1410
+ }
1411
+ writeOffset = align4(writeOffset);
1412
+ if (featureCount) {
1413
+ new Float32Array(buffer, writeOffset, featureCount).set(asset.features);
1414
+ }
1415
+ return buffer;
1416
+ }
1417
+ function parseTileAssetBinary(input) {
1418
+ const buffer = input instanceof ArrayBuffer ? input : input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
1419
+ if (buffer.byteLength < HEADER_BYTES) {
1420
+ throw new Error("Buffer too small for tile asset header");
1421
+ }
1422
+ const u8 = new Uint8Array(buffer);
1423
+ for (let i = 0; i < MAGIC_BYTES.length; i += 1) {
1424
+ if (u8[i] !== MAGIC_BYTES[i]) {
1425
+ throw new Error("Invalid tile asset magic header");
1426
+ }
1427
+ }
1428
+ const view = new DataView(buffer);
1429
+ let cursor = 4;
1430
+ const version = view.getUint32(cursor, true);
1431
+ cursor += 4;
1432
+ if (version !== TILE_ASSET_VERSION) {
1433
+ throw new Error(`Unsupported tile asset version ${version}`);
1434
+ }
1435
+ const flags = view.getUint32(cursor, true);
1436
+ cursor += 4;
1437
+ const tx = view.getInt32(cursor, true);
1438
+ cursor += 4;
1439
+ const tz = view.getInt32(cursor, true);
1440
+ cursor += 4;
1441
+ const level = view.getUint32(cursor, true);
1442
+ cursor += 4;
1443
+ const seed = view.getUint32(cursor, true);
1444
+ cursor += 4;
1445
+ const tileSizeWorld = view.getFloat32(cursor, true);
1446
+ cursor += 4;
1447
+ const gridSize = view.getUint32(cursor, true);
1448
+ cursor += 4;
1449
+ const heightScale = view.getFloat32(cursor, true);
1450
+ cursor += 4;
1451
+ const heightCount = view.getUint32(cursor, true);
1452
+ cursor += 4;
1453
+ const fieldCount = view.getUint32(cursor, true);
1454
+ cursor += 4;
1455
+ const materialCount = view.getUint32(cursor, true);
1456
+ cursor += 4;
1457
+ const featureCount = view.getUint32(cursor, true);
1458
+ cursor += 4;
1459
+ const fieldStride = view.getUint32(cursor, true);
1460
+ cursor += 4;
1461
+ const materialStride = view.getUint32(cursor, true);
1462
+ cursor += 4;
1463
+ const featureStride = view.getUint32(cursor, true);
1464
+ cursor += 4;
1465
+ const heightBytes = heightCount * 4;
1466
+ const fieldBytes = fieldCount * 4;
1467
+ const materialBytes = materialCount;
1468
+ const featureBytes = featureCount * 4;
1469
+ let required = HEADER_BYTES + heightBytes + fieldBytes + materialBytes;
1470
+ required = align4(required) + featureBytes;
1471
+ if (buffer.byteLength < required) {
1472
+ throw new Error("Tile asset buffer is truncated");
1473
+ }
1474
+ let readOffset = HEADER_BYTES;
1475
+ const height = new Float32Array(buffer, readOffset, heightCount);
1476
+ readOffset += heightCount * 4;
1477
+ const fields = fieldCount ? new Float32Array(buffer, readOffset, fieldCount) : void 0;
1478
+ readOffset += fieldCount * 4;
1479
+ const materials = materialCount ? new Uint8Array(buffer, readOffset, materialCount) : void 0;
1480
+ readOffset += materialCount;
1481
+ readOffset = align4(readOffset);
1482
+ const features = featureCount ? new Float32Array(buffer, readOffset, featureCount) : void 0;
1483
+ const key = {
1484
+ seed,
1485
+ tx,
1486
+ tz,
1487
+ level
1488
+ };
1489
+ if (flags & FLAG_HAS_TILE_SIZE) {
1490
+ key.tileSizeWorld = tileSizeWorld;
1491
+ }
1492
+ return {
1493
+ key,
1494
+ gridSize,
1495
+ heightScale,
1496
+ height,
1497
+ fields,
1498
+ fieldStride: fieldCount ? fieldStride : void 0,
1499
+ materials,
1500
+ materialStride: materialCount ? materialStride : void 0,
1501
+ features,
1502
+ featureStride: featureCount ? featureStride : void 0
1503
+ };
1504
+ }
1505
+ function serializeTileAssetBinaryFromJson(payload) {
1506
+ return serializeTileAssetBinary(parseTileAssetJson(payload));
1507
+ }
1508
+ function serializeTileAssetJsonFromBinary(input) {
1509
+ return serializeTileAssetJson(parseTileAssetBinary(input));
1510
+ }
1511
+
1512
+ // src/tile-cache.ts
1513
+ var DEFAULT_TILE_SIZE_WORLD = 64;
1514
+ function normalizeTileKey(key) {
1515
+ const tileSizeWorld = key.tileSizeWorld !== void 0 && Number.isFinite(key.tileSizeWorld) ? Number(key.tileSizeWorld) : void 0;
1516
+ return {
1517
+ seed: key.seed >>> 0,
1518
+ tx: Math.trunc(key.tx),
1519
+ tz: Math.trunc(key.tz),
1520
+ level: Math.max(0, Math.trunc(key.level)),
1521
+ tileSizeWorld
1522
+ };
1523
+ }
1524
+ function tileKeyToString(key) {
1525
+ const normalized = normalizeTileKey(key);
1526
+ const tileSize = normalized.tileSizeWorld === void 0 ? "default" : normalized.tileSizeWorld.toFixed(3);
1527
+ return [
1528
+ normalized.seed,
1529
+ normalized.level,
1530
+ normalized.tx,
1531
+ normalized.tz,
1532
+ tileSize
1533
+ ].join(":");
1534
+ }
1535
+ function resolveTileSizeWorld(key, defaultTileSizeWorld = DEFAULT_TILE_SIZE_WORLD) {
1536
+ return key.tileSizeWorld ?? defaultTileSizeWorld;
1537
+ }
1538
+ function tileBoundsWorld(key, defaultTileSizeWorld = DEFAULT_TILE_SIZE_WORLD) {
1539
+ const size = resolveTileSizeWorld(key, defaultTileSizeWorld);
1540
+ const minX = key.tx * size;
1541
+ const minZ = key.tz * size;
1542
+ return {
1543
+ minX,
1544
+ minZ,
1545
+ maxX: minX + size,
1546
+ maxZ: minZ + size,
1547
+ centerX: minX + size * 0.5,
1548
+ centerZ: minZ + size * 0.5,
1549
+ size
1550
+ };
1551
+ }
1552
+ function tileKeyFromWorldPosition(options) {
1553
+ const size = options.tileSizeWorld ?? options.defaultTileSizeWorld ?? DEFAULT_TILE_SIZE_WORLD;
1554
+ const tx = Math.floor(options.x / size);
1555
+ const tz = Math.floor(options.z / size);
1556
+ return {
1557
+ seed: options.seed,
1558
+ tx,
1559
+ tz,
1560
+ level: options.level ?? 0,
1561
+ tileSizeWorld: options.tileSizeWorld
1562
+ };
1563
+ }
1564
+ function tileAssetFileStem(key) {
1565
+ const normalized = normalizeTileKey(key);
1566
+ const suffix = normalized.tileSizeWorld === void 0 ? "" : `-ts${normalized.tileSizeWorld.toFixed(3)}`;
1567
+ return `tile-${normalized.seed}-${normalized.level}-${normalized.tx}-${normalized.tz}${suffix}`;
1568
+ }
1569
+ function estimateAssetBytes(asset, payload, binary) {
1570
+ let bytes = asset.height.length * 4;
1571
+ if (asset.fields) bytes += asset.fields.length * 4;
1572
+ if (asset.materials) bytes += asset.materials.length;
1573
+ if (asset.features) bytes += asset.features.length * 4;
1574
+ if (binary) bytes += binary.byteLength;
1575
+ if (payload) {
1576
+ bytes += payload.height.length * 4;
1577
+ }
1578
+ return bytes;
1579
+ }
1580
+ var TileCache = class {
1581
+ entries = /* @__PURE__ */ new Map();
1582
+ inflight = /* @__PURE__ */ new Map();
1583
+ totalBytes = 0;
1584
+ options;
1585
+ constructor(options = {}) {
1586
+ this.options = {
1587
+ maxEntries: options.maxEntries ?? 128,
1588
+ maxBytes: options.maxBytes ?? 128 * 1024 * 1024,
1589
+ keepBinary: options.keepBinary ?? true,
1590
+ keepJson: options.keepJson ?? true,
1591
+ writer: options.writer,
1592
+ now: options.now ?? (() => Date.now()),
1593
+ onEvict: options.onEvict ?? (() => {
1594
+ })
1595
+ };
1596
+ }
1597
+ getStats() {
1598
+ return {
1599
+ entries: this.entries.size,
1600
+ bytes: this.totalBytes,
1601
+ inflight: this.inflight.size
1602
+ };
1603
+ }
1604
+ has(key) {
1605
+ return this.entries.has(tileKeyToString(key));
1606
+ }
1607
+ get(key) {
1608
+ const id = tileKeyToString(key);
1609
+ const entry = this.entries.get(id);
1610
+ if (entry) {
1611
+ entry.lastAccess = this.options.now();
1612
+ }
1613
+ return entry;
1614
+ }
1615
+ async getOrCreate(key, generator) {
1616
+ const normalized = normalizeTileKey(key);
1617
+ const id = tileKeyToString(normalized);
1618
+ const cached = this.entries.get(id);
1619
+ if (cached && cached.status === "ready") {
1620
+ cached.lastAccess = this.options.now();
1621
+ return cached;
1622
+ }
1623
+ const inflight = this.inflight.get(id);
1624
+ if (inflight) {
1625
+ return inflight;
1626
+ }
1627
+ const entry = {
1628
+ key: normalized,
1629
+ status: "pending",
1630
+ bytes: 0,
1631
+ lastAccess: this.options.now()
1632
+ };
1633
+ this.entries.set(id, entry);
1634
+ const promise = (async () => {
1635
+ try {
1636
+ const asset = await generator(normalized);
1637
+ const baked = await bakeTileAsset(asset, this.options.writer);
1638
+ const payload = this.options.keepJson ? baked.payload : void 0;
1639
+ const binary = this.options.keepBinary ? baked.binary : void 0;
1640
+ const bytes = estimateAssetBytes(asset, payload, binary);
1641
+ const ready = {
1642
+ key: normalized,
1643
+ status: "ready",
1644
+ asset,
1645
+ payload,
1646
+ binary,
1647
+ bytes,
1648
+ lastAccess: this.options.now()
1649
+ };
1650
+ this.replaceEntry(id, ready);
1651
+ this.inflight.delete(id);
1652
+ this.evictIfNeeded();
1653
+ return ready;
1654
+ } catch (error) {
1655
+ const failed = {
1656
+ key: normalized,
1657
+ status: "error",
1658
+ bytes: 0,
1659
+ lastAccess: this.options.now(),
1660
+ error
1661
+ };
1662
+ this.replaceEntry(id, failed);
1663
+ this.inflight.delete(id);
1664
+ return failed;
1665
+ }
1666
+ })();
1667
+ this.inflight.set(id, promise);
1668
+ return promise;
1669
+ }
1670
+ delete(key) {
1671
+ const id = tileKeyToString(key);
1672
+ const entry = this.entries.get(id);
1673
+ if (!entry) return false;
1674
+ this.entries.delete(id);
1675
+ this.totalBytes -= entry.bytes;
1676
+ this.options.onEvict(entry);
1677
+ return true;
1678
+ }
1679
+ replaceEntry(id, entry) {
1680
+ const existing = this.entries.get(id);
1681
+ if (existing) {
1682
+ this.totalBytes -= existing.bytes;
1683
+ }
1684
+ this.entries.set(id, entry);
1685
+ this.totalBytes += entry.bytes;
1686
+ }
1687
+ evictIfNeeded() {
1688
+ const { maxEntries, maxBytes } = this.options;
1689
+ while (this.entries.size > maxEntries || this.totalBytes > maxBytes) {
1690
+ let oldestId = null;
1691
+ let oldestTime = Infinity;
1692
+ for (const [id, entry2] of this.entries) {
1693
+ if (entry2.status !== "ready") continue;
1694
+ if (entry2.lastAccess < oldestTime) {
1695
+ oldestTime = entry2.lastAccess;
1696
+ oldestId = id;
1697
+ }
1698
+ }
1699
+ if (!oldestId) {
1700
+ break;
1701
+ }
1702
+ const entry = this.entries.get(oldestId);
1703
+ if (entry) {
1704
+ this.entries.delete(oldestId);
1705
+ this.totalBytes -= entry.bytes;
1706
+ this.options.onEvict(entry);
1707
+ } else {
1708
+ break;
1709
+ }
1710
+ }
1711
+ }
1712
+ };
1713
+
1714
+ // src/mesh.ts
1715
+ function clamp3(value, min, max) {
1716
+ return Math.min(max, Math.max(min, value));
1717
+ }
1718
+ function normalize(vec) {
1719
+ const len = Math.hypot(vec[0], vec[1], vec[2]);
1720
+ if (len === 0) return [0, 1, 0];
1721
+ return [vec[0] / len, vec[1] / len, vec[2] / len];
1722
+ }
1723
+ function computeNormal(a, b, c) {
1724
+ const ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
1725
+ const ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
1726
+ return normalize([
1727
+ ab[1] * ac[2] - ab[2] * ac[1],
1728
+ ab[2] * ac[0] - ab[0] * ac[2],
1729
+ ab[0] * ac[1] - ab[1] * ac[0]
1730
+ ]);
1731
+ }
1732
+ function shade(color, factor) {
1733
+ return [color[0] * factor, color[1] * factor, color[2] * factor];
1734
+ }
1735
+ function createMeshBuilder(sizeOrOptions = 1) {
1736
+ const options = typeof sizeOrOptions === "number" ? { size: sizeOrOptions } : sizeOrOptions;
1737
+ const size = options.size ?? 1;
1738
+ const includeGeomorph = options.includeGeomorph ?? false;
1739
+ const defaultMaterial = options.defaultMaterial ?? 0;
1740
+ const foliageMaterial = options.foliageMaterial ?? defaultMaterial;
1741
+ const vertices = [];
1742
+ const boxMin = [];
1743
+ const boxMax = [];
1744
+ let treeMeshCount = 0;
1745
+ const vertexStride = includeGeomorph ? 13 : 11;
1746
+ const pushVertex = (pos, normal, color, sway = 0, material = defaultMaterial, geomorph) => {
1747
+ vertices.push(
1748
+ pos[0],
1749
+ pos[1],
1750
+ pos[2],
1751
+ normal[0],
1752
+ normal[1],
1753
+ normal[2],
1754
+ color[0],
1755
+ color[1],
1756
+ color[2],
1757
+ sway,
1758
+ material
1759
+ );
1760
+ if (includeGeomorph) {
1761
+ const targetY = geomorph?.targetY ?? pos[1];
1762
+ const weight = geomorph?.weight ?? 0;
1763
+ vertices.push(targetY, weight);
1764
+ }
1765
+ };
1766
+ const addTriangle = (a, b, c, normal, color, swayA = 0, swayB = swayA, swayC = swayA, material = defaultMaterial, morphA, morphB, morphC) => {
1767
+ pushVertex(a, normal, color, swayA, material, morphA);
1768
+ pushVertex(b, normal, color, swayB, material, morphB);
1769
+ pushVertex(c, normal, color, swayC, material, morphC);
1770
+ };
1771
+ const addQuad = (a, b, c, d, normal, color, swayA = 0, swayB = swayA, swayC = swayA, swayD = swayA, material = defaultMaterial, morphA, morphB, morphC, morphD) => {
1772
+ addTriangle(a, b, c, normal, color, swayA, swayB, swayC, material, morphA, morphB, morphC);
1773
+ addTriangle(c, d, a, normal, color, swayC, swayD, swayA, material, morphC, morphD, morphA);
1774
+ };
1775
+ const addPrism = (center, radius, bottom, height, color, swayBase = 0, swayScale = 0, material = defaultMaterial) => {
1776
+ const top = [];
1777
+ const base = [];
1778
+ const baseY = center[1] + bottom;
1779
+ const topY = baseY + height;
1780
+ const safeHeight = Math.max(height, 1e-3);
1781
+ const swayFor = (y) => swayBase + swayScale * clamp3((y - baseY) / safeHeight, 0, 1);
1782
+ for (let i = 0; i < 6; i += 1) {
1783
+ const angle = Math.PI / 180 * (60 * i - 30);
1784
+ const x = center[0] + radius * Math.cos(angle);
1785
+ const z = center[2] + radius * Math.sin(angle);
1786
+ top.push([x, topY, z]);
1787
+ base.push([x, baseY, z]);
1788
+ }
1789
+ const topCenter = [center[0], topY, center[2]];
1790
+ const topSway = swayFor(topY);
1791
+ for (let i = 0; i < 6; i += 1) {
1792
+ const a = topCenter;
1793
+ const b = top[i];
1794
+ const c = top[(i + 1) % 6];
1795
+ const normal = computeNormal(a, b, c);
1796
+ addTriangle(a, b, c, normal, color, topSway, topSway, topSway, material);
1797
+ }
1798
+ for (let i = 0; i < 6; i += 1) {
1799
+ const top0 = top[i];
1800
+ const top1 = top[(i + 1) % 6];
1801
+ const bottom0 = base[i];
1802
+ const bottom1 = base[(i + 1) % 6];
1803
+ const normal = computeNormal(top0, bottom0, bottom1);
1804
+ const swayTop0 = swayFor(top0[1]);
1805
+ const swayTop1 = swayFor(top1[1]);
1806
+ const swayBottom0 = swayFor(bottom0[1]);
1807
+ const swayBottom1 = swayFor(bottom1[1]);
1808
+ addQuad(
1809
+ top0,
1810
+ bottom0,
1811
+ bottom1,
1812
+ top1,
1813
+ normal,
1814
+ shade(color, 0.85),
1815
+ swayTop0,
1816
+ swayBottom0,
1817
+ swayBottom1,
1818
+ swayTop1,
1819
+ material
1820
+ );
1821
+ }
1822
+ };
1823
+ const addTreeMesh = (center, baseHeight, seedValue, material = defaultMaterial) => {
1824
+ const trunkRadius = size * (0.12 + seedValue * 0.05);
1825
+ const trunkHeight = 0.5 + seedValue * 0.6;
1826
+ const canopyRadius = size * (0.36 + seedValue * 0.18);
1827
+ const canopyHeight = 0.6 + seedValue * 0.4;
1828
+ const trunkColor = [0.28, 0.18, 0.1];
1829
+ const leafColor = [0.18, 0.45, 0.2];
1830
+ addPrism(center, trunkRadius, baseHeight, trunkHeight, trunkColor, 0.02, 0.28, material);
1831
+ addPrism(
1832
+ [center[0], baseHeight + trunkHeight * 0.7, center[2]],
1833
+ canopyRadius,
1834
+ 0,
1835
+ canopyHeight * 0.55,
1836
+ leafColor,
1837
+ 0.12,
1838
+ 0.9,
1839
+ foliageMaterial
1840
+ );
1841
+ addPrism(
1842
+ [center[0], baseHeight + trunkHeight + canopyHeight * 0.1, center[2]],
1843
+ canopyRadius * 0.7,
1844
+ 0,
1845
+ canopyHeight * 0.35,
1846
+ shade(leafColor, 0.95),
1847
+ 0.2,
1848
+ 1.1,
1849
+ foliageMaterial
1850
+ );
1851
+ treeMeshCount += 1;
1852
+ };
1853
+ const addBounds = (points, minY = 0, maxYOverride) => {
1854
+ const xs = points.map((p) => p[0]);
1855
+ const zs = points.map((p) => p[2]);
1856
+ const ys = points.map((p) => p[1]);
1857
+ const minX = Math.min(...xs);
1858
+ const maxX = Math.max(...xs);
1859
+ const minZ = Math.min(...zs);
1860
+ const maxZ = Math.max(...zs);
1861
+ const maxY = typeof maxYOverride === "number" ? maxYOverride : Math.max(...ys);
1862
+ boxMin.push(minX, minY, minZ, 0);
1863
+ boxMax.push(maxX, maxY, maxZ, 0);
1864
+ };
1865
+ return {
1866
+ vertices,
1867
+ boxMin,
1868
+ boxMax,
1869
+ vertexStride,
1870
+ includeGeomorph,
1871
+ addTriangle,
1872
+ addQuad,
1873
+ addTreeMesh,
1874
+ addBounds,
1875
+ get treeMeshCount() {
1876
+ return treeMeshCount;
1877
+ }
1878
+ };
1879
+ }
1880
+ // Annotate the CommonJS export names for ESM import in node:
1881
+ 0 && (module.exports = {
1882
+ DEFAULT_TILE_SIZE_WORLD,
1883
+ FIELD_DOWNWARD_MAX,
1884
+ FIELD_UPWARD_MIN,
1885
+ FRACTAL_ASSET_VERSION,
1886
+ FRACTAL_SAMPLE_STRIDE,
1887
+ MacroBiome,
1888
+ MacroBiomeLabel,
1889
+ MicroFeature,
1890
+ MicroFeatureLabel,
1891
+ SlopeBand,
1892
+ SlopeBandLabel,
1893
+ SurfaceCover,
1894
+ SurfaceCoverLabel,
1895
+ TILE_ASSET_VERSION,
1896
+ TerrainBiome,
1897
+ TerrainBiomeLabel,
1898
+ TileCache,
1899
+ assetMatches,
1900
+ axialToWorld,
1901
+ bakeTileAsset,
1902
+ buildHexLevels,
1903
+ classifySlopeBand,
1904
+ computeNormal,
1905
+ createFractalPrepassRunner,
1906
+ createMeshBuilder,
1907
+ createPerfMonitor,
1908
+ defaultFieldParams,
1909
+ defaultFractalMandelSettings,
1910
+ encodeTerrainParams,
1911
+ fieldWgslUrl,
1912
+ fractalPrepassWgslUrl,
1913
+ generateHexGrid,
1914
+ generateTemperateMixedForest,
1915
+ hexAreaFromSide,
1916
+ hexSideFromArea,
1917
+ loadFieldWgsl,
1918
+ loadFractalPrepassWgsl,
1919
+ loadTerrainWgsl,
1920
+ normalize,
1921
+ normalizeTileKey,
1922
+ packHexCells,
1923
+ parseFractalAsset,
1924
+ parseTileAssetBinary,
1925
+ parseTileAssetJson,
1926
+ resolveTileSizeWorld,
1927
+ sampleFieldStack,
1928
+ serializeFractalAsset,
1929
+ serializeTileAssetBinary,
1930
+ serializeTileAssetBinaryFromJson,
1931
+ serializeTileAssetJson,
1932
+ serializeTileAssetJsonFromBinary,
1933
+ shade,
1934
+ terrainWgslUrl,
1935
+ tileAssetFileStem,
1936
+ tileBoundsWorld,
1937
+ tileKeyFromWorldPosition,
1938
+ tileKeyToString,
1939
+ unpackTerrain,
1940
+ validateTileAssetPayload
1941
+ });
1942
+ //# sourceMappingURL=index.cjs.map