Two signed brushes, four ADRs, and the docs catching up - #15
Merged
Lincoln504 merged 10 commits intoJul 29, 2026
Conversation
Size is the control you reach for between strokes rather than before a run of them, which is why every painting tool binds it to these two keys and why a slider alone is not enough. Geometric, not linear. The radius spans fifty to one, so a fixed step is either useless at the small end or glacial at the large; a quarter each press is eighteen presses end to end and means the same proportion wherever you are, which is what "a bit bigger" means when the thing is a circle. The keys were already taken — they swing the sun — and they still do when the brush is stowed. The guarded arm sits above the sky arm, so arming the tool takes the keys under your left hand and stowing it gives them back. That is what arming a tool means; the panel says which state you are in; and the sun now has two sliders of its own. Verified in headless Chromium: armed, 0.080 → ]x3 → 0.156 (x1.950 against a wanted 1.953, the rest being the readout's three decimals) → [x3 → 0.080 exactly. Clamped at 0.01 and 0.50. Stowed, the brush does not move.
`scripts/build-web.sh` writes it from lattice.wgsl + terrain.wgsl at build time so diag.html bisects the shader the app actually ships, and .gitignore has carried both the rule and the reason since it was added: "A committed copy drifts." The untrack never happened, so the file stayed tracked and every build rewrote it. Three commits have quietly carried a regenerated copy along with their real changes, contributing 554 lines of generated churn to any review of this branch. Removed from the index only. The build regenerates it, and it is ignored now that git is no longer holding on to it.
docs/IMPLEMENTATION.md §5: "Every architectural decision that changes an FSD section gets a short ADR in docs/adr/; the FSD is updated in the same PR." Three commits on this branch changed FSD.md — FR-TER-3 amended, FR-TER-3a added wholesale — and the directory did not exist yet, so this creates it and writes the two that are owed. 0001 — Erosion is a filter re-applied from the document, not a bake. Why "apply" became "derive": a baked amount can only go up, undo had to carry a 512² PNG per click, and a baked map cannot be re-derived at a new resolution. Records the costs too — the chain re-runs on every upstream edit, and the filter is an appearance of erosion rather than a simulation of one, which is why both mechanisms are offered rather than one replacing the other. 0002 — A control earns its place by measurement, or it becomes a constant. How thirteen parameters became seven, with the numbers: octave count changed nothing past Nyquist, which at the default gully scale was five eighths of its travel. The method is the decision, and it has since been applied twice more. Each carries two dates, the decision's and the record's, since both were written after the fact. FSD.md FR-TER-3a now links to them.
FR-TER-4 listed five brush verbs. Two of them are the same verb with a sign, one needs a per-stroke target height that breaks the property letting any number of strokes cost two passes, and one is a rule rather than a gesture. So the brush is two channels: height +raise −carve relief +soften −sharpen Relief is one kernel with a sign, not two filters. Positive walks each texel toward its local mean; negative walks it away, which is the same kernel run backwards — an unsharp mask, and the exact inverse rather than a second operation chosen to look opposite. It cost one comparison and no new pass. A brush is a direct instrument: you push the ground, the ground moves, and the result is under the cursor while you draw. A target that changed which *rule* governs a region would be indirect — the result depends on parameters in another panel — so it belongs to that operator, as a mask edited where the operator is edited. The tell is the panel: a direct target needs one word, an indirect one needs a sentence. Consequences, all simplifications: the erosion filter no longer knows paint exists, so `apply` and `ridge_map` lost a parameter and the filter's GPU parameter block is a fixed-size header again rather than one with a runtime-sized mask array in its tail. Three mask channels and three megabytes of upload go with it. Also here: - A preset clears the painting. Strokes are placed against the ground that was under them, and a preset replaces exactly that ground; kept, they land on features that no longer exist, which reads as the preset being broken. One undo brings them back. - The Muse bar is over the viewport again, centred, near the bottom. Given a column of its own it became a panel — a place you go rather than a thing you summon — and it held a fifth of the width permanently for a control used in bursts. Two columns now. - The Objects section and its drawer are gone; selection verbs live on the selection, in the transform HUD. ADR 0003 records the decision; FSD FR-TER-4 and UX §5.5 amended in the same commit, per the working agreement. Parity caught a real bug on the way: removing the `paint_zones` flag without its three padding words left `zones` at offset 76 on the Rust side and 64 in the WGSL, which showed up as a 0.118 height difference.
`protect_flats` sets the slope below which nothing is carved, and it was applied as `max(slope - guard, 0)`. That is continuous in value and not in slope: the instant the guard is crossed the ramp goes from flat to rising, and the eye reads a discontinuous derivative as an edge. Worse, of all the places to put one. The guard is a *slope* threshold, so its edge follows a slope isoline all the way around every protected region — the contour-following crease this filter is otherwise built to avoid, and the same artefact the band crossfade and the painted override each have their own argument for not producing. `soft_floor` is the standard quadratic smooth-max: exact outside ±k, continuous in slope everywhere, one clamp and three multiplies. The house already had the idiom — `round_toe` is the same kind of C1 knee for the ridge and crease rounding — it had just never been applied to the guard. The corner's half-width is a *fraction of the guard* rather than an absolute, which buys two things. A guard of 1.5 gets a transition proportional to itself instead of one tuned for some other value. And a guard of zero — the default, and what every document written before the control existed carries — takes the early return and is bit-identical to the `max` it replaces. No preset, no golden and no parity case moves, which the tests state directly: bit-equality at zero, and the same ramp restored exactly a half-width past the corner. Measured as the largest second difference across the threshold, which is what curvature is: the clip spikes to the full sample spacing, the rounded form stays two orders of magnitude below it.
Three altitude regions is the right shape. What they are regions *of* decides their names, their defaults and where their boundaries go — and the climate answer is wrong in a way that only shows on three of the four palettes. Follow a grain of rock down a hillside: it is stripped from the crest, carried down the slope, and dropped on the floor. Source, transport, sink. That is why the three want different settings, why the deposited sediment is a different tone in each, and why the middle one keeps the filter's defaults — transport is the branching-gully case the technique was built for. So the regions are `floor` / `slope` / `crest`, not `lowland` / `montane` / `alpine`. Trees and snow are climate; the palettes are not all temperate, and a name that is wrong for three of four presets is worse than a duller one that is right for all of them. The old words still parse. And the boundaries now come off the palette. `material_bands` reads the two lines from the material's own layer windows — where its ground cover stops and where its cap layer starts — so a preset that picks a palette picks the lines with it. They were fixed at 0.55 and 0.74, verdant's, for every scene: desert sand stops at 0.52 and its caprock starts at 0.45, so on a desert preset the middle region straddled two colours and the top began a third of the way above where the rock actually changed. Three palettes banded against a fourth palette's geography. The stored field names stay `treeline` and `snowline` — they name the same two numbers and every saved document carries them. The language spells them `lower` and `upper`; the panel labels them by the regions they divide. ADR 0004, with FSD FR-TER-3a amended in the same commit.
valley · hillside · summit, replacing floor · slope · crest. `slope` was the real problem. The filter already uses that word for steepness — `slope_onset`, `protect_flats`, and the material's own windows are all slope in the steepness sense — so a region called `slope` meant one word had two meanings one panel apart, which is the exact collision the terminology pass is meant to remove. `floor` and `crest` were merely weak. A tab label has to be picturable by someone who has not read the reasoning behind it, and "crest" is a line rather than a region. The sediment-budget argument is unchanged and still the reason the three exist; the names now say *where*, and the doc comment says why. Source / transport / sink was considered and rejected on the same ground: exactly what they are, and nobody can picture them. Every earlier spelling still parses — floor, slope, crest, and the biome words before them — so no saved script or transcript breaks.
The three regions are divided at two altitudes, and the point of dividing them there is that the region you are tuning is the region you can *see* — so the lines are read off the material palette's own layer windows. Presets already did that. Picking a palette from the Material panel did not, so switching a verdant scene to desert left the regions banded at 0.55 and 0.74 against sand that stops at 0.52 and caprock that starts at 0.45. Now the lines follow the palette — **unless they have been moved by hand**, in which case they were moved on purpose and following would throw that away. "Moved by hand" is decided by comparison, not by a flag: if the lines are where the *old* palette put them, nothing has touched them. A flag would be a second piece of state saying what the numbers already say, and every document written before it existed would have to guess its value. It lives in `Document::dispatch`, not in the shell or the compiler, so it holds for every path into the document — a panel click, a DSL line, a preset, a model tier — and cannot be forgotten by one of them. It is part of the same command, so one undo takes the palette and the lines back together.
§4 still said "three columns", called the assistant a docked panel rather than a bar over the picture, and drew an OBJECTS section in the settings column. None of those has been true since the assistant went back over the viewport and the Objects section came out. UX.md is a shipped spec, not a proposal, and the working agreement treats it as binding — so a reviewer opening the UI change would have found the spec and the code disagreeing about the single most visible fact of the application. Fixed the prose, the section list and the diagram.
Found by sweeping every user-visible string against the code rather than against memory. - `HELP` — what the console prints when you type `help` — offered `paint ground|smooth|lowland|montane|alpine` and `filter zone=lowland|…`. Instructions for two brushes that no longer exist and three region names that changed. The worst of the four: it is the one place a user goes when they do not already know the answer. - `README.md` advertised "painting, on five channels: raise, soften, and one mask per erosion band". - `docs/CONSOLE.md` documented the old region names in its grammar block, its worked examples, and a paragraph explaining the bands by treeline and snowline. - The same file's line reference still spelled the two boundaries `treeline=` and `snowline=` rather than `lower=`/`upper=`, and did not mention that they now follow the material palette. All four now say what the code does. The older spellings still parse, and CONSOLE.md says so, so nothing anyone has saved stops working.
Collaborator
Author
|
On #3 — "Control over geometry". This PR deletes The capability did not go anywhere:
So this is the drawer being folded into the three-column shell, not object control being removed. What #3 asks for that genuinely does not exist anywhere yet is texture maps — there is no image-texture path in the material system at all; materials are procedural palettes over a heightmap. That is a real gap and a real piece of work, and nothing in this stack addresses it. Happy to take it as the next branch if you want it prioritised. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
37 files changed, 1353 insertions(+), 1426 deletions(-)· gate green atf3c4ebe(fmt, clippy, workspace tests against a real adapter, wasm build, headless-Chromium smoke) · payload 489399 gz (-1423 from the previous PR, against the 6 MB NFR-2 budget)Part of a 12-PR stack over
pr/00-ci. Based onpr/08-paint-and-the-printer; retargets tomainonce that lands.