using System.IO; using ACadSharp.Entities; using ACadSharp.IO; using Rozpisky.Dxf; using Rozpisky.Rendering; using Xunit; namespace Rozpisky.Tests; /// /// Analytické oblouky end-to-end: model /, jemnost vzorkování /// (sagitta) a round-trip přes DXF export () + import /// (). Hlídá zejména znaménka (CCW/CW) a převod bulge → oblouk. /// 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.Stav(KresliciStav.Vychozi); 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.Stav(KresliciStav.Vychozi); g.Stroke(contour); var arc = Assert.Single(g.Document.Entities.OfType()); 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; } }