Files
Rozpisky/Rozpisky/Rendering/DxfProfileRenderer.cs
T
marekandClaude Opus 5 a900acc7bb Entity: rozvinout bloky, doplnit elipsy a hlásit, co program neumí
Switch v Replay neměl default větev: INSERT, ELLIPSE, POINT, SOLID, 3DFACE
a POLYLINE3D propadly bez akce a bez záznamu. V dodávaných souborech žádná
z nich není, ale uživatel si přidává vlastní loga – to nejhorší, co se mu může
stát, je logo, které se nevykreslí a nikdo mu to neřekne.

- INSERT se rozvine rekurzivně (vztažný bod, měřítko os, natočení), s ochranou
  proti bloku vkládajícímu sám sebe. Uvnitř bloku se hladina „0" a barva
  ByBlock berou od vkládající entity – proto jde stejný blok vložit dvakrát
  a pokaždé v jiné barvě.
- TransformRenderer umí celou afinní transformaci, ne jen měřítko a posun.
  Oblouk zůstane obloukem jen u podobnosti bez zrcadlení; jinak by z kružnice
  byla elipsa, kterou kreslicí rozhraní nezná, tak se rozloží na lomenou čáru.
- ELLIPSE, POINT, SOLID, 3DFACE, POLYLINE3D se kreslí. U SOLID pozor na
  prohozený 3. a 4. roh, jinak vyjdou přesýpací hodiny.
- Zbytek se počítá do DxfTemplate.NezpracovaneEntity a souhrn exportu (PDF
  i DXF) ho vypíše. Export se kvůli tomu nezastaví.
- Spline se dělí podle délky (dřív pevných 64 dílků – málo pro dlouhé křivky,
  zbytečně moc pro krátké).
- Verze výstupního DXF se zafixovala na AC1027 (jako zdrojová šablona). Dřív ji
  určoval default ACadSharp, který může upgrade balíčku tiše změnit.

Šablona i všech 9 log projdou s prázdným počítadlem (test). Render log do PNG
je pixel po pixelu shodný s předchozím stavem. 114 testů zelených.

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

389 lines
17 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 jménem, měřítkem a úhlem v CADu tak zůstane
// editovatelný. Rozvinuté čáry (Vypln.Cary) jsou jen pro rastrové renderery.
var hatch = new Hatch
{
IsSolid = vypln.JeSolid,
Pattern = new HatchPattern(vypln.JeSolid ? "SOLID" : vypln.Vzor!),
PatternScale = vypln.JeSolid || vypln.Meritko <= 0 ? 1 : vypln.Meritko,
PatternAngle = vypln.JeSolid ? 0 : vypln.UhelDeg * Math.PI / 180.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>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,
};
}