From a900acc7bb4c014560b384af6d6dffe355dd7e3e Mon Sep 17 00:00:00 2001 From: Josef Marek Date: Mon, 7 Sep 2026 14:46:02 +0200 Subject: [PATCH] =?UTF-8?q?Entity:=20rozvinout=20bloky,=20doplnit=20elipsy?= =?UTF-8?q?=20a=20hl=C3=A1sit,=20co=20program=20neum=C3=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Rozpisky.Tests/DxfBarvyRoundTripTesty.cs | 9 ++ Rozpisky.Tests/EntityTesty.cs | 194 +++++++++++++++++++++++ Rozpisky.Tests/RecordingRenderer.cs | 9 +- Rozpisky/Dxf/DxfTemplate.cs | 180 +++++++++++++++++++-- Rozpisky/Dxf/RozpiskaRenderer.cs | 22 +++ Rozpisky/Rendering/DxfProfileRenderer.cs | 6 + Rozpisky/Rendering/TransformRenderer.cs | 144 +++++++++++++---- Rozpisky/ViewModels/MainViewModel.cs | 29 +++- 8 files changed, 548 insertions(+), 45 deletions(-) create mode 100644 Rozpisky.Tests/EntityTesty.cs diff --git a/Rozpisky.Tests/DxfBarvyRoundTripTesty.cs b/Rozpisky.Tests/DxfBarvyRoundTripTesty.cs index f467c39..0d5dd50 100644 --- a/Rozpisky.Tests/DxfBarvyRoundTripTesty.cs +++ b/Rozpisky.Tests/DxfBarvyRoundTripTesty.cs @@ -87,6 +87,15 @@ public class DxfBarvyRoundTripTesty : IDisposable } } + [Fact] + public void VerzeVystupuJeZafixovana() + { + // Bez explicitního nastavení určí verzi výchozí hodnota ACadSharp – a tu může upgrade + // balíčku tiše změnit. Verze rozhoduje, čím půjde výkres u kolegy otevřít. + var doc = Kolecko(); + Assert.Equal(DxfProfileRenderer.VerzeVystupu, doc.Header.Version); + } + [Fact] public void RamecekNoPrintZustaneNetisknutelny() { diff --git a/Rozpisky.Tests/EntityTesty.cs b/Rozpisky.Tests/EntityTesty.cs new file mode 100644 index 0000000..2df4ac3 --- /dev/null +++ b/Rozpisky.Tests/EntityTesty.cs @@ -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; + +/// +/// Entity, které dřív propadly switchem bez default 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í. +/// +public class EntityTesty +{ + /// Přehraje dokument sestavený v paměti přes dočasný soubor (DxfTemplate čte ze souboru). + private static (RecordingRenderer G, IReadOnlyDictionary 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) { } + } + } + + /// Dokument s jedním blokem (čtvercová čára 0,0–10,10) a jeho vložením. + private static CadDocument SBlokem(Action 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) { } + } + } +} diff --git a/Rozpisky.Tests/RecordingRenderer.cs b/Rozpisky.Tests/RecordingRenderer.cs index b7515f2..a76dc21 100644 --- a/Rozpisky.Tests/RecordingRenderer.cs +++ b/Rozpisky.Tests/RecordingRenderer.cs @@ -33,7 +33,14 @@ public sealed class RecordingRenderer : IProfileRenderer public void Stav(in KresliciStav s) => Stavy.Add(s); - public void Line(double x1, double y1, double x2, double y2) => LineCount++; + /// Koncové body zaznamenaných úseček – testy tvrdí i o poloze, nejen o počtu. + public List<((double X, double Y) Od, (double X, double Y) Do)> Lines { get; } = new(); + + public void Line(double x1, double y1, double x2, double y2) + { + LineCount++; + Lines.Add(((x1, y1), (x2, y2))); + } public void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false) => Polylines.Add(pts); diff --git a/Rozpisky/Dxf/DxfTemplate.cs b/Rozpisky/Dxf/DxfTemplate.cs index 270e004..43905fa 100644 --- a/Rozpisky/Dxf/DxfTemplate.cs +++ b/Rozpisky/Dxf/DxfTemplate.cs @@ -86,12 +86,34 @@ public sealed partial class DxfTemplate /// (např. A4 rámeček) – pro DXF export jako referenci hranice papíru. public void Replay(IProfileRenderer g, IReadOnlyDictionary? values = null, bool includeNoPrint = false) { - // Jeden průchod v pořadí kresby (viz EntitiesInDrawOrder) – zachovává překrývání entit ze - // zdroje (výplně vzadu, popisky vepředu apod.). Dispatch podle typu; odvozené typy (Arc:Circle, - // AttributeDefinition:AttributeBase:TextEntity) musí být ve switchi PŘED svými předky. - foreach (var e in EntitiesInDrawOrder()) + _nezpracovane.Clear(); + Prehraj(g, EntitiesInDrawOrder(), values, includeNoPrint, vlozeny: null, hloubka: 0); + } + + /// Počty entit, které kreslítko neumí, podle typu (např. DIMENSION → 3). + /// Naplní se při každém . Prázdné = vykreslilo se všechno. Slouží + /// k upozornění uživatele, když si přidá logo s prvky, které program nezná – dřív takové + /// prvky mizely tiše. + public IReadOnlyDictionary NezpracovaneEntity => _nezpracovane; + + private readonly Dictionary _nezpracovane = new(StringComparer.Ordinal); + + /// Nejhlubší zanoření bloků, do kterého se ještě jde. Chrání před blokem, který + /// (chybou ve zdroji) vkládá sám sebe. + private const int MaxHloubkaBloku = 8; + + /// Jeden průchod v pořadí kresby (viz ) – zachovává + /// překrývání entit ze zdroje (výplně vzadu, popisky vepředu). Dispatch podle typu; odvozené + /// typy (Arc:Circle, AttributeDefinition:AttributeBase:TextEntity) musí být ve switchi PŘED + /// svými předky. = stav vkládajícího INSERTu při rozvíjení + /// bloku (řeší barvy ByBlock a hladinu „0"), null v model space. + private void Prehraj(IProfileRenderer g, IEnumerable entity, + IReadOnlyDictionary? values, bool includeNoPrint, + KresliciStav? vlozeny, int hloubka) + { + foreach (var e in entity) { - if (!SetLayer(g, e, includeNoPrint)) continue; + if (!SetLayer(g, e, includeNoPrint, vlozeny)) continue; switch (e) { case Line l: @@ -110,7 +132,7 @@ public sealed partial class DxfTemplate // Spliny (NURBS) – tesselace na lomenou čáru přes ACadSharp. case Spline sp: { - var pts = sp.PolygonalVertexes(64).Select(p => (p.X, p.Y)).ToList(); + var pts = sp.PolygonalVertexes(DilkuSplinu(sp)).Select(p => (p.X, p.Y)).ToList(); if (pts.Count >= 2) g.Polyline(pts, sp.IsClosed); break; } @@ -174,10 +196,107 @@ public sealed partial class DxfTemplate StyleOf(mt.Style), wf, mt.RectangleWidth); break; } + + // Vložený blok – rozvine se rekurzivně. Bez toho by se uživatelovo logo s blokem + // nevykreslilo vůbec a bez hlášky. + case Insert ins: + RozvinBlok(g, ins, values, includeNoPrint, hloubka); + break; + + // Elipsa: rozhraní zná jen kruhové oblouky, takže se navzorkuje na lomenou čáru. + case Ellipse el: + { + var body = VzorkujEllipse(el); + if (body.Count >= 2) g.Polyline(body, el.IsFullEllipse); + break; + } + + // Bod – kreslí se jako tečka (úsečka nulové délky, kulaté zakončení čáry). + case Point p: + g.Line(p.Location.X, p.Location.Y, p.Location.X, p.Location.Y); + break; + + // Vyplněný čtyřúhelník. Pozor: v DXF jsou 3. a 4. roh prohozené proti obvodu. + case Solid s: + KresliCtyruhelnik(g, s.FirstCorner.X, s.FirstCorner.Y, s.SecondCorner.X, s.SecondCorner.Y, + s.ThirdCorner.X, s.ThirdCorner.Y, s.FourthCorner.X, s.FourthCorner.Y); + break; + + case Face3D f: + KresliCtyruhelnik(g, f.FirstCorner.X, f.FirstCorner.Y, f.SecondCorner.X, f.SecondCorner.Y, + f.ThirdCorner.X, f.ThirdCorner.Y, f.FourthCorner.X, f.FourthCorner.Y); + break; + + // Prostorová polylinie – promítne se do roviny XY (kresba je 2D). + case Polyline3D pl3: + { + var body = pl3.Vertices.Select(v => (v.Location.X, v.Location.Y)).ToList(); + if (body.Count >= 2) g.Polyline(body, pl3.IsClosed); + break; + } + + default: + Nezpracovano(e.ObjectName); + break; } } } + /// Rozvine INSERT: entity bloku se přehrají přes transformaci (posun na + /// vkládací bod, měřítko os, natočení). Barvy ByBlock a hladina „0" uvnitř bloku + /// přebírají stav vkládající entity – tak to dělá i AutoCAD. + private void RozvinBlok(IProfileRenderer g, Insert ins, + IReadOnlyDictionary? values, bool includeNoPrint, int hloubka) + { + if (hloubka >= MaxHloubkaBloku) { Nezpracovano("INSERT (příliš zanořený)"); return; } + + var blok = ins.Block; + if (blok is null) { Nezpracovano("INSERT (chybí definice bloku)"); return; } + + var zaklad = blok.BlockEntity?.BasePoint ?? default; + var transformace = TransformRenderer.ProBlok( + g, ins.InsertPoint.X, ins.InsertPoint.Y, zaklad.X, zaklad.Y, + ins.XScale, ins.YScale, ins.Rotation); + + var stavVkladajici = new KresliciStav(HladinaZ(ins.Layer), BarvaZ(ins.Color), + ins.LineType?.Name, TloustkaZ(ins.LineWeight)); + Prehraj(transformace, blok.Entities, values, includeNoPrint, stavVkladajici, hloubka + 1); + } + + /// Vyplněný čtyřúhelník ze čtyř rohů v pořadí DXF (3. a 4. roh prohozený). + /// Trojúhelník se pozná podle shodného 3. a 4. rohu. + private static void KresliCtyruhelnik(IProfileRenderer g, double x1, double y1, double x2, double y2, + double x3, double y3, double x4, double y4) + { + var hrany = new List { Edge.Line(x1, y1, x2, y2), Edge.Line(x2, y2, x4, y4) }; + if (Math.Abs(x3 - x4) > 1e-9 || Math.Abs(y3 - y4) > 1e-9) + hrany.Add(Edge.Line(x4, y4, x3, y3)); + hrany.Add(Edge.Line(hrany[^1].EX, hrany[^1].EY, x1, y1)); + g.Fill(new[] { new Contour(hrany, Closed: true) }); + } + + /// Na kolik dílků rozložit spline. Pevných 64 bylo málo pro dlouhé křivky (viditelné + /// hrany) a zbytečně moc pro krátké; počítá se z délky řídicího polygonu tak, aby jeden dílek + /// vyšel zhruba na 0,3 mm výkresu. + private static int DilkuSplinu(Spline sp) + { + const double DilekMm = 0.3; + double delka = 0; + var body = sp.ControlPoints; + for (int i = 1; i < body.Count; i++) + { + double dx = body[i].X - body[i - 1].X, dy = body[i].Y - body[i - 1].Y; + delka += Math.Sqrt(dx * dx + dy * dy); + } + return Math.Clamp((int)Math.Ceiling(delka / DilekMm), 16, 512); + } + + private void Nezpracovano(string typ) + => _nezpracovane[typ] = _nezpracovane.GetValueOrDefault(typ) + 1; + + /// Tloušťka čáry v mm z DXF hodnoty (setiny mm); záporná = podle hladiny → null. + private static double? TloustkaZ(LineWeightType lw) => (int)lw > 0 ? (int)lw / 100.0 : null; + /// Entity model-space v pořadí kresby: pokud má blok tabulku pořadí (SortEntitiesTable, /// tj. „draw order" nastavené v CADu), seřadí podle ní; jinak zachová pořadí ze souboru. Nižší sort /// handle = kreslí se dřív (vzadu). LINQ OrderBy je stabilní → entity bez záznamu drží pořadí. @@ -208,18 +327,30 @@ public sealed partial class DxfTemplate /// Nastaví hladinu do kreslítka. Vrací false, pokud se hladina nemá tisknout (přeskočit kresbu). /// = true → geometrie na se nepřeskočí /// (kreslí se i přes vypnutý plot flag – DXF reference A4 rámečku). - private static bool SetLayer(IProfileRenderer g, Entity e, bool includeNoPrint = false) + private static bool SetLayer(IProfileRenderer g, Entity e, bool includeNoPrint = false, + KresliciStav? vlozeny = null) { var layer = e.Layer; 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; - int lw = (int)e.LineWeight; // LineWeightType: Wnn = nn/100 mm, záporné = ByLayer/Default/ByBlock - g.Stav(new KresliciStav( - HladinaZ(layer), - BarvaZ(e.Color), - e.LineType?.Name, - lw > 0 ? lw / 100.0 : null)); // null = tloušťka podle hladiny + + var hladina = HladinaZ(layer); + var barva = BarvaZ(e.Color); + var tloustka = TloustkaZ(e.LineWeight); + var typCary = e.LineType?.Name; + + // Uvnitř bloku: hladina „0" a barva/typ čáry ByBlock se berou od vkládající entity – + // právě proto se stejný blok dá vložit dvakrát a pokaždé v jiné barvě. + if (vlozeny is { } v) + { + if (hladina.Nazev is null or "0") hladina = v.Hladina; + if (barva.Druh == BarvaDruh.ByBlock) barva = v.Barva; + tloustka ??= v.TloustkaMm; + if (typCary is null or "ByBlock") typCary = v.TypCary; + } + + g.Stav(new KresliciStav(hladina, barva, typCary, tloustka)); return true; } @@ -621,6 +752,29 @@ public sealed partial class DxfTemplate return pts; } + /// Navzorkuje entitu ELLIPSE na body. Kreslicí rozhraní zná jen kruhové oblouky, + /// takže elipsa jde do lomené čáry – dělení podle hlavní poloosy, aby byla dost hladká. + /// Parametry (nikoli úhly) jdou vždy proti směru hodin od StartParameter. + private static List<(double X, double Y)> VzorkujEllipse(Ellipse el) + { + double sa = el.StartParameter, ea = el.EndParameter; + if (ea <= sa) ea += 2 * Math.PI; + double sweep = ea - sa; + + double mx = el.MajorAxisEndPoint.X, my = el.MajorAxisEndPoint.Y; + double vx = -my * el.RadiusRatio, vy = mx * el.RadiusRatio; // vedlejší poloosa = kolmice·ratio + double major = Math.Sqrt(mx * mx + my * my); + + int seg = Math.Max(8, ContourGeometry.ArcSegments(sweep, Math.Max(major, 1e-9), 0.02)); + var pts = new List<(double X, double Y)>(seg + 1); + for (int i = 0; i <= seg; i++) + { + double t = sa + sweep * i / seg, c = Math.Cos(t), s = Math.Sin(t); + pts.Add((el.Center.X + c * mx + s * vx, el.Center.Y + c * my + s * vy)); + } + return pts; + } + /// Navzorkuje eliptický oblouk hatch hranice na body v OCS (kolmice·ratio, CW přes osu X). private static List<(double X, double Y)> VzorkujEllipse(Hatch.BoundaryPath.Ellipse el) { diff --git a/Rozpisky/Dxf/RozpiskaRenderer.cs b/Rozpisky/Dxf/RozpiskaRenderer.cs index 32c1f47..ced0a64 100644 --- a/Rozpisky/Dxf/RozpiskaRenderer.cs +++ b/Rozpisky/Dxf/RozpiskaRenderer.cs @@ -81,6 +81,28 @@ public sealed class RozpiskaRenderer catch { return null; } // poškozený/nečitelný DXF fragment → bez loga }); + /// Prvky vložených DXF (log, schémat), které kreslítko neumí – po jednom řádku na + /// soubor, např. „nase_logo.dxf: DIMENSION ×3, LEADER ×1". Prázdné = vykreslí se všechno. + /// Uživatel si přidává vlastní loga; bez tohohle by v nich chybějící prvky prostě zmizely + /// a nikdo by se to nedozvěděl. Nekreslí se nic – kresba jde do , + /// jen aby se sesbírala statistika. + public static IReadOnlyList NepodporovanePrvky(IEnumerable cestyKFragmentum) + { + var hlaseni = new List(); + foreach (var cesta in cestyKFragmentum.Where(c => !string.IsNullOrWhiteSpace(c)).Distinct(StringComparer.OrdinalIgnoreCase)) + { + var frag = NactiFragment(cesta); + if (frag is null) continue; + + frag.Replay(new BoundsRenderer()); + if (frag.NezpracovaneEntity.Count == 0) continue; + + var vypis = string.Join(", ", frag.NezpracovaneEntity.OrderBy(p => p.Key).Select(p => $"{p.Key} ×{p.Value}")); + hlaseni.Add($"{Path.GetFileName(cesta)}: {vypis}"); + } + return hlaseni; + } + public void ExportPdf(Projekt p, Priloha r, IEnumerable revize, Ciselniky c, string path, IReadOnlyDictionary? loga = null) => Exporters.PdfA4(BuildDraw(p, r, revize, c, vyplnene: true, loga), path, frame: _tpl.SheetFrame); diff --git a/Rozpisky/Rendering/DxfProfileRenderer.cs b/Rozpisky/Rendering/DxfProfileRenderer.cs index d70c4c9..83a8aba 100644 --- a/Rozpisky/Rendering/DxfProfileRenderer.cs +++ b/Rozpisky/Rendering/DxfProfileRenderer.cs @@ -37,11 +37,17 @@ public sealed class DxfProfileRenderer : IProfileRenderer } _vrstvy[_aktualniVrstva.Name] = _aktualniVrstva; _aktualniBarva = Color.ByLayer; + _doc.Header.Version = VerzeVystupu; } /// Hotový výkres se všemi rozpiskami (pro DxfWriter.Write). public CadDocument Document => _doc; + /// Verze DXF, ve které se výkres zapisuje – shodná se zdrojovou šablonou + /// (AutoCAD 2013). Nastavuje se natvrdo: kdyby se nechala na výchozí hodnotě ACadSharp, + /// změnil by ji tiše upgrade balíčku a s ní i to, čím půjde výkres otevřít. + public const ACadVersion VerzeVystupu = ACadVersion.AC1027; + public void Stav(in KresliciStav s) { _aktualniVrstva = NajdiVrstvu(s.Hladina); diff --git a/Rozpisky/Rendering/TransformRenderer.cs b/Rozpisky/Rendering/TransformRenderer.cs index e7e6d5a..d39269a 100644 --- a/Rozpisky/Rendering/TransformRenderer.cs +++ b/Rozpisky/Rendering/TransformRenderer.cs @@ -1,63 +1,153 @@ namespace Rozpisky.Rendering; /// -/// Dekorátor , který na všechny souřadnice aplikuje -/// uniformní měřítko + posun: X' = Ox + S·X, Y' = Oy + S·Y. Slouží k umístění -/// externího DXF fragmentu (logo / orientační schéma) do vymezeného rámečku rozpisky – -/// fragment se nakreslí ve svých souřadnicích a tato vrstva ho přemapuje do cílového obdélníku. -/// Výška textu, poloměr a tloušťka čar se škálují ·S; hladina/barva propadá beze změny. +/// Dekorátor , který na souřadnice aplikuje afinní transformaci +/// X' = A·x + C·y + Tx, Y' = B·x + D·y + Ty. +/// Dvě použití: umístění externího DXF fragmentu (logo / orientační schéma) do rámečku +/// rozpisky – tam stačí měřítko a posun () +/// – a rozvinutí vloženého bloku (INSERT), kde přibude natočení a nestejné měřítko os +/// (). +/// Kruhový oblouk zůstane obloukem jen tehdy, je-li transformace podobnost (stejné +/// měřítko obou os a kolmé osy). Při nestejném měřítku by z něj byla elipsa, kterou kreslicí +/// rozhraní nezná – tam se oblouk rozloží na lomenou čáru. /// public sealed class TransformRenderer : IProfileRenderer { private readonly IProfileRenderer _inner; - private readonly double _s, _ox, _oy; + private readonly double _a, _b, _c, _d, _tx, _ty; + /// Měřítko délek (u podobnosti přesné, jinak odmocnina z determinantu). + private readonly double _meritko; + + /// Natočení transformace ve stupních – přičítá se k natočení textů. + private readonly double _uhelDeg; + + /// true = oblouky a kružnice zůstanou oblouky a kružnicemi. Platí pro podobnost bez + /// zrcadlení; při nestejném měřítku os by z kružnice byla elipsa a při zrcadlení by se obrátil + /// smysl oblouku – v obou případech je bezpečnější rozložit tvar na lomenou čáru. + private readonly bool _zachovavaOblouky; + + /// Uniformní měřítko + posun, bez natočení (umístění fragmentu do rámečku). public TransformRenderer(IProfileRenderer inner, double scale, double offsetX, double offsetY) + : this(inner, scale, 0, 0, scale, offsetX, offsetY) { } + + private TransformRenderer(IProfileRenderer inner, double a, double b, double c, double d, double tx, double ty) { _inner = inner; - _s = scale; - _ox = offsetX; - _oy = offsetY; + _a = a; _b = b; _c = c; _d = d; _tx = tx; _ty = ty; + + double delkaX = Math.Sqrt(a * a + b * b), delkaY = Math.Sqrt(c * c + d * d); + double skalar = a * c + b * d; // 0 = osy jsou kolmé + bool podobnost = Math.Abs(delkaX - delkaY) <= 1e-9 * Math.Max(delkaX, 1) + && Math.Abs(skalar) <= 1e-9 * Math.Max(delkaX * delkaY, 1); + + double det = a * d - b * c; + _zachovavaOblouky = podobnost && det > 0; + _meritko = podobnost ? delkaX : Math.Sqrt(Math.Abs(det)); + _uhelDeg = Math.Atan2(b, a) * 180 / Math.PI; } - private double X(double x) => _ox + _s * x; - private double Y(double y) => _oy + _s * y; + /// Transformace vloženého bloku: entity bloku jsou zapsané vůči jeho vztažnému bodu, + /// ten se klade na vkládací bod. Tedy p' = vkládací + R·S·(p − vztažný). Nulové měřítko + /// (setkáno v poškozených souborech) se bere jako 1, jinak by blok zkolaboval do bodu. + public static TransformRenderer ProBlok( + IProfileRenderer inner, double vlozX, double vlozY, double zakladX, double zakladY, + double meritkoX, double meritkoY, double natoceniRad) + { + if (meritkoX == 0) meritkoX = 1; + if (meritkoY == 0) meritkoY = 1; + double cos = Math.Cos(natoceniRad), sin = Math.Sin(natoceniRad); + double a = meritkoX * cos, b = meritkoX * sin, c = -meritkoY * sin, d = meritkoY * cos; + return new TransformRenderer( + inner, a, b, c, d, + vlozX - (a * zakladX + c * zakladY), + vlozY - (b * zakladX + d * zakladY)); + } + + private double X(double x, double y) => _tx + _a * x + _c * y; + private double Y(double x, double y) => _ty + _b * x + _d * y; - /// 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 }, + TloustkaMm = s.TloustkaMm * _meritko, + Hladina = s.Hladina with { TloustkaMm = s.Hladina.TloustkaMm * _meritko }, }); 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, y1), Y(x1, y1), X(x2, y2), Y(x2, y2)); public void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false) - => _inner.Polyline(pts.Select(p => (X(p.X), Y(p.Y))).ToList(), closed); + => _inner.Polyline(pts.Select(p => (X(p.X, p.Y), Y(p.X, p.Y))).ToList(), closed); public void Circle(double cx, double cy, double r) - => _inner.Circle(X(cx), Y(cy), r * _s); + { + if (_zachovavaOblouky) { _inner.Circle(X(cx, cy), Y(cx, cy), r * _meritko); return; } - public void Stroke(Contour c) => _inner.Stroke(new Contour(c.Edges.Select(T).ToList(), c.Closed)); + // Nestejné měřítko os → z kružnice je elipsa; kreslicí rozhraní ji nezná, tedy lomená čára. + int n = Math.Max(16, ContourGeometry.ArcSegments(2 * Math.PI, Math.Max(r * _meritko, 1e-9), 0.02)); + var body = new List<(double X, double Y)>(n); + for (int i = 0; i < n; i++) + { + double t = 2 * Math.PI * i / n; + double x = cx + r * Math.Cos(t), y = cy + r * Math.Sin(t); + body.Add((X(x, y), Y(x, y))); + } + _inner.Polyline(body, closed: true); + } - /// Přemapuje hranu: body scale+offset, poloměr ·S, úhly beze změny (transformace je - /// uniformní scale + posun, bez rotace/zrcadlení). - private Edge T(Edge e) => e.Kind == EdgeKind.Line - ? Edge.Line(X(e.SX), Y(e.SY), X(e.EX), Y(e.EY)) - : Edge.Arc(X(e.SX), Y(e.SY), X(e.EX), Y(e.EY), X(e.Cx), Y(e.Cy), e.R * _s, e.A0, e.Sweep); + public void Stroke(Contour c) + { + if (_zachovavaOblouky) { _inner.Stroke(new Contour(c.Edges.Select(T).ToList(), c.Closed)); return; } + _inner.Polyline(Zplosti(c), c.Closed); + } + + /// Přemapuje hranu (jen pro transformace zachovávající oblouky): body afinně, + /// poloměr ·měřítko, počáteční úhel o natočení, rozsah beze změny. + private Edge T(Edge e) + { + if (e.Kind == EdgeKind.Line) + return Edge.Line(X(e.SX, e.SY), Y(e.SX, e.SY), X(e.EX, e.EY), Y(e.EX, e.EY)); + + return Edge.Arc( + X(e.SX, e.SY), Y(e.SX, e.SY), X(e.EX, e.EY), Y(e.EX, e.EY), + X(e.Cx, e.Cy), Y(e.Cx, e.Cy), e.R * _meritko, e.A0 + _uhelDeg * Math.PI / 180, e.Sweep); + } + + private List<(double X, double Y)> Zplosti(Contour c) + { + var body = new List<(double X, double Y)>(); + foreach (var e in c.Edges) e.AppendPoints(body, 0.02); + if (c.Edges.Count > 0) body.Add((c.Edges[^1].EX, c.Edges[^1].EY)); + return body.Select(p => (X(p.X, p.Y), Y(p.X, p.Y))).ToList(); + } 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) - => _inner.Text(X(x), Y(y), text, heightMm * _s, h, v, rotationDeg, style, widthFactor, maxWidthMm * _s); + => _inner.Text(X(x, y), Y(x, y), text, heightMm * _meritko, h, v, rotationDeg + _uhelDeg, + style, widthFactor, maxWidthMm * _meritko); public void Fill(IReadOnlyList loops, Vypln vypln = default) => _inner.Fill( - loops.Select(c => new Contour(c.Edges.Select(T).ToList(), c.Closed)).ToList(), + 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), Y(u.Y1), X(u.X2), Y(u.Y2))).ToList() }); + : 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(), + }); + + /// Obrys rozložený na úsečky (pro transformace, které oblouk nezachovají). + private List Usecky(Contour c) + { + var body = Zplosti(c); + var hrany = new List(Math.Max(body.Count - 1, 0)); + for (int i = 0; i + 1 < body.Count; i++) + hrany.Add(Edge.Line(body[i].X, body[i].Y, body[i + 1].X, body[i + 1].Y)); + return hrany; + } } diff --git a/Rozpisky/ViewModels/MainViewModel.cs b/Rozpisky/ViewModels/MainViewModel.cs index e020911..f766660 100644 --- a/Rozpisky/ViewModels/MainViewModel.cs +++ b/Rozpisky/ViewModels/MainViewModel.cs @@ -834,6 +834,7 @@ public sealed class MainViewModel : ObservableObject _exportZrusen = false; int hotovo = 0, bezJmena = 0, n = prilohy.Count, index = 0; var chyby = new List(); + var pouziteLoga = new HashSet(StringComparer.OrdinalIgnoreCase); try { foreach (var p in prilohy) @@ -853,7 +854,9 @@ public sealed class MainViewModel : ObservableObject try { - renderer.ExportPdf(Projekt, p, Revize, Ciselniky, cesta, ResolveLoga(p)); + var loga = ResolveLoga(p); + pouziteLoga.UnionWith(loga.Values); + renderer.ExportPdf(Projekt, p, Revize, Ciselniky, cesta, loga); hotovo++; } catch (Exception ex) @@ -868,7 +871,8 @@ public sealed class MainViewModel : ObservableObject StavovaZprava = "Připraveno"; } - ZobrazSouhrnExportu(cilRozpisky, hotovo, bezJmena, chyby); + ZobrazSouhrnExportu(cilRozpisky, hotovo, bezJmena, chyby, + RozpiskaRenderer.NepodporovanePrvky(pouziteLoga)); } /// Mezera mezi rozpiskami v dlaždicovém DXF (výkresové jednotky). @@ -912,6 +916,7 @@ public sealed class MainViewModel : ObservableObject int cols = Math.Min(DxfSloupcu, Math.Max(1, n)); var dxf = new DxfProfileRenderer(); var chyby = new List(); + var pouziteLoga = new HashSet(StringComparer.OrdinalIgnoreCase); try { for (int i = 0; i < n; i++) @@ -925,8 +930,10 @@ public sealed class MainViewModel : ObservableObject double oy = -row * (cell.H + DxfMezera); // další řádky pod sebe (osa Y nahoru) try { + var loga = ResolveLoga(prilohy[i]); + pouziteLoga.UnionWith(loga.Values); var draw = renderer.BuildDraw(Projekt, prilohy[i], Revize, Ciselniky, vyplnene: true, - ResolveLoga(prilohy[i]), ramecekA4: true); + loga, ramecekA4: true); // Generálka naprázdno: výjimka uprostřed kreslení by ve sdíleném dokumentu // nechala torzo rozpisky (entity přidané před pádem nejde vzít zpět). Do // skutečného DXF se tak kreslí až kresba, která celá prošla. @@ -950,6 +957,7 @@ public sealed class MainViewModel : ObservableObject StavovaZprava = "Připraveno"; return; } + // Verzi výstupu drží DxfProfileRenderer (natvrdo, ať ji neurčí default balíčku). DxfWriter.Write(cesta, dxf.Document, binary: true); // binární DXF = výrazně menší soubor } catch (Exception ex) @@ -971,6 +979,7 @@ public sealed class MainViewModel : ObservableObject sb.AppendLine($"Chyby: {chyby.Count}"); foreach (var ch in chyby) sb.AppendLine($" • {ch}"); } + PripojNepodporovane(sb, RozpiskaRenderer.NepodporovanePrvky(pouziteLoga)); sb.AppendLine(); sb.Append($"Soubor: {cesta}"); MessageBox.Show(sb.ToString(), "Export do DXF dokončen", MessageBoxButton.OK, @@ -1200,7 +1209,8 @@ public sealed class MainViewModel : ObservableObject } /// Souhrn dávkového exportu (počet hotových, přeskočených bez jména, chyby) + cílová složka. - private static void ZobrazSouhrnExportu(string cilRozpisky, int hotovo, int bezJmena, IReadOnlyList chyby) + private static void ZobrazSouhrnExportu(string cilRozpisky, int hotovo, int bezJmena, + IReadOnlyList chyby, IReadOnlyList nepodporovane) { var sb = new System.Text.StringBuilder(); sb.AppendLine($"Vyexportováno do PDF: {hotovo}"); @@ -1210,6 +1220,7 @@ public sealed class MainViewModel : ObservableObject sb.AppendLine($"Chyby: {chyby.Count}"); foreach (var ch in chyby) sb.AppendLine($" • {ch}"); } + PripojNepodporovane(sb, nepodporovane); sb.AppendLine(); sb.Append($"Složka: {cilRozpisky}"); @@ -1217,6 +1228,16 @@ public sealed class MainViewModel : ObservableObject chyby.Count > 0 ? MessageBoxImage.Warning : MessageBoxImage.Information); } + /// Přidá do souhrnu upozornění na prvky vložených DXF, které kreslítko neumí. + /// Export se kvůli nim nezastaví – jen se nevykreslí, a o tom se uživatel musí dozvědět. + private static void PripojNepodporovane(System.Text.StringBuilder sb, IReadOnlyList nepodporovane) + { + if (nepodporovane.Count == 0) return; + sb.AppendLine(); + sb.AppendLine("Ve vložených DXF jsou prvky, které program neumí vykreslit:"); + foreach (var radek in nepodporovane) sb.AppendLine($" • {radek}"); + } + /// Vybere DXF orientačního schématu pro vybranou přílohu. Leží-li soubor ve složce /// aktuálního .rzp, uloží se relativně (jen jméno), jinak absolutní cestou. private void VyberSchema()