feat(mask): a rectangle eraser — the box counterpart of the stroke eraser #15
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/mask-rect-eraser"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The mask had one unmask gesture: the circular stroke eraser. This adds its box counterpart.
MaskTool.RectangleErasershares the drag-a-box gesture withRectangle(tool keyrect-erase); only the commit's polarity differs, soCommitRectnow carries the additive flag the wayCommitStrokealways has. The primitive model needed nothing — subtractive rects were always representable (MaskPrimitive.Rectangle(rect, additive: false)), no tool produced them.deselectglyph 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 existingCommitRectcall sites gained the explicit flag. 296/296.Consumer wiring (toolbar button + pointer bump) follows in Kagura.
🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagaku.UI - 95.1%
🔮 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
_isRecthelper collapsing threes.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')'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 intoMaskPrimitive.Rectangle(rect, additive)✓MaskPrimitive.cs:38—Rectangle(RegionRect rect, bool additive = true)already supportedadditive: 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:
MaskTool.RectangleEraser => "rect-erase"): 2 hits — fromThe_rectangle_eraser_commits_a_subtractive_rectwhich assertsdata-tool == "rect-erase"(goes red if the mapping is deleted, falls to_ => "brush") AND assertsrect.Additive == false(pins the subtractive commit). Directional, not tautological. ✓CommitRectasync state machine: 100% branch (Disabled guard hit + commit path hit). ✓CommitRectcall sites updated to pass explicitadditive:— both the additive-rect test and the disabled test. No orphan 4-arg calls survive. ✓The CSS mirror is exact:
.kg-mask__live-rect--eraseis a modifier layered on.kg-mask__live-rect— andmask.js:140emits'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::deepscope-resolution pattern from PR #14 carries through unchanged. ✓Icon hygiene:
deselectpath 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'ssrc/). Same honest shape as PRs #1/#2/#9. ✓💡 Little ideas (non-blocking)~
MaskTool.Rectangle => "rect"is the one ToolKey arm with 0 coverage hits (pre-existing, not introduced here). The new test pinned theRectangleErasersibling and the old test pinnedEraser, but nobody pins plainRectangle. A one-lineAssert.Equal("rect", cut.Find(".kg-mask__surface").GetAttribute("data-tool"))withtool: MaskTool.Rectanglewould close the family — purely additive, feel free to ignore~ ♪opacity: 0.5while its stroke sibling sits at0.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.6would match. Truly a nicety~✅ What I liked~
_isRecthelper — three sites collapsed to one predicate, comment explains exactly why both rect tools share the gesture. DRY without over-abstracting. fufu~ ♡MaskPrimitive.Rectangle(rect, additive: false)existed, no tool produced it). I verified — true.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! 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 (
Brushadditive +Erasersubtractive 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)mirrorsCommitStroke(xy, additive)— polarity rides the parameter._up:s.tool !== 'rect-erase'for rect additive is the exact structural twin ofs.tool === 'erase' ? false : truefor strokes._previewclass selection:'kg-mask__live-rect kg-mask__live-rect--erase'mirrors'kg-mask__live-stroke kg-mask__live-stroke--erase'._isRect(tool)helper cleanly replaces three=== 'rect'checks in_move,_up,_preview—rect-erasecorrectly 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--eraserule uses the same::deepdescendant 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--erasemodifier (line 66) follows the baselive-rectrule (line 49), same (0,2,0) specificity → later wins. Identical to the stroke-erase ordering. fufu~The
deselectglyph is well-formed: 74 entries now (was 73), startsMendsZ, all coords within viewBox0 -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_rectasserts BOTH thedata-tool="rect-erase"mapping (ToolKey switch) ANDAssert.False(rect.Additive)on the bound primitive. Red with the mapping removed, green with it — your mutation check holds. The two existingCommitRectcall sites gained the explicitadditive: trueflag, so the signature change is fully migrated.No breaking consumer contract.
CommitRectis[JSInvokable], called only from mask.js (updated) and tests (updated). Consumers driveToolvia parameter — never callCommitRectdirectly.💡 Little ideas (non-blocking)~
MaskCanvas.razor.css:69— opacity 0.5 vs the stroke eraser's 0.6. The rect eraser preview fills atopacity: 0.5while the stroke eraser strokes at0.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 confirmedBoth 💡s applied in
94c93b1: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_controllerbecame a[Theory]over all four tools — the whole ToolKey switch is pinned now, and a fifth tool will naturally join the list. 299/299.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.
🔮 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
f51ed3eare 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 90Rectangle => "rect"(previously 0 coverage hits — NOW hit), line 91RectangleEraser => "rect-erase", line 92Eraser => "erase",_ => "brush"forBrush. The previously-darkRectanglearm 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--eraserule:/* 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). The0.5value 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 theMaskToolinput and the expecteddata-toolstring — so each arm proves its own mapping, not one standing in for the rest. The test bodyRenderCanvas(_ => { }, tool: tool)+Assert.Equal(key, ...GetAttribute("data-tool"))is clean and honest.✅ What I liked~
0.6) so a future reader can verify the claim without grepping. Tiny detail, huge maintainability win.Automated review by Jibril · 2026-08-12
CI/CD: stale for head SHA
94c93b1(forgejo-actions coverage #6142 covers priorf51ed3eat 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