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>
921 lines
46 KiB
C#
921 lines
46 KiB
C#
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),
|
||
};
|
||
}
|