Colour coverage per cutter, shaded by how often it passed - #1
Open
gfonsecabr wants to merge 1 commit into
Open
Conversation
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>
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.
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_plotandcreate_solution_animationare affected; theanimation deepens a cell each time a cutter comes back over it.
How
Counting placements needs multiplicities, which a boolean mask cannot hold, so
grid.pygains the counting twin of what it already had:CellCounts— a multiplicity per cell, laid out exactly likeCellSet,with
nonzero()reading it back as one.dilate_counts—dilatewith multiplicities. It reuses_horizontal_runsand the prefix-sum trick from_dilate_horizontally: wheredilation 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_countsbeside_blit, with the clipping factored into a shared_overlap.SolutionValidator._anchorsbecomes public asreachable_anchors. Both theplot 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_outlineand
_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:
positions()ends back on its start, so eachedge contributes its start but not its end. The point two edges share counts
once and a lap sums to the tour's length.
_walkpads a short tour by repeating its final positionso 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:
nonzero()still equals the verifier's per-tour swept set, so nothing is lostor invented by splitting the coverage;
Tests
314 pass, up from 283. The new ones cover
CellCounts/dilate_countsagainstnaive 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_countsto0/1, and
_blit_countsoverwriting instead of adding — and each is caught. Twoof 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_startunpacked the animation's artists as
(_, _, patch), which assumed a singlecoverage 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-filesandruff check/ruff format --checkare clean.Note
The tuning constants —
_SWEPT_LIGHTEN,_SWEPT_DARKEN,_SWEPT_ALPHA— sittogether at the top of
visualize.py. The alpha trades off against overlaplegibility: 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.