Files
Rozpisky/Rozpisky.Tests/ObloukyTesty.cs
T
marekandClaude Opus 5 a6377ab583 Baseline: současný stav projektu Rozpisky
První commit — WPF aplikace pro generování výkresových rohových razítek
(.NET 10, SkiaSharp, ACadSharp, ClosedXML) včetně xUnit testů, DXF šablony,
log a dokumentace. Build výstupy (bin/, obj/, .vs/) jsou ignorovány.

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

139 lines
5.9 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.Entities;
using ACadSharp.IO;
using Rozpisky.Dxf;
using Rozpisky.Rendering;
using Xunit;
namespace Rozpisky.Tests;
/// <summary>
/// Analytické oblouky end-to-end: model <see cref="Edge"/>/<see cref="Contour"/>, jemnost vzorkování
/// (sagitta) a round-trip přes DXF export (<see cref="DxfProfileRenderer"/>) + import
/// (<see cref="DxfTemplate"/>). Hlídá zejména znaménka (CCW/CW) a převod bulge → oblouk.
/// </summary>
public class ObloukyTesty
{
private static (double W, double H, double MinX, double MinY) Bbox(IEnumerable<(double X, double Y)> pts)
{
double ax = double.MaxValue, ay = double.MaxValue, bx = double.MinValue, by = double.MinValue;
foreach (var (x, y) in pts) { ax = Math.Min(ax, x); ay = Math.Min(ay, y); bx = Math.Max(bx, x); by = Math.Max(by, y); }
return (bx - ax, by - ay, ax, ay);
}
private static List<(double X, double Y)> Zplosti(Contour c, double tol = 0.005)
{
var pts = new List<(double X, double Y)>();
foreach (var e in c.Edges) e.AppendPoints(pts, tol);
if (c.Edges.Count > 0) pts.Add((c.Edges[^1].EX, c.Edges[^1].EY));
return pts;
}
[Fact]
public void Edge_PointAt_A_Reversed_SediKoncoveBody()
{
// Čtvrtkružnice jednotkového kruhu proti směru hodin z (1,0) do (0,1).
var e = Edge.Arc(1, 0, 0, 1, 0, 0, r: 1, a0: 0, sweep: Math.PI / 2);
var s = e.PointAt(e.A0);
var en = e.PointAt(e.A0 + e.Sweep);
Assert.Equal(1, s.X, 6); Assert.Equal(0, s.Y, 6);
Assert.Equal(0, en.X, 6); Assert.Equal(1, en.Y, 6);
var r = e.Reversed();
Assert.Equal(e.EX, r.SX, 9); Assert.Equal(e.SX, r.EX, 9);
Assert.Equal(Math.PI / 2, r.A0, 9);
Assert.Equal(-Math.PI / 2, r.Sweep, 9);
// Otočená hrana má stejný geometrický počátek jako původní konec.
var rs = r.PointAt(r.A0);
Assert.Equal(0, rs.X, 6); Assert.Equal(1, rs.Y, 6);
}
[Theory]
[InlineData(10.0, 0.02)]
[InlineData(3.825, 0.02)]
[InlineData(50.0, 0.05)]
public void ArcSegments_DodrziSagittu(double r, double tol)
{
double sweep = 2 * Math.PI;
int n = ContourGeometry.ArcSegments(sweep, r, tol);
double delta = sweep / n;
double sagitta = r * (1 - Math.Cos(delta / 2));
Assert.True(sagitta <= tol + 1e-9, $"sagitta {sagitta} > tol {tol} (n={n})");
}
[Fact]
public void AnalytickyKruh_ZplosteniDaSpravnyBbox()
{
// Jediná Arc hrana přes celý kruh (r=3,825) → bbox = průměr, ostrý na tětivovou toleranci.
var e = Edge.Arc(3.825, 0, 3.825, 0, 0, 0, r: 3.825, a0: 0, sweep: 2 * Math.PI);
var (w, h, _, _) = Bbox(Zplosti(new Contour(new[] { e }, Closed: true)));
Assert.InRange(w, 7.64, 7.65);
Assert.InRange(h, 7.64, 7.65);
}
[Theory]
[InlineData(1.0)] // CCW horní půlkruh
[InlineData(-1.0)] // CW dolní půlkruh hlídá normalizaci úhlů v exportu i importu
public void RoundTrip_HatchOblouk_ZachovaGeometrii(double smer)
{
double r = 5;
double sweep = smer * Math.PI;
// Půldisk: oblouk (5,0)→(-5,0) + úsečka zpět.
var arc = Edge.Arc(5, 0, -5, 0, 0, 0, r, a0: 0, sweep: sweep);
var line = Edge.Line(-5, 0, 5, 0);
var contour = new Contour(new[] { arc, line }, Closed: true);
var g = new DxfProfileRenderer();
g.Layer("0", 0x000000);
g.Fill(new[] { contour });
// Export → dočasný DXF → import zpět a porovnání bboxu (sagitta ≤ 0,02 mm).
string path = Path.Combine(Path.GetTempPath(), $"rozpisky_arc_{Guid.NewGuid():N}.dxf");
try
{
DxfWriter.Write(path, g.Document, binary: false);
var rec = new RecordingRenderer();
new DxfTemplate(path).Replay(rec);
var loop = Assert.Single(Assert.Single(rec.FillContours));
var (w, h, minX, minY) = Bbox(Zplosti(loop, 0.02));
Assert.InRange(w, 9.9, 10.05); // šířka = 2r
Assert.InRange(h, 4.95, 5.05); // výška = r
Assert.InRange(minX, -5.05, -4.95);
if (smer > 0) Assert.InRange(minY, -0.05, 0.05); // horní půlkruh: y ∈ [0, 5]
else Assert.InRange(minY, -5.05, -4.95); // dolní půlkruh: y ∈ [-5, 0]
}
finally { if (File.Exists(path)) File.Delete(path); }
}
[Theory]
[InlineData(1.0)]
[InlineData(-1.0)]
public void StandaloneArc_Export_DaEntituArcSpravnymUhlem(double smer)
{
double r = 4, a0 = 0.3, sweep = smer * 1.2;
var edge = Edge.Arc(0, 0, 0, 0, cx: 1, cy: 2, r: r, a0: a0, sweep: sweep);
// dopočti skutečné koncové body
var s = edge.PointAt(a0); var e = edge.PointAt(a0 + sweep);
var contour = new Contour(new[] { Edge.Arc(s.X, s.Y, e.X, e.Y, 1, 2, r, a0, sweep) }, Closed: false);
var g = new DxfProfileRenderer();
g.Layer("0", 0);
g.Stroke(contour);
var arc = Assert.Single(g.Document.Entities.OfType<Arc>());
Assert.Equal(1, arc.Center.X, 6);
Assert.Equal(2, arc.Center.Y, 6);
Assert.Equal(r, arc.Radius, 6);
// ARC entita je vždy CCW start→end; pro CW rozsah se meze prohodí, ale koncové BODY musí sedět.
var pStart = (arc.Center.X + r * Math.Cos(arc.StartAngle), arc.Center.Y + r * Math.Sin(arc.StartAngle));
var pEnd = (arc.Center.X + r * Math.Cos(arc.EndAngle), arc.Center.Y + r * Math.Sin(arc.EndAngle));
// množina koncových bodů oblouku se musí shodovat (v jednom z pořadí).
bool ok = (Blizko(pStart, s) && Blizko(pEnd, e)) || (Blizko(pStart, e) && Blizko(pEnd, s));
Assert.True(ok, $"koncové body oblouku nesedí: start={pStart} end={pEnd} vs s={s} e={e}");
static bool Blizko((double X, double Y) a, (double X, double Y) b)
=> Math.Abs(a.X - b.X) < 1e-6 && Math.Abs(a.Y - b.Y) < 1e-6;
}
}