První commit — WPF aplikace pro generování výkresových rohových razítek (.NET 10, SkiaSharp, ACadSharp, ClosedXML) včetně xUnit testů, DXF šablony, log a dokumentace. Build výstupy (bin/, obj/, .vs/) jsou ignorovány. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
223 lines
12 KiB
Markdown
223 lines
12 KiB
Markdown
# 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: **`net9.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** = `Layer.LineWeight` (enum, hodnota 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`).
|
||
- **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
|
||
│ ├─ 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: měřítko + posun (fit fragmentů/loga)
|
||
├─ Dxf/ ← čtení šablony, mapování hodnot, kódy
|
||
│ ├─ DxfTemplate.cs ← načte DXF šablonu a přehraje ji do IProfileRenderer
|
||
│ ├─ 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
|
||
```
|
||
|
||
Vedle .exe se dodávají editovatelné soubory: `Rozpiska.dxf` (šablona), `SEZNAM.xlsx` (mustr seznamu),
|
||
`ciselniky.json`, `mapovani.json`, `cad.json`, `layout.json`, `loga/` a manuál SŽ (PDF).
|
||
|
||
---
|
||
|
||
## 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 readonly record struct TextStyle(string? FontFamily = null, bool Bold = false,
|
||
bool Italic = false, string? FontFile = null);
|
||
|
||
public interface IProfileRenderer
|
||
{
|
||
void Layer(string name, int rgb, double lineWeightMm = 0.25); // barva 0xRRGGBB, tloušťka ByLayer
|
||
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 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<IReadOnlyList<(double X, double Y)>> loops); // HATCH výplně (even-odd)
|
||
}
|
||
```
|
||
|
||
- `rgb` je barva `0xRRGGBB` rozbalená z DXF (ByLayer i index/true-color). Bílá se kreslí černě
|
||
(bílá na bílém papíře by zmizela).
|
||
- `widthFactor` = horizontální měřítko písma (DXF width factor). `maxWidthMm` = šířka pro zalomení
|
||
víceřádkového textu (0 = jen explicitní zlomy).
|
||
|
||
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`** – pro každou hladinu založí `Layer` (barva + tloušťka), entity přidává do
|
||
`Document`. 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: na souřadnice aplikuje uniformní měřítko + posun. Slouží
|
||
k umístění vkládaného DXF fragmentu (logo / schéma) do vymezeného rámečku.
|
||
|
||
---
|
||
|
||
## 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<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`](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<IProfileRenderer>`:
|
||
|
||
```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_<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`](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.
|
||
- **Configy vedle .exe** (`ciselniky.json`, `cad.json`, `layout.json`, `mapovani.json`) 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).
|
||
- **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í.
|