163 lines
16 KiB
Markdown
163 lines
16 KiB
Markdown
# CLAUDE.md
|
||
|
||
## Project Overview
|
||
|
||
World Orogen — a browser-based procedural planet generator using Three.js and ES modules with no build step.
|
||
|
||
**World Orogen is concept art for planets, not a geophysical simulator.** Every feature should prioritize making the output *look* more believable or helping users iterate faster. Never slow down generation to chase physical accuracy — if a simpler approximation looks just as good, use it. However, the scientific grounding is what makes the output convincing: tectonic models inspired by real geology, pressure-driven wind patterns, and Köppen classification aren't optional polish — they're the reason the output passes the glance test. Preserve and extend this scientific foundation whenever it serves the visuals. The tool's job is to be the fastest path from a blank page to a world worth building on.
|
||
|
||
## Guiding Principles
|
||
|
||
All three tenets should be considered simultaneously. When they conflict, break ties in this order:
|
||
|
||
1. **Artistic appeal** — The output should look visually interesting and compelling, informed by real science but not constrained by it. Aesthetics come first.
|
||
2. **Ease of use and efficiency** — The interface should be approachable and intuitive. Generation should be fast. Don't sacrifice usability for realism.
|
||
3. **Scientific plausibility** — Terrain, tectonics, and geology should be grounded in real planetary science. Results don't need to be physically accurate simulations, but they should be believable.
|
||
|
||
## What Users Love (Protect These)
|
||
|
||
User feedback consistently highlights these as World Orogen's core strengths. Any change should preserve or enhance them — never degrade them as a side effect.
|
||
|
||
1. **Climate simulation depth** — The climate view (wind, ocean currents, precipitation, Köppen) is the single most-cited differentiator. Users call it "the only map generator with this level of detail" and say it's what sets Orogen apart from Azgaar and every other tool. Never simplify or remove climate layers. When adding features, consider whether they can leverage the climate system (e.g. rivers fed by precipitation, settlements placed by climate).
|
||
|
||
2. **Instant, in-browser, zero-friction** — No install, no account, no build step. Users love that they can open a URL and have a planet in seconds. Never add mandatory sign-up, downloads, or server dependencies. Keep generation fast — if a feature risks slowing generation significantly, make it optional or deferred (like the existing on-demand climate above 300K).
|
||
|
||
3. **Interactive plate editing** — Users say "haven't seen this functionality anywhere else." The Ctrl-click multi-select → Rebuild workflow is a key differentiator. Don't break this interaction pattern. Extend it (e.g. plate direction editing) rather than replacing it.
|
||
|
||
4. **True globe with proper wrapping** — Users who came from Azgaar specifically cite the globe as a reason they switched. The globe-first experience, equirectangular map as secondary view, and seamless wrapping matter. Don't make the map view primary or break globe rendering.
|
||
|
||
5. **Free and open source** — Repeatedly praised. No paywalls, no feature-gating, no "pro" tier. This is a trust signal that drives adoption and contributions.
|
||
|
||
6. **Works on mobile** — Users are surprised it runs well on phones. Maintain the responsive bottom-sheet layout, touch targets, and pinch-to-zoom. Don't add features that only work on desktop without a mobile equivalent.
|
||
|
||
7. **Terrain aesthetics** — "Fractal-looking mountains," realistic erosion, organic coastlines. The visual quality of the terrain itself gets specific praise. Protect the artistic output of the erosion and terrain post-processing pipeline.
|
||
|
||
When proposing a new feature or change, ask: "Does this preserve all seven strengths above?" If it trades one for another, flag the tradeoff explicitly.
|
||
|
||
## Key Rules
|
||
|
||
After any code change, check whether README.md needs updating. The README documents all UI controls, features, algorithms, and project structure. If a change adds, removes, or modifies any of the following, update the README to match:
|
||
|
||
- Sliders, dropdowns, toggles, or other UI controls (names, ranges, defaults)
|
||
- User interactions (keyboard shortcuts, mouse actions, edit behaviors)
|
||
- Generation pipeline steps or algorithms
|
||
- Visual features (rendering, overlays, debug layers)
|
||
- Project file structure (new files, renamed files, removed files)
|
||
- External dependencies
|
||
|
||
After any code change, check whether the tutorial modal content (in `index.html`, inside `#tutorialOverlay`) needs updating. The tutorial steps describe the app's features and interactions. If a change adds, removes, or modifies any of the following, update the relevant tutorial step to match:
|
||
|
||
- Core workflow (how to generate a planet, what controls to use)
|
||
- Interactive features (navigation, editing, keyboard/mouse actions)
|
||
- What the tool does or its key selling points
|
||
|
||
After any code change that adds significant user-facing features, ask the developer if they would like to update the What's New modal (in `index.html`, inside `#whatsNewOverlay`). The modal is version-gated by the `VERSION` constant in `initWhatsNew()` in `js/main.js` — bumping this string will show the modal again to returning users on their next visit.
|
||
|
||
After any code change that affects the UI, ensure it works on mobile. The app uses a responsive bottom-sheet layout on screens ≤ 768px (`styles.css` media queries) and has touch-specific behavior throughout. If a change adds, removes, or modifies any of the following, verify and update the mobile experience:
|
||
|
||
- New buttons or controls — must have ≥ 44px touch targets on mobile (see `@media (max-width: 768px)` in `styles.css`)
|
||
- New interactions — must have touch equivalents; desktop uses Ctrl-click for plate editing, mobile uses `state.editMode` toggle (`js/edit-mode.js`); desktop uses scroll-to-zoom, mobile uses pinch (`js/scene.js`)
|
||
- Tooltips — must reposition above their trigger on mobile, not to the right (overflow off-screen)
|
||
- New overlays or modals — must be usable within the bottom-sheet layout and not be hidden behind it
|
||
- Performance-sensitive features — consider lower thresholds on touch devices (detail warnings, export limits); check `state.isTouchDevice` in `js/state.js`
|
||
- Info/hint text — update both desktop text (in `index.html`) and the mobile-specific text set in `js/main.js` (search for `state.isTouchDevice`)
|
||
|
||
After any code change to simulation or climate code, ensure **scale invariance** — the result must look equivalent regardless of the Detail slider (numRegions from 2K to 2.5M). The key rule: never use raw cell-hop counts or neighbor-displacement magnitudes without scaling by resolution. Specifically:
|
||
|
||
- **Smoothing passes** must target a physical distance: `Math.max(minPasses, Math.round(targetKm / avgEdgeKm))` where `avgEdgeKm = (π × 6371) / √numRegions`. Never write a bare `smooth(mesh, field, 5)`.
|
||
- **Multipliers on neighbor-displacement quantities** (e.g. wind convergence, which sums `wind · displacement`) must normalize by `avgEdgeRad = π / √numRegions` since displacement magnitudes shrink at higher resolution.
|
||
- **BFS hop thresholds** must be expressed as `Math.round(targetKm / avgEdgeKm)`, not as fixed integers.
|
||
- **Thresholds in physical units** (degrees latitude, km altitude, °C, mm precipitation) are inherently scale-invariant and do NOT need scaling — e.g. "28° from ITCZ" or "heightKm > 1.5" are fine at any resolution.
|
||
- When in doubt, ask: "if I double numRegions, does this value change meaning?" If yes, it needs scaling.
|
||
|
||
After any code change that adds, removes, or modifies features, check whether the SEO and AISEO files need updating. The project has several files that describe the app to search engines and AI models. These must stay accurate — outdated claims are worse than no claims. If a change adds, removes, or modifies any of the following, update the relevant files:
|
||
|
||
- **`index.html` `<head>` meta tags** — The `<title>`, `description`, `og:description`, `twitter:description`, and `keywords` meta tags describe what the app does. Update if core capabilities change (e.g. new simulation type, new export format, new interaction mode).
|
||
- **`index.html` JSON-LD structured data** — The `<script type="application/ld+json">` block contains a `WebApplication` schema with a `featureList` array. Add or remove entries when major features are added or removed.
|
||
- **`index.html` hidden `<main>` block** — The visually hidden semantic HTML block (right after `<body>`) describes the app for crawlers. Update its feature list, use cases, or description when the app's capabilities change meaningfully.
|
||
- **`llms.txt`** — A plain-text file at the project root that describes the tool for AI assistants. Update its feature list, "who it's for" section, or technical details when capabilities change. Keep it concise and factual.
|
||
- **`sitemap.xml`** — Update the `<lastmod>` date when deploying significant changes.
|
||
|
||
Files that rarely need updating: `robots.txt` (only if adding pages or restricting crawlers), `CNAME` (only if domain changes), `preview.png` (only if the app's visual appearance changes dramatically).
|
||
|
||
After any code change that adds, removes, or modifies slider controls, update the planet code encoding in `js/planet-code.js` to match. The planet code packs the seed and all slider values into a compact base36 string using mixed-radix integer packing. If a slider's range, step, or count changes, or if a new slider is added, update:
|
||
|
||
- The `SLIDERS` array (min, step, count for each slider)
|
||
- The `RADICES` array (the count values in right-to-left order)
|
||
- The `encodePlanetCode` and `decodePlanetCode` functions (packing/unpacking order)
|
||
- The corresponding slider wiring in `js/main.js` (the `map` objects in the `generate-done` handler, `applyCode`, and hash-loading code)
|
||
|
||
## Painted-map import (js/painted.js)
|
||
|
||
The import page has a second source: a painting whose colours are legend *classes* — uplift rates and
|
||
erodibilities, never heights — solved into terrain by a Braun-Willett stream-power solve on the sphere mesh.
|
||
The legend JSON schema is shared with the Salty terrain generator (`Tools/Terrain`, `terrain plan`), so keep
|
||
it compatible: read new keys optionally, never rename existing ones. Rules that follow from the physics:
|
||
|
||
- **Paint the uplift, never the height.** Nothing on this path may hand the solve a painted surface; the
|
||
painting decides where the land rises and how fast, and the rivers, divides and valleys are the solve's.
|
||
- **Thresholds are quantiles of the planet.** The massif fabric and the rock field are cut by rank over every
|
||
region on the globe (`rankField`), never per class or per landmass, so an island gets all of a massif or
|
||
none of it the way a real island would.
|
||
- **The coast moves before the solve.** Coast roughening is noise on the signed distance from the painted
|
||
waterline, applied to the land mask, not a warp of the finished elevation.
|
||
- **Relief is a scale, not a solve parameter.** For n = 1 the steady state is linear in U/K, so the Peak
|
||
Height slider rescales the solved field afterwards; do not add a clamp inside the loop that would break
|
||
that linearity without a reason written down.
|
||
- Every noise field is sampled at the region's 3D position on the unit sphere, so the seam and the poles
|
||
need no special handling; keep it that way.
|
||
- The class, uplift, erodibility, drainage, slope and basin layers are debug layers coloured by
|
||
`js/painted-layers.js`, which planet-mesh.js uses for the globe, the map and the exports alike; a new
|
||
layer is added there once and nowhere else.
|
||
- **The class table prints the typical angle, not only the divide angle** (`js/painted-report.js`, ported from
|
||
the Go tool's plan report). The divide is the steepest ground a rate can make and almost none of a map is
|
||
divide; the median is about a third of it in tangent. An author who reads the divide as the landscape sets
|
||
every rate two or three times too hot, so the typical angle is the column and the divide is the tooltip.
|
||
These functions are checked against `terrain plan`'s `plan.json` and must stay exact.
|
||
- **The overlay is a texture, never a region colour** (`js/painted-overlay.js`, `painted-overlay-view.js`). It
|
||
is painted at the template's resolution, where a road is a few pixels wide and a region here is tens of
|
||
kilometres across, so voting it onto the mesh would lose every thin stroke. Marks *are* voted onto regions
|
||
for one thing only: `coast_jitter`, the single mark property any pass reads. Blank on an overlay is alpha,
|
||
never a reserved colour, and an opaque pixel matching no mark is dropped and counted rather than snapped to
|
||
the nearest - the class legend's rule inverted, because most of an overlay is nothing.
|
||
- **Planet.json outranks a legend's own `planet` block.** It is the manifest the two-hour bake reads; a legend's
|
||
copy is a convenience for when it is absent.
|
||
- **The studio link is read-only by construction.** `terrain studio` sets its CORS header on GET and HEAD alone
|
||
and answers no preflight, so this page can read the painting and the legends and can never paint, save, plan
|
||
or bake. Do not ask for that to be widened.
|
||
|
||
## Unreal landscape export (js/unreal-export.js, unreal-render.js, unreal-ui.js)
|
||
|
||
A third export, beside the map PNGs: a window of the planet rendered straight into the tile set Unreal
|
||
Engine's landscape importer takes - per-tile 16-bit heights at 255*N+1 vertices, an 8-bit weightmap per
|
||
paint layer, and the `Region.json` that describes the grid - written to a folder through the File System
|
||
Access API. It exists because the map exports cannot be imported: one is a picture, and the other is a flat
|
||
equirectangular PNG with **no scale on it**, so the consumer had to invent metres-per-pixel. Rules:
|
||
|
||
- **The scale is an input and it is recorded.** `planet_circumference_km` is what turns the window's degrees
|
||
into ground. It is asked for because it cannot be derived from a sphere mesh, and the manifest writes back
|
||
the metres-per-pixel, the window in degrees and the projection, so nothing downstream guesses again.
|
||
- **Sample the window once, then cut it.** Every tile is resampled out of one float raster by its *global*
|
||
vertex position, which is what makes a shared column bit-identical between neighbours. Never render a tile
|
||
under its own camera: one 16-bit step is centimetres of crack along a seam, and a rasteriser gives no
|
||
guarantee that two frustums agree on a shared edge. The seam test is the acceptance gate.
|
||
- **Tiles carry a one-vertex margin while the layers are derived.** The layers read slope; a one-sided
|
||
difference at a tile edge is not what the neighbour computes there, and without the margin every boundary
|
||
is a one-vertex line of different paint.
|
||
- **Heights ride `heightmapColor`'s -5..6 km ramp, read as floats.** Do not write kilometres straight into
|
||
the vertex colour attribute to save the conversion: negative values then depend on three.js colour
|
||
management staying out of the way, and the float target already resolves a millimetre over that ramp.
|
||
- **Never `<input type="number">` for a decimal.** It formats and parses in the browser's locale, so on a
|
||
comma-decimal machine `0.17` displays as "0,17" and `.value` comes back empty - the setting silently
|
||
becomes NaN and the export writes a tile set of nothing. Text plus `inputmode="decimal"`, parsed here.
|
||
- **The panel's job is the number not yet typed.** Every field re-plans on the keystroke and the readout says
|
||
what it bought, including what the flat reading costs at the window's edges. This is the same service
|
||
`terrain plan` does for a legend: the expensive step must never be how you find out a number was wrong.
|
||
- **This does not make ground finer.** The mesh resolves a couple of hundred metres and no sampling invents
|
||
what is not there. The export fixes the *shape* the ground arrives in, not its detail; the detail is the
|
||
Salty terrain generator's `terrain tiles`.
|
||
- **An existing `Region.json` is never replaced**, only written beside as `Region.generated.json`. A
|
||
hand-written manifest is mostly the reasoning behind its numbers, and the same rule already governs
|
||
`terrain studio`, which saves by patching a legend's *text* so its commentary survives. Tiles are data and
|
||
are overwritten; a manifest is an argument and is not.
|