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>
This commit is contained in:
2026-09-07 14:46:02 +02:00
co-authored by Claude Opus 5
parent 001afc2d81
commit a900acc7bb
8 changed files with 548 additions and 45 deletions
+194
View File
@@ -0,0 +1,194 @@
using ACadSharp;
using ACadSharp.Entities;
using ACadSharp.Tables;
using CSMath;
using Rozpisky.Dxf;
using Rozpisky.Rendering;
using Xunit;
using AcColor = ACadSharp.Color;
namespace Rozpisky.Tests;
/// <summary>
/// Entity, které dřív propadly <c>switch</c>em bez <c>default</c> větve, a tedy zmizely tiše:
/// vložené bloky (INSERT), elipsy, body, vyplněné čtyřúhelníky. Uživatel si přidává vlastní
/// loga, takže „nevykreslí se a nikdo se to nedozví" je nejhorší možné chování.
/// </summary>
public class EntityTesty
{
/// <summary>Přehraje dokument sestavený v paměti přes dočasný soubor (DxfTemplate čte ze souboru).</summary>
private static (RecordingRenderer G, IReadOnlyDictionary<string, int> Nezpracovane) Prehraj(CadDocument doc)
{
string cesta = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
"rozpisky-entity-" + Guid.NewGuid().ToString("N") + ".dxf");
try
{
ACadSharp.IO.DxfWriter.Write(cesta, doc, binary: true);
var tpl = new DxfTemplate(cesta);
var g = new RecordingRenderer();
tpl.Replay(g);
return (g, tpl.NezpracovaneEntity);
}
finally
{
try { System.IO.File.Delete(cesta); } catch (System.IO.IOException) { }
}
}
/// <summary>Dokument s jedním blokem (čtvercová čára 0,010,10) a jeho vložením.</summary>
private static CadDocument SBlokem(Action<Insert> uprav, AcColor? barvaCary = null)
{
var doc = new CadDocument();
var blok = new BlockRecord("ctverec");
blok.Entities.Add(new Line(new XYZ(0, 0, 0), new XYZ(10, 0, 0))
{
Color = barvaCary ?? AcColor.ByLayer,
});
doc.BlockRecords.Add(blok);
var ins = new Insert(blok);
uprav(ins);
doc.Entities.Add(ins);
return doc;
}
[Fact]
public void VsechnaDodavanaDataSeVykresliBezeZbytku()
{
// Šablona i všech 9 log musí projít beze zbytku jinak se v nich něco nekreslí.
var sablona = new DxfTemplate(TestData.Sablona);
sablona.Replay(new RecordingRenderer(), values: null, includeNoPrint: true);
Assert.Empty(sablona.NezpracovaneEntity);
foreach (var logo in TestData.VsechnaLoga)
{
var tpl = new DxfTemplate(TestData.Logo(logo));
tpl.Replay(new RecordingRenderer());
Assert.True(tpl.NezpracovaneEntity.Count == 0,
$"{logo}: nezpracované entity {string.Join(", ", tpl.NezpracovaneEntity.Select(p => $"{p.Key}×{p.Value}"))}");
}
}
[Fact]
public void VlozenyBlokSeRozvineNaSpravnemMiste()
{
var (g, nezpracovane) = Prehraj(SBlokem(ins => ins.InsertPoint = new XYZ(100, 50, 0)));
Assert.Empty(nezpracovane);
Assert.Equal(1, g.LineCount);
Assert.Equal((100, 50), g.Lines[0].Od);
Assert.Equal((110, 50), g.Lines[0].Do);
}
[Fact]
public void MeritkoANatoceniBlokuSeProjevi()
{
var (g, _) = Prehraj(SBlokem(ins =>
{
ins.InsertPoint = new XYZ(0, 0, 0);
ins.XScale = 2;
ins.YScale = 2;
ins.Rotation = Math.PI / 2; // 90° → úsečka z osy X na osu Y
}));
Assert.Equal(1, g.LineCount);
var (od, doB) = g.Lines[0];
Assert.Equal(0, od.X, 6);
Assert.Equal(0, od.Y, 6);
Assert.Equal(0, doB.X, 6);
Assert.Equal(20, doB.Y, 6);
}
[Fact]
public void BarvaByBlockSeVezmeZVkladajiciEntity()
{
// Tohle je smysl ByBlock: stejný blok vložený dvakrát může mít pokaždé jinou barvu.
var (g, _) = Prehraj(SBlokem(
ins => { ins.InsertPoint = new XYZ(0, 0, 0); ins.Color = new AcColor(5); },
barvaCary: AcColor.ByBlock));
// Stavů jsou dva: jeden za samotný INSERT, druhý za úsečku uvnitř bloku zajímá nás ten druhý.
var stav = g.PosledniStav;
Assert.Equal(BarvaDruh.Index, stav.Barva.Druh);
Assert.Equal(5, stav.Barva.Index);
}
[Fact]
public void ElipsaSeVykresliJakoLomenaCara()
{
var doc = new CadDocument();
doc.Entities.Add(new Ellipse
{
Center = new XYZ(0, 0, 0),
MajorAxisEndPoint = new XYZ(10, 0, 0),
RadiusRatio = 0.5,
StartParameter = 0,
EndParameter = 2 * Math.PI,
});
var (g, nezpracovane) = Prehraj(doc);
Assert.Empty(nezpracovane);
var body = Assert.Single(g.Polylines);
Assert.True(body.Count >= 8);
// Body musí ležet na elipse s poloosami 10 a 5.
foreach (var (x, y) in body)
Assert.Equal(1.0, x * x / 100 + y * y / 25, 3);
}
[Fact]
public void SolidSeVyplniVeSpravnemPoradiRohu()
{
// V DXF jsou 3. a 4. roh proti obvodu prohozené při špatném pořadí vyjde přesýpací hodiny.
var doc = new CadDocument();
doc.Entities.Add(new Solid
{
FirstCorner = new XYZ(0, 0, 0),
SecondCorner = new XYZ(10, 0, 0),
ThirdCorner = new XYZ(0, 10, 0),
FourthCorner = new XYZ(10, 10, 0),
});
var (g, nezpracovane) = Prehraj(doc);
Assert.Empty(nezpracovane);
var smycky = Assert.Single(g.Fills);
var body = Assert.Single(smycky);
Assert.Equal(new[] { (0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0) },
body.Take(4).Select(p => (Math.Round(p.X, 6), Math.Round(p.Y, 6))));
}
[Fact]
public void NeznamaEntitaSeSpocitaMistoTichehoZahozeni()
{
var doc = new CadDocument();
doc.Entities.Add(new Ray { StartPoint = new XYZ(0, 0, 0), Direction = new XYZ(1, 0, 0) });
var (g, nezpracovane) = Prehraj(doc);
Assert.Empty(g.Polylines);
Assert.Equal(0, g.LineCount);
Assert.Equal(1, nezpracovane.Values.Sum());
}
[Fact]
public void PocitadloSeMeziPruchodyNehromadi()
{
var doc = new CadDocument();
doc.Entities.Add(new Ray { StartPoint = new XYZ(0, 0, 0), Direction = new XYZ(1, 0, 0) });
string cesta = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
"rozpisky-entity-" + Guid.NewGuid().ToString("N") + ".dxf");
try
{
ACadSharp.IO.DxfWriter.Write(cesta, doc, binary: true);
var tpl = new DxfTemplate(cesta);
tpl.Replay(new RecordingRenderer());
tpl.Replay(new RecordingRenderer());
Assert.Equal(1, tpl.NezpracovaneEntity.Values.Sum());
}
finally
{
try { System.IO.File.Delete(cesta); } catch (System.IO.IOException) { }
}
}
}