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>
This commit is contained in:
2026-09-07 09:45:49 +02:00
co-authored by Claude Opus 5
commit a6377ab583
107 changed files with 349485 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
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;
}
}