Sprint: 10 Dates: 2026-10-28 - 2026-11-01 Sprint Goal: Let an operator select multiple canvas elements at once (rubber-band or modifier-click, mixing standalone elements and Address Controls) and align/distribute them together, while also finishing the epic-4 wrap/clip chain's live visual indicator — the first new selection model this designer has ever had, plus a small, low-risk finish to already-shipped work.
| Story | Size | Status | Tasks |
|---|---|---|---|
| Add a live wrap/clip indicator for text elements | 3 points | Done | - [x] Determine, per selected/previewed element with a box, whether the resolved text's natural (unwrapped) GDI+-measured size exceeds the box width (wrapping) and/or the wrapped height exceeds the box height (clipping) - [x] Draw a small visual indicator (dashed-orange-style, consistent with the existing unmapped-column warning) on TemplateCanvasControl and TemplatePreviewControl when either condition is true for the current sample record, drawn inside the existing per-element rotation transform so it rotates for free - [x] Unit tests for the wrap/clip detection logic (Desktop.Core, framework-free) - [x] Live built-form smoke: a narrow (120pt) boxed element's real canvas screenshot shows both genuine multi-line wrap and the dashed-orange indicator appearing correctly |
| Select multiple elements at once on the canvas | 5 points | Done | - [x] Add a multi-select data structure alongside the existing single-selection state (CanvasElementEditor for standalone elements, TemplateCanvasControl for Address Controls) rather than replacing it, so every existing single-select code path (properties panel, rotate/resize handles, line drill-in) keeps working unchanged when only one item is selected - [x] Rubber-band selection: a click-drag starting on empty canvas space (not on any element/handle) draws a selection rectangle and selects every element/control whose bounding box intersects it on release - [x] Modifier-click (Ctrl/Shift): adds or removes a single element/control from the current multi-selection without disturbing the rest - [x] Visible selection indication for every selected item, not just the primary one - [x] Dragging any selected item moves the entire multi-selection together, preserving each item's relative offset (delta-based movement, not absolute repositioning) - [x] Properties panel shows a clear “N items selected” state and disables per-item property editing while a multi-selection is active (Development Team's chosen simplest option from the story's own notes) - [x] Unit tests: rubber-band hit-testing, modifier-click toggle, multi-drag delta math (Desktop.Core) - [x] Live built-form smoke (touches the properties panel's multi-item behavior) |
| Align and distribute multiple elements | 5 points | Done | - [x] Alignment operations (left/right/top/bottom edges, horizontal/vertical centers) over the current multi-selection, using each item's existing bounding box (Address Control's Width/computed height, standalone elements’ measured or box width/height) - [x] Distribution operations (equal horizontal/vertical spacing) over the current multi-selection - [x] A toolbar affordance to trigger each operation, enabled only when 2+ items are selected, clearly disabled (not silently a no-op) otherwise - [x] Unit tests for the alignment/distribution math (Desktop.Core, framework-free) - [x] Live full built-form smoke with a fresh screenshot (this story adds a new toolbar control to TemplateDesignerForm, per the Sprint 7 retrospective's carry-in) |
logs/impediment_log.md. Neither blocks this sprint's committed items.Sequenced by dependency and risk: the small, independent indicator story first (quick, de-risks nothing else but clears easily), then the hard-dependent multi-select/align pair in order.
| Batch | Story | Why it's gated here |
|---|---|---|
| 1 | Add a live wrap/clip indicator for text elements | No dependency on anything else this sprint; small and independent. |
| 2 | Select multiple elements at once on the canvas | No dependency on Batch 1, but sequenced next as the foundational half of the epic-6 pair. |
| 3 | Align and distribute multiple elements | Strictly depends on Batch 2's multi-selection existing — there is nothing to align/distribute without it. |
| Day | Date | Completed | Planned | Blocked/At risk |
|---|---|---|---|---|
| 1 | 2026-10-27 | Batch 1 (“Add a live wrap/clip indicator for text elements”, 3 points) done, all ACs met. New framework-free WrapClipDetector (Desktop.Core) compares an element's natural (unwrapped) GDI+-measured width against its box width, and its GDI+-wrapped height against an explicit clip ceiling when one is set (never flags clipping otherwise, since there is no ceiling to exceed). Wired into TemplateCanvasControl.DrawElement and TemplatePreviewControl.DrawItem's existing box-mode branches, drawing a dashed-orange outline (the unmapped-column warning's visual language) inside the same rotation transform every other per-element visual already uses, so it orbits for free when rotated. Mid-batch, the human product owner raised two live-build issues, addressed together before continuing to Batch 2 (full detail in epics/02_template_designer_gui_foundation.md's and epics/04_live_preview_and_record_navigation.md's post-Sprint-9 correction notes): (1) no way to delete a placed element or Address Control — added CanvasElementEditor.RemoveSelected()/TemplateCanvasControl.RemoveSelectedElement(), wired to the Delete/Backspace key and a new toolbar button; (2) the resize handle should control only box width, font size set elsewhere — removed the Sprint 8 uniform-font-scale handle behavior entirely, ResizeDragTo now only ever sets Width, and TextElementLayout.HasBox/TemplateElement.HasBox were decoupled from Height (a new independent HasHeightClip flag) as a direct consequence, with DebenuPdfRenderer.AddPage gaining a “Width alone” native-wrap-no-clip render path (DrawWrappedText unrotated, GetWrappedText+DrawRotatedMultiLineText rotated). Tests: net +12 (8 obsolete font-scale tests removed, 20 new/updated across CanvasElementEditorTests, TextElementPropertiesEditorTests, TemplateLayoutXmlSerializerTests, TemplateXmlParserTests, DebenuPdfRendererWrapTests, plus the new WrapClipDetectorTests). Full suite 476/476 -> 488/488. Live-verified: a real, licensed end-to-end CLI render of a width-only (no height) template showed correct unrotated and rotated auto-height wrap with no clipping; a reflection-driven built-form harness confirmed a real mouse-driven resize drag sets only Width (Height/FontSize unchanged), the real Delete key removes the selected element, and a real canvas screenshot shows both genuine wrap and the new indicator on a narrow boxed element. One disclosed evidence-depth caveat: the toolbar Delete button's own PerformClick() didn't reliably fire in the synthetic harness (a known WinForms quirk absent a real shown window) — the underlying method was proven correct via a direct call, and the button's wiring matches every other proven-working toolbar button. |
Batch 2, “Select multiple elements at once on the canvas” (5 points, epic 6). | None. |
| 2 | 2026-10-27 | Ad-hoc regression fix, before starting Batch 2: user reported clicking on and moving a dynamic placeholder had become “not smooth any more.” Full detail in epics/02_template_designer_gui_foundation.md's new post-Sprint-10-Batch-1 note. Root cause, found by direct Stopwatch instrumentation of TemplateCanvasControl.OnMouseDown rather than guessing: SelectionChanged fired unconditionally on every left-click, including a click on an already-selected element (the normal way to start a drag), and TemplateDesignerForm's handler runs the full un-gated RefreshPropertiesPanel() every time that fires — a cost that only became perceptible (~40-60ms/click, measured) once this sprint's added property-panel rows made the refresh heavier. Fixed by only raising SelectionChanged when the pre-click and post-click selection identity actually differ. Live-verified with the same instrumented harness: an already-selected element's second click dropped from ~50ms to 0.295ms; a genuine selection change still pays the warranted refresh cost unchanged. Full suite re-run: Desktop.Core 364/364, CLI 124/124 (fix is isolated to the WinForms-only TemplateCanvasControl, which has no automated test project). |
Batch 2, “Select multiple elements at once on the canvas” (5 points, epic 6). | None. |
| 3 | 2026-10-27 | Batch 2 (“Select multiple elements at once on the canvas”, 5 points) done, all ACs met. Built as a strictly additive layer per the story's own conversation notes: CanvasElementEditor.MultiSelected (a HashSet<TextElementLayout>) alongside the existing Selected field, and a parallel TemplateCanvasControl._multiSelectedAddressControls for Address Controls, with a CollapseMultiSelectionIfSingular rule ensuring every existing single-select code path keeps working unchanged whenever 0 or 1 items end up selected. Rubber-band selection (CanvasElementEditor.ElementsInRect, rotation-aware via rotated-corner AABB, plus a parallel TemplateCanvasControl.AddressControlsInRect) and modifier-click toggle (ToggleMultiSelect, seeding from the current single selection) both replace or extend the multi-selection on release/click; group drag (BeginMultiDrag/MultiDragTo, snapping the shared delta once rather than each item's position independently) moves the whole set together preserving relative offsets; DrawAddressControl was split into separate isPrimarySelected (handles) and isMultiSelected (highlight-only) parameters so every selected item is visibly marked without ambiguous multi-item handles; the properties panel shows “N items selected” and disables per-item editing (Delete disabled too, rather than extended to a bulk operation — out of this story's scope). Tests: +12 (Desktop.Core, including a rotated-element ElementsInRect case and a snap-to-grid MultiDragTo case). Full suite 364/364 -> 376/376 (Desktop.Core; CLI unaffected at 124/124). Live-verified with a reflection-driven harness against the real built form: rubber-band selected exactly the 2 of 3 elements it should; the panel showed the disabled “2 items selected.” state; toggling a third item in and the first back out produced the exact expected membership; a group drag moved two elements by an identical measured delta with their relative offset exactly unchanged and the unselected third element untouched; clicking empty space cleared the selection; a real canvas screenshot confirmed every multi-selected item shows the highlight border. One disclosed evidence-depth caveat: Control.ModifierKeys has no public setter and queries live keyboard state, so modifier-click was verified by calling the same private toggle method the click handler delegates to, directly, rather than through a real simulated key-press. One pre-existing, unrelated cosmetic gap found (not fixed) during verification: a plain element's selection highlight border visibly wraps only part of a multi-word display string — confirmed present identically for ordinary single-selection, logged to logs/technical_debt_log.md (2026-10-27) rather than fixed in-scope. |
Batch 3, “Align and distribute multiple elements” (5 points, epic 6, depends on Batch 2's multi-selection now existing). | None. |
| 4 | 2026-10-27 | Batch 3 (“Align and distribute multiple elements”, 5 points) done, all ACs met — Sprint 10 committed scope now 13/13 points complete. New framework-free AlignmentCalculator (Desktop.Core) computes pure position deltas from a list of world-space bounding boxes: ComputeAlignmentDeltas (6 edges, relative to the selection's combined bounding box) and ComputeDistributionDeltas (equal center-to-center spacing, a documented MVP simplification vs. edge-to-edge gap spacing). TemplateCanvasControl feeds it a mixed bounds list built from a newly-public CanvasElementEditor.GetWorldBounds (standalone elements, reusing Batch 2's rotation-aware AABB) and a new GetAddressControlWorldBounds (Address Controls, extracted from Batch 2's AddressControlsInRect to remove a duplicate copy of the corner-rotation math), applying each returned delta as a plain X += Dx; Y += Dy. A new toolbar row (8 buttons: 6 alignment edges + 2 distribution axes) was appended to the existing element toolbar — it wraps to a second line on its own, no new form layout row needed — enabled only while 2+ items are multi-selected, refreshed from inside the existing RefreshSelectionLabel so it can never drift out of sync. Tests: +12 (AlignmentCalculatorTests, Desktop.Core, framework-free, covering all 6 edges, 3- and 4-item distribution including an out-of-order case, and the sub-3/empty no-op cases). Full suite 376/376 -> 388/388. Live-verified with a reflection-driven harness against the real built form: the toolbar correctly disabled at 0 and 1 selected, enabled at 2+; Align Left/Top moved a 3-element selection to the exact expected shared edge; Distribute Horizontal left both extreme elements’ centers unchanged and placed the middle element's center at the exact arithmetic midpoint; a fresh full-form screenshot (not reused from Batch 2, per this story's own conversation notes) confirmed the new toolbar row, its enabled state, and every selected item's highlight all render correctly together. |
Sprint Review and Retrospective (sprint's committed scope is now fully done). | None. |
Powered by TurnKey Linux.