Files
marekandClaude Opus 5 1e5a552c13 Oprava: exportovaná šrafa šla ven bez definice vzoru a ve špatném měřítku
Nahlásil uživatel: šrafa v exportovaném DXF nedrží měřítko, takže na první
pohled ani nevypadá jako šrafa, a v každém výkresu se chová jinak.

Dvě příčiny:

1. Do výstupu šlo jen *jméno* vzoru (ANSI31), bez definičních čar — ověřeno
   na exportovaném souboru: definicnichCar=0. DXF přitom nese definici vzoru
   uvnitř entity HATCH; bez ní si ji cílový CAD dosadí ze svých .pat, ve svých
   jednotkách a měřítku. Odtud „v každém výkresu jinak". Vypln proto nese
   VzorCara (úhel, vztažný bod, rozteč, čárkování) a export ji zapisuje.

2. Vzor se neškáloval spolu s geometrií. Logo se do rámečku rozpisky vejde jen
   zmenšené (typicky ~0,1×), ale rozteč vzoru zůstávala původní — v CADu z toho
   byla buď plná plocha, nebo pár čar přes celé logo. TransformRenderer teď
   mapuje i definici: vztažný bod celou transformací, rozteč jako vektor (bez
   posunu), délky čárek a měřítko × měřítko transformace.

Měřítko a úhel se do HATCH zapisují neutrální (1 a 0) a efektivní hodnoty nese
definice. ACadSharp totiž při zápisu násobí rozteč hodnotou PatternScale, ale
při čtení zpátky nedělí — s nenulovým měřítkem by se vzor s každým kolečkem
desetkrát roztáhl (změřeno).

Po opravě je rozteč v exportu shodná se zdrojem 1:1 a při zmenšení loga 0,1×
klesne desetkrát, takže počet čar přes logo zůstává stejný. Náhledy všech
9 log jsou pixel po pixelu beze změny. 119 testů zelených.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:08:54 +02:00

