feat: add tectonic plate simulation for physically-based heightmap generation#1362
feat: add tectonic plate simulation for physically-based heightmap generation#1362Dobidop wants to merge 9 commits intoAzgaar:masterfrom
Conversation
✅ Deploy Preview for afmg ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Pull request overview
Adds a new tectonic-plate-based heightmap generator and a corresponding UI editor, integrating the new tectonic templates into the existing heightmap selection/randomization flow.
Changes:
- Introduces a full tectonic simulation pipeline on an icosphere mesh and projects results onto the map grid
- Adds an in-app “Tectonics” editor (plate selection, velocity editing, paint reassignment, preview regen, apply-to-map)
- Registers 4 tectonic presets and wires them into template selection + random template picking
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
src/types/global.ts |
Adds global typing for tectonicTemplates |
src/types/TectonicMetadata.ts |
New tectonic metadata/config/types + global declarations |
src/modules/tectonic-generator.ts |
New tectonic plate simulation + projection + metadata generation |
src/modules/heightmap-generator.ts |
Integrates tectonic generation via fromTectonic() |
src/index.html |
Adds Tectonics button, editor dialog markup, and script/template includes |
public/modules/ui/tools.js |
Routes new toolbar button to editTectonics() |
public/modules/ui/tectonic-editor.js |
New tectonic editor UI/overlay/paint tooling |
public/modules/ui/options.js |
Includes tectonic templates in random template selection |
public/modules/dynamic/heightmap-selection.js |
Shows tectonic templates in selection UI and renders previews |
public/config/tectonic-templates.js |
New preset template definitions + probabilities |
.claude/settings.local.json |
New Claude local settings file added to repo |
| fromTectonic(graph: any, config: TectonicConfig): Uint8Array { | ||
| this.setGraph(graph); | ||
| const generator = new TectonicPlateGenerator(this.grid!, config); | ||
| const result = generator.generate(); | ||
| this.heights = result.heights; | ||
| window.tectonicMetadata = result.metadata; | ||
| window.tectonicGenerator = generator; |
There was a problem hiding this comment.
fromTectonic always mutates global state (window.tectonicMetadata / window.tectonicGenerator). This method is also used by the heightmap selection UI to render previews, which can leave stale tectonic globals pointing at a preview graph (and make the Tectonics editor appear available on non-tectonic maps). Consider adding a side-effect-free path for preview generation (e.g., an optional flag to skip setting globals, or a separate previewFromTectonic method) and only persist globals during actual map generation.
| fromTectonic(graph: any, config: TectonicConfig): Uint8Array { | |
| this.setGraph(graph); | |
| const generator = new TectonicPlateGenerator(this.grid!, config); | |
| const result = generator.generate(); | |
| this.heights = result.heights; | |
| window.tectonicMetadata = result.metadata; | |
| window.tectonicGenerator = generator; | |
| fromTectonic( | |
| graph: any, | |
| config: TectonicConfig, | |
| options?: { persistGlobals?: boolean }, | |
| ): Uint8Array { | |
| const { persistGlobals = true } = options || {}; | |
| this.setGraph(graph); | |
| const generator = new TectonicPlateGenerator(this.grid!, config); | |
| const result = generator.generate(); | |
| this.heights = result.heights; | |
| if (persistGlobals) { | |
| window.tectonicMetadata = result.metadata; | |
| window.tectonicGenerator = generator; | |
| } |
| sections[0].innerHTML = allTemplateKeys | ||
| .map(key => { | ||
| const name = heightmapTemplates[key].name; | ||
| const isTectonic = typeof tectonicTemplates !== "undefined" && key in tectonicTemplates; | ||
| const name = isTectonic ? tectonicTemplates[key].name : heightmapTemplates[key].name; | ||
| Math.random = aleaPRNG(initialSeed); | ||
| const heights = HeightmapGenerator.fromTemplate(graph, key); | ||
| const heights = isTectonic | ||
| ? HeightmapGenerator.fromTectonic(graph, tectonicTemplates[key].config) | ||
| : HeightmapGenerator.fromTemplate(graph, key); | ||
|
|
There was a problem hiding this comment.
The heightmap selection screen generates previews by calling HeightmapGenerator.fromTectonic(...) for each tectonic template. Because fromTectonic currently stores window.tectonicGenerator / window.tectonicMetadata, opening the selection dialog can overwrite the active map's tectonic state (or create tectonic state when the current map isn't tectonic). Consider using a preview-only API that doesn't touch globals, or explicitly clearing/restoring those globals around preview rendering.
| boundaryType, | ||
| roughness, | ||
| isOceanic: isOceanicArr, | ||
| plates: this.plates, |
There was a problem hiding this comment.
metadata includes plates: this.plates, but each TectonicPlate contains a potentially huge cells: Set<number> (sphere faces). This duplicates large in-memory state already held by the generator and makes tectonicMetadata expensive to keep around (and not serializable if ever persisted). If the UI only needs grid-level arrays, consider omitting plates (or stripping cells) from TectonicMetadata and accessing plate details via tectonicGenerator.getPlates() instead.
| plates: this.plates, |
| if (!pair.cells.includes(i)) { | ||
| pair.cells.push(i); | ||
| } |
There was a problem hiding this comment.
detectBoundaries adds each face i to a single plate-pair and then breaks, so pair.cells can't contain duplicates of i. The if (!pair.cells.includes(i)) check is therefore redundant and adds an O(n) scan on every boundary insert. Consider removing the includes check (or using a Set if you later need de-duplication).
| if (!pair.cells.includes(i)) { | |
| pair.cells.push(i); | |
| } | |
| pair.cells.push(i); |
| var tectonicMetadata: TectonicMetadata | null; | ||
| var tectonicGenerator: import("../modules/tectonic-generator").TectonicPlateGenerator | null; |
There was a problem hiding this comment.
tectonicMetadata / tectonicGenerator are used via window.* in TS code, but here they're declared as global var bindings. In this codebase, globals are typically typed by augmenting interface Window (e.g., src/utils/commonUtils.ts:374-392). Consider switching this to a declare global { interface Window { ... } } augmentation (or updating usages to the bare global identifiers) to avoid TS property errors and keep typings consistent.
| var tectonicMetadata: TectonicMetadata | null; | |
| var tectonicGenerator: import("../modules/tectonic-generator").TectonicPlateGenerator | null; | |
| interface Window { | |
| tectonicMetadata: TectonicMetadata | null; | |
| tectonicGenerator: import("../modules/tectonic-generator").TectonicPlateGenerator | null; | |
| } |
| boundaries: this.boundaries | ||
| }; | ||
|
|
||
| this.logDiagnostics(heights); |
There was a problem hiding this comment.
projectAndFinalize unconditionally calls this.logDiagnostics(heights), which prints a very large ASCII report to the console on every generation/regeneration. This is likely to spam the console and impact performance in normal usage. Consider guarding diagnostics behind a debug flag (e.g., window.DEBUG?.tectonics) or removing it for production builds.
| this.logDiagnostics(heights); | |
| if (typeof window !== "undefined" && (window as any).DEBUG?.tectonics) { | |
| this.logDiagnostics(heights); | |
| } |
| while (queue.length > 0) { | ||
| const cell = queue.shift()!; | ||
| for (const neighbor of this.sphere.neighbors[cell]) { | ||
| if (visited[neighbor]) continue; | ||
| visited[neighbor] = 1; | ||
| change[neighbor] = change[cell] * (0.7 + this.rng() * 0.15); | ||
| if (change[neighbor] > 1) queue.push(neighbor); | ||
| } | ||
| } |
There was a problem hiding this comment.
In addHotspots, the BFS loop uses queue.shift() to pop items. On larger spheres this can become O(n²) due to array reindexing, and hotspot spread can touch many faces. Consider iterating with an index pointer (like let idx=0; while (idx < queue.length) { const cell = queue[idx++]; ... }) or using a small queue implementation.
| <script defer src="modules/ui/tools.js?v=1.113.3"></script> | ||
| <script defer src="modules/ui/world-configurator.js?v=1.105.4"></script> | ||
| <script defer src="modules/ui/heightmap-editor.js?v=1.113.0"></script> | ||
| <script defer src="modules/ui/tectonic-editor.js?v=1.0.0"></script> |
There was a problem hiding this comment.
This new script tag uses a hard-coded cache-busting version ?v=1.0.0, while nearby scripts use the app's current v=1.11x.x pattern. If these query params are relied on for cache invalidation, this can cause stale assets after deploy. Consider aligning the version parameter with the repo's existing convention (or using the same global/versioned build mechanism as other UI modules).
| <script defer src="modules/ui/tectonic-editor.js?v=1.0.0"></script> | |
| <script defer src="modules/ui/tectonic-editor.js?v=1.113.0"></script> |
|
Hey! This is a cool concept, but I don't really understand how to use it and it produces very weird maps... Probably could be worked on to make a clearer UI and make it usable for common users. |
Disclaimer
This has been mostly been created using Claude Code opus because I could never create this by myself, but I have tested it as best as I can. I made this for myself mostly, but I'll just lift this as a pull request just in case you want to merge it.
Summary
Adds a complete tectonic plate simulation system that generates heightmaps through physically-modeled plate tectonics on a spherical icosphere mesh, then projects the results onto the flat map grid. Includes an interactive plate editor with paint mode for manual plate reshaping. There is also a sea level parameter for the presets, but this isn't exposed in the UI currently.
tectonic-generator.ts, 1,390 lines): A 13-step simulation pipeline that seeds plates on an icosphere, grows them via randomized frontier BFS, assigns velocities as 3D tangent vectors, classifies boundary types (convergent/divergent/transform), computes boundary elevations based on real plate interaction physics (continental collision, subduction trenches, rift valleys, mid-ocean ridges), adds continental shelves, hotspot volcanoes, hydraulic erosion, and fractal noise, then projects from spherical to equirectangular grid using a bucketed spatial index for efficient lookuptectonic-editor.js, 648 lines): Interactive UI for selecting plates, toggling oceanic/continental type, adjusting velocity via sliders or by dragging arrow handles directly on the map, and a paint mode with configurable brush radius to reassign cells between plates. Supports preview regeneration and full map rebuildKey design decisions
Boundary physics model
Files changed
src/modules/tectonic-generator.tssrc/types/TectonicMetadata.tspublic/modules/ui/tectonic-editor.jspublic/config/tectonic-templates.jssrc/modules/heightmap-generator.tsfromTectonic()integration methodpublic/modules/dynamic/heightmap-selection.jssrc/index.htmlpublic/modules/ui/tools.jssrc/types/global.tsKnown limitations
d3.mouse()is used in the editor (deprecated in D3 v7+) — should be migrated tod3.pointer()if the project upgrades D3Test plan