# Rozpisky – generátor výkresových rohových razítek (C# / .NET) Standalone WPF aplikace (náhrada původního excelového nástroje „Rozpisky 1.0.8"). Spravuje projekt, seznam příloh, revize a číselníky, a z **DXF šablony rozpisky** generuje **náhled**, **PDF na A4**, **hromadný DXF** (pole jako `TEXT`/`MTEXT` entity), **seznam příloh do XLSX** a přes připojený CAD i **tisk rozvržení**. Celé kreslení stojí na jedné myšlence: existuje jedno **kreslicí rozhraní** ([`IProfileRenderer`](../Rozpisky/Rendering/IProfileRenderer.cs) – „kam se kreslí") a několik jeho implementací. Tatáž kresba teče na obrazovku (náhled), do PNG, do PDF i do DXF. Čtečka šablony ([`DxfTemplate`](../Rozpisky/Dxf/DxfTemplate.cs)) jen čte DXF a „přehrává" ho do tohoto rozhraní. --- ## 1. NuGet balíčky Cílový framework: **`net10.0-windows`**, `UseWPF=true`, platforma **AnyCPU**. Připojení na běžící CAD a MS Excel jde přes COM (pozdní vazba přes `dynamic`) – bitness běhového procesu se řídí hostitelem, zvláštní `PlatformTarget` se nenastavuje. | Balíček | Verze | K čemu | Klíčové API | |---|---|---|---| | **SkiaSharp** | `2.88.8` | 2D rasterizace + **PDF**. Kreslení geometrie/textu/obrázků; export PNG i PDF. | `SKCanvas`, `SKPaint`, `SKPath`, `SKBitmap`, `SKImage`, `SKTypeface`, `SKDocument.CreatePdf`, `SKFileWStream` | | **SkiaSharp.Views.WPF** | `2.88.8` | WPF plátno pro živý náhled (zoom/posun). **Jen pro GUI** – headless export ho nepotřebuje. | `SKElement`, `OnPaintSurface(SKPaintSurfaceEventArgs)` | | **ACadSharp** | `3.6.29` | Čtení i **zápis** DXF (novější verze, výplně HATCH, XRecord metadata atributů). Nahradilo dřívější netDxf. | `DxfReader.Read`, `DxfWriter.Write`, `CadDocument`, `ACadSharp.Entities.*`, `ACadSharp.Tables.TextStyle` | | **ClosedXML** | `0.105.0` | Vyplnění mustru „Seznam příloh" (`SEZNAM.xlsx`) při exportu seznamu. | `XLWorkbook`, `IXLWorksheet`, `Cell(...).Value`, `SaveAs` | > **Pozn. k ACadSharp:** na rozdíl od netDxf vystaví font ze stylu i box-šířku víceřádkových atributů > (XRecord, group 41), takže jde dlouhý text v poli správně zalomit. DXF se zapisuje **binárně** > (`DxfWriter.Write(..., binary: true)`) – výrazně menší soubor. Převod XLSX → PDF (tisk seznamu) jde > přes **nainstalovaný MS Excel** (COM), ne přes ACadSharp/ClosedXML. ### Zápis DXF – co je potřeba vědět - **Tloušťka čáry** je enum v 1/100 mm (standardní hodnoty `0,5,9,13,15,18,20,25,30,35,40,50,53,60,70,80,90,100,106,120,140,158,200,211`); zapisuje se na hladinu (`Layer.LineWeight`) i na entitu, která ji přebíjí. Milimetry z kreslicího rozhraní se přichytí k nejbližší standardní hodnotě. - **Barva** se zapisuje ve stejném druhu, v jakém přišla ze zdroje: indexovaná číslem (group 62), přímá jako RGB (group 420), `ByLayer`/`ByBlock` odkazem. Zplošťování na RGB by výkres v CADu odřízlo od tabulky barev. - **Diakritika** se do DXF ukládá jako Unicode escape `\U+XXXX` (např. `š` → `\U+0161`) – AutoCAD i ostatní čtečky to čtou správně. - Pole, která byla v šabloně `ATTDEF`, se při vyplnění zapisují jako obyčejné `TEXT`/`MTEXT` entity (žádné atributy bloku). - Objekty v netisknutelných hladinách (`PlotFlag=0`) se **nevykreslují**, ale ví se o nich kvůli místům pro loga (viz [`DxfTemplate.PlaceholderHladiny`](../Rozpisky/Dxf/DxfTemplate.cs)). --- ## 2. Struktura kódu Aplikace je členěná dle vrstev (MVVM + služby): ``` Rozpisky/ ├─ Rendering/ ← kreslicí vrstva (nezávislá na cíli) │ ├─ IProfileRenderer.cs ← kreslicí rozhraní + HAlign/VAlign + TextStyle + Vypln │ ├─ SkiaProfileRenderer.cs ← výstup přes SkiaSharp (obrazovka / PNG / PDF) │ ├─ DxfProfileRenderer.cs ← výstup do DXF (ACadSharp) │ ├─ BoundsRenderer.cs ← spočítá bbox kresby (fit / umístění na A4) │ ├─ TransformRenderer.cs ← dekorátor: afinní transformace (fit loga, rozvinutí bloku) │ ├─ KresliciStav.cs ← barva s původem (ACI/RGB/ByLayer), hladina, typ čáry, tloušťka │ └─ AciPaleta.cs ← indexované barvy AutoCADu (ACI 0–255) → RGB ├─ Dxf/ ← čtení šablony, mapování hodnot, kódy │ ├─ DxfTemplate.cs ← načte DXF šablonu a přehraje ji do IProfileRenderer │ ├─ SrafaVzor.cs ← rozvine vzor šrafy (ANSI31 apod.) na čáry │ ├─ RozpiskaRenderer.cs ← spojuje šablonu + hodnoty + exportéry (fasáda pro UI) │ ├─ RozpiskaValues.cs ← „tag DXF → hodnota" z projektu/přílohy/číselníků (reflexí dle mapovani.json) │ ├─ MapovaniStore.cs ← načtení mapování z mapovani.json (config vedle .exe) │ ├─ KodPrilohy.cs ← 47-poziční strojový kód přílohy (manuál SŽ kap. 3) │ ├─ Exporters.cs ← PngA4 / PdfA4 / RenderView (kreslí libovolný Action) │ └─ LogaResolver.cs ← dohledání DXF log/schémat k příloze ├─ Cad/ ← připojení na běžící CAD (COM) + aktualizace/tisk rozvržení ├─ Xlsx/ ← export seznamu příloh (ClosedXML) + XLSX→PDF (Excel COM) ├─ Data/ ← persistence: .rzp projekt, číselníky, atomický zápis configů ├─ Models/ ← datový model (Projekt, Priloha, Revize, Ciselniky, Nastaveni…) ├─ ViewModels/ ← MainViewModel (stav, akce, dirty-flag, export) ├─ Views/ + Themes/ + Theming/ + Behaviors/ ← WPF UI (dark theme, grid chování) ├─ App.xaml(.cs) ← vstupní bod + globální záchytná síť výjimek (error.log) └─ MainWindow.xaml(.cs) ← hlavní okno ``` Datové soubory jsou rozdělené podle toho, kdo je vlastní: | Kde | Co | Kdo zapisuje | |---|---|---| | vedle `.exe` (zdroj `Rozpisky/Podklady/`) | `Rozpiska.dxf`, `SEZNAM.xlsx`, `ciselniky.vychozi.json`, `mapovani.json`, `loga/`, manuál SŽ (PDF) | nikdo – dodává se s programem, každá instalace přepíše | | `%APPDATA%\Rozpisky` ([`UzivatelskaData`](../Rozpisky/Data/UzivatelskaData.cs)) | `ciselniky.user.json`, `cad.json`, `layout.json`, `error.log`, `tisk_diag.log`, `loga/` (vlastní loga uživatele) | aplikace a uživatel | | kdekoli u uživatele | projekty `.rzp` | aplikace na vyžádání | Rozdělení existuje proto, aby přeinstalace novou verzí (nebo `.exe` v `Program Files`, kam se nesmí psát) nesmazala nic, co si uživatel nastřádal. Portable režim se zapíná souborem `portable.txt` vedle `.exe` – pak se všechno vrátí do složky aplikace. --- ## 3. Kreslicí rozhraní Vše se kreslí ve **výkresových milimetrech**, osa **Y nahoru** (jako v CAD/DXF). ```csharp public enum HAlign { Left, Center, Right } public enum VAlign { Baseline, Bottom, Middle, Top } public enum BarvaDruh { ByLayer, ByBlock, Index, True } public readonly record struct Barva(BarvaDruh Druh, byte Index, int Rgb); public readonly record struct HladinaStav(string Nazev, Barva Barva, string TypCary = "Continuous", double TloustkaMm = 0, bool Tisknout = true, bool Zapnuta = true); public readonly record struct KresliciStav(HladinaStav Hladina, Barva Barva, string? TypCary = null, double? TloustkaMm = null); public readonly record struct TextStyle(string? FontFamily = null, bool Bold = false, bool Italic = false, string? FontFile = null, double ObliqueDeg = 0, string? Nazev = null, bool JeShx = false); public readonly record struct Usecka(double X1, double Y1, double X2, double Y2); public readonly record struct Vypln(string? Vzor = null, double Meritko = 1, double UhelDeg = 0, IReadOnlyList? Cary = null); public interface IProfileRenderer { 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); void Stroke(Contour c); // obrys se zachovanými oblouky 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); void Fill(IReadOnlyList loops, Vypln vypln = default); // HATCH výplně (even-odd) } ``` **Proč `KresliciStav` a ne `int rgb`.** Šablona i všech 9 log jsou zapsané výhradně **indexovanými barvami AutoCADu** (group 62). Kdyby rozhraní neslo jen výsledné RGB, export by z nich udělal natvrdo true color (group 420), výkres by v CADu přestal reagovat na tabulku barev a `ByLayer` by se rozpadlo na kopie barvy hladiny. Barva proto nese **původ**, ne jen hodnotu: rastrové renderery si vyžádají hotové RGB přes `RgbProKresbu()`, DXF export zapíše původní druh. Ze stejného důvodu se přenáší celý popis hladiny – ve výstupu se staví z tabulky hladin, ne z první entity, která na ní přistála. - **Paleta ACI** ([`AciPaleta`](../Rozpisky/Rendering/AciPaleta.cs)) je vlastní, ne z ACadSharp: ta se od AutoCADu liší u 33 indexů, mimo jiné u 8 (v šabloně použitý) a u posledního indexu většiny desítek odstínů, kde vrací barvu ze sousední skupiny (29 a 39 mají dokonce tutéž). Rozsah 10–249 se dopočítává, ne vypisuje: 24 odstínů × 5 jasů × 2 sytosti. - `TextStyle.Nazev` = jméno stylu ve zdroji („Popis základní"). Nese se dál, aby ho export zapsal beze změny – jeden zdrojový styl = jeden styl ve výstupu. - `widthFactor` = horizontální měřítko písma (DXF width factor). `maxWidthMm` = šířka pro zalomení víceřádkového textu (0 = jen explicitní zlomy). - **Bílá se kreslí černě** (bílá na bílém papíře by zmizela) – jen v rastrových rendererech; do DXF jde barva beze změny. Implementace: - **`SkiaProfileRenderer`** – kreslí na `SKCanvas`. Dostane funkci `Map(mmX, mmY) → bod plátna` a `pxPerMm`. Text vzpřímeně (rotace opačně, plátno má Y dolů), fonty ze stylu/souboru s fallbackem. Je `IDisposable` (uvolní typefaces) – používat přes `using`. - **`DxfProfileRenderer`** – staví ACadSharp entity do `Document`; hladiny, textové styly i barvy podle zdroje. Verze výstupu je zafixovaná (`VerzeVystupu`), ať ji neurčí default balíčku. Výsledek zapíše `DxfWriter.Write(path, renderer.Document, binary: true)`. - **`BoundsRenderer`** – nic nekreslí, jen sčítá min/max souřadnic (fit / vystředění na A4). - **`TransformRenderer`** – dekorátor s afinní transformací: měřítko + posun (umístění loga do rámečku), nebo měřítko os + natočení + posun (rozvinutí bloku, `ProBlok`). Oblouk zůstane obloukem jen u podobnosti bez zrcadlení – jinak by z kružnice byla elipsa, kterou rozhraní nezná, a tvar se rozloží na lomenou čáru. ### Které DXF entity se čtou | Entita | Jak se kreslí | |---|---| | `LINE`, `CIRCLE`, `ARC` | přímo; oblouk analyticky, hladký v každém přiblížení | | `LWPOLYLINE`, `POLYLINE` (2D) | lomená čára, s `bulge` jako obrys s kruhovými oblouky | | `POLYLINE` (3D) | promítnuto do roviny XY | | `SPLINE` | lomená čára, dílek ≈ 0,3 mm podle délky řídicího polygonu | | `ELLIPSE` | lomená čára (rozhraní zná jen kruhové oblouky) | | `TEXT`, `MTEXT`, `ATTDEF` | text se stylem, zarovnáním, sklonem a šířkovým faktorem | | `HATCH` | plná výplň, nebo vzor rozvinutý na čáry ([`SrafaVzor`](../Rozpisky/Dxf/SrafaVzor.cs)) | | `SOLID`, `3DFACE` | vyplněný čtyřúhelník (v DXF jsou 3. a 4. roh prohozené) | | `POINT` | tečka | | `INSERT` | rekurzivně rozvinutý blok: vztažný bod, měřítko os, natočení, `ByBlock` barvy | | ostatní | nekreslí se, ale **spočítá se** do `DxfTemplate.NezpracovaneEntity`; souhrn exportu to vypíše | ### Známá omezení - **Vnitřní formátovací kódy MTEXTu** (`\H` výška, `\C` barva, `\f` font, `\S` zlomek) se ztrácejí – čte se `MText.PlainText`, který je odstraňuje. Zvlášť se čte jen `\W` (šířkový faktor), protože ho ACadSharp v `PlainText` nechává a jinak by se vykreslil jako text. - **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í 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. --- ## 4. Čtečka šablony a fasáda pro UI Nízkoúrovňová čtečka [`DxfTemplate`](../Rozpisky/Dxf/DxfTemplate.cs): ```csharp var tpl = new DxfTemplate("Rozpiska.dxf"); tpl.Replay(g); // NÁHLED: pole se vykreslí jako jejich názvy (tagy) tpl.Replay(g, values); // VYPLNĚNÍ: pole = hodnota; nevyplněné = prázdné tpl.Replay(g, values, includeNoPrint); // + volitelně A4 rámeček !!NOPRINT (pro DXF export/tisk) ``` `Replay` přehraje statickou geometrii (`Line`/`Polyline`), popisky (`Text`/`MText`) a pole (`ATTDEF` z model space) do rozhraní. `SheetFrame` vrací A4 rámeček šablony (referenční obdélník pro vystředění), `Placeholders` vrací rámečky pro loga/schéma podle vyhrazených hladin, `Bounds` bbox kresby (pro fit vkládaného fragmentu). Fasáda pro UI [`RozpiskaRenderer`](../Rozpisky/Dxf/RozpiskaRenderer.cs) spojuje šablonu, mapování hodnot a exportéry na jedno místo (šablona se cachuje): ```csharp var r = new RozpiskaRenderer(Nastaveni.SablonaDxf); Action draw = r.BuildDraw(projekt, priloha, revize, ciselniky, vyplnene: true, loga, ramecekA4: false); r.ExportPdf(projekt, priloha, revize, ciselniky, "rozpiska.pdf", loga); // A4 PDF vč. log r.ExportPng(projekt, priloha, revize, ciselniky, "nahled.png", loga); // A4 PNG ``` Hodnoty polí sestavuje [`RozpiskaValues`](../Rozpisky/Dxf/RozpiskaValues.cs): mapování „pole programu → DXF atribut" **není v kódu**, čte se z `mapovani.json` a hodnoty se dotahují reflexí. K tomu se algoritmicky doplní 47-poziční strojový kód (`K1`–`K47`, viz [`KodPrilohy`](../Rozpisky/Dxf/KodPrilohy.cs)). --- ## 5. Export na A4 (`Exporters`) [`Exporters`](../Rozpisky/Dxf/Exporters.cs) vykreslí **libovolný** `Action`: ```csharp Exporters.PngA4(draw, "rozpiska.png", dpi: 200, frame: sheetFrame); // A4 raster Exporters.PdfA4(draw, "rozpiska.pdf", frame: sheetFrame); // A4 vektor (PDF, 1:1) Exporters.RenderView(canvas, wpx, hpx, draw, frame, zoom, panX, panY); // živý náhled na SKElement ``` Princip A4: referenční obdélník (`frame` = A4 rámeček šablony, jinak `BoundsRenderer`) se mapuje **1:1** a vystředí na papír; zmenší se jen tehdy, když se do tisknutelné plochy nevejde. PDF se kreslí přes `SKDocument.CreatePdf` (1 mm = 72/25,4 bodu; text vektorový, tloušťky v reálných mm). --- ## 6. Demo data (`DemoData`) [`DemoData`](../Rozpisky/Data/DemoData.cs) naplní úvodní stav při startu: `SeedProjekt(vm)` (ukázkový projekt + přílohy), `SeedCiselniky(c)` a `SeedKraje(c)` (výchozí číselníky, pokud `ciselniky.json` chybí). --- ## 7. Toky exportu (přehled) | Akce | Vstup | Výstup | Přes | |---|---|---|---| | Náhled rozpisky | příloha | okno se SKElement | `RozpiskaRenderer.BuildDraw` → `Exporters.RenderView` | | Export do PDF (výběr/vše) | přílohy | `Rozpisky\*.pdf` | `RozpiskaRenderer.ExportPdf` (per-item, chyby se sbírají) | | Export do DXF (hromadně) | přílohy | `__ Otevřená\_rozpisky.dxf` | dlaždicová mřížka do `DxfProfileRenderer` → `DxfWriter.Write` | | Export seznamu | objekt | `SEZNAM_.xlsx` vedle .rzp | `SeznamExporter` (ClosedXML mustr) | | Tisk seznamu | dřív exportovaný XLSX | `Přílohy\*.pdf` | `ExcelToPdf` (MS Excel COM) | | Aktualizace/tisk rozvržení | přílohy | přejmenování/tisk v CADu | `Cad/CadLayoutService`, `Cad/CadTiskService` (COM) | Aplikace **nemá CLI režim** – vše jede z GUI. Neočekávané výjimky zachytává globální síť v [`App.xaml.cs`](../Rozpisky/App.xaml.cs) a zapisuje je do `error.log` vedle .exe. --- ## 8. Persistence a robustnost - **Projekt `.rzp`** ([`ProjektStore`](../Rozpisky/Data/ProjektStore.cs)) – JSON s exkluzivním zámkem drženým po dobu otevření (ostatní procesy smí jen číst). Zápis serializuje **před** useknutím souboru, aby pád nezanechal prázdný soubor. - **JSON configy** (`ciselniky.user.json`, `cad.json`, `layout.json` v uživatelské složce, `mapovani.json` vedle .exe) jdou přes [`JsonConfigFile`](../Rozpisky/Data/JsonConfigFile.cs): **atomický zápis** (přes `.tmp`) a při poškozeném souboru **záloha do `.bak` + upozornění** (místo tichého přepsání výchozími daty). - **Číselníky ve dvou vrstvách** ([`CiselnikyStore`](../Rozpisky/Data/CiselnikyStore.cs)) – dodávaná `ciselniky.vychozi.json` (jen ke čtení) a uživatelská `ciselniky.user.json`, kde je uložený jen **rozdíl** proti ní (přidané/upravené položky + klíče skrytých). Nová verze programu tak smí dodávaný číselník měnit, aniž by uživatel přišel o svoje záznamy. - **Migrace starých poloh** ([`Migrace`](../Rozpisky/Data/Migrace.cs)) – data zapisovaná dřív vedle .exe se při startu přenesou do uživatelské složky; originál se jen přejmenuje na `.migrovano`. - **Loga ve dvou složkách** ([`LogaResolver`](../Rozpisky/Dxf/LogaResolver.cs)) – hledá se nejdřív v uživatelské `loga\`, pak v dodávané; nabídka v UI je sjednocením obou. - **Zamčený cíl exportu** – před zápisem PDF/XLSX/DXF se ověří, zda soubor nedrží jiný program (Excel, prohlížeč PDF), a nabídne Pokračovat/Přerušit. --- ## 9. Závislost na šabloně Šablona je libovolný DXF, kde: - rám a mřížku tvoří `LINE` / `LWPOLYLINE`, - pevné popisky jsou `TEXT` / `MTEXT`, - **vyplňovaná pole jsou `ATTDEF`** v model space (jejich `Tag` = klíč do hodnot), - placeholdery pro loga jsou uzavřené polylinie na vyhrazených netisknutelných hladinách (`Rozpiska_logo1`…, `Rozpiska_Orientační schéma`) – logo/schéma se fitne do jejich ohraničení, samotný placeholder se nekreslí (`PlotFlag=0`), - A4 hranici papíru vymezuje rámeček na hladině `!!NOPRINT`. Tím je celý vzhled rozpisky daný **datovým souborem a mapováním** (`Rozpiska.dxf` + `mapovani.json`), ne kódem – pro jinou rozpisku stačí jiný DXF a mapování.