|
- namespace EnvelopeRenderer.Cli.Render;
-
- /// <summary>
- /// Pure geometry for Sprint 4's "Collapse blank optional address lines consistently": given a
- /// set of text lines (each with an X/Y position and whether it should collapse for this
- /// particular record), decides which lines are visible and where the visible ones land after
- /// closing the gaps left by any collapsed lines above them.
- ///
- /// <para><b>Grouping rule (a Development Team design decision — the template format has no
- /// explicit "address block" concept):</b> lines that share the same X position (rounded to 2
- /// decimal places, matching the precision the designer's properties panel and XML persistence
- /// already use) are treated as one vertical stack — i.e. one address block — ordered top-to-
- /// bottom by descending Y (PDF points increase upward, so the top line of a block has the
- /// highest Y). Lines at a different X never affect each other's collapsing math. This mirrors
- /// how a real address block is authored today: every line in it shares the same left margin.</para>
- ///
- /// <para><b>Shift math:</b> for a stack ordered top-to-bottom, line k's "row height" is defined
- /// as the gap between it and the line immediately below it, i.e. <c>Y[k] - Y[k+1]</c> (using the
- /// stack's original, undisturbed Y values — never an already-shifted one, so consecutive
- /// collapsed lines don't compound rounding error). When line k collapses, every line below it in
- /// the same stack shifts upward by that row height. The last line in a stack has no line below
- /// it, so its own row height is irrelevant (nothing shifts if it collapses) — a deliberate
- /// no-op, not a bug.</para>
- /// </summary>
- public static class AddressLineCollapser
- {
- /// <summary>One input line: its original template position and whether this particular
- /// record's resolved value makes it collapse (i.e. the caller has already combined "is this
- /// element configured as collapsible" with "is its resolved text blank for this record").</summary>
- public readonly record struct Line(double X, double Y, bool ShouldCollapse);
-
- /// <summary>One resolved line, same order/count as the input. <see cref="Visible"/> is
- /// <c>false</c> exactly for lines whose own <see cref="Line.ShouldCollapse"/> was true — the
- /// caller should skip drawing those. <see cref="EffectiveY"/> is the Y every line (visible or
- /// not) should be treated as occupying after any collapsed lines above it in its stack have
- /// closed their gaps.</summary>
- public readonly record struct Resolved(bool Visible, double EffectiveY);
-
- /// <summary>Rounding precision (decimal places) used to decide whether two lines share the
- /// same X position and therefore belong to the same address-line stack. 2 decimal places
- /// matches the precision already used by the designer's XML persistence and properties
- /// panel, so two lines placed at "the same" X via the UI reliably group together even after a
- /// save/reload round trip through floating point.</summary>
- private const int GroupingPrecision = 2;
-
- public static IReadOnlyList<Resolved> Resolve(IReadOnlyList<Line> lines)
- {
- var result = new Resolved[lines.Count];
-
- var groups = lines
- .Select((line, index) => (Line: line, Index: index))
- .GroupBy(t => Math.Round(t.Line.X, GroupingPrecision));
-
- foreach (var group in groups)
- {
- var stack = group.OrderByDescending(t => t.Line.Y).ToList();
- var cumulativeShift = 0.0;
-
- for (var i = 0; i < stack.Count; i++)
- {
- var (line, index) = stack[i];
- result[index] = new Resolved(!line.ShouldCollapse, line.Y + cumulativeShift);
-
- if (line.ShouldCollapse && i < stack.Count - 1)
- {
- var nextY = stack[i + 1].Line.Y;
- cumulativeShift += line.Y - nextY;
- }
- }
- }
-
- return result;
- }
- }
|