diff --git a/.claude/agents/render-dxf.md b/.claude/agents/render-dxf.md
index 63b20a1..bd5cb23 100644
--- a/.claude/agents/render-dxf.md
+++ b/.claude/agents/render-dxf.md
@@ -51,6 +51,6 @@ Needituj produkční data: `Rozpisky/Podklady/Rozpiska.dxf`, `vzorky/schema.dxf`
3. Ověř:
`dotnet build "Rozpisky.sln" -nologo -v q -clp:ErrorsOnly` a
`dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q`
- (baseline je 114 zelených testů — nesmí ubýt).
+ (baseline je 119 zelených testů — nesmí ubýt).
4. V odpovědi vrať: co jsi změnil (soubor:řádek), proč, a výsledek buildu a testů. Když něco
neprošlo, řekni to rovnou i s chybovou hláškou — nezakrývej to.
diff --git a/.claude/agents/verifikator.md b/.claude/agents/verifikator.md
index 5536cd7..d83f403 100644
--- a/.claude/agents/verifikator.md
+++ b/.claude/agents/verifikator.md
@@ -18,7 +18,7 @@ dotnet test "C:\Users\marek\_Osobní\C sharp\EXE\Rozpisky\Rozpisky.Tests\Rozpis
Testy spusť i tehdy, když build projde s varováními. Když build spadne, testy nespouštěj
a rovnou hlas chyby buildu.
-Baseline, se kterou porovnáváš: **build bez chyb a bez varování, 114 testů zelených, 0 přeskočených.**
+Baseline, se kterou porovnáváš: **build bez chyb a bez varování, 119 testů zelených, 0 přeskočených.**
Volitelně (jen když o to volající požádá) ověř, že aplikace naběhne:
spusť `Rozpisky\bin\Debug\net10.0-windows\Rozpisky.exe`, počkej ~8 s, zkontroluj, že proces
diff --git a/.claude/agents/wpf-ui.md b/.claude/agents/wpf-ui.md
index a207945..942d106 100644
--- a/.claude/agents/wpf-ui.md
+++ b/.claude/agents/wpf-ui.md
@@ -36,7 +36,7 @@ nebo `Rozpisky/Xlsx/` jen popiš v odpovědi a nech je na volajícím.
začneš měnit UI.
2. `MainViewModel.cs` je velký; hledej v něm grepem, nečti ho celý zbytečně.
3. Ověř: `dotnet build "Rozpisky.sln" -nologo -v q -clp:ErrorsOnly` a
- `dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q` (baseline 114 zelených).
+ `dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q` (baseline 119 zelených).
Build musí být **bez varování** — chyby v XAML se často projeví jen jako varování.
4. WPF chyby v bindingu build nezachytí. Pokud změna může binding rozbít, řekni to explicitně
a doporuč ruční spuštění aplikace.
diff --git a/CLAUDE.md b/CLAUDE.md
index f696ac9..026400a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -11,7 +11,7 @@ dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q
dotnet run --project "Rozpisky\Rozpisky.csproj"
```
-Baseline: build bez chyb a varování, **114 testů zelených**. Před hlášením hotovo obojí spustit
+Baseline: build bez chyb a varování, **119 testů zelených**. Před hlášením hotovo obojí spustit
(nebo použít `/overit`).
## Architektura — jediné pravidlo, které se nesmí porušit
diff --git a/Rozpisky.Tests/SrafaExportTesty.cs b/Rozpisky.Tests/SrafaExportTesty.cs
new file mode 100644
index 0000000..5f04b97
--- /dev/null
+++ b/Rozpisky.Tests/SrafaExportTesty.cs
@@ -0,0 +1,111 @@
+using System.IO;
+using ACadSharp.Entities;
+using ACadSharp.IO;
+using Rozpisky.Dxf;
+using Rozpisky.Rendering;
+using Xunit;
+
+namespace Rozpisky.Tests;
+
+///
+/// Šrafa v exportovaném DXF. Vzor musí jít ven i s definičními čarami – DXF je nese uvnitř
+/// entity HATCH, a bez nich si ho cílový CAD dosadí ze svých .pat, ve svých
+/// jednotkách a měřítku. Pak vypadá v každém výkresu jinak a většinou tak hustě, že to ani
+/// nevypadá jako šrafa. A protože se logo do rámečku zmenšuje, musí se s ním zmenšit i vzor.
+///
+public class SrafaExportTesty : IDisposable
+{
+ private readonly string _soubor = Path.Combine(Path.GetTempPath(), "rozpisky-srafa-" + Guid.NewGuid().ToString("N") + ".dxf");
+
+ public void Dispose()
+ {
+ try { File.Delete(_soubor); } catch (IOException) { }
+ GC.SuppressFinalize(this);
+ }
+
+ /// Přehraje logo do DXF (volitelně přes zmenšení do rámečku) a načte výsledek zpět.
+ private List Kolecko(double meritko)
+ {
+ var dxf = new DxfProfileRenderer();
+ IProfileRenderer cil = meritko == 1 ? dxf : new TransformRenderer(dxf, meritko, 0, 0);
+ new DxfTemplate(TestData.Logo("md_sfdi.dxf")).Replay(cil);
+ DxfWriter.Write(_soubor, dxf.Document, binary: true);
+ return DxfReader.Read(_soubor).Entities.OfType().ToList();
+ }
+
+ /// Rozteč rovnoběžek vzoru = kolmá složka posunu mezi sousedními čarami.
+ private static double Roztec(HatchPattern.Line cara)
+ {
+ double nx = -Math.Sin(cara.Angle), ny = Math.Cos(cara.Angle);
+ return Math.Abs(cara.Offset.X * nx + cara.Offset.Y * ny);
+ }
+
+ private static double RoztecZdroje()
+ {
+ var zdroj = DxfReader.Read(TestData.Logo("md_sfdi.dxf"))
+ .Entities.OfType().First(h => !h.IsSolid);
+ return Roztec(zdroj.Pattern!.Lines[0]);
+ }
+
+ [Fact]
+ public void ExportovanaSrafaNeseDefiniciVzoru()
+ {
+ var srafy = Kolecko(meritko: 1).Where(h => !h.IsSolid).ToList();
+
+ Assert.NotEmpty(srafy);
+ foreach (var h in srafy)
+ {
+ Assert.Equal("ANSI31", h.Pattern?.Name);
+ Assert.NotNull(h.Pattern);
+ Assert.NotEmpty(h.Pattern!.Lines); // tohle chybělo: vzor šel ven jen jménem
+ }
+ }
+
+ [Fact]
+ public void RoztecVzoruOdpovidaZdroji()
+ {
+ var srafa = Kolecko(meritko: 1).First(h => !h.IsSolid);
+ Assert.Equal(RoztecZdroje(), Roztec(srafa.Pattern!.Lines[0]), 9);
+ }
+
+ [Fact]
+ public void ZmensenimLogaSeZmensiIVzor()
+ {
+ // Logo se do rámečku rozpisky vejde jen zmenšené. Kdyby si vzor nechal původní rozteč,
+ // vyšla by v CADu buď plná plocha, nebo pár čar přes celé logo.
+ const double meritko = 0.1;
+ var srafa = Kolecko(meritko).First(h => !h.IsSolid);
+
+ Assert.Equal(RoztecZdroje() * meritko, Roztec(srafa.Pattern!.Lines[0]), 9);
+ }
+
+ [Fact]
+ public void MeritkoAUhelZustavajiNeutralni()
+ {
+ // ACadSharp při zápisu násobí rozteč vzoru hodnotou PatternScale, ale při čtení zpátky
+ // nedělí – s nenulovým měřítkem by se vzor s každým kolečkem roztáhl. Efektivní hodnoty
+ // proto nese samotná definice a měřítko s úhlem zůstávají neutrální.
+ var srafa = Kolecko(meritko: 1).First(h => !h.IsSolid);
+
+ Assert.Equal(1, srafa.PatternScale, 9);
+ Assert.Equal(0, srafa.PatternAngle, 9);
+ }
+
+ [Fact]
+ public void PlnaVyplnZustavaSolidBezVzoru()
+ {
+ // md_sfdi má všechny šrafy vzorované, plné výplně jsou v ostatních logách.
+ var dxf = new DxfProfileRenderer();
+ new DxfTemplate(TestData.Logo("exprojekt.dxf")).Replay(dxf);
+ DxfWriter.Write(_soubor, dxf.Document, binary: true);
+ var solidni = DxfReader.Read(_soubor).Entities.OfType().ToList();
+
+ Assert.NotEmpty(solidni);
+ foreach (var h in solidni)
+ {
+ Assert.True(h.IsSolid);
+ Assert.Equal("SOLID", h.Pattern?.Name);
+ Assert.Empty(h.Pattern!.Lines);
+ }
+ }
+}
diff --git a/Rozpisky/Dxf/DxfTemplate.cs b/Rozpisky/Dxf/DxfTemplate.cs
index 43905fa..57d9dc6 100644
--- a/Rozpisky/Dxf/DxfTemplate.cs
+++ b/Rozpisky/Dxf/DxfTemplate.cs
@@ -384,8 +384,23 @@ public sealed partial class DxfTemplate
|| vzor.Equals("SOLID", StringComparison.OrdinalIgnoreCase))
return default;
+ var definice = DefiniceVzoru(hatch.Pattern);
return Vypln.Vzorovana(vzor, hatch.PatternScale, hatch.PatternAngle * RadToDeg,
- SrafaVzor.Cary(hatch.Pattern, smycky));
+ definice, SrafaVzor.Cary(definice, smycky));
+ }
+
+ /// Definiční čáry vzoru šrafy z entity. DXF je nese uvnitř HATCH (group 78 a dál),
+ /// tedy už v efektivních hodnotách – měřítko a úhel z vlastností šrafy jsou v nich započítané.
+ private static List DefiniceVzoru(HatchPattern? vzor)
+ {
+ var vysledek = new List();
+ foreach (var cara in vzor?.Lines ?? new List())
+ vysledek.Add(new VzorCara(
+ cara.Angle * RadToDeg,
+ (cara.BasePoint.X, cara.BasePoint.Y),
+ (cara.Offset.X, cara.Offset.Y),
+ cara.DashLengths?.ToList() ?? new List()));
+ return vysledek;
}
/// Popis hladiny pro kreslítko – celý, ať se dá ve výstupu postavit znovu
diff --git a/Rozpisky/Dxf/SrafaVzor.cs b/Rozpisky/Dxf/SrafaVzor.cs
index 22f53d6..3baa4b6 100644
--- a/Rozpisky/Dxf/SrafaVzor.cs
+++ b/Rozpisky/Dxf/SrafaVzor.cs
@@ -1,4 +1,3 @@
-using ACadSharp.Entities;
using Rozpisky.Rendering;
namespace Rozpisky.Dxf;
@@ -19,10 +18,10 @@ internal static class SrafaVzor
/// Čáry vzoru uvnitř zadaných smyček. Prázdné, když vzor není použitelný –
/// volající pak plochu nechá bez výplně (lepší než ji zaplácnout načerno).
- public static IReadOnlyList Cary(HatchPattern? vzor, IReadOnlyList smycky)
+ public static IReadOnlyList Cary(IReadOnlyList? definice, IReadOnlyList smycky)
{
var vysledek = new List();
- if (vzor?.Lines is not { Count: > 0 }) return vysledek;
+ if (definice is not { Count: > 0 }) return vysledek;
var polygony = Zplosti(smycky);
if (polygony.Count == 0) return vysledek;
@@ -30,7 +29,7 @@ internal static class SrafaVzor
var (minX, minY, maxX, maxY) = Rozsah(polygony);
if (maxX - minX <= 0 || maxY - minY <= 0) return vysledek;
- foreach (var cara in vzor.Lines)
+ foreach (var cara in definice)
{
PridejCaryVzoru(cara, polygony, minX, minY, maxX, maxY, vysledek);
if (vysledek.Count >= MaxCar) break;
@@ -42,10 +41,11 @@ internal static class SrafaVzor
/// rovnoběžkami je vektor Offset ve světových souřadnicích: jeho kolmá složka určuje
/// rozteč, podélná posouvá čárkování. U ANSI31 vychází přesně kolmý na směr 45°.
private static void PridejCaryVzoru(
- HatchPattern.Line cara, List> polygony,
+ VzorCara cara, List> polygony,
double minX, double minY, double maxX, double maxY, List vysledek)
{
- double dx = Math.Cos(cara.Angle), dy = Math.Sin(cara.Angle); // směr čáry
+ double uhel = cara.UhelDeg * Math.PI / 180;
+ double dx = Math.Cos(uhel), dy = Math.Sin(uhel); // směr čáry
double nx = -dy, ny = dx; // kolmice
double ox = cara.Offset.X, oy = cara.Offset.Y;
@@ -53,7 +53,7 @@ internal static class SrafaVzor
if (Math.Abs(kolma) < 1e-9) return; // rovnoběžky by splynuly
// Kolik kroků pokryje bbox: promítni jeho rohy na kolmici.
- double zaklad = cara.BasePoint.X * nx + cara.BasePoint.Y * ny;
+ double zaklad = cara.Zaklad.X * nx + cara.Zaklad.Y * ny;
var projekce = new[]
{
minX * nx + minY * ny, maxX * nx + minY * ny,
@@ -66,11 +66,11 @@ internal static class SrafaVzor
int od = (int)Math.Floor(kOd) - 1, doK = (int)Math.Ceiling(kDo) + 1;
if ((long)doK - od > MaxCar) return; // nesmyslně husté měřítko
- var carky = Carky(cara.DashLengths);
+ var carky = Carky(cara.Carky);
for (int k = od; k <= doK; k++)
{
- double px = cara.BasePoint.X + k * ox, py = cara.BasePoint.Y + k * oy;
+ double px = cara.Zaklad.X + k * ox, py = cara.Zaklad.Y + k * oy;
foreach (var (t1, t2) in Uvnitr(polygony, px, py, dx, dy))
foreach (var (u1, u2) in Orez(carky, t1, t2))
{
diff --git a/Rozpisky/Rendering/DxfProfileRenderer.cs b/Rozpisky/Rendering/DxfProfileRenderer.cs
index 83a8aba..e00b5ff 100644
--- a/Rozpisky/Rendering/DxfProfileRenderer.cs
+++ b/Rozpisky/Rendering/DxfProfileRenderer.cs
@@ -165,14 +165,20 @@ public sealed class DxfProfileRenderer : IProfileRenderer
if (loops.Count == 0) return;
try
{
- // Vzor šrafy se zapisuje zpátky jménem, měřítkem a úhlem – v CADu tak zůstane
- // editovatelný. Rozvinuté čáry (Vypln.Cary) jsou jen pro rastrové renderery.
+ // Vzor šrafy se zapisuje zpátky i s definičními čarami – DXF je nese uvnitř entity
+ // HATCH, a bez nich si cílový CAD vzor dosadí ze svých .pat, ve svých jednotkách
+ // a měřítku. Rozvinuté čáry (Vypln.Cary) jsou naopak jen pro rastrové renderery.
+ //
+ // Měřítko a úhel zůstávají neutrální (1 a 0), protože definiční čáry jsou už
+ // v efektivních hodnotách – měřítko i natočení jsou v nich započítané. ACadSharp
+ // při zápisu násobí rozteč vzoru hodnotou PatternScale, ale při čtení zpátky nedělí;
+ // s nenulovým měřítkem by se tak vzor s každým kolečkem roztáhl.
var hatch = new Hatch
{
IsSolid = vypln.JeSolid,
- Pattern = new HatchPattern(vypln.JeSolid ? "SOLID" : vypln.Vzor!),
- PatternScale = vypln.JeSolid || vypln.Meritko <= 0 ? 1 : vypln.Meritko,
- PatternAngle = vypln.JeSolid ? 0 : vypln.UhelDeg * Math.PI / 180.0,
+ Pattern = VzorProHatch(vypln),
+ PatternScale = 1,
+ PatternAngle = 0,
};
foreach (var loop in loops)
{
@@ -201,6 +207,28 @@ public sealed class DxfProfileRenderer : IProfileRenderer
}
}
+ /// Vzor pro entitu HATCH: jméno a definiční čáry. Bez definičních čar má výstup jen
+ /// jméno vzoru a cílový CAD si podle něj dosadí vlastní definici – v jiných jednotkách
+ /// a jiném měřítku, takže šrafa vypadá v každém výkresu jinak.
+ private static HatchPattern VzorProHatch(Vypln vypln)
+ {
+ var vzor = new HatchPattern(vypln.JeSolid ? "SOLID" : vypln.Vzor!);
+ if (vypln.JeSolid) return vzor;
+
+ foreach (var cara in vypln.Definice ?? Array.Empty())
+ {
+ var radek = new HatchPattern.Line
+ {
+ Angle = cara.UhelDeg * Math.PI / 180.0,
+ BasePoint = new XY(cara.Zaklad.X, cara.Zaklad.Y),
+ Offset = new XY(cara.Offset.X, cara.Offset.Y),
+ };
+ foreach (var d in cara.Carky) radek.DashLengths.Add(d);
+ vzor.Lines.Add(radek);
+ }
+ return vzor;
+ }
+
/// Směrový vektor osy X textu pro dané natočení – tím se natáčí MTEXT.
private static XYZ SmerOsyX(double rotationDeg)
{
diff --git a/Rozpisky/Rendering/IProfileRenderer.cs b/Rozpisky/Rendering/IProfileRenderer.cs
index a089e6b..5ef9703 100644
--- a/Rozpisky/Rendering/IProfileRenderer.cs
+++ b/Rozpisky/Rendering/IProfileRenderer.cs
@@ -56,23 +56,37 @@ public readonly record struct Edge(
/// Úsečka ve výkresových mm – čára vzoru šrafy.
public readonly record struct Usecka(double X1, double Y1, double X2, double Y2);
+///
+/// Jedna definiční čára vzoru šrafy = celá rodina rovnoběžek: směr ,
+/// jedna čára prochází , sousední jsou posunuté o vektor
+/// (kolmá složka = rozteč, podélná posouvá čárkování).
+/// = délky čárek a mezer (záporná = mezera); prázdné = plná čára.
+/// Tohle je skutečná definice vzoru, ne jen jeho jméno. DXF ji nese uvnitř entity
+/// HATCH, takže je nutné ji zapsat i do výstupu – jinak si ji cílový CAD dosadí ze svých
+/// .pat souborů, ve svých jednotkách a měřítku, a šrafa vyjde pokaždé jinak.
+///
+public readonly record struct VzorCara(
+ double UhelDeg, (double X, double Y) Zaklad, (double X, double Y) Offset, IReadOnlyList Carky);
+
///
/// Druh výplně plochy. default = plná výplň (SOLID).
/// Nesolidní šrafa (ANSI31 apod.) se rastrovým rendererům předává už rozvinutá na
/// , protože Skia vzory šraf neumí; DXF export naopak
-/// ignoruje a zapíše zpátky HATCH s původním jménem vzoru, měřítkem a úhlem, aby zůstal
-/// v CADu editovatelný. Dřív se každá šrafa kreslila jako plná výplň – z logotypu
-/// md_sfdi.dxf byl černý blok.
+/// ignoruje a zapíše zpátky HATCH se jménem vzoru, měřítkem, úhlem a definičními čarami
+/// (), aby zůstal editovatelný a vypadal stejně jako ve zdroji. Dřív se
+/// každá šrafa kreslila jako plná výplň – z logotypu md_sfdi.dxf byl černý blok.
///
public readonly record struct Vypln(
- string? Vzor = null, double Meritko = 1, double UhelDeg = 0, IReadOnlyList? Cary = null)
+ string? Vzor = null, double Meritko = 1, double UhelDeg = 0,
+ IReadOnlyList? Cary = null, IReadOnlyList? Definice = null)
{
/// Plná výplň – buď se vzor neuvádí (default), nebo je to SOLID.
public bool JeSolid => string.IsNullOrEmpty(Vzor) || Vzor.Equals("SOLID", StringComparison.OrdinalIgnoreCase);
- /// Šrafa se vzorem, už rozvinutá na čáry pro rastrové renderery.
- public static Vypln Vzorovana(string vzor, double meritko, double uhelDeg, IReadOnlyList cary)
- => new(vzor, meritko, uhelDeg, cary);
+ /// Šrafa se vzorem: definice pro DXF a z ní rozvinuté čáry pro rastr.
+ public static Vypln Vzorovana(string vzor, double meritko, double uhelDeg,
+ IReadOnlyList definice, IReadOnlyList cary)
+ => new(vzor, meritko, uhelDeg, cary, definice);
}
/// Obrys = zřetězené hrany (konec jedné = počátek další). = true →
diff --git a/Rozpisky/Rendering/TransformRenderer.cs b/Rozpisky/Rendering/TransformRenderer.cs
index d39269a..90af946 100644
--- a/Rozpisky/Rendering/TransformRenderer.cs
+++ b/Rozpisky/Rendering/TransformRenderer.cs
@@ -132,14 +132,32 @@ public sealed class TransformRenderer : IProfileRenderer
=> _inner.Fill(
loops.Select(c => new Contour(
_zachovavaOblouky ? c.Edges.Select(T).ToList() : Usecky(c), c.Closed)).ToList(),
- vypln.Cary is null
- ? vypln
- : vypln with
- {
- Cary = vypln.Cary
- .Select(u => new Usecka(X(u.X1, u.Y1), Y(u.X1, u.Y1), X(u.X2, u.Y2), Y(u.X2, u.Y2)))
- .ToList(),
- });
+ Vypln(vypln));
+
+ /// Přemapuje výplň spolu s geometrií. Šrafa se musí zmenšit se svou plochou –
+ /// jinak logo zmenšené do rámečku dostane vzor v původní rozteči a v CADu z něj vyjde
+ /// buď plná plocha, nebo pár čar přes celé logo.
+ private Vypln Vypln(Vypln v)
+ {
+ if (v.JeSolid) return v;
+ return v with
+ {
+ Meritko = v.Meritko * _meritko,
+ UhelDeg = v.UhelDeg + _uhelDeg,
+ Cary = v.Cary?.Select(u => new Usecka(X(u.X1, u.Y1), Y(u.X1, u.Y1), X(u.X2, u.Y2), Y(u.X2, u.Y2))).ToList(),
+ Definice = v.Definice?.Select(Vzor).ToList(),
+ };
+ }
+
+ /// Definiční čára vzoru: vztažný bod je bod (celá transformace), rozteč je vektor
+ /// (jen lineární část, bez posunu), délky čárek se škálují, směr se otočí.
+ private VzorCara Vzor(VzorCara c) => c with
+ {
+ UhelDeg = c.UhelDeg + _uhelDeg,
+ Zaklad = (X(c.Zaklad.X, c.Zaklad.Y), Y(c.Zaklad.X, c.Zaklad.Y)),
+ Offset = (_a * c.Offset.X + _c * c.Offset.Y, _b * c.Offset.X + _d * c.Offset.Y),
+ Carky = c.Carky.Select(d => d * _meritko).ToList(),
+ };
/// Obrys rozložený na úsečky (pro transformace, které oblouk nezachovají).
private List Usecky(Contour c)
diff --git a/docs/ROZPISKY.md b/docs/ROZPISKY.md
index 68dd564..adeb461 100644
--- a/docs/ROZPISKY.md
+++ b/docs/ROZPISKY.md
@@ -189,8 +189,11 @@ Implementace:
- **Zarovnání textu `Aligned` / `Fit`** se aproximuje na `Left` – text se neroztáhne na cílovou délku.
- **Typ čáry** se přenáší jako název, ale rastrové renderery kreslí vždy plnou čáru; čárkované styly
se projeví až v DXF (v dodávaných souborech je stejně všude `Continuous`).
-- **Šrafy** se do DXF zapisují se jménem vzoru, měřítkem a úhlem, ale bez definice čar vzoru – CAD
- si ji dohledá ve svých `.pat`. Vzor, který cílový CAD nezná, se v něm nevykreslí.
+- **Šrafy** se do DXF zapisují i s definičními čarami vzoru (DXF je nese uvnitř entity `HATCH`),
+ takže vypadají stejně jako ve zdroji nezávisle na tom, jaké `.pat` a jednotky má cílový CAD.
+ Měřítko a úhel šrafy zůstávají neutrální (1 a 0) – efektivní hodnoty nese samotná definice.
+ Důvod: ACadSharp při zápisu násobí rozteč vzoru hodnotou `PatternScale`, ale při čtení zpátky
+ nedělí, takže s nenulovým měřítkem by se vzor s každým kolečkem roztáhl.
---