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:
@@ -0,0 +1,55 @@
|
||||
using Rozpisky.Dxf;
|
||||
using Rozpisky.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Bbox pro fit fragmentu (loga do placeholderu): dřív se text počítal jen jako vkládací bod,
|
||||
/// takže textová loga mohla přetéct rámeček. Teď se bbox počítá replayem do BoundsRenderer.
|
||||
/// </summary>
|
||||
public class BoundsTesty
|
||||
{
|
||||
[Fact]
|
||||
public void VsechnaLoga_MajiNeprazdneBounds()
|
||||
{
|
||||
foreach (var name in TestData.VsechnaLoga)
|
||||
{
|
||||
var tpl = new DxfTemplate(TestData.Logo(name));
|
||||
Assert.True(tpl.Bounds is { W: > 0, H: > 0 }, $"{name}: Bounds = {tpl.Bounds}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bounds_ZahrnujiTextIVyplne()
|
||||
{
|
||||
// sprava_zeleznic: 2 texty + 2 SOLID šrafy. Bbox musí obsáhnout všechny smyčky výplní
|
||||
// a být širší než samotné výplně (texty SPRÁVA/ŽELEZNIC vedle značky).
|
||||
var tpl = new DxfTemplate(TestData.Logo("sprava_zeleznic.dxf"));
|
||||
var rec = new RecordingRenderer();
|
||||
tpl.Replay(rec);
|
||||
|
||||
var body = rec.Fills.SelectMany(f => f).SelectMany(l => l).ToList();
|
||||
Assert.NotEmpty(body);
|
||||
var b = tpl.Bounds!.Value;
|
||||
Assert.True(body.All(p => p.X >= b.X - 1e-6 && p.X <= b.X + b.W + 1e-6
|
||||
&& p.Y >= b.Y - 1e-6 && p.Y <= b.Y + b.H + 1e-6));
|
||||
double vyplneW = body.Max(p => p.X) - body.Min(p => p.X);
|
||||
Assert.True(b.W > vyplneW, $"bbox W={b.W:F2} nezahrnuje texty (výplně W={vyplneW:F2})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BoundsRenderer_RespektujeZarovnaniTextu()
|
||||
{
|
||||
// Center/Right roste od kotvy doleva; Top dolů – dřív box rostl vždy jen doprava nahoru.
|
||||
var stred = new BoundsRenderer();
|
||||
stred.Text(0, 0, "ABCD", 10, HAlign.Center, VAlign.Middle);
|
||||
Assert.True(stred.MinX < 0 && stred.MaxX > 0, $"Center: {stred.MinX}..{stred.MaxX}");
|
||||
Assert.True(stred.MinY < 0 && stred.MaxY > 0, $"Middle: {stred.MinY}..{stred.MaxY}");
|
||||
|
||||
var vpravo = new BoundsRenderer();
|
||||
vpravo.Text(0, 0, "ABCD", 10, HAlign.Right, VAlign.Top);
|
||||
Assert.True(vpravo.MinX < 0 && vpravo.MaxX <= 1e-9, $"Right: {vpravo.MinX}..{vpravo.MaxX}");
|
||||
Assert.True(vpravo.MinY < 0 && vpravo.MaxY <= 1e-9, $"Top: {vpravo.MinY}..{vpravo.MaxY}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using Rozpisky.Dxf;
|
||||
using Xunit;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Šrafy (HATCH) s hranovými hranicemi (úsečky, oblouky, elipsy) – dřív se tiše přeskočily,
|
||||
/// takže tečky a oblé výplně v logách chyběly. Hodnoty jsou předpočítané ze skutečných DXF.
|
||||
/// </summary>
|
||||
public class HatchTesty
|
||||
{
|
||||
private static RecordingRenderer Prehraj(string logo)
|
||||
{
|
||||
var rec = new RecordingRenderer();
|
||||
new DxfTemplate(TestData.Logo(logo)).Replay(rec);
|
||||
return rec;
|
||||
}
|
||||
|
||||
private static (double W, double H, double Cx, double Cy) Bbox(IReadOnlyList<(double X, double Y)> loop)
|
||||
{
|
||||
double ax = double.MaxValue, ay = double.MaxValue, bx = double.MinValue, by = double.MinValue;
|
||||
foreach (var (x, y) in loop)
|
||||
{
|
||||
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 + bx) / 2, (ay + by) / 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tesia_TeckaZKruhoveHrany_SeVyplni()
|
||||
{
|
||||
var rec = Prehraj("tesia.dxf");
|
||||
// 2 SOLID hatche: obrys žárovky (polyline s bulge) + tečka (plná kruhová hrana r≈0,78).
|
||||
var smycky = rec.Fills.SelectMany(f => f).ToList();
|
||||
Assert.Equal(2, smycky.Count);
|
||||
// Tečka = menší smyčka; průměr ≈ 1,56 mm (plný kruh z jediné obloukové hrany).
|
||||
var tecka = smycky.OrderBy(l => Bbox(l).W).First();
|
||||
var (w, h, _, _) = Bbox(tecka);
|
||||
Assert.InRange(w, 1.50, 1.62);
|
||||
Assert.InRange(h, 1.50, 1.62);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SudopBrno_MichaneUseckoveAObloukoveHrany_DajiUzavreneSmycky()
|
||||
{
|
||||
var rec = Prehraj("sudopBrno.dxf");
|
||||
// 6 SOLID šraf, všechny jen s hranovými cestami (úsečky + oblouky) → dřív 0 výplní.
|
||||
// Referenční bboxy spočtené nezávisle hustým vzorkováním raw dat hran (512 dílků/oblouk).
|
||||
Assert.Equal(6, rec.Fills.Count);
|
||||
var smycky = rec.Fills.SelectMany(f => f).ToList();
|
||||
Assert.Equal(7, smycky.Count); // jeden hatch má 2 cesty, ostatní po jedné
|
||||
Assert.All(smycky, l => Assert.True(l.Count >= 3));
|
||||
|
||||
// Nezávisle na pořadí (draw order přes SortEntitiesTable může hatche přeuspořádat) ověříme,
|
||||
// že existují smyčky s referenčními bboxy – včetně plné kružnice r=3,825 (7,649²) z jediné
|
||||
// obloukové hrany a řetězů úsečka+oblouk vč. ccw=false (zrcadlené úhly). Chybné znaménko/
|
||||
// otočení hrany nebo ztracená kružnice by referenci znatelně změnily.
|
||||
MaSmyckuBbox(smycky, 7.649, 7.649);
|
||||
MaSmyckuBbox(smycky, 5.571, 0.626);
|
||||
MaSmyckuBbox(smycky, 2.092, 0.066);
|
||||
MaSmyckuBbox(smycky, 7.807, 1.085);
|
||||
MaSmyckuBbox(smycky, 11.290, 1.733);
|
||||
}
|
||||
|
||||
private static void MaSmyckuBbox(IReadOnlyList<IReadOnlyList<(double X, double Y)>> smycky, double refW, double refH)
|
||||
=> Assert.Contains(smycky, l =>
|
||||
{
|
||||
var (w, h, _, _) = Bbox(l);
|
||||
return w >= refW - 0.1 && w <= refW + 0.02 && h >= refH - 0.1 && h <= refH + 0.02;
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void Signalprojekt_KruhovaAEliptickaHrana_SeVyplni()
|
||||
{
|
||||
var rec = Prehraj("signalprojekt.dxf");
|
||||
Assert.Equal(2, rec.Fills.Count);
|
||||
var smycky = rec.Fills.SelectMany(f => f).ToList();
|
||||
// Eliptická cesta: poloosy 1,16 a 0,759·1,16 → bbox ≈ 1,76 × 2,32 (hlavní osa svisle).
|
||||
Assert.Contains(smycky, l =>
|
||||
{
|
||||
var (w, h, _, _) = Bbox(l);
|
||||
double lo = Math.Min(w, h), hi = Math.Max(w, h);
|
||||
return Math.Abs(hi - 2.32) < 0.05 && Math.Abs(lo - 1.76) < 0.05;
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PolylinioveHranice_BezRegrese()
|
||||
{
|
||||
// exprojekt: 3 SOLID šrafy čistě s polyline hranicemi – musí zůstat beze změny.
|
||||
var rec = Prehraj("exprojekt.dxf");
|
||||
Assert.Equal(3, rec.Fills.Count);
|
||||
Assert.All(rec.Fills, f => Assert.All(f, l => Assert.True(l.Count >= 3)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using Rozpisky.Dxf;
|
||||
using Rozpisky.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Čistá byznys logika převodu vstupů na SŽ-kódy a názvy: 47-poziční kód
|
||||
/// (<see cref="KodPrilohy"/>), rozpad čísla objektu (<see cref="ObjektOznaceni"/>) a automatické
|
||||
/// jméno rozvržení (<see cref="Priloha.JmenoRozvrzeniAuto"/>). Hlídá pevnou šířku/pozice kódu –
|
||||
/// tichý posun by jinak zkazil kód na každém výkresu. Očekávané hodnoty jsou worked examples
|
||||
/// z doc-komentářů příslušných metod.
|
||||
/// </summary>
|
||||
public class KodovaniTesty
|
||||
{
|
||||
private static Projekt VzorProjekt() => new()
|
||||
{
|
||||
Skod = "SO12345",
|
||||
StupenDokumentace = "DUSP",
|
||||
};
|
||||
|
||||
private static Priloha VzorPriloha() => new()
|
||||
{
|
||||
CisloObjektu = "SO131.45.12.01",
|
||||
OznaceniCasti = "D",
|
||||
OznaceniDilciCasti = "D110",
|
||||
TypPrilohy = "D1",
|
||||
DilciPriloha = "1",
|
||||
PoradiPrilohy = "001",
|
||||
R1 = "V00",
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Sestav_MaPevnychSedmActyricetZnaku_APozice()
|
||||
{
|
||||
var kod = KodPrilohy.Sestav(VzorProjekt(), VzorPriloha());
|
||||
Assert.Equal(KodPrilohy.Delka, kod.Length);
|
||||
Assert.Equal("SO12345XXXX_DUSP_D110_SO1314512_01_D1_1_001_V00", kod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sestav_PrazdnaPoleDoplniX_ProrezanaOrizne()
|
||||
{
|
||||
var p = new Projekt { Skod = "", StupenDokumentace = "" };
|
||||
var r = new Priloha { CisloObjektu = "", R1 = "" };
|
||||
var kod = KodPrilohy.Sestav(p, r);
|
||||
Assert.Equal(KodPrilohy.Delka, kod.Length);
|
||||
// Samé X kromě separátorů na pevných pozicích 12,17,22,32,35,38,40,44.
|
||||
Assert.Equal("XXXXXXXXXXX_XXXX_XXXX_XXXXXXXXX_XX_XX_X_XXX_XXX", kod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SestavDesky_MaPevnyStred_XX_X_000()
|
||||
{
|
||||
var kod = KodPrilohy.SestavDesky(VzorProjekt(), VzorPriloha());
|
||||
Assert.Equal(KodPrilohy.Delka, kod.Length);
|
||||
Assert.Equal("SO12345XXXX_DUSP_D110_SO1314512_01_XX_X_000_V00", kod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SestavSeznam_PrepiseStredAVoltelnouRevizi()
|
||||
{
|
||||
var kod = KodPrilohy.SestavSeznam(VzorProjekt(), VzorPriloha(), revizeKod: "P01");
|
||||
Assert.Equal(KodPrilohy.Delka, kod.Length);
|
||||
Assert.Equal("SO12345XXXX_DUSP_D110_SO1314512_01_XX_X_001_P01", kod);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("V00", 'V', "00")] // vedoucí znak = typ, zbytek číslo
|
||||
[InlineData("002", '0', "02")] // číslicový typ 0, číslo z dalších číslic
|
||||
[InlineData("", 'X', "XX")] // bez revize → X + XX
|
||||
public void RozpadRevize_SePromitneDoPozic45Az47(string rev, char typ, string cislo)
|
||||
{
|
||||
var r = VzorPriloha();
|
||||
r.R1 = rev;
|
||||
var kod = KodPrilohy.Sestav(VzorProjekt(), r);
|
||||
Assert.Equal(typ, kod[44]); // pozice 45 (0-based 44)
|
||||
Assert.Equal(cislo, kod.Substring(45)); // pozice 46–47
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjektOznaceni_Rozpad_RozdeliNaSlozky()
|
||||
{
|
||||
var o = ObjektOznaceni.Rozpad("SO131.45.12.01");
|
||||
Assert.Equal("SO", o.Prefix);
|
||||
Assert.Equal("131", o.Cislo);
|
||||
Assert.Equal("45", o.Usek);
|
||||
Assert.Equal("12", o.Razeni);
|
||||
Assert.Equal("01", o.Podobjekt);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("SK11", "SK", "11")] // jen řada, bez dalších segmentů
|
||||
[InlineData("PS 22-3_4", "PS", "")] // libovolný oddělovač; "PS" samé písmeno → prázdné číslo
|
||||
public void ObjektOznaceni_Rozpad_ZvladneOddelovaceAKratkeVstupy(string vstup, string prefix, string cislo)
|
||||
{
|
||||
var o = ObjektOznaceni.Rozpad(vstup);
|
||||
Assert.Equal(prefix, o.Prefix);
|
||||
Assert.Equal(cislo, o.Cislo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjektOznaceni_Rozpad_PrazdnyVstupDaSamePrazdne()
|
||||
{
|
||||
var o = ObjektOznaceni.Rozpad("");
|
||||
Assert.Equal(new ObjektOznaceni("", "", "", "", ""), o);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JmenoRozvrzeniAuto_SkladaZObjektuAPrilohy()
|
||||
{
|
||||
var r = VzorPriloha();
|
||||
Assert.Equal("SO1314512_01_D1_1_001", r.JmenoRozvrzeniAuto);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JmenoRozvrzeniAuto_ProDeskyJeObjektovySpecial()
|
||||
{
|
||||
var r = VzorPriloha();
|
||||
r.JeDesky = true;
|
||||
Assert.Equal("SO1314512_01_XX_X_000", r.JmenoRozvrzeniAuto);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NazevObjektovehoSpecialu_TypXXZustavaLiteralne()
|
||||
{
|
||||
Assert.Equal("SO1314512_01_XX_X_001",
|
||||
Priloha.NazevObjektovehoSpecialu("SO131.45.12.01", "001"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.IO;
|
||||
using ACadSharp.Entities;
|
||||
using ACadSharp.IO;
|
||||
using Rozpisky.Dxf;
|
||||
using Rozpisky.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Oblique úhel textu (kurzíva náklonem, DXF group 51/50) – dřív se ignoroval a text
|
||||
/// se kreslil vzpřímeně. „SUDOP BRNO" má v DXF group 51 = 15° a group 41 (width) = 0,7.
|
||||
/// </summary>
|
||||
public class ObliqueTesty
|
||||
{
|
||||
[Fact]
|
||||
public void SudopBrno_TextNeseObliqueAWidthFactor()
|
||||
{
|
||||
var rec = new RecordingRenderer();
|
||||
new DxfTemplate(TestData.Logo("sudopBrno.dxf")).Replay(rec);
|
||||
|
||||
var t = Assert.Single(rec.Texts);
|
||||
Assert.Equal("SUDOP BRNO", t.Text);
|
||||
Assert.Equal(15.0, t.Style.ObliqueDeg, 2); // DXF group 51 = 15.0
|
||||
Assert.Equal(0.7, t.WidthFactor, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OstatniLoga_MajiObliqueNula()
|
||||
{
|
||||
// Regrese: žádné jiné logo oblique nemá – nesmí se objevit nenulový sklon.
|
||||
foreach (var name in TestData.VsechnaLoga.Where(n => n != "sudopBrno.dxf"))
|
||||
{
|
||||
var rec = new RecordingRenderer();
|
||||
new DxfTemplate(TestData.Logo(name)).Replay(rec);
|
||||
Assert.All(rec.Texts, t => Assert.Equal(0, t.Style.ObliqueDeg));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DxfExport_ZachovaObliqueNaTextu()
|
||||
{
|
||||
var dxf = new DxfProfileRenderer();
|
||||
dxf.Layer("0", 0x000000, 0.25);
|
||||
dxf.Text(10, 20, "Zkouška", 2.5, style: new TextStyle("Arial", ObliqueDeg: 3));
|
||||
|
||||
string path = Path.Combine(Path.GetTempPath(), $"rozpisky-oblique-{Guid.NewGuid():N}.dxf");
|
||||
try
|
||||
{
|
||||
DxfWriter.Write(path, dxf.Document, binary: false);
|
||||
var doc = DxfReader.Read(path);
|
||||
var t = Assert.Single(doc.Entities.OfType<TextEntity>());
|
||||
Assert.Equal(3.0 * Math.PI / 180.0, t.ObliqueAngle, 6);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.IO;
|
||||
using Rozpisky.Dxf;
|
||||
using SkiaSharp;
|
||||
using Xunit;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Vyrenderuje všech 9 log do PNG – vizuální porovnání před/po změnách parseru
|
||||
/// (uživatel nemá screenshoty aplikace, výstupní soubory ano). Cílová složka
|
||||
/// z env proměnné <c>ROZPISKY_PNG_OUT</c>, jinak %TEMP%\rozpisky-loga-png.
|
||||
/// </summary>
|
||||
public class PngHarness
|
||||
{
|
||||
[Fact]
|
||||
public void VyrenderujVsechnaLogaDoPng()
|
||||
{
|
||||
string outDir = Environment.GetEnvironmentVariable("ROZPISKY_PNG_OUT")
|
||||
?? Path.Combine(Path.GetTempPath(), "rozpisky-loga-png");
|
||||
Directory.CreateDirectory(outDir);
|
||||
|
||||
const int W = 900, H = 600;
|
||||
foreach (var name in TestData.VsechnaLoga)
|
||||
{
|
||||
var tpl = new DxfTemplate(TestData.Logo(name));
|
||||
using var bmp = new SKBitmap(W, H);
|
||||
using var canvas = new SKCanvas(bmp);
|
||||
Exporters.RenderView(canvas, W, H, g => tpl.Replay(g), tpl.FrameBounds ?? tpl.Bounds,
|
||||
zoom: 1, panXpx: 0, panYpx: 0);
|
||||
using var img = SKImage.FromBitmap(bmp);
|
||||
using var data = img.Encode(SKEncodedImageFormat.Png, 100);
|
||||
using var fs = File.Create(Path.Combine(outDir, Path.ChangeExtension(name, ".png")));
|
||||
data.SaveTo(fs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Rozpisky.Rendering;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Testovací <see cref="IProfileRenderer"/>: nic nekreslí, jen zaznamenává volání primitiv,
|
||||
/// aby testy mohly assertovat, co přesně parser (<c>DxfTemplate.Replay</c>) vyprodukoval.
|
||||
/// Obrysy (<see cref="Contour"/>) se pro pohodlné assertování bboxů zaznamenávají jak analyticky
|
||||
/// (<see cref="FillContours"/>, <see cref="StrokeContours"/>), tak jemně zploštěné na body
|
||||
/// (<see cref="Fills"/>, <see cref="Polylines"/>) – zpětně kompatibilní s dřívějšími testy.
|
||||
/// </summary>
|
||||
public sealed class RecordingRenderer : IProfileRenderer
|
||||
{
|
||||
public sealed record TextCall(double X, double Y, string Text, double HeightMm, HAlign H, VAlign V,
|
||||
double RotationDeg, TextStyle Style, double WidthFactor, double MaxWidthMm);
|
||||
|
||||
/// <summary>Každé volání Fill = smyčky jednoho HATCH, zploštěné na body.</summary>
|
||||
public List<IReadOnlyList<IReadOnlyList<(double X, double Y)>>> Fills { get; } = new();
|
||||
/// <summary>Analytické obrysy jednotlivých volání Fill (se zachovanými oblouky).</summary>
|
||||
public List<IReadOnlyList<Contour>> FillContours { get; } = new();
|
||||
/// <summary>Analytické obrysy z volání Stroke (bulge polylinie, samostatné oblouky).</summary>
|
||||
public List<Contour> StrokeContours { get; } = new();
|
||||
public List<TextCall> Texts { get; } = new();
|
||||
/// <summary>Zploštěné polylinie i obrysy ze Stroke (kvůli zpětné kompatibilitě testů).</summary>
|
||||
public List<IReadOnlyList<(double X, double Y)>> Polylines { get; } = new();
|
||||
public int LineCount { get; private set; }
|
||||
public int CircleCount { get; private set; }
|
||||
|
||||
public void Layer(string name, int rgb, double lineWeightMm = 0.25) { }
|
||||
|
||||
public void Line(double x1, double y1, double x2, double y2) => LineCount++;
|
||||
|
||||
public void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false) => Polylines.Add(pts);
|
||||
|
||||
public void Circle(double cx, double cy, double r) => CircleCount++;
|
||||
|
||||
public void Stroke(Contour c)
|
||||
{
|
||||
StrokeContours.Add(c);
|
||||
Polylines.Add(Zplosti(c));
|
||||
}
|
||||
|
||||
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)
|
||||
=> Texts.Add(new TextCall(x, y, text, heightMm, h, v, rotationDeg, style, widthFactor, maxWidthMm));
|
||||
|
||||
public void Fill(IReadOnlyList<Contour> loops)
|
||||
{
|
||||
FillContours.Add(loops);
|
||||
Fills.Add(loops.Select(Zplosti).ToList());
|
||||
}
|
||||
|
||||
/// <summary>Zploští obrys na body (jemně, sagitta ≤ 0,02 mm) – pro bbox asserty v testech.</summary>
|
||||
private static IReadOnlyList<(double X, double Y)> Zplosti(Contour c)
|
||||
{
|
||||
var pts = new List<(double X, double Y)>();
|
||||
foreach (var e in c.Edges) e.AppendPoints(pts, maxSagitta: 0.02);
|
||||
if (c.Edges.Count > 0) pts.Add((c.Edges[^1].EX, c.Edges[^1].EY));
|
||||
return pts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Rozpisky\Rozpisky.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,53 @@
|
||||
using Rozpisky.Rendering;
|
||||
using SkiaSharp;
|
||||
using Xunit;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Skutečný render Skií (ne jen zplošťení obrysu): plná kružnice tvořená jedinou obloukovou
|
||||
/// hranou (sweep 360°) se musí vykreslit, ne „propadnout" degenerovaným <c>ArcTo</c>.
|
||||
/// </summary>
|
||||
public class SkiaRenderTesty
|
||||
{
|
||||
/// <summary>Vykreslí kresbu na 200×200 px bitmapu (mm 1:1, střed plátna = mm (0,0)) a vrátí
|
||||
/// počet nebílých pixelů.</summary>
|
||||
private static int PocetVyplnenych(Action<IProfileRenderer> draw)
|
||||
{
|
||||
const int S = 200;
|
||||
using var bmp = new SKBitmap(S, S);
|
||||
using var canvas = new SKCanvas(bmp);
|
||||
canvas.Clear(SKColors.White);
|
||||
SKPoint Map(double x, double y) => new((float)(S / 2 + x * 10), (float)(S / 2 - y * 10)); // 10 px/mm, Y nahoru
|
||||
using (var r = new SkiaProfileRenderer(canvas, Map, pxPerMm: 10))
|
||||
draw(r);
|
||||
|
||||
int n = 0;
|
||||
for (int y = 0; y < S; y++)
|
||||
for (int x = 0; x < S; x++)
|
||||
if (bmp.GetPixel(x, y) != SKColors.White) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlnaKruznice_JakoJednaObloukovaHrana_SeVyplni()
|
||||
{
|
||||
// Kruh r=5 mm ze středu (0,0), jediná Arc hrana se sweepem 360°.
|
||||
var edge = Edge.Arc(5, 0, 5, 0, 0, 0, r: 5, a0: 0, sweep: 2 * Math.PI);
|
||||
var contour = new Contour(new[] { edge }, Closed: true);
|
||||
|
||||
int filled = PocetVyplnenych(g => { g.Layer("0", 0); g.Fill(new[] { contour }); });
|
||||
|
||||
// Plocha kruhu r=5 mm × (10 px/mm)² = π·2500 ≈ 7854 px; tolerantně > 5000.
|
||||
Assert.True(filled > 5000, $"plná kružnice se nevykreslila (vyplněno {filled} px)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObloukovyOblouk_SeVykresli_Stroke()
|
||||
{
|
||||
// Kontrolní: běžný oblouk (< 360°) přes Stroke musí něco nakreslit.
|
||||
var edge = Edge.Arc(5, 0, 0, 5, 0, 0, r: 5, a0: 0, sweep: Math.PI / 2);
|
||||
int filled = PocetVyplnenych(g => { g.Layer("0", 0); g.Stroke(new Contour(new[] { edge }, Closed: false)); });
|
||||
Assert.True(filled > 50, $"oblouk se nevykreslil (vyplněno {filled} px)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.IO;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>Cesty k testovacím datům (loga DXF dodávaná s programem).</summary>
|
||||
public static class TestData
|
||||
{
|
||||
/// <summary>Kořen repozitáře – nalezen od bin složky testů směrem nahoru podle Rozpisky.sln.</summary>
|
||||
public static string RepoRoot { get; } = NajdiKoren();
|
||||
|
||||
/// <summary>Plná cesta k logu ve zdrojové složce <c>Rozpisky\loga</c>.</summary>
|
||||
public static string Logo(string fileName) => Path.Combine(RepoRoot, "Rozpisky", "loga", fileName);
|
||||
|
||||
/// <summary>Všech 9 log dodávaných s programem.</summary>
|
||||
public static readonly string[] VsechnaLoga =
|
||||
{
|
||||
"eu_op_doprava.dxf", "md_sfdi.dxf", "sprava_zeleznic.dxf", "exprojekt.dxf",
|
||||
"signalprojekt.dxf", "tesia.dxf", "mco.dxf", "exprojekt+mco.dxf", "sudopBrno.dxf",
|
||||
};
|
||||
|
||||
private static string NajdiKoren()
|
||||
{
|
||||
var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (dir is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(dir.FullName, "Rozpisky.sln"))) return dir.FullName;
|
||||
dir = dir.Parent!;
|
||||
}
|
||||
throw new InvalidOperationException("Rozpisky.sln nenalezen nad " + AppContext.BaseDirectory);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user