Files
Rozpisky/Rozpisky/Rendering/DxfProfileRenderer.cs
T
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

417 lines
18 KiB
C#
Raw 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 ACadSharp;
using ACadSharp.Entities;
using CSMath;
using AcLayer = ACadSharp.Tables.Layer;
using AcTextStyle = ACadSharp.Tables.TextStyle;
using FontFlags = ACadSharp.Tables.FontFlags;
namespace Rozpisky.Rendering;
/// <summary>
/// <see cref="IProfileRenderer"/>, který místo rasterizace staví <b>ACadSharp entity</b> do jednoho
/// <see cref="CadDocument"/> pro export rozpisek do DXF. Implementuje celý kontrakt rozhraní
/// (6 primitiv), takže má stejné pokrytí jako vykreslovací engine: složitější DXF entity (spline,
/// oblouk, hatch) jsou už ve fázi čtení (<c>DxfTemplate.Replay</c>) převedené na čáry/polylinie/výplně.
/// <para>Zachovává <b>barvu, tloušťku čáry a styl/zarovnání písma</b> přesně podle zdroje stav se
/// přebírá z <see cref="Stav"/> (barva i s původem: ACI index / true color / ByLayer) a z parametru <c>style</c>
/// metody <see cref="Text"/> (font). Souřadnice jsou ve výkresových mm, osa Y nahoru shodně s DXF.
/// Jednořádkový text → <c>TEXT</c>, víceřádkový (<c>maxWidthMm &gt; 0</c>) → <c>MTEXT</c>.</para>
/// </summary>
public sealed class DxfProfileRenderer : IProfileRenderer
{
private readonly CadDocument _doc = new();
private readonly Dictionary<string, AcLayer> _vrstvy = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, AcTextStyle> _styly = new(StringComparer.OrdinalIgnoreCase);
private AcLayer _aktualniVrstva;
private Color _aktualniBarva;
private LineWeightType _aktualniTloustka = LineWeightType.Default;
public DxfProfileRenderer()
{
if (!_doc.Layers.TryGetValue(AcLayer.DefaultName, out _aktualniVrstva!))
{
_aktualniVrstva = new AcLayer(AcLayer.DefaultName);
_doc.Layers.Add(_aktualniVrstva);
}
_vrstvy[_aktualniVrstva.Name] = _aktualniVrstva;
_aktualniBarva = Color.ByLayer;
_doc.Header.Version = VerzeVystupu;
}
/// <summary>Hotový výkres se všemi rozpiskami (pro <c>DxfWriter.Write</c>).</summary>
public CadDocument Document => _doc;
/// <summary>Verze DXF, ve které se výkres zapisuje shodná se zdrojovou šablonou
/// (AutoCAD 2013). Nastavuje se natvrdo: kdyby se nechala na výchozí hodnotě ACadSharp,
/// změnil by ji tiše upgrade balíčku a s ní i to, čím půjde výkres otevřít.</summary>
public const ACadVersion VerzeVystupu = ACadVersion.AC1027;
public void Stav(in KresliciStav s)
{
_aktualniVrstva = NajdiVrstvu(s.Hladina);
_aktualniBarva = NaColor(s.Barva);
_aktualniTloustka = s.TloustkaMm is { } mm ? SnapTloustka(mm) : LineWeightType.ByLayer;
}
/// <summary>Najde/založí hladinu podle popisu ze zdroje. Vlastnosti se berou z tabulky hladin,
/// ne z první entity, která na hladině přistála jinak by se ztratil příznak tisku
/// (<c>!!NOPRINT</c>) a barva hladiny by byla náhodná.</summary>
private AcLayer NajdiVrstvu(HladinaStav h)
{
string name = string.IsNullOrEmpty(h.Nazev) ? AcLayer.DefaultName : h.Nazev;
if (_vrstvy.TryGetValue(name, out var vrstva)) return vrstva;
if (!_doc.Layers.TryGetValue(name, out vrstva!))
{
vrstva = new AcLayer(name);
_doc.Layers.Add(vrstva);
}
vrstva.Color = h.Barva.JeVlastni ? NaColor(h.Barva) : new Color(7);
vrstva.LineWeight = h.TloustkaMm > 0 ? SnapTloustka(h.TloustkaMm) : LineWeightType.Default;
vrstva.PlotFlag = h.Tisknout;
vrstva.IsOn = h.Zapnuta;
if (!string.IsNullOrWhiteSpace(h.TypCary) && _doc.LineTypes.TryGetValue(h.TypCary, out var lt))
vrstva.LineType = lt;
_vrstvy[name] = vrstva;
return vrstva;
}
public void Line(double x1, double y1, double x2, double y2)
=> Pridej(new Line(new XYZ(x1, y1, 0), new XYZ(x2, y2, 0)));
public void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false)
{
if (pts.Count == 0) return;
Pridej(new LwPolyline(pts.Select(p => new XY(p.X, p.Y))) { IsClosed = closed });
}
public void Circle(double cx, double cy, double r)
{
if (r <= 0) return;
Pridej(new Circle(new XYZ(cx, cy, 0), r));
}
public void Stroke(Contour c)
{
var edges = c.Edges;
if (edges.Count == 0) return;
// Samostatný oblouk → věrná entita ARC (bulge polylinie by u velkého rozsahu byla nepřesná).
if (edges.Count == 1 && edges[0].Kind == EdgeKind.Arc && !c.Closed)
{
var e = edges[0];
// ARC se kreslí proti směru hodin start→end; pro záporný rozsah (CW) prohodíme meze.
double start = e.Sweep >= 0 ? e.A0 : e.A0 + e.Sweep;
double end = e.Sweep >= 0 ? e.A0 + e.Sweep : e.A0;
Pridej(new Arc(new XYZ(e.Cx, e.Cy, 0), e.R, start, end));
return;
}
// Jinak LwPolyline s bulge: oblouk → bulge = tan(Sweep/4), úsečka → 0.
var lw = new LwPolyline { IsClosed = c.Closed };
foreach (var e in edges)
lw.Vertices.Add(new LwPolyline.Vertex(new XY(e.SX, e.SY))
{ Bulge = e.Kind == EdgeKind.Arc ? Math.Tan(e.Sweep / 4) : 0 });
if (!c.Closed)
lw.Vertices.Add(new LwPolyline.Vertex(new XY(edges[^1].EX, edges[^1].EY)));
Pridej(lw);
}
public void Text(double x, double y, string text, double heightMm,
HAlign h = HAlign.Left, VAlign v = VAlign.Baseline, double rotationDeg = 0,
TextStyle style = default, double widthFactor = 1, double maxWidthMm = 0)
{
if (string.IsNullOrEmpty(text) || heightMm <= 0) return;
var styl = StylProEntitu(style);
if (maxWidthMm > 0)
{
var mtext = new MText
{
Value = MTextHodnota(text, widthFactor),
InsertPoint = new XYZ(x, y, 0),
Height = heightMm,
// MTEXT nemá zapisovatelné natočení nese ho směrový vektor osy X (group 11).
AlignmentPoint = SmerOsyX(rotationDeg),
RectangleWidth = maxWidthMm,
AttachmentPoint = MapAttach(h, v),
};
if (styl is not null) mtext.Style = styl;
Pridej(mtext);
}
else
{
var t = new TextEntity
{
Value = text,
InsertPoint = new XYZ(x, y, 0),
AlignmentPoint = new XYZ(x, y, 0),
Height = heightMm,
Rotation = rotationDeg * Math.PI / 180.0,
ObliqueAngle = style.ObliqueDeg * Math.PI / 180.0,
WidthFactor = widthFactor <= 0 ? 1 : widthFactor,
HorizontalAlignment = MapH(h),
VerticalAlignment = MapV(v),
};
if (styl is not null) t.Style = styl;
Pridej(t);
}
}
public void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default)
{
if (loops.Count == 0) return;
try
{
// Vzor šrafy se zapisuje zpátky i s definičními čarami DXF je nese uvnitř entity
// HATCH, a bez nich si cílový CAD vzor dosadí ze svých .pat, ve svých jednotkách
// a měřítku. Rozvinuté čáry (Vypln.Cary) jsou naopak jen pro rastrové renderery.
//
// Měřítko a úhel zůstávají neutrální (1 a 0), protože definiční čáry jsou už
// v efektivních hodnotách měřítko i natočení jsou v nich započítané. ACadSharp
// při zápisu násobí rozteč vzoru hodnotou PatternScale, ale při čtení zpátky nedělí;
// s nenulovým měřítkem by se tak vzor s každým kolečkem roztáhl.
var hatch = new Hatch
{
IsSolid = vypln.JeSolid,
Pattern = VzorProHatch(vypln),
PatternScale = 1,
PatternAngle = 0,
};
foreach (var loop in loops)
{
if (loop.Edges.Count == 0) continue;
var path = new Hatch.BoundaryPath();
if (loop.Edges.All(e => e.Kind == EdgeKind.Line))
{
// Samé úsečky → kompaktní polylinie (jako dřív).
path.Edges.Add(new Hatch.BoundaryPath.Polyline(
loop.Edges.Select(e => new XYZ(e.SX, e.SY, 0)), true));
}
else
{
// Obsahuje oblouky → věrné hranové edge (úsečka / kruhový oblouk).
foreach (var e in loop.Edges) path.Edges.Add(HranaNaBoundary(e));
}
hatch.Paths.Add(path);
}
if (hatch.Paths.Count == 0) return;
Pridej(hatch);
}
catch
{
// Záloha: výplň nešla sestavit → alespoň obrys smyček.
foreach (var loop in loops) Stroke(loop with { Closed = true });
}
}
/// <summary>Vzor pro entitu HATCH: jméno a definiční čáry. Bez definičních čar má výstup jen
/// jméno vzoru a cílový CAD si podle něj dosadí vlastní definici v jiných jednotkách
/// a jiném měřítku, takže šrafa vypadá v každém výkresu jinak.</summary>
private static HatchPattern VzorProHatch(Vypln vypln)
{
var vzor = new HatchPattern(vypln.JeSolid ? "SOLID" : vypln.Vzor!);
if (vypln.JeSolid) return vzor;
foreach (var cara in vypln.Definice ?? Array.Empty<VzorCara>())
{
var radek = new HatchPattern.Line
{
Angle = cara.UhelDeg * Math.PI / 180.0,
BasePoint = new XY(cara.Zaklad.X, cara.Zaklad.Y),
Offset = new XY(cara.Offset.X, cara.Offset.Y),
};
foreach (var d in cara.Carky) radek.DashLengths.Add(d);
vzor.Lines.Add(radek);
}
return vzor;
}
/// <summary>Směrový vektor osy X textu pro dané natočení tím se natáčí MTEXT.</summary>
private static XYZ SmerOsyX(double rotationDeg)
{
double rad = rotationDeg * Math.PI / 180.0;
return new XYZ(Math.Cos(rad), Math.Sin(rad), 0);
}
/// <summary>Text pro MTEXT. Escapuje znaky, které jsou v MTEXTu řídicí (zpětné lomítko a
/// složené závorky) bez toho by uživatelova hodnota se zpětným lomítkem rozbila formátování ,
/// zlomy řádků převede na <c>\P</c> a šířkový faktor doplní kódem <c>\W</c>: MTEXT ho na rozdíl
/// od TEXTu nemá jako vlastnost entity, takže jinak by se ztratil.</summary>
private static string MTextHodnota(string text, double widthFactor)
{
var sb = new System.Text.StringBuilder();
if (widthFactor > 0 && Math.Abs(widthFactor - 1) > 1e-9)
sb.Append(@"\W")
.Append(widthFactor.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture))
.Append(';');
foreach (var znak in text)
{
switch (znak)
{
case '\\': sb.Append(@"\\"); break;
case '{': sb.Append(@"\{"); break;
case '}': sb.Append(@"\}"); break;
case '\n': sb.Append(@"\P"); break;
case '\r': break;
default: sb.Append(znak); break;
}
}
return sb.ToString();
}
/// <summary>Převede analytickou hranu obrysu na DXF hraniční edge šrafy (úsečka / kruhový oblouk).
/// U oblouku: záporný rozsah (CW) se v DXF vyjádří <c>CounterClockWise=false</c>, kde čtečka úhly
/// zrcadlí přes osu X proto pro CW meze <c>A0</c> / <c>(A0+Sweep)</c>, znormalizované do [0,2π).</summary>
private static Hatch.BoundaryPath.Edge HranaNaBoundary(Edge e)
{
if (e.Kind == EdgeKind.Line)
return new Hatch.BoundaryPath.Line { Start = new XY(e.SX, e.SY), End = new XY(e.EX, e.EY) };
bool ccw = e.Sweep >= 0;
double sa = Norm(ccw ? e.A0 : -e.A0);
double ea = Norm(ccw ? e.A0 + e.Sweep : -(e.A0 + e.Sweep));
if (ea <= sa) ea += 2 * Math.PI;
return new Hatch.BoundaryPath.Arc
{
Center = new XY(e.Cx, e.Cy),
Radius = e.R,
StartAngle = sa,
EndAngle = ea,
CounterClockWise = ccw,
};
}
private static double Norm(double a)
{
a %= 2 * Math.PI;
return a < 0 ? a + 2 * Math.PI : a;
}
// ── pomocné ────────────────────────────────────────────────────────────
private void Pridej(Entity e)
{
e.Layer = _aktualniVrstva;
e.Color = _aktualniBarva;
e.LineWeight = _aktualniTloustka;
_doc.Entities.Add(e);
}
/// <summary>Najde/vytvoří DXF textový styl pro daný vzhled písma (font soubor + řez). Null = bez
/// stylu (použije se výchozí styl dokumentu).</summary>
private AcTextStyle? StylProEntitu(TextStyle s)
{
// Soubor fontu: přednostně ten z předlohy (.ttf i .shx), jinak odvozeno z rodiny písma.
string? file = s.FontFile ?? (string.IsNullOrWhiteSpace(s.FontFamily) ? null : s.FontFamily + ".ttf");
if (file is null && string.IsNullOrWhiteSpace(s.FontFamily) && string.IsNullOrWhiteSpace(s.Nazev))
return null;
// Klíčem je jméno stylu ze zdroje, pokud ho zná jeden zdrojový styl = jeden styl ve
// výstupu. Bez toho vznikal styl pro každou kombinaci fontu a sklonu a jména se
// vymýšlela (Verdana_1, Arial_2), takže se ve správci stylů nedalo nic najít.
string klic = string.IsNullOrWhiteSpace(s.Nazev)
? $"\0odvozeny|{file}|{s.FontFamily}|{(s.Bold ? "B" : "")}{(s.Italic ? "I" : "")}|{s.ObliqueDeg}"
: s.Nazev!;
if (_styly.TryGetValue(klic, out var styl)) return styl;
var ff = FontFlags.Regular;
if (s.Bold) ff |= FontFlags.Bold;
if (s.Italic) ff |= FontFlags.Italic;
string nazev = string.IsNullOrWhiteSpace(s.Nazev)
? UnikatniNazevStylu(s.FontFamily ?? Path.GetFileNameWithoutExtension(file) ?? "Styl", s.Bold, s.Italic)
: s.Nazev!;
if (_doc.TextStyles.TryGetValue(nazev, out var existujici))
{
_styly[klic] = existujici;
return existujici;
}
// Oblique i na stylu TEXT ho nese sám (group 51), ale MTEXT sklon přebírá jen ze stylu.
styl = new AcTextStyle(nazev)
{
Filename = file ?? "",
TrueType = ff,
ObliqueAngle = s.ObliqueDeg * Math.PI / 180.0,
Flags = s.JeShx ? ACadSharp.Tables.StyleFlags.IsShape : ACadSharp.Tables.StyleFlags.None,
};
_doc.TextStyles.Add(styl);
_styly[klic] = styl;
return styl;
}
private string UnikatniNazevStylu(string zaklad, bool bold, bool italic)
{
var sb = new System.Text.StringBuilder();
foreach (var c in zaklad) sb.Append(char.IsLetterOrDigit(c) ? c : '_');
if (bold) sb.Append("_B");
if (italic) sb.Append("_I");
string nazev = sb.ToString();
if (string.IsNullOrEmpty(nazev)) nazev = "Styl";
string kandidat = nazev;
for (int i = 1; _doc.TextStyles.Contains(kandidat); i++) kandidat = $"{nazev}_{i}";
return kandidat;
}
/// <summary>Barva do DXF <b>ve stejném druhu, v jakém přišla ze zdroje</b>: indexovaná zůstane
/// číslem (group 62), přímá se zapíše jako RGB (group 420) a ByLayer/ByBlock zůstane odkazem.
/// Dřív se všechno zplošťovalo na RGB, takže výkres v CADu přestal reagovat na barvy hladin.</summary>
private static Color NaColor(Barva b) => b.Druh switch
{
BarvaDruh.Index => new Color((short)b.Index),
BarvaDruh.True => new Color((byte)((b.Rgb >> 16) & 0xFF), (byte)((b.Rgb >> 8) & 0xFF), (byte)(b.Rgb & 0xFF)),
BarvaDruh.ByBlock => Color.ByBlock,
_ => Color.ByLayer,
};
/// <summary>Standardní tloušťky čar (1/100 mm) dle DXF; vstupní mm se přichytí k nejbližší.</summary>
private static readonly int[] StdTloustky =
{ 0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158, 200, 211 };
private static LineWeightType SnapTloustka(double mm)
{
if (mm <= 0) return LineWeightType.Default;
int h = (int)Math.Round(mm * 100);
int best = StdTloustky[0], bestDiff = int.MaxValue;
foreach (var s in StdTloustky)
{
int d = Math.Abs(s - h);
if (d < bestDiff) { bestDiff = d; best = s; }
}
return (LineWeightType)best;
}
private static TextHorizontalAlignment MapH(HAlign h) => h switch
{
HAlign.Center => TextHorizontalAlignment.Center,
HAlign.Right => TextHorizontalAlignment.Right,
_ => TextHorizontalAlignment.Left,
};
private static TextVerticalAlignmentType MapV(VAlign v) => v switch
{
VAlign.Top => TextVerticalAlignmentType.Top,
VAlign.Middle => TextVerticalAlignmentType.Middle,
VAlign.Bottom => TextVerticalAlignmentType.Bottom,
_ => TextVerticalAlignmentType.Baseline,
};
private static AttachmentPointType MapAttach(HAlign h, VAlign v) => (v, h) switch
{
(VAlign.Top, HAlign.Left) => AttachmentPointType.TopLeft,
(VAlign.Top, HAlign.Center) => AttachmentPointType.TopCenter,
(VAlign.Top, HAlign.Right) => AttachmentPointType.TopRight,
(VAlign.Middle, HAlign.Left) => AttachmentPointType.MiddleLeft,
(VAlign.Middle, HAlign.Center) => AttachmentPointType.MiddleCenter,
(VAlign.Middle, HAlign.Right) => AttachmentPointType.MiddleRight,
(_, HAlign.Center) => AttachmentPointType.BottomCenter,
(_, HAlign.Right) => AttachmentPointType.BottomRight,
_ => AttachmentPointType.BottomLeft,
};
}