Skip to content

Two signed brushes, four ADRs, and the docs catching up - #15

Merged
Lincoln504 merged 10 commits into
pr/08-paint-and-the-printerfrom
pr/09-brushes-adrs-docs
Jul 29, 2026
Merged

Two signed brushes, four ADRs, and the docs catching up#15
Lincoln504 merged 10 commits into
pr/08-paint-and-the-printerfrom
pr/09-brushes-adrs-docs

Conversation

@Lincoln504

Copy link
Copy Markdown
Collaborator
  • [ and ] size the brush — the one setting you change mid-run
  • Untrack app/public/terrain.wgsl — the .gitignore already said to
  • The two ADRs the working agreement already required
  • Two brushes, both signed: height and relief
  • Round the flat guard's corner — a clip is an edge you can see
  • Erosion regions are a sediment budget, not a climate
  • Name the erosion regions after places you can picture
  • The erosion regions follow the palette they are drawn against
  • UX.md described a workspace the code no longer builds
  • Four surfaces still describing a brush and three bands that are gone

37 files changed, 1353 insertions(+), 1426 deletions(-) · gate green at f3c4ebe (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 on pr/08-paint-and-the-printer; retargets to main once that lands.

⚠️ Please merge with a merge commit — squash or rebase rewrites the SHAs and breaks every PR above this one in the stack.

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.
@Lincoln504

Copy link
Copy Markdown
Collaborator Author

On #3 — "Control over geometry". This PR deletes ObjectsDrawer.svelte, and I want to flag that against your issue rather than have it turn up in a diff, because the timing looks worse than the change is.

The capability did not go anywhere:

  • objects are still added, from the settings panel (+ sphere / cube / torus / …)
  • Gizmo and TransformHud are both still mounted in App.svelte
  • TransformHud gives numeric position / rotation / scale entry — which is FSD FR-SCN-2 ("gizmos and numeric entry")

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.

@Lincoln504
Lincoln504 merged commit 17591c0 into pr/08-paint-and-the-printer Jul 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant