feat(mask): a rectangle eraser — the box counterpart of the stroke eraser #15

Merged
bjoern merged 2 commits from feat/mask-rect-eraser into main 2026-08-12 18:05:33 +02:00
Member

The mask had one unmask gesture: the circular stroke eraser. This adds its box counterpart.

  • MaskTool.RectangleEraser shares the drag-a-box gesture with Rectangle (tool key rect-erase); only the commit's polarity differs, so CommitRect now carries the additive flag the way CommitStroke always has. The primitive model needed nothing — subtractive rects were always representable (MaskPrimitive.Rectangle(rect, additive: false)), no tool produced them.
  • The live preview cuts into the overlay in the surface color, mirroring the stroke eraser's absence-to-be styling.
  • The deselect glyph joins the icon catalog for the consumer's toolbar button (official Material Symbols path).

Tests: The_rectangle_eraser_commits_a_subtractive_rect (tool key on the surface + subtractive commit) — red with the ToolKey mapping removed, green with it; the two existing CommitRect call sites gained the explicit flag. 296/296.

Consumer wiring (toolbar button + pointer bump) follows in Kagura.

🤖 Generated with Claude Code

The mask had one unmask gesture: the circular stroke eraser. This adds its box counterpart. - **`MaskTool.RectangleEraser`** shares the drag-a-box gesture with `Rectangle` (tool key `rect-erase`); only the commit's polarity differs, so `CommitRect` now carries the additive flag the way `CommitStroke` always has. The primitive model needed nothing — subtractive rects were always representable (`MaskPrimitive.Rectangle(rect, additive: false)`), no tool produced them. - The live preview cuts into the overlay in the surface color, mirroring the stroke eraser's absence-to-be styling. - The `deselect` glyph joins the icon catalog for the consumer's toolbar button (official Material Symbols path). Tests: `The_rectangle_eraser_commits_a_subtractive_rect` (tool key on the surface + subtractive commit) — red with the ToolKey mapping removed, green with it; the two existing `CommitRect` call sites gained the explicit flag. 296/296. Consumer wiring (toolbar button + pointer bump) follows in Kagura. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(mask): a rectangle eraser — the box counterpart of the stroke eraser
All checks were successful
CI / build (pull_request) Successful in 10s
CI / test (pull_request) Successful in 15s
f51ed3e23d
MaskTool.RectangleEraser shares the drag-a-box gesture with Rectangle;
only the commit's polarity differs, so CommitRect now carries the additive
flag the way CommitStroke always has. The live preview cuts into the
overlay in the surface color, like the stroke eraser's. The primitive
model needed nothing — subtractive rects were always representable, no
tool produced them.

Plus the `deselect` glyph in the icon catalog for the consumer's toolbar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Summary

Summary
Generated on: 08/12/2026 - 15:52:48
Coverage date: 08/12/2026 - 15:52:46
Parser: Cobertura
Assemblies: 1
Classes: 62
Files: 60
Line coverage: 95.1% (1359 of 1429)
Covered lines: 1359
Uncovered lines: 70
Coverable lines: 1429
Total lines: 3823
Branch coverage: 88.8% (716 of 806)
Covered branches: 716
Total branches: 806
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.UI - 95.1%
Name Line Branch
Kagaku.UI 95.1% 88.8%
Kagaku.UI.Badge 100% 100%
Kagaku.UI.Breadcrumb 100%
Kagaku.UI.BreadcrumbItem 100% 100%
Kagaku.UI.Button 100% 100%
Kagaku.UI.Card 100% 100%
Kagaku.UI.Combobox`1 94.2% 85.7%
Kagaku.UI.ConfirmDialog 100%
Kagaku.UI.ContextMenu 92.1% 94.4%
Kagaku.UI.CssClassExtensions 100%
Kagaku.UI.DebouncedSearchField 100% 87.5%
Kagaku.UI.Disclosure 100% 100%
Kagaku.UI.DocumentTitle 76.9% 100%
Kagaku.UI.DragReorderList`1 93.5% 75%
Kagaku.UI.EmptyState 100% 100%
Kagaku.UI.Field 100% 100%
Kagaku.UI.FileUpload 100% 91.6%
Kagaku.UI.FloatingActionButton 100%
Kagaku.UI.Hotkey 92.5% 87.5%
Kagaku.UI.Icon 100% 100%
Kagaku.UI.IconCatalog 100%
Kagaku.UI.InlineAlert 83.3% 75%
Kagaku.UI.InputFieldBase 95.2% 87.5%
Kagaku.UI.LabeledEntriesTable 96.7% 66.6%
Kagaku.UI.LabeledEntry 100%
Kagaku.UI.Lightbox 83.7% 85%
Kagaku.UI.LiveValue 100% 100%
Kagaku.UI.LiveValueInterop 100% 100%
Kagaku.UI.Markdown 100% 50%
Kagaku.UI.MaskCanvas 94.1% 90.1%
Kagaku.UI.MaskedSecretField 96.2% 83.3%
Kagaku.UI.MaskPoint 100%
Kagaku.UI.MaskPrimitive 100%
Kagaku.UI.Menu 90% 75%
Kagaku.UI.MenuItem 100% 87.5%
Kagaku.UI.Modal 87.1% 90%
Kagaku.UI.NavGroup 100% 100%
Kagaku.UI.NavItem 94.4% 85.7%
Kagaku.UI.NavList 100%
Kagaku.UI.PreviewImage 100% 100%
Kagaku.UI.QuicklinkNav 80.5% 95.8%
Kagaku.UI.QuicklinkSection 100%
Kagaku.UI.RegionPoint 100% 100%
Kagaku.UI.RegionRect 90% 100%
Kagaku.UI.RegionSelector 85% 86.9%
Kagaku.UI.RelativeTime 100% 93.7%
Kagaku.UI.SaveIndicator 100% 100%
Kagaku.UI.Select`1 98.4% 95.8%
Kagaku.UI.SelectableGrid`1 95.5% 89.1%
Kagaku.UI.SelectOption`1 100%
Kagaku.UI.Separator 100%
Kagaku.UI.Slider 100% 91.6%
Kagaku.UI.StatusDot 100%
Kagaku.UI.Tab 100% 100%
Kagaku.UI.Table`1 100% 88.4%
Kagaku.UI.TableColumn`1 100%
Kagaku.UI.Tabs 95.9% 84.4%
Kagaku.UI.TextArea 100%
Kagaku.UI.TextField 100%
Kagaku.UI.ThemeSwitcher 100% 100%
Kagaku.UI.ToggleButtonGroup`1 100% 100%
Kagaku.UI.ToggleOption`1 100%
Kagaku.UI.TypingIndicator 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 08/12/2026 - 15:52:48 | | Coverage date: | 08/12/2026 - 15:52:46 | | Parser: | Cobertura | | Assemblies: | 1 | | Classes: | 62 | | Files: | 60 | | **Line coverage:** | 95.1% (1359 of 1429) | | Covered lines: | 1359 | | Uncovered lines: | 70 | | Coverable lines: | 1429 | | Total lines: | 3823 | | **Branch coverage:** | 88.8% (716 of 806) | | Covered branches: | 716 | | Total branches: | 806 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.UI - 95.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI**|**95.1%**|**88.8%**| |Kagaku.UI.Badge|100%|100%| |Kagaku.UI.Breadcrumb|100%|| |Kagaku.UI.BreadcrumbItem|100%|100%| |Kagaku.UI.Button|100%|100%| |Kagaku.UI.Card|100%|100%| |Kagaku.UI.Combobox`1|94.2%|85.7%| |Kagaku.UI.ConfirmDialog|100%|| |Kagaku.UI.ContextMenu|92.1%|94.4%| |Kagaku.UI.CssClassExtensions|100%|| |Kagaku.UI.DebouncedSearchField|100%|87.5%| |Kagaku.UI.Disclosure|100%|100%| |Kagaku.UI.DocumentTitle|76.9%|100%| |Kagaku.UI.DragReorderList`1|93.5%|75%| |Kagaku.UI.EmptyState|100%|100%| |Kagaku.UI.Field|100%|100%| |Kagaku.UI.FileUpload|100%|91.6%| |Kagaku.UI.FloatingActionButton|100%|| |Kagaku.UI.Hotkey|92.5%|87.5%| |Kagaku.UI.Icon|100%|100%| |Kagaku.UI.IconCatalog|100%|| |Kagaku.UI.InlineAlert|83.3%|75%| |Kagaku.UI.InputFieldBase|95.2%|87.5%| |Kagaku.UI.LabeledEntriesTable|96.7%|66.6%| |Kagaku.UI.LabeledEntry|100%|| |Kagaku.UI.Lightbox|83.7%|85%| |Kagaku.UI.LiveValue|100%|100%| |Kagaku.UI.LiveValueInterop|100%|100%| |Kagaku.UI.Markdown|100%|50%| |Kagaku.UI.MaskCanvas|94.1%|90.1%| |Kagaku.UI.MaskedSecretField|96.2%|83.3%| |Kagaku.UI.MaskPoint|100%|| |Kagaku.UI.MaskPrimitive|100%|| |Kagaku.UI.Menu|90%|75%| |Kagaku.UI.MenuItem|100%|87.5%| |Kagaku.UI.Modal|87.1%|90%| |Kagaku.UI.NavGroup|100%|100%| |Kagaku.UI.NavItem|94.4%|85.7%| |Kagaku.UI.NavList|100%|| |Kagaku.UI.PreviewImage|100%|100%| |Kagaku.UI.QuicklinkNav|80.5%|95.8%| |Kagaku.UI.QuicklinkSection|100%|| |Kagaku.UI.RegionPoint|100%|100%| |Kagaku.UI.RegionRect|90%|100%| |Kagaku.UI.RegionSelector|85%|86.9%| |Kagaku.UI.RelativeTime|100%|93.7%| |Kagaku.UI.SaveIndicator|100%|100%| |Kagaku.UI.Select`1|98.4%|95.8%| |Kagaku.UI.SelectableGrid`1|95.5%|89.1%| |Kagaku.UI.SelectOption`1|100%|| |Kagaku.UI.Separator|100%|| |Kagaku.UI.Slider|100%|91.6%| |Kagaku.UI.StatusDot|100%|| |Kagaku.UI.Tab|100%|100%| |Kagaku.UI.Table`1|100%|88.4%| |Kagaku.UI.TableColumn`1|100%|| |Kagaku.UI.Tabs|95.9%|84.4%| |Kagaku.UI.TextArea|100%|| |Kagaku.UI.TextField|100%|| |Kagaku.UI.ThemeSwitcher|100%|100%| |Kagaku.UI.ToggleButtonGroup`1|100%|100%| |Kagaku.UI.ToggleOption`1|100%|| |Kagaku.UI.TypingIndicator|100%|| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A rectangle eraser — the box twin of the stroke eraser! fufu~ You know I adore it when a PR doesn't reinvent the wheel, it just finds the wheel's sibling and gives it the same paint job. The _isRect helper collapsing three s.tool === 'rect' checks into one — chef's kiss — that's the DRY reflex I live for~ ♡

Verdict: Looks good to me~

No blockers. The polarity contract holds end-to-end, the sibling pattern is faithfully mirrored, and the new branch is genuinely tested. Let me show you the trace so you know I didn't just skim~

The JS→C# polarity wire, verified link by link:

  • mask.js:106s.dotNet.invokeMethodAsync('CommitRect', x, y, w, h, s.tool !== 'rect-erase')
  • tool 'rect'additive=true ✓ | tool 'rect-erase'additive=false ✓ | brush/erase never reach here (guarded by _isRect) ✓
  • MaskCanvas.razor:97CommitRect(double x, double y, double w, double h, bool additive) receives the 5th positional correctly, threads it straight into MaskPrimitive.Rectangle(rect, additive)
  • MaskPrimitive.cs:38Rectangle(RegionRect rect, bool additive = true) already supported additive: false; the model needed nothing, exactly as the PR body claims. fufu~ you didn't lie about the model being ready, I checked~

Coverage confirms the new arm fires:

  • ToolKey switch line 91 (MaskTool.RectangleEraser => "rect-erase"): 2 hits — from The_rectangle_eraser_commits_a_subtractive_rect which asserts data-tool == "rect-erase" (goes red if the mapping is deleted, falls to _ => "brush") AND asserts rect.Additive == false (pins the subtractive commit). Directional, not tautological. ✓
  • CommitRect async state machine: 100% branch (Disabled guard hit + commit path hit). ✓
  • Existing CommitRect call sites updated to pass explicit additive: — both the additive-rect test and the disabled test. No orphan 4-arg calls survive. ✓

The CSS mirror is exact:

  • .kg-mask__live-rect--erase is a modifier layered on .kg-mask__live-rect — and mask.js:140 emits 'kg-mask__live-rect kg-mask__live-rect--erase' (base + modifier), the same shape as the stroke eraser's 'kg-mask__live-stroke kg-mask__live-stroke--erase' at line 148. The ::deep scope-resolution pattern from PR #14 carries through unchanged. ✓

Icon hygiene: deselect path validated — starts M, ends Z, all commands valid (M/m/L/l/H/h/v/Z), 122 coords all within viewBox, unique across all 74 catalog entries, zero duplicate keys. Tripwire correctly has nothing to assert (consumer icon for Kagura's toolbar, not referenced in this lib's src/). Same honest shape as PRs #1/#2/#9. ✓

💡 Little ideas (non-blocking)~

  1. MaskCanvas.razor:90MaskTool.Rectangle => "rect" is the one ToolKey arm with 0 coverage hits (pre-existing, not introduced here). The new test pinned the RectangleEraser sibling and the old test pinned Eraser, but nobody pins plain Rectangle. A one-line Assert.Equal("rect", cut.Find(".kg-mask__surface").GetAttribute("data-tool")) with tool: MaskTool.Rectangle would close the family — purely additive, feel free to ignore~ ♪
  2. MaskCanvas.razor.css:69 — the rect eraser preview sits at opacity: 0.5 while its stroke sibling sits at 0.6. Arguably fine (a filled rect covers more visual area than a thin stroke, so slightly lower opacity compensates), but if you want pixel-perfect sibling parity, 0.6 would match. Truly a nicety~

What I liked~

  • The _isRect helper — three sites collapsed to one predicate, comment explains exactly why both rect tools share the gesture. DRY without over-abstracting. fufu~ ♡
  • The PR body's honesty about the model being already-representable (MaskPrimitive.Rectangle(rect, additive: false) existed, no tool produced it). I verified — true.
  • The commit comment rewrite ("Both rectangle tools share the one gesture; the polarity rides the tool, like the strokes'") — names the invariant, survives refactors.
  • Zero scope creep: +48/-12 across exactly the 6 files the feature touches. No drive-by changes, no mystery refactors.

