Barvy: zachovat ACI index a ByLayer po celé cestě DXF → model → DXF

Šablona i všech 9 log jsou zapsané výhradně indexovanými barvami AutoCADu
(group 62) – true color v nich není ani jednou. Kreslicí rozhraní přitom
neslo jen `int rgb`, takže se index i ByLayer ztratily už při čtení a export
zapisoval všechno jako natvrdo RGB (group 420). Výkres v CADu pak přestal
reagovat na tabulku barev a barva hladiny se rozpadla na kopie na entitách.

`IProfileRenderer.Layer(name, rgb, mm)` se mění na `Stav(in KresliciStav)`.
Stav nese barvu i s původem (`Barva`: ACI index / true color / ByLayer /
ByBlock), typ čáry, tloušťku a celý popis hladiny (`HladinaStav`) – hladina
se ve výstupu staví z tabulky hladin, ne z první entity, která na ní
přistála. Rámeček `!!NOPRINT` proto zůstane netisknutelný.

Paleta ACI je vlastní (`AciPaleta`), ne z ACadSharp: ta se od AutoCADu liší
u 33 indexů, mimo jiné u 8 (použitý v šabloně) a u posledního indexu většiny
desítek odstínů, kde vrací barvu ze sousední skupiny – 29 a 39 tam dokonce
dostanou tutéž barvu. Rozsah 10–249 se dopočítává, ne vypisuje.

Co ověřují nové testy (93 zelených):
- paleta proti AutoCADu i proti ACadSharp, se seznamem známých odchylek,
- rozřešení ByLayer/ByBlock, tloušťky a typu čáry,
- kolečko šablona → export → načtení: žádná entita není true color, indexy
  odpovídají zdroji, ByLayer zůstane ByLayer, hladiny mají barvu i příznak
  tisku ze zdrojové tabulky.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 12:13:32 +02:00
