Skip to content

Colour coverage per cutter, shaded by how often it passed - #1

Open
gfonsecabr wants to merge 1 commit into
CG-SHOP:mainfrom
gfonsecabr:coverage-shaded-per-cutter
Open

Colour coverage per cutter, shaded by how often it passed#1
gfonsecabr wants to merge 1 commit into
CG-SHOP:mainfrom
gfonsecabr:coverage-shaded-per-cutter

Conversation

@gfonsecabr

Copy link
Copy Markdown

What

The swept area was drawn as one flat green wash. That answers neither which
cutter covered this?
nor how much effort went here? — a cutter that mows the
same corridor nine times looked exactly like one that passed once.

Each cutter now fills the area it sweeps in its own colour — the same one its
tour is drawn in — graded by how often it passed over each cell: washed out
towards white at one pass, deepened past its colour towards black at the
busiest. All cutters share one scale, so a given shade means the same number of
passes whichever cutter drew it, and where two of them sweep the same cell their
fills blend.

Both create_solution_plot and create_solution_animation are affected; the
animation deepens a cell each time a cutter comes back over it.

How

Counting placements needs multiplicities, which a boolean mask cannot hold, so
grid.py gains the counting twin of what it already had:

  • CellCounts — a multiplicity per cell, laid out exactly like CellSet,
    with nonzero() reading it back as one.
  • dilate_countsdilate with multiplicities. It reuses
    _horizontal_runs and the prefix-sum trick from _dilate_horizontally: where
    dilation asks whether any shifted copy covers a cell, this sums how many
    do. Runs partition the structuring element, so one window sum per run counts
    every placement exactly once, and the cost still does not depend on run
    length.
  • _blit_counts beside _blit, with the clipping factored into a shared
    _overlap.

SolutionValidator._anchors becomes public as reachable_anchors. Both the
plot and the animation must drop exactly the placements the verifier drops;
duplicating that clipping would let the picture drift from the verdict printed
above it.

I also pulled the region outline and the tour drawing into _draw_region_outline
and _draw_tours, which the two public functions had drifting copies of.

Two details the counts have to get right

Both are silent if wrong, so both are asserted directly:

  1. The closing step. A tour's positions() ends back on its start, so each
    edge contributes its start but not its end. The point two edges share counts
    once and a lap sums to the tour's length.
  2. Parked cutters. _walk pads a short tour by repeating its final position
    so every cutter animates to the same length. Boolean coverage did not care;
    counts would have piled hundreds of phantom passes onto a parked cutter's
    spot. Each tour now counts only its own steps.

Verification

Cross-checked against a brute-force count — walk every step, stamp every cutter
cell — over self-crossing tours, stationary cutters, off-centre cutters, regions
with holes, thin back-and-forth tours, and excursions outside the region's
reach. Three properties hold in every case:

  • the counts match brute force exactly;
  • nonzero() still equals the verifier's per-tour swept set, so nothing is lost
    or invented by splitting the coverage;
  • the animation's last frame equals the plot's counts cell for cell.

Tests

314 pass, up from 283. The new ones cover CellCounts/dilate_counts against
naive stamping, the counting semantics above, the shared scale, the shape of the
ramp, and animation-versus-plot agreement.

I mutation-tested them rather than trusting green: eight deliberate breakages —
inclusive instead of half-open edges, counting the parked padding, dropping the
anchor clipping, a per-cutter instead of shared scale, a ramp stopping at the
plain colour, removing the scale's floor of two, clamping dilate_counts to
0/1, and _blit_counts overwriting instead of adding — and each is caught. Two
of my tests initially survived their mutation (the scale test used cutters with
equal maxima; the clipping test used a single-cell cutter, whose out-of-reach
placements never reach back into the box) and were strengthened until they
failed.

One existing test needed adjusting: test_a_cutter_with_a_short_tour_waits_at_its_start
unpacked the animation's artists as (_, _, patch), which assumed a single
coverage raster; there is now one per cutter. Three others that asked about the
whole swept area were leaning on there happening to be a single tour, and now
say what they mean.

pre-commit run --all-files and ruff check / ruff format --check are clean.

Note

The tuning constants — _SWEPT_LIGHTEN, _SWEPT_DARKEN, _SWEPT_ALPHA — sit
together at the top of visualize.py. The alpha trades off against overlap
legibility: more opaque holds a wider shading span, more transparent lets a
cutter underneath show through. Current values keep about 0.07 luminance per
pass while leaving overlap visible. Happy to retune if you'd rather weight that
differently.

The swept area was one flat green wash, which answers neither "who covered
this?" nor "how much effort went here?". Each cutter now fills the area it
sweeps in its own colour -- the same one its tour is drawn in -- graded by
how often it passed over each cell: washed out towards white at one pass,
deepened past its colour towards black at the busiest. All cutters share one
scale, so a shade means the same number of passes whoever drew it.

Counting placements needs multiplicities, which a boolean mask cannot hold,
so `grid.py` gains the counting twin of what it already had:

- `CellCounts`, laid out exactly like `CellSet`
- `dilate_counts`, mirroring `dilate`; it reuses `_horizontal_runs` and the
  prefix-sum trick of `_dilate_horizontally`, summing a window per run
  instead of OR-ing it, so the cost still does not depend on run length
- `_blit_counts` beside `_blit`, sharing the clipping via `_overlap`

Two details the counts have to get right, both silent if wrong:

- a tour closes onto its own start, so each edge contributes its start but
  not its end; a lap then sums to the tour's length
- `_walk` pads a short tour by repeating its last position so every cutter
  animates to the same length. Boolean coverage did not care; counts would
  have piled hundreds of phantom passes onto a parked cutter's spot, so each
  tour counts only its own steps.

`SolutionValidator._anchors` becomes public as `reachable_anchors`: both the
plot and the animation have to drop exactly the placements the verifier
drops, and duplicating that clipping would let the picture drift from the
verdict it is captioned with.

The animation shades the same way and its last frame leaves exactly the
counts the plot draws, which is asserted directly.

Verified against a brute-force count -- walk every step, stamp every cutter
cell -- over self-crossing tours, stationary cutters, off-centre cutters,
regions with holes, and excursions outside the region's reach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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