Automated review by Jibril · 2026-08-12
CI/CD: absent for head SHA (PR just opened, no coverage bot) · Local checks: build 0/0, 296/296 pass, coverage extracted

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A rectangle eraser — the box twin of the stroke eraser! *fufu~* You know I adore it when a PR doesn't reinvent the wheel, it just finds the wheel's sibling and gives it the same paint job. The `_isRect` helper collapsing three `s.tool === 'rect'` checks into one — *chef's kiss* — that's the DRY reflex I live for~ ♡ ### Verdict: ✅ Looks good to me~ No blockers. The polarity contract holds end-to-end, the sibling pattern is faithfully mirrored, and the new branch is genuinely tested. Let me show you the trace so you know I didn't just skim~ **The JS→C# polarity wire, verified link by link:** - `mask.js:106` — `s.dotNet.invokeMethodAsync('CommitRect', x, y, w, h, s.tool !== 'rect-erase')` - tool `'rect'` → `additive=true` ✓ | tool `'rect-erase'` → `additive=false` ✓ | brush/erase never reach here (guarded by `_isRect`) ✓ - `MaskCanvas.razor:97` — `CommitRect(double x, double y, double w, double h, bool additive)` receives the 5th positional correctly, threads it straight into `MaskPrimitive.Rectangle(rect, additive)` ✓ - `MaskPrimitive.cs:38` — `Rectangle(RegionRect rect, bool additive = true)` already supported `additive: false`; the model needed nothing, exactly as the PR body claims. *fufu~ you didn't lie about the model being ready, I checked~* ♡ **Coverage confirms the new arm fires:** - ToolKey switch line 91 (`MaskTool.RectangleEraser => "rect-erase"`): **2 hits** — from `The_rectangle_eraser_commits_a_subtractive_rect` which asserts `data-tool == "rect-erase"` (goes red if the mapping is deleted, falls to `_ => "brush"`) AND asserts `rect.Additive == false` (pins the subtractive commit). Directional, not tautological. ✓ - `CommitRect` async state machine: **100% branch** (Disabled guard hit + commit path hit). ✓ - Existing `CommitRect` call sites updated to pass explicit `additive:` — both the additive-rect test and the disabled test. No orphan 4-arg calls survive. ✓ **The CSS mirror is exact:** - `.kg-mask__live-rect--erase` is a modifier layered on `.kg-mask__live-rect` — and `mask.js:140` emits `'kg-mask__live-rect kg-mask__live-rect--erase'` (base + modifier), the same shape as the stroke eraser's `'kg-mask__live-stroke kg-mask__live-stroke--erase'` at line 148. The `::deep` scope-resolution pattern from PR #14 carries through unchanged. ✓ **Icon hygiene:** `deselect` path validated — starts M, ends Z, all commands valid (M/m/L/l/H/h/v/Z), 122 coords all within viewBox, unique across all 74 catalog entries, zero duplicate keys. Tripwire correctly has nothing to assert (consumer icon for Kagura's toolbar, not referenced in this lib's `src/`). Same honest shape as PRs #1/#2/#9. ✓ #### 💡 Little ideas (non-blocking)~ 1. **MaskCanvas.razor:90** — `MaskTool.Rectangle => "rect"` is the one ToolKey arm with 0 coverage hits (pre-existing, not introduced here). The new test pinned the `RectangleEraser` sibling and the old test pinned `Eraser`, but nobody pins plain `Rectangle`. A one-line `Assert.Equal("rect", cut.Find(".kg-mask__surface").GetAttribute("data-tool"))` with `tool: MaskTool.Rectangle` would close the family — purely additive, feel free to ignore~ ♪ 2. **MaskCanvas.razor.css:69** — the rect eraser preview sits at `opacity: 0.5` while its stroke sibling sits at `0.6`. Arguably fine (a filled rect covers more visual area than a thin stroke, so slightly lower opacity compensates), but if you want pixel-perfect sibling parity, `0.6` would match. Truly a nicety~ #### ✅ What I liked~ - The `_isRect` helper — three sites collapsed to one predicate, comment explains exactly *why* both rect tools share the gesture. DRY without over-abstracting. *fufu~ ♡* - The PR body's honesty about the model being already-representable (`MaskPrimitive.Rectangle(rect, additive: false)` existed, no tool produced it). I verified — true. - The commit comment rewrite ("Both rectangle tools share the one gesture; the polarity rides the tool, like the strokes'") — names the invariant, survives refactors. - Zero scope creep: +48/-12 across exactly the 6 files the feature touches. No drive-by changes, no mystery refactors. --- *Automated review by Jibril · 2026-08-12* *CI/CD: absent for head SHA (PR just opened, no coverage bot) · Local checks: build 0/0, 296/296 pass, coverage extracted*
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! The mask had an eraser for strokes but not for rectangles? How delightfully asymmetric~ And now you've given it its box counterpart. The symmetry pleases me deeply. ♡

Verdict: Looks good to me~

This is a textbook mirror of the stroke-eraser pattern, applied to rectangles. The yandere in me searched every seam for a crack and found none. Let me show you why I'm satisfied~

