Sweep adjustment drag handles for remaining 24 shapes (P3-A.2)#226
Conversation
PR #210 (P3-A.1 pilot, adjustment drag handles for 9 pilot shapes) landed but the task pair stayed in active/ because tasks-archive.mjs gates archival on every todo checkbox being checked, and the pilot's 85 checkboxes were never flipped. Flipping them to match shipped reality (PR #210 closed deferred items as accepted limitations in the lessons doc) and running pnpm tasks:archive moves the pair into archive/2026/05/ and rebuilds both task indexes. P3-A.2 picks up where the pilot closed: register ADJUSTMENT_HANDLES for the 24 remaining P1/P2 shapes (triangle, basic polygons, 8 arrows, 3 callouts, 6 equation shapes) that already declare ADJUSTMENT_SPECS but currently render with default-only adjustments. The plan's first task also closes the pilot's three deferred limitations (8px corner inset guard, rotated paint test, STAR_MIN/ STAR_MAX duplication) so the sweep ships on a clean regression baseline rather than dragging old debt forward.
Three carry-overs from the pilot lessons doc, landed first so each later P3-A.2 shape commit ships on a clean regression baseline: * 8px corner inset guard on roundRect and wedgeRectCallout `position` functions. A roundRect with adjustments=[0] previously painted the diamond at (0,0) — directly under the NW resize handle — and a callout whose tail landed at any frame corner did the same. The guards leave `apply` (data) unchanged while clamping `position` (paint) so the diamond stays at least 8px away from corners. Tiny degenerate frames (w<16) fall back to the raw position to avoid an inverted clamp. Wedge inset only fires when the tail is inside the frame; tails outside (the common callout case) keep their attached-to-tip position. * Shared `adjustmentLocalToWorld` / `adjustmentWorldToLocal` in `interactions/adjustment.ts`. The overlay's paint-side rotation and the editor's drag-side inverse rotation were two structurally separate inline calculations that could silently drift. Both call sites now route through the same exported helpers, and a new round-trip test runs the star5 handle through paint→hit-test on a 30°-rotated 200×100 frame so future divergence trips. * `radialStarHandle` accepts its star's `AdjustmentSpec` rather than hardcoding STAR_MIN/STAR_MAX. The six stars are uniform today but the duplication would silently desync if a future star (or shape lifting the factory) declared a different max. Callers in star4..star10 pass their spec entry through. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Adds yellow-diamond drag handles for triangle, parallelogram,
trapezoid, hexagon, octagon, plus, and pentagonArrow. All seven
share a new linearTopEdgeHandle factory in shapes/handles.ts: each
shape supplies its own forward(adj, frame) → x and inverse(x, frame)
→ adj because the mappings genuinely differ — direct w-scaling for
triangle/parallelogram/trapezoid, min(w,h)-scaling for hexagon/
octagon, centered for plus, reversed for pentagonArrow. The factory
adds the spec-driven clamp on apply plus the 8px corner inset guard
on paint so boundary values never paint under a corner resize
handle.
roundRect is retrofitted onto the same factory, dropping the inline
inset logic from T1 so the corner-inset behavior has a single
source of truth.
Per the P3-A.1 lessons-doc recommendation, per-shape tests stay one
smoke case each ("registers a single handle on the top edge at the
expected position"); geometric coverage lives in handles.test.ts on
the factory itself (round-trip, inset, clamp, tiny-frame fallback,
spec-default fallback). plus and pentagonArrow get an extra round-
trip case each because their non-standard inverse math warrants
per-shape verification beyond the factory.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Two shapes that do not fit the linearTopEdgeHandle factory: * `can`: the adjustment is the lid half-height (linear-y), so the diamond paints at (w/2, ry) on the lid line. As the user drags down, the lid grows taller. The y axis gets the same 8px inset as the x axis on top-edge handles — boundary adjustments would otherwise overlap the N/S resize handles. * `donut`: the ring thickness `t = adj × min(w,h)` is *subtractive* (`innerRx = outerRx - t`) rather than the multiplicative ratio used by stars, so the `radialStarHandle` factory does not apply. The handle inlines its own forward/inverse and paints where the inner ring crosses the horizontal centre line on the right side (`w - t`, `h/2`). Dragging leftward thickens the ring. The 8px inset logic that the linear factory built in for the seven T2 shapes is now exported as `insetAlongAxis(coord, dim)` so these two inline handles share the same guard. `linearTopEdgeHandle` is refactored to call the same helper — single source of truth for the corner inset behaviour. Both shapes get smoke tests covering registration, the inset on boundary values, and the inverse math (clamping past spec max). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
right/left/up/down/leftRightArrow each get a head-length handle and a head-width handle. The handles are inline per arrow rather than shared through a factory because the geometry differs more than it matches: * axis direction varies (x for right/left/leftRight, y for up/down) * head-length scaling differs (w for right/left, h for up/down, w/2 for leftRight whose two heads each take half the width) * head-width scaling differs (h/2 vs w/2 depending on arrow axis) * head start position differs (left side for right/leftRight, right side for left, top for up, bottom for down) A `directionalArrowHandles(direction)` factory could collapse the rotational symmetry between right and left, or up and down, but it would need to take ~6 parameters to do so (axisStart, axisDir, perpDir, lengthScale, widthScale, plus per-direction inverse math) — the parameter list ends up the same size as the inline body. Inline is clearer per-shape and easier to debug. All handles share the existing insetAlongAxis helper for the 8px corner inset on both axes, and pass through the non-controlled adjustment index from startAdjustments per the AdjustmentHandle contract. Head-width apply uses Math.abs around the centerline so dragging either above or below grows the head symmetrically. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
quadArrow has three adjustments — head length, head width, shaft thickness — all OOXML thousandths of min(w,h). The three diamonds cluster around the TOP arrowhead, the closest 4-fold-symmetry axis the user will visually associate with each adjustment: [0] head length → (cx, head) drag vertically [1] head width → (cx + headHalf, head) drag horizontally [2] shaft → (cx + shaft, cy) drag horizontally At defaults (all three = 22500) the head-width and shaft handles share an x but live on different y rows so they remain visually separable. Inline rather than factored: a 4-headed arrow has no 1-handle-controls-many-arrows reuse opportunity. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
wedgeEllipseCallout, cloudCallout, and wedgeRoundRectCallout all share the same tail-tip encoding as wedgeRectCallout from the pilot — adjustments [0]/[1] are signed thousandths of (w, h) around the frame centre. The pilot's inline tail position math (including the corner-inset guard from T1) is promoted into a new pointTailHandle(specX, specY) factory in callouts/handles.ts, and wedgeRectCallout is retrofitted to use it. The three new callouts each register the same factory call. wedgeRoundRectCallout adds a second handle for its corner-radius adjustment (index 2). linearTopEdgeHandle from shapes/handles.ts is extended with an `index?: number` parameter so it can drive a non- zero index while passing the other adjustments through from startAdjustments unchanged — the existing 7 single-adjustment shapes keep working without changes (default index = 0). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Closes the P3-A.2 sweep at 33/33 — every ADJUSTMENT_SPECS entry now has a matching ADJUSTMENT_HANDLES registration. * mathPlus reuses linearTopEdgeHandle with the same forward/inverse as basic/plus (the shape is geometrically identical; only the default thickness differs). * mathMinus is a single horizontal bar — one linear-y handle at the top of the bar, dragging up grows thickness. * mathMultiply is the rotated +; after 45° rotation the upper inner notch lands at (cx, cy - t * SQRT1_2), so the handle paints there and the inverse divides by SQRT1_2 to recover thickness. * mathEqual takes two handles on the upper bar — top edge for bar thickness, bottom edge (= upper gap boundary) for gap. * mathDivide adds three: bar top, top-dot right edge for radius, and the midpoint between bar top and top-dot bottom for gap. * mathNotEqual repeats mathEqual's bar+gap pattern and adds a slash- thickness handle on the upper edge of the -45° slash. The slash thickness handle projects pointer movement onto the slash's perpendicular direction so dragging perpendicular to the slash changes slashT and parallel does not. All handles use the start-adjustments pass-through pattern (spread + index assignment) so each handle drives exactly one index and leaves the others unchanged. Per-shape smoke tests assert registration count and the default-position math. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Two closures for the post-sweep state: * `axisLabel?: string` on AdjustmentSpec — overrides the lastWord heuristic in formatAdjustments when the heuristic would collide. After P3-A.2 the only currently-colliding case is mathNotEqual, whose "Bar thickness" and "Slash thickness" would both collapse to "thickness"; explicit labels "bar" and "slash" disambiguate the tooltip. Single-axis specs and the other multi-axis shapes don't need it. * formatAdjustments (with its lastWord helper) is moved out of editor.ts into interactions/adjustment.ts so it can be unit- tested directly. Three new test cases cover single-axis output, the lastWord fallback, and the axisLabel override. * shapes/index.test.ts grows two assertions: every ADJUSTMENT_SPECS entry now has a matching ADJUSTMENT_HANDLES registration, and the two maps have the same size. The original "every HANDLES kind has a SPECS" assertion is the converse; this new direction is what P3-A.2 actually closes. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The pilot 9 shapes have a visual baseline since PR #210; the 24 P3-A.2 sweep shapes (triangle, parallelogram, trapezoid, hexagon, octagon, plus, pentagonArrow, can, donut, 5 directional arrows, quadArrow, 3 wedge/cloud callouts, 6 math equation shapes) now get their own `shapes-adjustments-sweep` scenario — a 6×4 grid at OOXML defaults. Path-builder regressions on any of these shapes will surface here. The pilot scenario remains untouched so its baseline approval from PR #210 carries forward. Adding a sibling scenario rather than renaming avoids a baseline-rewrite on the pilot. Two pieces of plumbing: the new scenario id is added to the scenarioIds whitelist in verify-visual-browser.mjs (the harness does NOT auto-discover scenarios from the .tsx); and the four composite full-harness PNGs are updated because they include the new section in their layout. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Captures scope-reconciliation surprises (donut isn't radial in the star factory's sense; mathMultiply rotation needs SQRT1_2), the factory consolidation matrix (linearTopEdgeHandle vs. inline arrows vs. pointTailHandle), the visual-harness scenarioIds whitelist gotcha, and the pre-commit hook output-buffer behaviour the sweep tripped into. Also records what to lean on for P3-B. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
📝 WalkthroughWalkthroughThis PR implements interactive adjustment drag handles for 24 slide shape kinds (basic, arrows, callouts, and equations) by introducing shared handle factories, per-shape handle exports with tests, refactored editor coordinate utilities, registry registration, and visual verification infrastructure. The change spans 170+ modified files with ~1400 lines added. ChangesAdjustment Handles Sweep Implementation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
Verification: verify:selfResult: ✅ PASS in 181.8s
Verification: verify:integrationResult: ✅ PASS |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (10)
packages/slides/src/view/canvas/shapes/equation/math-divide.handles.test.ts (1)
4-21: ⚡ Quick winConsider adding apply() round-trip tests.
The test validates
position()output but doesn't verify thatapply()correctly updates adjustments when a handle is dragged. Round-trip tests (position → simulated drag → apply → verify adjustment values) would strengthen confidence in the handle behavior, especially for handle[2] which depends on the bar adjustment.Example test structure
it('handle[0] apply updates bar thickness', () => { const initial = [23520, 5880, 11760]; const size = { w: 200, h: 100 }; // Simulate dragging bar handle upward (reduce bar thickness) const result = MATH_DIVIDE_HANDLES[0].apply(size, initial, { x: 100, y: 30 }); expect(result[0]).toBeLessThan(initial[0]); expect(result[0]).toBeGreaterThanOrEqual(MATH_DIVIDE_ADJUSTMENTS[0].min); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/view/canvas/shapes/equation/math-divide.handles.test.ts` around lines 4 - 21, Add round-trip tests that exercise MATH_DIVIDE_HANDLES' position() → simulated drag → apply() flow to ensure adjustments update correctly; for each handle (especially MATH_DIVIDE_HANDLES[0], [1], and [2]) call position() to get the handle coords, simulate a drag target (e.g., move y up/down or x left/right), invoke the handle's apply(size, initialAdjustments, draggedPoint) and assert the returned adjustments changed in the expected direction and remain within MATH_DIVIDE_ADJUSTMENTS[n].min/max bounds (and for handle[2] include cases where bar adjustment from handle[0] affects its result). Ensure tests reference the functions MATH_DIVIDE_HANDLES.position and MATH_DIVIDE_HANDLES.apply and use the MATH_DIVIDE_ADJUSTMENTS constants for assertions.packages/slides/src/view/canvas/shapes/arrows/left-arrow.handles.test.ts (1)
4-11: ⚡ Quick winConsider adding test coverage for
applylogic and boundary conditions.The test only verifies handle count and a single
positioncall with default adjustments. Consider adding tests for:
- The
applyfunction's pointer-to-adjustment conversion and clamping- Boundary values (min/max from
ARROW_ADJUSTMENTS)- Edge cases like zero dimensions or extreme pointer positions
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/view/canvas/shapes/arrows/left-arrow.handles.test.ts` around lines 4 - 11, Add unit tests for LEFT_ARROW_HANDLES to cover the apply logic and boundary conditions: write tests that call LEFT_ARROW_HANDLES[0].apply (and other handles) with pointer vectors that map to min/max ARROW_ADJUSTMENTS values and verify clamping, test pointer-to-adjustment conversion for typical and extreme pointer positions (very large/small/negative), and include edge cases like zero width or height to ensure position({w:0,h:0}, ...) and apply behave correctly; reference LEFT_ARROW_HANDLES, its apply and position methods, and ARROW_ADJUSTMENTS in your assertions.packages/slides/src/view/canvas/shapes/equation/math-equal.handles.test.ts (1)
4-15: ⚡ Quick winAdd
apply()and boundary clamp assertions for both handles.This suite currently verifies only
position(). The critical pointer→adjustment conversion path inapply()(including min/max clamping) is untested.Proposed test additions
describe('MATH_EQUAL_HANDLES', () => { it('registers thickness and gap handles', () => { @@ expect(p1.y).toBeCloseTo(44.12, 2); }); + + it('thickness apply updates index 0 and preserves index 1', () => { + const next = MATH_EQUAL_HANDLES[0].apply({ w: 200, h: 100 }, [23520, 11760], { x: 100, y: 0 }); + expect(next).toEqual([44120, 11760]); + }); + + it('gap apply clamps to spec range', () => { + const hi = MATH_EQUAL_HANDLES[1].apply({ w: 200, h: 100 }, [23520, 11760], { x: 100, y: 0 }); + const lo = MATH_EQUAL_HANDLES[1].apply({ w: 200, h: 100 }, [23520, 11760], { x: 100, y: 100 }); + expect(hi).toEqual([23520, 50000]); + expect(lo).toEqual([23520, 0]); + }); });As per coding guidelines,
**/*.{test,spec}.{js,ts,jsx,tsx}: Write tests for critical business logic and edge cases.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/view/canvas/shapes/equation/math-equal.handles.test.ts` around lines 4 - 15, Add tests for the handles' apply() behavior and boundary clamping: for each handle in MATH_EQUAL_HANDLES, call its apply(pointer, size, currentValues) with representative pointer positions (including edge/out-of-bounds cases) and assert the returned adjustment matches expected thickness/gap deltas and is clamped within allowed min/max; keep existing position() assertions and add symmetrically for both handles (use MATH_EQUAL_HANDLES[0].apply and MATH_EQUAL_HANDLES[1].apply, and verify pointer→adjustment conversion and min/max enforcement).packages/slides/src/view/canvas/shapes/basic/octagon.handles.test.ts (1)
4-12: ⚡ Quick winExpand this suite to cover
apply()inversion and clamp boundaries.Right now it only checks
position(). Addapply()assertions so drag behavior and min/max clamping are protected from regressions.Proposed test additions
describe('OCTAGON_HANDLES', () => { it('registers a single linear-x handle on the top edge', () => { @@ expect(p.y).toBe(0); }); + + it('apply inverts pointer.x back to the adjustment domain', () => { + const next = OCTAGON_HANDLES[0].apply({ w: 200, h: 100 }, [29289], { x: 29.289, y: 0 }); + expect(next).toEqual([29289]); + }); + + it('apply clamps at spec min/max', () => { + const min = OCTAGON_HANDLES[0].apply({ w: 200, h: 100 }, [29289], { x: -50, y: 0 }); + const max = OCTAGON_HANDLES[0].apply({ w: 200, h: 100 }, [29289], { x: 9999, y: 0 }); + expect(min).toEqual([0]); + expect(max).toEqual([50000]); + }); });As per coding guidelines,
**/*.{test,spec}.{js,ts,jsx,tsx}: Write tests for critical business logic and edge cases.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/view/canvas/shapes/basic/octagon.handles.test.ts` around lines 4 - 12, Extend the OCTAGON_HANDLES test to also verify the handle's apply() is the inverse of position() and that applied values are clamped: for OCTAGON_HANDLES[0] (use the same size {w:200,h:100} and initial cut array [29289]) compute p = position(...), then call apply(..., { x: p.x + 10, y: p.y }) and assert the returned cut array changed from 29289 in the expected direction (i.e. increased) and that re-running position() with the new cut yields an x close to p.x + 10; also call apply with extreme points (e.g. x < 0 and x > w) and assert the returned cut is clamped to the min/max allowed values (0 and 100000 normalised as in this codebase) so out-of-bounds drags produce boundary values.packages/slides/src/view/canvas/shapes/basic/triangle.handles.test.ts (1)
5-9: ⚡ Quick winCover inverse behavior with a round-trip test.
This test only checks
position(). Addapply(position(...))round-trip to catch inverse-mapping regressions.Proposed test addition
describe('TRIANGLE_HANDLES', () => { it('registers a single linear-x handle on the top edge', () => { expect(TRIANGLE_HANDLES).toHaveLength(1); const p = TRIANGLE_HANDLES[0].position({ w: 200, h: 100 }, [50000]); expect(p).toEqual({ x: 100, y: 0 }); // apex centred at default }); + + it('round-trips position ↔ apply at default', () => { + const frame = { w: 200, h: 100 }; + const start = [50000]; + const p = TRIANGLE_HANDLES[0].position(frame, start); + const back = TRIANGLE_HANDLES[0].apply(frame, start, p); + expect(back[0]).toBeCloseTo(start[0], -1); + }); });As per coding guidelines
**/*.{test,spec}.{js,ts,jsx,tsx}: Write tests for critical business logic and edge cases.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/view/canvas/shapes/basic/triangle.handles.test.ts` around lines 5 - 9, Add a round-trip assertion that verifies the handle's inverse mapping: for the first handle in TRIANGLE_HANDLES call position with the same shape ({ w: 200, h: 100 }) and seed ([50000]) to get p, then call TRIANGLE_HANDLES[0].apply(p, { w: 200, h: 100 }) and assert the result equals the original input used by position (e.g., [50000]); this will ensure position(...) → apply(...) round-trip consistency and catch inverse-mapping regressions.packages/slides/src/view/canvas/shapes/basic/hexagon.handles.test.ts (1)
5-10: ⚡ Quick winAdd an inverse round-trip assertion for the handle.
This currently validates only
position(); a break inapply()/inverse math would still pass. Add a position↔apply round-trip check in this suite.Proposed test addition
describe('HEXAGON_HANDLES', () => { it('registers a single linear-x handle on the top edge', () => { expect(HEXAGON_HANDLES).toHaveLength(1); // notch = 25% * min(200, 100) = 25 const p = HEXAGON_HANDLES[0].position({ w: 200, h: 100 }, [25000]); expect(p).toEqual({ x: 25, y: 0 }); }); + + it('round-trips position ↔ apply at default', () => { + const frame = { w: 200, h: 100 }; + const start = [25000]; + const p = HEXAGON_HANDLES[0].position(frame, start); + const back = HEXAGON_HANDLES[0].apply(frame, start, p); + expect(back[0]).toBeCloseTo(start[0], -1); + }); });As per coding guidelines
**/*.{test,spec}.{js,ts,jsx,tsx}: Write tests for critical business logic and edge cases.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/view/canvas/shapes/basic/hexagon.handles.test.ts` around lines 5 - 10, The test only asserts HEXAGON_HANDLES[0].position(...) — add an inverse round-trip assertion that verifies apply() reverses position(): capture the handle param used (e.g. values = [25000]), compute p = HEXAGON_HANDLES[0].position({ w:200,h:100 }, values), then call HEXAGON_HANDLES[0].apply({ w:200,h:100 }, p) and expect the returned values to equal the original values (or equal within a small tolerance if numeric scaling occurs). Use the same handle symbol HEXAGON_HANDLES[0], the position and apply methods, and include the round-trip expect in the same test case.packages/slides/src/view/canvas/shapes/callouts/wedge-round-rect-callout.handles.test.ts (1)
9-28: ⚡ Quick winAdd direct coverage for the tail handle path.
Line 9 onward validates only the corner-radius handle (
handles[1]). Please add one assertion forhandles[0]position(...)and oneapply(...)check so both registered handles have shape-level integration coverage.Proposed test extension
describe('WEDGE_ROUND_RECT_CALLOUT_HANDLES', () => { @@ it('corner-radius handle controls adjustments[2], leaving tail adjustments unchanged', () => { @@ expect(next[2]).toBe(30000); }); + + it('tail handle updates tail adjustments', () => { + const start = [-20833, 62500, 16667]; + const p = WEDGE_ROUND_RECT_CALLOUT_HANDLES[0].position({ w: 200, h: 100 }, start); + expect(p).toBeDefined(); + + const next = WEDGE_ROUND_RECT_CALLOUT_HANDLES[0].apply( + { w: 200, h: 100 }, + start, + { x: 150, y: 120 }, + ); + expect(next[0]).not.toBe(start[0]); + expect(next[1]).not.toBe(start[1]); + expect(next[2]).toBe(start[2]); + }); });As per coding guidelines, "Write tests for critical business logic and edge cases".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/view/canvas/shapes/callouts/wedge-round-rect-callout.handles.test.ts` around lines 9 - 28, Add explicit coverage for the tail handle (handles[0]) by calling WEDGE_ROUND_RECT_CALLOUT_HANDLES[0].position(...) with the same input used for handles[1] and asserting the returned x/y match the expected tail-position values, then call WEDGE_ROUND_RECT_CALLOUT_HANDLES[0].apply(...) with a dragged point and assert that the tail values (first two entries) change as expected while the radius (third entry) remains unchanged; locate usages of WEDGE_ROUND_RECT_CALLOUT_HANDLES[1].position/apply in the test and mirror them for index 0, asserting the correct mutated array entries for the tail and unchanged radius.packages/slides/src/view/canvas/shapes/equation/math-multiply.handles.test.ts (1)
4-12: ⚡ Quick winAdd
applycoverage for clamp and inverse behavior.This suite only validates
position;apply(the drag mutation path) should also be covered with at least one round-trip and min/max clamp case.Proposed test additions
describe('MATH_MULTIPLY_HANDLES', () => { it('registers a single arm-thickness handle on the rotated cross', () => { @@ expect(p.y).toBeCloseTo(50 - 23.52 * Math.SQRT1_2, 2); }); + + it('apply round-trips position for an in-range value', () => { + const handle = MATH_MULTIPLY_HANDLES[0]; + const frame = { w: 200, h: 100 }; + const start = [23520]; + const p = handle.position(frame, start); + const next = handle.apply(frame, start, p); + expect(next[0]).toBeCloseTo(start[0], 0); + }); + + it('apply clamps when dragged far below center', () => { + const next = MATH_MULTIPLY_HANDLES[0].apply( + { w: 200, h: 100 }, + [23520], + { x: 100, y: 1000 }, + ); + expect(next[0]).toBe(0); + }); });As per coding guidelines,
**/*.{test,spec}.{js,ts,jsx,tsx}: Write tests for critical business logic and edge cases.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/view/canvas/shapes/equation/math-multiply.handles.test.ts` around lines 4 - 12, Add tests covering the handle's apply (drag) behavior for MATH_MULTIPLY_HANDLES in addition to the existing position assertions: call the handle's apply on MATH_MULTIPLY_HANDLES[0] with an initial args array (e.g., [23520]) and a delta that should round-trip (apply then re-calc position gives same value), then add two separate tests that apply deltas pushing the parameter below its minimum and above its maximum and assert the resulting value is clamped to expected min/max; reference the handle via MATH_MULTIPLY_HANDLES[0].apply(...) and verify by reading the mutated args and/or recomputing position via MATH_MULTIPLY_HANDLES[0].position(...) to confirm clamping and round-trip behavior.packages/slides/src/view/canvas/shapes/equation/math-minus.handles.test.ts (1)
4-12: ⚡ Quick winCover the
applypath in this suite.Please add at least one inverse round-trip case and one clamp boundary case for
MATH_MINUS_HANDLES[0].apply(...).As per coding guidelines,
**/*.{test,spec}.{js,ts,jsx,tsx}: Write tests for critical business logic and edge cases.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/view/canvas/shapes/equation/math-minus.handles.test.ts` around lines 4 - 12, Add tests that exercise the apply path of MATH_MINUS_HANDLES[0]: (1) an inverse round-trip case — call handler.position(...) to get a position, then pass that position through MATH_MINUS_HANDLES[0].apply(...) and assert the returned values (e.g., the first element of the values array) equal the original input within tolerance; and (2) a clamp/boundary case — call .apply(...) with positions outside the allowed area (e.g., beyond left/right/top/bottom or extreme y) and assert the returned value is clamped to the min/max expected by the handler. Use the existing position call to derive realistic inputs and reference MATH_MINUS_HANDLES[0].position and MATH_MINUS_HANDLES[0].apply when locating code to change.packages/slides/src/view/canvas/shapes/equation/math-not-equal.handles.test.ts (1)
4-16: ⚡ Quick winExpand tests beyond slash-handle position.
Please add tests for
apply(clamp + round-trip) and at least one check each for bar/gap handle positions to protect all three adjustment axes.As per coding guidelines,
**/*.{test,spec}.{js,ts,jsx,tsx}: Write tests for critical business logic and edge cases.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/view/canvas/shapes/equation/math-not-equal.handles.test.ts` around lines 4 - 16, Add tests to cover all three adjustment axes for MATH_NOT_EQUAL_HANDLES: extend the existing test suite to (1) assert the bar handle (index 0) position for a given size and values array, (2) assert the gap handle (index 1) position similarly, and (3) test the slash handle's apply method by calling its apply with out-of-range values to verify clamping and then calling position to verify round-trip rounding behavior; reference the handles via MATH_NOT_EQUAL_HANDLES[0], [1], [2], use the handle.apply function to check clamping/round-trip, and use handle.position to assert expected x/y coordinates (use toBeCloseTo for floating checks).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/slides/src/view/editor/interactions/adjustment.ts`:
- Around line 98-101: The tooltip formatting currently calls
specs[...].format(v) using values that may be missing, causing "undefined" or
passing undefined into format; update both the single-spec branch (the if
(specs.length === 1) block) and the multi-spec branch (around the code at the
103-106 region) to first coalesce each value with its spec-level fallback (e.g.,
use v = values[i] ?? specs[i].default or specs[i].defaultValue) and then call
specs[i].format only with that concrete value (or fall back to String(fallback)
when no format exists); ensure you reference specs, values, and format so the
guard prevents calling format(undefined) and avoids emitting "undefined" in
tooltips.
---
Nitpick comments:
In `@packages/slides/src/view/canvas/shapes/arrows/left-arrow.handles.test.ts`:
- Around line 4-11: Add unit tests for LEFT_ARROW_HANDLES to cover the apply
logic and boundary conditions: write tests that call LEFT_ARROW_HANDLES[0].apply
(and other handles) with pointer vectors that map to min/max ARROW_ADJUSTMENTS
values and verify clamping, test pointer-to-adjustment conversion for typical
and extreme pointer positions (very large/small/negative), and include edge
cases like zero width or height to ensure position({w:0,h:0}, ...) and apply
behave correctly; reference LEFT_ARROW_HANDLES, its apply and position methods,
and ARROW_ADJUSTMENTS in your assertions.
In `@packages/slides/src/view/canvas/shapes/basic/hexagon.handles.test.ts`:
- Around line 5-10: The test only asserts HEXAGON_HANDLES[0].position(...) — add
an inverse round-trip assertion that verifies apply() reverses position():
capture the handle param used (e.g. values = [25000]), compute p =
HEXAGON_HANDLES[0].position({ w:200,h:100 }, values), then call
HEXAGON_HANDLES[0].apply({ w:200,h:100 }, p) and expect the returned values to
equal the original values (or equal within a small tolerance if numeric scaling
occurs). Use the same handle symbol HEXAGON_HANDLES[0], the position and apply
methods, and include the round-trip expect in the same test case.
In `@packages/slides/src/view/canvas/shapes/basic/octagon.handles.test.ts`:
- Around line 4-12: Extend the OCTAGON_HANDLES test to also verify the handle's
apply() is the inverse of position() and that applied values are clamped: for
OCTAGON_HANDLES[0] (use the same size {w:200,h:100} and initial cut array
[29289]) compute p = position(...), then call apply(..., { x: p.x + 10, y: p.y
}) and assert the returned cut array changed from 29289 in the expected
direction (i.e. increased) and that re-running position() with the new cut
yields an x close to p.x + 10; also call apply with extreme points (e.g. x < 0
and x > w) and assert the returned cut is clamped to the min/max allowed values
(0 and 100000 normalised as in this codebase) so out-of-bounds drags produce
boundary values.
In `@packages/slides/src/view/canvas/shapes/basic/triangle.handles.test.ts`:
- Around line 5-9: Add a round-trip assertion that verifies the handle's inverse
mapping: for the first handle in TRIANGLE_HANDLES call position with the same
shape ({ w: 200, h: 100 }) and seed ([50000]) to get p, then call
TRIANGLE_HANDLES[0].apply(p, { w: 200, h: 100 }) and assert the result equals
the original input used by position (e.g., [50000]); this will ensure
position(...) → apply(...) round-trip consistency and catch inverse-mapping
regressions.
In
`@packages/slides/src/view/canvas/shapes/callouts/wedge-round-rect-callout.handles.test.ts`:
- Around line 9-28: Add explicit coverage for the tail handle (handles[0]) by
calling WEDGE_ROUND_RECT_CALLOUT_HANDLES[0].position(...) with the same input
used for handles[1] and asserting the returned x/y match the expected
tail-position values, then call WEDGE_ROUND_RECT_CALLOUT_HANDLES[0].apply(...)
with a dragged point and assert that the tail values (first two entries) change
as expected while the radius (third entry) remains unchanged; locate usages of
WEDGE_ROUND_RECT_CALLOUT_HANDLES[1].position/apply in the test and mirror them
for index 0, asserting the correct mutated array entries for the tail and
unchanged radius.
In `@packages/slides/src/view/canvas/shapes/equation/math-divide.handles.test.ts`:
- Around line 4-21: Add round-trip tests that exercise MATH_DIVIDE_HANDLES'
position() → simulated drag → apply() flow to ensure adjustments update
correctly; for each handle (especially MATH_DIVIDE_HANDLES[0], [1], and [2])
call position() to get the handle coords, simulate a drag target (e.g., move y
up/down or x left/right), invoke the handle's apply(size, initialAdjustments,
draggedPoint) and assert the returned adjustments changed in the expected
direction and remain within MATH_DIVIDE_ADJUSTMENTS[n].min/max bounds (and for
handle[2] include cases where bar adjustment from handle[0] affects its result).
Ensure tests reference the functions MATH_DIVIDE_HANDLES.position and
MATH_DIVIDE_HANDLES.apply and use the MATH_DIVIDE_ADJUSTMENTS constants for
assertions.
In `@packages/slides/src/view/canvas/shapes/equation/math-equal.handles.test.ts`:
- Around line 4-15: Add tests for the handles' apply() behavior and boundary
clamping: for each handle in MATH_EQUAL_HANDLES, call its apply(pointer, size,
currentValues) with representative pointer positions (including
edge/out-of-bounds cases) and assert the returned adjustment matches expected
thickness/gap deltas and is clamped within allowed min/max; keep existing
position() assertions and add symmetrically for both handles (use
MATH_EQUAL_HANDLES[0].apply and MATH_EQUAL_HANDLES[1].apply, and verify
pointer→adjustment conversion and min/max enforcement).
In `@packages/slides/src/view/canvas/shapes/equation/math-minus.handles.test.ts`:
- Around line 4-12: Add tests that exercise the apply path of
MATH_MINUS_HANDLES[0]: (1) an inverse round-trip case — call
handler.position(...) to get a position, then pass that position through
MATH_MINUS_HANDLES[0].apply(...) and assert the returned values (e.g., the first
element of the values array) equal the original input within tolerance; and (2)
a clamp/boundary case — call .apply(...) with positions outside the allowed area
(e.g., beyond left/right/top/bottom or extreme y) and assert the returned value
is clamped to the min/max expected by the handler. Use the existing position
call to derive realistic inputs and reference MATH_MINUS_HANDLES[0].position and
MATH_MINUS_HANDLES[0].apply when locating code to change.
In
`@packages/slides/src/view/canvas/shapes/equation/math-multiply.handles.test.ts`:
- Around line 4-12: Add tests covering the handle's apply (drag) behavior for
MATH_MULTIPLY_HANDLES in addition to the existing position assertions: call the
handle's apply on MATH_MULTIPLY_HANDLES[0] with an initial args array (e.g.,
[23520]) and a delta that should round-trip (apply then re-calc position gives
same value), then add two separate tests that apply deltas pushing the parameter
below its minimum and above its maximum and assert the resulting value is
clamped to expected min/max; reference the handle via
MATH_MULTIPLY_HANDLES[0].apply(...) and verify by reading the mutated args
and/or recomputing position via MATH_MULTIPLY_HANDLES[0].position(...) to
confirm clamping and round-trip behavior.
In
`@packages/slides/src/view/canvas/shapes/equation/math-not-equal.handles.test.ts`:
- Around line 4-16: Add tests to cover all three adjustment axes for
MATH_NOT_EQUAL_HANDLES: extend the existing test suite to (1) assert the bar
handle (index 0) position for a given size and values array, (2) assert the gap
handle (index 1) position similarly, and (3) test the slash handle's apply
method by calling its apply with out-of-range values to verify clamping and then
calling position to verify round-trip rounding behavior; reference the handles
via MATH_NOT_EQUAL_HANDLES[0], [1], [2], use the handle.apply function to check
clamping/round-trip, and use handle.position to assert expected x/y coordinates
(use toBeCloseTo for floating checks).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ccd400d2-57e8-42af-a8de-28589e651498
⛔ Files ignored due to path filters (8)
packages/frontend/tests/visual/baselines/harness-visual.browser.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.shapes-adjustments-sweep.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.shapes-adjustments-sweep.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.shapes-adjustments-sweep.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.shapes-adjustments-sweep.pngis excluded by!**/*.png
📒 Files selected for processing (77)
docs/tasks/README.mddocs/tasks/active/20260512-slides-shapes-p3a2-sweep-lessons.mddocs/tasks/active/20260512-slides-shapes-p3a2-sweep-todo.mddocs/tasks/archive/2026/05/20260510-slides-shapes-p3a-pilot-lessons.mddocs/tasks/archive/2026/05/20260510-slides-shapes-p3a-pilot-todo.mddocs/tasks/archive/README.mdpackages/frontend/scripts/verify-visual-browser.mjspackages/frontend/src/app/harness/visual/slides-scenarios.tsxpackages/slides/src/view/canvas/shapes/arrows/down-arrow.handles.test.tspackages/slides/src/view/canvas/shapes/arrows/down-arrow.tspackages/slides/src/view/canvas/shapes/arrows/left-arrow.handles.test.tspackages/slides/src/view/canvas/shapes/arrows/left-arrow.tspackages/slides/src/view/canvas/shapes/arrows/left-right-arrow.handles.test.tspackages/slides/src/view/canvas/shapes/arrows/left-right-arrow.tspackages/slides/src/view/canvas/shapes/arrows/pentagon-arrow.handles.test.tspackages/slides/src/view/canvas/shapes/arrows/pentagon-arrow.tspackages/slides/src/view/canvas/shapes/arrows/quad-arrow.handles.test.tspackages/slides/src/view/canvas/shapes/arrows/quad-arrow.tspackages/slides/src/view/canvas/shapes/arrows/right-arrow.handles.test.tspackages/slides/src/view/canvas/shapes/arrows/right-arrow.tspackages/slides/src/view/canvas/shapes/arrows/up-arrow.handles.test.tspackages/slides/src/view/canvas/shapes/arrows/up-arrow.tspackages/slides/src/view/canvas/shapes/basic/can.handles.test.tspackages/slides/src/view/canvas/shapes/basic/can.tspackages/slides/src/view/canvas/shapes/basic/donut.handles.test.tspackages/slides/src/view/canvas/shapes/basic/donut.tspackages/slides/src/view/canvas/shapes/basic/hexagon.handles.test.tspackages/slides/src/view/canvas/shapes/basic/hexagon.tspackages/slides/src/view/canvas/shapes/basic/octagon.handles.test.tspackages/slides/src/view/canvas/shapes/basic/octagon.tspackages/slides/src/view/canvas/shapes/basic/parallelogram.handles.test.tspackages/slides/src/view/canvas/shapes/basic/parallelogram.tspackages/slides/src/view/canvas/shapes/basic/plus.handles.test.tspackages/slides/src/view/canvas/shapes/basic/plus.tspackages/slides/src/view/canvas/shapes/basic/round-rect.handles.test.tspackages/slides/src/view/canvas/shapes/basic/round-rect.tspackages/slides/src/view/canvas/shapes/basic/trapezoid.handles.test.tspackages/slides/src/view/canvas/shapes/basic/trapezoid.tspackages/slides/src/view/canvas/shapes/basic/triangle.handles.test.tspackages/slides/src/view/canvas/shapes/basic/triangle.tspackages/slides/src/view/canvas/shapes/builder.tspackages/slides/src/view/canvas/shapes/callouts/cloud-callout.handles.test.tspackages/slides/src/view/canvas/shapes/callouts/cloud-callout.tspackages/slides/src/view/canvas/shapes/callouts/handles.tspackages/slides/src/view/canvas/shapes/callouts/wedge-ellipse-callout.handles.test.tspackages/slides/src/view/canvas/shapes/callouts/wedge-ellipse-callout.tspackages/slides/src/view/canvas/shapes/callouts/wedge-rect-callout.handles.test.tspackages/slides/src/view/canvas/shapes/callouts/wedge-rect-callout.tspackages/slides/src/view/canvas/shapes/callouts/wedge-round-rect-callout.handles.test.tspackages/slides/src/view/canvas/shapes/callouts/wedge-round-rect-callout.tspackages/slides/src/view/canvas/shapes/equation/math-divide.handles.test.tspackages/slides/src/view/canvas/shapes/equation/math-divide.tspackages/slides/src/view/canvas/shapes/equation/math-equal.handles.test.tspackages/slides/src/view/canvas/shapes/equation/math-equal.tspackages/slides/src/view/canvas/shapes/equation/math-minus.handles.test.tspackages/slides/src/view/canvas/shapes/equation/math-minus.tspackages/slides/src/view/canvas/shapes/equation/math-multiply.handles.test.tspackages/slides/src/view/canvas/shapes/equation/math-multiply.tspackages/slides/src/view/canvas/shapes/equation/math-not-equal.handles.test.tspackages/slides/src/view/canvas/shapes/equation/math-not-equal.tspackages/slides/src/view/canvas/shapes/equation/math-plus.handles.test.tspackages/slides/src/view/canvas/shapes/equation/math-plus.tspackages/slides/src/view/canvas/shapes/handles.test.tspackages/slides/src/view/canvas/shapes/handles.tspackages/slides/src/view/canvas/shapes/index.test.tspackages/slides/src/view/canvas/shapes/index.tspackages/slides/src/view/canvas/shapes/stars/handles.tspackages/slides/src/view/canvas/shapes/stars/star10.tspackages/slides/src/view/canvas/shapes/stars/star4.tspackages/slides/src/view/canvas/shapes/stars/star5.tspackages/slides/src/view/canvas/shapes/stars/star6.tspackages/slides/src/view/canvas/shapes/stars/star7.tspackages/slides/src/view/canvas/shapes/stars/star8.tspackages/slides/src/view/editor/editor.tspackages/slides/src/view/editor/interactions/adjustment.test.tspackages/slides/src/view/editor/interactions/adjustment.tspackages/slides/src/view/editor/overlay.ts
All P3-A.2 todo checkboxes flipped to done (the work landed across the 10 task commits + lessons commit on this branch). pnpm tasks:archive moves the todo/lessons pair into archive/2026/05/ and regenerates both task indexes. slides-shapes-p3a-adjustments.md's "Out-of-scope follow-ups" table strikes out the P3-A.2 row and notes PR #226 as the landing reference, so future readers of the design doc see immediately that the sweep is shipped rather than still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Summary
Completes the P3-A adjustment-UX workstream. PR #210 shipped yellow-diamond drag handles for the 9 pilot shapes (P3-A.1) and
slides-shapes-p3a-adjustments.mddeferred the remaining 24 shapes to a mechanical sweep — this is that sweep.After merge, every P1/P2 shape with an
ADJUSTMENT_SPECSentry (33 shapes total) renders with a draggable handle for each adjustment value, instead of being locked to defaults.Sweep coverage (24 shapes)
linearTopEdgeHandlefactory:triangle,parallelogram,trapezoid,hexagon,octagon,plus,pentagonArrow.roundRectretrofitted onto the same factory.can,donut.rightArrow,leftArrow,upArrow,downArrow,leftRightArrow. Per-direction math differed enough that a factory wouldn't have saved code.quadArrow(inline 3-handle): head length / head width / shaft thickness.pointTailHandlefactory:wedgeRoundRectCallout(tail + corner radius),wedgeEllipseCallout,cloudCallout. Pilot'swedgeRectCalloutretrofitted onto the same factory so corner-inset behaviour has a single source of truth.mathPlus,mathMinus,mathMultiply,mathEqual,mathDivide,mathNotEqual.mathPlusreuses the linear factory (geometrically identical tobasic/plus).P3-A.1 deferred follow-ups closed in T1
roundRectandwedgeRectCalloutso boundary adjustments never paint under a corner resize handle (later generalised intoinsetAlongAxis, used by every new handle).adjustmentLocalToWorld/adjustmentWorldToLocalextracted intointeractions/adjustment.ts. The overlay's paint-side rotation and the editor's drag-side inverse rotation now share one source of truth, with a 30°-rotated star5 round-trip test as the regression guard.radialStarHandlereads its star'sAdjustmentSpecinstead of hardcodingSTAR_MIN/STAR_MAX.New abstractions / plumbing
axisLabel?: stringonAdjustmentSpecfor multi-axis tooltips when thelastWordheuristic collides. After the sweep onlymathNotEqualpopulates it ("Bar thickness"vs."Slash thickness"collapse to"thickness"otherwise).formatAdjustmentsmoves fromeditor.tstointeractions/adjustment.tsso it's directly unit-testable.shapes/index.test.tsgrows two assertions: everyADJUSTMENT_SPECSentry has a matchingADJUSTMENT_HANDLESregistration, and the two maps have the same size. P3-A.2 is the invariant boundary.Visual baseline
shapes-adjustments-sweepscenario (6×4 grid of the 24 sweep shapes at OOXML defaults). Pilot scenario untouched so its baseline approval from PR Add adjustment drag handles for 9 pilot slides shapes (P3-A.1) #210 carries forward.pnpm verify:browser:docker:update.Test plan
pnpm verify:fast— green (full lint + every package's unit tests, ~764 tests)pnpm --filter @wafflebase/slides test— green (130 test files / 528 tests; includes 24 new shape*.handles.test.tssmoke tests, factory round-trip tests, rotated-frame paint↔hit-test invariant,formatAdjustmentsaxisLabel test, and the new registry consistency assertions)pnpm verify:browser:docker:update— green with 4 newshapes-adjustments-sweepbaselines + 4 modified composite full-harness baselinespnpm dev— author/edit each of the 24 sweep shapes, confirm the diamond drags, the tooltip shows readable values, and the live preview tracks the drag without lagNotes
docs/tasks/active/20260512-slides-shapes-p3a2-sweep-lessons.md. Highlights: factory consolidation matrix (when a shared factory earned its keep vs. when inline was clearer),mathMultiplyrotation gotcha (SQRT1_2 falls out of the 45° rotation matrix),verify-visual-browser.mjsscenarioIds whitelist (NOT auto-discovered fromslides-scenarios.tsx).Out-of-scope follow-ups
linearTopEdgeHandle,pointTailHandle,insetAlongAxis.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Tests
Documentation