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>
154 lines
7.3 KiB
C#
154 lines
7.3 KiB
C#
namespace Rozpisky.Rendering;
|
||
|
||
/// <summary>
|
||
/// Dekorátor <see cref="IProfileRenderer"/>, který na souřadnice aplikuje afinní transformaci
|
||
/// <c>X' = A·x + C·y + Tx</c>, <c>Y' = B·x + D·y + Ty</c>.
|
||
/// <para>Dvě použití: umístění externího DXF fragmentu (logo / orientační schéma) do rámečku
|
||
/// rozpisky – tam stačí měřítko a posun (<see cref="TransformRenderer(IProfileRenderer,double,double,double)"/>)
|
||
/// – a rozvinutí vloženého bloku (<c>INSERT</c>), kde přibude natočení a nestejné měřítko os
|
||
/// (<see cref="ProBlok"/>).</para>
|
||
/// <para>Kruhový oblouk zůstane obloukem jen tehdy, je-li transformace <b>podobnost</b> (stejné
|
||
/// měřítko obou os a kolmé osy). Při nestejném měřítku by z něj byla elipsa, kterou kreslicí
|
||
/// rozhraní nezná – tam se oblouk rozloží na lomenou čáru.</para>
|
||
/// </summary>
|
||
public sealed class TransformRenderer : IProfileRenderer
|
||
{
|
||
private readonly IProfileRenderer _inner;
|
||
private readonly double _a, _b, _c, _d, _tx, _ty;
|
||
|
||
/// <summary>Měřítko délek (u podobnosti přesné, jinak odmocnina z determinantu).</summary>
|
||
private readonly double _meritko;
|
||
|
||
/// <summary>Natočení transformace ve stupních – přičítá se k natočení textů.</summary>
|
||
private readonly double _uhelDeg;
|
||
|
||
/// <summary>true = oblouky a kružnice zůstanou oblouky a kružnicemi. Platí pro podobnost bez
|
||
/// zrcadlení; při nestejném měřítku os by z kružnice byla elipsa a při zrcadlení by se obrátil
|
||
/// smysl oblouku – v obou případech je bezpečnější rozložit tvar na lomenou čáru.</summary>
|
||
private readonly bool _zachovavaOblouky;
|
||
|
||
/// <summary>Uniformní měřítko + posun, bez natočení (umístění fragmentu do rámečku).</summary>
|
||
public TransformRenderer(IProfileRenderer inner, double scale, double offsetX, double offsetY)
|
||
: this(inner, scale, 0, 0, scale, offsetX, offsetY) { }
|
||
|
||
private TransformRenderer(IProfileRenderer inner, double a, double b, double c, double d, double tx, double ty)
|
||
{
|
||
_inner = inner;
|
||
_a = a; _b = b; _c = c; _d = d; _tx = tx; _ty = ty;
|
||
|
||
double delkaX = Math.Sqrt(a * a + b * b), delkaY = Math.Sqrt(c * c + d * d);
|
||
double skalar = a * c + b * d; // 0 = osy jsou kolmé
|
||
bool podobnost = Math.Abs(delkaX - delkaY) <= 1e-9 * Math.Max(delkaX, 1)
|
||
&& Math.Abs(skalar) <= 1e-9 * Math.Max(delkaX * delkaY, 1);
|
||
|
||
double det = a * d - b * c;
|
||
_zachovavaOblouky = podobnost && det > 0;
|
||
_meritko = podobnost ? delkaX : Math.Sqrt(Math.Abs(det));
|
||
_uhelDeg = Math.Atan2(b, a) * 180 / Math.PI;
|
||
}
|
||
|
||
/// <summary>Transformace vloženého bloku: entity bloku jsou zapsané vůči jeho vztažnému bodu,
|
||
/// ten se klade na vkládací bod. Tedy <c>p' = vkládací + R·S·(p − vztažný)</c>. Nulové měřítko
|
||
/// (setkáno v poškozených souborech) se bere jako 1, jinak by blok zkolaboval do bodu.</summary>
|
||
public static TransformRenderer ProBlok(
|
||
IProfileRenderer inner, double vlozX, double vlozY, double zakladX, double zakladY,
|
||
double meritkoX, double meritkoY, double natoceniRad)
|
||
{
|
||
if (meritkoX == 0) meritkoX = 1;
|
||
if (meritkoY == 0) meritkoY = 1;
|
||
double cos = Math.Cos(natoceniRad), sin = Math.Sin(natoceniRad);
|
||
double a = meritkoX * cos, b = meritkoX * sin, c = -meritkoY * sin, d = meritkoY * cos;
|
||
return new TransformRenderer(
|
||
inner, a, b, c, d,
|
||
vlozX - (a * zakladX + c * zakladY),
|
||
vlozY - (b * zakladX + d * zakladY));
|
||
}
|
||
|
||
private double X(double x, double y) => _tx + _a * x + _c * y;
|
||
private double Y(double x, double y) => _ty + _b * x + _d * y;
|
||
|
||
public void Stav(in KresliciStav s)
|
||
=> _inner.Stav(s with
|
||
{
|
||
TloustkaMm = s.TloustkaMm * _meritko,
|
||
Hladina = s.Hladina with { TloustkaMm = s.Hladina.TloustkaMm * _meritko },
|
||
});
|
||
|
||
public void Line(double x1, double y1, double x2, double y2)
|
||
=> _inner.Line(X(x1, y1), Y(x1, y1), X(x2, y2), Y(x2, y2));
|
||
|
||
public void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false)
|
||
=> _inner.Polyline(pts.Select(p => (X(p.X, p.Y), Y(p.X, p.Y))).ToList(), closed);
|
||
|
||
public void Circle(double cx, double cy, double r)
|
||
{
|
||
if (_zachovavaOblouky) { _inner.Circle(X(cx, cy), Y(cx, cy), r * _meritko); return; }
|
||
|
||
// Nestejné měřítko os → z kružnice je elipsa; kreslicí rozhraní ji nezná, tedy lomená čára.
|
||
int n = Math.Max(16, ContourGeometry.ArcSegments(2 * Math.PI, Math.Max(r * _meritko, 1e-9), 0.02));
|
||
var body = new List<(double X, double Y)>(n);
|
||
for (int i = 0; i < n; i++)
|
||
{
|
||
double t = 2 * Math.PI * i / n;
|
||
double x = cx + r * Math.Cos(t), y = cy + r * Math.Sin(t);
|
||
body.Add((X(x, y), Y(x, y)));
|
||
}
|
||
_inner.Polyline(body, closed: true);
|
||
}
|
||
|
||
public void Stroke(Contour c)
|
||
{
|
||
if (_zachovavaOblouky) { _inner.Stroke(new Contour(c.Edges.Select(T).ToList(), c.Closed)); return; }
|
||
_inner.Polyline(Zplosti(c), c.Closed);
|
||
}
|
||
|
||
/// <summary>Přemapuje hranu (jen pro transformace zachovávající oblouky): body afinně,
|
||
/// poloměr ·měřítko, počáteční úhel o natočení, rozsah beze změny.</summary>
|
||
private Edge T(Edge e)
|
||
{
|
||
if (e.Kind == EdgeKind.Line)
|
||
return Edge.Line(X(e.SX, e.SY), Y(e.SX, e.SY), X(e.EX, e.EY), Y(e.EX, e.EY));
|
||
|
||
return Edge.Arc(
|
||
X(e.SX, e.SY), Y(e.SX, e.SY), X(e.EX, e.EY), Y(e.EX, e.EY),
|
||
X(e.Cx, e.Cy), Y(e.Cx, e.Cy), e.R * _meritko, e.A0 + _uhelDeg * Math.PI / 180, e.Sweep);
|
||
}
|
||
|
||
private List<(double X, double Y)> Zplosti(Contour c)
|
||
{
|
||
var body = new List<(double X, double Y)>();
|
||
foreach (var e in c.Edges) e.AppendPoints(body, 0.02);
|
||
if (c.Edges.Count > 0) body.Add((c.Edges[^1].EX, c.Edges[^1].EY));
|
||
return body.Select(p => (X(p.X, p.Y), Y(p.X, p.Y))).ToList();
|
||
}
|
||
|
||
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)
|
||
=> _inner.Text(X(x, y), Y(x, y), text, heightMm * _meritko, h, v, rotationDeg + _uhelDeg,
|
||
style, widthFactor, maxWidthMm * _meritko);
|
||
|
||
public void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default)
|
||
=> _inner.Fill(
|
||
loops.Select(c => new Contour(
|
||
_zachovavaOblouky ? c.Edges.Select(T).ToList() : Usecky(c), c.Closed)).ToList(),
|
||
vypln.Cary is null
|
||
? vypln
|
||
: vypln with
|
||
{
|
||
Cary = vypln.Cary
|
||
.Select(u => new Usecka(X(u.X1, u.Y1), Y(u.X1, u.Y1), X(u.X2, u.Y2), Y(u.X2, u.Y2)))
|
||
.ToList(),
|
||
});
|
||
|
||
/// <summary>Obrys rozložený na úsečky (pro transformace, které oblouk nezachovají).</summary>
|
||
private List<Edge> Usecky(Contour c)
|
||
{
|
||
var body = Zplosti(c);
|
||
var hrany = new List<Edge>(Math.Max(body.Count - 1, 0));
|
||
for (int i = 0; i + 1 < body.Count; i++)
|
||
hrany.Add(Edge.Line(body[i].X, body[i].Y, body[i + 1].X, body[i + 1].Y));
|
||
return hrany;
|
||
}
|
||
}
|