Files
Rozpisky/Rozpisky/Rendering/TransformRenderer.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

154 lines
7.3 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.
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;
}
}