namespace EnvelopeRenderer.Cli.Render; /// /// One `<text>` element from the template. Sprint 5, "Mix static text and CSV fields within /// a single text element": content is now an ordered sequence of s /// (literal-text runs and field-token runs), concatenated per record at render time — the /// pre-Sprint-5 "either pure static text or a single bound column" rule is simply the one-run /// special case of this same model (/ below are /// back-compat read-only projections for that one-run case, not separate storage), not a /// parallel code path that could drift from it. enforces "every /// run is exactly one of literal/field" at parse time. /// /// Sprint 4, "Collapse blank optional address lines consistently": /// when true and this element's resolved text is blank for a given record, the element /// is not drawn and every other element sharing its X position (an "address line stack" — see /// ) shifts upward to close the gap. Defaults to false /// so every template written before this story renders identically (a blank line simply prints /// nothing at its fixed position, exactly as before). /// Sprint 4, "Set a rotation angle for text and dynamic field elements": /// degrees, positive or negative, applied around the element's own bounding-box center (not its /// X/Y anchor). Defaults to 0 so every template written before this story renders /// unchanged. /// Sprint 9, "Add an adjustable width and height with text wrapping...": /// when set (together with ), the element's content wraps within this /// width instead of drawing as one unbounded line. null (the default) means every /// template written before this story renders exactly as before — no wrap, no width concept at /// all. /// Paired with : wrapped content that would exceed /// this height is clipped rather than growing the box or shrinking the text. Only meaningful when /// is also set; null is the pre-Sprint-9 default. public sealed record TemplateElement( double X, double Y, string FontName, double Size, IReadOnlyList Runs, bool Collapsible = false, double Angle = 0, int RenderOrder = 0, double? Width = null, double? Height = null) { /// True only when both and are set — a /// one-sided value (e.g. a width with no height) is treated as "no box" rather than guessing /// a missing dimension, since wrap-without-a-clip-boundary or clip-without-a-wrap-width both /// have no well-defined meaning for this story. public bool HasBox => Width is > 0 && Height is > 0; /// Back-compat convenience factory matching the pre-Sprint-5 "pure static text" /// shape — builds the equivalent one-literal-run list. (A second /// constructor overload was deliberately rejected here in favor of named factory methods: a /// positional/named-argument overload alongside the primary constructor's own /// Collapsible/Angle parameters is ambiguous to the compiler — CS1744 — for any /// caller using named arguments for either, which every Collapsible: true/ /// Angle: 45 call site in this codebase does.) public static TemplateElement Static( double x, double y, string fontName, double size, string text, bool collapsible = false, double angle = 0) => new(x, y, fontName, size, new[] { new TemplateTextRun(text, null) }, collapsible, angle); /// Back-compat convenience factory matching the pre-Sprint-5 "single bound column" /// shape — builds the equivalent one-field-run list. public static TemplateElement Dynamic( double x, double y, string fontName, double size, string columnName, bool collapsible = false, double angle = 0) => new(x, y, fontName, size, new[] { new TemplateTextRun(null, columnName) }, collapsible, angle); public bool IsDynamic => Runs.Any(r => r.IsField); /// True only for the legacy "exactly one field run, nothing else" shape — the only /// case that still has a single, unambiguous bound column (used by the render-time pre-flight /// column check's error-message grouping and by any code that only makes sense for a /// single-column binding). public bool HasSingleColumnRun => Runs.Count == 1 && Runs[0].IsField; /// Back-compat projection: non-null only for the legacy single-literal-run case. public string? StaticText => Runs.Count == 1 && !Runs[0].IsField ? Runs[0].Literal : null; /// Back-compat projection: non-null only for the legacy single-field-run case. public string? ColumnName => HasSingleColumnRun ? Runs[0].ColumnName : null; }