921 lines
46 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.IO;
using System.Text.RegularExpressions;
using ACadSharp;
using ACadSharp.Entities;
using ACadSharp.IO;
using ACadSharp.Objects;
using CSMath;
using Rozpisky.Rendering;
using AcTextStyle = ACadSharp.Tables.TextStyle;
using FontFlags = ACadSharp.Tables.FontFlags;
namespace Rozpisky.Dxf;
/// <summary>
/// Načte DXF šablonu rozpisky (ACadSharp) a „přehraje" ji do <see cref="IProfileRenderer"/>
/// (ROZPISKY.md §4). Statická geometrie + popisky se kreslí vždy; pole (ATTDEF) se buď
/// vykreslí jako názvy (předpis), nebo vyplní hodnotami. ACadSharp na rozdíl od netDxf
/// vystaví font ze stylu i box-šířku víceřádkových atributů (XRecord, group 41).
/// </summary>
public sealed partial class DxfTemplate
{
private readonly CadDocument _doc;
/// <summary>Hladina vodicího rámečku uvnitř vkládaných DXF (loga/schéma) definuje návrhový
/// obdélník pro umístění. Nekreslí se a slouží jako reference pro fit místo bboxu kresby.</summary>
public const string HladinaNoPrint = "!!NOPRINT";
/// <summary>Převod úhlu z radiánů (ACadSharp vrací rotace v radiánech) na stupně (renderer).</summary>
private const double RadToDeg = 180.0 / Math.PI;
/// <summary>Hladiny vyhrazené pro placeholder-rámečky (loga + orientační schéma). Obdélníky na
/// těchto hladinách jsou jen vodítka (poloha + max. velikost), nekreslí se (mají PlotFlag=0).</summary>
public static readonly IReadOnlyList<string> PlaceholderHladiny = new[]
{
"Rozpiska_logo1", "Rozpiska_logo2", "Rozpiska_logo3", "Rozpiska_logo4", "Rozpiska_logo5",
"Rozpiska_Orientační schéma",
};
public DxfTemplate(string path)
{
_doc = DxfReader.Read(path) ?? throw new InvalidDataException($"DXF nelze načíst: {path}");
SheetFrame = FindSheetFrame();
Placeholders = FindPlaceholders();
}
/// <summary>Bounds A4 rámečku šablony (uzavřená polylinie ~A4) referenční obdélník pro mapování
/// na papír 1:1, aby rozpiska byla vystředěná dle návrhu. Null = nenalezen (export centruje obsah).</summary>
public (double X, double Y, double W, double H)? SheetFrame { get; }
/// <summary>Rámečky placeholderů (hladina → obdélník v mm) z uzavřených polylinií na
/// <see cref="PlaceholderHladiny"/>. Do těchto obdélníků se fitují loga / orientační schéma.</summary>
public IReadOnlyDictionary<string, (double X, double Y, double W, double H)> Placeholders { get; }
private (double X, double Y, double W, double H)? _bounds;
private bool _boundsComputed;
/// <summary>Ohraničení (bbox) kreslené geometrie a textů v mm slouží k výpočtu fitu, když se
/// tato šablona použije jako vkládaný fragment (logo / schéma). Null = prázdná/degenerovaná kresba.</summary>
public (double X, double Y, double W, double H)? Bounds
{
get
{
if (!_boundsComputed) { _bounds = ComputeBounds(); _boundsComputed = true; }
return _bounds;
}
}
private (double X, double Y, double W, double H)? _frameBounds;
private bool _frameComputed;
/// <summary>Bbox vodicího rámečku na hladině <see cref="HladinaNoPrint"/> (pokud existuje)
/// preferovaná reference pro fit vkládaného loga/schématu místo bboxu kresby. Null = bez rámečku.</summary>
public (double X, double Y, double W, double H)? FrameBounds
{
get
{
if (!_frameComputed) { _frameBounds = ComputeFrameBounds(); _frameComputed = true; }
return _frameBounds;
}
}
private IEnumerable<AttributeDefinition> AttrDefs => _doc.Entities.OfType<AttributeDefinition>();
/// <summary>Přehraje šablonu do kreslítka. <paramref name="values"/> = null → vykreslí názvy polí.
/// <paramref name="includeNoPrint"/> = true → vykreslí i geometrii na hladině <see cref="HladinaNoPrint"/>
/// (např. A4 rámeček) pro DXF export jako referenci hranice papíru.</summary>
public void Replay(IProfileRenderer g, IReadOnlyDictionary<string, string>? values = null, bool includeNoPrint = false)
{
_nezpracovane.Clear();
Prehraj(g, EntitiesInDrawOrder(), values, includeNoPrint, vlozeny: null, hloubka: 0);
}
/// <summary>Počty entit, které kreslítko neumí, podle typu (např. <c>DIMENSION</c> → 3).
/// Naplní se při každém <see cref="Replay"/>. Prázdné = vykreslilo se všechno. Slouží
/// k upozornění uživatele, když si přidá logo s prvky, které program nezná dřív takové
/// prvky mizely tiše.</summary>
public IReadOnlyDictionary<string, int> NezpracovaneEntity => _nezpracovane;
private readonly Dictionary<string, int> _nezpracovane = new(StringComparer.Ordinal);
/// <summary>Nejhlubší zanoření bloků, do kterého se ještě jde. Chrání před blokem, který
/// (chybou ve zdroji) vkládá sám sebe.</summary>
private const int MaxHloubkaBloku = 8;
/// <summary>Jeden průchod v pořadí kresby (viz <see cref="EntitiesInDrawOrder"/>) zachovává
/// překrývání entit ze zdroje (výplně vzadu, popisky vepředu). Dispatch podle typu; odvozené
/// typy (Arc:Circle, AttributeDefinition:AttributeBase:TextEntity) musí být ve switchi PŘED
/// svými předky. <paramref name="vlozeny"/> = stav vkládajícího <c>INSERT</c>u při rozvíjení
/// bloku (řeší barvy ByBlock a hladinu „0"), null v model space.</summary>
private void Prehraj(IProfileRenderer g, IEnumerable<Entity> entity,
IReadOnlyDictionary<string, string>? values, bool includeNoPrint,
KresliciStav? vlozeny, int hloubka)
{
foreach (var e in entity)
{
if (!SetLayer(g, e, includeNoPrint, vlozeny)) continue;
switch (e)
{
case Line l:
g.Line(l.StartPoint.X, l.StartPoint.Y, l.EndPoint.X, l.EndPoint.Y);
break;
case LwPolyline pl:
KresliPolyline(g, pl.Vertices.Select(v => (v.Location.X, v.Location.Y, v.Bulge)).ToList(), pl.IsClosed);
break;
// Klasické 2D polylinie (Polyline2D) v mapách/schématech časté (tratě), i s bulge oblouky.
case Polyline2D pl:
KresliPolyline(g, pl.Vertices.Select(v => (v.Location.X, v.Location.Y, v.Bulge)).ToList(), pl.IsClosed);
break;
// Spliny (NURBS) tesselace na lomenou čáru přes ACadSharp.
case Spline sp:
{
var pts = sp.PolygonalVertexes(DilkuSplinu(sp)).Select(p => (p.X, p.Y)).ToList();
if (pts.Count >= 2) g.Polyline(pts, sp.IsClosed);
break;
}
// Oblouk (Arc dědí z Circle) analytický, aby byl hladký v každém přiblížení.
case Arc a:
g.Stroke(ArcContour(a));
break;
case Circle c:
g.Circle(c.Center.X, c.Center.Y, c.Radius);
break;
// Výplně (HATCH) vyplněné plochy (typicky loga). Každá cesta = smyčka, even-odd díry.
case Hatch hatch:
{
var loops = HatchSmycky(hatch);
if (loops.Count > 0) g.Fill(loops, VyplnZ(hatch, loops));
break;
}
// Pole (ATTDEF) buď názvy polí (předpis), nebo vyplněné hodnoty.
case AttributeDefinition ad:
{
string text;
if (values is null)
{
text = ad.Tag; // předpis vykresli název pole
}
else
{
values.TryGetValue(NormalizeTag(ad.Tag), out var val);
if (string.IsNullOrEmpty(val)) break; // nevyplněné pole = prázdné
text = val;
}
var (x, y, h, v) = Place(ad.HorizontalAlignment, ad.VerticalAlignment, ad.InsertPoint, ad.AlignmentPoint);
// Víceřádkové atributy nesou box-šířku (rectangle width) v XRecordu group 41.
g.Text(x, y, text, ad.Height, h, v, ad.Rotation * RadToDeg, StyleOf(ad.Style, ad.ObliqueAngle), ad.WidthFactor, MultilineWidth(ad));
break;
}
// Ostatní atributy (hodnoty INSERTů) nekreslíme řeší se přes ATTDEF šablony.
case AttributeBase:
break;
case TextEntity t:
{
var (x, y, h, v) = Place(t.HorizontalAlignment, t.VerticalAlignment, t.InsertPoint, t.AlignmentPoint);
g.Text(x, y, t.Value, t.Height, h, v, t.Rotation * RadToDeg, StyleOf(t.Style, t.ObliqueAngle), t.WidthFactor);
break;
}
case MText mt:
{
var (h, v) = MapMTextAlign(mt.AttachmentPoint);
// MTEXT nemá šířkový faktor jako vlastnost entity nese ho buď kód \W
// na začátku hodnoty, nebo (jinak) styl textu.
var (text, wfKod) = MTextSirka(mt.Value, mt.PlainText);
double wf = wfKod ?? (mt.Style?.Width > 0 ? mt.Style.Width : 1);
g.Text(mt.InsertPoint.X, mt.InsertPoint.Y, text, mt.Height, h, v, mt.Rotation * RadToDeg,
StyleOf(mt.Style), wf, mt.RectangleWidth);
break;
}
// Vložený blok rozvine se rekurzivně. Bez toho by se uživatelovo logo s blokem
// nevykreslilo vůbec a bez hlášky.
case Insert ins:
RozvinBlok(g, ins, values, includeNoPrint, hloubka);
break;
// Elipsa: rozhraní zná jen kruhové oblouky, takže se navzorkuje na lomenou čáru.
case Ellipse el:
{
var body = VzorkujEllipse(el);
if (body.Count >= 2) g.Polyline(body, el.IsFullEllipse);
break;
}
// Bod kreslí se jako tečka (úsečka nulové délky, kulaté zakončení čáry).
case Point p:
g.Line(p.Location.X, p.Location.Y, p.Location.X, p.Location.Y);
break;
// Vyplněný čtyřúhelník. Pozor: v DXF jsou 3. a 4. roh prohozené proti obvodu.
case Solid s:
KresliCtyruhelnik(g, s.FirstCorner.X, s.FirstCorner.Y, s.SecondCorner.X, s.SecondCorner.Y,
s.ThirdCorner.X, s.ThirdCorner.Y, s.FourthCorner.X, s.FourthCorner.Y);
break;
case Face3D f:
KresliCtyruhelnik(g, f.FirstCorner.X, f.FirstCorner.Y, f.SecondCorner.X, f.SecondCorner.Y,
f.ThirdCorner.X, f.ThirdCorner.Y, f.FourthCorner.X, f.FourthCorner.Y);
break;
// Prostorová polylinie promítne se do roviny XY (kresba je 2D).
case Polyline3D pl3:
{
var body = pl3.Vertices.Select(v => (v.Location.X, v.Location.Y)).ToList();
if (body.Count >= 2) g.Polyline(body, pl3.IsClosed);
break;
}
default:
Nezpracovano(e.ObjectName);
break;
}
}
}
/// <summary>Rozvine <c>INSERT</c>: entity bloku se přehrají přes transformaci (posun na
/// vkládací bod, měřítko os, natočení). Barvy <c>ByBlock</c> a hladina „0" uvnitř bloku
/// přebírají stav vkládající entity tak to dělá i AutoCAD.</summary>
private void RozvinBlok(IProfileRenderer g, Insert ins,
IReadOnlyDictionary<string, string>? values, bool includeNoPrint, int hloubka)
{
if (hloubka >= MaxHloubkaBloku) { Nezpracovano("INSERT (příliš zanořený)"); return; }
var blok = ins.Block;
if (blok is null) { Nezpracovano("INSERT (chybí definice bloku)"); return; }
var zaklad = blok.BlockEntity?.BasePoint ?? default;
var transformace = TransformRenderer.ProBlok(
g, ins.InsertPoint.X, ins.InsertPoint.Y, zaklad.X, zaklad.Y,
ins.XScale, ins.YScale, ins.Rotation);
var stavVkladajici = new KresliciStav(HladinaZ(ins.Layer), BarvaZ(ins.Color),
ins.LineType?.Name, TloustkaZ(ins.LineWeight));
Prehraj(transformace, blok.Entities, values, includeNoPrint, stavVkladajici, hloubka + 1);
}
/// <summary>Vyplněný čtyřúhelník ze čtyř rohů v pořadí DXF (3. a 4. roh prohozený).
/// Trojúhelník se pozná podle shodného 3. a 4. rohu.</summary>
private static void KresliCtyruhelnik(IProfileRenderer g, double x1, double y1, double x2, double y2,
double x3, double y3, double x4, double y4)
{
var hrany = new List<Edge> { Edge.Line(x1, y1, x2, y2), Edge.Line(x2, y2, x4, y4) };
if (Math.Abs(x3 - x4) > 1e-9 || Math.Abs(y3 - y4) > 1e-9)
hrany.Add(Edge.Line(x4, y4, x3, y3));
hrany.Add(Edge.Line(hrany[^1].EX, hrany[^1].EY, x1, y1));
g.Fill(new[] { new Contour(hrany, Closed: true) });
}
/// <summary>Na kolik dílků rozložit spline. Pevných 64 bylo málo pro dlouhé křivky (viditelné
/// hrany) a zbytečně moc pro krátké; počítá se z délky řídicího polygonu tak, aby jeden dílek
/// vyšel zhruba na 0,3 mm výkresu.</summary>
private static int DilkuSplinu(Spline sp)
{
const double DilekMm = 0.3;
double delka = 0;
var body = sp.ControlPoints;
for (int i = 1; i < body.Count; i++)
{
double dx = body[i].X - body[i - 1].X, dy = body[i].Y - body[i - 1].Y;
delka += Math.Sqrt(dx * dx + dy * dy);
}
return Math.Clamp((int)Math.Ceiling(delka / DilekMm), 16, 512);
}
private void Nezpracovano(string typ)
=> _nezpracovane[typ] = _nezpracovane.GetValueOrDefault(typ) + 1;
/// <summary>Tloušťka čáry v mm z DXF hodnoty (setiny mm); záporná = podle hladiny → null.</summary>
private static double? TloustkaZ(LineWeightType lw) => (int)lw > 0 ? (int)lw / 100.0 : null;
/// <summary>Entity model-space v pořadí kresby: pokud má blok tabulku pořadí (<c>SortEntitiesTable</c>,
/// tj. „draw order" nastavené v CADu), seřadí podle ní; jinak zachová pořadí ze souboru. Nižší sort
/// handle = kreslí se dřív (vzadu). LINQ <c>OrderBy</c> je stabilní → entity bez záznamu drží pořadí.</summary>
private IEnumerable<Entity> EntitiesInDrawOrder()
{
var ents = _doc.Entities.OfType<Entity>();
var sort = _doc.ModelSpace?.SortEntitiesTable;
if (sort is null) return ents;
return ents.OrderBy(e => SortHandle(sort, e));
static ulong SortHandle(ACadSharp.Objects.SortEntitiesTable t, Entity e)
{
try { return t.GetSorterHandle(e); } catch { return e.Handle; }
}
}
/// <summary>Vykreslí LwPolyline/Polyline2D: bez oblouků jako lomenou čáru (<c>Polyline</c>),
/// s bulge jako obrys se zachovanými kruhovými oblouky (<c>Stroke</c>).</summary>
private static void KresliPolyline(IProfileRenderer g, IReadOnlyList<(double X, double Y, double Bulge)> verts, bool closed)
{
if (verts.Count < 2) return;
bool maOblouk = false;
for (int i = 0; i < verts.Count; i++) if (Math.Abs(verts[i].Bulge) > 1e-9) { maOblouk = true; break; }
if (!maOblouk) { g.Polyline(verts.Select(v => (v.X, v.Y)).ToList(), closed); return; }
g.Stroke(PolylineContour(verts, closed));
}
/// <summary>Nastaví hladinu do kreslítka. Vrací false, pokud se hladina nemá tisknout (přeskočit kresbu).
/// <paramref name="includeNoPrint"/> = true → geometrie na <see cref="HladinaNoPrint"/> se nepřeskočí
/// (kreslí se i přes vypnutý plot flag DXF reference A4 rámečku).</summary>
private static bool SetLayer(IProfileRenderer g, Entity e, bool includeNoPrint = false,
KresliciStav? vlozeny = null)
{
var layer = e.Layer;
bool noPrint = JeNoPrint(e);
if (noPrint && !includeNoPrint) return false; // vodicí rámeček vkládaného DXF se nekreslí
if (!noPrint && layer is not null && (!layer.PlotFlag || !layer.IsOn)) return false;
var hladina = HladinaZ(layer);
var barva = BarvaZ(e.Color);
var tloustka = TloustkaZ(e.LineWeight);
var typCary = e.LineType?.Name;
// Uvnitř bloku: hladina „0" a barva/typ čáry ByBlock se berou od vkládající entity
// právě proto se stejný blok dá vložit dvakrát a pokaždé v jiné barvě.
if (vlozeny is { } v)
{
if (hladina.Nazev is null or "0") hladina = v.Hladina;
if (barva.Druh == BarvaDruh.ByBlock) barva = v.Barva;
tloustka ??= v.TloustkaMm;
if (typCary is null or "ByBlock") typCary = v.TypCary;
}
g.Stav(new KresliciStav(hladina, barva, typCary, tloustka));
return true;
}
/// <summary>Odloupne z MTEXTu úvodní kód šířkového faktoru <c>\W1.2;</c>. ACadSharp ho
/// v <c>PlainText</c> nechává (jen bez zpětného lomítka), takže by se jinak vykreslil jako
/// text. Vrací čistý text a faktor, když tam byl.</summary>
internal static (string Text, double? WidthFactor) MTextSirka(string? hodnota, string? plainText)
{
string plain = plainText ?? "";
if (hodnota is null || !hodnota.StartsWith(@"\W", StringComparison.Ordinal)) return (plain, null);
int strednik = hodnota.IndexOf(';', 2);
if (strednik < 0) return (plain, null);
if (!double.TryParse(hodnota[2..strednik], System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out double wf) || wf <= 0)
return (plain, null);
// Z PlainText ubyla jen zpětná lomítka, tedy „W1.2;" bez prvního znaku kódu.
string predpona = hodnota[1..(strednik + 1)];
if (plain.StartsWith(predpona, StringComparison.Ordinal)) plain = plain[predpona.Length..];
return (plain, wf);
}
/// <summary>Druh výplně pro HATCH. Plná (SOLID) projde beze změny; u vzoru se vzor rozvine
/// na čáry pro rastrové renderery, ale jméno vzoru, měřítko a úhel se nesou dál, aby DXF
/// export zapsal zase editovatelnou šrafu. Když se vzor rozvinout nepodaří, plocha zůstane
/// prázdná dřív se v takovém případě kreslila plná výplň a z logotypu byl černý blok.</summary>
private static Vypln VyplnZ(Hatch hatch, IReadOnlyList<Contour> smycky)
{
string vzor = hatch.Pattern?.Name ?? "SOLID";
if (hatch.IsSolid || string.IsNullOrWhiteSpace(vzor)
|| vzor.Equals("SOLID", StringComparison.OrdinalIgnoreCase))
return default;
var definice = DefiniceVzoru(hatch.Pattern);
return Vypln.Vzorovana(vzor, hatch.PatternScale, hatch.PatternAngle * RadToDeg,
definice, SrafaVzor.Cary(definice, smycky));
}
/// <summary>Definiční čáry vzoru šrafy z entity. DXF je nese uvnitř <c>HATCH</c> (group 78 a dál),
/// tedy už v efektivních hodnotách měřítko a úhel z vlastností šrafy jsou v nich započítané.</summary>
private static List<VzorCara> DefiniceVzoru(HatchPattern? vzor)
{
var vysledek = new List<VzorCara>();
foreach (var cara in vzor?.Lines ?? new List<HatchPattern.Line>())
vysledek.Add(new VzorCara(
cara.Angle * RadToDeg,
(cara.BasePoint.X, cara.BasePoint.Y),
(cara.Offset.X, cara.Offset.Y),
cara.DashLengths?.ToList() ?? new List<double>()));
return vysledek;
}
/// <summary>Popis hladiny pro kreslítko celý, ať se dá ve výstupu postavit znovu
/// (barva z tabulky hladin, tloušťka, typ čáry, příznak tisku).</summary>
private static HladinaStav HladinaZ(ACadSharp.Tables.Layer? layer)
{
if (layer is null) return HladinaStav.Vychozi;
int lw = (int)layer.LineWeight;
return new HladinaStav(
layer.Name,
BarvaZ(layer.Color),
layer.LineType?.Name ?? "Continuous",
lw > 0 ? lw / 100.0 : 0,
layer.PlotFlag,
layer.IsOn);
}
/// <summary>Barva z DXF i s původem: index (group 62), true color (420) nebo odkaz
/// ByLayer/ByBlock. Původ se nese dál, aby ho DXF export mohl zapsat beze změny
/// zplošťování na RGB dřív z indexovaných barev dělalo natvrdo RGB.</summary>
private static Barva BarvaZ(ACadSharp.Color c)
{
if (c.IsByLayer) return Barva.ZHladiny;
if (c.IsByBlock) return Barva.ZBloku;
if (c.IsTrueColor) return Barva.Prima((c.R << 16) | (c.G << 8) | c.B);
return c.Index is >= 1 and <= 255 ? Barva.Aci(c.Index) : Barva.Aci(7);
}
/// <summary>Zjistí kotvu a zarovnání textu (Left+Baseline → vkládací bod, jinak zarovnávací bod).</summary>
private static (double x, double y, HAlign h, VAlign v) Place(
TextHorizontalAlignment ha, TextVerticalAlignmentType va, XYZ insert, XYZ align)
{
var h = ha switch
{
TextHorizontalAlignment.Center or TextHorizontalAlignment.Middle => HAlign.Center,
TextHorizontalAlignment.Right => HAlign.Right,
_ => HAlign.Left, // Left / Aligned / Fit (aproximace)
};
var v = va switch
{
TextVerticalAlignmentType.Top => VAlign.Top,
TextVerticalAlignmentType.Middle => VAlign.Middle,
TextVerticalAlignmentType.Bottom => VAlign.Bottom,
_ => VAlign.Baseline,
};
bool useAlign = ha != TextHorizontalAlignment.Left || va != TextVerticalAlignmentType.Baseline;
var p = useAlign ? align : insert;
return (p.X, p.Y, h, v);
}
/// <summary>Převede DXF textový styl na vzhled písma rendereru. Pro TTF předá soubor
/// (přesný řez verdanab.ttf), SHX → fallback. Řez i jako záloha z TrueType flagů.
/// <paramref name="entityObliqueRad"/> = oblique úhel entity (group 51, radiány) rozhodující;
/// při 0 se použije oblique stylu (group 50 je jen výchozí hodnota při tvorbě textu).</summary>
private static Rozpisky.Rendering.TextStyle StyleOf(AcTextStyle? s, double entityObliqueRad = 0)
{
double obliqueRad = entityObliqueRad != 0 ? entityObliqueRad : s?.ObliqueAngle ?? 0;
if (s is null) return new(ObliqueDeg: obliqueRad * RadToDeg);
string? file = string.IsNullOrWhiteSpace(s.Filename) ? null : s.Filename;
string? ttf = s.IsShapeFile ? null : file;
bool bold = s.TrueType.HasFlag(FontFlags.Bold);
bool italic = s.TrueType.HasFlag(FontFlags.Italic);
// Rodina písma: z .ttf souboru; když styl soubor nemá (TTF zadané jen jménem), vezmi
// název typu písma z XData(ACAD) tam AutoCAD ukládá rodinu (např. „Arial Black").
string? family = ttf is not null ? Path.GetFileNameWithoutExtension(ttf) : FontRodinaZeXData(s);
return new(family, bold, italic, file, obliqueRad * RadToDeg, s.Name, s.IsShapeFile);
}
/// <summary>Název rodiny TrueType písma uložený ve stylu jako XData appid „ACAD" (první ASCII
/// řetězec), používaný když styl nemá <c>.ttf</c> soubor. Null = není.</summary>
private static string? FontRodinaZeXData(AcTextStyle s)
{
if (s.ExtendedData is not { } xd) return null;
foreach (var entry in xd)
{
if (!string.Equals(entry.Key?.Name, "ACAD", StringComparison.OrdinalIgnoreCase)) continue;
var recs = entry.Value?.Records;
if (recs is null) continue;
foreach (var rec in recs)
if (rec.GetType().GetProperty("Value")?.GetValue(rec) is string str && !string.IsNullOrWhiteSpace(str))
return str.Trim();
}
return null;
}
/// <summary>Najde A4 rámeček: uzavřenou polylinii s bbox ≈ A4 (210×297 / 297×210), největší vyhovující.</summary>
private (double X, double Y, double W, double H)? FindSheetFrame()
{
(double X, double Y, double W, double H)? best = null;
foreach (var pl in _doc.Entities.OfType<LwPolyline>())
{
if (!pl.IsClosed || pl.Vertices.Count < 4) continue;
double ax = double.MaxValue, ay = double.MaxValue, bx = double.MinValue, by = double.MinValue;
foreach (var v in pl.Vertices)
{
ax = Math.Min(ax, v.Location.X); ay = Math.Min(ay, v.Location.Y);
bx = Math.Max(bx, v.Location.X); by = Math.Max(by, v.Location.Y);
}
double w = bx - ax, h = by - ay;
bool a4 = (Near(w, 297) && Near(h, 210)) || (Near(w, 210) && Near(h, 297));
if (a4 && (best is null || w * h > best.Value.W * best.Value.H))
best = (ax, ay, w, h);
}
return best;
static bool Near(double a, double b) => Math.Abs(a - b) < 2.0;
}
/// <summary>Najde rámečky placeholderů: uzavřené polylinie na <see cref="PlaceholderHladiny"/>,
/// pro každou hladinu bbox vrcholů. Při více polyliniích na hladině vezme největší.</summary>
private IReadOnlyDictionary<string, (double X, double Y, double W, double H)> FindPlaceholders()
{
var hladiny = new HashSet<string>(PlaceholderHladiny, StringComparer.OrdinalIgnoreCase);
var res = new Dictionary<string, (double X, double Y, double W, double H)>(StringComparer.OrdinalIgnoreCase);
foreach (var pl in _doc.Entities.OfType<LwPolyline>())
{
var name = pl.Layer?.Name;
if (name is null || !hladiny.Contains(name) || pl.Vertices.Count < 2) continue;
double ax = double.MaxValue, ay = double.MaxValue, bx = double.MinValue, by = double.MinValue;
foreach (var v in pl.Vertices)
{
ax = Math.Min(ax, v.Location.X); ay = Math.Min(ay, v.Location.Y);
bx = Math.Max(bx, v.Location.X); by = Math.Max(by, v.Location.Y);
}
var rect = (ax, ay, bx - ax, by - ay);
if (!res.TryGetValue(name, out var prev) || rect.Item3 * rect.Item4 > prev.W * prev.H)
res[name] = rect;
}
return res;
}
/// <summary>Spočítá bbox kreslené geometrie (čáry, polylinie, výplně, texty) pro fit fragmentu.
/// Přehrává tutéž kresbu jako <see cref="Replay"/> do <see cref="BoundsRenderer"/>, takže bbox
/// vždy odpovídá tomu, co se skutečně kreslí (texty včetně odhadu šířky/výšky, ne jen kotvy).
/// Prázdný slovník hodnot → pole (ATTDEF) se přeskočí; null by vykreslil názvy tagů.
/// Vodicí rámeček na hladině <see cref="HladinaNoPrint"/> se nezapočítává (řeší <see cref="FrameBounds"/>).</summary>
private (double X, double Y, double W, double H)? ComputeBounds()
{
var b = new BoundsRenderer();
Replay(b, values: new Dictionary<string, string>());
return b.IsEmpty || b.Width <= 0 || b.Height <= 0 ? null : (b.MinX, b.MinY, b.Width, b.Height);
}
/// <summary>Bbox vodicího rámečku (uzavřená polylinie / čáry na hladině <see cref="HladinaNoPrint"/>).</summary>
private (double X, double Y, double W, double H)? ComputeFrameBounds()
{
double ax = double.MaxValue, ay = double.MaxValue, bx = double.MinValue, by = double.MinValue;
bool any = false;
void Add(double x, double y) { ax = Math.Min(ax, x); ay = Math.Min(ay, y); bx = Math.Max(bx, x); by = Math.Max(by, y); any = true; }
foreach (var pl in _doc.Entities.OfType<LwPolyline>().Where(JeNoPrint))
foreach (var v in pl.Vertices) Add(v.Location.X, v.Location.Y);
foreach (var l in _doc.Entities.OfType<Line>().Where(JeNoPrint))
{
Add(l.StartPoint.X, l.StartPoint.Y); Add(l.EndPoint.X, l.EndPoint.Y);
}
if (!any) return null;
double w = bx - ax, h = by - ay;
return w > 0 && h > 0 ? (ax, ay, w, h) : null;
}
/// <summary>True, pokud entita leží na vodicí hladině <see cref="HladinaNoPrint"/> (nekreslit, jen reference).</summary>
private static bool JeNoPrint(Entity e) => string.Equals(e.Layer?.Name, HladinaNoPrint, StringComparison.OrdinalIgnoreCase);
/// <summary>Převede hraniční cesty HATCH na vyplnitelné obrysy (<see cref="Contour"/>) se
/// zachovanými kruhovými oblouky. Každá cesta = jedna smyčka; hrany (úsečky, oblouky, elipsy,
/// polylinie s bulge, spliny) se převedou na analytické <see cref="Edge"/> a zřetězí za sebou
/// navazující hrana může být v souboru orientovaná i opačně (řeší se otočením běhu hran).</summary>
private static List<Contour> HatchSmycky(Hatch hatch)
{
// Souřadnice hranic jsou v OCS entity (dané extruzní normálou group 210/220/230). Pro normálu
// (0,0,1) je převod do WCS identita; zrcadlené hatch (např. písmeno „O" v mco.dxf má normálu
// (0,0,-1)) by se bez převodu vykreslily na chybné X pozici. Osy OCS spočteme jednou pro celý
// hatch. Kladný determinant os = přímá izometrie (rotace) → oblouky lze převést analyticky
// (pootočením úhlu); záporný = zrcadlení → oblouky/elipsy raději jemně zploštíme na úsečky.
var (axx, axy, ayx, ayy) = OcsOsy(hatch.Normal.X, hatch.Normal.Y, hatch.Normal.Z);
(double X, double Y) ToWcs(double x, double y) => (x * axx + y * ayx, x * axy + y * ayy);
double det = axx * ayy - axy * ayx;
bool direct = det > 0;
double alpha = Math.Atan2(axy, axx); // úhel pootočení os OCS→WCS (jen pro direct case)
var loops = new List<Contour>();
foreach (var path in hatch.Paths)
{
var loop = new List<Edge>();
foreach (var edge in path.Edges)
{
var run = HranaNaEdges(edge, ToWcs, direct, alpha);
if (run.Count == 0) continue;
if (loop.Count > 0)
{
var last = (loop[^1].EX, loop[^1].EY);
// Orientaci běhu hran určí bližší konec k dosavadnímu konci smyčky.
if (Dist2(last, (run[0].SX, run[0].SY)) > Dist2(last, (run[^1].EX, run[^1].EY)))
run = ReverseRun(run);
}
loop.AddRange(run);
}
// ≥1 hrana: samostatný kruhový/eliptický oblouk (1 hrana) je platná výplň (plný kruh).
if (loop.Count >= 1) loops.Add(new Contour(loop, Closed: true));
}
return loops;
static double Dist2((double X, double Y) a, (double X, double Y) b)
{
double dx = a.X - b.X, dy = a.Y - b.Y;
return dx * dx + dy * dy;
}
}
/// <summary>Otočí běh hran (obrácené pořadí i směr každé hrany), aby navázal na předchozí konec.</summary>
private static List<Edge> ReverseRun(List<Edge> run)
{
var res = new List<Edge>(run.Count);
for (int i = run.Count - 1; i >= 0; i--) res.Add(run[i].Reversed());
return res;
}
/// <summary>Převede jednu hranu HATCH hranice na analytické <see cref="Edge"/> ve WCS. Kruhové
/// oblouky (přímý ARC i bulge v polylinii) se při přímé izometrii OCS (<paramref name="direct"/>)
/// zachovají analyticky (pootočení úhlu o <paramref name="alpha"/>); elipsy, spliny a oblouky pod
/// zrcadlením se jemně zploští na úsečky (viz <see cref="LineEdgesZOcs"/>). Nepodporovaný typ → prázdné.</summary>
private static List<Edge> HranaNaEdges(Hatch.BoundaryPath.Edge edge,
Func<double, double, (double X, double Y)> toWcs, bool direct, double alpha)
{
switch (edge)
{
case Hatch.BoundaryPath.Line ln:
{
var s = toWcs(ln.Start.X, ln.Start.Y);
var e = toWcs(ln.End.X, ln.End.Y);
return new() { Edge.Line(s.X, s.Y, e.X, e.Y) };
}
case Hatch.BoundaryPath.Polyline ple:
{
var verts = ple.Vertices;
if (verts.Count < 2) return new();
var bulges = ple.Bulges?.ToList();
var edges = new List<Edge>();
int n = verts.Count;
int segments = ple.IsClosed ? n : n - 1;
for (int i = 0; i < segments; i++)
{
double ax = verts[i].X, ay = verts[i].Y;
double bx = verts[(i + 1) % n].X, by = verts[(i + 1) % n].Y;
double bulge = bulges is not null && i < bulges.Count ? bulges[i] : 0;
if (Math.Abs(bulge) > 1e-9)
{
if (direct) edges.Add(BulgeEdge(ax, ay, bx, by, bulge, toWcs, alpha));
else
{
var body = new List<(double X, double Y)> { (ax, ay) };
ZplostiOblouk(body, ax, ay, bx, by, bulge);
body.Add((bx, by));
edges.AddRange(LineEdgesZOcs(body, toWcs));
}
}
else
{
var s = toWcs(ax, ay); var e = toWcs(bx, by);
edges.Add(Edge.Line(s.X, s.Y, e.X, e.Y));
}
}
return edges;
}
case Hatch.BoundaryPath.Arc arc when direct:
return new() { ArcEdge(arc.Center.X, arc.Center.Y, arc.Radius, arc.StartAngle, arc.EndAngle, arc.CounterClockWise, toWcs, alpha) };
case Hatch.BoundaryPath.Arc arc:
return LineEdgesZOcs(VzorkujArc(arc), toWcs);
case Hatch.BoundaryPath.Ellipse el:
// Elipsy vždy jemně zploštíme (analytický převod přes zrcadlení OCS by byl křehký).
return LineEdgesZOcs(VzorkujEllipse(el), toWcs);
case Hatch.BoundaryPath.Spline sp:
{
var pts = sp.FitPoints is { Count: > 1 } fit
? fit.Select(p => (p.X, p.Y)).ToList()
: sp.ControlPoints?.Select(p => (p.X, p.Y)).ToList() ?? new();
return LineEdgesZOcs(pts, toWcs);
}
default:
return new();
}
}
/// <summary>Analytický kruhový oblouk hatch hranice (přímá izometrie OCS). Úhly v radiánech,
/// <paramref name="ccw"/>=false znamená v DXF zrcadlení přes osu X OCS → ve WCS úhel <c>α−sa</c>
/// se záporným rozsahem; <paramref name="ccw"/>=true → <c>sa+α</c> s kladným rozsahem.</summary>
private static Edge ArcEdge(double cx, double cy, double r, double sa, double ea, bool ccw,
Func<double, double, (double X, double Y)> toWcs, double alpha)
{
if (ea <= sa) ea += 2 * Math.PI;
double mag = ea - sa;
var c = toWcs(cx, cy);
double a0 = ccw ? sa + alpha : alpha - sa;
double sweep = ccw ? mag : -mag;
var s = (c.X + r * Math.Cos(a0), c.Y + r * Math.Sin(a0));
var e = (c.X + r * Math.Cos(a0 + sweep), c.Y + r * Math.Sin(a0 + sweep));
return Edge.Arc(s.Item1, s.Item2, e.Item1, e.Item2, c.X, c.Y, r, a0, sweep);
}
/// <summary>Analytický oblouk z bulge segmentu (přímá izometrie OCS): standardní parametrizace
/// <c>střed + r·(cos θ, sin θ)</c> → úhel se pootočí o <paramref name="alpha"/>, rozsah = θ.</summary>
private static Edge BulgeEdge(double ax, double ay, double bx, double by, double bulge,
Func<double, double, (double X, double Y)> toWcs, double alpha)
{
var (cx, cy, r, sa, theta) = BulgeArc(ax, ay, bx, by, bulge);
var c = toWcs(cx, cy);
var s = toWcs(ax, ay); var e = toWcs(bx, by);
return Edge.Arc(s.X, s.Y, e.X, e.Y, c.X, c.Y, r, sa + alpha, theta);
}
/// <summary>Střed, (kladný) poloměr, počáteční úhel a znaménkový rozsah oblouku z DXF bulge
/// (=tan(θ/4)) mezi body (x1,y1)(x2,y2); standardní parametrizace (bod = střed + r·(cos,sin)).</summary>
private static (double Cx, double Cy, double R, double Sa, double Theta) BulgeArc(
double x1, double y1, double x2, double y2, double bulge)
{
double theta = 4.0 * Math.Atan(bulge);
double dx = x2 - x1, dy = y2 - y1, chord = Math.Sqrt(dx * dx + dy * dy);
double r = chord / (2.0 * Math.Sin(theta / 2.0));
double mx = (x1 + x2) / 2.0, my = (y1 + y2) / 2.0;
double dd = r * Math.Cos(theta / 2.0);
double ux = -dy / chord, uy = dx / chord;
double cx = mx + dd * ux, cy = my + dd * uy;
double sa = Math.Atan2(y1 - cy, x1 - cx);
return (cx, cy, Math.Abs(r), sa, theta);
}
/// <summary>Převede řadu bodů v OCS na navazující úsečkové hrany ve WCS (přeskočí degenerované).</summary>
private static List<Edge> LineEdgesZOcs(IReadOnlyList<(double X, double Y)> ocsPts,
Func<double, double, (double X, double Y)> toWcs)
{
var edges = new List<Edge>();
if (ocsPts.Count < 2) return edges;
var prev = toWcs(ocsPts[0].X, ocsPts[0].Y);
for (int i = 1; i < ocsPts.Count; i++)
{
var cur = toWcs(ocsPts[i].X, ocsPts[i].Y);
double ddx = cur.X - prev.X, ddy = cur.Y - prev.Y;
if (ddx * ddx + ddy * ddy > 1e-14) { edges.Add(Edge.Line(prev.X, prev.Y, cur.X, cur.Y)); prev = cur; }
}
return edges;
}
/// <summary>Navzorkuje kruhový oblouk hatch hranice na body v OCS (zrcadlení CW přes osu X).</summary>
private static List<(double X, double Y)> VzorkujArc(Hatch.BoundaryPath.Arc arc)
{
double sa = arc.StartAngle, ea = arc.EndAngle;
if (ea <= sa) ea += 2 * Math.PI;
double sweep = ea - sa, sign = arc.CounterClockWise ? 1 : -1;
int seg = ContourGeometry.ArcSegments(sweep, arc.Radius, 0.02);
var pts = new List<(double X, double Y)>(seg + 1);
for (int i = 0; i <= seg; i++)
{
double t = sa + sweep * i / seg;
pts.Add((arc.Center.X + arc.Radius * Math.Cos(t), arc.Center.Y + sign * arc.Radius * Math.Sin(t)));
}
return pts;
}
/// <summary>Navzorkuje entitu ELLIPSE na body. Kreslicí rozhraní zná jen kruhové oblouky,
/// takže elipsa jde do lomené čáry dělení podle hlavní poloosy, aby byla dost hladká.
/// Parametry (nikoli úhly) jdou vždy proti směru hodin od <c>StartParameter</c>.</summary>
private static List<(double X, double Y)> VzorkujEllipse(Ellipse el)
{
double sa = el.StartParameter, ea = el.EndParameter;
if (ea <= sa) ea += 2 * Math.PI;
double sweep = ea - sa;
double mx = el.MajorAxisEndPoint.X, my = el.MajorAxisEndPoint.Y;
double vx = -my * el.RadiusRatio, vy = mx * el.RadiusRatio; // vedlejší poloosa = kolmice·ratio
double major = Math.Sqrt(mx * mx + my * my);
int seg = Math.Max(8, ContourGeometry.ArcSegments(sweep, Math.Max(major, 1e-9), 0.02));
var pts = new List<(double X, double Y)>(seg + 1);
for (int i = 0; i <= seg; i++)
{
double t = sa + sweep * i / seg, c = Math.Cos(t), s = Math.Sin(t);
pts.Add((el.Center.X + c * mx + s * vx, el.Center.Y + c * my + s * vy));
}
return pts;
}
/// <summary>Navzorkuje eliptický oblouk hatch hranice na body v OCS (kolmice·ratio, CW přes osu X).</summary>
private static List<(double X, double Y)> VzorkujEllipse(Hatch.BoundaryPath.Ellipse el)
{
double sa = el.StartAngle, ea = el.EndAngle;
if (ea <= sa) ea += 2 * Math.PI;
double sweep = ea - sa, sign = el.CounterClockWise ? 1 : -1;
double mx = el.MajorAxisEndPoint.X, my = el.MajorAxisEndPoint.Y;
double vx = -my * el.RadiusRatio, vy = mx * el.RadiusRatio;
double major = Math.Sqrt(mx * mx + my * my);
int seg = Math.Max(8, ContourGeometry.ArcSegments(sweep, Math.Max(major, 1e-9), 0.02));
var pts = new List<(double X, double Y)>(seg + 1);
for (int i = 0; i <= seg; i++)
{
double t = sa + sweep * i / seg, c = Math.Cos(t), s = sign * Math.Sin(t);
pts.Add((el.Center.X + c * mx + s * vx, el.Center.Y + c * my + s * vy));
}
return pts;
}
/// <summary>Analytický obrys samostatné entity ARC (vždy proti směru hodin od StartAngle k EndAngle).</summary>
private static Contour ArcContour(Arc a)
{
double sa = a.StartAngle, ea = a.EndAngle;
if (ea <= sa) ea += 2 * Math.PI;
double r = a.Radius, cx = a.Center.X, cy = a.Center.Y;
var s = (cx + r * Math.Cos(sa), cy + r * Math.Sin(sa));
var e = (cx + r * Math.Cos(ea), cy + r * Math.Sin(ea));
return new Contour(new[] { Edge.Arc(s.Item1, s.Item2, e.Item1, e.Item2, cx, cy, r, sa, ea - sa) }, Closed: false);
}
/// <summary>Obrys stroked polylinie (LwPolyline/Polyline2D) se zachovanými oblouky z bulge.
/// Souřadnice bere jako WCS (normálu entity ignoruje shodně s předchozím chováním).</summary>
private static Contour PolylineContour(IReadOnlyList<(double X, double Y, double Bulge)> verts, bool closed)
{
var edges = new List<Edge>();
int n = verts.Count;
int segments = closed ? n : n - 1;
for (int i = 0; i < segments; i++)
{
var a = verts[i]; var b = verts[(i + 1) % n];
if (Math.Abs(a.Bulge) > 1e-9)
{
var (cx, cy, r, sa, theta) = BulgeArc(a.X, a.Y, b.X, b.Y, a.Bulge);
edges.Add(Edge.Arc(a.X, a.Y, b.X, b.Y, cx, cy, r, sa, theta));
}
else edges.Add(Edge.Line(a.X, a.Y, b.X, b.Y));
}
return new Contour(edges, closed);
}
/// <summary>Osy OCS (arbitrary axis algorithm dle DXF specifikace) pro převod 2D bodů entity
/// s extruzní normálou <paramref name="nx"/>,<paramref name="ny"/>,<paramref name="nz"/> do WCS:
/// WCS = ocsX·ax + ocsY·ay. Pro normálu (0,0,1) je to identita, pro (0,0,-1) zrcadlí X. Vrací
/// jen XY složky obou os (Z se pro 2D kresbu ignoruje).</summary>
private static (double axx, double axy, double ayx, double ayy) OcsOsy(double nx, double ny, double nz)
{
double len = Math.Sqrt(nx * nx + ny * ny + nz * nz);
if (len < 1e-12) return (1, 0, 0, 1); // degenerovaná normála → identita
nx /= len; ny /= len; nz /= len;
// Referenční osa: Wy pro (téměř) svislou normálu, jinak Wz.
double rx, ry, rz;
if (Math.Abs(nx) < 1.0 / 64 && Math.Abs(ny) < 1.0 / 64) { rx = 0; ry = 1; rz = 0; }
else { rx = 0; ry = 0; rz = 1; }
// ax = R × N (normalizované), ay = N × ax (normalizované)
double axx = ry * nz - rz * ny, axy = rz * nx - rx * nz, axz = rx * ny - ry * nx;
double al = Math.Sqrt(axx * axx + axy * axy + axz * axz);
axx /= al; axy /= al; axz /= al;
double ayx = ny * axz - nz * axy, ayy = nz * axx - nx * axz;
double ayl = Math.Sqrt(ayx * ayx + ayy * ayy + (nx * axy - ny * axx) * (nx * axy - ny * axx));
return (axx, axy, ayx / ayl, ayy / ayl);
}
/// <summary>Přidá body oblouku (bez koncových) mezi (x1,y1) a (x2,y2) dle DXF bulge
/// (=tan(θ/4), θ = úhel oblouku; kladný = proti směru hodin).</summary>
private static void ZplostiOblouk(List<(double X, double Y)> body, double x1, double y1, double x2, double y2, double bulge)
{
double theta = 4.0 * Math.Atan(bulge); // úhel oblouku (se znaménkem)
double dx = x2 - x1, dy = y2 - y1;
double chord = Math.Sqrt(dx * dx + dy * dy);
if (chord < 1e-9) return;
double r = chord / (2.0 * Math.Sin(theta / 2.0)); // poloměr (se znaménkem)
double mx = (x1 + x2) / 2.0, my = (y1 + y2) / 2.0;
double dd = r * Math.Cos(theta / 2.0); // vzdálenost středu od středu tětivy
double ux = -dy / chord, uy = dx / chord; // levá normála tětivy
double cx = mx + dd * ux, cy = my + dd * uy;
double sa = Math.Atan2(y1 - cy, x1 - cx);
double ar = Math.Abs(r);
int seg = Math.Max(2, (int)Math.Ceiling(Math.Abs(theta) / (Math.PI / 8))); // ~16 dílků/kruh
for (int i = 1; i < seg; i++)
{
double t = sa + theta * i / seg;
body.Add((cx + ar * Math.Cos(t), cy + ar * Math.Sin(t)));
}
}
/// <summary>Box-šířka víceřádkového atributu z XRecordu (group 41); 0 = jednořádkový.</summary>
private static double MultilineWidth(AttributeDefinition ad)
{
var xd = ad.XDictionary;
if (xd is null) return 0;
foreach (var obj in xd)
if (obj is XRecord xr)
foreach (var e in xr.Entries)
if (e.Code == 41 && e.Value is double w && w > 0)
return w;
return 0;
}
/// <summary>Šablona má tagy se suffixem (NAZEV_AKCE_001) → párovat bez suffixu, case-insensitive.</summary>
public static string NormalizeTag(string tag) => SuffixRegex().Replace(tag, "").ToUpperInvariant();
[GeneratedRegex(@"_\d+$")]
private static partial Regex SuffixRegex();
private static (HAlign, VAlign) MapMTextAlign(AttachmentPointType a) => a switch
{
AttachmentPointType.TopLeft => (HAlign.Left, VAlign.Top),
AttachmentPointType.TopCenter => (HAlign.Center, VAlign.Top),
AttachmentPointType.TopRight => (HAlign.Right, VAlign.Top),
AttachmentPointType.MiddleLeft => (HAlign.Left, VAlign.Middle),
AttachmentPointType.MiddleCenter => (HAlign.Center, VAlign.Middle),
AttachmentPointType.MiddleRight => (HAlign.Right, VAlign.Middle),
AttachmentPointType.BottomLeft => (HAlign.Left, VAlign.Bottom),
AttachmentPointType.BottomCenter => (HAlign.Center, VAlign.Bottom),
AttachmentPointType.BottomRight => (HAlign.Right, VAlign.Bottom),
_ => (HAlign.Left, VAlign.Top),
};
}