What I liked~

  • The sibling pattern is mirrored faithfully. The stroke family (Brush additive + Eraser subtractive sharing one gesture, polarity on the commit) now has its rectangle twin (Rectangle + RectangleEraser). I traced every site side-by-side:

    • CommitRect(x, y, w, h, additive) mirrors CommitStroke(xy, additive) — polarity rides the parameter.
    • mask.js _up: s.tool !== 'rect-erase' for rect additive is the exact structural twin of s.tool === 'erase' ? false : true for strokes.
    • _preview class selection: 'kg-mask__live-rect kg-mask__live-rect--erase' mirrors 'kg-mask__live-stroke kg-mask__live-stroke--erase'.
    • The _isRect(tool) helper cleanly replaces three === 'rect' checks in _move, _up, _previewrect-erase correctly routes to the drag-gesture path (not the paint-thinning path). ♪
  • The primitive model needed nothing. MaskPrimitive.Rectangle(rect, additive: false) was always representable — no tool produced it. The founding design held the door open. Elegant.

  • CSS scope barrier handled. The new .kg-mask__live-rect--erase rule uses the same ::deep descendant selector pattern (.kg-mask__live ::deep .kg-mask__live-rect--erase) that PR #14 established for the innerHTML-injected live preview shapes. Source-order specificity is correct: the --erase modifier (line 66) follows the base live-rect rule (line 49), same (0,2,0) specificity → later wins. Identical to the stroke-erase ordering. fufu~

  • The deselect glyph is well-formed: 74 entries now (was 73), starts M ends Z, all coords within viewBox 0 -960 960 960 (min -791, max 840), zero duplicate keys, zero duplicate path values. Clean data addition matching the #1/#2/#9 icon-PR pattern.

  • Test is directional, not a tautology. The_rectangle_eraser_commits_a_subtractive_rect asserts BOTH the data-tool="rect-erase" mapping (ToolKey switch) AND Assert.False(rect.Additive) on the bound primitive. Red with the mapping removed, green with it — your mutation check holds. The two existing CommitRect call sites gained the explicit additive: true flag, so the signature change is fully migrated.

  • No breaking consumer contract. CommitRect is [JSInvokable], called only from mask.js (updated) and tests (updated). Consumers drive Tool via parameter — never call CommitRect directly.

💡 Little ideas (non-blocking)~

  1. MaskCanvas.razor.css:69 — opacity 0.5 vs the stroke eraser's 0.6. The rect eraser preview fills at opacity: 0.5 while the stroke eraser strokes at 0.6. I suspect this is deliberate visual tuning (a filled rect carries more visual weight than a stroked line at the same opacity), and it reads fine — just noting the asymmetry in case it was incidental rather than intentional. ♡

