@threenative/terrain 0.0.0-stage → 0.1.1

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/AGENT_GUIDE.md ADDED
@@ -0,0 +1,194 @@
1
+ # Strata: instructions for a coding agent
2
+
3
+ ## Read this first
4
+
5
+ Use the typed public `@threenative/terrain` imports to author terrain. The headless implementation is recovered from the owner-supplied Strata source. An operation records intent; `evaluate()` produces disposable data. Never modify an evaluated height array expecting to edit the recipe.
6
+
7
+ World units are metres. Y is up. X and Z range from `-size/2` to `+size/2`. Brush centres and masks use `[x,z]`; spline points use `[x,y,z]`. Rotation and slope thresholds use degrees. **Placement output yaw uses radians.** Heights are absolute world elevations, not normalized texture values.
8
+
9
+ Prefer stable semantic IDs: `base`, `north-peak`, `camp-pad`, `access-road`, `river-main`, `forest`. Repeating an operation with an existing ID replaces it **in place**. It does not append another layer. Use `label` for a human-readable name in fluent calls; the saved layer uses `name`. In `biome()` the `name` parameter is the biome key, not the display name.
10
+
11
+ ## Workflow
12
+
13
+ 1. Establish world size, seed, and a low preview resolution. Add base noise and large landforms.
14
+ 2. Add erosion before fine roads/building pads. Roads and pads take their elevation from the ground they cross unless you pin one.
15
+ 3. Add surface/biome rules and population. Put rules after the geometry changes whose slopes they should see. Road painting can override earlier surface rules; a later reset-material pass can also erase a road's paint.
16
+ 4. Inspect the result and diagnostics. Modify known IDs, keeping the source stack small.
17
+ 5. Export the recipe and derived runtime data at an explicit resolution. Verify the high-resolution result; resolution-dependent simulation is not guaranteed to match the preview exactly.
18
+
19
+ Use `transaction()` for a synchronous semantic edit that should undo as a unit. Validation failure rolls back the entire batch. `applyPatch()` accepts JSON-only atomic commands, useful for a tool-calling agent:
20
+
21
+ ```js
22
+ terrain.applyPatch([
23
+ { op: 'update', id: 'north-peak', patch: { params: { amplitude: 120 } } },
24
+ { op: 'update', id: 'river-main', patch: { params: { width: 22 } } },
25
+ { op: 'toggle', id: 'erosion', enabled: true },
26
+ ]);
27
+ ```
28
+
29
+ Allowed patch commands: `upsert` with `layer`, `update` with `id` and `patch`, `remove` with `id`, `move` with `id` and zero-based `index`, `toggle` with `id` and optional `enabled`. Do not invent methods such as `terrain.generate()`, `terrain.bake()` or `terrain.saveGLB()`. Use `evaluate()`, `bakeTerrain()` and `makeExport()`.
30
+
31
+ ## API patterns
32
+
33
+ ```js
34
+ import { Terrain, Mask, bakeMesh, bakeTerrain, makeExport } from '@threenative/terrain';
35
+ const t = new Terrain({ size: 512, resolution: 129, seed: 73 });
36
+
37
+ t.noise({ id: 'base', base: 15, amplitude: 20, scale: 140, octaves: 5, warp: 30 });
38
+ t.sculpt({ id: 'hill', at: [0,0], radius: 45, strength: 12, falloff: .7 });
39
+ t.sculpt({ id: 'ditch', points: [[-40,10],[0,30],[40,35]], radius: 10, strength: -5 });
40
+ t.smooth({ id: 'soften', at: [0,0], radius: 65, strength: .6, iterations: 4 });
41
+ t.flatten({ id: 'pad', at: [-70,50], radius: 28, height: 22, strength: 1 });
42
+ t.ramp({ id: 'ramp', from: [-70,22,50], to: [-20,38,50], width: 15, shoulder: 10 });
43
+ t.stamp({ id: 'cliff-mass', shape: 'mesa', at: [90,-70], radius: [70,40], amplitude: 50, rotation: 30 });
44
+ t.erode({ id: 'thermal', method: 'thermal', iterations: 15, talus: 32, rate: .22 });
45
+ t.erode({ id: 'hydraulic', method: 'hydraulic', droplets: 5000, maxSteps: 40 });
46
+ t.terrace({ id: 'strata', step: 8, softness: .22, strength: .7 });
47
+ ```
48
+
49
+ A `flatten` with no `height` levels the pad to the ground already under it and lets the blend reach as far as its deepest cut or fill, so it is a bench rather than a mesa.
50
+
51
+ Stroke tools accept `at` or a polyline `points`. Brush radius is a **radius**, not diameter. Strength is signed metres for sculpt; `0..1` for smoothing, flattening and paint. Stroke coverage uses maximum falloff coverage, not repeated accumulation at every mouse event. Holding a brush stationary does not keep adding height. Points are interpolated at `2 * radius * spacing`; jitter is seeded. Procedural stamps use `amplitude` in metres. `opacity` is available on every layer.
52
+
53
+ Masks are serializable expressions, never callbacks:
54
+
55
+ ```js
56
+ const forest = Mask.and(
57
+ Mask.height(8, 85, 10), // altitude band, feathered boundaries
58
+ Mask.slope(0, 34, 6), // slope degrees
59
+ Mask.not(Mask.material('road')),
60
+ Mask.not(Mask.circle([-70, 50], 38, .3)),
61
+ );
62
+
63
+ t.biome({ id: 'forest-zone', label: 'Conifer biome', name: 'forest', mask: forest });
64
+ t.scatter({ id: 'trees', asset: 'pine', count: 900, minDistance: 5,
65
+ scale: [.75,1.25], maxSlope: 35, mask: Mask.biome('forest') });
66
+ t.clear({ id: 'clear-camp', target: 'scatter', mask: Mask.circle([-70,50],35) });
67
+ ```
68
+
69
+ Additional masks: `Mask.rectangle(at, size, rotation, falloff)`, `Mask.noise(scale, threshold, seed, fade)`, `Mask.or(...)`, `Mask.all()`, `Mask.none()`. Slope/height masks are evaluated against the terrain at that operation's stage. Population is deferred: scatter rules sample the **final** heights/materials/biomes and re-evaluate their mask there, so later height edits do not leave trees floating. Clear-scatter masks affect preceding rules only. Minimum distance is guaranteed **within each rule**, not across different scatter rules. The bounded rejection sampler may place fewer than requested; inspect diagnostics rather than assuming an exact count.
70
+
71
+ Materials are exactly `grass`, `dirt`, `rock`, `snow`, `sand`, `mud`, `road`, `moss`. Splat arrays are sample-major: `splat[sampleIndex * 8 + channel]`. Weights sum to one. `paint()` blends and renormalizes; `materials()` applies ordered rules. A later rule can override an earlier one.
72
+
73
+ ```js
74
+ t.materials({ id: 'surfaces', base: 'grass', rules: [
75
+ { material: 'sand', mask: Mask.height(-100, 5, 3) },
76
+ { material: 'rock', mask: Mask.slope(42, 90, 8) },
77
+ { material: 'snow', mask: Mask.height(95, 1000, 15) },
78
+ ] });
79
+ t.paint({ id: 'camp-dirt', at: [-70,50], radius: 27, material: 'dirt', strength: .9 });
80
+ ```
81
+
82
+ ## Roads, rivers and water
83
+
84
+ A road, river or ramp **grades itself to the ground it crosses** when a control point leaves its elevation out (`null`, or `NaN` in code) or when it sets `followTerrain: true`. The elevation is sampled along the centreline, low-passed along arc length, then held inside `maxGrade` (default 0.12) and `maxCut`/`maxFill` (default 2.5 m / 1.5 m); the shoulder widens into a batter with the cut or fill, so a corridor never stands on a retaining step. Leaving the ground is the hard limit: where the ground itself is steeper than `maxGrade`, the road is steep too. Absolute elevations are still honoured exactly, so existing documents keep their meaning. Horizontal spline interpolation is Catmull–Rom; vertical interpolation is linear between controls to avoid vertical overshoot. Use `smooth:false` for a polyline.
85
+
86
+ ```js
87
+ t.road({ id: 'access-road', points: [[-220,null,100],[-150,null,70],[-70,null,50]], width: 12, shoulder: 8 });
88
+ t.river({ id: 'river-main', points: [[20,14,-240],[35,8,0],[80,3,240]], width: 18, depth: 4, enforceDownhill: true });
89
+ t.water({ id: 'lake', kind: 'lake', at: [80,210], radius: 75, level: 4 });
90
+ ```
91
+
92
+ `enforceDownhill:true` validates non-increasing Y, rather than silently changing elevations in the API. River carving only lowers terrain. The water surface is a ribbon, not a flow simulation. Keep profiles sensible and avoid self-intersecting splines.
93
+
94
+ Lake flood fill starts at the supplied location, follows connected samples below `level`, and is limited by `radius`/mask. A seed above water level produces an empty mask and a diagnostic. Oceans flood from map boundaries. Water placement does not itself excavate a basin: lower the basin first. Scatter avoids flooded lake/ocean samples and submerged river ribbons by default.
95
+
96
+ ## Copy/paste and external heightfields
97
+
98
+ ```js
99
+ const heights = t.copyRegion({ at: [90,-70], size: 110, resolution: 65 });
100
+ t.paste({ id: 'copied-mass', data: heights, at: [-120,-90], size: 110,
101
+ rotation: 90, mirrorX: true, blend: 'replace', falloff: .2 });
102
+ ```
103
+
104
+ The copy contains heights only. Paste/stamp data is `{width,height,values}` in world metres. Fluent `heightmap()` / `paste()` / `stamp()` normalize typed arrays to plain arrays; raw recipe JSON must already contain plain arrays. RAW files have no self-describing dimensions/range. Preserve sidecars. PNG import accepts non-interlaced grayscale 8/16-bit images only, not RGB/palette images, and reports `hasEmbeddedRange`.
105
+
106
+ Stamp and paste footprints use `at: [x,z]`, `radius: [halfWidth,halfDepth]` or
107
+ `size: [width,depth]`, and `rotation` in degrees (positive turns +X towards +Z).
108
+ Existing `radius` takes precedence over `size`. All three landform operations use
109
+ positive `scale` for vertical gain and `offset` in metres after that gain;
110
+ additive blends add both the scaled height and offset. Heightmap accepts optional
111
+ `at`, `size`, `rotation` and `falloff` to place a bounded rectangular footprint.
112
+ Without these footprint fields it retains the supplied full-world sampling.
113
+ Outside a bounded footprint, existing terrain remains unchanged. Samples must
114
+ all be finite numbers. X/Z rotations that create overhangs have no heightfield
115
+ representation. These transforms change recipe parameters, not an independent
116
+ solid hidden inside the evaluated terrain.
117
+
118
+ ## Inspect and export
119
+
120
+ ```js
121
+ console.log(t.inspect({ resolution: 129 }));
122
+ const recipe = t.toJSON();
123
+ const same = Terrain.fromJSON(recipe);
124
+ const result = same.evaluate({ resolution: 257 });
125
+ const runtime = bakeTerrain(result, { chunkCells: 64, lodSteps: [1,2,4], skirtDepth: 6 });
126
+ const archive = await makeExport(result, recipe, 'runtime');
127
+ // Node: await writeFile(archive.name, archive.bytes)
128
+ ```
129
+
130
+ `makeExport` kinds are `project`, `png`, `raw`, `splat`, `glb`, `runtime`; all return `{name,type,bytes}`. Exporting does not mutate the authoring recipe. Collision height samples are row-major from Z=-size/2 to Z=+size/2, with X increasing along each row. Grid vertices are `(x/(N-1)-.5)*size, height[z*N+x], (z/(N-1)-.5)*size`.
131
+
132
+ Do not promise navmesh, physics registration, EXR, seamless mixed-LOD stitching, procedural caves, true water simulation, or production art from this version. GLB exports terrain only; load placement assets separately. Native-engine integration is via arrays/manifests and still needs an engine-specific adapter.
133
+
134
+ ## Bounds and operational safety
135
+
136
+ Valid grid resolutions: 17, 33, 65, 129, 257, 513, 1025 **vertices per side**. Use 129 or 257 while iterating, and inspect before moving to 513/1025. Changing resolution can change erosion and population. No cross-resolution exactness guarantee exists.
137
+
138
+ Maximum recipe layers: 512; maximum requested instances per scatter rule: 20,000; hydraulic droplets: 200,000; steps per droplet: 128; thermal/smooth iterations: 200; noise octaves: 10; spline/brush input points: 8192; interpolated brush centres per stroke: 65,536. These are input safeguards, not real-time performance targets. Do not choose all maximums together.
139
+
140
+ The evaluator is synchronous. An AbortSignal is checked between operations, not inside a long erosion pass. For real cancellation of a headless job, isolate it in a Node worker/process and terminate the worker. A prefix cache stores private snapshots, up to 48 MiB of counted height/splat/biome arrays by default; JavaScript metadata and output arrays are additional memory.
141
+
142
+ Never execute untrusted JavaScript from a recipe. The editor parses data only. Surface validation errors and preserve the previous valid recipe. Save the recipe, not just the baked mesh, before destructive project housekeeping.
143
+
144
+ Splatmaps are linear data, not color images: use a non-sRGB/NoColorSpace texture setting and renormalize sampled weights in the consuming shader. Do not use an 8-bit canvas conversion to read a PNG16 heightfield; the supplied decoder preserves its 16-bit samples.
145
+
146
+ ## Live editor: environment, assets and surfaces
147
+
148
+ The running editor exposes three revision-checked operations beside the document, each also behind a GUI
149
+ panel that calls the same code. A write names `baseRevision`; a stale one is a 409 and nothing changes.
150
+
151
+ - `controller.environment({ op: "get" | "patch" | "reset" }, revision)`: sun azimuth/elevation/intensity/colour,
152
+ sky fill, sky colour or image, `exp2` haze, exposure, sea colours, and an independent `lighting` image.
153
+ `null` returns a field to the project's own value; an unsupported fog mode is refused by name. The
154
+ project's `src/render/` decides what each value does. Nothing here evaluates the terrain.
155
+ - `controller.asset({ op: "register" | "upload" | "adjust" | "remove" | "list" | "map" | "unmap" }, revision)`:
156
+ `register` takes an absolute local path (an asset-MCP download result), `upload` takes base64 bytes. Files
157
+ are stored as `<kind>/<sha12>-<id>.<ext>` so a replacement is a new URL; GLB, PNG, JPEG, WebP, HDR and EXR are
158
+ recognised by their bytes, measured, and refused by name when damaged, over the project's limits, or when a
159
+ GLB points outside itself. Same id with other bytes needs `replace: true`. `remove` drops the entry, never
160
+ the file, and is refused while a surface or an environment still uses it.
161
+ - `map` binds an image to a `<surface>.<channel>` input the project exposes (`bark.albedo`, `stone.normal`);
162
+ albedo is read as sRGB, every other channel as linear data. Placing a model is a `scatter` layer whose
163
+ `asset` is its id; `adjust` sets the file's unit scale and pivot without editing it.
164
+
165
+ ## Three.js ownership
166
+
167
+ `@threenative/terrain/three` exports `toGeometry(bakeMesh(state))`. It creates only ordinary indexed geometry using your installed Three.js. Create the mesh/material in your own `src/render/`, register collision through the existing engine API, and dispose the geometry yourself. Baked vertex colours are absent by default; pass an explicit eight-channel sRGB `palette` to `bakeMesh` or `bakeTerrain` when wanted. The addon supplies no palette or material. The legacy `makeExport(..., "glb")` contains terrain only; use the separate static-world encoder below for resolved models and baked surfaces. The default GUI full-world action, chosen-resolution material/deformation baking and five final starter exports remain pending.
168
+
169
+
170
+ ## Resolved static-world GLB
171
+
172
+ Import `exportWorldGLB` from `@threenative/terrain/export` in browser authoring source. Its import is DOM-free; encoding requires FileReader and canvas. Root and `/three` remain headless. This entry uses the installed Three.js GLTFExporter rather than a terrain-specific loader format.
173
+
174
+ ```ts
175
+ import { exportWorldGLB } from '@threenative/terrain/export';
176
+ const output = await exportWorldGLB({
177
+ revision, // accepted document's 64-character content hash
178
+ snapshotTime, // finite seconds, pinned across all baked deformations
179
+ state, // evaluated committed recipe with applied override records
180
+ terrain, // canonical metre-frame Mesh with UVs and prepared PBR maps
181
+ assets, // Map<assetId, Object3D>: actual static models
182
+ transforms, // Map<placementId, Matrix4>: actual final grounded root matrices
183
+ water, // [{ id, object, time: snapshotTime, staleFrames: 0 }]
184
+ });
185
+ // output.bytes is the self-contained world.glb; persist only after success.
186
+ ```
187
+
188
+ Game source owns all geometry, materials and images. The terrain needs albedo `map`, `normalMap`, `roughnessMap` and `aoMap`; vertex colours alone are insufficient. MeshStandardMaterial metallic/roughness surfaces are supported. Colour/emissive images are sRGB, data maps are NoColorSpace. Supply decoded byte images (at most 16 megapixels); DataTexture images need complete RGBA bytes. Bake alpha into the colour image. Bake displacement, bump/light maps, node shaders, skinning/morphing and wind into the supplied static geometry/material first. Use tangent-space normal maps with equal X/Y scale magnitudes; bake object-space/anisotropic maps first. MeshPhysicalMaterial extensions are outside this bounded encoder's contract.
189
+
190
+ All final matrices use metres, Y-up, positive scale and no shear. Keys are durable placement IDs, never instance indices; unmatched matrices fail. Ordinary glTF nodes share model mesh data without requiring EXT_mesh_gpu_instancing. Source geometry, images and material values are copied before asynchronous encoding, so later source edits cannot mix the export snapshot. Input objects remain owned by the caller.
191
+
192
+ Every evaluated water body/river needs its actual baked object at exactly `snapshotTime` with `staleFrames: 0`. A low-resolution or stale ocean CPU height sample does not satisfy this contract. Missing/unbaked content rejects before producing a download; preserve the previous valid export and document. Encoding does not perform the project's material bake, chosen-resolution re-evaluation or ocean readback for you.
193
+
194
+ `output.report` records revision, resolution, time and placement/water IDs. Model cameras/lights and source userData are excluded. The receiving game supplies its own lighting, sky/environment, fog, exposure, post and live water/wind; GLB does not carry those running systems. A vanilla Three.js GLTFLoader or a normal game model loader can consume the bytes without this addon.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ThreeNative contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,72 @@
1
- # Temporary Holding Version
1
+ # @threenative/terrain
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Optional, headless terrain authoring recovered from Strata. One seeded recipe produces
4
+ height/splat arrays, resolved placements, baked LOD chunks and numerical exports.
5
+ Games consume baked data and own appearance, collision registration and disposal.
6
+
7
+ ```ts
8
+ import { Terrain, Mask, bakeMesh } from '@threenative/terrain';
9
+ import { toGeometry } from '@threenative/terrain/three';
10
+
11
+ const terrain = new Terrain({ size: 512, resolution: 257, seed: 73 })
12
+ .noise({ id: 'hills', amplitude: 35 })
13
+ .flatten({ id: 'camp', height: 12, mask: Mask.circle([20, 30], 15) });
14
+ const geometry = toGeometry(bakeMesh(terrain.evaluate()));
15
+ // Your game creates the Mesh/material and disposes geometry.
16
+ ```
17
+
18
+ Repeating an operation ID replaces that layer in place. `applyPatch` batches edits
19
+ atomically; malformed writes preserve the previous recipe. Evaluation is synchronous
20
+ and belongs in authoring/build jobs. Browser/editor dependencies are absent from
21
+ root and geometry imports. Three.js is a peer, never a bundled second copy.
22
+
23
+ The recovered `makeExport` GLB/runtime archives contain terrain geometry. The separate
24
+ `@threenative/terrain/export` entry exports resolved static worlds through Three.js
25
+ GLTFExporter: the caller supplies final models/matrices, baked UV PBR maps and
26
+ coherent water snapshots. It requires browser FileReader/canvas for encoding,
27
+ embeds images/buffers, excludes model cameras/lights, and uses ordinary shared
28
+ mesh nodes. Missing models/maps, shader deformation or stale water fail by name.
29
+ The default GUI full-world action, chosen-resolution baking and five final starter
30
+ world exports remain pending. See the agent guide for the explicit contract.
31
+ No browser/native terrain-game acceptance result is claimed by these library tests.
32
+
33
+ See [agent guide](AGENT_GUIDE.md) and [source provenance](THIRD_PARTY_NOTICES.md).
34
+
35
+ Optional live authoring uses `@threenative/terrain/editor/server` in the project's
36
+ Vite configuration and `@threenative/terrain/editor` in its browser authoring entry.
37
+ `terrainEditor({ documentPath })` mounts revisioned JSON/SSE endpoints on the
38
+ existing loopback server. The configured file contains `{ version: 1, recipe }`;
39
+ agent patches and GUI changes share that authority. Invalid external saves retain
40
+ the last valid document with a diagnostic, and stale writes return conflict.
41
+
42
+ The project provides `/terrain-editor/index.html` and calls `mountTerrainEditor`
43
+ with its own `IEditorView` and material colours. The addon creates no renderer or
44
+ scene. `TerrainEditorController(editorUrl)` exposes `activate`, `snapshot` and
45
+ atomic `commit`; the activation result names the actually bound URL, project,
46
+ session and revision. Use a configured forwarded viewer URL for remote access;
47
+ the loopback API does not become a public write service.
48
+
49
+ Scatter placement `id` identifies the layer, unsigned seed and candidate attempt,
50
+ not the accepted array index. Mask/spacing changes can remove candidates without
51
+ retargeting surviving keys or their seeded scale/yaw. Changing a seed creates new
52
+ identities. Save keys rather than mesh instance indices for later authoring edits.
53
+
54
+ The authoring document may also contain `placementOverrides`, keyed by those IDs.
55
+ `validatePlacementOverrides()` validates finite position/quaternion/positive scale
56
+ and defaults omitted `grounding` to true. `applyPlacementOverrides(state, overrides)`
57
+ attaches each requested transform to its placement and reports retained unmatched
58
+ keys. `bakeTerrain()` preserves those records. The game applies grounding against
59
+ its actual mesh bounds and terrain triangles; disabling grounding retains measured
60
+ clearance. The example's Select tool uses ordinary Three.js TransformControls and
61
+ numeric fields, saves once per drag, and preserves newer edits on conflict.
62
+
63
+ `mountTerrainEditor` passes `(host, controller)` to `createView`. The view implements
64
+ `setDocument(document, revision)` for metadata changes without terrain evaluation;
65
+ its `update(state)` may return the resolved state for inspection and data bakes.
66
+
67
+ The example also selects stamp, paste and heightmap layers by stable ID. One proxy
68
+ edits recipe footprints and vertical gain/offset; its line follows the visible
69
+ terrain, with rebuilding on commit. X/Z rotation is disabled because a heightfield
70
+ cannot overhang. `stamp`/`paste` accept positive vertical `scale`; `heightmap` also
71
+ accepts optional metre `at`/`size`, degree `rotation` and `falloff` for a bounded
72
+ footprint. The original full-world heightmap path remains the default.
@@ -0,0 +1,38 @@
1
+ # Strata source provenance
2
+
3
+ João supplied `strata-terrain.html` and `AGENT_GUIDE.md` and explicitly confirmed
4
+ on 2026-09-30: “I own it; use MIT.” The recovered modules and their integration
5
+ are distributed under the accompanying MIT license. The supplied files contained
6
+ no earlier copyright/license notice; no upstream license or attribution is invented.
7
+
8
+ Source SHA-256:
9
+
10
+ - `strata-terrain.html`: `c0230e6891db829a5196d93ff7c9d714a8f2d964cac4722eb8ce4b2f723d794b`.
11
+ - `AGENT_GUIDE.md`: `487453218f870c5e829d58aea3fe9323e74ea93e0aecef8d5c329ecf56972d06`.
12
+
13
+ Recovery used the HTML's named `sources` module map, rather than its duplicated
14
+ worker blob or minified viewer. The algorithms, validation and transactions are
15
+ maintained as TypeScript source. Viewer appearance/renderer code is excluded;
16
+ baking takes optional caller-owned colours and exports no chosen material.
17
+
18
+ The optional `src/editor/` tooling also recovers the supplied app, icons, starter
19
+ recipes, worker and static UI shell. Local-storage authority and the separate
20
+ Three.js viewer are replaced by a revisioned project document and a required
21
+ project-owned view. Formatting changes preserve the recovered controls; GUI
22
+ swatches receive the game's explicit colours. Root and `/three` imports do not
23
+ load this tooling.
24
+
25
+ Maintained behavior difference: scatter candidates use layer/unsigned-seed/attempt
26
+ keys and consume the same five seeded draws even when rejected. This fixes the
27
+ supplied accepted-index identity and conditional-RNG defect. Placement lists
28
+ intentionally differ from the supplied source; the supplied noise/erosion
29
+ regression remains unchanged. The adapter's pine, boulder and grass shapes are
30
+ adapted in the example's editable `src/render/props.ts`, using installed merge and
31
+ instance mechanisms; no addon material or renderer is introduced.
32
+
33
+ Landform extensions add positive vertical gain to stamp/paste and a bounded,
34
+ translated/rotated rectangular footprint to heightmap. Additive stamp/paste now
35
+ honor the supplied vertical offset, which the recovered additive branch ignored.
36
+ Height data validation rejects missing or nonnumeric samples. Default gain/offset and heightmaps without
37
+ footprint fields retain the recovered sampling; the numerical noise/erosion
38
+ golden remains unchanged.
@@ -0,0 +1,231 @@
1
+ import { P as PatchCommand } from '../terrain-gPnkCU6S.js';
2
+ import { I as ITerrainState } from '../types-Ant8-Axc.js';
3
+ import { I as IAuthoringDocument, a as ISavedCamera, b as IFocusRequest, c as IFocusOutcome, d as IFocusBounds, e as IEnvironment, f as IEditorActivation, g as IEditorSnapshot, h as ISpatialQuery, i as ISpatialObservation, j as ICameraOperation, k as ICameraResult, l as IEnvironmentOperation, m as IEnvironmentResult, n as IAssetOperation, o as IAssetResult } from '../server-ByM2hmYJ.js';
4
+ export { p as IAssetAdjust, q as IAssetBounds, r as IAssetKind, s as IAssetLimits, t as ICameraPose, u as ICameraSet, v as IFocusFraming, w as IFocusResolver, x as IFocusTarget, y as IHexColour, z as IPointObservation, A as IProfileObservation, B as IProfileSample, C as IProjectAsset, D as IReferenceObservation, E as ISavedSpatialReference, F as ISpatialControlPoint, G as ISpatialReference, H as ISurfaceMapping, J as ISurfaceMappings, K as ISurfaceReading, L as checkEnvironmentAssets, M as focusCamera, N as inspectSpatial, O as probeTerrain, P as runCameraOperation, Q as runEnvironmentOperation, R as sniff, S as surfaceSpace, T as validateAsset, U as validateAssets, V as validateCamera, W as validateCameras, X as validateEnvironment, Y as validateSpatialReference, Z as validateSurfaces } from '../server-ByM2hmYJ.js';
5
+ import 'vite';
6
+
7
+ /**
8
+ * Image header inspection for registered files: the real format, the real pixel size and whether the
9
+ * container is whole. It reads headers, never pixels, and never decodes into a texture: the project's
10
+ * render source loads the image through `ctx.assets` with the colour space its channel needs.
11
+ */
12
+ interface IImageReport {
13
+ readonly format: "png" | "jpeg" | "webp" | "hdr" | "exr";
14
+ readonly width: number;
15
+ readonly height: number;
16
+ readonly diagnostics: string[];
17
+ }
18
+ /**
19
+ * What an image's pixels mean once a game binds them. Colour is stored in sRGB and decoded to linear
20
+ * when sampled; every other channel is a number and must never be decoded.
21
+ */
22
+ /**
23
+ * The colour space of each surface channel.
24
+ * @summary Map a PBR channel name to the colour space its pixels are read in
25
+ * @requires npm i -D @threenative/terrain
26
+ * @situation load an imported albedo as sRGB and a normal, roughness, ao, height, opacity or metalness map as linear data
27
+ * @constraint a table only; the project's render source loads and binds the texture
28
+ * @example const space = SURFACE_CHANNELS.normal;
29
+ * @override the project owns which surface inputs exist and what they draw
30
+ */
31
+ declare const SURFACE_CHANNELS: {
32
+ readonly albedo: "srgb";
33
+ readonly ao: "linear";
34
+ readonly height: "linear";
35
+ readonly metalness: "linear";
36
+ readonly normal: "linear";
37
+ readonly opacity: "linear";
38
+ readonly roughness: "linear";
39
+ };
40
+ type ISurfaceChannel = keyof typeof SURFACE_CHANNELS;
41
+ /**
42
+ * Validate a PNG, JPEG, WebP, Radiance HDR or OpenEXR container and read its pixel size.
43
+ * @summary Inspect an image file's container and pixel size without decoding it
44
+ * @requires npm i -D @threenative/terrain
45
+ * @situation check an imported surface or environment image for damage and for the project's size limit
46
+ * @constraint headers and checksums only; throws by name for a damaged, truncated or oversize file
47
+ * @example const report = inspectImage(bytes, "png", { maxDimension: 8192 });
48
+ * @override the project sets the dimension limit
49
+ */
50
+ declare function inspectImage(bytes: Uint8Array, format: string, limits: {
51
+ maxDimension: number;
52
+ }): IImageReport;
53
+
54
+ /** The live view's own camera surface, shared by the GUI list and a controller. */
55
+ interface IViewCamera {
56
+ /** The actual current viewer pose, not the last saved bookmark. */
57
+ read(): IViewerPose;
58
+ /** Apply a saved pose to the real camera, or the ordinary editor camera when given null. */
59
+ apply(camera: ISavedCamera | null): void;
60
+ /**
61
+ * Frame a target for the live viewport; an unknown target keeps the last valid camera and
62
+ * reports why. The view measures the aspect itself, so a caller cannot save a stale one.
63
+ */
64
+ focus(request: Omit<IFocusRequest, "aspect">): IFocusOutcome;
65
+ /** Resolves a non-point target against the live world; the view owns the world matrices. */
66
+ resolve(target: {
67
+ kind: "prop" | "landmark" | "region";
68
+ id: string;
69
+ }): IFocusBounds | undefined;
70
+ /** Clip-square extent of bounds under the live camera, so a framing can be measured, not assumed. */
71
+ measure(bounds: IFocusBounds): {
72
+ x: number;
73
+ y: number;
74
+ z: boolean;
75
+ };
76
+ }
77
+ /** The live view's environment surface: bind saved overrides to the project's own render source. */
78
+ interface IViewEnvironment {
79
+ /**
80
+ * Apply saved overrides to the real lights, haze, sky, exposure and sea. Absent fields restore
81
+ * the project's own value; a field this source cannot honour throws by name and the scene keeps
82
+ * its last valid look.
83
+ */
84
+ apply(environment: IEnvironment | undefined): void;
85
+ /** The effective values now in use: the project's own, with the saved overrides on top. */
86
+ read(): IEnvironment;
87
+ }
88
+ interface IViewerPose {
89
+ readonly position: readonly [number, number, number];
90
+ readonly target: readonly [number, number, number];
91
+ readonly up: readonly [number, number, number];
92
+ readonly projection: "perspective" | "orthographic";
93
+ readonly fov: number | null;
94
+ readonly extent: number | null;
95
+ readonly zoom: number | null;
96
+ readonly near: number;
97
+ readonly far: number;
98
+ readonly aspect: number;
99
+ readonly activeCamera: string | null;
100
+ }
101
+ interface IEditorView {
102
+ readonly backend: string;
103
+ update(state: ITerrainState): ITerrainState | undefined;
104
+ setDocument(document: IAuthoringDocument, revision: string): void;
105
+ pick(clientX: number, clientY: number): [number, number, number] | null;
106
+ setMode(mode: string): void;
107
+ setBrush(brush: {
108
+ at: [number, number];
109
+ radius: number;
110
+ rotation?: number;
111
+ shape?: string;
112
+ } | null): void;
113
+ showSpline(points: readonly [number, number, number][]): void;
114
+ setNavigation(enabled: boolean): void;
115
+ setSelection?(enabled: boolean): void;
116
+ selectLayer?(id: string): void;
117
+ setView(view: string): void;
118
+ frame(): void;
119
+ registerAsset?(id: string, object: unknown): void;
120
+ /**
121
+ * The asset IDs this project can place and scatter. The recovered palette asks the view rather
122
+ * than shipping a starter list, so a game's own props and its registered imports are what the
123
+ * scatter and clear controls offer.
124
+ */
125
+ propAssets?(): readonly string[];
126
+ /**
127
+ * Settles once every registered model the view started loading is placeable or has failed. The
128
+ * preview waits on it before drawing a revision, so a placement of a model still arriving is
129
+ * not reported as a missing asset.
130
+ */
131
+ assetsReady?(): Promise<void>;
132
+ /**
133
+ * The texture inputs of this project's render source that an imported image can replace, named
134
+ * `<surface>.<channel>`. The project decides what exists; the GUI offers exactly this list.
135
+ */
136
+ surfaceInputs?(): readonly {
137
+ input: string;
138
+ channel: string;
139
+ }[];
140
+ cameras?(): IViewCamera;
141
+ environment?(): IViewEnvironment;
142
+ /**
143
+ * The whole rendered revision as a portable GLB: terrain, resolved models with their final
144
+ * transforms, this game's baked water and its portable PBR surfaces. Optional because a headless
145
+ * or DOM-free view has nothing to encode from; the export card then says so rather than quietly
146
+ * falling back to the terrain-only worker export.
147
+ */
148
+ exportCurrentWorld?(): Promise<{
149
+ name: string;
150
+ mime: string;
151
+ bytes: Uint8Array;
152
+ }>;
153
+ dispose(): void;
154
+ }
155
+ /**
156
+ * HTTP controller for the project-local terrain editor.
157
+ * @requires npm i -D @threenative/terrain
158
+ * @situation inspect and atomically edit the terrain editor from an agent
159
+ * @constraint optional tooling; only subscribe/mount touches DOM; stale revisions fail explicitly
160
+ * @example const controller = new TerrainEditorController(editorUrl); const snapshot = await controller.snapshot();
161
+ * @override editorUrl is the activation-returned project URL
162
+ */
163
+ declare class TerrainEditorController {
164
+ #private;
165
+ readonly baseUrl: string;
166
+ constructor(editorUrl: string);
167
+ activate(): Promise<IEditorActivation>;
168
+ snapshot(): Promise<IEditorSnapshot>;
169
+ /**
170
+ * Answer a read-only spatial query against one exact revision, GUI or headless.
171
+ * @param query a `point`, `profile` or `reference` query from the saved document revision
172
+ */
173
+ inspect(query: ISpatialQuery, baseRevision: string): Promise<{
174
+ result: ISpatialObservation;
175
+ }>;
176
+ /**
177
+ * Run one create / get / list / update / delete / activate camera operation against one exact
178
+ * revision. Editor observation cameras only: gameplay cameras belong to the consuming game.
179
+ * @param operation a camera operation from {@link ICameraOperation}
180
+ */
181
+ camera(operation: ICameraOperation, baseRevision: string): Promise<ICameraResult & {
182
+ revision: string;
183
+ }>;
184
+ /**
185
+ * Run one get / patch / reset against the saved preview environment at one exact revision.
186
+ * Appearance only: terrain, collision and placements are never re-evaluated.
187
+ * @param operation an environment operation from {@link IEnvironmentOperation}
188
+ */
189
+ environment(operation: IEnvironmentOperation, baseRevision: string): Promise<IEnvironmentResult & {
190
+ revision: string;
191
+ }>;
192
+ /**
193
+ * Register, upload, adjust, remove or list project assets at one exact revision. `register`
194
+ * takes a local file path, the shape an asset-MCP download returns; `upload` takes bytes a
195
+ * browser read. Both store the file under a content-hashed name and measure it.
196
+ * @param operation an asset operation from {@link IAssetOperation}
197
+ */
198
+ asset(operation: IAssetOperation, baseRevision: string): Promise<IAssetResult & {
199
+ revision: string;
200
+ }>;
201
+ commit(transaction: {
202
+ baseRevision: string;
203
+ document: IAuthoringDocument;
204
+ } | {
205
+ baseRevision: string;
206
+ commands: readonly PatchCommand[];
207
+ }): Promise<IEditorSnapshot>;
208
+ subscribe(listener: (snapshot: IEditorSnapshot) => void, onError: () => void): () => void;
209
+ }
210
+ /**
211
+ * Mount recovered terrain controls around the project-owned ThreeNative view.
212
+ * @requires npm i -D @threenative/terrain
213
+ * @situation open the terrain brush and layer GUI around game-owned rendering
214
+ * @constraint browser authoring only; createView and materialColours are required game choices
215
+ * @example await mountTerrainEditor({ createView: createEditorView, materialColours: terrainPalette });
216
+ * @override createView owns the renderer scene, materials, lighting and camera
217
+ */
218
+ declare function mountTerrainEditor(options: {
219
+ createView(host: HTMLElement, controller: TerrainEditorController): Promise<IEditorView>;
220
+ materialColours: readonly (readonly [number, number, number])[];
221
+ onEvaluated?(event: {
222
+ revision: string;
223
+ state: ITerrainState;
224
+ ms: number;
225
+ }): void;
226
+ }): Promise<{
227
+ controller: TerrainEditorController;
228
+ dispose(): void;
229
+ }>;
230
+
231
+ export { IAssetOperation, IAssetResult, ICameraOperation, ICameraResult, type IEditorView, IEnvironment, IEnvironmentOperation, IEnvironmentResult, IFocusBounds, IFocusOutcome, IFocusRequest, type IImageReport, ISavedCamera, ISpatialObservation, ISpatialQuery, type ISurfaceChannel, type IViewCamera, type IViewEnvironment, type IViewerPose, SURFACE_CHANNELS, TerrainEditorController, inspectImage, mountTerrainEditor };