diff --git a/Rozpisky.Tests/AciPaletaTesty.cs b/Rozpisky.Tests/AciPaletaTesty.cs new file mode 100644 index 0000000..080f848 --- /dev/null +++ b/Rozpisky.Tests/AciPaletaTesty.cs @@ -0,0 +1,87 @@ +using Rozpisky.Rendering; +using Xunit; +using AcColor = ACadSharp.Color; + +namespace Rozpisky.Tests; + +/// +/// Paleta indexovaných barev AutoCADu. Rozhoduje o tom, jak vypadá náhled proti CADu – +/// v exportu se index zapisuje číslem, takže tahle tabulka do souboru nikdy neproteče. +/// +public class AciPaletaTesty +{ + [Theory] + [InlineData(1, 0xFF0000)] [InlineData(2, 0xFFFF00)] [InlineData(3, 0x00FF00)] + [InlineData(4, 0x00FFFF)] [InlineData(5, 0x0000FF)] [InlineData(6, 0xFF00FF)] + [InlineData(7, 0xFFFFFF)] [InlineData(8, 0x414141)] [InlineData(9, 0x808080)] + [InlineData(255, 0xFFFFFF)] [InlineData(250, 0x333333)] + public void ZakladniBarvySedle(int index, int rgb) => Assert.Equal(rgb, AciPaleta.Rgb(index)); + + [Theory] + // Odstínová část: první čtveřice ze skupiny červené a modrozelené, ověřená proti AutoCADu. + [InlineData(10, 0xFF0000)] [InlineData(11, 0xFF7F7F)] [InlineData(12, 0xA50000)] + [InlineData(13, 0xA55252)] [InlineData(20, 0xFF3F00)] [InlineData(21, 0xFF9F7F)] + [InlineData(22, 0xA52900)] [InlineData(136, 0x004C4C)] [InlineData(18, 0x260000)] + public void OdstinySeDopocitavajiSpravne(int index, int rgb) => Assert.Equal(rgb, AciPaleta.Rgb(index)); + + [Fact] + public void MimoRozsahJeCerna() + { + Assert.Equal(0x000000, AciPaleta.Rgb(-1)); + Assert.Equal(0x000000, AciPaleta.Rgb(256)); + Assert.Equal(0x000000, AciPaleta.Rgb(0)); // 0 = ByBlock, samostatně se nekreslí + } + + [Fact] + public void KazdyOdstinMaVlastniBarvu() + { + // 24 skupin po 10 – žádné dvě sousední skupiny nesmí vyjít stejně. Přesně tímhle se + // ACadSharp plete (29 a 39 tam mají tutéž barvu), a proto máme paletu vlastní. + for (int skupina = 0; skupina + 1 < 24; skupina++) + for (int pozice = 0; pozice < 10; pozice++) + Assert.NotEqual(AciPaleta.Rgb(10 + skupina * 10 + pozice), + AciPaleta.Rgb(20 + skupina * 10 + pozice)); + } + + /// Indexy, kde se ACadSharp.Color.GetIndexRGB od AutoCADu liší o víc než + /// zaokrouhlení: dva stupně šedi (8, 9), šedý klín na konci palety (250–254) a poslední + /// index většiny desítek odstínů (29, 119, 139, …), kde ACadSharp bere barvu ze sousední + /// skupiny – proto tam vycházejí i barvy mimo jas dané pozice. + private static readonly int[] ZnameOdchylky = + { + 8, 9, 29, 91, 109, 119, 129, 139, 145, 147, 149, 157, 159, 167, 169, 177, 179, + 187, 189, 192, 197, 199, 203, 209, 219, 229, 239, 249, 250, 251, 252, 253, 254, + }; + + [Fact] + public void ShodujeSeSAcadSharpKromeZnamychOdchylek() + { + // Pojistka proti tichému rozejití při upgradu balíčku. Jinde než v seznamu se smí lišit + // nanejvýš o 1 na kanál (jiné zaokrouhlení při dopočtu odstínu) – to okem nepoznat. + var zname = new HashSet(ZnameOdchylky); + var novaOdchylka = new List(); + var zmizelaOdchylka = new List(); + + for (int i = 0; i <= 255; i++) + { + var acad = AcColor.GetIndexRGB((byte)i); + int nas = AciPaleta.Rgb(i); + int rozdil = Math.Max(Math.Abs(acad[0] - ((nas >> 16) & 0xFF)), + Math.Max(Math.Abs(acad[1] - ((nas >> 8) & 0xFF)), + Math.Abs(acad[2] - (nas & 0xFF)))); + if (rozdil > 1 && !zname.Contains(i)) novaOdchylka.Add(i); + if (rozdil <= 1 && zname.Contains(i)) zmizelaOdchylka.Add(i); + } + + Assert.Empty(novaOdchylka); // ACadSharp se rozešel s AutoCADem na dalším indexu + Assert.Empty(zmizelaOdchylka); // ACadSharp se opravil – seznam zúžit + } + + [Fact] + public void NejblizsiIndexNajdePresnouShoduIPriblizek() + { + Assert.Equal(1, AciPaleta.NejblizsiIndex(0xFF0000)); + Assert.Equal(5, AciPaleta.NejblizsiIndex(0x0000FF)); + Assert.Equal(0xFF0000, AciPaleta.Rgb(AciPaleta.NejblizsiIndex(0xFE0101))); + } +} diff --git a/Rozpisky.Tests/BarvaTesty.cs b/Rozpisky.Tests/BarvaTesty.cs new file mode 100644 index 0000000..ac30be8 --- /dev/null +++ b/Rozpisky.Tests/BarvaTesty.cs @@ -0,0 +1,64 @@ +using Rozpisky.Rendering; +using Xunit; + +namespace Rozpisky.Tests; + +/// +/// Rozřešení stavu kresby: co se má nakreslit, když barvu neurčí entita, ale hladina. +/// Chyba tady se projeví špatnou barvou v náhledu, ne pádem – proto explicitní testy. +/// +public class BarvaTesty +{ + private static KresliciStav Stav(Barva entita, Barva hladina) + => new(HladinaStav.Vychozi with { Barva = hladina }, entita); + + [Fact] + public void VlastniBarvaEntityPrebijiHladinu() + { + Assert.Equal(0x0000FF, Stav(Barva.Aci(5), Barva.Aci(1)).RgbProKresbu()); + Assert.Equal(0x123456, Stav(Barva.Prima(0x123456), Barva.Aci(1)).RgbProKresbu()); + } + + [Fact] + public void ByLayerSiVezmeBarvuHladiny() + => Assert.Equal(0xFF0000, Stav(Barva.ZHladiny, Barva.Aci(1)).RgbProKresbu()); + + [Fact] + public void ByBlockMimoBlokSeChovaJakoByLayer() + => Assert.Equal(0xFF0000, Stav(Barva.ZBloku, Barva.Aci(1)).RgbProKresbu()); + + [Fact] + public void ByLayerNadHladinouBezBarvyDaCernou() + => Assert.Equal(0x000000, Stav(Barva.ZHladiny, Barva.ZHladiny).RgbProKresbu()); + + [Fact] + public void TloustkaPadaZEntityNaHladinuAPakNaVychozi() + { + var h = HladinaStav.Vychozi with { TloustkaMm = 0.5 }; + Assert.Equal(0.35, new KresliciStav(h, Barva.ZHladiny, TloustkaMm: 0.35).TloustkaKresby(), 3); + Assert.Equal(0.5, new KresliciStav(h, Barva.ZHladiny).TloustkaKresby(), 3); + Assert.Equal(KresliciStav.VychoziTloustkaMm, + new KresliciStav(HladinaStav.Vychozi, Barva.ZHladiny).TloustkaKresby(), 3); + } + + [Fact] + public void TypCaryPadaZEntityNaHladinuAPakNaContinuous() + { + var h = HladinaStav.Vychozi with { TypCary = "DASHED" }; + Assert.Equal("CENTER", new KresliciStav(h, Barva.ZHladiny, TypCary: "CENTER").TypCaryKresby()); + Assert.Equal("DASHED", new KresliciStav(h, Barva.ZHladiny).TypCaryKresby()); + Assert.Equal("Continuous", + new KresliciStav(h with { TypCary = "" }, Barva.ZHladiny).TypCaryKresby()); + } + + [Fact] + public void IndexNeseIRgbProKresbu() + { + var b = Barva.Aci(5); + Assert.Equal(BarvaDruh.Index, b.Druh); + Assert.Equal(5, b.Index); + Assert.Equal(0x0000FF, b.Rgb); + Assert.True(b.JeVlastni); + Assert.False(Barva.ZHladiny.JeVlastni); + } +} diff --git a/Rozpisky.Tests/DxfBarvyRoundTripTesty.cs b/Rozpisky.Tests/DxfBarvyRoundTripTesty.cs new file mode 100644 index 0000000..f467c39 --- /dev/null +++ b/Rozpisky.Tests/DxfBarvyRoundTripTesty.cs @@ -0,0 +1,99 @@ +using System.IO; +using ACadSharp.Entities; +using ACadSharp.IO; +using Rozpisky.Dxf; +using Rozpisky.Rendering; +using Xunit; + +namespace Rozpisky.Tests; + +/// +/// Cesta barev tam a zpět: Rozpiska.dxf → soubor → +/// zpátky. Šablona i všech 9 log jsou zapsané výhradně indexovanými barvami AutoCADu +/// (group 62); dřív z nich export dělal natvrdo RGB (group 420), takže výkres v CADu přestal +/// reagovat na tabulku barev a ByLayer se rozpadlo na kopie barvy hladiny. +/// +public class DxfBarvyRoundTripTesty : IDisposable +{ + private readonly string _soubor = Path.Combine(Path.GetTempPath(), "rozpisky-barvy-" + Guid.NewGuid().ToString("N") + ".dxf"); + + public void Dispose() + { + try { File.Delete(_soubor); } catch (IOException) { } + GC.SuppressFinalize(this); + } + + /// Přehraje šablonu do DXF rendereru, zapíše a načte zpět. + private ACadSharp.CadDocument Kolecko(bool sRameckem = true) + { + var tpl = new DxfTemplate(TestData.Sablona); + var dxf = new DxfProfileRenderer(); + tpl.Replay(dxf, values: null, includeNoPrint: sRameckem); + DxfWriter.Write(_soubor, dxf.Document, binary: true); + return DxfReader.Read(_soubor); + } + + [Fact] + public void ZadnaEntitaNevyjdeJakoTrueColor() + { + var doc = Kolecko(); + var prohresky = doc.Entities.OfType().Where(e => e.Color.IsTrueColor).ToList(); + Assert.Empty(prohresky); + } + + [Fact] + public void PouziteIndexyOdpovidajiZdroji() + { + // Indexy, které kreslítko dostalo ze šablony, musí být tytéž, jaké skončí v souboru. + var zaznam = new RecordingRenderer(); + new DxfTemplate(TestData.Sablona).Replay(zaznam, values: null, includeNoPrint: true); + var zdrojove = zaznam.Stavy.Where(s => s.Barva.Druh == BarvaDruh.Index) + .Select(s => (int)s.Barva.Index).ToHashSet(); + + var doc = Kolecko(); + var vysledne = doc.Entities.OfType() + .Where(e => !e.Color.IsByLayer && !e.Color.IsByBlock) + .Select(e => (int)e.Color.Index).ToHashSet(); + + Assert.NotEmpty(zdrojove); + Assert.Equal(zdrojove, vysledne); + } + + [Fact] + public void ByLayerZustaneByLayer() + { + // Šablona má 41 entit s barvou 256 (ByLayer). Když se rozbalí na konkrétní RGB, + // přestane jít barva výkresu měnit přes hladinu – přesně to uživatel hlásí. + var zaznam = new RecordingRenderer(); + new DxfTemplate(TestData.Sablona).Replay(zaznam, values: null, includeNoPrint: true); + int zdrojovych = zaznam.Stavy.Count(s => s.Barva.Druh == BarvaDruh.ByLayer); + Assert.True(zdrojovych > 0, "šablona žádnou ByLayer entitu nemá – test ztratil smysl"); + + var doc = Kolecko(); + Assert.Contains(doc.Entities.OfType(), e => e.Color.IsByLayer); + } + + [Fact] + public void HladinyMajiVlastnostiZTabulkyNeZPrvniEntity() + { + var zdroj = ACadSharp.IO.DxfReader.Read(TestData.Sablona); + var doc = Kolecko(); + + foreach (var vrstva in doc.Layers) + { + if (!zdroj.Layers.TryGetValue(vrstva.Name, out var puvodni)) continue; + Assert.Equal(puvodni.Color.Index, vrstva.Color.Index); + Assert.Equal(puvodni.PlotFlag, vrstva.PlotFlag); + } + } + + [Fact] + public void RamecekNoPrintZustaneNetisknutelny() + { + // Rámeček A4 se do exportu kreslí jako reference, ale tisknout se nesmí. + var doc = Kolecko(); + Assert.True(doc.Layers.TryGetValue(DxfTemplate.HladinaNoPrint, out var vrstva), + "hladina !!NOPRINT v exportu chybí"); + Assert.False(vrstva!.PlotFlag); + } +} diff --git a/Rozpisky.Tests/ObliqueTesty.cs b/Rozpisky.Tests/ObliqueTesty.cs index a794064..57959e4 100644 --- a/Rozpisky.Tests/ObliqueTesty.cs +++ b/Rozpisky.Tests/ObliqueTesty.cs @@ -41,7 +41,7 @@ public class ObliqueTesty public void DxfExport_ZachovaObliqueNaTextu() { var dxf = new DxfProfileRenderer(); - dxf.Layer("0", 0x000000, 0.25); + dxf.Stav(new KresliciStav(HladinaStav.Vychozi, Barva.Aci(7), TloustkaMm: 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"); diff --git a/Rozpisky.Tests/ObloukyTesty.cs b/Rozpisky.Tests/ObloukyTesty.cs index 86712ba..87cfb46 100644 --- a/Rozpisky.Tests/ObloukyTesty.cs +++ b/Rozpisky.Tests/ObloukyTesty.cs @@ -84,7 +84,7 @@ public class ObloukyTesty var contour = new Contour(new[] { arc, line }, Closed: true); var g = new DxfProfileRenderer(); - g.Layer("0", 0x000000); + g.Stav(KresliciStav.Vychozi); g.Fill(new[] { contour }); // Export → dočasný DXF → import zpět a porovnání bboxu (sagitta ≤ 0,02 mm). @@ -118,7 +118,7 @@ public class ObloukyTesty 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.Stav(KresliciStav.Vychozi); g.Stroke(contour); var arc = Assert.Single(g.Document.Entities.OfType()); diff --git a/Rozpisky.Tests/RecordingRenderer.cs b/Rozpisky.Tests/RecordingRenderer.cs index 7e03f62..54fd95a 100644 --- a/Rozpisky.Tests/RecordingRenderer.cs +++ b/Rozpisky.Tests/RecordingRenderer.cs @@ -26,7 +26,12 @@ public sealed class RecordingRenderer : IProfileRenderer public int LineCount { get; private set; } public int CircleCount { get; private set; } - public void Layer(string name, int rgb, double lineWeightMm = 0.25) { } + /// Stavy kresby v pořadí, jak přišly – testy tak můžou tvrdit i o barvě a hladině. + public List Stavy { get; } = new(); + /// Stav platný pro naposledy zaznamenanou entitu. + public KresliciStav PosledniStav => Stavy.Count > 0 ? Stavy[^1] : KresliciStav.Vychozi; + + public void Stav(in KresliciStav s) => Stavy.Add(s); public void Line(double x1, double y1, double x2, double y2) => LineCount++; diff --git a/Rozpisky.Tests/SkiaRenderTesty.cs b/Rozpisky.Tests/SkiaRenderTesty.cs index b3e30bd..45cd678 100644 --- a/Rozpisky.Tests/SkiaRenderTesty.cs +++ b/Rozpisky.Tests/SkiaRenderTesty.cs @@ -36,7 +36,7 @@ public class SkiaRenderTesty 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 }); }); + int filled = PocetVyplnenych(g => { g.Stav(KresliciStav.Vychozi); 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)"); @@ -47,7 +47,7 @@ public class SkiaRenderTesty { // 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)); }); + int filled = PocetVyplnenych(g => { g.Stav(KresliciStav.Vychozi); g.Stroke(new Contour(new[] { edge }, Closed: false)); }); Assert.True(filled > 50, $"oblouk se nevykreslil (vyplněno {filled} px)"); } } diff --git a/Rozpisky.Tests/TestData.cs b/Rozpisky.Tests/TestData.cs index 50d3988..c0ea1ae 100644 --- a/Rozpisky.Tests/TestData.cs +++ b/Rozpisky.Tests/TestData.cs @@ -11,6 +11,9 @@ public static class TestData /// Plná cesta k logu ve zdrojové složce Rozpisky\Podklady\loga. public static string Logo(string fileName) => Path.Combine(RepoRoot, "Rozpisky", "Podklady", "loga", fileName); + /// Plná cesta k DXF šabloně rozpisky. + public static string Sablona => Path.Combine(RepoRoot, "Rozpisky", "Podklady", "Rozpiska.dxf"); + /// Všech 9 log dodávaných s programem. public static readonly string[] VsechnaLoga = { diff --git a/Rozpisky/Dxf/DxfTemplate.cs b/Rozpisky/Dxf/DxfTemplate.cs index 1a9e9e0..dceeb12 100644 --- a/Rozpisky/Dxf/DxfTemplate.cs +++ b/Rozpisky/Dxf/DxfTemplate.cs @@ -210,14 +210,41 @@ public sealed partial class DxfTemplate bool noPrint = JeNoPrint(e); if (noPrint && !includeNoPrint) return false; // vodicí rámeček vkládaného DXF se nekreslí if (!noPrint && layer is not null && (!layer.PlotFlag || !layer.IsOn)) return false; - var col = e.Color.IsByLayer && layer is not null ? layer.Color : e.Color; // ByLayer → barva hladiny - int rgb = (col.R << 16) | (col.G << 8) | col.B; // ACadSharp rozbalí i index/true-color int lw = (int)e.LineWeight; // LineWeightType: Wnn = nn/100 mm, záporné = ByLayer/Default/ByBlock - if (lw < 0 && layer is not null) lw = (int)layer.LineWeight; - g.Layer(layer?.Name ?? "0", rgb, lw > 0 ? lw / 100.0 : 0.18); + g.Stav(new KresliciStav( + HladinaZ(layer), + BarvaZ(e.Color), + e.LineType?.Name, + lw > 0 ? lw / 100.0 : null)); // null = tloušťka podle hladiny return true; } + /// Popis hladiny pro kreslítko – celý, ať se dá ve výstupu postavit znovu + /// (barva z tabulky hladin, tloušťka, typ čáry, příznak tisku). + private static HladinaStav HladinaZ(ACadSharp.Tables.Layer? layer) + { + if (layer is null) return HladinaStav.Vychozi; + int lw = (int)layer.LineWeight; + return new HladinaStav( + layer.Name, + BarvaZ(layer.Color), + layer.LineType?.Name ?? "Continuous", + lw > 0 ? lw / 100.0 : 0, + layer.PlotFlag, + layer.IsOn); + } + + /// Barva z DXF i s původem: index (group 62), true color (420) nebo odkaz + /// ByLayer/ByBlock. Původ se nese dál, aby ho DXF export mohl zapsat beze změny – + /// zplošťování na RGB dřív z indexovaných barev dělalo natvrdo RGB. + private static Barva BarvaZ(ACadSharp.Color c) + { + if (c.IsByLayer) return Barva.ZHladiny; + if (c.IsByBlock) return Barva.ZBloku; + if (c.IsTrueColor) return Barva.Prima((c.R << 16) | (c.G << 8) | c.B); + return c.Index is >= 1 and <= 255 ? Barva.Aci(c.Index) : Barva.Aci(7); + } + /// Zjistí kotvu a zarovnání textu (Left+Baseline → vkládací bod, jinak zarovnávací bod). private static (double x, double y, HAlign h, VAlign v) Place( TextHorizontalAlignment ha, TextVerticalAlignmentType va, XYZ insert, XYZ align) diff --git a/Rozpisky/Rendering/AciPaleta.cs b/Rozpisky/Rendering/AciPaleta.cs new file mode 100644 index 0000000..6366bec --- /dev/null +++ b/Rozpisky/Rendering/AciPaleta.cs @@ -0,0 +1,98 @@ +namespace Rozpisky.Rendering; + +/// +/// Paleta indexovaných barev AutoCADu (ACI 0–255). Index je to, co nese DXF group 62; na RGB se +/// překládá až při rasterizaci (náhled, PNG, PDF) – do DXF se zapisuje zpátky jako číslo, +/// takže se cestou tam a zpět nic neztratí. +/// Rozsah 10–249 je dopočítaný, ne vypsaný: 24 odstínů po 15° × 5 jasů, každý ve variantě +/// sytá / poloviční sytost. Krajní hodnoty (0–9, 250–255) jsou tabulkou, protože do schématu +/// nezapadají. +/// Proč ne ACadSharp.Color.GetIndexRGB: ta se od AutoCADu liší u indexů +/// 8 a 9 (o dva stupně šedi), u 250–254 a u posledního indexu každé skupiny odstínů (19, 29, …, +/// 249) – tam vrací hodnotu ze sousední skupiny, takže např. 29 a 39 dostanou stejnou barvu. +/// Index 8 je přitom v Rozpiska.dxf použitý. Viz AciPaletaTesty. +/// +public static class AciPaleta +{ + /// ACI 0–9. Index 0 (ByBlock) a 7 se kreslí podle pozadí – tady černá / bílá. + private static readonly int[] Zakladni = + { + 0x000000, // 0 – ByBlock, samostatně se nekreslí + 0xFF0000, 0xFFFF00, 0x00FF00, 0x00FFFF, 0x0000FF, 0xFF00FF, 0xFFFFFF, + 0x414141, // 8 – tmavá šedá (ACadSharp tu má 0x808080) + 0x808080, // 9 – šedá (ACadSharp tu má 0xC0C0C0) + }; + + /// ACI 250–255 – šedý klín zakončený bílou. + private static readonly int[] Sede = { 0x333333, 0x505050, 0x696969, 0x828282, 0xBEBEBE, 0xFFFFFF }; + + /// Jas pro pozici v desetici: pětice 100 / 65 / 50 / 30 / 15 %, každá dvakrát + /// (sudá pozice = plná sytost, lichá = poloviční). + private static readonly int[] Jasy = { 0xFF, 0xFF, 0xA5, 0xA5, 0x7F, 0x7F, 0x4C, 0x4C, 0x26, 0x26 }; + + /// RGB 0xRRGGBB pro daný ACI index. Mimo rozsah 0–255 → černá. + public static int Rgb(int index) + { + if (index is < 0 or > 255) return 0x000000; + if (index <= 9) return Zakladni[index]; + if (index >= 250) return Sede[index - 250]; + return Odstin(index); + } + + /// Nejbližší ACI index k RGB (pro cizí soubory zapsané v true color). Prohledává + /// 1–255 podle eukleidovské vzdálenosti v RGB – hrubé, ale pro převod stačí. + public static byte NejblizsiIndex(int rgb) + { + int r = (rgb >> 16) & 0xFF, g = (rgb >> 8) & 0xFF, b = rgb & 0xFF; + byte nej = 7; + long nejVzdalenost = long.MaxValue; + for (int i = 1; i <= 255; i++) + { + int c = Rgb(i); + long dr = r - ((c >> 16) & 0xFF), dg = g - ((c >> 8) & 0xFF), db = b - (c & 0xFF); + long d = dr * dr + dg * dg + db * db; + if (d >= nejVzdalenost) continue; + nejVzdalenost = d; + nej = (byte)i; + if (d == 0) break; + } + return nej; + } + + /// Barva odstínové části palety (10–249): odstín podle desítky, jas a sytost + /// podle pozice v ní. Lichá pozice = tatáž barva smíchaná do poloviny s jasem + /// (odbarvená), proto c + (jas − c) / 2. + private static int Odstin(int index) + { + int skupina = (index - 10) / 10; // 0–23 → úhel odstínu 0°, 15°, … 345° + int pozice = (index - 10) % 10; // 0–9 → jas a sytost + int jas = Jasy[pozice]; + var (r, g, b) = Hsv(skupina * 15.0, jas); + if ((pozice & 1) == 1) + { + r += (jas - r) / 2; + g += (jas - g) / 2; + b += (jas - b) / 2; + } + return (r << 16) | (g << 8) | b; + } + + /// Plně sytá barva daného odstínu a jasu (HSV se saturací 1). Dělení na šestiny + /// kruhu; v každé roste jeden kanál a klesá jiný. Ořezává se dolů, jako to dělá AutoCAD. + private static (int R, int G, int B) Hsv(double uhelDeg, int jas) + { + int sestina = (int)(uhelDeg / 60); + double podil = uhelDeg / 60 - sestina; + int rostouci = (int)(jas * podil); + int klesajici = (int)(jas * (1 - podil)); + return sestina switch + { + 0 => (jas, rostouci, 0), + 1 => (klesajici, jas, 0), + 2 => (0, jas, rostouci), + 3 => (0, klesajici, jas), + 4 => (rostouci, 0, jas), + _ => (jas, 0, klesajici), + }; + } +} diff --git a/Rozpisky/Rendering/BoundsRenderer.cs b/Rozpisky/Rendering/BoundsRenderer.cs index 9a3113b..b06bf46 100644 --- a/Rozpisky/Rendering/BoundsRenderer.cs +++ b/Rozpisky/Rendering/BoundsRenderer.cs @@ -23,7 +23,7 @@ public sealed class BoundsRenderer : IProfileRenderer if (y > MaxY) MaxY = y; } - public void Layer(string name, int rgb, double lineWeightMm = 0.25) { } + public void Stav(in KresliciStav s) { } public void Line(double x1, double y1, double x2, double y2) { Add(x1, y1); Add(x2, y2); } diff --git a/Rozpisky/Rendering/DxfProfileRenderer.cs b/Rozpisky/Rendering/DxfProfileRenderer.cs index 9abf734..a655c4e 100644 --- a/Rozpisky/Rendering/DxfProfileRenderer.cs +++ b/Rozpisky/Rendering/DxfProfileRenderer.cs @@ -14,7 +14,7 @@ namespace Rozpisky.Rendering; /// (6 primitiv), takže má stejné pokrytí jako vykreslovací engine: složitější DXF entity (spline, /// oblouk, hatch) jsou už ve fázi čtení (DxfTemplate.Replay) převedené na čáry/polylinie/výplně. /// Zachovává barvu, tloušťku čáry a styl/zarovnání písma přesně podle zdroje – stav se -/// přebírá z (barva + tloušťka pro každou entitu) a z parametru style +/// přebírá z (barva i s původem: ACI index / true color / ByLayer) a z parametru style /// metody (font). Souřadnice jsou ve výkresových mm, osa Y nahoru – shodně s DXF. /// Jednořádkový text → TEXT, víceřádkový (maxWidthMm > 0) → MTEXT. /// @@ -36,27 +36,40 @@ public sealed class DxfProfileRenderer : IProfileRenderer _doc.Layers.Add(_aktualniVrstva); } _vrstvy[_aktualniVrstva.Name] = _aktualniVrstva; - _aktualniBarva = new Color(0, 0, 0); + _aktualniBarva = Color.ByLayer; } /// Hotový výkres se všemi rozpiskami (pro DxfWriter.Write). public CadDocument Document => _doc; - public void Layer(string name, int rgb, double lineWeightMm = 0.25) + public void Stav(in KresliciStav s) { - if (string.IsNullOrEmpty(name)) name = AcLayer.DefaultName; - if (!_vrstvy.TryGetValue(name, out var vrstva)) + _aktualniVrstva = NajdiVrstvu(s.Hladina); + _aktualniBarva = NaColor(s.Barva); + _aktualniTloustka = s.TloustkaMm is { } mm ? SnapTloustka(mm) : LineWeightType.ByLayer; + } + + /// Najde/založí hladinu podle popisu ze zdroje. Vlastnosti se berou z tabulky hladin, + /// ne z první entity, která na hladině přistála – jinak by se ztratil příznak tisku + /// (!!NOPRINT) a barva hladiny by byla náhodná. + private AcLayer NajdiVrstvu(HladinaStav h) + { + string name = string.IsNullOrEmpty(h.Nazev) ? AcLayer.DefaultName : h.Nazev; + if (_vrstvy.TryGetValue(name, out var vrstva)) return vrstva; + + if (!_doc.Layers.TryGetValue(name, out vrstva!)) { - if (!_doc.Layers.TryGetValue(name, out vrstva!)) - { - vrstva = new AcLayer(name) { Color = RgbNaColor(rgb) }; - _doc.Layers.Add(vrstva); - } - _vrstvy[name] = vrstva; + vrstva = new AcLayer(name); + _doc.Layers.Add(vrstva); } - _aktualniVrstva = vrstva; - _aktualniBarva = RgbNaColor(rgb); // barva entity dle zdroje (ByLayer už rozbalil DxfTemplate) - _aktualniTloustka = SnapTloustka(lineWeightMm); + vrstva.Color = h.Barva.JeVlastni ? NaColor(h.Barva) : new Color(7); + vrstva.LineWeight = h.TloustkaMm > 0 ? SnapTloustka(h.TloustkaMm) : LineWeightType.Default; + vrstva.PlotFlag = h.Tisknout; + vrstva.IsOn = h.Zapnuta; + if (!string.IsNullOrWhiteSpace(h.TypCary) && _doc.LineTypes.TryGetValue(h.TypCary, out var lt)) + vrstva.LineType = lt; + _vrstvy[name] = vrstva; + return vrstva; } public void Line(double x1, double y1, double x2, double y2) @@ -246,8 +259,16 @@ public sealed class DxfProfileRenderer : IProfileRenderer return kandidat; } - private static Color RgbNaColor(int rgb) - => new((byte)((rgb >> 16) & 0xFF), (byte)((rgb >> 8) & 0xFF), (byte)(rgb & 0xFF)); + /// Barva do DXF ve stejném druhu, v jakém přišla ze zdroje: indexovaná zůstane + /// číslem (group 62), přímá se zapíše jako RGB (group 420) a ByLayer/ByBlock zůstane odkazem. + /// Dřív se všechno zplošťovalo na RGB, takže výkres v CADu přestal reagovat na barvy hladin. + private static Color NaColor(Barva b) => b.Druh switch + { + BarvaDruh.Index => new Color((short)b.Index), + BarvaDruh.True => new Color((byte)((b.Rgb >> 16) & 0xFF), (byte)((b.Rgb >> 8) & 0xFF), (byte)(b.Rgb & 0xFF)), + BarvaDruh.ByBlock => Color.ByBlock, + _ => Color.ByLayer, + }; /// Standardní tloušťky čar (1/100 mm) dle DXF; vstupní mm se přichytí k nejbližší. private static readonly int[] StdTloustky = diff --git a/Rozpisky/Rendering/IProfileRenderer.cs b/Rozpisky/Rendering/IProfileRenderer.cs index ca96cff..c5cd576 100644 --- a/Rozpisky/Rendering/IProfileRenderer.cs +++ b/Rozpisky/Rendering/IProfileRenderer.cs @@ -84,10 +84,12 @@ public static class ContourGeometry /// public interface IProfileRenderer { - /// Nastaví aktuální hladinu/styl kresby. = barva 0xRRGGBB - /// (rozbalená z DXF, ByLayer i index/true-color), tloušťka v mm. Bílá se kreslí černě + /// Nastaví aktuální stav kresby – hladinu a to, čím ji entita přebíjí + /// (). Barva si nese svůj původ (ACI index / true color / ByLayer), + /// aby ji DXF export mohl zapsat tak, jak byla ve zdroji. Rastrové renderery si vyžádají + /// hotové RGB přes ; bílá se kreslí černě /// (bílá na bílém papíře by zmizela). - void Layer(string name, int rgb, double lineWeightMm = 0.25); + void Stav(in KresliciStav s); void Line(double x1, double y1, double x2, double y2); void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false); void Circle(double cx, double cy, double r); diff --git a/Rozpisky/Rendering/KresliciStav.cs b/Rozpisky/Rendering/KresliciStav.cs new file mode 100644 index 0000000..dd11072 --- /dev/null +++ b/Rozpisky/Rendering/KresliciStav.cs @@ -0,0 +1,79 @@ +namespace Rozpisky.Rendering; + +/// Původ barvy podle DXF. a nenesou vlastní +/// hodnotu – barva se dopočítá z hladiny (resp. z vkládaného bloku). +public enum BarvaDruh : byte { ByLayer, ByBlock, Index, True } + +/// +/// Barva tak, jak je zapsaná v DXF – i s původem, ne jen výsledné RGB. Bez toho by se +/// při exportu z indexované barvy (group 62, „barva 5") stala natvrdo RGB (group 420) a výkres +/// by v CADu přestal reagovat na tabulku barev; ByLayer by se rozpadlo na kopie barvy hladiny. +/// je vždy vyplněné pro kresbu na obrazovku; u ByLayer/ByBlock je +/// bezvýznamné (rozřeší se až v ). +/// +public readonly record struct Barva(BarvaDruh Druh, byte Index, int Rgb) +{ + /// Indexovaná barva AutoCADu (ACI 1–255). + public static Barva Aci(int index) => new(BarvaDruh.Index, (byte)index, AciPaleta.Rgb(index)); + + /// Přímá barva (true color, DXF group 420). + public static Barva Prima(int rgb) => new(BarvaDruh.True, 0, rgb & 0xFFFFFF); + + /// Barva se převezme z hladiny (ACI 256). + public static readonly Barva ZHladiny = new(BarvaDruh.ByLayer, 0, 0); + + /// Barva se převezme z bloku, do kterého je entita vložená (ACI 0). + public static readonly Barva ZBloku = new(BarvaDruh.ByBlock, 0, 0); + + /// true = barva nese vlastní hodnotu a nemusí se dohledávat jinde. + public bool JeVlastni => Druh is BarvaDruh.Index or BarvaDruh.True; +} + +/// +/// Vlastnosti hladiny ze zdrojového výkresu. Přenášejí se celé, aby se hladina dala ve výstupu +/// postavit znovu – dřív se její barva odvozovala od první entity, která na ní přistála, a +/// příznak tisku se ztrácel (rámeček !!NOPRINT pak ve výstupu vycházel tisknutelný). +/// +public readonly record struct HladinaStav( + string Nazev, Barva Barva, string TypCary = "Continuous", + double TloustkaMm = 0, bool Tisknout = true, bool Zapnuta = true) +{ + /// Hladina „0" s barvou 7 – použije se, když zdroj hladinu neuvádí. + public static readonly HladinaStav Vychozi = new("0", Barva.Aci(7)); +} + +/// +/// Stav kresby pro jednu entitu: hladina a to, čím ji entita přebíjí. null u typu čáry +/// i tloušťky znamená „podle hladiny" – stejná sémantika jako v DXF. +/// +public readonly record struct KresliciStav( + HladinaStav Hladina, Barva Barva, string? TypCary = null, double? TloustkaMm = null) +{ + /// Výchozí tloušťka, když ji neurčí ani entita, ani hladina (DXF „Default"). + public const double VychoziTloustkaMm = 0.18; + + /// Barva pro rasterizaci: ByLayer i ByBlock se rozřeší barvou hladiny. + /// (ByBlock se sem dostane jen mimo blok, kde se chová jako ByLayer.) + public int RgbProKresbu() + { + if (Barva.JeVlastni) return Barva.Rgb; + return Hladina.Barva.JeVlastni ? Hladina.Barva.Rgb : 0x000000; + } + + /// Tloušťka čáry v mm pro rasterizaci. + public double TloustkaKresby() + { + double mm = TloustkaMm ?? Hladina.TloustkaMm; + return mm > 0 ? mm : VychoziTloustkaMm; + } + + /// Název typu čáry (Continuous, DASHED…). + public string TypCaryKresby() + { + var t = TypCary ?? Hladina.TypCary; + return string.IsNullOrWhiteSpace(t) ? "Continuous" : t; + } + + /// Stav pro kresbu mimo zdrojový výkres (rámečky, pomocná grafika). + public static KresliciStav Vychozi => new(HladinaStav.Vychozi, Barva.ZHladiny); +} diff --git a/Rozpisky/Rendering/SkiaProfileRenderer.cs b/Rozpisky/Rendering/SkiaProfileRenderer.cs index e0c66e0..87df9d0 100644 --- a/Rozpisky/Rendering/SkiaProfileRenderer.cs +++ b/Rozpisky/Rendering/SkiaProfileRenderer.cs @@ -116,9 +116,10 @@ public sealed class SkiaProfileRenderer : IProfileRenderer, IDisposable private float StrokePx => Math.Max((float)(_lineWeightMm * _pxPerMm), _minStrokePx); - public void Layer(string name, int rgb, double lineWeightMm = 0.25) + public void Stav(in KresliciStav s) { - _lineWeightMm = lineWeightMm; + _lineWeightMm = s.TloustkaKresby(); + int rgb = s.RgbProKresbu(); byte r = (byte)((rgb >> 16) & 0xFF), g = (byte)((rgb >> 8) & 0xFF), b = (byte)(rgb & 0xFF); if (r == 255 && g == 255 && b == 255) r = g = b = 0; // bílá na bílém papíře → černá var c = new SKColor(r, g, b); diff --git a/Rozpisky/Rendering/TransformRenderer.cs b/Rozpisky/Rendering/TransformRenderer.cs index 69e06bb..9514eb5 100644 --- a/Rozpisky/Rendering/TransformRenderer.cs +++ b/Rozpisky/Rendering/TransformRenderer.cs @@ -23,8 +23,14 @@ public sealed class TransformRenderer : IProfileRenderer private double X(double x) => _ox + _s * x; private double Y(double y) => _oy + _s * y; - public void Layer(string name, int rgb, double lineWeightMm = 0.25) - => _inner.Layer(name, rgb, lineWeightMm * _s); + /// Stav propadá beze změny až na tloušťku čáry – ta je v mm, takže se škáluje + /// stejně jako geometrie (na entitě i na hladině). + public void Stav(in KresliciStav s) + => _inner.Stav(s with + { + TloustkaMm = s.TloustkaMm * _s, + Hladina = s.Hladina with { TloustkaMm = s.Hladina.TloustkaMm * _s }, + }); public void Line(double x1, double y1, double x2, double y2) => _inner.Line(X(x1), Y(y1), X(x2), Y(y2));