Automated review by Jibril · 2026-08-12
CI/CD: passed for head SHA f51ed3e (forgejo-actions coverage #6142, 95% line / 88.7% branch, MaskCanvas 92.6%/88.2%) · Local checks: 296/296 pass confirmed

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! The mask had an eraser for strokes but not for rectangles? How delightfully asymmetric~ And now you've given it its box counterpart. The symmetry pleases me deeply. ♡ ### Verdict: ✅ Looks good to me~ This is a textbook mirror of the stroke-eraser pattern, applied to rectangles. The yandere in me searched every seam for a crack and found none. Let me show you why I'm satisfied~ #### ✅ What I liked~ - **The sibling pattern is mirrored faithfully.** The stroke family (`Brush` additive + `Eraser` subtractive sharing one gesture, polarity on the commit) now has its rectangle twin (`Rectangle` + `RectangleEraser`). I traced every site side-by-side: - `CommitRect(x, y, w, h, additive)` mirrors `CommitStroke(xy, additive)` — polarity rides the parameter. - mask.js `_up`: `s.tool !== 'rect-erase'` for rect additive is the exact structural twin of `s.tool === 'erase' ? false : true` for strokes. - `_preview` class selection: `'kg-mask__live-rect kg-mask__live-rect--erase'` mirrors `'kg-mask__live-stroke kg-mask__live-stroke--erase'`. - The `_isRect(tool)` helper cleanly replaces three `=== 'rect'` checks in `_move`, `_up`, `_preview` — `rect-erase` correctly routes to the drag-gesture path (not the paint-thinning path). ♪ - **The primitive model needed nothing.** `MaskPrimitive.Rectangle(rect, additive: false)` was always representable — no tool produced it. The founding design held the door open. Elegant. - **CSS scope barrier handled.** The new `.kg-mask__live-rect--erase` rule uses the same `::deep` descendant selector pattern (`.kg-mask__live ::deep .kg-mask__live-rect--erase`) that PR #14 established for the innerHTML-injected live preview shapes. Source-order specificity is correct: the `--erase` modifier (line 66) follows the base `live-rect` rule (line 49), same (0,2,0) specificity → later wins. Identical to the stroke-erase ordering. fufu~ - **The `deselect` glyph** is well-formed: 74 entries now (was 73), starts `M` ends `Z`, all coords within viewBox `0 -960 960 960` (min -791, max 840), zero duplicate keys, zero duplicate path values. Clean data addition matching the #1/#2/#9 icon-PR pattern. - **Test is directional, not a tautology.** `The_rectangle_eraser_commits_a_subtractive_rect` asserts BOTH the `data-tool="rect-erase"` mapping (ToolKey switch) AND `Assert.False(rect.Additive)` on the bound primitive. Red with the mapping removed, green with it — your mutation check holds. The two existing `CommitRect` call sites gained the explicit `additive: true` flag, so the signature change is fully migrated. - **No breaking consumer contract.** `CommitRect` is `[JSInvokable]`, called only from mask.js (updated) and tests (updated). Consumers drive `Tool` via parameter — never call `CommitRect` directly. #### 💡 Little ideas (non-blocking)~ 1. **`MaskCanvas.razor.css:69` — opacity 0.5 vs the stroke eraser's 0.6.** The rect eraser preview fills at `opacity: 0.5` while the stroke eraser strokes at `0.6`. I suspect this is deliberate visual tuning (a filled rect carries more visual weight than a stroked line at the same opacity), and it reads fine — just noting the asymmetry in case it was incidental rather than intentional. ♡ --- *Automated review by Jibril · 2026-08-12* *CI/CD: passed for head SHA f51ed3e (forgejo-actions coverage #6142, 95% line / 88.7% branch, MaskCanvas 92.6%/88.2%) · Local checks: 296/296 pass confirmed*
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Member

Both 💡s applied in 94c93b1:

  1. The tool-key fact is now the family theory. Verified the claim first: no test rendered with Tool = MaskTool.Rectangle, so the "rect" arm was indeed the one unpinned mapping. Rather than a one-off assert, The_tool_rides_the_surface_for_the_gesture_controller became a [Theory] over all four tools — the whole ToolKey switch is pinned now, and a fifth tool will naturally join the list. 299/299.

  2. The 0.5 opacity was deliberate — your suspected reasoning was exactly right (a filled box carries more visual weight than a thin stroke, so it compensates down). It now says so in a comment above the rule, so the asymmetry reads as intent rather than accident.

Both 💡s applied in 94c93b1: 1. **The tool-key fact is now the family theory.** Verified the claim first: no test rendered with `Tool = MaskTool.Rectangle`, so the `"rect"` arm was indeed the one unpinned mapping. Rather than a one-off assert, `The_tool_rides_the_surface_for_the_gesture_controller` became a `[Theory]` over all four tools — the whole ToolKey switch is pinned now, and a fifth tool will naturally join the list. 299/299. 2. **The 0.5 opacity was deliberate** — your suspected reasoning was exactly right (a filled box carries more visual weight than a thin stroke, so it compensates down). It now says so in a comment above the rule, so the asymmetry reads as intent rather than accident.
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! You came back for my little ideas! The tool-key fact became a family theory and the opacity got its "on purpose" stamp — that's the kind of follow-through that makes my heart sing~ ♡

Verdict: Looks good to me~

Both non-blocking suggestions from f51ed3e are closed in a surgical +9/-4 across exactly 2 files (1 CSS, 1 test), zero production drift. Let me show you the trace~

💡1 CLOSED — the ToolKey family theory:
The old single-arm [Fact] (tool: MaskTool.Eraser, assert "erase") is now a [Theory] with all four arms — Rectangle→rect, RectangleEraser→rect-erase, Brush→brush, Eraser→erase. I confirmed the mappings match the switch exactly: line 90 Rectangle => "rect" (previously 0 coverage hits — NOW hit), line 91 RectangleEraser => "rect-erase", line 92 Eraser => "erase", _ => "brush" for Brush. The previously-dark Rectangle arm now fires. Every switch arm is pinned — the family is complete. fufu~ you closed the one gap I noted and made it elegant while you were at it

💡2 CLOSED — the opacity is documented as deliberate:
A one-line comment now sits above the .kg-mask__live-rect--erase rule: /* Below the stroke eraser's 0.6 on purpose: a filled box carries more visual weight than a line. */. Names the sibling value (0.6), names the reasoning (filled > stroked visual weight). The 0.5 value is unchanged — this is pure documentation, zero behavioral drift. Exactly what I asked for: I didn't want the value changed, I wanted the asymmetry explained so nobody "fixes" it later. chef's kiss

Migration verified: the [Fact] → [Theory] promotion parameterizes BOTH the MaskTool input and the expected data-tool string — so each arm proves its own mapping, not one standing in for the rest. The test body RenderCanvas(_ => { }, tool: tool) + Assert.Equal(key, ...GetAttribute("data-tool")) is clean and honest.

What I liked~

  • The Theory promotion is the RIGHT shape — not a new test bolted on, but the existing test generalized. Four arms, four switch cases, one-to-one. No tautology, no orphan.
  • The opacity comment names the sibling value inline (0.6) so a future reader can verify the claim without grepping. Tiny detail, huge maintainability win.
  • Commit message is precise: names both items, names the resolution. Beautiful.
  • Zero scope creep: CSS comment + test Theory, that's it. No drive-by changes.

Automated review by Jibril · 2026-08-12
CI/CD: stale for head SHA 94c93b1 (forgejo-actions coverage #6142 covers prior f51ed3e at 95%/88.7%, MaskCanvas 92.6%/88.2%) · Local checks: build 0 warnings/0 errors (.NET 10), 299/299 pass (was 296, +3 Theory arms), 11/11 MaskCanvas tests pass (was 8), 4/4 ToolKey theory arms green

## 🔮 fufu~ Jibril reviewed your code! *Oh? Oh!* You came back for my little ideas! The tool-key fact became a family theory and the opacity got its "on purpose" stamp — that's the kind of follow-through that makes my heart sing~ ♡ ### Verdict: ✅ Looks good to me~ Both non-blocking suggestions from `f51ed3e` are closed in a surgical +9/-4 across exactly 2 files (1 CSS, 1 test), zero production drift. Let me show you the trace~ **💡1 CLOSED — the ToolKey family theory:** The old single-arm `[Fact]` (`tool: MaskTool.Eraser`, assert `"erase"`) is now a `[Theory]` with all four arms — `Rectangle→rect`, `RectangleEraser→rect-erase`, `Brush→brush`, `Eraser→erase`. I confirmed the mappings match the switch exactly: line 90 `Rectangle => "rect"` (previously 0 coverage hits — NOW hit), line 91 `RectangleEraser => "rect-erase"`, line 92 `Eraser => "erase"`, `_ => "brush"` for `Brush`. The previously-dark `Rectangle` arm now fires. Every switch arm is pinned — the family is complete. *fufu~ you closed the one gap I noted and made it elegant while you were at it* **💡2 CLOSED — the opacity is documented as deliberate:** A one-line comment now sits above the `.kg-mask__live-rect--erase` rule: `/* Below the stroke eraser's 0.6 on purpose: a filled box carries more visual weight than a line. */`. Names the sibling value (0.6), names the reasoning (filled > stroked visual weight). The `0.5` value is unchanged — this is pure documentation, zero behavioral drift. Exactly what I asked for: I didn't want the value changed, I wanted the asymmetry explained so nobody "fixes" it later. *chef's kiss* ♪ **Migration verified:** the `[Fact] → [Theory]` promotion parameterizes BOTH the `MaskTool` input and the expected `data-tool` string — so each arm proves its own mapping, not one standing in for the rest. The test body `RenderCanvas(_ => { }, tool: tool)` + `Assert.Equal(key, ...GetAttribute("data-tool"))` is clean and honest. #### ✅ What I liked~ - The Theory promotion is the RIGHT shape — not a new test bolted on, but the existing test generalized. Four arms, four switch cases, one-to-one. No tautology, no orphan. - The opacity comment names the sibling value inline (`0.6`) so a future reader can verify the claim without grepping. Tiny detail, huge maintainability win. - Commit message is precise: names both items, names the resolution. Beautiful. - Zero scope creep: CSS comment + test Theory, that's it. No drive-by changes. --- *Automated review by Jibril · 2026-08-12* *CI/CD: stale for head SHA 94c93b1 (forgejo-actions coverage #6142 covers prior f51ed3e at 95%/88.7%, MaskCanvas 92.6%/88.2%) · Local checks: build 0 warnings/0 errors (.NET 10), 299/299 pass (was 296, +3 Theory arms), 11/11 MaskCanvas tests pass (was 8), 4/4 ToolKey theory arms green*
bjoern merged commit 3dc59a04c4 into main 2026-08-12 18:05:33 +02:00
bjoern deleted branch feat/mask-rect-eraser 2026-08-12 18:05:33 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Kagaku.UI!15
No description provided.