Nahlásil uživatel: šrafa v exportovaném DXF nedrží měřítko, takže na první pohled ani nevypadá jako šrafa, a v každém výkresu se chová jinak. Dvě příčiny: 1. Do výstupu šlo jen *jméno* vzoru (ANSI31), bez definičních čar — ověřeno na exportovaném souboru: definicnichCar=0. DXF přitom nese definici vzoru uvnitř entity HATCH; bez ní si ji cílový CAD dosadí ze svých .pat, ve svých jednotkách a měřítku. Odtud „v každém výkresu jinak". Vypln proto nese VzorCara (úhel, vztažný bod, rozteč, čárkování) a export ji zapisuje. 2. Vzor se neškáloval spolu s geometrií. Logo se do rámečku rozpisky vejde jen zmenšené (typicky ~0,1×), ale rozteč vzoru zůstávala původní — v CADu z toho byla buď plná plocha, nebo pár čar přes celé logo. TransformRenderer teď mapuje i definici: vztažný bod celou transformací, rozteč jako vektor (bez posunu), délky čárek a měřítko × měřítko transformace. Měřítko a úhel se do HATCH zapisují neutrální (1 a 0) a efektivní hodnoty nese definice. ACadSharp totiž při zápisu násobí rozteč hodnotou PatternScale, ale při čtení zpátky nedělí — s nenulovým měřítkem by se vzor s každým kolečkem desetkrát roztáhl (změřeno). Po opravě je rozteč v exportu shodná se zdrojem 1:1 a při zmenšení loga 0,1× klesne desetkrát, takže počet čar přes logo zůstává stejný. Náhledy všech 9 log jsou pixel po pixelu beze změny. 119 testů zelených. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
19 KiB
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 – „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) 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/ByBlockodkazem. 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/MTEXTentity (žá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 (vizDxfTemplate.PlaceholderHladiny).
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<IProfileRenderer>)
│ └─ 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) |
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).
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<Usecka>? 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<Contour> 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) 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í naSKCanvas. Dostane funkciMap(mmX, mmY) → bod plátnaapxPerMm. Text vzpřímeně (rotace opačně, plátno má Y dolů), fonty ze stylu/souboru s fallbackem. JeIDisposable(uvolní typefaces) – používat přesusing.DxfProfileRenderer– staví ACadSharp entity doDocument; hladiny, textové styly i barvy podle zdroje. Verze výstupu je zafixovaná (VerzeVystupu), ať ji neurčí default balíčku. Výsledek zapíšeDxfWriter.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) |
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 (
\Hvýška,\Cbarva,\ffont,\Szlomek) se ztrácejí – čte seMText.PlainText, který je odstraňuje. Zvlášť se čte jen\W(šířkový faktor), protože ho ACadSharp vPlainTextnechává a jinak by se vykreslil jako text. - Zarovnání textu
Aligned/Fitse aproximuje naLeft– 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é.pata 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 hodnotouPatternScale, 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:
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 spojuje šablonu, mapování hodnot
a exportéry na jedno místo (šablona se cachuje):
var r = new RozpiskaRenderer(Nastaveni.SablonaDxf);
Action<IProfileRenderer> 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: 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).
5. Export na A4 (Exporters)
Exporters vykreslí libovolný Action<IProfileRenderer>:
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 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_<objekt>.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 a zapisuje je do error.log vedle .exe.
8. Persistence a robustnost
- Projekt
.rzp(ProjektStore) – 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.jsonv uživatelské složce,mapovani.jsonvedle .exe) jdou přesJsonConfigFile: 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) – 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) – 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) – 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
ATTDEFv model space (jejichTag= 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í.