Sprint: 8 Dates: 2026-10-26 - 2026-10-30 Sprint Goal: Ship whole-Address-Control rotation end-to-end — settable via the properties panel and adjustable by dragging a canvas handle — staying record-stable and visually consistent across the editing canvas, the Sprint 7 live preview, and the real CLI-rendered PDF, directly delivering the human product owner's reversal of the Sprint 6 Review's “whole-control rotation remains out of scope” call.
| Story | Size | Status | Tasks |
|---|---|---|---|
| Rotate the whole Address Control as a single unit | 8 points | Done | - [x] Enable and wire the properties panel's existing -360.0/360.0, 0.1-precision angle input for a selected Address Control (currently disabled for that selection) - [x] Add a point-rotation-around-a-pivot helper to EnvelopeRenderer.Cli's RenderEngine (no equivalent exists there today) and reuse the same formula on the desktop side (extracted to a new shared PointRotation helper, since CanvasElementEditor.RotatePointAroundPivot was private) so each visible line's anchor is rotated around the control's authored, unrotated box center before being handed to the existing per-line draw call — no DebenuPdfRenderer changes needed - [x] Apply the same rigid-group rotation to TemplateCanvasControl.DrawAddressControl (box, every line's text, and any per-line highlight/selection overlay together, via a GDI+ transform around the box center) and to TemplatePreviewBuilder's address-control counterpart - [x] Port a rotated-rectangle test to TemplateCanvasControl.HitTestAddressControl (and HitTestAddressResizeHandle, plus the resize-drag math itself) so a rotated control stays correctly click-selectable/resizable at its rotated position - [x] Confirm the pivot is always computed from the control's authored X/Y/Width/Height, never from any record's AddressLineCollapser-shifted line positions — regression tests with two records (one collapsing a blank line, one not) assert an identical pivot, both CLI and desktop - [x] Persist a new angle attribute on <addressControl> (default 0) in TemplateLayoutXmlSerializer/TemplateXmlParser; confirmed pre-existing templates (no attribute) render unchanged - [x] Unit tests: rigid rotation math (CLI and desktop), collapse/pivot-stability regression, persistence round-trip (hit-test itself lives in the WinForms Views project and is covered by the live smoke below, not a unit test — see new technical-debt log entry) - [x] Live actual built-form smoke showing a rotated Address Control matching across the canvas, the live preview, and a real CLI render of the same template |
| Rotate the whole Address Control by dragging a handle on the canvas | 5 points | Done | - [x] Add a control-specific handle-position/hit-test/rotate-drag path for the Address Control, paralleling (not reusing as-is) CanvasElementEditor's existing TextElementLayout-only handle methods — implemented as a new framework-free AddressControlRotateHandle class (Desktop.Core) so it is unit tested, computed around the control's box center established by the prior story - [x] Paint the rotate-handle glyph for a selected Address Control in TemplateCanvasControl (visual style consistent with the standalone-element handle — same SeaGreen dot/dashed line/white outline, same HandleOffsetPoints/HandleHitRadiusPoints constants reused for consistency) - [x] Keep the properties panel's numeric angle field and the canvas handle synchronized in both directions (drag updates the number live; typing a number moves the handle) — free from Batch 1's existing wiring, confirmed live - [x] Confirm dragging the handle persists the resulting angle the same way a typed value does, and does not disturb line-drill-in editing or the control's existing whole-unit move/resize behavior - [x] Unit tests for handle position/hit-test/drag-angle math ( AddressControlRotateHandleTests) - [x] Live actual built-form smoke of a real handle drag rotating the control, synced with the properties panel, in the built .exe — fresh screenshots (not reused from Batch 1), per the Sprint 7 retrospective carry-in |
CanvasElementEditor, TemplateCanvasControl, TemplateDesignerForm). Concretely: this sprint's drag-handle story adds a new, Address-Control-specific handle/hit-test/rotate-drag path in CanvasElementEditor, while the multi-select story reworks CanvasElementEditor.Selected from a single reference into a genuinely new multi-item selection model (confirmed by code inspection at 2026-10-16 refinement: no multi-select concept exists anywhere today) — a foundational, first-of-its-kind interaction rework in the exact same class, with the exact same character of risk this team has consistently avoided stacking alongside another large/novel story in one sprint (the same reasoning “Undo and redo layout changes”‘s own sizing note gives for staying solo). Sequencing both in the same sprint, even in separate batches, risks one story's in-progress selection/handle-model change complicating the other's, beyond what the point totals alone would suggest. No external date or contractual pressure was cited anywhere in this backlog to justify reaching for a new, unproven high purely to keep capacity from going idle.epics/01_end_to_end_text_rendering_slice.md, 5 pts, labeled Ready) is a placeholder from the very first, 2026-09-04 onboarding refinement, never re-verified since — the same kind of staleness that made epic 4's “Ready” label wrong at the 2026-10-16 refinement (it predates rotation, mixed content, the Address Control, and the live preview, all of which likely already satisfy this story's acceptance criteria as a byproduct). Pulling it without a real re-verification pass would repeat that exact mistake rather than learn from it, so it is left out and flagged below for product-owner attention rather than blindly committed. “Undo and redo layout changes” (13 pts) is both too large to be a “rounding out” item and its own sizing note already recommends against pairing it with another large/novel story. Nothing else in the backlog is currently Ready.backlog/backlog.md's order. Reasoning: it is a direct, recent, explicit human-product-owner request reversing a scope call the same human product owner made only one sprint ago (Sprint 6 Review), and it is exactly what this sprint commits to — keeping the backlog's stated order and the sprint's actual commitment consistent, rather than committing one thing while the backlog implies another is more urgent.logs/impediment_log.md. Neither blocks this sprint's committed items.Sequenced by feature dependency (only two committed stories this sprint, hard-dependent on each other).
| Batch | Story | Why it's gated here |
|---|---|---|
| 1 | Rotate the whole Address Control as a single unit | No dependency on anything else. Delivers the underlying RotationAngle property, persistence, and rigid-group rotation/pivot math that the drag-handle story below needs. |
| 2 | Rotate the whole Address Control by dragging a handle on the canvas | Strictly depends on Batch 1's angle property, persistence, and render/pivot logic — adds only the interactive handle and its live drag behavior on top. |
| Day | Date | Completed | Planned | Blocked/At risk |
|---|---|---|---|---|
| 1 | 2026-10-26 | Batch 1 (“Rotate the whole Address Control as a single unit”, 8 points) done, all 6 ACs met. Added AddressControlLayout.RotationAngle/BoxCenter (desktop) and TemplateAddressControl.RotationAngle/Height/BoxCenter (CLI) — independently-implemented parity, per this project's established CLI/desktop split — confirming by direct inspection that both Height (line font sizes + LineSpacingMultiplier) and Width (author-set) never depend on any record's resolved text, so BoxCenter is a pure function of authored geometry alone. Added RenderEngine.BuildAddressControlDraws’ own rotate-point-around-pivot helper (no equivalent existed in EnvelopeRenderer.Cli before this story) and a new shared EnvelopeRenderer.Desktop.Core.Design.PointRotation helper on the desktop side (extracted since CanvasElementEditor.RotatePointAroundPivot was private to that class) reused by TemplatePreviewBuilder and TemplateCanvasControl. Each visible line's per-record (collapse-adjusted) anchor is rotated as a rigid group around the fixed BoxCenter pivot, then handed to the existing fixed-pivot TextDraw/PreviewTextDraw path exactly as a rotated dynamic/mixed standalone element already does — confirmed zero DebenuPdfRenderer changes were needed. TemplateCanvasControl.DrawAddressControl now wraps its whole unrotated paint call (box, lines, highlight fills, selection border, and the pre-existing resize handle) in one GDI+ rotation transform around BoxCenter, so everything rotates together automatically; HitTestAddressControl and HitTestAddressResizeHandle (plus the resize-drag math itself, a correctness fix beyond the story's literal AC list, found while implementing the rotated hit test) now rotate the click point backward into local space before testing, so a rotated control stays correctly click-selectable and resizable at its real on-screen position. Persistence: new optional angle attribute on <addressControl> (default 0) in both TemplateLayoutXmlSerializer and TemplateXmlParser, following the exact convention <text>'s own angle already uses; TEMPLATE_FORMAT.md updated. Tests: 20 new (6 CLI — RenderEngineTests/TemplateXmlParserTests — and 14 desktop — AddressControlLayoutTests, new PointRotationTests, TemplateLayoutXmlSerializerTests, TemplatePreviewBuilderTests), including explicit pivot-stability regression tests (two records, one collapsing a blank line, one not, asserting an identical rotated position for the unaffected first line) on both CLI and desktop. Full suite 402/402 -> 422/422 (301->315 desktop, 101->107 CLI). Live verification: real CLI render of a 20-degree-rotated copy of sample-envelope-template3.xml against the real 392-record sample CSV (exit 0, all 392 pages) — confirmed via direct PDF content-stream inspection that the rotated render emits real cos(20)/sin(20) rotation matrices with distinct per-line translations (proving each line rotated individually as part of one rigid group), while the same template with no angle attribute produces zero rotation matrices at all (proving the zero-angle path is untouched). Built the desktop .exe and drove the real TemplateDesignerForm/TemplateCanvasControl/TemplatePreviewControl via a reflection-driven harness (same technique as Sprint 7): loaded the rotated template, screenshotted canvas + preview + full form showing the identical 20-degree rotation on both surfaces; simulated real mouse clicks through the actual OnMouseDown/OnMouseUp handlers proving the rotated control is selectable at its real (forward-rotated) position and correctly NOT selectable at its old un-rotated position; typed 65 into the real properties-panel _angleInput control and confirmed the canvas/preview both live-updated to the new angle; saved and reopened the template, confirming the panel-driven angle round-tripped through XML. New technical debt logged (Low impact): the pre-existing (Sprint 6) Address Control hit-test/move/resize logic still lives directly in the WinForms TemplateCanvasControl rather than a CanvasElementEditor-equivalent, so it remains smoke-tested only, not unit tested — this sprint's new rotation math was deliberately extracted to unit-testable Desktop.Core classes, but retroactively refactoring the older code was out of scope. |
Begin Batch 2 (“Rotate the whole Address Control by dragging a handle on the canvas”), which depends on this batch's angle property, persistence, and render/pivot logic. | None. |
| 1 | 2026-10-26 | Batch 2 (“Rotate the whole Address Control by dragging a handle on the canvas”, 5 points) done, all 5 ACs met, bringing Sprint 8 to 13/13 points Done. Added EnvelopeRenderer.Desktop.Core.Design.AddressControlRotateHandle — a new, framework-free, unit-tested class (mirroring CanvasElementEditor's per-element handle math but implemented fresh against the control's own box geometry, since no equivalent existed for a selected Address Control) providing LocalPosition/WorldPosition/HitTest/RotateDragTo, reusing CanvasElementEditor.HandleOffsetPoints/HandleHitRadiusPoints directly for exact visual/hit-test consistency with the standalone-element handle (the story's own recommended default). TemplateCanvasControl paints the handle (same SeaGreen-dot/dashed-line/white-outline style as DrawRotateHandle) inside the same rotation transform DrawAddressControlUnrotated already runs under, so it automatically orbits with the box; a new _isRotatingAddressControl drag-gesture flag parallels the existing _isResizingAddressControl one in OnMouseDown/OnMouseMove/OnMouseUp. The properties panel's angle field and the canvas handle were already bidirectionally wired by Batch 1's RefreshPropertiesPanel/SetSelectedRotationAngle plumbing, so no new sync code was needed — confirmed live rather than assumed. Tests: 11 new (AddressControlRotateHandleTests — local/world position, hit-test including a rotated-vs-unrotated-position discrimination case, rotate-drag-to-angle math, pivot-exactly-under-pointer no-op, and a bidirectional angle-drives-position check). Full suite 422/422 -> 433/433 (315->326 desktop, 107 CLI unchanged). Live verification: built the desktop .exe and, via the same reflection-driven harness, added a fresh Address Control interactively (AddAddressControl(), not loaded from XML), screenshotted it unrotated with both handles visible; grabbed the real rotate handle via a simulated OnMouseDown at its actual computed world position, dragged mid-gesture to a point 90 degrees around the pivot (screenshotted before mouse-up, confirming ~89.4 degrees live in both the model and the real _angleInput control — small deviation from the harness's pixel-rounding, not a model precision issue) and confirmed the drilled-in address line index (1) was unchanged mid-drag; released the drag and confirmed the angle persisted; typed -45 into the real Angle field afterward and confirmed the handle/box moved to match; confirmed the control's existing whole-unit move still worked correctly afterward (X+20/Y-20 within the same pixel-rounding tolerance). Took a fresh full-form screenshot for this batch (not reused from Batch 1), per the Sprint 7 retrospective carry-in explicitly reinforced in this sprint's own plan. One test-design note, not a product defect: a later step in the same harness run clicked the control's body to test whole-unit move, which (correctly, by pre-existing Sprint 6 design) re-picked the drilled-in line under that click point — unrelated to the rotate-drag itself, which the mid-drag check already separately confirmed does not disturb line selection. |
Sprint 8 complete — move to product-owner Sprint Review. | None. |
Powered by TurnKey Linux.