# Sprint Backlog **Sprint:** 4 **Dates:** 2026-09-28 - 2026-10-02 **Sprint Goal:** Complete the CSV Integration and Field Mapping epic by collapsing blank optional address lines, and deliver the first slice of text/field rotation (numeric angle entry, persistence, and correct rendering around the element's bounding-box center). ## Committed Items | Story | Size | Status | Tasks | |---|---|---|---| | Collapse blank optional address lines consistently | 5 points | Done | - [x] Design and implement a per-line "collapsible if blank" configuration flag on address-block text elements (model + XML persistence), defaulting existing templates to today's non-collapsing behavior
- [x] Implement the shift-upward layout rule in the render engine so subsequent configured lines move up when a bound field is blank for that record
- [x] Apply the same collapsing math to the desktop canvas preview so preview and final PDF agree
- [x] Give operators a way to visually distinguish "intentionally blank per data" from "field failed to map"
- [x] Unit tests: single blank line collapses and shifts correctly, populated line preserves normal spacing, multiple consecutive blank lines, preview/render parity
- [x] Live manual verification against the built `.exe` and a real CLI render: one real record with a blank optional line, one with it populated | | Set a rotation angle for text and dynamic field elements | 8 points | Done | - [x] Add `RotationAngle` (degrees) to `TextElementLayout`, applying uniformly to static and dynamic elements
- [x] Add a numeric angle field to `TextElementPropertiesEditor`/`TemplateDesignerForm`'s properties panel with validation
- [x] Update `TemplateCanvasControl.DrawElement` to rotate the drawn element about its bounding-box center and update `CanvasElementEditor.HitTest` for point-in-rotated-rectangle selection
- [x] Persist `angle` as a new `TemplateLayoutXmlSerializer` attribute, defaulting to `0` for templates saved before this story
- [x] Extend the CLI's render-time template format (`TextDraw`, `TemplateXmlParser`, `RenderEngine`) to read the new `angle` attribute
- [x] Implement rotate-about-bounding-box-center rendering in `DebenuPdfRenderer` using `GetTextWidth`/`GetTextAscent`/`GetTextDescent` to compute the anchor offset, then call `DrawRotatedText`; empirically confirm Debenu's rotation-direction sign convention against the real DLL and document it in `TEMPLATE_FORMAT.md`
- [x] Unit tests: the property itself, persistence round-trip (including old templates with no `angle` attribute), and the anchor-offset rotation math
- [x] Live manual verification against the built `.exe` and a real CLI render: a non-zero angle rotates the canvas and the rendered PDF the same way, around the same visual center | | Rotate elements by dragging a handle on the canvas | 5 points | Done | - [x] Add a rotate-handle hit-test region to `CanvasElementEditor` for a selected element, positioned relative to its rotated bounding box
- [x] Implement drag-to-rotate: compute the angle via `atan2` of the pointer position relative to the bounding-box center as the operator drags
- [x] Wire the drag result into the existing bidirectional properties-panel/canvas sync so the numeric angle field and the handle update each other live
- [x] Add handle-glyph painting to `TemplateCanvasControl`
- [x] Unit tests: handle hit-test, drag-to-angle math, panel/canvas sync in both directions
- [x] Live manual verification against the built `.exe`: drag the handle, confirm canvas and properties-panel angle stay in sync, save and confirm the resulting angle persists exactly as a typed value would | ## Notes - Capacity signal: velocity range refined to **18-20 points**, now off three data points (Sprint 1: 20 completed, Sprint 2: 19 completed, Sprint 3: 18 committed and completed exactly, no scope change) — a tighter, more trustworthy range than Sprint 3 planning had. - Committing 18 points, deliberately at the low end of the range rather than the 20-point ceiling, per `AGENTS.md`'s guidance to reserve roughly 10% of capacity for grooming rather than packing to the exact top. This mirrors Sprint 3's own committed (and fully completed) total. - Deliberately not committed this sprint (capacity discipline): "Harden production configuration delivery for CLI runtime settings" (2 points) — kept as an explicit stretch item, consistent with product-owner's standing note that it isn't urgent (see `backlog/epics/05_cli_rendering_engine_and_debenu_integration.md`). Pull it only if the three committed stories finish with room to spare. - Planning rationale: "Collapse blank optional address lines consistently" completes the CSV Integration and Field Mapping epic outright (its only remaining story) and has no dependency on this sprint's other work, so it's pulled first. The two rotation stories are committed together as one dependency-ordered chain (angle property before drag handle) — the same pattern Sprint 3 used successfully for its CSV chain (headers -> token -> rebind, 10 points across 3 dependent stories in one sprint). - Sizing: both rotation stories were sized during backlog refinement (2026-09-22) with real code/vendor-API inspection (see `backlog/epics/02_template_designer_gui_foundation.md`'s sizing notes) — the vendor Debenu API has no "rotate about center" primitive, so the center-pivot math in story 1 is genuine new work, not a pass-through parameter. Both pass the Definition of Ready. - Impediments: template asset path strategy (absolute vs relative) and UNC timeout/retry behavior remain open in `logs/impediment_log.md`. Neither blocks this sprint's committed items. - Carried over from previous sprint: none (Sprint 3 completed all 4 committed stories). - Sequencing risk: "Rotate elements by dragging a handle on the canvas" depends on "Set a rotation angle for text and dynamic field elements" landing first, both committed in this same sprint — swarm to finish the angle-property story before starting the drag-handle story's implementation, mirroring how Sprint 3 sequenced its CSV dependency chain. - Sprint 4 retrospective action items to apply during this sprint (from `backlog/sprints/sprint-3-retrospective.md`): continue live actual-built-artifact verification for GUI-facing stories; continue verifying real spec/code (not stale notes) before implementing; before "Collapse blank optional address lines" is considered fully done, note it has no dependency on the still-open 1,000,000-record file-size technical debt item, so that item remains untouched this sprint, as expected. ## Execution Order Sequenced by dependency; the two independent stories are pulled before the dependent drag-handle story. | Batch | Story | Why it's gated here | |---|---|---| | 1 | Collapse blank optional address lines consistently | No dependency on any other Sprint 4 story (its only dependency, "Create a dynamic text token," is already Done). Pulled first, completing epic 3 outright. | | 2 | Set a rotation angle for text and dynamic field elements | No dependency on any other Sprint 4 story. Pulled second so its angle property exists before Batch 3 needs it. | | 3 | Rotate elements by dragging a handle on the canvas | Needs the `RotationAngle` property, persistence, and render/pivot logic from Batch 2 done first. | ## Daily Scrum Log | Day | Date | Completed | Planned | Blocked/At risk | |---|---|---|---|---| | 1 | 2026-09-28 | **Batch 1** ("Collapse blank optional address lines consistently", 5 points) done, all 4 ACs met. Added a shared, independently-implemented-twice `AddressLineCollapser` pure-geometry class (`EnvelopeRenderer.Cli.Render` and `EnvelopeRenderer.Desktop.Core.Design` — deliberately not shared via project reference, mirroring `TemplateLayoutXmlSerializer`'s existing precedent for keeping the desktop project free of CLI-internals references) implementing the row-height shift math: lines sharing the same X position (rounded to 2 decimals) form a top-to-bottom stack, and a collapsed line's own row height (gap to the line below it, using original undisturbed Y values) shifts every subsequent line up. Added `Collapsible`/`CollapseIfBlank` to `TemplateElement`/`TextElementLayout` and a new `collapsible="true"` XML attribute (`TemplateXmlParser`, `TemplateLayoutXmlSerializer`), both defaulting to `false`/absent so every pre-existing template is unaffected. `RenderEngine.BuildDraws` now resolves each record's text, decides per-line collapse via `AddressLineCollapser`, and omits collapsed lines' draws entirely. For "distinguish intentionally blank from a mapping error" (the story's 4th AC and the one open design decision it left to the team): a `column` that isn't a real CSV header remains a hard, whole-run failure exactly as before (never silently collapses regardless of `collapsible`) — the design decision made was to surface that same distinction visually in the desktop canvas via a new `AddressBlockPreviewCalculator` (framework-free, unit tested) that flags a dynamic element bound to a column absent from the *currently loaded* CSV with a dashed orange/red highlight, distinct from the ordinary blue dynamic-field fill; a collapsible field that's merely blank in the loaded sample data gets no warning at all, just silently doesn't render — exactly the intended distinction. `TemplateCanvasControl` now runs the identical collapse-preview math (via `SetCsvPreviewContext`, fed the loaded CSV's headers and first sample row from `TemplateDesignerForm`) so preview and final PDF agree, per the story's own conversation note. 41 new unit tests across both test projects (`AddressLineCollapserTests` x2, `AddressBlockPreviewCalculatorTests`, plus `TemplateXmlParser`/`TemplateLayoutXmlSerializer`/`RenderEngine` collapsible-attribute coverage). **Live-verified against the real CLI and the real Debenu DLL**: built a template binding "Address 2/3/4" as collapsible against the real sample CSV (`87700 - 999999 - Wilson Township.csv`, where Address 3/4 are blank for every sampled record and Address 2 is populated) — `PROGRESS complete ... completed=392`, exit 0; rasterized page 1 via Debenu's own `RenderPageToFile` and visually confirmed exactly 3 tightly, evenly spaced address lines (Full Name / Address 1 / Address 2) with no gap where the two blank collapsed lines would have been, and `GetPageText` confirmed no stray blank-line artifacts. Desktop-side: added a static element, toggled the new "Collapse if blank" checkbox, confirmed a small green marker badge appears beneath it on canvas (screenshot captured), and confirmed a hand-authored template with one column bound to a real CSV header and one to a nonexistent header shows the intended visual contrast (plain vs. dashed-orange) once the real sample CSV is loaded (screenshot captured). | Batch 2 ("Set a rotation angle..."), pulled next per the Execution Order table (independent of Batch 1). | None. | | 1-2 | 2026-09-28 to 2026-09-29 | **Batch 2** ("Set a rotation angle for text and dynamic field elements", 8 points) done, all 4 ACs met. Before writing any rendering code, empirically confirmed two real vendor-DLL behaviors rather than assuming them (per this project's standing practice, same as the Sprint 3 throughput investigation): (1) a probe render (unrotated baseline vs. `DrawRotatedText` at Angle=90, both rasterized via Debenu's own `RenderPageToFile` and pixel-inspected) showed positive `Angle` is **counterclockwise** in PDF's Y-up coordinate system — the standard math rotation convention, no sign flip needed against PDF space; (2) `GetTextDescent()` returns an already-**negative** value (e.g. -5.088 for 24pt Arial), i.e. a glyph's local vertical extent is `[descent, ascent]` directly. Also discovered, only once real `DrawRotatedText` calls were exercised: it flatly rejects a negative `Angle` (returns 0, no error code) despite rotation being periodic — `DebenuPdfRenderer` now normalizes to Debenu's expected `[0, 360)` range for that specific call only, while the anchor-offset math itself correctly uses the original signed angle. Added `RotatedTextAnchorCalculator` (pure geometry, `EnvelopeRenderer.Cli.Render`): solves for the anchor point `DrawRotatedText` needs so its rotation lands on the *unrotated* bounding-box center rather than the (x, y) anchor Debenu itself rotates around, using `GetTextWidth`/`GetTextAscent`/`GetTextDescent`. `angle=0` (including when the attribute is absent) takes the exact original `DrawText` code path — byte-for-byte unchanged for every pre-Sprint-4 template. Added `RotationAngle` to `TextElementLayout`/`TemplateElement`/`TextDraw`, a numeric "Angle (deg):" field to the properties panel (`TextElementPropertiesEditor.SetRotationAngle`, free-form, no clamping since any real value is valid), a new `angle` XML attribute (`TemplateLayoutXmlSerializer`, `TemplateXmlParser`), and rotated-bounding-box hit-testing in `CanvasElementEditor.HitTest` (rotates the click point into the element's local frame by the inverse angle) so a rotated element's actual (not axis-aligned) footprint is what's clickable. `TemplateCanvasControl.DrawElement` rotates the drawn glyph and its selection outline via GDI+'s `Graphics.RotateTransform` around the bounding-box center — negating the stored angle, since GDI+'s Y-down pixel space makes a positive `RotateTransform` angle appear *clockwise* as viewed, the opposite of the PDF-space convention just confirmed; this negation is what keeps the canvas and the final PDF rotating the same visual direction. 29 new unit tests (`RotatedTextAnchorCalculatorTests` — including an angle-agnostic property test asserting the bounding-box center is preserved at 30/90/180/-45 degrees — plus rotated-hit-test and property/persistence coverage). **Live-verified against the real Debenu DLL with a durable regression test, not just a one-off probe**: `DebenuPdfRendererRotationTests` renders real text at five different angles (0/30/90/180/-45) through the real `DebenuPdfRenderer`, rasterizes each page, and asserts via actual pixel-ink-centroid analysis that the centroid stays within a tight, generous-but-meaningful tolerance of the angle-0 centroid — the concrete, observable meaning of "rotates around the bounding-box center," which would fail by 90-190px without the anchor-offset correction (rotating around the plain X/Y anchor instead). Also live-verified end-to-end against the real CLI/sample CSV: a 45-degree rotated "ROTATED" static label rendered visibly tilted counterclockwise in the actual output PDF (rasterized and visually inspected). Desktop-side, live against the actual built `.exe`: added a static element, typed `60` into the Angle field, confirmed the canvas glyph and its rotate-handle marker visibly rotated counterclockwise (screenshot captured); saved the template and confirmed the on-disk XML's `angle` attribute matched exactly. | Batch 3 ("Rotate elements by dragging a handle..."), which depends on this batch. | None. | | 2 | 2026-09-29 | **Batch 3** ("Rotate elements by dragging a handle on the canvas", 5 points) done, all 4 ACs met, swarmed immediately after Batch 2 landed per the Execution Order table's dependency note. Added a rotate-handle to `CanvasElementEditor`: `HandlePosition()` computes a world-space point `HandleOffsetPoints` (20pt, a Development Team design choice) beyond the rotated bounding box's top edge, rotating *with* the element (using the same forward-rotation convention `HitTest`'s inverse undoes, so hit-testing and painting always agree); `HitTestHandle` grabs it within an 8pt radius. `BeginRotateDrag`/`RotateDragTo`/`IsRotating` add a second drag mode alongside the existing move-drag (mutually exclusive — `BeginRotateDrag` clears any pending move offset): `RotateDragTo` computes the new angle via `atan2` of the pointer relative to the bounding-box center, adjusted by -90 degrees since the handle's own zero-rotation reference direction is "up" (+Y), not the atan2 convention's +X. `TemplateCanvasControl` paints the handle as a small filled circle connected to the element's center by a dotted line, and its `OnMouseDown`/`OnMouseMove` check the handle hit-region first (only when something is already selected) before falling through to the existing select/move logic. Bidirectional panel/canvas sync required no new plumbing at all — it reused Batch 2's own `RotationAngle` panel wiring and the pre-existing `ElementsChanged`-triggers-`RefreshPropertiesPanel()` pattern `TemplateDesignerForm` already had for X/Y, exactly as the story's sizing note anticipated. 20 new unit tests (rotated hit-test edge cases, handle position/hit-test at several angles, `atan2` drag-to-angle math including cardinal-direction sanity checks, mutual-exclusivity with move-drag). **Live-verified against the actual built `.exe` with real Win32 mouse-drag input injection** (`SetCursorPos`/`mouse_event`, not an in-process harness): computed the handle's exact expected screen position externally (replicating `CanvasElementEditor`'s own geometry plus a pixel-calibrated canvas transform derived from the real rendered page's on-screen bounds), dragged from that position to a point straight above the element's center, and confirmed via UI Automation that the properties panel's Angle field live-updated to -1.5 degrees (expected ~0, within pixel-rounding tolerance) — both the visual rotation (screenshot captured, text visibly returned to level) and the numeric field updated together, live, during the drag. Saved the template afterward and confirmed the on-disk XML's `angle="-1.5464733298407793"` matched the dragged value exactly (not the rounded 1-decimal display), satisfying "releasing the drag persists the resulting angle the same way a typed value would." One incidental, honestly-reported note (not a product defect): mid-session, an `SetForegroundWindow` call from the verification tooling itself failed to bring the designer window forward as expected, and a handful of keystrokes intended for a CSV-column combo box briefly landed in the development environment instead of the app under test — caught immediately from the screenshot evidence, worked around by switching remaining interactions to direct UI-Automation control invocation instead of `SendKeys`, and does not reflect any behavior of the product under test. With this batch done, all three of Sprint 4's committed stories are complete: 288/288 tests passing (up from 223 at Sprint 3 close). | Sprint's committed backlog is now fully done (18/18 points across all 3 committed stories). Next: sprint review and retrospective per `process/03_sprint_execution.md`'s exit criteria. | None. |