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>
389 lines
17 KiB
C#
389 lines
17 KiB
C#
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 > 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,
|
||
};
|
||
}
|