co-authored by Claude Opus 5
parent 7e7097c360
commit d66aec5299
16 changed files with 526 additions and 34 deletions
+87
View File
@@ -0,0 +1,87 @@
using Rozpisky.Rendering;
using Xunit;
using AcColor = ACadSharp.Color;
namespace Rozpisky.Tests;
/// <summary>
/// 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.
/// </summary>
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));
}
/// <summary>Indexy, kde se <c>ACadSharp.Color.GetIndexRGB</c> od AutoCADu liší o víc než
/// zaokrouhlení: dva stupně šedi (8, 9), šedý klín na konci palety (250254) 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.</summary>
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<int>(ZnameOdchylky);
var novaOdchylka = new List<int>();
var zmizelaOdchylka = new List<int>();
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)));
}
}
+64
View File
@@ -0,0 +1,64 @@
using Rozpisky.Rendering;
using Xunit;
namespace Rozpisky.Tests;
/// <summary>
/// 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.
/// </summary>
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);
}
}
+99
View File
@@ -0,0 +1,99 @@
using System.IO;
using ACadSharp.Entities;
using ACadSharp.IO;
using Rozpisky.Dxf;
using Rozpisky.Rendering;
using Xunit;
namespace Rozpisky.Tests;
/// <summary>
/// Cesta barev tam a zpět: <c>Rozpiska.dxf</c> → <see cref="DxfProfileRenderer"/> → 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.
/// </summary>
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);
}
/// <summary>Přehraje šablonu do DXF rendereru, zapíše a načte zpět.</summary>
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<Entity>().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<Entity>()
.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<Entity>(), 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);
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ public class ObliqueTesty
public void DxfExport_ZachovaObliqueNaTextu() public void DxfExport_ZachovaObliqueNaTextu()
{ {
var dxf = new DxfProfileRenderer(); 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)); 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"); string path = Path.Combine(Path.GetTempPath(), $"rozpisky-oblique-{Guid.NewGuid():N}.dxf");
+2 -2
View File
@@ -84,7 +84,7 @@ public class ObloukyTesty
var contour = new Contour(new[] { arc, line }, Closed: true); var contour = new Contour(new[] { arc, line }, Closed: true);
var g = new DxfProfileRenderer(); var g = new DxfProfileRenderer();
g.Layer("0", 0x000000); g.Stav(KresliciStav.Vychozi);
g.Fill(new[] { contour }); g.Fill(new[] { contour });
// Export → dočasný DXF → import zpět a porovnání bboxu (sagitta ≤ 0,02 mm). // 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 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(); var g = new DxfProfileRenderer();
g.Layer("0", 0); g.Stav(KresliciStav.Vychozi);
g.Stroke(contour); g.Stroke(contour);
var arc = Assert.Single(g.Document.Entities.OfType<Arc>()); var arc = Assert.Single(g.Document.Entities.OfType<Arc>());
+6 -1
View File
@@ -26,7 +26,12 @@ public sealed class RecordingRenderer : IProfileRenderer
public int LineCount { get; private set; } public int LineCount { get; private set; }
public int CircleCount { get; private set; } public int CircleCount { get; private set; }
public void Layer(string name, int rgb, double lineWeightMm = 0.25) { } /// <summary>Stavy kresby v pořadí, jak přišly testy tak můžou tvrdit i o barvě a hladině.</summary>
public List<KresliciStav> Stavy { get; } = new();
/// <summary>Stav platný pro naposledy zaznamenanou entitu.</summary>
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++; public void Line(double x1, double y1, double x2, double y2) => LineCount++;
+2 -2
View File
@@ -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 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); 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. // 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)"); 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. // 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); 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)"); Assert.True(filled > 50, $"oblouk se nevykreslil (vyplněno {filled} px)");
} }
} }
+3
View File
@@ -11,6 +11,9 @@ public static class TestData
/// <summary>Plná cesta k logu ve zdrojové složce <c>Rozpisky\Podklady\loga</c>.</summary> /// <summary>Plná cesta k logu ve zdrojové složce <c>Rozpisky\Podklady\loga</c>.</summary>
public static string Logo(string fileName) => Path.Combine(RepoRoot, "Rozpisky", "Podklady", "loga", fileName); public static string Logo(string fileName) => Path.Combine(RepoRoot, "Rozpisky", "Podklady", "loga", fileName);
/// <summary>Plná cesta k DXF šabloně rozpisky.</summary>
public static string Sablona => Path.Combine(RepoRoot, "Rozpisky", "Podklady", "Rozpiska.dxf");
/// <summary>Všech 9 log dodávaných s programem.</summary> /// <summary>Všech 9 log dodávaných s programem.</summary>
public static readonly string[] VsechnaLoga = public static readonly string[] VsechnaLoga =
{ {
+31 -4
View File
@@ -210,14 +210,41 @@ public sealed partial class DxfTemplate
bool noPrint = JeNoPrint(e); bool noPrint = JeNoPrint(e);
if (noPrint && !includeNoPrint) return false; // vodicí rámeček vkládaného DXF se nekreslí 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; 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 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.Stav(new KresliciStav(
g.Layer(layer?.Name ?? "0", rgb, lw > 0 ? lw / 100.0 : 0.18); HladinaZ(layer),
BarvaZ(e.Color),
e.LineType?.Name,
lw > 0 ? lw / 100.0 : null)); // null = tloušťka podle hladiny
return true; return true;
} }
/// <summary>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).</summary>
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);
}
/// <summary>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.</summary>
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);
}
/// <summary>Zjistí kotvu a zarovnání textu (Left+Baseline → vkládací bod, jinak zarovnávací bod).</summary> /// <summary>Zjistí kotvu a zarovnání textu (Left+Baseline → vkládací bod, jinak zarovnávací bod).</summary>
private static (double x, double y, HAlign h, VAlign v) Place( private static (double x, double y, HAlign h, VAlign v) Place(
TextHorizontalAlignment ha, TextVerticalAlignmentType va, XYZ insert, XYZ align) TextHorizontalAlignment ha, TextVerticalAlignmentType va, XYZ insert, XYZ align)
+98
View File
@@ -0,0 +1,98 @@
namespace Rozpisky.Rendering;
/// <summary>
/// Paleta indexovaných barev AutoCADu (ACI 0255). 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 <b>číslo</b>,
/// takže se cestou tam a zpět nic neztratí.
/// <para>Rozsah 10249 je dopočítaný, ne vypsaný: 24 odstínů po 15° × 5 jasů, každý ve variantě
/// sytá / poloviční sytost. Krajní hodnoty (09, 250255) jsou tabulkou, protože do schématu
/// nezapadají.</para>
/// <para><b>Proč ne <c>ACadSharp.Color.GetIndexRGB</c>:</b> ta se od AutoCADu liší u indexů
/// 8 a 9 (o dva stupně šedi), u 250254 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 <c>Rozpiska.dxf</c> použitý. Viz <c>AciPaletaTesty</c>.</para>
/// </summary>
public static class AciPaleta
{
/// <summary>ACI 09. Index 0 (ByBlock) a 7 se kreslí podle pozadí tady černá / bílá.</summary>
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)
};
/// <summary>ACI 250255 šedý klín zakončený bílou.</summary>
private static readonly int[] Sede = { 0x333333, 0x505050, 0x696969, 0x828282, 0xBEBEBE, 0xFFFFFF };
/// <summary>Jas pro pozici v desetici: pětice 100 / 65 / 50 / 30 / 15 %, každá dvakrát
/// (sudá pozice = plná sytost, lichá = poloviční).</summary>
private static readonly int[] Jasy = { 0xFF, 0xFF, 0xA5, 0xA5, 0x7F, 0x7F, 0x4C, 0x4C, 0x26, 0x26 };
/// <summary>RGB 0xRRGGBB pro daný ACI index. Mimo rozsah 0255 → černá.</summary>
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);
}
/// <summary>Nejbližší ACI index k RGB (pro cizí soubory zapsané v true color). Prohledává
/// 1255 podle eukleidovské vzdálenosti v RGB hrubé, ale pro převod stačí.</summary>
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;
}
/// <summary>Barva odstínové části palety (10249): 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>c + (jas c) / 2</c>.</summary>
private static int Odstin(int index)
{
int skupina = (index - 10) / 10; // 023 → úhel odstínu 0°, 15°, … 345°
int pozice = (index - 10) % 10; // 09 → 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;
}
/// <summary>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.</summary>
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),
};
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ public sealed class BoundsRenderer : IProfileRenderer
if (y > MaxY) MaxY = y; 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); } public void Line(double x1, double y1, double x2, double y2) { Add(x1, y1); Add(x2, y2); }
+33 -12
View File
@@ -14,7 +14,7 @@ namespace Rozpisky.Rendering;
/// (6 primitiv), takže má stejné pokrytí jako vykreslovací engine: složitější DXF entity (spline, /// (6 primitiv), takže má stejné pokrytí jako vykreslovací engine: složitější DXF entity (spline,
/// oblouk, hatch) jsou už ve fázi čtení (<c>DxfTemplate.Replay</c>) převedené na čáry/polylinie/výplně. /// oblouk, hatch) jsou už ve fázi čtení (<c>DxfTemplate.Replay</c>) převedené na čáry/polylinie/výplně.
/// <para>Zachovává <b>barvu, tloušťku čáry a styl/zarovnání písma</b> přesně podle zdroje stav se /// <para>Zachovává <b>barvu, tloušťku čáry a styl/zarovnání písma</b> přesně podle zdroje stav se
/// přebírá z <see cref="Layer"/> (barva + tloušťka pro každou entitu) a z parametru <c>style</c> /// přebírá z <see cref="Stav"/> (barva i s původem: ACI index / true color / ByLayer) a z parametru <c>style</c>
/// metody <see cref="Text"/> (font). Souřadnice jsou ve výkresových mm, osa Y nahoru shodně s DXF. /// metody <see cref="Text"/> (font). Souřadnice jsou ve výkresových mm, osa Y nahoru shodně s DXF.
/// Jednořádkový text → <c>TEXT</c>, víceřádkový (<c>maxWidthMm &gt; 0</c>) → <c>MTEXT</c>.</para> /// Jednořádkový text → <c>TEXT</c>, víceřádkový (<c>maxWidthMm &gt; 0</c>) → <c>MTEXT</c>.</para>
/// </summary> /// </summary>
@@ -36,27 +36,40 @@ public sealed class DxfProfileRenderer : IProfileRenderer
_doc.Layers.Add(_aktualniVrstva); _doc.Layers.Add(_aktualniVrstva);
} }
_vrstvy[_aktualniVrstva.Name] = _aktualniVrstva; _vrstvy[_aktualniVrstva.Name] = _aktualniVrstva;
_aktualniBarva = new Color(0, 0, 0); _aktualniBarva = Color.ByLayer;
} }
/// <summary>Hotový výkres se všemi rozpiskami (pro <c>DxfWriter.Write</c>).</summary> /// <summary>Hotový výkres se všemi rozpiskami (pro <c>DxfWriter.Write</c>).</summary>
public CadDocument Document => _doc; 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; _aktualniVrstva = NajdiVrstvu(s.Hladina);
if (!_vrstvy.TryGetValue(name, out var vrstva)) _aktualniBarva = NaColor(s.Barva);
_aktualniTloustka = s.TloustkaMm is { } mm ? SnapTloustka(mm) : LineWeightType.ByLayer;
}
/// <summary>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
/// (<c>!!NOPRINT</c>) a barva hladiny by byla náhodná.</summary>
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) }; vrstva = new AcLayer(name);
_doc.Layers.Add(vrstva); _doc.Layers.Add(vrstva);
} }
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; _vrstvy[name] = vrstva;
} return vrstva;
_aktualniVrstva = vrstva;
_aktualniBarva = RgbNaColor(rgb); // barva entity dle zdroje (ByLayer už rozbalil DxfTemplate)
_aktualniTloustka = SnapTloustka(lineWeightMm);
} }
public void Line(double x1, double y1, double x2, double y2) public void Line(double x1, double y1, double x2, double y2)
@@ -246,8 +259,16 @@ public sealed class DxfProfileRenderer : IProfileRenderer
return kandidat; return kandidat;
} }
private static Color RgbNaColor(int rgb) /// <summary>Barva do DXF <b>ve stejném druhu, v jakém přišla ze zdroje</b>: indexovaná zůstane
=> new((byte)((rgb >> 16) & 0xFF), (byte)((rgb >> 8) & 0xFF), (byte)(rgb & 0xFF)); /// čí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.</summary>
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,
};
/// <summary>Standardní tloušťky čar (1/100 mm) dle DXF; vstupní mm se přichytí k nejbližší.</summary> /// <summary>Standardní tloušťky čar (1/100 mm) dle DXF; vstupní mm se přichytí k nejbližší.</summary>
private static readonly int[] StdTloustky = private static readonly int[] StdTloustky =
+5 -3
View File
@@ -84,10 +84,12 @@ public static class ContourGeometry
/// </summary> /// </summary>
public interface IProfileRenderer public interface IProfileRenderer
{ {
/// <summary>Nastaví aktuální hladinu/styl kresby. <paramref name="rgb"/> = barva 0xRRGGBB /// <summary>Nastaví aktuální stav kresby hladinu a to, čím ji entita přebíjí
/// (rozbalená z DXF, ByLayer i index/true-color), tloušťka v mm. Bílá se kreslí černě /// (<see cref="KresliciStav"/>). 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 <see cref="KresliciStav.RgbProKresbu"/>; bílá se kreslí černě
/// (bílá na bílém papíře by zmizela).</summary> /// (bílá na bílém papíře by zmizela).</summary>
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 Line(double x1, double y1, double x2, double y2);
void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false); void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false);
void Circle(double cx, double cy, double r); void Circle(double cx, double cy, double r);
+79
View File
@@ -0,0 +1,79 @@
namespace Rozpisky.Rendering;
/// <summary>Původ barvy podle DXF. <see cref="ByLayer"/> a <see cref="ByBlock"/> nenesou vlastní
/// hodnotu barva se dopočítá z hladiny (resp. z vkládaného bloku).</summary>
public enum BarvaDruh : byte { ByLayer, ByBlock, Index, True }
/// <summary>
/// Barva tak, jak je zapsaná v DXF <b>i s původem</b>, 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.
/// <para><paramref name="Rgb"/> je vždy vyplněné pro kresbu na obrazovku; u ByLayer/ByBlock je
/// bezvýznamné (rozřeší se až v <see cref="KresliciStav.RgbProKresbu"/>).</para>
/// </summary>
public readonly record struct Barva(BarvaDruh Druh, byte Index, int Rgb)
{
/// <summary>Indexovaná barva AutoCADu (ACI 1255).</summary>
public static Barva Aci(int index) => new(BarvaDruh.Index, (byte)index, AciPaleta.Rgb(index));
/// <summary>Přímá barva (true color, DXF group 420).</summary>
public static Barva Prima(int rgb) => new(BarvaDruh.True, 0, rgb & 0xFFFFFF);
/// <summary>Barva se převezme z hladiny (ACI 256).</summary>
public static readonly Barva ZHladiny = new(BarvaDruh.ByLayer, 0, 0);
/// <summary>Barva se převezme z bloku, do kterého je entita vložená (ACI 0).</summary>
public static readonly Barva ZBloku = new(BarvaDruh.ByBlock, 0, 0);
/// <summary>true = barva nese vlastní hodnotu a nemusí se dohledávat jinde.</summary>
public bool JeVlastni => Druh is BarvaDruh.Index or BarvaDruh.True;
}
/// <summary>
/// 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 <c>!!NOPRINT</c> pak ve výstupu vycházel tisknutelný).
/// </summary>
public readonly record struct HladinaStav(
string Nazev, Barva Barva, string TypCary = "Continuous",
double TloustkaMm = 0, bool Tisknout = true, bool Zapnuta = true)
{
/// <summary>Hladina „0" s barvou 7 použije se, když zdroj hladinu neuvádí.</summary>
public static readonly HladinaStav Vychozi = new("0", Barva.Aci(7));
}
/// <summary>
/// Stav kresby pro jednu entitu: hladina a to, čím ji entita přebíjí. <c>null</c> u typu čáry
/// i tloušťky znamená „podle hladiny" stejná sémantika jako v DXF.
/// </summary>
public readonly record struct KresliciStav(
HladinaStav Hladina, Barva Barva, string? TypCary = null, double? TloustkaMm = null)
{
/// <summary>Výchozí tloušťka, když ji neurčí ani entita, ani hladina (DXF „Default").</summary>
public const double VychoziTloustkaMm = 0.18;
/// <summary>Barva pro rasterizaci: ByLayer i ByBlock se rozřeší barvou hladiny.
/// (ByBlock se sem dostane jen mimo blok, kde se chová jako ByLayer.)</summary>
public int RgbProKresbu()
{
if (Barva.JeVlastni) return Barva.Rgb;
return Hladina.Barva.JeVlastni ? Hladina.Barva.Rgb : 0x000000;
}
/// <summary>Tloušťka čáry v mm pro rasterizaci.</summary>
public double TloustkaKresby()
{
double mm = TloustkaMm ?? Hladina.TloustkaMm;
return mm > 0 ? mm : VychoziTloustkaMm;
}
/// <summary>Název typu čáry (Continuous, DASHED…).</summary>
public string TypCaryKresby()
{
var t = TypCary ?? Hladina.TypCary;
return string.IsNullOrWhiteSpace(t) ? "Continuous" : t;
}
/// <summary>Stav pro kresbu mimo zdrojový výkres (rámečky, pomocná grafika).</summary>
public static KresliciStav Vychozi => new(HladinaStav.Vychozi, Barva.ZHladiny);
}
+3 -2
View File
@@ -116,9 +116,10 @@ public sealed class SkiaProfileRenderer : IProfileRenderer, IDisposable
private float StrokePx => Math.Max((float)(_lineWeightMm * _pxPerMm), _minStrokePx); 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); 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á 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); var c = new SKColor(r, g, b);
+8 -2
View File
@@ -23,8 +23,14 @@ public sealed class TransformRenderer : IProfileRenderer
private double X(double x) => _ox + _s * x; private double X(double x) => _ox + _s * x;
private double Y(double y) => _oy + _s * y; private double Y(double y) => _oy + _s * y;
public void Layer(string name, int rgb, double lineWeightMm = 0.25) /// <summary>Stav propadá beze změny až na tloušťku čáry ta je v mm, takže se škáluje
=> _inner.Layer(name, rgb, lineWeightMm * _s); /// stejně jako geometrie (na entitě i na hladině).</summary>
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) public void Line(double x1, double y1, double x2, double y2)
=> _inner.Line(X(x1), Y(y1), X(x2), Y(y2)); => _inner.Line(X(x1), Y(y1), X(x2), Y(y2));