Baseline: současný stav projektu Rozpisky

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>
This commit is contained in:
2026-09-07 09:45:49 +02:00
co-authored by Claude Opus 5
commit a6377ab583
107 changed files with 349485 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
{
"permissions": {
"allow": [
"Bash(cd \"D:/Přesunuté/300_Programování/00_NET/00_Csharp/01_ExeApps/Rozpisky\" && echo \"=== v projektu ===\" && find . -type f \\\\\\( -iname \"*.xls\" -o -iname \"*.xlsx\" -o -iname \"*.xlsm\" -o -iname \"*.csv\" \\\\\\) -not -path '*/.vs/*' 2>/dev/null; echo \"=== o úroveň výš \\(01_ExeApps\\) ===\" && find .. -maxdepth 2 -type f \\\\\\( -iname \"*.xls\" -o -iname \"*.xlsx\" -o -iname \"*.xlsm\" \\\\\\) 2>/dev/null | head -40)",
"Bash(python --version)",
"Bash(py --version)",
"Bash(python -c \"import openpyxl; print\\('openpyxl ok'\\)\")",
"Bash(python -)",
"Bash(PYTHONIOENCODING=utf-8 PYTHONUTF8=1 python -)",
"Bash(grep -nE '^#{1,4} ' SZ_SM011_P10_Manual_v6.md)",
"Bash(grep -nE '^#{2,3} ' ROZHRANI.md)",
"Bash(pip install *)",
"Bash(python -c ' *)",
"Bash(dotnet build *)",
"Bash(dotnet run *)",
"Bash(dotnet build-server *)",
"Bash(rm -rf __verify)",
"PowerShell(Get-Process Rozpisky -ErrorAction SilentlyContinue)",
"Bash(start \"\" \"bin/Debug/net9.0-windows/Rozpisky.exe\")",
"Bash(python -c \"import openpyxl; wb=openpyxl.load_workbook\\('Rozpisky 1.0.8.xlsx', data_only=True\\); print\\([ws.title for ws in wb.worksheets]\\)\")",
"PowerShell(dotnet build \"C:\\\\Users\\\\marek\\\\_Osobn\\\\C sharp\\\\Rozpisky\\\\Rozpisky\\\\Rozpisky.csproj\" -nologo -v q 2>&1)",
"Bash(awk 'prev==\" 2\"{print $0} {prev=$0}' Rozpiska.dxf)",
"Bash(awk ' *)",
"PowerShell(dotnet build \"C:\\\\Users\\\\marek\\\\_Osobn\\\\C sharp\\\\Rozpisky\\\\Rozpisky\\\\Rozpisky.csproj\" -nologo 2>&1)",
"Bash(grep -nE \"[SP]K?[0-9X]{2,}.*_.*X|_X{2,}_|kód|Kód\" SZ_SM011_P10_Manual_v6.md)",
"PowerShell(dotnet run --project \"C:\\\\Users\\\\marek\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\C--Users-marek--Osobn--C-sharp-Rozpisky\\\\ae89cc85-57aa-4d51-a1bc-065fe7454d9d\\\\scratchpad\\\\kodtest\\\\kodtest.csproj\" -v q --nologo 2>&1)",
"mcp__visualize__read_me",
"mcp__visualize__show_widget",
"PowerShell(dotnet run --project \"C:\\\\Users\\\\marek\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\C--Users-marek--Osobn--C-sharp-Rozpisky\\\\ae89cc85-57aa-4d51-a1bc-065fe7454d9d\\\\scratchpad\\\\icongen\\\\icongen.csproj\" -v q --nologo 2>&1)",
"Bash(mkdir -p \"/c/Users/marek/AppData/Local/Temp/claude/C--Users-marek--Osobn--C-sharp-Rozpisky/ae89cc85-57aa-4d51-a1bc-065fe7454d9d/scratchpad/fragtest\" *)",
"PowerShell(dotnet run --project \"C:\\\\Users\\\\marek\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\C--Users-marek--Osobn--C-sharp-Rozpisky\\\\ae89cc85-57aa-4d51-a1bc-065fe7454d9d\\\\scratchpad\\\\fragtest\\\\fragtest.csproj\" -c Debug 2>&1)",
"Bash(cp \"Loga/logo1.dxf\" \"Rozpisky/loga/eu_op_doprava.dxf\")",
"Bash(cp \"Loga/logo2.dxf\" \"Rozpisky/loga/md_sfdi.dxf\")",
"Bash(cp \"Loga/logo3.dxf\" \"Rozpisky/loga/sprava_zeleznic.dxf\")",
"Bash(cp \"Loga/logo4.dxf\" \"Rozpisky/loga/exprojekt.dxf\")",
"Bash(rm -rf \"Loga\")",
"PowerShell(dotnet build \"D:\\\\Pesunut\\\\300_Programovn\\\\00_NET\\\\00_Csharp\\\\01_ExeApps\\\\Rozpisky\\\\Rozpisky\\\\Rozpisky.csproj\" -nologo -clp:ErrorsOnly)",
"Bash(xargs wc -l)",
"Bash(find \"/mnt/d/Přesunuté/300_Programování/00_NET/00_Csharp/01_ExeApps/Rozpisky\" -name \"*.cs\" -type f | grep -i cad)",
"Bash(Get-ChildItem -Path \"D:\\\\Přesunuté\\\\300_Programování\\\\00_NET\\\\00_Csharp\\\\01_ExeApps\\\\Rozpisky\" -Recurse -Directory)",
"Bash(Select-Object -ExpandProperty FullName)",
"Bash(Get-ChildItem -Path \"D:\\\\Přesunuté\\\\300_Programování\\\\00_NET\\\\00_Csharp\\\\01_ExeApps\\\\Rozpisky\" -Recurse -File -Filter \"*.csproj\")",
"Bash(Select-Object FullName)",
"Bash(find \"D:\\\\\\\\Přesunuté\\\\\\\\300_Programování\\\\\\\\00_NET\\\\\\\\00_Csharp\\\\\\\\01_ExeApps\\\\\\\\Rozpisky\" -type f -name \"*.csproj\")",
"Bash(find \"D:\\\\\\\\Přesunuté\\\\\\\\300_Programování\\\\\\\\00_NET\\\\\\\\00_Csharp\\\\\\\\01_ExeApps\\\\\\\\Rozpisky\" -type f \\\\\\( -name \"*.xaml\" -o -name \"*.xaml.cs\" -o -name \"*.cs\" \\\\\\))",
"Bash(find \"D:\\\\\\\\Přesunuté\\\\\\\\300_Programování\\\\\\\\00_NET\\\\\\\\00_Csharp\\\\\\\\01_ExeApps\\\\\\\\Rozpisky\\\\\\\\Rozpisky\" -type f -name \"*.cs\" ! -path \"*/obj/*\")",
"Bash(find \"D:\\\\\\\\Přesunuté\\\\\\\\300_Programování\\\\\\\\00_NET\\\\\\\\00_Csharp\\\\\\\\01_ExeApps\\\\\\\\Rozpisky\\\\\\\\Rozpisky\" -name \"NahledWindow.xaml.cs\")",
"Bash(find \"D:\\\\\\\\Přesunuté\\\\\\\\300_Programování\\\\\\\\00_NET\\\\\\\\00_Csharp\\\\\\\\01_ExeApps\\\\\\\\Rozpisky\\\\\\\\Rozpisky\\\\\\\\Views\" -name \"*.xaml.cs\")",
"Bash(grep -v \"^.*\\\\\\\\.g\\\\\\\\.cs$\")",
"PowerShell(dotnet build \"D:\\\\Pesunut\\\\300_Programovn\\\\00_NET\\\\00_Csharp\\\\01_ExeApps\\\\Rozpisky\" --nologo -v q)",
"PowerShell(Start-Sleep -Seconds 4; Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing; $b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds; $bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height; $g = [System.Drawing.Graphics]::FromImage\\($bmp\\); $g.CopyFromScreen\\($b.Location, [System.Drawing.Point]::Empty, $b.Size\\); $path = \"C:\\\\Users\\\\jomar\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--P-esunut--300-Programov-n--00-NET-00-Csharp-01-ExeApps-Rozpisky\\\\558d2d58-c486-4dcd-a406-0270aa5c30ac\\\\scratchpad\\\\rozpisky-main.png\"; $bmp.Save\\($path, [System.Drawing.Imaging.ImageFormat]::Png\\); $g.Dispose\\(\\); $bmp.Dispose\\(\\); $path)",
"PowerShell(Stop-Process -Name Rozpisky -Force -ErrorAction SilentlyContinue)",
"Bash(cp /d/Přesunuté/300_Programování/00_NET/XX_Podklady/WPF-DarkTheme-Kit/Themes/Colors.Dark.xaml /d/Přesunuté/300_Programování/00_NET/00_Csharp/01_ExeApps/Rozpisky/Rozpisky/Themes/Colors.Dark.xaml)",
"Bash(cp /d/Přesunuté/300_Programování/00_NET/XX_Podklady/WPF-DarkTheme-Kit/Themes/Metrics.xaml /d/Přesunuté/300_Programování/00_NET/00_Csharp/01_ExeApps/Rozpisky/Rozpisky/Themes/Metrics.xaml)",
"Bash(sed 's/AppTheme\\\\.Theming/Rozpisky.Theming/' /d/Přesunuté/300_Programování/00_NET/XX_Podklady/WPF-DarkTheme-Kit/Themes/Controls.xaml)",
"Bash(sed 's/AppTheme\\\\.Theming/Rozpisky.Theming/' /d/Přesunuté/300_Programování/00_NET/XX_Podklady/WPF-DarkTheme-Kit/Theming/Icon.cs)",
"Bash(sed 's/AppTheme\\\\.Theming/Rozpisky.Theming/' /d/Přesunuté/300_Programování/00_NET/XX_Podklady/WPF-DarkTheme-Kit/Theming/GridTheme.cs)",
"Bash(rm \"/d/Přesunuté/300_Programování/00_NET/00_Csharp/01_ExeApps/Rozpisky/Rozpisky/Behaviors/FillHandle.cs\")",
"PowerShell($exe = Get-ChildItem \"D:\\\\Přesunuté\\\\300_Programování\\\\00_NET\\\\00_Csharp\\\\01_ExeApps\\\\Rozpisky\\\\Rozpisky\\\\bin\\\\Debug\\\\net9.0-windows\" -Filter Rozpisky.exe | Select-Object -First 1; $p = Start-Process $exe.FullName -PassThru; Start-Sleep -Seconds 8; if \\($p.HasExited\\) { \"APP EXITED, code: $\\($p.ExitCode\\)\" } else { \"APP RUNNING \\(MainWindow title: $\\($p.MainWindowTitle\\)\\)\"; Stop-Process -Id $p.Id -Force -Confirm:$false })",
"PowerShell($exe = Get-ChildItem \"D:\\\\Přesunuté\\\\300_Programování\\\\00_NET\\\\00_Csharp\\\\01_ExeApps\\\\Rozpisky\\\\Rozpisky\\\\bin\\\\Debug\\\\net9.0-windows\" -Filter Rozpisky.exe | Select-Object -First 1; $p = Start-Process $exe.FullName -PassThru; Start-Sleep -Seconds 8; if \\($p.HasExited\\) { \"APP EXITED, code: $\\($p.ExitCode\\)\" } else { \"APP RUNNING \\(title: $\\($p.MainWindowTitle\\)\\)\"; Stop-Process -Id $p.Id -Force -Confirm:$false })",
"PowerShell($src = \"D:\\\\Přesunuté\\\\300_Programování\\\\00_NET\\\\XX_Podklady\\\\WPF-DarkTheme-Kit\"; $zip = \"D:\\\\Přesunuté\\\\300_Programování\\\\00_NET\\\\XX_Podklady\\\\WPF-DarkTheme-Kit.zip\"; if \\(Test-Path $zip\\) { Remove-Item $zip -Force }; Compress-Archive -Path $src -DestinationPath $zip -Force; $item = Get-Item $zip; \"ZIP: $\\($item.FullName\\)\"; \"Velikost: {0:N0} B\" -f $item.Length; Add-Type -AssemblyName System.IO.Compression.FileSystem; $z = [System.IO.Compression.ZipFile]::OpenRead\\($zip\\); $z.Entries | Select-Object -ExpandProperty FullName; $z.Dispose\\(\\))",
"PowerShell($src = \"D:\\\\Přesunuté\\\\300_Programování\\\\00_NET\\\\XX_Podklady\\\\WPF-DarkTheme-Kit\"; $zip = \"D:\\\\Přesunuté\\\\300_Programování\\\\00_NET\\\\XX_Podklady\\\\WPF-DarkTheme-Kit.zip\"; $staging = Join-Path $env:TEMP \"wdtk_stage\"; if \\(Test-Path $staging\\) { Remove-Item $staging -Recurse -Force }; New-Item -ItemType Directory -Path $staging | Out-Null; Copy-Item $src -Destination $staging -Recurse; Remove-Item \\(Join-Path $staging \"WPF-DarkTheme-Kit\\\\.claude\"\\) -Recurse -Force -ErrorAction SilentlyContinue; if \\(Test-Path $zip\\) { Remove-Item $zip -Force }; Compress-Archive -Path \\(Join-Path $staging \"WPF-DarkTheme-Kit\"\\) -DestinationPath $zip -Force; Remove-Item $staging -Recurse -Force; $item = Get-Item $zip; \"ZIP: $\\($item.FullName\\)\"; \"Velikost: {0:N0} B\" -f $item.Length; Add-Type -AssemblyName System.IO.Compression.FileSystem; $z = [System.IO.Compression.ZipFile]::OpenRead\\($zip\\); $z.Entries | Select-Object -ExpandProperty FullName; $z.Dispose\\(\\))",
"PowerShell(dotnet build Rozpisky\\\\Rozpisky.csproj -clp:ErrorsOnly -v q 2>&1)",
"Bash(xargs grep -l \"RelayCommand\\\\|class.*Command\")",
"Bash(xargs grep -l \"class RelayCommand\")"
]
}
}
+13
View File
@@ -0,0 +1,13 @@
# Build výstupy
bin/
obj/
# Visual Studio
.vs/
*.user
*.suo
# Dočasné / pracovní
_render_ukazka/
__verify/
*.bak
+224
View File
@@ -0,0 +1,224 @@
# Podklad: Práce s CADem, rozvržení a tisk
> Výcuc funkcionality (bez kódu) pro převod VBA verze 1.0.8 do C#.
> Zaměřeno **čistě na práci s CADem, rozvržení (layouty) a tisk** bez rozpisek.
> Popisuje funkcionalitu + použité ActiveX/COM API, které se bude převádět.
---
## 1. Připojení na CAD (`clsCadLink`)
Centrální objekt pro veškerou komunikaci s CADem. Vše ostatní jde přes něj (`cad.Doc.*`).
**Funkcionalita:**
- **Late-binding** přes `GetObject(, ProgID)` připojuje se POUZE na **již běžící** CAD, nic nespouští.
- Zkouší v pořadí: `AutoCAD.Application``ZWCAD.Application``BricscadApp.AcadApplication`. První, který běží, vyhraje.
- Po napojení na aplikaci si vezme `mApp.ActiveDocument`. Když CAD běží, ale není otevřený výkres → chyba.
- Drží: `App` (aplikace), `Doc` (aktivní dokument), `Engine` (který ProgID se chytil), `LastError`, `IsConnected`.
**Pomocné metody nad dokumentem (volitelné, používá je generování):**
- `CheckLayer(name)` → existuje hladina? (`Doc.Layers(name)`)
- `AddLayer(name, color, lineWeight)``Doc.Layers.Add`
- `CheckBlock(name)` → existuje definice bloku? (`Doc.Blocks(name)`)
- `AddBlock(path)` → vloží blok ze souboru do ModelSpace (`InsertBlock`) jen kvůli načtení definice a hned referenci smaže.
**Pro C#:** V .NET přes COM Interop (`Marshal.GetActiveObject("AutoCAD.Application")`) nebo silně typované `Autodesk.AutoCAD.Interop` (`AcadApplication`, `AcadDocument`). Pozor `GetActiveObject` je v moderním .NET potřeba volat ručně přes `Marshal`. Pořadí ProgID zachovej jako fallback řetězec.
---
## 2. Datové nosiče (čtení dat z Excelu, bez CADu)
### `Layout` (data jednoho rozvržení)
Načítá řádek z listu **SEZNAM** + pojmenovanou buňku `stavba`. Drží metadata jednoho výkresu:
- `JmenoZakazka` (z named range `stavba`), `JmenoObjekt`, `JmenoRozvrzeni` (= název layoutu, sl. 28), `NazevPrilohy`, `MeritkoPrilohy` (sl. 23), `PocetPapiru`, `Ctb` (název CTB stylu, sl. 30).
- `InicializaceDat(radek)` načte tyto hodnoty z konkrétního řádku.
- Spojování hodnot oddělovačem (název + dílčí část) přes pomocnou funkci.
### `NastaveniTiskarny` (držák tiskových hodnot)
Defaultní hodnoty tisku převedené z ActiveX konstant na **číselné literály** (kvůli late-bindingu):
- `Tiskarna` = `"DWG to PDF.pc5"`, `StyleSheet` = `"zwcad.ctb"`
- `PaperUnits = 1` (mm), `PlotRotation = 0`, `PlotType = 5` (Layout), `StandardScale = 16` (ac1_1, tj. 1:1 pozor, AcPlotScale je enum, hodnota 1 by byla ac1_128in_1ft = 1:1536), `PlotViewportFirst = True`, `PlotOrigin = (0,0)`.
- `NastavRozmerPapiru(sirka, vyska)` → sestaví `CanonicalMediaName` jako `"420x297"` (z reálných rozměrů rámečku).
**Pro C#:** Tohle jsou jednoduché DTO/POCO třídy. V C# můžeš vrátit silně typované enumy (`AcPlotType.acLayout` atd.) místo magických čísel, ale zachovej mapování:
| Konstanta | Hodnota | Význam |
|---|---|---|
| PaperUnits mm | 1 | acMillimeters |
| Rotation 0° | 0 | ac0degrees |
| PlotType Layout | 5 | acLayout |
| PlotType Window | 4 | acWindow |
| Scale 1:1 | 1 | ac1_1 |
| Scale ToFit | 0 | acScaleToFit |
---
## 3. Konfigurace tiskárny a papíru (`modPlotConfig`)
**Funkcionalita:**
- **Cache do skrytého listu `_Plot`** (xlSheetVeryHidden): ukládá poslední zvolenou tiskárnu (`ConfigName`, B1) a papír (`MediaName`, B2). Při portu nahradíš nějakým config souborem / settings.
- **`TiskarnyZCad(cad)`** → seznam dostupných tiskáren: `cad.Doc.ActiveLayout.GetPlotDeviceNames`.
- **`PapiryZCad(cad, device)`** → seznam papírů pro danou tiskárnu. Trik: vytvoří dočasnou plot konfiguraci `Doc.PlotConfigurations.Add("__tmpQ", True)`, nastaví `configName = device`, zavolá `RefreshPlotDeviceInfo`, načte `GetCanonicalMediaNames` a dočasnou konfiguraci zase smaže.
- **`OveritKonfiguraci(...)`** → před tiskem ověří, že uložená tiskárna i papír reálně existují v aktuálním CADu (stejnou technikou). Vrací popis chyby.
- **`ZobrazPlotConfig`** → modální formulář pro výběr (UI nahradíš WinForms/WPF dialogem).
**Klíčové API:** `Layout.GetPlotDeviceNames`, `PlotConfigurations.Add/Item/Delete`, `PlotConfiguration.configName`, `.RefreshPlotDeviceInfo`, `.GetCanonicalMediaNames`.
---
## 4. Aktualizace / úprava rozvržení (`modAktualizace`) — JÁDRO
Hlavní procedura `TestAktualizace`. Podle vybraného rozsahu řádků na listu SEZNAM upraví odpovídající layouty ve výkresu.
**Tok funkcionality:**
1. **Ochranné podmínky:** musí běžet na listu SEZNAM, výběr musí začínat řádkem ≥ 2.
2. Načte pole `Layout` objektů z vybraných řádků.
3. Připojí se na CAD (`clsCadLink`).
4. **Sestaví seznam všech layoutů** ve výkresu (`Doc.Layouts`), pro každý `Name` a `TabOrder`, a **seřadí podle TabOrder** (bubble sort). Pozn.: `Layouts.Count - 1` = počet rozvržení bez modelového prostoru.
5. **Kontroly počtu:** rozsah nesmí být větší než počet layoutů ani než počet zbývajících layoutů od aktivního.
6. **Aktivní layout nesmí být "Model"** jinak vyzve uživatele aby aktivoval rozvržení. Začíná se od `ActiveLayout.TabOrder`.
7. **Dvoufázové přejmenování (kvůli konfliktům jmen):** nejdřív přejmenuje cílové layouty na **dočasné náhodné názvy** (`GenerateSecureAlphaNumeric(15) & i`), aby se předešlo kolizi při finálním přejmenování.
8. **Hlavní smyčka** přes layouty podle TabOrder (s progress formulářem + možnost zrušit):
- Přejmenuje layout na cílový `JmenoRozvrzeni` (chráněno error handlerem).
- **Automatické vyhodnocení měřítka** (pokud zapnuto named range `MERITKOAUTOMATICKY`) viz níže.
- **Projde všechny entity v `Layout.Block`** a hledá blokové reference (`blk.ObjectName = "AcDbBlockReference"`):
- **Blok `vykres_hlavicka`** → pokud `HasAttributes`, projde `GetAttributes` a podle `TagString` zapíše hodnoty: `STAVBA`, `OBJEKT`, `PRILOHA`, `MERITKO` (formátuje jako `"M 1:500"` jen když obsahuje `"1:"`). Po změně `TextString` zavolá `AttList(k).Update`.
- **Blok `ramecek`** → pokud je dynamický (`IsDynamicBlock`), `GetBoundingBox Poc, Kon` → z rozměrů spočítá **počet papírů**: `delka = RoundUp(Kon(0)/210)`, `vyska = RoundUp(Kon(1)/297)`, `PocetPapiru = delka*vyska`, a nastaví formát papíru (`NastavRozmerPapiru`).
- **Nastavení tiskárny na layout** (pokud zapnuto named range `NASTAVENITISKARNY = 1`): zapíše do layoutu `ConfigName, CanonicalMediaName, PaperUnits, PlotOrigin, PlotRotation, PlotType, StandardScale, PlotViewportsFirst, StyleSheet`. Pokud je vyplněn vlastní CTB, použije `Ctb & ".ctb"`.
9. **Zápis zpět do Excelu:** počet papírů (sl. 24) a měřítka (sl. 23) zpět do listu SEZNAM.
### Vyhodnocení měřítka z výřezů (`VyhodnotMeritka`)
- Projde entity layoutu a hledá **viewporty** (`ObjectName = "AcDbViewport"`) v hladině **`!!NOPRINT`**.
- Z `viewport.CustomScale` spočítá jmenovatel měřítka: `jmenovatel = Round((1/CustomScale) * jednotkyFaktor)`.
- `jednotkyFaktor` podle systémové proměnné `INSUNITS` (`Doc.GetVariable("INSUNITS")`): 4 = mm (1), 6 = m (1000).
- Sesbírá unikátní měřítka, seřadí vzestupně, vrátí např. `"1:500, 1:1000"`.
- **Pozor (zjištěno při portu):** blok layoutu obsahuje kromě nakreslených výřezů i neviditelný
pseudoviewport „pohledu na papír“ (DXF VIEWPORT id 1; vzniká při aktivaci layoutu a kopíruje se
s layoutem, může ležet i v hladině `!!NOPRINT` a kdekoliv v pořadí bloku). Jeho `CustomScale`
je aktuální zoom → falešná měřítka (u výkresů v metrech typicky 1:1000). Jednoznačná identifikace:
aktivovat layout, vypnout `MSpace` a přečíst `Doc.ActivePViewport.Handle` to je on (ověřeno na
AutoCADu). Nouzová heuristika bez aktivace: pseudoviewport má `StandardScale = 2` (`acVp1_1`)
trvale zamrzlé; skutečné výřezy s měřítkem ze seznamu mají `1` (`acVpCustomScale`) heuristika
by ale zahodila i skutečný výřez nastavený přes enum na 1:1.
**Pro C# nejdůležitější API této části:**
- `Doc.Layouts` (kolekce), `Layout.Name`, `.TabOrder`, `.Block` (iterace entit).
- Entita: `.ObjectName` (`"AcDbBlockReference"`, `"AcDbViewport"`), `.Name`, `.IsDynamicBlock`, `.HasAttributes`, `.GetAttributes()`, `.GetBoundingBox(out min, out max)`, `.Layer`, `.CustomScale`.
- Atribut: `.TagString`, `.TextString`, `.Update()`.
- Layout plot props: `.ConfigName, .CanonicalMediaName, .PaperUnits, .PlotOrigin, .PlotRotation, .PlotType, .StandardScale, .PlotViewportsFirst, .StyleSheet`.
- `Doc.GetVariable("INSUNITS")`.
- **Pozor na late-bound rozdíl:** ve verzi 1.0.7 (ZWCAD) se typ poznával přes `TypeName(x)="IZcadBlockReference"`. Tady se to dělá robustněji přes `x.ObjectName`. V C# se silně typovaným interopem můžeš použít `is AcadBlockReference` / `is AcadPViewport`, ale `ObjectName` je univerzálnější napříč enginy.
---
## 5. Tisk rozvržení do PDF (`modTiskPriloh`)
`TiskPrilohyDoPdf` hromadný tisk vybraných layoutů do PDF souborů.
**Tok:**
1. Ochranné podmínky (list SEZNAM, řádek ≥ 2), připojení CAD.
2. **Připraví cílovou složku** exportu.
3. Sestaví seznam názvů layoutů k tisku (sl. 28), prázdné přeskočí.
4. **Zazálohuje a přenastaví systémové proměnné** výkresu:
- `BACKGROUNDPLOT` → 0 (tisk na popředí = čeká se na dokončení důležité pro spolehlivost!).
- `PLOTTRANSPARENCYOVERRIDE` → 2 (ignorovat průhlednost).
- `LTSCALE` ukládá originál, volitelně přenastaví dle named range `LTSCALE` a hodnoty z buňky (sl. 29) per řádek (s validací: prázdné/nečíselné/≤0 → 1).
5. **Smyčka tisku** (progress + cancel):
- Cesta PDF = `složka & nazev & ".pdf"`.
- **Kontrola, zda PDF není otevřené** (`IsFileOpen`) → dialog Abort/Retry/Ignore.
- Najde layout `Doc.Layouts(nazev)`, neexistuje → přeskočí.
- Nastaví LTSCALE.
- **Aktivuje layout** (`Doc.ActiveLayout = Layout`) a **vytiskne** (`Doc.Plot.PlotToFile cesta`).
- Error handling per soubor, počítá vytisknuto/přeskočeno.
6. **Obnoví** původní systémové proměnné.
**Klíčové API:** `Doc.GetVariable/SetVariable`, `Doc.Layouts(name)`, `Doc.ActiveLayout = layout`, `Doc.Plot.PlotToFile(path)`.
---
## 6. Tisk bloku oknem (`modPrint`)
Dvě varianty tisku jednotlivého bloku podle jeho bounding boxu (window plot):
- **`VytiskniRozpisku`** → `Doc.Plot.PlotToDevice` (na zařízení).
- **`VytiskniRozpiskuDoSouboru`** → `Doc.Plot.PlotToFile(cesta)` (do souboru, nastaví `BACKGROUNDPLOT=0`).
**Postup nastavení layoutu pro window-plot:**
1. `blk.GetBoundingBox(min, max)` → rohy okna.
2. Na `ActiveLayout`: `ConfigName`, `RefreshPlotDeviceInfo`, `CanonicalMediaName`, `PlotRotation = 0`, `PlotType = 4` (acWindow), `SetWindowToPlot(ll, ur)`, `CenterPlot = True`, `UseStandardScale = True`, `StandardScale = 0` (acScaleToFit), `PlotWithPlotStyles = True`, `StyleSheet = ""`, `PlotViewportBorders = False`, `PlotHidden = False`.
3. Tisk.
**Pro C#:** `SetWindowToPlot` bere dvě 2-prvková `double[]` pole (lower-left, upper-right). Pozor na předávání polí přes COM (varianty).
---
## 7. Čtení dat / atributů z CADu
### Vyhledání bloků v ModelSpace (`modRozpisky.NajdiRozpisky`)
Obecný vzor procházení výkresu (použitelný i mimo rozpisky):
- `cad.Doc.ModelSpace` → iterace `For Each ent In ms`.
- Filtr: `ent.ObjectName = "AcDbBlockReference"` a porovnání názvu.
- **Název bloku včetně dynamických:** zkusí `EffectiveName`, fallback `Name` (`NazevBloku`).
- Čte `insertionPoint` (pole `[x,y,z]`), filtruje X > 0, sbírá do kolekce.
- **Seřazení podle X** (index sort) vzestupně.
- Progress každých 50 entit + možnost zrušit (vyhozením vlastní chyby `vbObjectError + 100`).
### Výpis atributů (`modVypisAtributu`)
Diagnostika pro daný blok:
- `blk.insertionPoint`.
- `blk.HasAttributes``blk.GetAttributes` (proměnné/vyplnitelné) → `TagString` + `TextString`.
- `blk.GetConstantAttributes` (konstantní, jen pro info).
**Klíčové API:** `Doc.ModelSpace` (iterovatelná kolekce), `entity.ObjectName`, `.EffectiveName/.Name`, `.InsertionPoint`, `.HasAttributes`, `.GetAttributes()`, `.GetConstantAttributes()`.
---
## 8. Pomocné funkce (`Funkce`) — bez CADu
- `TurnOffStuff` / `TurnOnStuff` vypnutí/zapnutí přepočtu, událostí, překreslování Excelu (zrychlení). V C# přes Excel interop nebo to vynecháš.
- `VratZnakNeboX` ořez/doplnění textu na pevnou délku.
- `GetValueFromTable` vyhledávání v Excel ListObject tabulce dle klíče (jako VLOOKUP).
- **`IsFileOpen`** zjistí, zda je soubor zamčený jiným procesem (otevře `Binary Access Read Lock Read`, chyba 70 = otevřeno). V C# → `try` na `FileStream` s `FileShare.None`.
- **`VybratCestuKeSlozce`** dialog výběru složky. V C# → `FolderBrowserDialog`/`OpenFolderDialog`.
---
## 9. Poznámky pro převod do C#
1. **Late-binding → typed interop:** VBA tu používá `As Object` + late-binding kvůli podpoře více CADů (AutoCAD/ZWCAD/BricsCAD). V C# zvážit: buď `dynamic` (zachová multi-engine flexibilitu), nebo `Autodesk.AutoCAD.Interop` (typová bezpečnost, ale váže tě na AutoCAD). Detekce typu entit přes `.ObjectName` (string) funguje univerzálně napříč enginy doporučuji ponechat.
2. **Magická čísla** plot konstant (viz tabulka v sekci 2) v C# nahraď enumy z interopu, ale ověř hodnoty proti cílovému CADu (komentář v `NastaveniTiskarny` na to upozorňuje).
3. **Pole přes COM:** `GetBoundingBox`, `GetAttributes`, `InsertionPoint`, `PlotOrigin`, `SetWindowToPlot` pracují s `object`/`Variant` poli `double[]`. V C# je třeba je správně boxovat/castovat (`(double[])`, `out object`).
4. **Systémové proměnné** se čtou/zapisují přes `Doc.GetVariable/SetVariable` stejné názvy (`BACKGROUNDPLOT`, `PLOTTRANSPARENCYOVERRIDE`, `LTSCALE`, `INSUNITS`). Vždy zazálohovat a obnovit.
5. **`BACKGROUNDPLOT = 0`** je nutný pro synchronní tisk (jinak `PlotToFile` nečeká na dokončení a soubory mohou být nedokončené).
6. **Plot konfigurace** se vytváří/maže jako dočasné objekty (`__tmpQ`) v C# obal do `try/finally`, ať se vždy uklidí.
7. **Iterace `Layout.Block`** vrací modelspace/paperspace entity daného layoutu v typed interopu `AcadLayout.Block` je `AcadBlock`.
8. Zdroj dat (list SEZNAM, named ranges `stavba`, `MERITKOAUTOMATICKY`, `NASTAVENITISKARNY`, `LTSCALE`) je dnes Excel při portu rozhodni, odkud je vezme nová appka (DB/JSON/UI).
9. **Celočíselné systémové proměnné přes `SetVariable` posílat jako `short` (VT_I2):** VBA `Integer`
je 16bitový, .NET `int` se přes COM marshaluje jako VT_I4 a AutoCAD na
`SetVariable("PLOTTRANSPARENCYOVERRIDE", 2)` s VT_I4 **spadl nativně** (fatal error bez výjimky,
bez error.log; lokalizováno breadcrumb logem `tisk_diag.log`, 2026-07-05). V C# proto `(short)0`,
`(short)2` apod.; `LTSCALE` je RTREAL → `double` je správně. Navíc: zápis PLOTTRANSPARENCYOVERRIDE
se přeskočí, když už cílovou hodnotu má, selhání téhle kosmetické proměnné tisk nezastaví (vlastní
try/catch) a při obnově se vrací jen proměnné, které se skutečně změnily
(`CadTiskService.VytiskniRozvrzeni`). **Stejná třída problému: `Doc.Layouts.Item(neexistující
jméno)`** místo chytatelné výjimky nativní pád aplikace (2026-07-05). Layout se proto hledá
výčtem kolekce `Layouts` a porovnáním `Name` (`CadTiskService.NajdiLayout`); obecně na COM
rozhraní CADu **nestavět control flow na vyhazujících voláních** tam, kde jde výsledek zjistit
výčtem/porovnáním, udělat to tak.
10. **Odchylka od VBA rozměr rámečku:** originál (§4, bod `ramecek`) čte rozměr přes `GetBoundingBox`. C# port to **záměrně nedělá** bounding box je nespolehlivý (závisí na tom, co je v bloku zrovna vykreslené/viditelné). Blok `ramecek` je dynamický se dvěma viditelnostmi (`Benefit`/`Libovolný`); port čte rozměr přímo z dynamických parametrů `GetDynamicBlockProperties()` dvojice `VzdálenostBx`/`VzdálenostBy` pro viditelnost `Benefit`, `VzdálenostLx`/`VzdálenostLy` pro `Libovolný`. Aktivní viditelnost se pozná podle toho, která vlastnost má aktuální hodnotu rovnou `"Benefit"`/`"Libovolný"` (viz `CadLayoutService.NajdiViditelnost`), ne podle názvu vlastnosti viditelnosti samotné (ten nemusí být `Visibility1`).
---
## 10. Mapa souborů (kde co hledat)
| Soubor | Odpovídá za |
|---|---|
| `clsCadLink.cls` | Připojení k CADu, helpery pro hladiny a bloky |
| `Layout.cls` | Data jednoho rozvržení (čtení ze SEZNAM) |
| `NastaveniTiskarny.cls` | Default hodnoty tisku (DTO) |
| `modPlotConfig.bas` | Seznam tiskáren/papírů, ověření, cache `_Plot` |
| `modAktualizace.bas` | **Jádro** úprava layoutů, atributů, měřítka, počtu papírů |
| `modTiskPriloh.bas` | Hromadný tisk layoutů do PDF |
| `modPrint.bas` | Window-plot jednoho bloku (na device / do souboru) |
| `modRozpisky.bas` | Vyhledávání bloků v ModelSpace |
| `modVypisAtributu.bas` | Diagnostický výpis atributů bloku |
| `Funkce.bas` | Sdílené pomocné funkce (Excel/IO, bez CADu) |
+408
View File
@@ -0,0 +1,408 @@
# Rozpisky rozhraní aplikace, datový model a budoucí funkcionality
Tento dokument popisuje **standalone aplikaci Rozpisky** z pohledu uživatele a dat: jaké má
obrazovky, co se v nich vyplňuje, jaké akce nabízí a jak je vnitřně uspořádaný datový model.
Navazuje na [`ROZPISKY.md`](ROZPISKY.md), který popisuje **vykreslovací/exportní jádro**
(kreslicí rozhraní `IProfileRenderer`, čtečka DXF šablony `DxfTemplate`, exporty PNG/PDF/DXF).
Aplikace je **standalone náhrada za původní excelový nástroj „Rozpisky 1.0.8"**. Z něj vychází
veškerá logika rozhraní i datový model. Tento dokument je proto současně i **přepisem původního
excelu** do podoby specifikace pro samostatnou aplikaci.
> **Styl WPF se zde neřeší** popisujeme rozhraní funkčně (obrazovky, pole, akce, tok dat).
> Vizuální styl bude dodán samostatně.
---
## 1. Účel a vztah k `ROZPISKY.md`
| Dokument | Co popisuje |
|---|---|
| [`ROZPISKY.md`](ROZPISKY.md) | **Jádro** jak se kreslí a exportuje (DXF → obrazovka / PNG / PDF / DXF). |
| `ROZHRANI.md` (tento) | **Aplikace** uživatelské rozhraní, datový model (projekt + seznam příloh), akce a roadmapa. |
| [`SZ_SM011_P10_Manual_v6.md`](SZ_SM011_P10_Manual_v6.md) | **Závazný manuál SŽ** (v06, 26. 1. 2026) struktura dokumentace, **kódové označení** (kap. 3) a **popisové pole / pravidla vyplnění** (kap. 4). Autoritativní zdroj pro význam polí a kódu. |
**Princip:** Uživatel jednou vyplní **údaje projektu** (společné všem výkresům) a postupně plní
**seznam příloh** (jeden řádek = jeden výkres = jedna rozpiska). Aplikace každý řádek spojí
s údaji projektu, naplní pole DXF šablony a umí jej zobrazit (náhled) a vyexportovat/vytisknout.
**Standalone-first:** aplikace nevyžaduje AutoCAD. Vstup je **DXF šablona + data**, výstup
**PNG / PDF / DXF** (engine z `ROZPISKY.md`). Integrace s AutoCADem (interop do `.dwg`, rozvržení,
CTB, tisk plotterem) je vedena **jen jako budoucí rozšíření** viz [sekce 8](#8-roadmapa-budoucích-funkcionalit).
---
## 2. Slovník pojmů
| Pojem | Význam |
|---|---|
| **Rozpiska** | Rohové razítko výkresu tabulka s údaji o projektu, příloze, autorech a revizích. |
| **Příloha** | Jeden výkres / list dokumentace. V seznamu = jeden řádek. |
| **Seznam příloh** | Tabulka všech příloh projektu (původní list `SEZNAM`). |
| **Rozvržení (layout)** | Pojmenované rozvržení výkresu (v AutoCADu paper-space layout). Jméno se generuje z identifikace přílohy. |
| **Orientační schéma** | Malé schéma v rozpisce naznačující polohu listu v rámci stavby (buňky `K1``K47`). |
| **Placeholder loga** | Vyhrazené místo v šabloně pro logo (objednatele/zhotovitele). Leží na netisknutelné hladině. |
| **Číselník** | Předvyplněný seznam hodnot (organizace, firmy, jména, profese, …) pro výběr v polích. |
| **Projektové pole** | Hodnota společná celému projektu (z údajů projektu). |
| **Řádkové pole** | Hodnota specifická pro jednu přílohu (ze seznamu příloh). |
---
## 3. Datový model
Model 1:1 odpovídá listům původního excelu. Entity:
### 3.1 Projekt (původní list `ÚDAJE`)
Údaje společné všem rozpiskám projektu. Část polí se **automaticky dohledává z číselníků**
(v excelu přes `VLOOKUP`).
| Pole | Popis | Zdroj |
|---|---|---|
| `stavba` | Název stavby / akce | ruční |
| `HIP` | Hlavní inženýr projektu | ruční |
| `investor` | Objednatel zakázky | číselník Organizace |
| `investorAdresa` | Adresa objednatele | **auto** z č. Organizace |
| `slozkaSZ` | Složka SŽ (zástupce objednatele) | ruční / číselník |
| `slozkaSZAdresa` | Adresa složky SŽ | **auto** z č. Organizace |
| `skod` | S-kód (číslo smlouvy dle SoD) | ruční |
| `cisloZakazky` | Číslo zakázky | ruční |
| `stupenDokumentace` | Stupeň dokumentace (např. DOS) | číselník Verze dokumentace |
| `smluvniDatumZpracovani` | Smluvní datum zpracování | ruční (datum) |
| `zhotovitelSOD` | Zhotovitel díla dle SoD | číselník Firmy |
| `adresaSOD` | Adresa zhotovitele | **auto** z č. Firmy |
| `telSOD` | Telefon zhotovitele | **auto** z č. Firmy |
| `emailSOD` | E-mail zhotovitele | **auto** z č. Firmy |
### 3.2 Příloha (řádek v seznamu, původní list `SEZNAM`)
Sloupce AAD; jeden řádek = jedna rozpiska. Souhrn:
| Sl. | Obsah | Sl. | Obsah |
|---|---|---|---|
| A | Označení části | O | Kontroloval (R1) |
| B | Označení dílčí části / řady | PU | R2R4 + kdo revidoval |
| C | Název objektu / dílčí části | V | Zhotovitel části |
| D | Číslo objektu / komplexu | W | Měřítko |
| E | Číslo přílohy typ | X | Formát × A4 |
| F | Číslo přílohy dílčí | Y | Kraj |
| G | Číslo přílohy pořadí | Z | Katastrální území |
| H | Název přílohy | AA | TUDU |
| I | Název dílčí části přílohy | AB | **Jméno rozvržení** (generováno) |
| J | Zkratka přílohy | AC | LTSCALE |
| K | Vypracoval | AD | CTB (styl tisku) |
| L | Odpovědný projektant | | |
| M | Specialista | | |
| N | Revize 1 (R1) | | |
> **Jméno rozvržení** (`AB`) je **poloautomatické**, řízené zaškrtávacím polem **Příloha objektu**
> (první sloupec seznamu, výchozí = zaškrtnuto):
> - **zaškrtnuto** → jméno se generuje dle manuálu SŽ **kap. 2.7.7.9** (číslo objektu bez teček,
> mezer a pomlček; složky podobjekt / typ / dílčí / pořadí přílohy oddělené `_`):
> `SO131.45.01` + `D1` + `1` + `001` = `SO1314501_D1_1_001`; u podobjektu `SO1314501_01_D1_1_001`;
> u skupiny `SK11311_D1_1_001`. Needituje se;
> - **odškrtnuto** → uživatel zadá jméno ručně (při přepnutí se předvyplní vygenerovanou hodnotou).
>
> Slouží jako jméno layoutu / základ názvu exportovaného souboru. Řádky se shodným jménem rozvržení
> (kolize názvu exportu) se v seznamu zvýrazní.
>
> Samostatné zaškrtávací pole **Rozpiska** (výchozí = zaškrtnuto) řídí, zda název PDF při exportu
> dostane příponu `_rozpiska` (např. `SO1314501_D1_1_001_rozpiska.pdf`). Je nezávislé na polích
> **Příloha objektu** a **Desky objektu** i rozpiska na spisové desky objektu může příponu dostat,
> pokud je pole zaškrtnuté.
### 3.3 Revize (původní list `REVIZE`)
Tabulka revizí: **REVIZE** (kód/číslo verze, např. `000`), **DATUM**, **POPIS**, **KONTROLOVAL**.
Na revize se odkazují řádkové revizní sloupce přílohy (R1R4) a revizní pole v rozpisce.
> **Revize jsou interní data projektu** patří vždy ke **konkrétní zakázce**, ukládají se spolu
> s projektem (ne do externí číselníkové databáze, viz [3.4](#34-číselníky)). Pravidla vyplnění
> revizí popisuje manuál SŽ, kap. 4.3.4 (viz [3.7](#37-kódové-označení-přílohy-47-pozic-pole-k1k47)).
### 3.4 Číselníky
| Číselník | Sloupce | Použití |
|---|---|---|
| **Organizace** | název, adresa | objednatel + složka SŽ (auto adresy) |
| **Firmy / zhotovitelé** | firma, adresa, telefon, e-mail | zhotovitel díla i objektu (auto kontakty) |
| **Jména** | jméno | vypracoval / odpovědný / specialista / kontroloval |
| **Profese** | profese | profesní zařazení |
| **Verze dokumentace** | verze | stupeň dokumentace |
| **Části** | označení, název části | části/objekty v seznamu |
> **Číselníky jsou sdílená data napříč projekty a žijí v samostatné databázi vedle `.exe`.**
> V původním Excelu šlo o samostatné listy s daty (označení dokumentace, data firem apod.).
> Ve standalone aplikaci se z nich stane **externí databázový soubor uložený vedle spustitelného
> souboru** (např. `cisleniky.db` / `.json` ve složce aplikace), aby ho šlo **průběžně doplňovat
> a sdílet** mezi projekty bez zásahu do aplikace. (Na rozdíl od revizí, které jsou interní
> k zakázce viz [3.3](#33-revize-původní-list-revize).)
### 3.5 Konfigurace
| Položka | Popis |
|---|---|
| `CestaExportu` | Kořenová cesta pro export (např. `C:\temp`). |
| `SlozkaExportu` | Podsložka exportu. |
| `MERITKOAUTOMATICKY` | Automatické měřítko. |
| `LTSCALE` | Měřítko typů čar (per výkres). |
| CTB | Styl tisku (per výkres). |
| Nastavení tiskárny | Pro tiskové akce. |
> Tato konfigurace **nejsou pole rozpisky** řídí export/tisk a generování rozvržení.
### 3.6 Mapování `pole aplikace ↔ DXF ATTDEF tag`
Šablona [`Rozpiska.dxf`](Rozpiska.dxf) obsahuje **101 polí (`ATTDEF`)**. Tag pole = klíč do
`values` při vyplnění (viz `DxfTemplate.Replay(g, values)` v `ROZPISKY.md`). Mapování na zdroj dat:
**Projektová pole (z údajů projektu):**
| DXF tag | Zdroj |
|---|---|
| `NAZEV_AKCE` | `stavba` |
| `SO_HIP` | `HIP` |
| `OBJEDNATEL_NAZEV` | `investor` |
| `OBJEDNATEL_ADRESA` | `investorAdresa` |
| `OBJEDNATEL_ZASTUPCE_NAZEV` | `slozkaSZ` |
| `OBJEDNATEL_ZASTUPCE_ADRESA` | `slozkaSZAdresa` |
| `S_KOD` | `skod` |
| `ZAK_CISLO` | `cisloZakazky` |
| `STUP_DOK` | `stupenDokumentace` |
| `DATUM` | `smluvniDatumZpracovani` |
| `ZHOTOVITEL_DILA_NAZEV` | `zhotovitelSOD` |
| `ZHOTOVITEL_DILA_ADRESA` | `adresaSOD` |
| `ZHOTOVITEL_DILA_TELEFON` | `telSOD` |
| `ZHOTOVITEL_DILA_EMAIL` | `emailSOD` |
**Řádková pole (z přílohy / seznamu).** Význam upřesněn dle grafické podoby popisového pole
(manuál SŽ, kap. 4.2.1 a 4.3.7):
| DXF tag | Význam / zdroj |
|---|---|
| `CAST` | Označení části dokumentace (např. `D`) SEZNAM A |
| `CAST_NAZEV` | Název části / profesní skupina (např. „Dokumentace objektů") |
| `OBJEKT_NAZEV` | Název objektu / dílčí části SEZNAM C |
| `RADA` | **řada** objektu (např. `SO131`) SEZNAM B |
| `USEK` | **úsek** složka označení objektu (kap. 2.5 / 3.2.4) |
| `RAZENI` | **řazení** složka označení objektu (kap. 2.5 / 3.2.4) |
| `PODOBJEKT` | **podobjekt** (např. `01`) kap. 3.2.5 |
| `DILCI_CAST` | dílčí část / řada přílohy (např. `D130`) |
| `TYP` | typ přílohy (např. `D.1`) SEZNAM E |
| `CISLO_PRILOHY` | číslo přílohy (3 cifry, např. `003`) SEZNAM G |
| `PRILOHA` | Název přílohy SEZNAM H |
| `PRILOHA_CAST` | Název dílčí části přílohy SEZNAM I |
| `SO_VYPRACOVAL_PRILOHA` | Zpracovatel přílohy SEZNAM K |
| `SO_ZODP_PROJEKTANT` | Odpovědný projektant SEZNAM L |
| `SO_SPECIALISTA` | Specialista SEZNAM M |
| `MERITKO` | Měřítko SEZNAM W |
| `FORMATY` | Formáty (× A4) SEZNAM X |
| `KRAJ` | Kraj SEZNAM Y |
| `KATASTRALNI_UZEMI` | Katastrální území SEZNAM Z |
| `TUDU` | TUDU SEZNAM AA |
| `ZHOTOVITEL_OBJEKTU_NAZEV` | Zhotovitel části / objektu SEZNAM V |
| `ZHOTOVITEL_OBJEKTU_ADRESA/TELEFON/EMAIL` | **auto** z č. Firmy podle `V` |
> Čtveřice `RADA` / `USEK` / `RAZENI` / `PODOBJEKT` tvoří dohromady **označení objektu**
> (např. `SO131 .15 .12 .01`); přesné rozčlenění čísla objektu řeší manuál kap. 2.5 a 3.2.4.
**Revizní pole (z přílohy R1R4 + list `REVIZE`):**
**4 revizní řádky** (sloupce *Revize / Datum / Popis / Kontroloval*, manuál kap. 4.3.4),
plněné z interní tabulky revizí ([3.3](#33-revize-původní-list-revize)):
| DXF tag | Zdroj |
|---|---|
| `REVIZE1CISLO`..`4CISLO` | číslo / kód verze revize (např. `000`) |
| `REVIZE1DAT`..`4DAT` | datum revize |
| `REVIZE1`..`REVIZE4` | popis revize |
| `SO_KONTROLOVAL1`..`4` | kdo kontroloval |
**Strojově čitelný kód:** `K1``K47` = **47 pozic kódového označení přílohy** (manuál kap. 3.1.5),
zobrazených znak po znaku v popisovém poli. **Negenerují se ručně aplikace je sestaví** z ostatních
polí (S-kód, stupeň dokumentace, část, objekt, podobjekt, typ, příloha, revize) viz
[3.7](#37-kódové-označení-přílohy-47-pozic-pole-k1k47).
**Orientační schéma:** samostatné **grafické pole** (pole 2, manuál kap. 4.3.2) schéma stavby
s barevným zvýrazněním polohy objektu; **není to `ATTDEF`**, ale obrazová oblast.
**Loga:** `Rozpiska_logo1``Rozpiska_logo5` **nejsou `ATTDEF`**, ale geometrické placeholdery
na netisknutelných hladinách (viz [sekce 6](#6-loga-a-orientační-schéma)).
> **Pozn. k mapování:** Význam polí byl upřesněn dle **manuálu SŽ** (kap. 3 a 4) původní excelová
> makra (VBA) se v `.xlsx` nedochovala, ale grafická podoba a pravidla vyplnění popisového pole jsou
> v manuálu závazné. Skupiny hladin `Rozpiska_pole_1``8` odpovídají různým stylům (velikost/zarovnání)
> textu polí viz [sekce 7](#7-metadata-polí-zalamování-styl-víceřádkovost).
### 3.7 Kódové označení přílohy (47 pozic, pole `K1``K47`)
Strojově čitelný kód (manuál SŽ kap. 3) má **pevných 47 pozic** rozdělených do 6 skupin; v rozpisce
se zobrazuje znak po znaku v polích `K1``K47`. **Aplikace ho generuje** z ostatních polí, ručně se
neopravuje. Neobsazená alfanumerická pozice = `X`, oddělovač = `_`.
| Skupina | Pozice | Význam | Zdroj polí |
|---|---|---|---|
| 1 | 111 | Identifikace stavby (S-kód / označení investora) | `S_KOD` |
| 2 | 1316 | Stupeň dokumentace (4 znaky, např. `DPSX`) | `STUP_DOK` |
| 3 | 1821 | Část / číselná řada (`D` + řada) | `CAST`, `RADA` |
| 4 | 2334 | Objekt + podobjekt (`SO`/`PS` + číslo + podobjekt) | `RADA`,`USEK`,`RAZENI`,`PODOBJEKT` |
| 5 | 3643 | Typ přílohy + číslo přílohy (`D1``_YYY`) | `TYP`, `CISLO_PRILOHY` |
| 6 | 4547 | Verze / revize (`P`/`0`/`V` + pořadí) | `REVIZEnCISLO` |
Příklad (manuál kap. 4.2.1):
`S123456789X _ DPSX _ D130 _ SO131_15_12 _ 01 _ D1 _ 2 _ 003 _ 000`
(v rozpisce bez mezer, každý znak v jedné buňce `K1``K47`).
> Detailní pravidla pozic jsou v manuálu kap. 3.2.13.2.8; generátor kódu je proto vlastní
> funkční blok aplikace (vstup = strukturovaná pole přílohy, výstup = 47 znaků).
---
## 4. Uživatelské rozhraní
Popsáno funkčně (bez vizuálního stylu).
### 4.1 Hlavní okno
Sekce odpovídající původním listům excelu + panel akcí:
- **Údaje projektu** formulář (viz 4.2).
- **Seznam příloh** mřížka (viz 4.3). Hlavní pracovní plocha.
- **Revize** tabulka revizí projektu.
- **Číselníky** správa hodnot (organizace, firmy, jména, profese, verze, části).
- **Nastavení** cesty exportu, šablona, tisk/konfigurace.
### 4.2 Formulář „Údaje projektu"
Pole dle [sekce 3.1](#31-projekt-původní-list-údaje). Výběr objednatele / zhotovitele z číselníku
**automaticky doplní** adresu a kontakty (náhrada excelového `VLOOKUP`).
### 4.3 Mřížka „Seznam příloh"
- Tabulka (DataGrid), kde **jeden řádek = jedna rozpiska**; sloupce dle [sekce 3.2](#32-příloha-řádek-v-seznamu-původní-list-seznam).
- Vybraná pole nabízejí výběr z **číselníků** (jména, firmy, kraj, části…).
- **Editace dat probíhá přímo v mřížce.**
- Sloupec **Jméno rozvržení** je poloautomatický viz [sekce 3.2](#32-příloha-řádek-v-seznamu-původní-list-seznam)
(řízeno polem **Příloha objektu**); duplicitní jména se zvýrazní.
### 4.4 Náhled rozpisky
- **Náhled se otevírá dvojklikem na řádek** seznamu → otevře se **samostatné okno** s náhledem
dané rozpisky.
- Náhled vykresluje engine z `ROZPISKY.md` (`IProfileRenderer` / `SkiaProfileRenderer`)
živě, ve výkresových milimetrech.
- Okno umožní přepnout: **vyplněné hodnoty** (`Replay(g, values)`) ↔ **předpis / názvy polí**
(`Replay(g)`).
- Z okna náhledu lze danou rozpisku rovnou vyexportovat / vytisknout.
---
## 5. Funkcionality / akce
Přehled vychází z ribbonu původního excelu; sloupec „Standalone chování" popisuje ekvivalent
v samostatné aplikaci.
| Akce (původně) | Standalone chování | Stav |
|---|---|---|
| **Uprav rozpisku** | Editace dat řádku v mřížce; **náhled dvojklikem na řádek** (samostatné okno, [4.4](#44-náhled-rozpisky)). Tlačítko „uprav" odpadá. | plán |
| **Uprav rozpisky** | Hromadné přegenerování / kontrola všech rozpisek (např. po změně údajů projektu). | plán |
| **Vytiskni rozpisku** | Export / tisk **jedné** rozpisky (PDF/PNG) přes `Exporters` (`PdfA4`, `PngA4`). | plán |
| **Vytiskni rozpisky** | **Dávkový** export/tisk všech rozpisek do `CestaExportu\SlozkaExportu`, název dle „Jména rozvržení". | plán |
| **Aktualizuj rozvržení** | Standalone ekvivalent: vygenerování/aktualizace výstupů (DXF/PDF) per rozvržení. (AutoCAD varianta → [roadmapa](#8-roadmapa-budoucích-funkcionalit).) | plán |
| **Vytiskni rozvržení** | Tisk rozvržení (přes vlastní engine / PrintDialog). (AutoCAD varianta → roadmapa.) | plán |
| **Tisk seznamu** | Tisk přehledu seznamu příloh. | plán |
| **Export seznamu (Otevřená verze)** | Export seznamu příloh do otevřeného `.xlsx`. | plán |
| **Navigace (Předchozí/Další)** | Navigace mezi sekcemi / řádky. | volitelné |
| **O programu** | Informace o aplikaci a verzi. | plán |
---
## 6. Loga a orientační schéma
- Šablona má **5 placeholderů na loga** (`Rozpiska_logo1``Rozpiska_logo5`).
- Placeholdery leží na **netisknutelných hladinách** (`!!NOPRINT`): jejich geometrie se **nevykresluje**,
ale aplikace o nich **musí vědět** (z dat šablony), aby do nich uměla posadit logo viz
`RectForLayer` / `g.Image(...)` v `ROZPISKY.md`.
- Logo se přiřazuje podle role: **loga institucí financujících stavbu** (pole 1, manuál kap. 4.3.1
nepovinné), **objednatel/investor**, **zhotovitel díla**, **zhotovitel části/objektu** (manuál
kap. 4.3.5/4.3.6). Načítá se ze souboru (`File.ReadAllBytes("logo.png")`).
- **Orientační schéma** je **samostatné grafické pole** (pole 2, manuál kap. 4.3.2) schéma stavby
s červeným zvýrazněním polohy dotčeného objektu (u liniových staveb klad listů). Nepovinné u malých
staveb. **Není to** pole `K1``K47` (to je strojový kód, viz [§ 3.7](#37-kódové-označení-přílohy-47-pozic-pole-k1k47)).
- Pole pro **otisk razítek** / razítko oprávněné osoby (pole 3, kap. 4.3.3) zůstává v elektronické
verzi obvykle prázdné.
---
## 7. Metadata polí (zalamování, styl, víceřádkovost)
Pole rozpisky nejsou jen „text na souřadnici". Aby výstup odpovídal šabloně, je třeba u každého
pole znát (viz požadavek v [`ROZPISKY.md`](ROZPISKY.md)):
- **velikost textu** (výška) daná stylem/atributem,
- **styl** atributu,
- **víceřádkovost** zda byl atribut víceřádkový,
- **šířku** pole pro **zalamování** dlouhého textu (adresy, dlouhé názvy),
- **zarovnání** (H/V).
Skupiny hladin `Rozpiska_pole_1``8` zřejmě odpovídají různým stylům polí. Tato metadata se mají
číst ze šablony (z `ATTDEF`) a nést spolu s hodnotou.
> **Dopad do jádra:** současná signatura `IProfileRenderer.Text(...)` v `ROZPISKY.md` nenese šířku
> ani příznak víceřádkovosti. Navrhuje se ji **rozšířit o šířku pole + zalamování** (resp. nést
> metadata pole z `DxfTemplate`), aby šlo dlouhé hodnoty správně zalomit. Tím se zároveň řeší
> rozpor mezi požadavkem na víceřádková pole a stávajícím omezením „jen jednořádkový `TEXT`".
---
## 8. Roadmapa budoucích funkcionalit
Orientační fázování (priorita shora dolů):
**Fáze 1 MVP (standalone):**
- Načtení DXF šablony a zjištění polí/placeholderů.
- Datový model: projekt + seznam příloh (v paměti).
- Mřížka seznamu, formulář údajů projektu.
- Náhled rozpisky dvojklikem na řádek.
- Export **jedné** rozpisky do PNG/PDF.
**Fáze 2 data a produktivita:**
- Číselníky + automatické doplnění adres/kontaktů (VLOOKUP).
- Revize.
- **Uložení/načtení projektu** (formát viz [otevřené otázky](#9-otevřené-otázky)).
- Import z původního `.xlsx` (přemostění starých dat).
- **Dávkový export** všech rozpisek.
**Fáze 3 výstupy:**
- Tisk (PrintDialog) jednotlivě i dávkově.
- Tisk / export **seznamu příloh** (i „Otevřená verze" do `.xlsx`).
- Loga ze souborů, orientační schéma.
**Fáze 4 kvalita výstupu:**
- **Víceřádková pole a zalamování** (viz [sekce 7](#7-metadata-polí-zalamování-styl-víceřádkovost)).
- Podpora více DXF šablon.
- Nastavení (cesty, měřítko, CTB, tiskárna).
**Budoucí blok AutoCAD integrace (interop):**
- Vyplnění a tisk rozpisek **přímo ve výkresech `.dwg`**.
- Práce s **rozvrženími (layouty)**, **CTB** styly, **LTSCALE**, tisk plotterem.
- Výstup zpět do `.dwg`.
- *(Mimo standalone jádro aktivuje se jen při dostupném AutoCADu.)*
---
## 9. Otevřené otázky
- **Formát uložení** rozdělené úložiště:
- **Projekt** (údaje + seznam příloh + **revize**) = interní data jedné zakázky → formát k rozhodnutí
(JSON / SQLite / vlastní).
- **Číselníky** (organizace, firmy, jména, profese, verze, části) = **externí databáze vedle `.exe`**,
sdílená a doplňovatelná napříč projekty ([3.4](#34-číselníky)) → formát k rozhodnutí.
- **Generátor 47-pozičního kódu** doimplementovat dle manuálu kap. 3.2.13.2.8 vč. okrajových případů
(sdružené objekty `SK`/`PK`, kap. 3.3; pracovní vs. definitivní verze).
- **Orientační schéma** jak se obrázek schématu získá/přiřadí k příloze (vstup uživatele vs. generování).
- **WPF styl** dodá uživatel samostatně; zde neřešen.
- **Import z původního `.xlsx`** mapování listů `ÚDAJE`/`SEZNAM`/`REVIZE` + číselníků do modelu.
- **Více DXF šablon** jedna pevná vs. volitelné šablony (základní pole / pole celé stavby / vložená
příloha, manuál kap. 4.2.14.2.3).
- **Lokalizace** zatím čeština.
+222
View File
@@ -0,0 +1,222 @@
# 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í.
+33542
View File
File diff suppressed because it is too large Load Diff
+55
View File
@@ -0,0 +1,55 @@
using Rozpisky.Dxf;
using Rozpisky.Rendering;
using Xunit;
namespace Rozpisky.Tests;
/// <summary>
/// Bbox pro fit fragmentu (loga do placeholderu): dřív se text počítal jen jako vkládací bod,
/// takže textová loga mohla přetéct rámeček. Teď se bbox počítá replayem do BoundsRenderer.
/// </summary>
public class BoundsTesty
{
[Fact]
public void VsechnaLoga_MajiNeprazdneBounds()
{
foreach (var name in TestData.VsechnaLoga)
{
var tpl = new DxfTemplate(TestData.Logo(name));
Assert.True(tpl.Bounds is { W: > 0, H: > 0 }, $"{name}: Bounds = {tpl.Bounds}");
}
}
[Fact]
public void Bounds_ZahrnujiTextIVyplne()
{
// sprava_zeleznic: 2 texty + 2 SOLID šrafy. Bbox musí obsáhnout všechny smyčky výplní
// a být širší než samotné výplně (texty SPRÁVA/ŽELEZNIC vedle značky).
var tpl = new DxfTemplate(TestData.Logo("sprava_zeleznic.dxf"));
var rec = new RecordingRenderer();
tpl.Replay(rec);
var body = rec.Fills.SelectMany(f => f).SelectMany(l => l).ToList();
Assert.NotEmpty(body);
var b = tpl.Bounds!.Value;
Assert.True(body.All(p => p.X >= b.X - 1e-6 && p.X <= b.X + b.W + 1e-6
&& p.Y >= b.Y - 1e-6 && p.Y <= b.Y + b.H + 1e-6));
double vyplneW = body.Max(p => p.X) - body.Min(p => p.X);
Assert.True(b.W > vyplneW, $"bbox W={b.W:F2} nezahrnuje texty (výplně W={vyplneW:F2})");
}
[Fact]
public void BoundsRenderer_RespektujeZarovnaniTextu()
{
// Center/Right roste od kotvy doleva; Top dolů dřív box rostl vždy jen doprava nahoru.
var stred = new BoundsRenderer();
stred.Text(0, 0, "ABCD", 10, HAlign.Center, VAlign.Middle);
Assert.True(stred.MinX < 0 && stred.MaxX > 0, $"Center: {stred.MinX}..{stred.MaxX}");
Assert.True(stred.MinY < 0 && stred.MaxY > 0, $"Middle: {stred.MinY}..{stred.MaxY}");
var vpravo = new BoundsRenderer();
vpravo.Text(0, 0, "ABCD", 10, HAlign.Right, VAlign.Top);
Assert.True(vpravo.MinX < 0 && vpravo.MaxX <= 1e-9, $"Right: {vpravo.MinX}..{vpravo.MaxX}");
Assert.True(vpravo.MinY < 0 && vpravo.MaxY <= 1e-9, $"Top: {vpravo.MinY}..{vpravo.MaxY}");
}
}
+96
View File
@@ -0,0 +1,96 @@
using Rozpisky.Dxf;
using Xunit;
namespace Rozpisky.Tests;
/// <summary>
/// Šrafy (HATCH) s hranovými hranicemi (úsečky, oblouky, elipsy) dřív se tiše přeskočily,
/// takže tečky a oblé výplně v logách chyběly. Hodnoty jsou předpočítané ze skutečných DXF.
/// </summary>
public class HatchTesty
{
private static RecordingRenderer Prehraj(string logo)
{
var rec = new RecordingRenderer();
new DxfTemplate(TestData.Logo(logo)).Replay(rec);
return rec;
}
private static (double W, double H, double Cx, double Cy) Bbox(IReadOnlyList<(double X, double Y)> loop)
{
double ax = double.MaxValue, ay = double.MaxValue, bx = double.MinValue, by = double.MinValue;
foreach (var (x, y) in loop)
{
ax = Math.Min(ax, x); ay = Math.Min(ay, y);
bx = Math.Max(bx, x); by = Math.Max(by, y);
}
return (bx - ax, by - ay, (ax + bx) / 2, (ay + by) / 2);
}
[Fact]
public void Tesia_TeckaZKruhoveHrany_SeVyplni()
{
var rec = Prehraj("tesia.dxf");
// 2 SOLID hatche: obrys žárovky (polyline s bulge) + tečka (plná kruhová hrana r≈0,78).
var smycky = rec.Fills.SelectMany(f => f).ToList();
Assert.Equal(2, smycky.Count);
// Tečka = menší smyčka; průměr ≈ 1,56 mm (plný kruh z jediné obloukové hrany).
var tecka = smycky.OrderBy(l => Bbox(l).W).First();
var (w, h, _, _) = Bbox(tecka);
Assert.InRange(w, 1.50, 1.62);
Assert.InRange(h, 1.50, 1.62);
}
[Fact]
public void SudopBrno_MichaneUseckoveAObloukoveHrany_DajiUzavreneSmycky()
{
var rec = Prehraj("sudopBrno.dxf");
// 6 SOLID šraf, všechny jen s hranovými cestami (úsečky + oblouky) → dřív 0 výplní.
// Referenční bboxy spočtené nezávisle hustým vzorkováním raw dat hran (512 dílků/oblouk).
Assert.Equal(6, rec.Fills.Count);
var smycky = rec.Fills.SelectMany(f => f).ToList();
Assert.Equal(7, smycky.Count); // jeden hatch má 2 cesty, ostatní po jedné
Assert.All(smycky, l => Assert.True(l.Count >= 3));
// Nezávisle na pořadí (draw order přes SortEntitiesTable může hatche přeuspořádat) ověříme,
// že existují smyčky s referenčními bboxy včetně plné kružnice r=3,825 (7,649²) z jediné
// obloukové hrany a řetězů úsečka+oblouk vč. ccw=false (zrcadlené úhly). Chybné znaménko/
// otočení hrany nebo ztracená kružnice by referenci znatelně změnily.
MaSmyckuBbox(smycky, 7.649, 7.649);
MaSmyckuBbox(smycky, 5.571, 0.626);
MaSmyckuBbox(smycky, 2.092, 0.066);
MaSmyckuBbox(smycky, 7.807, 1.085);
MaSmyckuBbox(smycky, 11.290, 1.733);
}
private static void MaSmyckuBbox(IReadOnlyList<IReadOnlyList<(double X, double Y)>> smycky, double refW, double refH)
=> Assert.Contains(smycky, l =>
{
var (w, h, _, _) = Bbox(l);
return w >= refW - 0.1 && w <= refW + 0.02 && h >= refH - 0.1 && h <= refH + 0.02;
});
[Fact]
public void Signalprojekt_KruhovaAEliptickaHrana_SeVyplni()
{
var rec = Prehraj("signalprojekt.dxf");
Assert.Equal(2, rec.Fills.Count);
var smycky = rec.Fills.SelectMany(f => f).ToList();
// Eliptická cesta: poloosy 1,16 a 0,759·1,16 → bbox ≈ 1,76 × 2,32 (hlavní osa svisle).
Assert.Contains(smycky, l =>
{
var (w, h, _, _) = Bbox(l);
double lo = Math.Min(w, h), hi = Math.Max(w, h);
return Math.Abs(hi - 2.32) < 0.05 && Math.Abs(lo - 1.76) < 0.05;
});
}
[Fact]
public void PolylinioveHranice_BezRegrese()
{
// exprojekt: 3 SOLID šrafy čistě s polyline hranicemi musí zůstat beze změny.
var rec = Prehraj("exprojekt.dxf");
Assert.Equal(3, rec.Fills.Count);
Assert.All(rec.Fills, f => Assert.All(f, l => Assert.True(l.Count >= 3)));
}
}
+130
View File
@@ -0,0 +1,130 @@
using Rozpisky.Dxf;
using Rozpisky.Models;
using Xunit;
namespace Rozpisky.Tests;
/// <summary>
/// Čistá byznys logika převodu vstupů na SŽ-kódy a názvy: 47-poziční kód
/// (<see cref="KodPrilohy"/>), rozpad čísla objektu (<see cref="ObjektOznaceni"/>) a automatické
/// jméno rozvržení (<see cref="Priloha.JmenoRozvrzeniAuto"/>). Hlídá pevnou šířku/pozice kódu
/// tichý posun by jinak zkazil kód na každém výkresu. Očekávané hodnoty jsou worked examples
/// z doc-komentářů příslušných metod.
/// </summary>
public class KodovaniTesty
{
private static Projekt VzorProjekt() => new()
{
Skod = "SO12345",
StupenDokumentace = "DUSP",
};
private static Priloha VzorPriloha() => new()
{
CisloObjektu = "SO131.45.12.01",
OznaceniCasti = "D",
OznaceniDilciCasti = "D110",
TypPrilohy = "D1",
DilciPriloha = "1",
PoradiPrilohy = "001",
R1 = "V00",
};
[Fact]
public void Sestav_MaPevnychSedmActyricetZnaku_APozice()
{
var kod = KodPrilohy.Sestav(VzorProjekt(), VzorPriloha());
Assert.Equal(KodPrilohy.Delka, kod.Length);
Assert.Equal("SO12345XXXX_DUSP_D110_SO1314512_01_D1_1_001_V00", kod);
}
[Fact]
public void Sestav_PrazdnaPoleDoplniX_ProrezanaOrizne()
{
var p = new Projekt { Skod = "", StupenDokumentace = "" };
var r = new Priloha { CisloObjektu = "", R1 = "" };
var kod = KodPrilohy.Sestav(p, r);
Assert.Equal(KodPrilohy.Delka, kod.Length);
// Samé X kromě separátorů na pevných pozicích 12,17,22,32,35,38,40,44.
Assert.Equal("XXXXXXXXXXX_XXXX_XXXX_XXXXXXXXX_XX_XX_X_XXX_XXX", kod);
}
[Fact]
public void SestavDesky_MaPevnyStred_XX_X_000()
{
var kod = KodPrilohy.SestavDesky(VzorProjekt(), VzorPriloha());
Assert.Equal(KodPrilohy.Delka, kod.Length);
Assert.Equal("SO12345XXXX_DUSP_D110_SO1314512_01_XX_X_000_V00", kod);
}
[Fact]
public void SestavSeznam_PrepiseStredAVoltelnouRevizi()
{
var kod = KodPrilohy.SestavSeznam(VzorProjekt(), VzorPriloha(), revizeKod: "P01");
Assert.Equal(KodPrilohy.Delka, kod.Length);
Assert.Equal("SO12345XXXX_DUSP_D110_SO1314512_01_XX_X_001_P01", kod);
}
[Theory]
[InlineData("V00", 'V', "00")] // vedoucí znak = typ, zbytek číslo
[InlineData("002", '0', "02")] // číslicový typ 0, číslo z dalších číslic
[InlineData("", 'X', "XX")] // bez revize → X + XX
public void RozpadRevize_SePromitneDoPozic45Az47(string rev, char typ, string cislo)
{
var r = VzorPriloha();
r.R1 = rev;
var kod = KodPrilohy.Sestav(VzorProjekt(), r);
Assert.Equal(typ, kod[44]); // pozice 45 (0-based 44)
Assert.Equal(cislo, kod.Substring(45)); // pozice 4647
}
[Fact]
public void ObjektOznaceni_Rozpad_RozdeliNaSlozky()
{
var o = ObjektOznaceni.Rozpad("SO131.45.12.01");
Assert.Equal("SO", o.Prefix);
Assert.Equal("131", o.Cislo);
Assert.Equal("45", o.Usek);
Assert.Equal("12", o.Razeni);
Assert.Equal("01", o.Podobjekt);
}
[Theory]
[InlineData("SK11", "SK", "11")] // jen řada, bez dalších segmentů
[InlineData("PS 22-3_4", "PS", "")] // libovolný oddělovač; "PS" samé písmeno → prázdné číslo
public void ObjektOznaceni_Rozpad_ZvladneOddelovaceAKratkeVstupy(string vstup, string prefix, string cislo)
{
var o = ObjektOznaceni.Rozpad(vstup);
Assert.Equal(prefix, o.Prefix);
Assert.Equal(cislo, o.Cislo);
}
[Fact]
public void ObjektOznaceni_Rozpad_PrazdnyVstupDaSamePrazdne()
{
var o = ObjektOznaceni.Rozpad("");
Assert.Equal(new ObjektOznaceni("", "", "", "", ""), o);
}
[Fact]
public void JmenoRozvrzeniAuto_SkladaZObjektuAPrilohy()
{
var r = VzorPriloha();
Assert.Equal("SO1314512_01_D1_1_001", r.JmenoRozvrzeniAuto);
}
[Fact]
public void JmenoRozvrzeniAuto_ProDeskyJeObjektovySpecial()
{
var r = VzorPriloha();
r.JeDesky = true;
Assert.Equal("SO1314512_01_XX_X_000", r.JmenoRozvrzeniAuto);
}
[Fact]
public void NazevObjektovehoSpecialu_TypXXZustavaLiteralne()
{
Assert.Equal("SO1314512_01_XX_X_001",
Priloha.NazevObjektovehoSpecialu("SO131.45.12.01", "001"));
}
}
+60
View File
@@ -0,0 +1,60 @@
using System.IO;
using ACadSharp.Entities;
using ACadSharp.IO;
using Rozpisky.Dxf;
using Rozpisky.Rendering;
using Xunit;
namespace Rozpisky.Tests;
/// <summary>
/// Oblique úhel textu (kurzíva náklonem, DXF group 51/50) dřív se ignoroval a text
/// se kreslil vzpřímeně. „SUDOP BRNO" má v DXF group 51 = 15° a group 41 (width) = 0,7.
/// </summary>
public class ObliqueTesty
{
[Fact]
public void SudopBrno_TextNeseObliqueAWidthFactor()
{
var rec = new RecordingRenderer();
new DxfTemplate(TestData.Logo("sudopBrno.dxf")).Replay(rec);
var t = Assert.Single(rec.Texts);
Assert.Equal("SUDOP BRNO", t.Text);
Assert.Equal(15.0, t.Style.ObliqueDeg, 2); // DXF group 51 = 15.0
Assert.Equal(0.7, t.WidthFactor, 3);
}
[Fact]
public void OstatniLoga_MajiObliqueNula()
{
// Regrese: žádné jiné logo oblique nemá nesmí se objevit nenulový sklon.
foreach (var name in TestData.VsechnaLoga.Where(n => n != "sudopBrno.dxf"))
{
var rec = new RecordingRenderer();
new DxfTemplate(TestData.Logo(name)).Replay(rec);
Assert.All(rec.Texts, t => Assert.Equal(0, t.Style.ObliqueDeg));
}
}
[Fact]
public void DxfExport_ZachovaObliqueNaTextu()
{
var dxf = new DxfProfileRenderer();
dxf.Layer("0", 0x000000, 0.25);
dxf.Text(10, 20, "Zkouška", 2.5, style: new TextStyle("Arial", ObliqueDeg: 3));
string path = Path.Combine(Path.GetTempPath(), $"rozpisky-oblique-{Guid.NewGuid():N}.dxf");
try
{
DxfWriter.Write(path, dxf.Document, binary: false);
var doc = DxfReader.Read(path);
var t = Assert.Single(doc.Entities.OfType<TextEntity>());
Assert.Equal(3.0 * Math.PI / 180.0, t.ObliqueAngle, 6);
}
finally
{
File.Delete(path);
}
}
}
+138
View File
@@ -0,0 +1,138 @@
using System.IO;
using ACadSharp.Entities;
using ACadSharp.IO;
using Rozpisky.Dxf;
using Rozpisky.Rendering;
using Xunit;
namespace Rozpisky.Tests;
/// <summary>
/// Analytické oblouky end-to-end: model <see cref="Edge"/>/<see cref="Contour"/>, jemnost vzorkování
/// (sagitta) a round-trip přes DXF export (<see cref="DxfProfileRenderer"/>) + import
/// (<see cref="DxfTemplate"/>). Hlídá zejména znaménka (CCW/CW) a převod bulge → oblouk.
/// </summary>
public class ObloukyTesty
{
private static (double W, double H, double MinX, double MinY) Bbox(IEnumerable<(double X, double Y)> pts)
{
double ax = double.MaxValue, ay = double.MaxValue, bx = double.MinValue, by = double.MinValue;
foreach (var (x, y) in pts) { ax = Math.Min(ax, x); ay = Math.Min(ay, y); bx = Math.Max(bx, x); by = Math.Max(by, y); }
return (bx - ax, by - ay, ax, ay);
}
private static List<(double X, double Y)> Zplosti(Contour c, double tol = 0.005)
{
var pts = new List<(double X, double Y)>();
foreach (var e in c.Edges) e.AppendPoints(pts, tol);
if (c.Edges.Count > 0) pts.Add((c.Edges[^1].EX, c.Edges[^1].EY));
return pts;
}
[Fact]
public void Edge_PointAt_A_Reversed_SediKoncoveBody()
{
// Čtvrtkružnice jednotkového kruhu proti směru hodin z (1,0) do (0,1).
var e = Edge.Arc(1, 0, 0, 1, 0, 0, r: 1, a0: 0, sweep: Math.PI / 2);
var s = e.PointAt(e.A0);
var en = e.PointAt(e.A0 + e.Sweep);
Assert.Equal(1, s.X, 6); Assert.Equal(0, s.Y, 6);
Assert.Equal(0, en.X, 6); Assert.Equal(1, en.Y, 6);
var r = e.Reversed();
Assert.Equal(e.EX, r.SX, 9); Assert.Equal(e.SX, r.EX, 9);
Assert.Equal(Math.PI / 2, r.A0, 9);
Assert.Equal(-Math.PI / 2, r.Sweep, 9);
// Otočená hrana má stejný geometrický počátek jako původní konec.
var rs = r.PointAt(r.A0);
Assert.Equal(0, rs.X, 6); Assert.Equal(1, rs.Y, 6);
}
[Theory]
[InlineData(10.0, 0.02)]
[InlineData(3.825, 0.02)]
[InlineData(50.0, 0.05)]
public void ArcSegments_DodrziSagittu(double r, double tol)
{
double sweep = 2 * Math.PI;
int n = ContourGeometry.ArcSegments(sweep, r, tol);
double delta = sweep / n;
double sagitta = r * (1 - Math.Cos(delta / 2));
Assert.True(sagitta <= tol + 1e-9, $"sagitta {sagitta} > tol {tol} (n={n})");
}
[Fact]
public void AnalytickyKruh_ZplosteniDaSpravnyBbox()
{
// Jediná Arc hrana přes celý kruh (r=3,825) → bbox = průměr, ostrý na tětivovou toleranci.
var e = Edge.Arc(3.825, 0, 3.825, 0, 0, 0, r: 3.825, a0: 0, sweep: 2 * Math.PI);
var (w, h, _, _) = Bbox(Zplosti(new Contour(new[] { e }, Closed: true)));
Assert.InRange(w, 7.64, 7.65);
Assert.InRange(h, 7.64, 7.65);
}
[Theory]
[InlineData(1.0)] // CCW horní půlkruh
[InlineData(-1.0)] // CW dolní půlkruh hlídá normalizaci úhlů v exportu i importu
public void RoundTrip_HatchOblouk_ZachovaGeometrii(double smer)
{
double r = 5;
double sweep = smer * Math.PI;
// Půldisk: oblouk (5,0)→(-5,0) + úsečka zpět.
var arc = Edge.Arc(5, 0, -5, 0, 0, 0, r, a0: 0, sweep: sweep);
var line = Edge.Line(-5, 0, 5, 0);
var contour = new Contour(new[] { arc, line }, Closed: true);
var g = new DxfProfileRenderer();
g.Layer("0", 0x000000);
g.Fill(new[] { contour });
// Export → dočasný DXF → import zpět a porovnání bboxu (sagitta ≤ 0,02 mm).
string path = Path.Combine(Path.GetTempPath(), $"rozpisky_arc_{Guid.NewGuid():N}.dxf");
try
{
DxfWriter.Write(path, g.Document, binary: false);
var rec = new RecordingRenderer();
new DxfTemplate(path).Replay(rec);
var loop = Assert.Single(Assert.Single(rec.FillContours));
var (w, h, minX, minY) = Bbox(Zplosti(loop, 0.02));
Assert.InRange(w, 9.9, 10.05); // šířka = 2r
Assert.InRange(h, 4.95, 5.05); // výška = r
Assert.InRange(minX, -5.05, -4.95);
if (smer > 0) Assert.InRange(minY, -0.05, 0.05); // horní půlkruh: y ∈ [0, 5]
else Assert.InRange(minY, -5.05, -4.95); // dolní půlkruh: y ∈ [-5, 0]
}
finally { if (File.Exists(path)) File.Delete(path); }
}
[Theory]
[InlineData(1.0)]
[InlineData(-1.0)]
public void StandaloneArc_Export_DaEntituArcSpravnymUhlem(double smer)
{
double r = 4, a0 = 0.3, sweep = smer * 1.2;
var edge = Edge.Arc(0, 0, 0, 0, cx: 1, cy: 2, r: r, a0: a0, sweep: sweep);
// dopočti skutečné koncové body
var s = edge.PointAt(a0); var e = edge.PointAt(a0 + sweep);
var contour = new Contour(new[] { Edge.Arc(s.X, s.Y, e.X, e.Y, 1, 2, r, a0, sweep) }, Closed: false);
var g = new DxfProfileRenderer();
g.Layer("0", 0);
g.Stroke(contour);
var arc = Assert.Single(g.Document.Entities.OfType<Arc>());
Assert.Equal(1, arc.Center.X, 6);
Assert.Equal(2, arc.Center.Y, 6);
Assert.Equal(r, arc.Radius, 6);
// ARC entita je vždy CCW start→end; pro CW rozsah se meze prohodí, ale koncové BODY musí sedět.
var pStart = (arc.Center.X + r * Math.Cos(arc.StartAngle), arc.Center.Y + r * Math.Sin(arc.StartAngle));
var pEnd = (arc.Center.X + r * Math.Cos(arc.EndAngle), arc.Center.Y + r * Math.Sin(arc.EndAngle));
// množina koncových bodů oblouku se musí shodovat (v jednom z pořadí).
bool ok = (Blizko(pStart, s) && Blizko(pEnd, e)) || (Blizko(pStart, e) && Blizko(pEnd, s));
Assert.True(ok, $"koncové body oblouku nesedí: start={pStart} end={pEnd} vs s={s} e={e}");
static bool Blizko((double X, double Y) a, (double X, double Y) b)
=> Math.Abs(a.X - b.X) < 1e-6 && Math.Abs(a.Y - b.Y) < 1e-6;
}
}
+36
View File
@@ -0,0 +1,36 @@
using System.IO;
using Rozpisky.Dxf;
using SkiaSharp;
using Xunit;
namespace Rozpisky.Tests;
/// <summary>
/// Vyrenderuje všech 9 log do PNG vizuální porovnání před/po změnách parseru
/// (uživatel nemá screenshoty aplikace, výstupní soubory ano). Cílová složka
/// z env proměnné <c>ROZPISKY_PNG_OUT</c>, jinak %TEMP%\rozpisky-loga-png.
/// </summary>
public class PngHarness
{
[Fact]
public void VyrenderujVsechnaLogaDoPng()
{
string outDir = Environment.GetEnvironmentVariable("ROZPISKY_PNG_OUT")
?? Path.Combine(Path.GetTempPath(), "rozpisky-loga-png");
Directory.CreateDirectory(outDir);
const int W = 900, H = 600;
foreach (var name in TestData.VsechnaLoga)
{
var tpl = new DxfTemplate(TestData.Logo(name));
using var bmp = new SKBitmap(W, H);
using var canvas = new SKCanvas(bmp);
Exporters.RenderView(canvas, W, H, g => tpl.Replay(g), tpl.FrameBounds ?? tpl.Bounds,
zoom: 1, panXpx: 0, panYpx: 0);
using var img = SKImage.FromBitmap(bmp);
using var data = img.Encode(SKEncodedImageFormat.Png, 100);
using var fs = File.Create(Path.Combine(outDir, Path.ChangeExtension(name, ".png")));
data.SaveTo(fs);
}
}
}
+62
View File
@@ -0,0 +1,62 @@
using Rozpisky.Rendering;
namespace Rozpisky.Tests;
/// <summary>
/// Testovací <see cref="IProfileRenderer"/>: nic nekreslí, jen zaznamenává volání primitiv,
/// aby testy mohly assertovat, co přesně parser (<c>DxfTemplate.Replay</c>) vyprodukoval.
/// Obrysy (<see cref="Contour"/>) se pro pohodlné assertování bboxů zaznamenávají jak analyticky
/// (<see cref="FillContours"/>, <see cref="StrokeContours"/>), tak jemně zploštěné na body
/// (<see cref="Fills"/>, <see cref="Polylines"/>) zpětně kompatibilní s dřívějšími testy.
/// </summary>
public sealed class RecordingRenderer : IProfileRenderer
{
public sealed record TextCall(double X, double Y, string Text, double HeightMm, HAlign H, VAlign V,
double RotationDeg, TextStyle Style, double WidthFactor, double MaxWidthMm);
/// <summary>Každé volání Fill = smyčky jednoho HATCH, zploštěné na body.</summary>
public List<IReadOnlyList<IReadOnlyList<(double X, double Y)>>> Fills { get; } = new();
/// <summary>Analytické obrysy jednotlivých volání Fill (se zachovanými oblouky).</summary>
public List<IReadOnlyList<Contour>> FillContours { get; } = new();
/// <summary>Analytické obrysy z volání Stroke (bulge polylinie, samostatné oblouky).</summary>
public List<Contour> StrokeContours { get; } = new();
public List<TextCall> Texts { get; } = new();
/// <summary>Zploštěné polylinie i obrysy ze Stroke (kvůli zpětné kompatibilitě testů).</summary>
public List<IReadOnlyList<(double X, double Y)>> Polylines { get; } = new();
public int LineCount { get; private set; }
public int CircleCount { get; private set; }
public void Layer(string name, int rgb, double lineWeightMm = 0.25) { }
public void Line(double x1, double y1, double x2, double y2) => LineCount++;
public void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false) => Polylines.Add(pts);
public void Circle(double cx, double cy, double r) => CircleCount++;
public void Stroke(Contour c)
{
StrokeContours.Add(c);
Polylines.Add(Zplosti(c));
}
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)
=> Texts.Add(new TextCall(x, y, text, heightMm, h, v, rotationDeg, style, widthFactor, maxWidthMm));
public void Fill(IReadOnlyList<Contour> loops)
{
FillContours.Add(loops);
Fills.Add(loops.Select(Zplosti).ToList());
}
/// <summary>Zploští obrys na body (jemně, sagitta ≤ 0,02 mm) pro bbox asserty v testech.</summary>
private static IReadOnlyList<(double X, double Y)> Zplosti(Contour c)
{
var pts = new List<(double X, double Y)>();
foreach (var e in c.Edges) e.AppendPoints(pts, maxSagitta: 0.02);
if (c.Edges.Count > 0) pts.Add((c.Edges[^1].EX, c.Edges[^1].EY));
return pts;
}
}
+21
View File
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Rozpisky\Rozpisky.csproj" />
</ItemGroup>
</Project>
+53
View File
@@ -0,0 +1,53 @@
using Rozpisky.Rendering;
using SkiaSharp;
using Xunit;
namespace Rozpisky.Tests;
/// <summary>
/// Skutečný render Skií (ne jen zplošťení obrysu): plná kružnice tvořená jedinou obloukovou
/// hranou (sweep 360°) se musí vykreslit, ne „propadnout" degenerovaným <c>ArcTo</c>.
/// </summary>
public class SkiaRenderTesty
{
/// <summary>Vykreslí kresbu na 200×200 px bitmapu (mm 1:1, střed plátna = mm (0,0)) a vrátí
/// počet nebílých pixelů.</summary>
private static int PocetVyplnenych(Action<IProfileRenderer> draw)
{
const int S = 200;
using var bmp = new SKBitmap(S, S);
using var canvas = new SKCanvas(bmp);
canvas.Clear(SKColors.White);
SKPoint Map(double x, double y) => new((float)(S / 2 + x * 10), (float)(S / 2 - y * 10)); // 10 px/mm, Y nahoru
using (var r = new SkiaProfileRenderer(canvas, Map, pxPerMm: 10))
draw(r);
int n = 0;
for (int y = 0; y < S; y++)
for (int x = 0; x < S; x++)
if (bmp.GetPixel(x, y) != SKColors.White) n++;
return n;
}
[Fact]
public void PlnaKruznice_JakoJednaObloukovaHrana_SeVyplni()
{
// Kruh r=5 mm ze středu (0,0), jediná Arc hrana se sweepem 360°.
var edge = Edge.Arc(5, 0, 5, 0, 0, 0, r: 5, a0: 0, sweep: 2 * Math.PI);
var contour = new Contour(new[] { edge }, Closed: true);
int filled = PocetVyplnenych(g => { g.Layer("0", 0); g.Fill(new[] { contour }); });
// Plocha kruhu r=5 mm × (10 px/mm)² = π·2500 ≈ 7854 px; tolerantně > 5000.
Assert.True(filled > 5000, $"plná kružnice se nevykreslila (vyplněno {filled} px)");
}
[Fact]
public void ObloukovyOblouk_SeVykresli_Stroke()
{
// Kontrolní: běžný oblouk (< 360°) přes Stroke musí něco nakreslit.
var edge = Edge.Arc(5, 0, 0, 5, 0, 0, r: 5, a0: 0, sweep: Math.PI / 2);
int filled = PocetVyplnenych(g => { g.Layer("0", 0); g.Stroke(new Contour(new[] { edge }, Closed: false)); });
Assert.True(filled > 50, $"oblouk se nevykreslil (vyplněno {filled} px)");
}
}
+31
View File
@@ -0,0 +1,31 @@
using System.IO;
namespace Rozpisky.Tests;
/// <summary>Cesty k testovacím datům (loga DXF dodávaná s programem).</summary>
public static class TestData
{
/// <summary>Kořen repozitáře nalezen od bin složky testů směrem nahoru podle Rozpisky.sln.</summary>
public static string RepoRoot { get; } = NajdiKoren();
/// <summary>Plná cesta k logu ve zdrojové složce <c>Rozpisky\loga</c>.</summary>
public static string Logo(string fileName) => Path.Combine(RepoRoot, "Rozpisky", "loga", fileName);
/// <summary>Všech 9 log dodávaných s programem.</summary>
public static readonly string[] VsechnaLoga =
{
"eu_op_doprava.dxf", "md_sfdi.dxf", "sprava_zeleznic.dxf", "exprojekt.dxf",
"signalprojekt.dxf", "tesia.dxf", "mco.dxf", "exprojekt+mco.dxf", "sudopBrno.dxf",
};
private static string NajdiKoren()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir is not null)
{
if (File.Exists(Path.Combine(dir.FullName, "Rozpisky.sln"))) return dir.FullName;
dir = dir.Parent!;
}
throw new InvalidOperationException("Rozpisky.sln nenalezen nad " + AppContext.BaseDirectory);
}
}
+51
View File
@@ -0,0 +1,51 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.37111.16
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rozpisky", "Rozpisky\Rozpisky.csproj", "{4FF58C86-3728-4E15-A904-069C6DE219E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rozpisky.Tests", "Rozpisky.Tests\Rozpisky.Tests.csproj", "{4DEE3E71-22B2-40BC-8084-29E7F6079350}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4FF58C86-3728-4E15-A904-069C6DE219E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4FF58C86-3728-4E15-A904-069C6DE219E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4FF58C86-3728-4E15-A904-069C6DE219E8}.Debug|x64.ActiveCfg = Debug|Any CPU
{4FF58C86-3728-4E15-A904-069C6DE219E8}.Debug|x64.Build.0 = Debug|Any CPU
{4FF58C86-3728-4E15-A904-069C6DE219E8}.Debug|x86.ActiveCfg = Debug|Any CPU
{4FF58C86-3728-4E15-A904-069C6DE219E8}.Debug|x86.Build.0 = Debug|Any CPU
{4FF58C86-3728-4E15-A904-069C6DE219E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4FF58C86-3728-4E15-A904-069C6DE219E8}.Release|Any CPU.Build.0 = Release|Any CPU
{4FF58C86-3728-4E15-A904-069C6DE219E8}.Release|x64.ActiveCfg = Release|Any CPU
{4FF58C86-3728-4E15-A904-069C6DE219E8}.Release|x64.Build.0 = Release|Any CPU
{4FF58C86-3728-4E15-A904-069C6DE219E8}.Release|x86.ActiveCfg = Release|Any CPU
{4FF58C86-3728-4E15-A904-069C6DE219E8}.Release|x86.Build.0 = Release|Any CPU
{4DEE3E71-22B2-40BC-8084-29E7F6079350}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4DEE3E71-22B2-40BC-8084-29E7F6079350}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4DEE3E71-22B2-40BC-8084-29E7F6079350}.Debug|x64.ActiveCfg = Debug|Any CPU
{4DEE3E71-22B2-40BC-8084-29E7F6079350}.Debug|x64.Build.0 = Debug|Any CPU
{4DEE3E71-22B2-40BC-8084-29E7F6079350}.Debug|x86.ActiveCfg = Debug|Any CPU
{4DEE3E71-22B2-40BC-8084-29E7F6079350}.Debug|x86.Build.0 = Debug|Any CPU
{4DEE3E71-22B2-40BC-8084-29E7F6079350}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4DEE3E71-22B2-40BC-8084-29E7F6079350}.Release|Any CPU.Build.0 = Release|Any CPU
{4DEE3E71-22B2-40BC-8084-29E7F6079350}.Release|x64.ActiveCfg = Release|Any CPU
{4DEE3E71-22B2-40BC-8084-29E7F6079350}.Release|x64.Build.0 = Release|Any CPU
{4DEE3E71-22B2-40BC-8084-29E7F6079350}.Release|x86.ActiveCfg = Release|Any CPU
{4DEE3E71-22B2-40BC-8084-29E7F6079350}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B4632CE4-CE7F-49CC-BEA0-29FE22B52CDC}
EndGlobalSection
EndGlobal
+15
View File
@@ -0,0 +1,15 @@
<Application x:Class="Rozpisky.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Rozpisky"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Themes/Colors.Dark.xaml"/>
<ResourceDictionary Source="Themes/Metrics.xaml"/>
<ResourceDictionary Source="Themes/Controls.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
+59
View File
@@ -0,0 +1,59 @@
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Threading;
namespace Rozpisky
{
/// <summary>
/// Globální záchytná síť pro neošetřené výjimky aplikace nesmí spadnout bez stopy.
/// Každá výjimka se zapíše do <c>error.log</c> vedle .exe (stejný portable princip jako
/// <c>cad.json</c>/<c>layout.json</c>) a uživateli se ukáže MessageBox. Výjimky na UI
/// (Dispatcher) vlákně se označí jako zpracované, takže aplikace běží dál rozdělaný
/// projekt zůstane otevřený a jde uložit.
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Úklid diagnostického logu tisku, ať po měsících nenaroste do enormních rozměrů.
Cad.CadTiskService.UklidLog();
DispatcherUnhandledException += (_, args) =>
{
ZalogujChybu("UI (Dispatcher)", args.Exception);
args.Handled = true; // aplikace přežije projekt jde pořád uložit
UkazChybu(args.Exception);
};
// Výjimky mimo UI vlákno proces už nejde zachránit, ale aspoň zůstane log.
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
ZalogujChybu("AppDomain", args.ExceptionObject as Exception);
// Neodchycené výjimky z fire-and-forget Tasků SetObserved zabrání eskalaci.
TaskScheduler.UnobservedTaskException += (_, args) =>
{
ZalogujChybu("Task (unobserved)", args.Exception);
args.SetObserved();
};
}
private static void ZalogujChybu(string zdroj, Exception? ex)
{
try
{
string radek = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {zdroj}: {ex?.ToString() ?? "(neznámá výjimka)"}"
+ Environment.NewLine + Environment.NewLine;
File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "error.log"), radek, Encoding.UTF8);
}
catch { /* read-only složka apod. MessageBox se ukáže i tak */ }
}
private static void UkazChybu(Exception ex) => MessageBox.Show(
"Nastala neočekávaná chyba, aplikace ale běží dál doporučujeme uložit projekt.\n\n" +
$"{ex.Message}\n\nPodrobnosti jsou v souboru error.log vedle aplikace.",
"Neočekávaná chyba", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
+10
View File
@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
+64
View File
@@ -0,0 +1,64 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace Rozpisky.Behaviors;
/// <summary>
/// Delete na needitovaném výběru buněk <see cref="DataGrid"/>u vymaže obsah textových buněk
/// (jako v Excelu), bez vstupu do edit módu. Needitovatelné a netextové sloupce (checkboxy,
/// auto „Jméno rozvržení") nechá být; při otevřené editaci nechá klávesu projít jako mazání
/// textu. Zapíná se per-grid: <c>beh:GridDelete.Enabled="True"</c>.
/// </summary>
public static class GridDelete
{
public static readonly DependencyProperty EnabledProperty =
DependencyProperty.RegisterAttached("Enabled", typeof(bool), typeof(GridDelete),
new PropertyMetadata(false, OnEnabledChanged));
public static void SetEnabled(DependencyObject o, bool v) => o.SetValue(EnabledProperty, v);
public static bool GetEnabled(DependencyObject o) => (bool)o.GetValue(EnabledProperty);
private static void OnEnabledChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is not DataGrid grid) return;
if ((bool)e.NewValue) grid.PreviewKeyDown += OnKeyDown;
else grid.PreviewKeyDown -= OnKeyDown;
}
private static void OnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Delete || sender is not DataGrid grid) return;
if (IsEditing(e.OriginalSource)) return;
if (ClearSelectedCells(grid)) e.Handled = true;
}
/// <summary>Vymaže obsah vybraných textových buněk. Vrací true, když se aspoň jedna dala
/// vymazat tj. klávesa se má považovat za zpracovanou.</summary>
private static bool ClearSelectedCells(DataGrid grid)
{
if (grid.IsReadOnly) return false;
bool any = false;
foreach (var info in grid.SelectedCells)
{
if (!info.IsValid || info.Item is null || info.Column is null || info.Column.IsReadOnly) continue;
if (info.Item == CollectionView.NewItemPlaceholder) continue;
if (PathOf(info.Column) is not { } path) continue;
var pi = info.Item.GetType().GetProperty(path);
if (pi is null || !pi.CanWrite || pi.PropertyType != typeof(string)) continue;
pi.SetValue(info.Item, string.Empty);
any = true;
}
return any;
}
/// <summary>Je fokus v editačním prvku buňky (probíhá editace)? Pak Delete necháme projít jako mazání textu.</summary>
private static bool IsEditing(object? source)
=> source is System.Windows.Controls.Primitives.TextBoxBase or ComboBox;
/// <summary>Cesta vlastnosti pro sloupec: přímá Binding (text/checkbox) nebo ClipboardContentBinding (šablona).</summary>
private static string? PathOf(DataGridColumn? col)
=> (col as DataGridBoundColumn)?.Binding is Binding b ? b.Path.Path
: (col?.ClipboardContentBinding as Binding)?.Path.Path;
}
+237
View File
@@ -0,0 +1,237 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace Rozpisky.Behaviors;
/// <summary>
/// Schránka pro <see cref="DataGrid"/> (Ctrl+C / Ctrl+V) přes <c>ClipboardContentBinding</c> sloupců
/// WPF DataGrid vkládání nativně neumí a jeho kopírování se v jiných aplikacích (Excel) nemusí projevit
/// (data dává na schránku bez „flush"). Zapíná se per-grid: <c>beh:GridPaste.Enabled="True"</c>.
///
/// - <b>Ctrl+C</b>: obálku výběru zkopíruje jako TSV (tab mezi sloupci, CRLF mezi řádky) a přes
/// <see cref="Clipboard.SetText(string)"/> (s persistem) Excel i vlastní grid ji pak převezmou.
/// - <b>Ctrl+V</b>: schránku (TSV) vloží do buněk. Při vícebuňkovém výběru se oblast výběru vyplní
/// dlaždicově (vzor ze schránky se opakuje jako fill handle); jinak se blok vloží od aktivní buňky
/// doprava/dolů. Řádky se nepřidávají (přetečení se zahodí), mimo rozsah sloupců i read-only sloupce
/// se přeskočí. Zápis jde přes <see cref="DataGridColumn.OnPastingCellClipboardContent"/> (bez edit
/// módu), nekonvertibilní hodnota buňku tiše vynechá shodně s <c>Theming.GridFill</c>.
/// - <b>Ctrl+D</b>: „fill down" jako v Excelu při výběru přes víc řádků vyplní horní řádek výběru
/// dolů do zbytku výběru; u výběru v jednom řádku zkopíruje hodnoty z řádku nad ním. Kopíruje se
/// přes ClipboardContentBinding sloupce (bez edit módu), stejně jako fill handle.
///
/// Při otevřené editaci buňky se klávesa nechá projít (kopíruje/vkládá se text v editoru). Bez
/// CSV-uvozovkového escapování a bez Ctrl+Z shodně s fill handle.
/// </summary>
public static class GridPaste
{
public static readonly DependencyProperty EnabledProperty =
DependencyProperty.RegisterAttached("Enabled", typeof(bool), typeof(GridPaste),
new PropertyMetadata(false, OnEnabledChanged));
public static void SetEnabled(DependencyObject o, bool v) => o.SetValue(EnabledProperty, v);
public static bool GetEnabled(DependencyObject o) => (bool)o.GetValue(EnabledProperty);
private static void OnEnabledChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is not DataGrid grid) return;
if ((bool)e.NewValue) grid.PreviewKeyDown += OnKeyDown;
else grid.PreviewKeyDown -= OnKeyDown;
}
private static void OnKeyDown(object sender, KeyEventArgs e)
{
if ((Keyboard.Modifiers & ModifierKeys.Control) == 0) return;
if (sender is not DataGrid grid) return;
if (IsEditing(e.OriginalSource)) return; // rozeditovaná buňka → schránku řeší editor
if (e.Key == Key.C) { if (Copy(grid)) e.Handled = true; }
else if (e.Key == Key.V) { if (Paste(grid)) e.Handled = true; }
else if (e.Key == Key.D) { if (FillDown(grid)) e.Handled = true; }
}
// ---------------------------------------------------------------- kopírování
/// <summary>Zkopíruje obálku výběru jako TSV na schránku (s persistem, ať ji převezme i Excel).</summary>
private static bool Copy(DataGrid grid)
{
if (SelectionBounds(grid) is not (int r0, int r1, int c0, int c1)) return false;
var cols = ColumnsByDisplay(grid);
var sb = new StringBuilder();
for (int r = r0; r <= r1; r++)
{
if (r < 0 || r >= grid.Items.Count) continue;
var item = grid.Items[r];
if (item == CollectionView.NewItemPlaceholder) continue;
for (int c = c0; c <= c1; c++)
{
if (c > c0) sb.Append('\t');
if (c >= 0 && c < cols.Count)
sb.Append(cols[c].OnCopyingCellClipboardContent(item)?.ToString());
}
sb.Append("\r\n");
}
try { Clipboard.SetText(sb.ToString(), TextDataFormat.UnicodeText); return true; }
catch { return false; } // schránku drží jiný proces → nech případně proběhnout nativní kopii
}
// ---------------------------------------------------------------- vkládání
private static bool Paste(DataGrid grid)
{
if (grid.IsReadOnly) return false;
var text = Clipboard.ContainsText() ? Clipboard.GetText() : null;
if (string.IsNullOrEmpty(text)) return false;
grid.CommitEdit(DataGridEditingUnit.Row, true);
var data = ParseTsv(text);
if (data.Count == 0) return false;
var cols = ColumnsByDisplay(grid);
// Vícebuňkový výběr → vyplň jeho obálku dlaždicově vzorem ze schránky (jako fill handle);
// 1×1 schránka tím vyplní celý výběr, 1×N se v širším výběru opakuje.
if (SelectionBounds(grid) is (int r0, int r1, int c0, int c1) && (r1 > r0 || c1 > c0))
{
for (int r = r0; r <= r1; r++)
{
if (r < 0 || r >= grid.Items.Count) continue;
var item = grid.Items[r];
if (item == CollectionView.NewItemPlaceholder) continue;
var src = data[Mod(r - r0, data.Count)];
if (src.Length == 0) continue;
for (int c = c0; c <= c1; c++)
if (c >= 0 && c < cols.Count)
WriteCell(cols[c], item, src[Mod(c - c0, src.Length)]);
}
return true;
}
// Jedna buňka (nebo bez výběru) → vlož celý blok od aktivní buňky doprava/dolů.
if (Anchor(grid) is not (int ar, int ac)) return false;
for (int i = 0; i < data.Count; i++)
{
int r = ar + i;
if (r >= grid.Items.Count) break;
var item = grid.Items[r];
if (item == CollectionView.NewItemPlaceholder) continue;
var src = data[i];
for (int j = 0; j < src.Length; j++)
{
int c = ac + j;
if (c >= 0 && c < cols.Count) WriteCell(cols[c], item, src[j]);
}
}
return true;
}
// ---------------------------------------------------------------- fill down (Ctrl+D)
/// <summary>Excelovské „fill down": vícebuňkový výběr přes řádky vyplní horní řádek výběru dolů;
/// výběr v jediném řádku převezme hodnoty z řádku nad ním. Kopíruje se přes ClipboardContentBinding
/// sloupce (bez edit módu). Vrací true, když se aspoň něco dalo zapsat.</summary>
private static bool FillDown(DataGrid grid)
{
if (grid.IsReadOnly) return false;
if (SelectionBounds(grid) is not (int r0, int r1, int c0, int c1)) return false;
grid.CommitEdit(DataGridEditingUnit.Row, true);
var cols = ColumnsByDisplay(grid);
// Výběr v jednom řádku → zdroj je řádek nad výběrem (jinak horní řádek výběru).
int srcRow = r1 > r0 ? r0 : r0 - 1;
if (srcRow < 0 || srcRow >= grid.Items.Count) return false;
var srcItem = grid.Items[srcRow];
if (srcItem == CollectionView.NewItemPlaceholder) return false;
bool any = false;
for (int r = r1 > r0 ? r0 + 1 : r0; r <= r1; r++)
{
if (r < 0 || r >= grid.Items.Count) continue;
var item = grid.Items[r];
if (item == CollectionView.NewItemPlaceholder) continue;
for (int c = c0; c <= c1; c++)
if (c >= 0 && c < cols.Count && CopyCell(cols[c], srcItem, item)) any = true;
}
return any;
}
/// <summary>Zkopíruje hodnotu buňky ze zdrojové do cílové položky přes clipboard-binding sloupce
/// (bez edit módu). Read-only sloupec a nekonvertibilní hodnotu tiše vynechá. Vrací true při zápisu.</summary>
private static bool CopyCell(DataGridColumn column, object srcItem, object dstItem)
{
if (column.IsReadOnly) return false;
try { column.OnPastingCellClipboardContent(dstItem, column.OnCopyingCellClipboardContent(srcItem)); return true; }
catch { return false; }
}
/// <summary>Zapíše hodnotu do buňky přes clipboard-binding sloupce (bez edit módu). Read-only sloupec
/// a nekonvertibilní hodnotu tiše vynechá stejně jako <c>GridFill.CopyCell</c>.</summary>
private static void WriteCell(DataGridColumn column, object item, string value)
{
if (column.IsReadOnly) return;
try { column.OnPastingCellClipboardContent(item, value); }
catch { /* nekonvertibilní hodnota (např. text do bool/čísla) → buňku vynech */ }
}
// ---------------------------------------------------------------- pomocné
/// <summary>Obálka výběru: min/max index řádku a min/max DisplayIndex sloupce, nebo null.
/// (DisplayIndex je souvislý 0..n-1, takže odpovídá indexu do <see cref="ColumnsByDisplay"/>.)</summary>
private static (int r0, int r1, int c0, int c1)? SelectionBounds(DataGrid grid)
{
var cells = grid.SelectedCells;
if (cells.Count == 0) return null;
int r0 = int.MaxValue, r1 = -1, c0 = int.MaxValue, c1 = -1;
var rowIndex = new Dictionary<object, int>(); // Items.IndexOf je O(n), memoizuj
foreach (var cell in cells)
{
if (!cell.IsValid || cell.Column is null || cell.Item is null) continue;
if (cell.Item == CollectionView.NewItemPlaceholder) continue;
if (!rowIndex.TryGetValue(cell.Item, out int r))
rowIndex[cell.Item] = r = grid.Items.IndexOf(cell.Item);
if (r < 0) continue;
if (r < r0) r0 = r;
if (r > r1) r1 = r;
int c = cell.Column.DisplayIndex;
if (c < c0) c0 = c;
if (c > c1) c1 = c;
}
return r1 < 0 ? null : (r0, r1, c0, c1);
}
/// <summary>Kotva vkládání bloku: aktivní buňka (řádek, DisplayIndex sloupce), fallback první vybraná.</summary>
private static (int r, int c)? Anchor(DataGrid grid)
{
var cur = grid.CurrentCell;
if (cur.IsValid && cur.Column is not null && cur.Item is not null
&& cur.Item != CollectionView.NewItemPlaceholder)
{
int r = grid.Items.IndexOf(cur.Item);
if (r >= 0) return (r, cur.Column.DisplayIndex);
}
return SelectionBounds(grid) is (int r0, _, int c0, _) ? (r0, c0) : null;
}
private static List<DataGridColumn> ColumnsByDisplay(DataGrid grid)
=> grid.Columns.OrderBy(c => c.DisplayIndex).ToList();
/// <summary>Rozparsuje TSV: řádky dle CR/LF, buňky dle tabulátoru. Koncový prázdný řádek
/// (Excel přidává závěrečné CRLF) se zahodí.</summary>
private static List<string[]> ParseTsv(string text)
{
var radky = text.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n').ToList();
while (radky.Count > 0 && radky[^1].Length == 0) radky.RemoveAt(radky.Count - 1);
return radky.Select(r => r.Split('\t')).ToList();
}
private static int Mod(int a, int m) => ((a % m) + m) % m;
/// <summary>Je fokus v editačním prvku buňky? Pak Ctrl+C/V necháme na editoru.</summary>
private static bool IsEditing(object? source)
=> source is System.Windows.Controls.Primitives.TextBoxBase or ComboBox;
}
+19
View File
@@ -0,0 +1,19 @@
namespace Rozpisky.Cad;
/// <summary>
/// Jeden podporovaný CAD systém ProgID pro COM attach a popisek do UI (PODKLAD_CAD_Layout_Tisk.md §1).
/// <para>
/// ProgID může (a někdy musí) být <b>verzově konkrétní</b>: verzově nezávislé „ZWCAD.Application" se u
/// některých instalací nenapojí na běžící instanci (<c>CLSIDFromProgID</c> vrátí CLSID, pod kterým
/// instance není v Running Object Table), zatímco „ZWCAD.Application.2023" míří rovnou na správnou třídu.
/// Proto seed v <see cref="CadEnginesStore"/> nabízí i verzované položky a UI má režim „Manuálně" na
/// vlastní ProgID (Nastavení → CAD).
/// </para>
/// </summary>
public sealed class CadEngine
{
public string ProgId { get; set; } = "";
public string Nazev { get; set; } = "";
public override string ToString() => Nazev;
}
+101
View File
@@ -0,0 +1,101 @@
using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json;
using Rozpisky.Data;
namespace Rozpisky.Cad;
/// <summary>
/// Načítání/ukládání seznamu podporovaných CAD systémů + naposledy vybraného enginu do JSON souboru
/// <b>vedle .exe</b> (portable). Stejný princip jako <see cref="Data.CiselnikyStore"/> /
/// <see cref="Dxf.MapovaniStore"/>: CAD je vlastnost stroje (ne zakázky), takže výběr žije mimo
/// projekt a je globální pro celou instalaci.
/// </summary>
public static class CadEnginesStore
{
public const string SouborNazev = "cad.json";
private static readonly JsonSerializerOptions Opts = new()
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
/// <summary>Výchozí umístění: soubor vedle .exe (portable).</summary>
public static string DefaultPath => Path.Combine(AppContext.BaseDirectory, SouborNazev);
/// <summary>Uložený výběr CADu: buď předdefinovaný engine (<see cref="VybranyProgId"/>), nebo
/// ruční režim „Manuálně" (<see cref="RucniRezim"/> + <see cref="RucniProgId"/>). Režim a ruční
/// text se drží zvlášť, aby se po startu obnovil přesně stav UI včetně (i prázdného) inputboxu
/// a aby ruční hodnota shodná s předdefinovaným ProgID nepřeklopila UI zpět do comboboxu.</summary>
public readonly record struct Vyber(string? VybranyProgId, bool RucniRezim, string? RucniProgId);
/// <summary>Načte seznam enginů a uložený výběr. Když soubor chybí/je poškozený, vrátí vestavěný
/// <see cref="Seed"/> (a rovnou ho uloží, aby šel soubor dál ručně upravovat).</summary>
public static (List<CadEngine> Engines, Vyber Vyber) Load(string? path = null)
{
path ??= DefaultPath;
try
{
if (File.Exists(path))
{
var data = JsonSerializer.Deserialize<Dto>(File.ReadAllText(path), Opts);
if (data?.Engines is { Count: > 0 })
{
// „Mrtvý“ ProgId (ručně upravený soubor) → žádný výběr comboboxu, ať nedrží
// engine, který v seznamu není. Ruční režim tím netknutý.
var vybrany = data.Engines.Any(e => e.ProgId == data.VybranyProgId)
? data.VybranyProgId : null;
return (data.Engines, new Vyber(vybrany, data.RucniRezim, data.RucniProgId));
}
// Soubor existuje, ale nenese žádný engine („null" / prázdný seznam) → stejné
// zacházení jako s rozbitým JSON, viz catch níže.
JsonConfigFile.ZalohujAOhlasPoskozeny(path);
}
}
catch (Exception ex) when (ex is IOException or JsonException or UnauthorizedAccessException)
{
// Poškozený / nečitelný soubor → fallback na seed. Soubor se hned dole přepíše seedem,
// proto se originál (třeba ručně dopsané ProgID) nejdřív zazálohuje a uživatel dozví.
JsonConfigFile.ZalohujAOhlasPoskozeny(path, ex);
}
var seed = Seed();
Save(seed, default, path);
return (seed, default);
}
/// <summary>Uloží seznam enginů + výběr (režim comboboxu vs. Manuálně a ruční text) atomicky
/// přes .tmp, aby pád uprostřed zápisu soubor nepoškodil. Chyby zápisu (např. read-only složka
/// u .exe v Program Files) propustí volajícímu.</summary>
public static void Save(List<CadEngine> engines, Vyber vyber, string? path = null)
=> JsonConfigFile.ZapisAtomicky(path ?? DefaultPath, JsonSerializer.Serialize(
new Dto
{
Engines = engines,
VybranyProgId = vyber.VybranyProgId,
RucniRezim = vyber.RucniRezim,
RucniProgId = vyber.RucniProgId,
}, Opts));
/// <summary>Vestavěný výchozí seznam CADů (databáze od programátora, PODKLAD_CAD_Layout_Tisk.md §1:
/// pořadí AutoCAD → ZWCAD → BricsCAD). Vedle verzově nezávislých ProgID („poslední nainstalovaná")
/// jsou i verzově konkrétní položky verzově nezávislé „ZWCAD.Application" se u některých instalací
/// na běžící instanci nenapojí, „ZWCAD.Application.2023" ano. Kdo potřebuje jinou verzi, dopíše ji
/// ručně přes režim „Manuálně" v UI (nebo do cad.json). Soubor jde kdykoli ručně upravit.</summary>
public static List<CadEngine> Seed() =>
[
new() { ProgId = "AutoCAD.Application", Nazev = "AutoCAD (poslední)" },
new() { ProgId = "ZWCAD.Application", Nazev = "ZWCAD (poslední)" },
new() { ProgId = "ZWCAD.Application.2023", Nazev = "ZWCAD 2023" },
new() { ProgId = "ZWCAD.Application.2022", Nazev = "ZWCAD 2022" },
new() { ProgId = "BricscadApp.AcadApplication", Nazev = "BricsCAD" },
];
private sealed class Dto
{
public List<CadEngine> Engines { get; set; } = new();
public string? VybranyProgId { get; set; }
public bool RucniRezim { get; set; }
public string? RucniProgId { get; set; }
}
}
+612
View File
@@ -0,0 +1,612 @@
using System.Globalization;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
using Rozpisky.Models;
namespace Rozpisky.Cad;
/// <summary>
/// „Aktualizuj rozvržení" port <c>modAktualizace</c> + <c>modPlotConfig</c>
/// (PODKLAD_CAD_Layout_Tisk.md §34) nad živým <see cref="CadLink.Doc"/> (dynamic COM, late-binding,
/// stejně jako zbytek CAD vrstvy). Vybrané řádky seznamu příloh se mapují <b>pozičně</b> na rozvržení
/// výkresu počínaje aktivním layoutem (ne podle jména) přesně jako originální VBA. Každý dílčí krok
/// je obalený vlastním <c>try/catch</c>: chyba se zaloguje a dávka pokračuje dál, nikdy nepadá jako celek.
/// <para>
/// Volání COM na CAD zůstává na UI (STA) vlákně, stejně jako zbytek COM interopu v aplikaci (viz
/// <c>MainViewModel.ExportujDoPdf</c>) mezi jednotlivými rozvrženími se <c>await Dispatcher.Yield</c>
/// nechá překreslit log okno a stavový řádek, žádný <c>Task.Run</c>.
/// </para>
/// </summary>
public static class CadLayoutService
{
private const string TmpPlotConfigName = "__tmpQ";
public static async Task AktualizujRozvrzeni(dynamic doc, string cadRodina, IReadOnlyList<Priloha> vybrane,
Projekt projekt, Nastaveni nastaveni, LayoutConfig cfg, ILayoutLog log, CancellationToken zruseno)
{
if (vybrane.Count == 0) { log.Chyba("Není vybraný žádný řádek seznamu."); return; }
List<(string Name, int TabOrder, dynamic Layout)> layouty;
try { layouty = NactiLayouty(doc); }
catch (Exception ex) { log.Chyba($"Načtení rozvržení výkresu selhalo: {ex.Message}"); return; }
var rozvrzeni = layouty.Where(l => l.TabOrder > 0).OrderBy(l => l.TabOrder).ToList();
if (rozvrzeni.Count == 0) { log.Chyba("Výkres nemá žádné rozvržení (jen modelový prostor)."); return; }
string aktivniJmeno;
try { aktivniJmeno = (string)doc.ActiveLayout.Name; }
catch (Exception ex) { log.Chyba($"Nelze zjistit aktivní rozvržení: {ex.Message}"); return; }
if (string.Equals(aktivniJmeno, "Model", StringComparison.OrdinalIgnoreCase))
{
log.Chyba("Aktivní je modelový prostor ve výkresu aktivujte rozvržení, od kterého se má " +
"aktualizace začít, a zkuste to znovu.");
return;
}
int startIdx = rozvrzeni.FindIndex(l => string.Equals(l.Name, aktivniJmeno, StringComparison.OrdinalIgnoreCase));
if (startIdx < 0) { log.Chyba($"Aktivní rozvržení „{aktivniJmeno}“ se mezi rozvrženími výkresu nenašlo."); return; }
if (vybrane.Count > rozvrzeni.Count - startIdx)
{
log.Chyba($"Vybráno {vybrane.Count} řádků, ale od aktivního rozvržení „{aktivniJmeno}“ zbývá " +
$"ve výkresu jen {rozvrzeni.Count - startIdx} rozvržení.");
return;
}
var cil = rozvrzeni.Skip(startIdx).Take(vybrane.Count).ToList();
// Kvůli identifikaci pseudoviewportu „papíru“ (viz níže) se každé rozvržení při zpracování
// aktivuje po dávce se vrátí to, které bylo aktivní na začátku (je to zároveň startovní
// značka pozičního mapování, uživatel by o ni neměl přijít).
dynamic puvodniAktivni;
try { puvodniAktivni = doc.ActiveLayout; }
catch (Exception ex) { log.Chyba($"Nelze přečíst aktivní rozvržení: {ex.Message}"); return; }
// Dvoufázové přejmenování (kolize jmen) nejdřív všechny na dočasný náhodný název.
for (int i = 0; i < cil.Count; i++)
{
var docasne = "__tmp_" + Guid.NewGuid().ToString("N");
try { cil[i].Layout.Name = docasne; }
catch (Exception ex) { log.Chyba($"Dočasné přejmenování rozvržení „{cil[i].Name}“ selhalo: {ex.Message}"); }
}
// Dvoufázové přejmenování výše uvolní jen jména rozvržení ve zpracovávané dávce (cil). Rozvržení
// MIMO dávku (před aktivním nebo za oknem výběru) si svá jména drží když se na některé z nich
// trefí cílové jméno, CAD rename odmítne s eDuplicateRecordName (0x802000D9). Stejně tak dvě řádky
// se stejným cílovým jménem: první projde, druhý spadne na duplicitu. Obojí odchytíme předem a
// vypíšeme srozumitelnou chybu místo kryptického kódu (řádek se přeskočí, dávka pokračuje).
var cilPuvodniJmena = new HashSet<string>(cil.Select(c => c.Name), StringComparer.OrdinalIgnoreCase);
var existujiciMimoDavku = new HashSet<string>(
layouty.Where(l => !cilPuvodniJmena.Contains(l.Name)).Select(l => l.Name),
StringComparer.OrdinalIgnoreCase);
var jizPrirazeno = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < cil.Count; i++)
{
if (zruseno.IsCancellationRequested) { log.Info("Zrušeno uživatelem aktualizace ukončena."); break; }
await Dispatcher.Yield(DispatcherPriority.Background); // nech log okno/stavový řádek překreslit
var p = vybrane[i];
dynamic layout = cil[i].Layout;
var cilJmeno = p.JmenoRozvrzeni?.Trim();
if (string.IsNullOrWhiteSpace(cilJmeno))
{
log.Chyba($"Řádek {i + 1} (dřívější rozvržení „{cil[i].Name}“): chybí jméno rozvržení, přeskočeno.");
continue;
}
if (jizPrirazeno.Contains(cilJmeno))
{
log.Chyba($"Řádek {i + 1}: jméno rozvržení „{cilJmeno}“ je zadané na víc řádcích přeskočeno.");
continue;
}
if (existujiciMimoDavku.Contains(cilJmeno))
{
log.Chyba($"„{cilJmeno}“: toto jméno už ve výkresu nese jiné rozvržení mimo zpracovávanou dávku " +
"přejmenování by skončilo chybou (eDuplicateRecordName / 0x802000D9), řádek přeskočen. " +
"Přejmenujte nebo odstraňte kolidující rozvržení, případně ho zahrňte do výběru.");
continue;
}
try { layout.Name = cilJmeno; }
catch (Exception ex) { log.Chyba($"Přejmenování na „{cilJmeno}“ selhalo: {ex.Message}"); continue; }
log.Info($"„{cilJmeno}“: přejmenováno.");
jizPrirazeno.Add(cilJmeno);
// LTSCALE se vyhodnocuje a zapisuje do seznamu vždy (nezávisle na Nastaveni.MeritkoAutomaticky
// i Nastaveni.LtscaleAutomaticky) samotný zápis do seznamu nic nemění na výkresu, ovlivní
// až případné použití při tisku (CadTiskService, tam je to za přepínačem LtscaleAutomaticky).
// Pseudoviewport „papíru“ umí CAD jednoznačně označit jen u aktivního rozvržení
// (ActivePViewport při vypnutém MSPACE) proto se rozvržení aktivuje a jeho handle
// se předá dál k vyloučení. Když se to nepovede, NajdiJmenovateleMeritka má fallback.
string? papirHandle = null;
try
{
doc.ActiveLayout = layout;
try { doc.MSpace = false; } catch { /* už je vypnutý */ }
papirHandle = (string)doc.ActivePViewport.Handle;
}
catch { /* fallback: heuristika StandardScale v NajdiJmenovateleMeritka */ }
try
{
// doc je dynamic → volání se váže dynamicky a vrací dynamic (nejde dekonstruovat,
// pojmenované prvky tuple za běhu neexistují) proto do explicitně otypované proměnné.
(SortedSet<long> Vsechny, long? Prvni) meritkaVyrezu =
NajdiJmenovateleMeritka(doc, layout, cfg.HladinaVyrezu, papirHandle);
if (nastaveni.MeritkoAutomaticky)
{
var meritka = VyhodnotMeritka(meritkaVyrezu.Vsechny);
if (meritka is not null)
{
p.Meritko = meritka;
log.Info($"„{cilJmeno}“: měřítko {meritka}.");
}
}
var ltscale = VyhodnotLtscale(meritkaVyrezu.Prvni);
if (ltscale is not null)
{
p.Ltscale = ltscale;
log.Info($"„{cilJmeno}“: LTSCALE {ltscale}.");
}
}
catch (Exception ex) { log.Chyba($"„{cilJmeno}“: vyhodnocení měřítka/LTSCALE selhalo: {ex.Message}"); }
(double W, double H)? papirMm = null;
try { papirMm = ZpracujBlokyEntit(layout, projekt, p, cfg, log, cilJmeno); }
catch (Exception ex) { log.Chyba($"„{cilJmeno}“: procházení entit rozvržení selhalo: {ex.Message}"); }
if (nastaveni.UpravovatTiskarnu)
{
try { NastavTiskLayoutu(doc, layout, cadRodina, p, cfg, papirMm, log, cilJmeno); }
catch (Exception ex) { log.Chyba($"„{cilJmeno}“: nastavení tiskárny selhalo: {ex.Message}"); }
}
}
try { doc.ActiveLayout = puvodniAktivni; }
catch (Exception ex) { log.Chyba($"Návrat na původní aktivní rozvržení selhal: {ex.Message}"); }
}
private static List<(string Name, int TabOrder, dynamic Layout)> NactiLayouty(dynamic doc)
{
var vysledek = new List<(string, int, dynamic)>();
foreach (dynamic layout in doc.Layouts)
vysledek.Add(((string)layout.Name, Convert.ToInt32(layout.TabOrder), layout));
return vysledek;
}
/// <summary>Projde entity rozvržení (blokové reference) vyplní blok hlavičky a z bloku rámečku
/// spočítá počet papírů (zapíše zpět do <see cref="Priloha.Formaty"/>). Vrací rozměr papíru v mm
/// odvozený z rámečku (pro nastavení tiskárny), nebo <c>null</c>, když rámeček nebyl nalezen.</summary>
private static (double W, double H)? ZpracujBlokyEntit(dynamic layout, Projekt projekt, Priloha p,
LayoutConfig cfg, ILayoutLog log, string cilJmeno)
{
(double W, double H)? papir = null;
bool hlavickaNalezena = false;
foreach (dynamic ent in layout.Block)
{
string objName;
try { objName = (string)ent.ObjectName; } catch { continue; }
if (objName != "AcDbBlockReference") continue;
string jmeno;
try { jmeno = (string)ent.EffectiveName; }
catch { try { jmeno = (string)ent.Name; } catch { continue; } }
if (string.Equals(jmeno, cfg.BlokHlavicky, StringComparison.OrdinalIgnoreCase))
{
hlavickaNalezena = true;
ZapisHlavicku(ent, projekt, p, log, cilJmeno);
}
else if (string.Equals(jmeno, cfg.BlokRamecek, StringComparison.OrdinalIgnoreCase))
{
bool dynamickyBlok;
try { dynamickyBlok = (bool)ent.IsDynamicBlock; } catch { dynamickyBlok = false; }
if (!dynamickyBlok) continue;
object[]? vlastnosti;
try { vlastnosti = (object[])ent.GetDynamicBlockProperties(); }
catch (Exception ex)
{
log.Chyba($"„{cilJmeno}“: čtení dynamických vlastností rámečku „{cfg.BlokRamecek}“ selhalo: {ex.Message}");
continue;
}
var viditelnost = NajdiViditelnost(vlastnosti);
if (viditelnost is null)
{
log.Chyba($"„{cilJmeno}“: u rámečku „{cfg.BlokRamecek}“ se nepodařilo zjistit viditelnost (Benefit/Libovolný).");
continue;
}
// Viditelnost "Benefit"/"Libovolný" určuje, které dvojici parametrů (VzdálenostBx/By vs.
// VzdálenostLx/Ly) věřit bounding box bloku je nespolehlivý (odvisí od toho, co je v bloku
// zrovna vidět), zatímco tyto parametry nesou zamýšlený rozměr rámečku přímo.
string pripona = string.Equals(viditelnost, "Benefit", StringComparison.OrdinalIgnoreCase) ? "B" : "L";
double? dx = NajdiHodnotu(vlastnosti, $"Vzdálenost{pripona}x");
double? dy = NajdiHodnotu(vlastnosti, $"Vzdálenost{pripona}y");
if (dx is null || dy is null)
{
log.Chyba($"„{cilJmeno}“: rámeček (viditelnost {viditelnost}) nemá parametr Vzdálenost{pripona}x/Vzdálenost{pripona}y.");
continue;
}
int delka = Math.Max(1, (int)Math.Ceiling(dx.Value / cfg.SirkaA4Mm));
int vyska = Math.Max(1, (int)Math.Ceiling(dy.Value / cfg.VyskaA4Mm));
int pocet = delka * vyska;
p.Formaty = pocet.ToString();
papir = (dx.Value, dy.Value); // reálný rozměr rámečku (podklad §2 „NastavRozmerPapiru" ne zaokrouhleno na A4)
log.Info($"„{cilJmeno}“: počet papírů {pocet} ({delka}×{vyska} A4, viditelnost {viditelnost}, rámeček {dx.Value:0}×{dy.Value:0} mm).");
}
}
if (!hlavickaNalezena)
log.Chyba($"„{cilJmeno}“: hlavička nevyplněna blok hlavičky „{cfg.BlokHlavicky}“ nebyl " +
"v rozvržení nalezen (žádný fallback).");
return papir;
}
/// <summary>Najde mezi dynamickými vlastnostmi bloku tu, jejíž aktuální hodnota odpovídá jednomu
/// z názvů viditelnosti rámečku ("Benefit"/"Libovolný") tím se pozná, který stav je zrovna aktivní,
/// aniž by bylo nutné znát přesný název vlastnosti viditelnosti (např. "Visibility1").</summary>
private static string? NajdiViditelnost(object[] vlastnosti)
{
foreach (dynamic v in vlastnosti)
{
string? hodnota;
try { hodnota = v.Value as string; } catch { continue; }
if (hodnota is null) continue;
if (string.Equals(hodnota, "Benefit", StringComparison.OrdinalIgnoreCase) ||
string.Equals(hodnota, "Libovolný", StringComparison.OrdinalIgnoreCase))
return hodnota;
}
return null;
}
/// <summary>Přečte hodnotu konkrétní dynamické vlastnosti bloku podle jejího názvu (<c>PropertyName</c>).</summary>
private static double? NajdiHodnotu(object[] vlastnosti, string nazevVlastnosti)
{
foreach (dynamic v in vlastnosti)
{
string nazev;
try { nazev = (string)v.PropertyName; } catch { continue; }
if (!string.Equals(nazev, nazevVlastnosti, StringComparison.OrdinalIgnoreCase)) continue;
try { return Convert.ToDouble(v.Value); } catch { return null; }
}
return null;
}
private static void ZapisHlavicku(dynamic ent, Projekt projekt, Priloha p, ILayoutLog log, string cilJmeno)
{
bool maAtributy;
try { maAtributy = (bool)ent.HasAttributes; } catch { maAtributy = false; }
if (!maAtributy) return;
object[] atributy;
try { atributy = (object[])ent.GetAttributes(); }
catch (Exception ex) { log.Chyba($"„{cilJmeno}“: čtení atributů hlavičky selhalo: {ex.Message}"); return; }
foreach (dynamic att in atributy)
{
string tag;
try { tag = (string)att.TagString; } catch { continue; }
string? hodnota = tag.ToUpperInvariant() switch
{
"STAVBA" => projekt.Stavba,
// OBJEKT = číslo objektu + mezera + název objektu; číslo nemusí být vyplněné, pak žádná mezera navíc.
"OBJEKT" => SpojNeprazdne(" ", p.CisloObjektu, p.NazevObjektu),
// PRILOHA = název přílohy název dílčí části přílohy; když jedna část chybí, pomlčka „nelevituje".
"PRILOHA" => SpojNeprazdne(" ", p.NazevPrilohy, p.NazevDilciCastiPrilohy),
"MERITKO" => FormatMeritko(p.Meritko),
_ => null,
};
if (hodnota is null) continue;
try { att.TextString = hodnota; att.Update(); }
catch (Exception ex) { log.Chyba($"„{cilJmeno}“: zápis atributu {tag} selhal: {ex.Message}"); }
}
log.Info($"„{cilJmeno}“: hlavička vyplněna.");
}
/// <summary>„M 1:500" jen když hodnota obsahuje „1:" (podklad §4) jinak (prázdné, „nezjištěno" apod.) beze změny.</summary>
private static string FormatMeritko(string meritko) =>
!string.IsNullOrEmpty(meritko) && meritko.Contains("1:") ? $"M {meritko}" : meritko;
/// <summary>Spojí části <paramref name="oddelovac"/>em, ale prázdné/whitespace části vynechá takže
/// oddělovač nikdy „nelevituje" u chybějící části (např. samotné číslo objektu bez názvu, nebo název
/// přílohy bez dílčí části).</summary>
private static string SpojNeprazdne(string oddelovac, params string?[] casti) =>
string.Join(oddelovac, casti.Where(c => !string.IsNullOrWhiteSpace(c)).Select(c => c!.Trim()));
/// <summary>Projde viewporty v hladině <paramref name="hladinaVyrezu"/> a z <c>CustomScale</c> spočítá
/// jmenovatele měřítka (dle <c>INSUNITS</c>: mm→1, m→1000). Vrací dvojici: <c>Vsechny</c> = unikátní
/// seřazená množina pro <see cref="VyhodnotMeritka"/> (formát „1:X"), <c>Prvni</c> = jmenovatel
/// prvního výřezu v pořadí iterace bloku pro <see cref="VyhodnotLtscale"/> (LTSCALE je vlastnost
/// celého výkresu → jedna hodnota). Sdílené, ať se výkres neprochází dvakrát.</summary>
private static (SortedSet<long> Vsechny, long? Prvni) NajdiJmenovateleMeritka(dynamic doc, dynamic layout,
string hladinaVyrezu, string? papirHandle)
{
int jednotkyFaktor;
try { jednotkyFaktor = Convert.ToInt32(doc.GetVariable("INSUNITS")) == 6 ? 1000 : 1; }
catch { jednotkyFaktor = 1; }
var jmenovatele = new SortedSet<long>();
long? prvni = null;
foreach (dynamic ent in layout.Block)
{
string objName;
try { objName = (string)ent.ObjectName; } catch { continue; }
if (objName != "AcDbViewport") continue;
string vrstva;
try { vrstva = (string)ent.Layer; } catch { continue; }
if (!string.Equals(vrstva, hladinaVyrezu, StringComparison.OrdinalIgnoreCase)) continue;
// Papírový prostor obsahuje kromě nakreslených výřezů i pseudoviewport „pohledu na papír“
// (v DXF VIEWPORT s id 1; vzniká při aktivaci layoutu a kopíruje se s ním). Není nijak
// vidět, může ležet na libovolné hladině (i hladině výřezů), kdekoliv v pořadí bloku
// a jeho CustomScale je aktuální zoom bez filtru vyrábí falešná měřítka (typicky 1:1000
// u výkresů v metrech). Primárně se vylučuje podle handle z ActivePViewport (jednoznačné);
// fallback bez handle: pseudoviewport má StandardScale trvale zamrzlé na acVp1_1 (2),
// zatímco výřezy s měřítkem ze seznamu hlásí acVpCustomScale (1) heuristika by ale
// přeskočila i skutečný výřez nastavený přes enum standardních měřítek na 1:1.
if (papirHandle is not null)
{
string handle;
try { handle = (string)ent.Handle; } catch { handle = ""; }
if (string.Equals(handle, papirHandle, StringComparison.OrdinalIgnoreCase)) continue;
}
else
{
try { if (Convert.ToInt32(ent.StandardScale) == 2) continue; } catch { /* nelze přečíst → počítat */ }
}
double meritko;
try { meritko = Convert.ToDouble(ent.CustomScale); } catch { continue; }
if (meritko <= 0) continue;
long jmenovatel = (long)Math.Round(1.0 / meritko * jednotkyFaktor);
if (jmenovatel > 0)
{
jmenovatele.Add(jmenovatel);
prvni ??= jmenovatel; // první výřez v pořadí iterace bloku zdroj jediné hodnoty LTSCALE
}
}
return (jmenovatele, prvni);
}
/// <summary>Formátuje jmenovatele měřítka jako „1:500, 1:1000", nebo <c>null</c>, když je množina prázdná.</summary>
private static string? VyhodnotMeritka(SortedSet<long> jmenovatele) =>
jmenovatele.Count == 0 ? null : string.Join(", ", jmenovatele.Select(j => $"1:{j}"));
/// <summary>LTSCALE odvozené z měřítka <b>prvního</b> výřezu ve výkresu: <c>1000 / jmenovatel</c>
/// (např. 1:500 → 2, 1:50 → 20). Vždy jen jedna hodnota LTSCALE je systémová proměnná celého
/// výkresu, takže i u layoutu s víc výřezy o různých měřítkách se bere jeden (první v pořadí
/// iterace bloku). <c>null</c>, když žádný výřez nebyl nalezen. Tisk (<see cref="CadTiskService"/>)
/// tuhle jedinou hodnotu parsuje přímo.</summary>
private static string? VyhodnotLtscale(long? prvniJmenovatel) => prvniJmenovatel is null
? null
: (1000.0 / prvniJmenovatel.Value).ToString("0.###", CultureInfo.InvariantCulture);
/// <summary>Nastaví tiskové vlastnosti rozvržení (podklad §23): tiskárnu, formát papíru, CTB a pevné
/// hodnoty (jednotky mm, bez rotace, tisk celého layoutu, měřítko 1:1, výřezy tisknout první). Tiskárna
/// se hledá v <see cref="LayoutConfig.Tiskarny"/> podle <paramref name="cadRodina"/> (viz komentář
/// u vlastnosti „DWG to PDF" má u každého CADu jinou příponu). Tiskárnu i formát papíru před zápisem
/// ověří v aktuálním CADu (<see cref="ZarizeniExistuje"/>/<see cref="ZjistiMedium"/>); když nejsou
/// k dispozici (nebo pro daný engine není v <c>layout.json</c> tiskárna vyplněná vůbec), tiskárna se
/// pro daný layout jen nenastaví (zbytek aktualizace pokračuje).</summary>
private static void NastavTiskLayoutu(dynamic doc, dynamic layout, string cadRodina, Priloha p, LayoutConfig cfg,
(double W, double H)? papirMm, ILayoutLog log, string cilJmeno)
{
// Bez rámečku není odkud vzít rozměr papíru dřív se dopočítával z A4, teď žádný fallback:
// rozměr papíru se v tiskárně nenastaví (a tím pádem ani zbytek nastavení tisku, aby nezůstal
// v nekonzistentním stavu s cizím/starým formátem).
if (papirMm is null)
{
log.Chyba($"„{cilJmeno}“: rámeček nebyl nalezen rozměr papíru v tiskárně se nenastaví (žádný fallback).");
return;
}
if (!cfg.Tiskarny.TryGetValue(cadRodina, out var zarizeni) || string.IsNullOrWhiteSpace(zarizeni))
{
log.Chyba($"„{cilJmeno}“: v layout.json není pro CAD „{cadRodina}“ vyplněná tiskárna " +
"(Tiskarny) tiskárna se pro tento layout nenastavuje.");
return;
}
if (!ZarizeniExistuje(layout, zarizeni))
{
log.Chyba($"„{cilJmeno}“: tiskárna „{zarizeni}“ není v CADu dostupná tiskárna se pro tento layout nenastavuje.");
return;
}
double sirkaMm = papirMm.Value.W;
double vyskaMm = papirMm.Value.H;
string popisPapiru = $"{(int)Math.Round(sirkaMm)}x{(int)Math.Round(vyskaMm)}";
var (canonicalMedia, dostupne) = ZjistiMedium((object)doc, zarizeni, sirkaMm, vyskaMm);
if (canonicalMedia is null)
{
string seznam = dostupne.Count > 0
? $" Dostupné formáty u této tiskárny: {string.Join(", ", dostupne)}."
: " (seznam dostupných formátů se nepodařilo přečíst).";
// Přesná (nezaokrouhlená) hodnota do logu kdyby shoda selhávala jen kvůli zaokrouhlení/toleranci,
// musí to tu být vidět (popisPapiru výše je zaokrouhlený na celé mm jen pro stručný název).
log.Chyba($"„{cilJmeno}“: formát papíru {popisPapiru} mm (přesně " +
$"{sirkaMm.ToString("0.####", CultureInfo.InvariantCulture)}×" +
$"{vyskaMm.ToString("0.####", CultureInfo.InvariantCulture)}) není u tiskárny „{zarizeni}“ " +
$"dostupný tiskárna se pro tento layout nenastavuje.{seznam}");
return;
}
// Celý název včetně přípony ".ctb" žádné auto-dopisování (na rozdíl od původního VBA). Výchozí
// CTB je per-engine (viz LayoutConfig.VychoziCtb) .ctb soubory NEJSOU sdílené mezi CADy.
string? ctb = p.Ctb;
if (string.IsNullOrWhiteSpace(ctb))
{
if (!cfg.VychoziCtb.TryGetValue(cadRodina, out var vychozi) || string.IsNullOrWhiteSpace(vychozi))
{
log.Chyba($"„{cilJmeno}“: v layout.json není pro CAD „{cadRodina}“ vyplněný výchozí CTB " +
"(VychoziCtb) a řádek nemá vlastní styl tisku se pro tento layout nenastavuje.");
ctb = null;
}
else ctb = vychozi;
}
TrySet(() => layout.ConfigName = zarizeni, "nastavení tiskárny", log, cilJmeno);
TrySet(() => layout.CanonicalMediaName = canonicalMedia, "nastavení formátu papíru", log, cilJmeno);
// Po změně tiskárny/formátu papíru musí CAD přehodnotit, co je pro tenhle layout u tohodle
// zařízení/formátu platné (tady i seznam standardních měřítek) bez toho UseStandardScale/
// StandardScale níže potichu neprojeví a CAD zůstane u dřívějšího (scale-to-fit/custom) měřítka.
TrySet(() => layout.RefreshPlotDeviceInfo(), "obnovení informací o tiskovém zařízení", log, cilJmeno);
TrySet(() => layout.PaperUnits = 1, "nastavení jednotek papíru (mm)", log, cilJmeno); // acMillimeters
TrySet(() => layout.PlotOrigin = new double[] { 0, 0 }, "nastavení počátku tisku", log, cilJmeno);
TrySet(() => layout.PlotRotation = 0, "nastavení rotace tisku", log, cilJmeno); // ac0degrees
TrySet(() => layout.PlotType = 5, "nastavení rozsahu tisku (layout)", log, cilJmeno); // acLayout
// StandardScale samo o sobě nic nedělá, dokud UseStandardScale není true jinak CAD dál používá
// ať už měl nastaveno cokoliv předtím (scale-to-fit, starý CustomScale apod.).
TrySet(() => layout.UseStandardScale = true, "zapnutí standardního měřítka", log, cilJmeno);
// Pozor: StandardScale je enum AcPlotScale, ne poměr 1 by znamenalo ac1_128in_1ft (1:1536).
TrySet(() => layout.StandardScale = 16, "nastavení měřítka tisku (1:1)", log, cilJmeno); // ac1_1
TrySet(() => layout.PlotViewportsFirst = true, "nastavení pořadí tisku výřezů", log, cilJmeno);
if (ctb is not null) TrySet(() => layout.StyleSheet = ctb, "nastavení stylu tisku (CTB)", log, cilJmeno);
log.Info($"„{cilJmeno}“: tiskárna {zarizeni}, papír {popisPapiru} mm ({canonicalMedia})," +
$" CTB {ctb ?? "nenastaveno"}.");
}
private static void TrySet(Action nastav, string popis, ILayoutLog log, string cilJmeno)
{
try { nastav(); }
catch (Exception ex) { log.Chyba($"„{cilJmeno}“: {popis} selhalo: {ex.Message}"); }
}
/// <summary>Ověří, že tiskárna existuje mezi zařízeními dostupnými v aktuálním CADu (podklad §3, <c>TiskarnyZCad</c>).</summary>
private static bool ZarizeniExistuje(dynamic layout, string zarizeni)
{
try
{
var jmena = (object[])layout.GetPlotDeviceNames();
return jmena.Any(n => string.Equals((string)n, zarizeni, StringComparison.OrdinalIgnoreCase));
}
catch { return false; }
}
/// <summary>Najde canonical název formátu papíru u dané tiskárny odpovídající rozměru
/// <paramref name="sirkaMm"/>×<paramref name="vyskaMm"/> (přes dočasnou <c>PlotConfiguration</c>,
/// kterou po ověření zase smaže). Vrací přímo canonical název, protože ten je potřeba pro
/// <c>layout.CanonicalMediaName</c> a u vlastních formátů se mezi CADy zásadně liší, proto se
/// zkouší tři strategie v pořadí od nejlevnější:
/// <list type="number">
/// <item><b>Přímá shoda názvu</b> ZWCAD ukládá vlastní formát jako čitelné „1260x420“.</item>
/// <item><b>Rozměr rozparsovaný z canonical názvu</b> AutoCAD dá do canonical názvu rovnou celý
/// popis včetně rozměru (např. „UserDefinedMetric (1260.00 x 420.00MM)“ jako jeden celý řetězec
/// ne generický „UserDefinedMetric“ a rozměr zvlášť v lokalizovaném názvu, jak by se čekalo;
/// <c>GetLocaleMediaNames()</c> pro tyhle vlastní formáty žádný samostatný záznam nedává, což při
/// párování podle indexu s <c>GetCanonicalMediaNames()</c> vedlo k tichému přeskočení).</item>
/// <item><b>Skutečný rozměr média přes <c>GetPaperSize</c></b> BricsCAD u PDF ovladače nenese
/// rozměr v canonical názvu vůbec (vlastní formáty jsou jen generické pojmenované sloty „Vlastní
/// velikost 1/2/3“, jejichž rozměr je uložený v PC3/PMP; runtime API à la AutoCAD UserDefinedMetric
/// neexistuje). Rozměr slotu se proto přečte tak, že se médium dočasně nastaví na throwaway
/// konfiguraci a zavolá <c>GetPaperSize</c> (vrací mm nezávisle na PaperUnits). <b>Pozor:</b> tohle
/// spáruje jen slot, jehož rozměr v PC3 rámečku skutečně odpovídá arbitrární rozměr BricsCAD přes
/// COM nastavit neumí, chybějící formát je nutné do PC3 předdefinovat ručně.</item>
/// </list>
/// Když nic nesedí, vrátí seznam dostupných formátů (u strategie 3 i s přečtenými rozměry) pro
/// diagnózu v logu.</summary>
private static (string? CanonicalMedia, IReadOnlyList<string> Dostupne) ZjistiMedium(dynamic doc, string zarizeni, double sirkaMm, double vyskaMm)
{
dynamic? tmp = null;
try
{
tmp = doc.PlotConfigurations.Add(TmpPlotConfigName, true);
tmp.ConfigName = zarizeni;
tmp.RefreshPlotDeviceInfo();
var canonical = ((object[])tmp.GetCanonicalMediaNames()).Select(n => (string)n).ToArray();
// 1) Přímá shoda názvu (ZWCAD: „840x297").
string zkratka = $"{(int)Math.Round(sirkaMm)}x{(int)Math.Round(vyskaMm)}";
var primy = canonical.FirstOrDefault(n => string.Equals(n, zkratka, StringComparison.OrdinalIgnoreCase));
if (primy is not null) return (primy, Array.Empty<string>());
// 2) Rozměr rozparsovaný z canonical názvu (AutoCAD: „UserDefinedMetric (840.00 x 297.00MM)").
foreach (var nazev in canonical)
{
var cisla = Regex.Matches(nazev, @"\d+(?:[.,]\d+)?")
.Select(m => double.Parse(m.Value.Replace(',', '.'), CultureInfo.InvariantCulture))
.ToArray();
if (cisla.Length != 2) continue;
bool shoda = (Priblizne(cisla[0], sirkaMm) && Priblizne(cisla[1], vyskaMm)) ||
(Priblizne(cisla[0], vyskaMm) && Priblizne(cisla[1], sirkaMm));
if (shoda) return (nazev, Array.Empty<string>());
}
// 3) Skutečný rozměr každého média přes GetPaperSize (BricsCAD generické sloty bez rozměru
// v názvu). Zároveň se sbírá čitelný seznam s rozměry pro diagnostiku, když nic nesedne.
var diag = new List<string>(canonical.Length);
foreach (var nazev in canonical)
{
(double W, double H)? rozmer;
try { rozmer = CtiRozmerMedia(tmp, nazev); }
catch { rozmer = null; } // žádné médium nesmí shodit celý průchod jen se přeskočí
if (rozmer is null) { diag.Add(nazev); continue; }
double w = rozmer.Value.W, h = rozmer.Value.H;
diag.Add($"{nazev} ({w:0}×{h:0} mm)");
bool shoda = (Priblizne(w, sirkaMm) && Priblizne(h, vyskaMm)) ||
(Priblizne(w, vyskaMm) && Priblizne(h, sirkaMm));
if (shoda) return (nazev, Array.Empty<string>());
}
return (null, diag);
}
catch { return (null, Array.Empty<string>()); }
finally
{
if (tmp is not null) { try { tmp.Delete(); } catch { /* dočasnou konfiguraci se nepodařilo smazat neblokující */ } }
}
}
/// <summary>Přečte skutečný rozměr média (v mm) tak, že ho dočasně nastaví na throwaway
/// <c>PlotConfiguration</c> a zavolá <c>GetPaperSize</c> jediná cesta, jak zjistit rozměr formátu,
/// který ho nemá v canonical názvu (BricsCAD vlastní sloty „Vlastní velikost 1/2/3“). <c>GetPaperSize</c>
/// má dva <c>out</c> parametry (šířka, výška v mm); volá se přes <see cref="Type.InvokeMember(string,
/// BindingFlags, Binder, object, object[], ParameterModifier[], CultureInfo, string[])"/> s byref
/// <see cref="ParameterModifier"/> a hodnoty se čtou zpět z pole <c>args</c>. Přímé volání přes
/// <c>dynamic</c> (<c>GetPaperSize(out object, out object)</c>) tady nejde DLR/COM marshaling
/// <c>out</c> parametrů v BricsCADu padá na <c>ArgumentException „Could not convert argument“</c>
/// (ověřeno 2026-07-07); InvokeMember s byref polem funguje napříč AutoCAD/ZWCAD/BricsCAD. Vrací
/// <c>null</c>, když médium nejde nastavit/přečíst nebo rozměr není kladný.</summary>
private static (double W, double H)? CtiRozmerMedia(dynamic plotConfig, string canonicalMedia)
{
try
{
plotConfig.CanonicalMediaName = canonicalMedia;
object cil = plotConfig;
var args = new object[] { 0.0, 0.0 };
var mods = new ParameterModifier(2); mods[0] = true; mods[1] = true;
cil.GetType().InvokeMember("GetPaperSize", BindingFlags.InvokeMethod, null, cil, args,
new[] { mods }, CultureInfo.InvariantCulture, null);
double w = Convert.ToDouble(args[0], CultureInfo.InvariantCulture);
double h = Convert.ToDouble(args[1], CultureInfo.InvariantCulture);
return (w > 0 && h > 0) ? (w, h) : null;
}
catch { return null; }
}
private static bool Priblizne(double a, double b) => Math.Abs(a - b) < 0.5;
}
+111
View File
@@ -0,0 +1,111 @@
using System.Runtime.InteropServices;
namespace Rozpisky.Cad;
/// <summary>
/// Připojení na běžící CAD (AutoCAD/ZWCAD/BricsCAD) přes COM automation port <c>clsCadLink</c>
/// (PODKLAD_CAD_Layout_Tisk.md §1). Pozdní vazba přes <c>dynamic</c> (stejný princip jako
/// <see cref="Xlsx.ExcelToPdf"/>), takže funguje napříč CAD systémy bez PIA reference.
/// <para>
/// Na rozdíl od <see cref="Xlsx.ExcelToPdf"/> se <b>nespouští</b> nová instance připojuje se
/// výhradně na už běžící CAD (<c>GetActiveObject</c>). <c>Marshal.GetActiveObject</c> byl v .NET
/// (Core/5+) odstraněn, proto je tu vlastní P/Invoke na <c>ole32</c>/<c>oleaut32</c>.
/// </para>
/// </summary>
public sealed class CadLink : IDisposable
{
// CO_E_CLASSSTRING ProgID není v registru zaregistrované (CAD není nainstalovaný).
private const uint CoEClassString = 0x800401F3;
// MK_E_UNAVAILABLE v Running Object Table není žádná instance (CAD neběží).
private const uint MkEUnavailable = 0x800401E3;
[DllImport("ole32.dll", PreserveSig = false)]
private static extern void CLSIDFromProgID([MarshalAs(UnmanagedType.LPWStr)] string progId, out Guid clsid);
[DllImport("oleaut32.dll", PreserveSig = false)]
private static extern void GetActiveObject(ref Guid rclsid, IntPtr reserved,
[MarshalAs(UnmanagedType.Interface)] out object ppunk);
public dynamic? App { get; private set; }
public dynamic? Doc { get; private set; }
public string Engine { get; private set; } = "";
/// <summary>ProgID, přes které jsme se připojili (verzově konkrétní i ruční). Zdroj pro <see cref="Rodina"/>.</summary>
public string ProgId { get; private set; } = "";
public bool IsConnected { get; private set; }
public string? LastError { get; private set; }
/// <summary>Rodina CADu (klíč do <c>layout.json</c> Tiskarny/VychoziCtb) odvozená z <see cref="ProgId"/>.</summary>
public string Rodina => RodinaZProgId(ProgId);
/// <summary>Zařadí ProgID do rodiny CADu („AutoCAD" / „ZWCAD" / „BricsCAD") nezávisle na verzi a
/// popisku enginu „ZWCAD.Application.2023" i „ZWCAD.Application" → „ZWCAD", „BricscadApp.AcadApplication"
/// → „BricsCAD". Tímto klíčem se párují per-engine hodnoty v <c>layout.json</c> (tiskárna/CTB), místo
/// dřívějšího křehkého párování podle popisného názvu enginu z cad.json. Neznámé ProgID (exotický ruční
/// zápis) vrátí samo sebe ať je v logu vidět a jde ho v layout.json ručně nakeyovat.</summary>
public static string RodinaZProgId(string? progId)
{
if (string.IsNullOrWhiteSpace(progId)) return "";
if (progId.Contains("AutoCAD", StringComparison.OrdinalIgnoreCase)) return "AutoCAD";
if (progId.Contains("ZWCAD", StringComparison.OrdinalIgnoreCase)) return "ZWCAD";
if (progId.Contains("Brics", StringComparison.OrdinalIgnoreCase)) return "BricsCAD";
return progId;
}
/// <summary>Připojí se na už běžící instanci <paramref name="progId"/> a vezme její aktivní
/// dokument. Nic nespouští. Vrací false a vyplní <see cref="LastError"/> čitelnou hláškou, když
/// CAD není nainstalovaný, neběží, nebo běží bez otevřeného výkresu.</summary>
public bool Connect(string progId, string nazev)
{
LastError = null;
object app;
try
{
CLSIDFromProgID(progId, out var clsid);
GetActiveObject(ref clsid, IntPtr.Zero, out app);
}
catch (COMException ex) when (unchecked((uint)ex.HResult) == CoEClassString)
{
LastError = $"{nazev} není nainstalovaný (COM třída „{progId}“ není zaregistrovaná).";
return false;
}
catch (COMException ex) when (unchecked((uint)ex.HResult) == MkEUnavailable)
{
LastError = $"{nazev} neběží. Spusť ho, otevři výkres a zkus to znovu.";
return false;
}
catch (COMException ex)
{
LastError = $"Připojení na {nazev} selhalo: {ex.Message}";
return false;
}
App = app;
try
{
var doc = App.ActiveDocument;
if (doc is null) throw new InvalidOperationException("ActiveDocument je prázdný.");
Doc = doc;
}
catch (Exception ex)
{
LastError = $"{nazev} běží, ale nemá otevřený žádný výkres ({ex.Message}).";
Marshal.FinalReleaseComObject(App);
App = null;
return false;
}
Engine = nazev;
ProgId = progId;
IsConnected = true;
return true;
}
public void Dispose()
{
if (Doc is not null) { Marshal.FinalReleaseComObject(Doc); Doc = null; }
if (App is not null) { Marshal.FinalReleaseComObject(App); App = null; }
IsConnected = false;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
+219
View File
@@ -0,0 +1,219 @@
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
using Rozpisky.Models;
namespace Rozpisky.Cad;
/// <summary>
/// „Vytiskni rozvržení" port <c>modTiskPriloh</c> (PODKLAD_CAD_Layout_Tisk.md §5): hromadný tisk
/// vybraných layoutů do PDF. Zálohuje a po dokončení (i při chybě/zrušení) vrací systémové proměnné
/// výkresu (<c>BACKGROUNDPLOT</c>, <c>PLOTTRANSPARENCYOVERRIDE</c>, <c>LTSCALE</c>).
/// </summary>
public static class CadTiskService
{
/// <summary>Vytiskne vybrané přílohy (jejich <see cref="Priloha.JmenoRozvrzeni"/>) do PDF do
/// <paramref name="cilovaSlozka"/>. <paramref name="sestavNazevSouboru"/> a
/// <paramref name="pockejNaUvolneniSouboru"/> se přebírají z <c>MainViewModel</c>, aby se název
/// souboru (přípona „_rozpiska") a chování při zamčeném cíli chovaly stejně jako u exportu do PDF.</summary>
public static async Task VytiskniRozvrzeni(dynamic doc, IReadOnlyList<Priloha> vybrane, string cilovaSlozka,
Nastaveni nastaveni, ILayoutLog log, Func<Priloha, string?> sestavNazevSouboru,
Func<string, bool> pockejNaUvolneniSouboru, CancellationToken zruseno)
{
if (vybrane.Count == 0) { log.Chyba("Není vybraný žádný řádek seznamu."); return; }
Diag($"Start tisku, {vybrane.Count} řádků, cíl {cilovaSlozka}");
// Celočíselné sysvars se přes COM nastavují jako short (VT_I2) VBA Integer. .NET int
// (VT_I4) u SetVariable("PLOTTRANSPARENCYOVERRIDE", 2) shodil AutoCAD/aplikaci nativně,
// bez výjimky a bez error.log (lokalizováno breadcrumbs v tisk_diag.log, 2026-07-05).
object? puvodniBackgroundPlot = null, puvodniTransparency = null, puvodniLtscale = null;
bool vratitBackgroundPlot = false, vratitTransparency = false, vratitLtscale = false;
try
{
Diag("GetVariable BACKGROUNDPLOT");
puvodniBackgroundPlot = doc.GetVariable("BACKGROUNDPLOT");
Diag("GetVariable LTSCALE");
puvodniLtscale = doc.GetVariable("LTSCALE");
Diag("SetVariable BACKGROUNDPLOT=0");
doc.SetVariable("BACKGROUNDPLOT", (short)0); // tisk na popředí PlotToFile musí počkat na dokončení
vratitBackgroundPlot = true;
}
catch (Exception ex)
{
log.Chyba($"Přípravu systémových proměnných výkresu se nepodařilo dokončit: {ex.Message}");
return;
}
// Ignorování průhlednosti je jen urychlení tisku jeho selhání nesmí tisk zastavit, proto
// vlastní try/catch. Zápis se navíc přeskočí, když už hodnota 2 je (žádné zbytečné COM volání
// proměnné, na které to padalo).
try
{
Diag("GetVariable PLOTTRANSPARENCYOVERRIDE");
puvodniTransparency = doc.GetVariable("PLOTTRANSPARENCYOVERRIDE");
if (Convert.ToInt32(puvodniTransparency) != 2)
{
Diag("SetVariable PLOTTRANSPARENCYOVERRIDE=2");
doc.SetVariable("PLOTTRANSPARENCYOVERRIDE", (short)2); // ignorovat průhlednost
vratitTransparency = true;
}
else Diag("PLOTTRANSPARENCYOVERRIDE už je 2, přeskakuji");
}
catch (Exception ex)
{
log.Chyba($"Nastavení PLOTTRANSPARENCYOVERRIDE selhalo (tisk pokračuje): {ex.Message}");
}
Diag("Sysvar připraveny");
int vytisknuto = 0, preskoceno = 0;
try
{
for (int i = 0; i < vybrane.Count; i++)
{
if (zruseno.IsCancellationRequested) { log.Info("Zrušeno uživatelem tisk ukončen."); break; }
await Dispatcher.Yield(DispatcherPriority.Background); // nech log okno/stavový řádek překreslit
var p = vybrane[i];
Diag($"Iterace {i}: JmenoRozvrzeni „{p.JmenoRozvrzeni}“");
var nazev = sestavNazevSouboru(p);
if (nazev is null)
{
log.Chyba($"Řádek {i + 1}: chybí jméno rozvržení, přeskočeno.");
preskoceno++;
continue;
}
if (nastaveni.LtscaleAutomaticky)
{
var ltscale = NactiLtscale(p.Ltscale);
Diag($"SetVariable LTSCALE={ltscale.ToString(CultureInfo.InvariantCulture)}");
try { doc.SetVariable("LTSCALE", ltscale); vratitLtscale = true; } // LTSCALE je RTREAL double je správný typ
catch (Exception ex) { log.Chyba($"„{nazev}“: nastavení LTSCALE selhalo: {ex.Message}"); }
}
var cesta = Path.Combine(cilovaSlozka, nazev + ".pdf");
if (!pockejNaUvolneniSouboru(cesta)) { log.Info("Přerušeno uživatelem (soubor byl používán)."); break; }
Diag($"Hledám layout „{p.JmenoRozvrzeni}“");
// Záměrně výčtem, ne doc.Layouts.Item(jméno): Item s neexistujícím klíčem AutoCAD
// přes COM neshodil do chytatelné výjimky, ale nativně položil aplikaci (stejná třída
// problému jako SetVariable s VT_I4 viz podklad §9 bod 9; nalezeno 2026-07-05).
dynamic? layout = NajdiLayout(doc, p.JmenoRozvrzeni!);
if (layout is null)
{
log.Chyba($"„{nazev}“: rozvržení „{p.JmenoRozvrzeni}“ ve výkresu neexistuje, přeskočeno.");
preskoceno++;
continue;
}
try
{
Diag($"Aktivuji layout „{nazev}“");
doc.ActiveLayout = layout;
// Breadcrumb těsně před nativním voláním: tvrdý pád CAD serveru (AccessViolation
// v PlotToFile) nezanechá stopu v error.log ani v log okně (to zmizí s procesem)
// poslední řádek v tisk_diag.log pak ukáže, na kterém layoutu to spadlo.
Diag($"Před PlotToFile: „{nazev}“ → {cesta}");
bool ok = doc.Plot.PlotToFile(cesta);
Diag($"Po PlotToFile: „{nazev}“ = {ok}");
if (ok)
{
log.Info($"„{nazev}“: vytištěno do {cesta}.");
vytisknuto++;
}
else
{
log.Chyba($"„{nazev}“: tisk se nezdařil (PlotToFile vrátil false zkontroluj " +
"tiskárnu/formát rozvržení přes „Aktualizuj rozvržení“).");
preskoceno++;
}
}
catch (Exception ex)
{
log.Chyba($"„{nazev}“: tisk selhal: {ex.Message}");
preskoceno++;
}
}
}
finally
{
// Vrací se jen to, co se skutečně změnilo zbytečné SetVariable na proměnnou, která se
// nenastavovala, je přesně to volání, na kterém to umí spadnout. Původní hodnoty jdou
// zpět tak, jak je GetVariable vydal (boxed variant se správným typem).
Diag("Obnova systémových proměnných");
if (vratitBackgroundPlot)
try { doc.SetVariable("BACKGROUNDPLOT", puvodniBackgroundPlot); } catch { /* nekritické */ }
if (vratitTransparency)
try { doc.SetVariable("PLOTTRANSPARENCYOVERRIDE", puvodniTransparency); } catch { /* nekritické */ }
if (vratitLtscale)
try { doc.SetVariable("LTSCALE", puvodniLtscale); } catch { /* nekritické */ }
}
log.Info($"Hotovo: vytištěno {vytisknuto}, přeskočeno {preskoceno}.");
Diag($"Konec tisku: vytištěno {vytisknuto}, přeskočeno {preskoceno}");
}
/// <summary>Najde rozvržení podle názvu výčtem kolekce <c>doc.Layouts</c> (názvy layoutů jsou
/// case-insensitive unikátní). Vrací <c>null</c>, když neexistuje bez vyhazujícího
/// <c>Layouts.Item</c>, který na AutoCADu při neexistujícím klíči shodil aplikaci nativně.</summary>
private static dynamic? NajdiLayout(dynamic doc, string jmeno)
{
try
{
foreach (dynamic l in doc.Layouts)
{
string n;
try { n = (string)l.Name; } catch { continue; }
if (string.Equals(n, jmeno, StringComparison.OrdinalIgnoreCase)) return l;
}
}
catch { /* kolekci se nepodařilo projít → jako nenalezeno, volající zaloguje a přeskočí */ }
return null;
}
/// <summary>Prázdné/nečíselné/≤0 → 1 (podklad §5). Bere jednu hodnotu <c>Priloha.Ltscale</c>
/// obsahuje po „Aktualizuj rozvržení“ jedinou hodnotu (z prvního výřezu, viz
/// <c>CadLayoutService.VyhodnotLtscale</c>).</summary>
private static double NactiLtscale(string? hodnota)
{
if (string.IsNullOrWhiteSpace(hodnota)) return 1;
if (double.TryParse(hodnota, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) && d > 0) return d;
if (double.TryParse(hodnota, NumberStyles.Float, CultureInfo.CurrentCulture, out d) && d > 0) return d;
return 1;
}
/// <summary>Horní mez velikosti <c>tisk_diag.log</c> (bajty). Po překročení se log při startu
/// aplikace smaže, aby po měsících provozu nenarostl do enormních rozměrů. Limit (ne mazání při
/// každém startu) proto, aby breadcrumbs po tvrdém pádu CADu přežily i běžný restart.</summary>
private const long MaxLogBajtu = 1 * 1024 * 1024; // 1 MB
/// <summary>Zavolat jednou při startu aplikace: když <c>tisk_diag.log</c> přeroste
/// <see cref="MaxLogBajtu"/>, smaže ho (jinak nechá být). Nesmí nic shodit.</summary>
internal static void UklidLog()
{
try
{
var cesta = Path.Combine(AppContext.BaseDirectory, "tisk_diag.log");
if (File.Exists(cesta) && new FileInfo(cesta).Length > MaxLogBajtu)
File.Delete(cesta);
}
catch { /* read-only složka apod. úklid nesmí přerušit start */ }
}
/// <summary>Diagnostický „chlebníček“ zapíše krok tisku do <c>tisk_diag.log</c> vedle .exe.
/// Slouží k lokalizaci tvrdého pádu CADu (nativní chyba v <c>PlotToFile</c>), který nezanechá
/// stopu v error.log ani v log okně (obojí zmizí s procesem). Zapisuje se synchronně (flush ihned),
/// takže poslední řádek přežije i pád procesu. Diagnostika sama nesmí nic shodit.</summary>
internal static void Diag(string krok)
{
try
{
File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "tisk_diag.log"),
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {krok}{Environment.NewLine}");
}
catch { /* read-only složka apod. nesmí přerušit tisk */ }
}
}
+77
View File
@@ -0,0 +1,77 @@
using System.Runtime.InteropServices;
namespace Rozpisky.Cad;
/// <summary>
/// COM message filtr (IMessageFilter z ole32, ne WinForms) proti „CAD je zaneprázdněný“.
/// Když v CADu běží příkaz nebo modální dialog, odmítá COM volání s
/// <c>RPC_E_CALL_REJECTED</c>/<c>RPC_E_SERVERCALL_RETRYLATER</c> bez filtru to znamená
/// vynechaný krok nebo pád celé dávky. Filtr říká COM runtime „zkus to za chvíli znovu“,
/// dokud se CAD neuvolní (max. <see cref="MaxCekaniMs"/>, pak výjimka propadne do
/// stávajících try/catch a zaloguje se jako dřív).
/// <para>
/// Platí jen pro aktuální STA vlákno registruje se přes <c>CoRegisterMessageFilter</c>
/// kolem každé CAD akce (<c>using var filtr = ComMessageFilter.Registruj();</c>) a při
/// Dispose vrátí původní filtr. Všechna CAD volání běží na UI (STA) vlákně, viz
/// <see cref="CadLayoutService"/>.
/// </para>
/// </summary>
public sealed class ComMessageFilter : IOleMessageFilter, IDisposable
{
private const int RetryZaMs = 150; // prodleva mezi pokusy
private const int MaxCekaniMs = 30_000; // pak to vzdát → COMException do logu
// SERVERCALL_ISHANDLED / SERVERCALL_RETRYLATER (enum SERVERCALL z objidl.h)
private const int ServerCallIsHandled = 0;
private const int ServerCallRetryLater = 2;
private const int PendingMsgWaitDefProcess = 2;
[DllImport("ole32.dll")]
private static extern int CoRegisterMessageFilter(IOleMessageFilter? newFilter, out IOleMessageFilter? oldFilter);
private IOleMessageFilter? _puvodni;
private ComMessageFilter() { }
/// <summary>Zaregistruje filtr na aktuálním (STA) vlákně. Když registrace selže
/// (např. MTA vlákno), vrátí no-op instanci CAD akce poběží jako dřív, bez retry.</summary>
public static ComMessageFilter Registruj()
{
var filtr = new ComMessageFilter();
try { CoRegisterMessageFilter(filtr, out filtr._puvodni); }
catch { /* bez filtru chování jako dřív */ }
return filtr;
}
public void Dispose()
{
try { CoRegisterMessageFilter(_puvodni, out _); }
catch { /* nekritické */ }
_puvodni = null;
}
// --- IOleMessageFilter (my jsme klient podstatné je RetryRejectedCall) ---
int IOleMessageFilter.HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo)
=> ServerCallIsHandled;
int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType)
{
// dwTickCount = jak dlouho už se na tohle volání čeká; ≥ 0 = zkus znovu za N ms, 1 = vzdát to.
if (dwRejectType == ServerCallRetryLater && dwTickCount < MaxCekaniMs)
return RetryZaMs;
return -1;
}
int IOleMessageFilter.MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType)
=> PendingMsgWaitDefProcess;
}
/// <summary>COM rozhraní IMessageFilter (objidl.h) nesouvisí s System.Windows.Forms.IMessageFilter.</summary>
[ComImport, Guid("00000016-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IOleMessageFilter
{
[PreserveSig] int HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo);
[PreserveSig] int RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType);
[PreserveSig] int MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType);
}
+19
View File
@@ -0,0 +1,19 @@
namespace Rozpisky.Cad;
/// <summary>
/// Průběžný log dávkových CAD akcí (aktualizace rozvržení, tisk do PDF) každý krok/chyba se
/// zapisuje sem, aby se nic neztratilo v jediném souhrnném MessageBoxu. Implementuje
/// <c>Rozpisky.Views.LayoutLogWindow</c>.
/// </summary>
public interface ILayoutLog
{
/// <summary>Zapíše informační řádek (úspěšný krok).</summary>
void Info(string zprava);
/// <summary>Zapíše chybový řádek nepřerušuje dávku, jen ji poznamená.</summary>
void Chyba(string zprava);
/// <summary>True, pokud uživatel v okně zvolil „Zrušit" kontroluje se mezi jednotlivými
/// layouty/soubory dávky.</summary>
bool Zruseno { get; }
}
+57
View File
@@ -0,0 +1,57 @@
namespace Rozpisky.Cad;
/// <summary>
/// Konfigurace „aktualizace rozvržení" / tisku do PDF (PODKLAD_CAD_Layout_Tisk.md §25) názvy
/// hledaných bloků/hladin ve výkresu a výchozí tiskárna/CTB. Drží se mimo kód v <see cref="LayoutConfigStore"/>
/// (JSON vedle .exe), protože se liší projekt od projektu / stroj od stroje a needituje se přes UI.
/// <para>
/// Ostatní plotovací konstanty (PaperUnits, PlotRotation, PlotType, StandardScale, PlotOrigin,
/// PlotViewportsFirst) zůstávají pevné v <c>CadLayoutService</c> jde o mapování na ActiveX
/// konstanty (viz podklad §2), ne o věc, kterou by uživatel přepínal mezi CADy.
/// </para>
/// </summary>
public sealed class LayoutConfig
{
/// <summary>Název bloku „rohové razítko" hledaného v každém rozvržení čte se z něj STAVBA/OBJEKT/PRILOHA/MERITKO.</summary>
public string BlokHlavicky { get; set; } = "vykres_hlavicka";
/// <summary>Název dynamického bloku rámečku, z jehož bounding boxu se počítá počet papírů (A4).</summary>
public string BlokRamecek { get; set; } = "ramecek";
/// <summary>Hladina, ve které se v rozvržení hledají výřezy (viewporty) pro vyhodnocení měřítka.</summary>
public string HladinaVyrezu { get; set; } = "!!NOPRINT";
/// <summary>Šířka jednoho archu A4 v mm (pro výpočet počtu papírů z rámečku).</summary>
public double SirkaA4Mm { get; set; } = 210;
/// <summary>Výška jednoho archu A4 v mm (pro výpočet počtu papírů z rámečku).</summary>
public double VyskaA4Mm { get; set; } = 297;
/// <summary>Výchozí tiskárna (ConfigName) podle <b>rodiny CADu</b> klíč je „AutoCAD" / „ZWCAD" /
/// „BricsCAD", odvozený z ProgID připojeného CADu přes <see cref="Cad.CadLink.RodinaZProgId"/>
/// (ne popisný <c>CadEngine.Nazev</c> z cad.json ten je verzovaný „ZWCAD 2023" apod. a nepároval by).
/// Tiskový ovladač „DWG to PDF" má u každého CADu jinou příponu (AutoCAD <c>.pc3</c>, ZWCAD <c>.pc5</c>,
/// BricsCAD jinak) proto per-rodina, ne jedna společná hodnota. Prázdná/chybějící hodnota = tiskárna se
/// pro danou rodinu nenastaví (zaloguje se jako chyba, zbytek aktualizace pokračuje).</summary>
public Dictionary<string, string> Tiskarny { get; set; } = new()
{
["AutoCAD"] = "DWG to PDF.pc3",
["ZWCAD"] = "DWG to PDF.pc5",
["BricsCAD"] = "DWG to PDF.pc3",
};
/// <summary>Výchozí styl tisku (StyleSheet) podle <b>rodiny CADu</b> (klíč jako u <see cref="Tiskarny"/>
/// „AutoCAD"/„ZWCAD"/„BricsCAD" z <see cref="Cad.CadLink.RodinaZProgId"/>), použije se, když <c>Priloha.Ctb</c>
/// je prázdné. <b>Na rozdíl od toho, co se původně čekalo, .ctb soubory NEJSOU stejné napříč CADy</b>
/// každý CAD má svou vlastní sadu ve svém instalačním adresáři (AutoCAD nemá „zwcad.ctb" a naopak),
/// takže je to per-rodina stejně jako <see cref="Tiskarny"/>. Prázdná/chybějící hodnota = CTB se pro
/// danou rodinu nenastaví (zaloguje se jako chyba, zbytek aktualizace pokračuje). POZOR: uveď <b>celý
/// název souboru včetně přípony</b> „.ctb" na rozdíl od původního VBA (které příponu automaticky
/// dopisovalo) se tu nic nedopisuje.</summary>
public Dictionary<string, string> VychoziCtb { get; set; } = new()
{
["AutoCAD"] = "acad.ctb",
["ZWCAD"] = "zwcad.ctb",
["BricsCAD"] = "default.ctb",
};
}
+57
View File
@@ -0,0 +1,57 @@
using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json;
using Rozpisky.Data;
namespace Rozpisky.Cad;
/// <summary>
/// Načítání/ukládání <see cref="LayoutConfig"/> do JSON souboru <b>vedle .exe</b> (portable). Stejný
/// princip jako <see cref="CadEnginesStore"/> konfigurace layoutů/tisku je vlastnost stroje/instalace
/// (bloky a hladiny šablony, dostupné tiskárny), needituje se přes UI, jen ručně v souboru.
/// </summary>
public static class LayoutConfigStore
{
public const string SouborNazev = "layout.json";
private static readonly JsonSerializerOptions Opts = new()
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
/// <summary>Výchozí umístění: soubor vedle .exe (portable).</summary>
public static string DefaultPath => Path.Combine(AppContext.BaseDirectory, SouborNazev);
/// <summary>Načte konfiguraci. Když soubor chybí/je poškozený, vrátí vestavěné výchozí hodnoty
/// (a rovnou je uloží, aby šel soubor dál ručně upravovat).</summary>
public static LayoutConfig Load(string? path = null)
{
path ??= DefaultPath;
try
{
if (File.Exists(path))
{
var cfg = JsonSerializer.Deserialize<LayoutConfig>(File.ReadAllText(path), Opts);
if (cfg is not null) return cfg;
// Soubor existuje, ale nenese data („null") → záloha + upozornění, viz catch níže.
JsonConfigFile.ZalohujAOhlasPoskozeny(path);
}
}
catch (Exception ex) when (ex is IOException or JsonException or UnauthorizedAccessException)
{
// Poškozený / nečitelný soubor → fallback na výchozí hodnoty. Soubor se hned dole
// přepíše seedem, proto se originál (ručně editovaná konfigurace tiskáren!) nejdřív
// zazálohuje a uživatel dozví.
JsonConfigFile.ZalohujAOhlasPoskozeny(path, ex);
}
var seed = new LayoutConfig();
Save(seed, path);
return seed;
}
/// <summary>Uloží konfiguraci (atomicky přes .tmp pád uprostřed zápisu nesmí soubor poškodit).
/// Chyby zápisu (např. read-only složka u .exe v Program Files) propustí volajícímu.</summary>
public static void Save(LayoutConfig cfg, string? path = null)
=> JsonConfigFile.ZapisAtomicky(path ?? DefaultPath, JsonSerializer.Serialize(cfg, Opts));
}
+86
View File
@@ -0,0 +1,86 @@
using System.Collections.ObjectModel;
using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json;
using Rozpisky.Models;
namespace Rozpisky.Data;
/// <summary>
/// Načítání/ukládání sdílených číselníků (organizace, firmy, jména, verze, části)
/// do JSON souboru <b>vedle .exe</b> (portable). Číselníky jsou společné všem projektům
/// (na rozdíl od <c>.rzp</c> přes <see cref="ProjektStore"/>), takže žijí mimo projekt.
/// Bez NuGet závislostí System.Text.Json je součást .NET.
/// </summary>
public static class CiselnikyStore
{
public const string SouborNazev = "ciselniky.json";
private static readonly JsonSerializerOptions Opts = new()
{
WriteIndented = true,
// Diakritika čitelně (ě š č…), ne jako \uXXXX.
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
/// <summary>Výchozí umístění: soubor vedle .exe (portable). Lze přepsat parametrem cesty.</summary>
public static string DefaultPath => Path.Combine(AppContext.BaseDirectory, SouborNazev);
/// <summary>Naplní cílové číselníky obsahem z JSON. Vrací false, když soubor neexistuje
/// nebo je poškozený volající pak nasází výchozí obsah (a uloží nový soubor).</summary>
public static bool LoadInto(Ciselniky target, string? path = null)
{
path ??= DefaultPath;
if (!File.Exists(path)) return false;
try
{
// Čteme do DTO se settable List<T>: System.Text.Json neplní get-only ObservableCollection.
var data = JsonSerializer.Deserialize<Dto>(File.ReadAllText(path), Opts);
if (data is null)
{
// Soubor existuje, ale nenese data („null") → stejné zacházení jako s rozbitým JSON.
JsonConfigFile.ZalohujAOhlasPoskozeny(path);
return false;
}
Replace(target.Organizace, data.Organizace);
Replace(target.Firmy, data.Firmy);
Replace(target.Jmena, data.Jmena);
Replace(target.VerzeDokumentace, data.VerzeDokumentace);
Replace(target.Casti, data.Casti);
Replace(target.Kraje, data.Kraje);
return true;
}
catch (Exception ex) when (ex is IOException or JsonException or UnauthorizedAccessException)
{
// Poškozený / nečitelný soubor → fallback na seed. Volající pak soubor přepíše výchozím
// obsahem, proto se originál nejdřív zazálohuje a uživatel dozví (jinak by o své
// číselníky přišel tiše).
JsonConfigFile.ZalohujAOhlasPoskozeny(path, ex);
return false;
}
}
/// <summary>Uloží číselníky do JSON (atomicky přes .tmp pád uprostřed zápisu nesmí soubor
/// poškodit). Chyby zápisu (např. read-only složka u .exe v Program Files) propustí volajícímu,
/// ať je může ošetřit dle kontextu (autosave je spolkne, ruční uložení zahlásí).</summary>
public static void Save(Ciselniky c, string? path = null)
=> JsonConfigFile.ZapisAtomicky(path ?? DefaultPath, JsonSerializer.Serialize(c, Opts));
private static void Replace<T>(ObservableCollection<T> target, IEnumerable<T> src)
{
target.Clear();
foreach (var x in src) target.Add(x);
}
/// <summary>Přechodný tvar pro čtení settable List&lt;T&gt; (na rozdíl od get-only kolekcí v
/// <see cref="Ciselniky"/>). Názvy vlastností musí odpovídat <see cref="Ciselniky"/> i zápisu.</summary>
private sealed class Dto
{
public List<Organizace> Organizace { get; set; } = new();
public List<Firma> Firmy { get; set; } = new();
public List<Jmeno> Jmena { get; set; } = new();
public List<VerzeDokumentace> VerzeDokumentace { get; set; } = new();
public List<Cast> Casti { get; set; } = new();
public List<Kraj> Kraje { get; set; } = new();
}
}
+99
View File
@@ -0,0 +1,99 @@
using Rozpisky.Models;
using Rozpisky.ViewModels;
namespace Rozpisky.Data;
/// <summary>
/// Ukázková data, aby byl po spuštění vidět styl i tok dat. Rozděleno na dvě části:
/// <see cref="SeedCiselniky"/> = výchozí obsah sdílených číselníků (nasází se jen když
/// vedle .exe ještě není <c>ciselniky.json</c>), a <see cref="SeedProjekt"/> = demo zakázka
/// (přílohy, revize) čistě demo, přepíše se otevřením reálného projektu.
/// </summary>
public static class DemoData
{
/// <summary>Výchozí obsah číselníků pro úplně první spuštění (než vznikne ciselniky.json).</summary>
public static void SeedCiselniky(Ciselniky c)
{
c.Organizace.Add(new Organizace { Nazev = "Správa železnic, s.o.", Adresa = "Dlážděná 1003/7, 110 00 Praha 1", Logo = "sprava_zeleznic.dxf" });
c.Organizace.Add(new Organizace { Nazev = "SŽ Stavební správa západ", Adresa = "Sokolovská 1955/278, 190 00 Praha 9" });
c.Firmy.Add(new Firma { Nazev = "EXPROJEKT a.s.", Adresa = "Nekonečná 8, 182 00 Praha 8", Telefon = "+420 222 333 444", Email = "info@exprojekt.cz", Logo = "exprojekt.dxf" });
c.Firmy.Add(new Firma { Nazev = "SUDOP PRAHA a.s.", Adresa = "Olšanská 2643/1a, 130 80 Praha 3", Telefon = "+420 267 094 111", Email = "sudop@sudop.cz" });
c.Firmy.Add(new Firma { Nazev = "METROPROJEKT Praha a.s.", Adresa = "Argentinská 1621/36, 170 00 Praha 7", Telefon = "+420 296 154 111", Email = "info@metroprojekt.cz" });
foreach (var j in new[] { "Ing. Jan Novák", "Ing. Petr Svoboda", "Ing. Eva Dvořáková", "Ing. Pavel Černý" })
c.Jmena.Add(new Jmeno { Hodnota = j });
foreach (var v in new[] { "DOS", "DUR", "DSP", "PDPS", "DPS", "RDS" })
c.VerzeDokumentace.Add(new VerzeDokumentace { Hodnota = v });
c.Casti.Add(new Cast { Oznaceni = "D", Nazev = "Dokumentace objektů" });
c.Casti.Add(new Cast { Oznaceni = "E", Nazev = "Dokladová část" });
SeedKraje(c);
}
/// <summary>Výchozí seznam krajů ČR (14 krajů). Nasází se jen do prázdného číselníku volá ho
/// jak první spuštění (<see cref="SeedCiselniky"/>), tak backfill starších <c>ciselniky.json</c>,
/// které kraje ještě neměly.</summary>
public static void SeedKraje(Ciselniky c)
{
if (c.Kraje.Count > 0) return;
foreach (var k in new[]
{
"Hlavní město Praha", "Středočeský", "Jihočeský", "Plzeňský", "Karlovarský",
"Ústecký", "Liberecký", "Královéhradecký", "Pardubický", "Kraj Vysočina",
"Jihomoravský", "Olomoucký", "Zlínský", "Moravskoslezský",
})
c.Kraje.Add(new Kraj { Nazev = k });
}
/// <summary>Demo zakázka (údaje projektu + pár příloh + revize). Reference do číselníků se
/// dohledávají podle hodnoty (FirstOrDefault), ať to funguje i s číselníky načtenými ze souboru.</summary>
public static void SeedProjekt(MainViewModel vm)
{
var c = vm.Ciselniky;
vm.Projekt.Stavba = "Modernizace traťového úseku Praha Beroun";
vm.Projekt.HIP = "Ing. Jan Novák";
vm.Projekt.Skod = "S123456789X";
vm.Projekt.CisloZakazky = "2026-014";
vm.SelectedInvestor = c.Organizace.FirstOrDefault();
vm.SelectedSlozkaSZ = c.Organizace.Skip(1).FirstOrDefault() ?? c.Organizace.FirstOrDefault();
vm.SelectedStupen = c.VerzeDokumentace.FirstOrDefault(v => v.Hodnota == "DPS") ?? c.VerzeDokumentace.FirstOrDefault();
vm.SelectedZhotovitelSOD = c.Firmy.FirstOrDefault();
vm.Projekt.SmluvniDatumZpracovani = "2026-03-15";
// Loga financujících institucí (DXF ve složce loga\ vedle .exe).
vm.Projekt.Logo1Soubor = "eu_op_doprava.dxf"; // dotační (vlevo)
vm.Projekt.Logo2Soubor = "md_sfdi.dxf"; // státní (vpravo)
// Demo: navázat loga na vybranou organizaci/firmu, ať se v náhledu ukážou i loga 3/4/5.
// (Stávající ciselniky.json vzniklý před polem Logo ho má prázdný doplníme, pokud chybí.)
if (vm.SelectedInvestor is { } inv && string.IsNullOrWhiteSpace(inv.Logo)) inv.Logo = "sprava_zeleznic.dxf";
if (vm.SelectedZhotovitelSOD is { } zh && string.IsNullOrWhiteSpace(zh.Logo)) zh.Logo = "exprojekt.dxf";
// Zhotovitel objektu = vybraná firma (aby se logo 5 spárovalo dle Priloha.ZhotovitelCasti).
string zhotovitelCasti = vm.SelectedZhotovitelSOD?.Nazev ?? "EXPROJEKT a.s.";
vm.Prilohy.Add(new Priloha
{
OznaceniCasti = "D", OznaceniDilciCasti = "D110", NazevObjektu = "Železniční svršek", CisloObjektu = "SO131.13.12.01",
TypPrilohy = "D.1", DilciPriloha = "1", PoradiPrilohy = "101",
NazevPrilohy = "Situace", Zkratka = "SIT", Vypracoval = "Ing. Petr Svoboda",
OdpovednyProjektant = "Ing. Jan Novák", R1 = "V00", KontrolovalR1 = "Ing. Pavel Černý",
ZhotovitelCasti = zhotovitelCasti, Meritko = "1:1000", Formaty = "4", Kraj = "Středočeský",
KatastralniUzemi = "viz textová část", Ltscale = "1.0", Ctb = "monochrome.ctb",
});
vm.Prilohy.Add(new Priloha
{
OznaceniCasti = "D", OznaceniDilciCasti = "D110", NazevObjektu = "Most v km 12,345", CisloObjektu = "SO201",
TypPrilohy = "D.1", DilciPriloha = "1", PoradiPrilohy = "201",
NazevPrilohy = "Podélný řez", Zkratka = "PR", Vypracoval = "Ing. Eva Dvořáková",
OdpovednyProjektant = "Ing. Jan Novák", R1 = "V00", KontrolovalR1 = "Ing. Pavel Černý",
ZhotovitelCasti = zhotovitelCasti, Meritko = "1:100", Formaty = "2", Kraj = "Středočeský",
KatastralniUzemi = "viz textová část", Ltscale = "1.0", Ctb = "monochrome.ctb",
});
vm.Revize.Add(new Revize { Cislo = "V00", Datum = "-", Popis = "Vyjádření, stanoviska, připomínky" });
vm.Revize.Add(new Revize { Cislo = "V01", Datum = "2026-03-15", Popis = "První vydání" });
}
}
+63
View File
@@ -0,0 +1,63 @@
using System.IO;
using System.Text;
using System.Windows;
namespace Rozpisky.Data;
/// <summary>
/// Společné zacházení s JSON konfiguračními soubory vedle .exe (<c>ciselniky.json</c>,
/// <c>cad.json</c>, <c>layout.json</c>, <c>mapovani.json</c>). Řeší dvě slabiny přímého
/// <c>File.WriteAllText</c>: pád uprostřed zápisu nesmí poškodit původní soubor (atomický zápis
/// přes .tmp) a poškozený soubor se nesmí tiše zahodit ručně editovaný config s překlepem by se
/// jinak bez varování přepsal výchozími hodnotami a uživatel by přišel o vlastní data.
/// </summary>
public static class JsonConfigFile
{
/// <summary>Zapíše text nejdřív do <c>cesta.tmp</c> a pak ho přejmenuje přes cíl. Pád uprostřed
/// zápisu tak nechá původní soubor netknutý (nedokončený .tmp se při dalším zápisu přepíše).</summary>
public static void ZapisAtomicky(string cesta, string obsah)
{
var tmp = cesta + ".tmp";
File.WriteAllText(tmp, obsah);
File.Move(tmp, cesta, overwrite: true);
}
/// <summary>Zavolat, když existující config nejde načíst (rozbité JSON, nečitelný soubor):
/// zkopíruje ho do <c>cesta.bak</c> (starší zálohu přepíše) a uživatele upozorní. Upozornění jde
/// do <c>error.log</c> hned a MessageBox se odloží přes Dispatcher configy se načítají už
/// v konstruktoru VM, kdy hlavní okno ještě nestojí. Kopíruje (nepřesouvá), aby soubor, který
/// nikdo nepřepisuje (mapovani.json), varoval při každém startu, dokud ho uživatel neopraví.</summary>
public static void ZalohujAOhlasPoskozeny(string cesta, Exception? duvod = null)
{
string? zaloha = cesta + ".bak";
try { File.Copy(cesta, zaloha, overwrite: true); }
catch { zaloha = null; } // i nečitelný originál → aspoň upozornit, bez zálohy
var jmeno = Path.GetFileName(cesta);
ZapisDoErrorLogu($"„{jmeno}“ je poškozený ({duvod?.Message ?? "neznámý důvod"}); " +
(zaloha is null ? "záloha se nepodařila." : $"záloha: {zaloha}"));
var zprava = $"Soubor {jmeno} je poškozený nebo nečitelný, načetly se výchozí hodnoty.\n\n" +
(zaloha is null
? "Zálohu původního souboru se nepodařilo vytvořit."
: $"Původní obsah je zálohovaný v:\n{zaloha}") +
"\n\nPodrobnosti jsou v souboru error.log vedle aplikace.";
// BeginInvoke: hláška se ukáže až po rozběhnutí UI smyčky (config se čte před oknem).
Application.Current?.Dispatcher.BeginInvoke(() =>
MessageBox.Show(zprava, "Poškozený konfigurační soubor",
MessageBoxButton.OK, MessageBoxImage.Warning));
}
/// <summary>Zápis do <c>error.log</c> stejným formátem jako globální záchytná síť v App.
/// Nesmí nic shodit (read-only složka apod.).</summary>
private static void ZapisDoErrorLogu(string text)
{
try
{
File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "error.log"),
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Config: {text}{Environment.NewLine}{Environment.NewLine}",
Encoding.UTF8);
}
catch { /* error.log je jen stopa navíc MessageBox se ukáže i tak */ }
}
}
+115
View File
@@ -0,0 +1,115 @@
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using Rozpisky.Models;
namespace Rozpisky.Data;
/// <summary>
/// Společné konstanty a JSON nastavení pro `.rzp` soubory. Vlastní čtení/zápis se zámkem
/// řeší <see cref="ProjektFileHandle"/>. Žádné NuGet závislosti (System.Text.Json je součást .NET).
/// </summary>
public static class ProjektStore
{
public const string Pripona = ".rzp";
public const string DialogFilter = "Projekt Rozpisky (*.rzp)|*.rzp|Všechny soubory (*.*)|*.*";
internal static readonly JsonSerializerOptions Opts = new()
{
WriteIndented = true,
// Aby se diakritika v souboru psala čitelně (ě š č…), ne jako \uXXXX.
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
}
/// <summary>
/// Otevřený `.rzp` soubor s exkluzivním zámkem drženým po celou dobu, co je projekt
/// otevřený. Díky <see cref="FileShare.Read"/> ho ostatní procesy smí jen číst/kopírovat,
/// ale nejde smazat, přejmenovat ani přepsat Windows ho hlásí jako „používaný". Ukládání
/// jde přes držený handle (zámek se neuvolní). Zavře se přes <see cref="Dispose"/> (zavření
/// projektu/appky); při pádu uvolní zámek operační systém sám.
/// </summary>
public sealed class ProjektFileHandle : IDisposable
{
private readonly FileStream _stream;
/// <summary>Cesta k zamčenému souboru.</summary>
public string Cesta { get; }
private ProjektFileHandle(string cesta, FileStream stream)
{
Cesta = cesta;
_stream = stream;
}
/// <summary>Otevře existující soubor s exkluzivním zámkem a vrátí načtený dokument.
/// Vyhodí <see cref="IOException"/>, když je soubor otevřený jinde (sdílecí porušení).</summary>
public static ProjektFileHandle Otevri(string cesta, out ProjektDocument doc)
{
var fs = new FileStream(cesta, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
try
{
string text = Cti(fs);
doc = JsonSerializer.Deserialize<ProjektDocument>(text, ProjektStore.Opts)
?? throw new InvalidDataException("Soubor projektu je prázdný nebo poškozený.");
return new ProjektFileHandle(cesta, fs);
}
catch
{
fs.Dispose(); // při chybě zámek nedržíme
throw;
}
}
/// <summary>Vytvoří/přepíše soubor na nové cestě, zapíše dokument a podrží zámek.
/// Vyhodí <see cref="IOException"/>, když je cílový soubor otevřený jinde.</summary>
public static ProjektFileHandle Vytvor(string cesta, ProjektDocument doc)
{
var fs = new FileStream(cesta, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
try
{
Zapis(fs, doc);
return new ProjektFileHandle(cesta, fs);
}
catch
{
fs.Dispose();
throw;
}
}
/// <summary>Přečte soubor jen pro čtení (bez zámku) i když ho jiný proces drží otevřený.
/// Žádá pouze <see cref="FileAccess.Read"/> a povoluje <see cref="FileShare.ReadWrite"/>, takže
/// nekoliduje s cizím zámkem. Handle se nedrží vrací jen načtený dokument.</summary>
public static ProjektDocument CtiJenProCteni(string cesta)
{
using var fs = new FileStream(cesta, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return JsonSerializer.Deserialize<ProjektDocument>(Cti(fs), ProjektStore.Opts)
?? throw new InvalidDataException("Soubor projektu je prázdný nebo poškozený.");
}
/// <summary>Uloží dokument přes držený zámek (soubor zůstává zamčený).</summary>
public void Uloz(ProjektDocument doc) => Zapis(_stream, doc);
private static void Zapis(FileStream fs, ProjektDocument doc)
{
// Serializace musí proběhnout PŘED useknutím souboru kdyby vyhodila výjimku
// až po SetLength(0), zůstal by soubor prázdný a projekt by byl nenávratně pryč.
string json = JsonSerializer.Serialize(doc, ProjektStore.Opts);
fs.Seek(0, SeekOrigin.Begin);
fs.SetLength(0);
using (var w = new StreamWriter(fs, new UTF8Encoding(false), 1024, leaveOpen: true))
w.Write(json);
fs.Flush(flushToDisk: true);
}
private static string Cti(FileStream fs)
{
fs.Seek(0, SeekOrigin.Begin);
using var r = new StreamReader(fs, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, 1024, leaveOpen: true);
return r.ReadToEnd();
}
public void Dispose() => _stream.Dispose();
}
+685
View File
@@ -0,0 +1,685 @@
using System.IO;
using System.Text.RegularExpressions;
using ACadSharp;
using ACadSharp.Entities;
using ACadSharp.IO;
using ACadSharp.Objects;
using CSMath;
using Rozpisky.Rendering;
using AcTextStyle = ACadSharp.Tables.TextStyle;
using FontFlags = ACadSharp.Tables.FontFlags;
namespace Rozpisky.Dxf;
/// <summary>
/// Načte DXF šablonu rozpisky (ACadSharp) a „přehraje" ji do <see cref="IProfileRenderer"/>
/// (ROZPISKY.md §4). Statická geometrie + popisky se kreslí vždy; pole (ATTDEF) se buď
/// vykreslí jako názvy (předpis), nebo vyplní hodnotami. ACadSharp na rozdíl od netDxf
/// vystaví font ze stylu i box-šířku víceřádkových atributů (XRecord, group 41).
/// </summary>
public sealed partial class DxfTemplate
{
private readonly CadDocument _doc;
/// <summary>Hladina vodicího rámečku uvnitř vkládaných DXF (loga/schéma) definuje návrhový
/// obdélník pro umístění. Nekreslí se a slouží jako reference pro fit místo bboxu kresby.</summary>
public const string HladinaNoPrint = "!!NOPRINT";
/// <summary>Převod úhlu z radiánů (ACadSharp vrací rotace v radiánech) na stupně (renderer).</summary>
private const double RadToDeg = 180.0 / Math.PI;
/// <summary>Hladiny vyhrazené pro placeholder-rámečky (loga + orientační schéma). Obdélníky na
/// těchto hladinách jsou jen vodítka (poloha + max. velikost), nekreslí se (mají PlotFlag=0).</summary>
public static readonly IReadOnlyList<string> PlaceholderHladiny = new[]
{
"Rozpiska_logo1", "Rozpiska_logo2", "Rozpiska_logo3", "Rozpiska_logo4", "Rozpiska_logo5",
"Rozpiska_Orientační schéma",
};
public DxfTemplate(string path)
{
_doc = DxfReader.Read(path) ?? throw new InvalidDataException($"DXF nelze načíst: {path}");
SheetFrame = FindSheetFrame();
Placeholders = FindPlaceholders();
}
/// <summary>Bounds A4 rámečku šablony (uzavřená polylinie ~A4) referenční obdélník pro mapování
/// na papír 1:1, aby rozpiska byla vystředěná dle návrhu. Null = nenalezen (export centruje obsah).</summary>
public (double X, double Y, double W, double H)? SheetFrame { get; }
/// <summary>Rámečky placeholderů (hladina → obdélník v mm) z uzavřených polylinií na
/// <see cref="PlaceholderHladiny"/>. Do těchto obdélníků se fitují loga / orientační schéma.</summary>
public IReadOnlyDictionary<string, (double X, double Y, double W, double H)> Placeholders { get; }
private (double X, double Y, double W, double H)? _bounds;
private bool _boundsComputed;
/// <summary>Ohraničení (bbox) kreslené geometrie a textů v mm slouží k výpočtu fitu, když se
/// tato šablona použije jako vkládaný fragment (logo / schéma). Null = prázdná/degenerovaná kresba.</summary>
public (double X, double Y, double W, double H)? Bounds
{
get
{
if (!_boundsComputed) { _bounds = ComputeBounds(); _boundsComputed = true; }
return _bounds;
}
}
private (double X, double Y, double W, double H)? _frameBounds;
private bool _frameComputed;
/// <summary>Bbox vodicího rámečku na hladině <see cref="HladinaNoPrint"/> (pokud existuje)
/// preferovaná reference pro fit vkládaného loga/schématu místo bboxu kresby. Null = bez rámečku.</summary>
public (double X, double Y, double W, double H)? FrameBounds
{
get
{
if (!_frameComputed) { _frameBounds = ComputeFrameBounds(); _frameComputed = true; }
return _frameBounds;
}
}
private IEnumerable<AttributeDefinition> AttrDefs => _doc.Entities.OfType<AttributeDefinition>();
/// <summary>Přehraje šablonu do kreslítka. <paramref name="values"/> = null → vykreslí názvy polí.
/// <paramref name="includeNoPrint"/> = true → vykreslí i geometrii na hladině <see cref="HladinaNoPrint"/>
/// (např. A4 rámeček) pro DXF export jako referenci hranice papíru.</summary>
public void Replay(IProfileRenderer g, IReadOnlyDictionary<string, string>? 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())
{
if (!SetLayer(g, e, includeNoPrint)) continue;
switch (e)
{
case Line l:
g.Line(l.StartPoint.X, l.StartPoint.Y, l.EndPoint.X, l.EndPoint.Y);
break;
case LwPolyline pl:
KresliPolyline(g, pl.Vertices.Select(v => (v.Location.X, v.Location.Y, v.Bulge)).ToList(), pl.IsClosed);
break;
// Klasické 2D polylinie (Polyline2D) v mapách/schématech časté (tratě), i s bulge oblouky.
case Polyline2D pl:
KresliPolyline(g, pl.Vertices.Select(v => (v.Location.X, v.Location.Y, v.Bulge)).ToList(), pl.IsClosed);
break;
// Spliny (NURBS) tesselace na lomenou čáru přes ACadSharp.
case Spline sp:
{
var pts = sp.PolygonalVertexes(64).Select(p => (p.X, p.Y)).ToList();
if (pts.Count >= 2) g.Polyline(pts, sp.IsClosed);
break;
}
// Oblouk (Arc dědí z Circle) analytický, aby byl hladký v každém přiblížení.
case Arc a:
g.Stroke(ArcContour(a));
break;
case Circle c:
g.Circle(c.Center.X, c.Center.Y, c.Radius);
break;
// Výplně (HATCH) vyplněné plochy (typicky loga). Každá cesta = smyčka, even-odd díry.
case Hatch hatch:
{
var loops = HatchSmycky(hatch);
if (loops.Count > 0) g.Fill(loops);
break;
}
// Pole (ATTDEF) buď názvy polí (předpis), nebo vyplněné hodnoty.
case AttributeDefinition ad:
{
string text;
if (values is null)
{
text = ad.Tag; // předpis vykresli název pole
}
else
{
values.TryGetValue(NormalizeTag(ad.Tag), out var val);
if (string.IsNullOrEmpty(val)) break; // nevyplněné pole = prázdné
text = val;
}
var (x, y, h, v) = Place(ad.HorizontalAlignment, ad.VerticalAlignment, ad.InsertPoint, ad.AlignmentPoint);
// Víceřádkové atributy nesou box-šířku (rectangle width) v XRecordu group 41.
g.Text(x, y, text, ad.Height, h, v, ad.Rotation * RadToDeg, StyleOf(ad.Style, ad.ObliqueAngle), ad.WidthFactor, MultilineWidth(ad));
break;
}
// Ostatní atributy (hodnoty INSERTů) nekreslíme řeší se přes ATTDEF šablony.
case AttributeBase:
break;
case TextEntity t:
{
var (x, y, h, v) = Place(t.HorizontalAlignment, t.VerticalAlignment, t.InsertPoint, t.AlignmentPoint);
g.Text(x, y, t.Value, t.Height, h, v, t.Rotation * RadToDeg, StyleOf(t.Style, t.ObliqueAngle), t.WidthFactor);
break;
}
case MText mt:
{
var (h, v) = MapMTextAlign(mt.AttachmentPoint);
g.Text(mt.InsertPoint.X, mt.InsertPoint.Y, mt.PlainText, mt.Height, h, v, mt.Rotation * RadToDeg,
StyleOf(mt.Style), 1, mt.RectangleWidth);
break;
}
}
}
}
/// <summary>Entity model-space v pořadí kresby: pokud má blok tabulku pořadí (<c>SortEntitiesTable</c>,
/// 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 <c>OrderBy</c> je stabilní → entity bez záznamu drží pořadí.</summary>
private IEnumerable<Entity> EntitiesInDrawOrder()
{
var ents = _doc.Entities.OfType<Entity>();
var sort = _doc.ModelSpace?.SortEntitiesTable;
if (sort is null) return ents;
return ents.OrderBy(e => SortHandle(sort, e));
static ulong SortHandle(ACadSharp.Objects.SortEntitiesTable t, Entity e)
{
try { return t.GetSorterHandle(e); } catch { return e.Handle; }
}
}
/// <summary>Vykreslí LwPolyline/Polyline2D: bez oblouků jako lomenou čáru (<c>Polyline</c>),
/// s bulge jako obrys se zachovanými kruhovými oblouky (<c>Stroke</c>).</summary>
private static void KresliPolyline(IProfileRenderer g, IReadOnlyList<(double X, double Y, double Bulge)> verts, bool closed)
{
if (verts.Count < 2) return;
bool maOblouk = false;
for (int i = 0; i < verts.Count; i++) if (Math.Abs(verts[i].Bulge) > 1e-9) { maOblouk = true; break; }
if (!maOblouk) { g.Polyline(verts.Select(v => (v.X, v.Y)).ToList(), closed); return; }
g.Stroke(PolylineContour(verts, closed));
}
/// <summary>Nastaví hladinu do kreslítka. Vrací false, pokud se hladina nemá tisknout (přeskočit kresbu).
/// <paramref name="includeNoPrint"/> = true → geometrie na <see cref="HladinaNoPrint"/> se nepřeskočí
/// (kreslí se i přes vypnutý plot flag DXF reference A4 rámečku).</summary>
private static bool SetLayer(IProfileRenderer g, Entity e, bool includeNoPrint = false)
{
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;
var col = e.Color.IsByLayer && layer is not null ? layer.Color : e.Color; // ByLayer → barva hladiny
int rgb = (col.R << 16) | (col.G << 8) | col.B; // ACadSharp rozbalí i index/true-color
int lw = (int)e.LineWeight; // LineWeightType: Wnn = nn/100 mm, záporné = ByLayer/Default/ByBlock
if (lw < 0 && layer is not null) lw = (int)layer.LineWeight;
g.Layer(layer?.Name ?? "0", rgb, lw > 0 ? lw / 100.0 : 0.18);
return true;
}
/// <summary>Zjistí kotvu a zarovnání textu (Left+Baseline → vkládací bod, jinak zarovnávací bod).</summary>
private static (double x, double y, HAlign h, VAlign v) Place(
TextHorizontalAlignment ha, TextVerticalAlignmentType va, XYZ insert, XYZ align)
{
var h = ha switch
{
TextHorizontalAlignment.Center or TextHorizontalAlignment.Middle => HAlign.Center,
TextHorizontalAlignment.Right => HAlign.Right,
_ => HAlign.Left, // Left / Aligned / Fit (aproximace)
};
var v = va switch
{
TextVerticalAlignmentType.Top => VAlign.Top,
TextVerticalAlignmentType.Middle => VAlign.Middle,
TextVerticalAlignmentType.Bottom => VAlign.Bottom,
_ => VAlign.Baseline,
};
bool useAlign = ha != TextHorizontalAlignment.Left || va != TextVerticalAlignmentType.Baseline;
var p = useAlign ? align : insert;
return (p.X, p.Y, h, v);
}
/// <summary>Převede DXF textový styl na vzhled písma rendereru. Pro TTF předá soubor
/// (přesný řez verdanab.ttf), SHX → fallback. Řez i jako záloha z TrueType flagů.
/// <paramref name="entityObliqueRad"/> = oblique úhel entity (group 51, radiány) rozhodující;
/// při 0 se použije oblique stylu (group 50 je jen výchozí hodnota při tvorbě textu).</summary>
private static Rozpisky.Rendering.TextStyle StyleOf(AcTextStyle? s, double entityObliqueRad = 0)
{
double obliqueRad = entityObliqueRad != 0 ? entityObliqueRad : s?.ObliqueAngle ?? 0;
if (s is null) return new(ObliqueDeg: obliqueRad * RadToDeg);
string? file = s.IsShapeFile || string.IsNullOrWhiteSpace(s.Filename) ? null : s.Filename;
bool bold = s.TrueType.HasFlag(FontFlags.Bold);
bool italic = s.TrueType.HasFlag(FontFlags.Italic);
// Rodina písma: z .ttf souboru; když styl soubor nemá (TTF zadané jen jménem), vezmi
// název typu písma z XData(ACAD) tam AutoCAD ukládá rodinu (např. „Arial Black").
string? family = file is not null ? Path.GetFileNameWithoutExtension(file) : FontRodinaZeXData(s);
return new(family, bold, italic, file, obliqueRad * RadToDeg);
}
/// <summary>Název rodiny TrueType písma uložený ve stylu jako XData appid „ACAD" (první ASCII
/// řetězec), používaný když styl nemá <c>.ttf</c> soubor. Null = není.</summary>
private static string? FontRodinaZeXData(AcTextStyle s)
{
if (s.ExtendedData is not { } xd) return null;
foreach (var entry in xd)
{
if (!string.Equals(entry.Key?.Name, "ACAD", StringComparison.OrdinalIgnoreCase)) continue;
var recs = entry.Value?.Records;
if (recs is null) continue;
foreach (var rec in recs)
if (rec.GetType().GetProperty("Value")?.GetValue(rec) is string str && !string.IsNullOrWhiteSpace(str))
return str.Trim();
}
return null;
}
/// <summary>Najde A4 rámeček: uzavřenou polylinii s bbox ≈ A4 (210×297 / 297×210), největší vyhovující.</summary>
private (double X, double Y, double W, double H)? FindSheetFrame()
{
(double X, double Y, double W, double H)? best = null;
foreach (var pl in _doc.Entities.OfType<LwPolyline>())
{
if (!pl.IsClosed || pl.Vertices.Count < 4) continue;
double ax = double.MaxValue, ay = double.MaxValue, bx = double.MinValue, by = double.MinValue;
foreach (var v in pl.Vertices)
{
ax = Math.Min(ax, v.Location.X); ay = Math.Min(ay, v.Location.Y);
bx = Math.Max(bx, v.Location.X); by = Math.Max(by, v.Location.Y);
}
double w = bx - ax, h = by - ay;
bool a4 = (Near(w, 297) && Near(h, 210)) || (Near(w, 210) && Near(h, 297));
if (a4 && (best is null || w * h > best.Value.W * best.Value.H))
best = (ax, ay, w, h);
}
return best;
static bool Near(double a, double b) => Math.Abs(a - b) < 2.0;
}
/// <summary>Najde rámečky placeholderů: uzavřené polylinie na <see cref="PlaceholderHladiny"/>,
/// pro každou hladinu bbox vrcholů. Při více polyliniích na hladině vezme největší.</summary>
private IReadOnlyDictionary<string, (double X, double Y, double W, double H)> FindPlaceholders()
{
var hladiny = new HashSet<string>(PlaceholderHladiny, StringComparer.OrdinalIgnoreCase);
var res = new Dictionary<string, (double X, double Y, double W, double H)>(StringComparer.OrdinalIgnoreCase);
foreach (var pl in _doc.Entities.OfType<LwPolyline>())
{
var name = pl.Layer?.Name;
if (name is null || !hladiny.Contains(name) || pl.Vertices.Count < 2) continue;
double ax = double.MaxValue, ay = double.MaxValue, bx = double.MinValue, by = double.MinValue;
foreach (var v in pl.Vertices)
{
ax = Math.Min(ax, v.Location.X); ay = Math.Min(ay, v.Location.Y);
bx = Math.Max(bx, v.Location.X); by = Math.Max(by, v.Location.Y);
}
var rect = (ax, ay, bx - ax, by - ay);
if (!res.TryGetValue(name, out var prev) || rect.Item3 * rect.Item4 > prev.W * prev.H)
res[name] = rect;
}
return res;
}
/// <summary>Spočítá bbox kreslené geometrie (čáry, polylinie, výplně, texty) pro fit fragmentu.
/// Přehrává tutéž kresbu jako <see cref="Replay"/> do <see cref="BoundsRenderer"/>, takže bbox
/// vždy odpovídá tomu, co se skutečně kreslí (texty včetně odhadu šířky/výšky, ne jen kotvy).
/// Prázdný slovník hodnot → pole (ATTDEF) se přeskočí; null by vykreslil názvy tagů.
/// Vodicí rámeček na hladině <see cref="HladinaNoPrint"/> se nezapočítává (řeší <see cref="FrameBounds"/>).</summary>
private (double X, double Y, double W, double H)? ComputeBounds()
{
var b = new BoundsRenderer();
Replay(b, values: new Dictionary<string, string>());
return b.IsEmpty || b.Width <= 0 || b.Height <= 0 ? null : (b.MinX, b.MinY, b.Width, b.Height);
}
/// <summary>Bbox vodicího rámečku (uzavřená polylinie / čáry na hladině <see cref="HladinaNoPrint"/>).</summary>
private (double X, double Y, double W, double H)? ComputeFrameBounds()
{
double ax = double.MaxValue, ay = double.MaxValue, bx = double.MinValue, by = double.MinValue;
bool any = false;
void Add(double x, double y) { ax = Math.Min(ax, x); ay = Math.Min(ay, y); bx = Math.Max(bx, x); by = Math.Max(by, y); any = true; }
foreach (var pl in _doc.Entities.OfType<LwPolyline>().Where(JeNoPrint))
foreach (var v in pl.Vertices) Add(v.Location.X, v.Location.Y);
foreach (var l in _doc.Entities.OfType<Line>().Where(JeNoPrint))
{
Add(l.StartPoint.X, l.StartPoint.Y); Add(l.EndPoint.X, l.EndPoint.Y);
}
if (!any) return null;
double w = bx - ax, h = by - ay;
return w > 0 && h > 0 ? (ax, ay, w, h) : null;
}
/// <summary>True, pokud entita leží na vodicí hladině <see cref="HladinaNoPrint"/> (nekreslit, jen reference).</summary>
private static bool JeNoPrint(Entity e) => string.Equals(e.Layer?.Name, HladinaNoPrint, StringComparison.OrdinalIgnoreCase);
/// <summary>Převede hraniční cesty HATCH na vyplnitelné obrysy (<see cref="Contour"/>) se
/// zachovanými kruhovými oblouky. Každá cesta = jedna smyčka; hrany (úsečky, oblouky, elipsy,
/// polylinie s bulge, spliny) se převedou na analytické <see cref="Edge"/> a zřetězí za sebou
/// navazující hrana může být v souboru orientovaná i opačně (řeší se otočením běhu hran).</summary>
private static List<Contour> HatchSmycky(Hatch hatch)
{
// Souřadnice hranic jsou v OCS entity (dané extruzní normálou group 210/220/230). Pro normálu
// (0,0,1) je převod do WCS identita; zrcadlené hatch (např. písmeno „O" v mco.dxf má normálu
// (0,0,-1)) by se bez převodu vykreslily na chybné X pozici. Osy OCS spočteme jednou pro celý
// hatch. Kladný determinant os = přímá izometrie (rotace) → oblouky lze převést analyticky
// (pootočením úhlu); záporný = zrcadlení → oblouky/elipsy raději jemně zploštíme na úsečky.
var (axx, axy, ayx, ayy) = OcsOsy(hatch.Normal.X, hatch.Normal.Y, hatch.Normal.Z);
(double X, double Y) ToWcs(double x, double y) => (x * axx + y * ayx, x * axy + y * ayy);
double det = axx * ayy - axy * ayx;
bool direct = det > 0;
double alpha = Math.Atan2(axy, axx); // úhel pootočení os OCS→WCS (jen pro direct case)
var loops = new List<Contour>();
foreach (var path in hatch.Paths)
{
var loop = new List<Edge>();
foreach (var edge in path.Edges)
{
var run = HranaNaEdges(edge, ToWcs, direct, alpha);
if (run.Count == 0) continue;
if (loop.Count > 0)
{
var last = (loop[^1].EX, loop[^1].EY);
// Orientaci běhu hran určí bližší konec k dosavadnímu konci smyčky.
if (Dist2(last, (run[0].SX, run[0].SY)) > Dist2(last, (run[^1].EX, run[^1].EY)))
run = ReverseRun(run);
}
loop.AddRange(run);
}
// ≥1 hrana: samostatný kruhový/eliptický oblouk (1 hrana) je platná výplň (plný kruh).
if (loop.Count >= 1) loops.Add(new Contour(loop, Closed: true));
}
return loops;
static double Dist2((double X, double Y) a, (double X, double Y) b)
{
double dx = a.X - b.X, dy = a.Y - b.Y;
return dx * dx + dy * dy;
}
}
/// <summary>Otočí běh hran (obrácené pořadí i směr každé hrany), aby navázal na předchozí konec.</summary>
private static List<Edge> ReverseRun(List<Edge> run)
{
var res = new List<Edge>(run.Count);
for (int i = run.Count - 1; i >= 0; i--) res.Add(run[i].Reversed());
return res;
}
/// <summary>Převede jednu hranu HATCH hranice na analytické <see cref="Edge"/> ve WCS. Kruhové
/// oblouky (přímý ARC i bulge v polylinii) se při přímé izometrii OCS (<paramref name="direct"/>)
/// zachovají analyticky (pootočení úhlu o <paramref name="alpha"/>); elipsy, spliny a oblouky pod
/// zrcadlením se jemně zploští na úsečky (viz <see cref="LineEdgesZOcs"/>). Nepodporovaný typ → prázdné.</summary>
private static List<Edge> HranaNaEdges(Hatch.BoundaryPath.Edge edge,
Func<double, double, (double X, double Y)> toWcs, bool direct, double alpha)
{
switch (edge)
{
case Hatch.BoundaryPath.Line ln:
{
var s = toWcs(ln.Start.X, ln.Start.Y);
var e = toWcs(ln.End.X, ln.End.Y);
return new() { Edge.Line(s.X, s.Y, e.X, e.Y) };
}
case Hatch.BoundaryPath.Polyline ple:
{
var verts = ple.Vertices;
if (verts.Count < 2) return new();
var bulges = ple.Bulges?.ToList();
var edges = new List<Edge>();
int n = verts.Count;
int segments = ple.IsClosed ? n : n - 1;
for (int i = 0; i < segments; i++)
{
double ax = verts[i].X, ay = verts[i].Y;
double bx = verts[(i + 1) % n].X, by = verts[(i + 1) % n].Y;
double bulge = bulges is not null && i < bulges.Count ? bulges[i] : 0;
if (Math.Abs(bulge) > 1e-9)
{
if (direct) edges.Add(BulgeEdge(ax, ay, bx, by, bulge, toWcs, alpha));
else
{
var body = new List<(double X, double Y)> { (ax, ay) };
ZplostiOblouk(body, ax, ay, bx, by, bulge);
body.Add((bx, by));
edges.AddRange(LineEdgesZOcs(body, toWcs));
}
}
else
{
var s = toWcs(ax, ay); var e = toWcs(bx, by);
edges.Add(Edge.Line(s.X, s.Y, e.X, e.Y));
}
}
return edges;
}
case Hatch.BoundaryPath.Arc arc when direct:
return new() { ArcEdge(arc.Center.X, arc.Center.Y, arc.Radius, arc.StartAngle, arc.EndAngle, arc.CounterClockWise, toWcs, alpha) };
case Hatch.BoundaryPath.Arc arc:
return LineEdgesZOcs(VzorkujArc(arc), toWcs);
case Hatch.BoundaryPath.Ellipse el:
// Elipsy vždy jemně zploštíme (analytický převod přes zrcadlení OCS by byl křehký).
return LineEdgesZOcs(VzorkujEllipse(el), toWcs);
case Hatch.BoundaryPath.Spline sp:
{
var pts = sp.FitPoints is { Count: > 1 } fit
? fit.Select(p => (p.X, p.Y)).ToList()
: sp.ControlPoints?.Select(p => (p.X, p.Y)).ToList() ?? new();
return LineEdgesZOcs(pts, toWcs);
}
default:
return new();
}
}
/// <summary>Analytický kruhový oblouk hatch hranice (přímá izometrie OCS). Úhly v radiánech,
/// <paramref name="ccw"/>=false znamená v DXF zrcadlení přes osu X OCS → ve WCS úhel <c>α−sa</c>
/// se záporným rozsahem; <paramref name="ccw"/>=true → <c>sa+α</c> s kladným rozsahem.</summary>
private static Edge ArcEdge(double cx, double cy, double r, double sa, double ea, bool ccw,
Func<double, double, (double X, double Y)> toWcs, double alpha)
{
if (ea <= sa) ea += 2 * Math.PI;
double mag = ea - sa;
var c = toWcs(cx, cy);
double a0 = ccw ? sa + alpha : alpha - sa;
double sweep = ccw ? mag : -mag;
var s = (c.X + r * Math.Cos(a0), c.Y + r * Math.Sin(a0));
var e = (c.X + r * Math.Cos(a0 + sweep), c.Y + r * Math.Sin(a0 + sweep));
return Edge.Arc(s.Item1, s.Item2, e.Item1, e.Item2, c.X, c.Y, r, a0, sweep);
}
/// <summary>Analytický oblouk z bulge segmentu (přímá izometrie OCS): standardní parametrizace
/// <c>střed + r·(cos θ, sin θ)</c> → úhel se pootočí o <paramref name="alpha"/>, rozsah = θ.</summary>
private static Edge BulgeEdge(double ax, double ay, double bx, double by, double bulge,
Func<double, double, (double X, double Y)> toWcs, double alpha)
{
var (cx, cy, r, sa, theta) = BulgeArc(ax, ay, bx, by, bulge);
var c = toWcs(cx, cy);
var s = toWcs(ax, ay); var e = toWcs(bx, by);
return Edge.Arc(s.X, s.Y, e.X, e.Y, c.X, c.Y, r, sa + alpha, theta);
}
/// <summary>Střed, (kladný) poloměr, počáteční úhel a znaménkový rozsah oblouku z DXF bulge
/// (=tan(θ/4)) mezi body (x1,y1)(x2,y2); standardní parametrizace (bod = střed + r·(cos,sin)).</summary>
private static (double Cx, double Cy, double R, double Sa, double Theta) BulgeArc(
double x1, double y1, double x2, double y2, double bulge)
{
double theta = 4.0 * Math.Atan(bulge);
double dx = x2 - x1, dy = y2 - y1, chord = Math.Sqrt(dx * dx + dy * dy);
double r = chord / (2.0 * Math.Sin(theta / 2.0));
double mx = (x1 + x2) / 2.0, my = (y1 + y2) / 2.0;
double dd = r * Math.Cos(theta / 2.0);
double ux = -dy / chord, uy = dx / chord;
double cx = mx + dd * ux, cy = my + dd * uy;
double sa = Math.Atan2(y1 - cy, x1 - cx);
return (cx, cy, Math.Abs(r), sa, theta);
}
/// <summary>Převede řadu bodů v OCS na navazující úsečkové hrany ve WCS (přeskočí degenerované).</summary>
private static List<Edge> LineEdgesZOcs(IReadOnlyList<(double X, double Y)> ocsPts,
Func<double, double, (double X, double Y)> toWcs)
{
var edges = new List<Edge>();
if (ocsPts.Count < 2) return edges;
var prev = toWcs(ocsPts[0].X, ocsPts[0].Y);
for (int i = 1; i < ocsPts.Count; i++)
{
var cur = toWcs(ocsPts[i].X, ocsPts[i].Y);
double ddx = cur.X - prev.X, ddy = cur.Y - prev.Y;
if (ddx * ddx + ddy * ddy > 1e-14) { edges.Add(Edge.Line(prev.X, prev.Y, cur.X, cur.Y)); prev = cur; }
}
return edges;
}
/// <summary>Navzorkuje kruhový oblouk hatch hranice na body v OCS (zrcadlení CW přes osu X).</summary>
private static List<(double X, double Y)> VzorkujArc(Hatch.BoundaryPath.Arc arc)
{
double sa = arc.StartAngle, ea = arc.EndAngle;
if (ea <= sa) ea += 2 * Math.PI;
double sweep = ea - sa, sign = arc.CounterClockWise ? 1 : -1;
int seg = ContourGeometry.ArcSegments(sweep, arc.Radius, 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;
pts.Add((arc.Center.X + arc.Radius * Math.Cos(t), arc.Center.Y + sign * arc.Radius * Math.Sin(t)));
}
return pts;
}
/// <summary>Navzorkuje eliptický oblouk hatch hranice na body v OCS (kolmice·ratio, CW přes osu X).</summary>
private static List<(double X, double Y)> VzorkujEllipse(Hatch.BoundaryPath.Ellipse el)
{
double sa = el.StartAngle, ea = el.EndAngle;
if (ea <= sa) ea += 2 * Math.PI;
double sweep = ea - sa, sign = el.CounterClockWise ? 1 : -1;
double mx = el.MajorAxisEndPoint.X, my = el.MajorAxisEndPoint.Y;
double vx = -my * el.RadiusRatio, vy = mx * el.RadiusRatio;
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 = sign * Math.Sin(t);
pts.Add((el.Center.X + c * mx + s * vx, el.Center.Y + c * my + s * vy));
}
return pts;
}
/// <summary>Analytický obrys samostatné entity ARC (vždy proti směru hodin od StartAngle k EndAngle).</summary>
private static Contour ArcContour(Arc a)
{
double sa = a.StartAngle, ea = a.EndAngle;
if (ea <= sa) ea += 2 * Math.PI;
double r = a.Radius, cx = a.Center.X, cy = a.Center.Y;
var s = (cx + r * Math.Cos(sa), cy + r * Math.Sin(sa));
var e = (cx + r * Math.Cos(ea), cy + r * Math.Sin(ea));
return new Contour(new[] { Edge.Arc(s.Item1, s.Item2, e.Item1, e.Item2, cx, cy, r, sa, ea - sa) }, Closed: false);
}
/// <summary>Obrys stroked polylinie (LwPolyline/Polyline2D) se zachovanými oblouky z bulge.
/// Souřadnice bere jako WCS (normálu entity ignoruje shodně s předchozím chováním).</summary>
private static Contour PolylineContour(IReadOnlyList<(double X, double Y, double Bulge)> verts, bool closed)
{
var edges = new List<Edge>();
int n = verts.Count;
int segments = closed ? n : n - 1;
for (int i = 0; i < segments; i++)
{
var a = verts[i]; var b = verts[(i + 1) % n];
if (Math.Abs(a.Bulge) > 1e-9)
{
var (cx, cy, r, sa, theta) = BulgeArc(a.X, a.Y, b.X, b.Y, a.Bulge);
edges.Add(Edge.Arc(a.X, a.Y, b.X, b.Y, cx, cy, r, sa, theta));
}
else edges.Add(Edge.Line(a.X, a.Y, b.X, b.Y));
}
return new Contour(edges, closed);
}
/// <summary>Osy OCS (arbitrary axis algorithm dle DXF specifikace) pro převod 2D bodů entity
/// s extruzní normálou <paramref name="nx"/>,<paramref name="ny"/>,<paramref name="nz"/> do WCS:
/// WCS = ocsX·ax + ocsY·ay. Pro normálu (0,0,1) je to identita, pro (0,0,-1) zrcadlí X. Vrací
/// jen XY složky obou os (Z se pro 2D kresbu ignoruje).</summary>
private static (double axx, double axy, double ayx, double ayy) OcsOsy(double nx, double ny, double nz)
{
double len = Math.Sqrt(nx * nx + ny * ny + nz * nz);
if (len < 1e-12) return (1, 0, 0, 1); // degenerovaná normála → identita
nx /= len; ny /= len; nz /= len;
// Referenční osa: Wy pro (téměř) svislou normálu, jinak Wz.
double rx, ry, rz;
if (Math.Abs(nx) < 1.0 / 64 && Math.Abs(ny) < 1.0 / 64) { rx = 0; ry = 1; rz = 0; }
else { rx = 0; ry = 0; rz = 1; }
// ax = R × N (normalizované), ay = N × ax (normalizované)
double axx = ry * nz - rz * ny, axy = rz * nx - rx * nz, axz = rx * ny - ry * nx;
double al = Math.Sqrt(axx * axx + axy * axy + axz * axz);
axx /= al; axy /= al; axz /= al;
double ayx = ny * axz - nz * axy, ayy = nz * axx - nx * axz;
double ayl = Math.Sqrt(ayx * ayx + ayy * ayy + (nx * axy - ny * axx) * (nx * axy - ny * axx));
return (axx, axy, ayx / ayl, ayy / ayl);
}
/// <summary>Přidá body oblouku (bez koncových) mezi (x1,y1) a (x2,y2) dle DXF bulge
/// (=tan(θ/4), θ = úhel oblouku; kladný = proti směru hodin).</summary>
private static void ZplostiOblouk(List<(double X, double Y)> body, double x1, double y1, double x2, double y2, double bulge)
{
double theta = 4.0 * Math.Atan(bulge); // úhel oblouku (se znaménkem)
double dx = x2 - x1, dy = y2 - y1;
double chord = Math.Sqrt(dx * dx + dy * dy);
if (chord < 1e-9) return;
double r = chord / (2.0 * Math.Sin(theta / 2.0)); // poloměr (se znaménkem)
double mx = (x1 + x2) / 2.0, my = (y1 + y2) / 2.0;
double dd = r * Math.Cos(theta / 2.0); // vzdálenost středu od středu tětivy
double ux = -dy / chord, uy = dx / chord; // levá normála tětivy
double cx = mx + dd * ux, cy = my + dd * uy;
double sa = Math.Atan2(y1 - cy, x1 - cx);
double ar = Math.Abs(r);
int seg = Math.Max(2, (int)Math.Ceiling(Math.Abs(theta) / (Math.PI / 8))); // ~16 dílků/kruh
for (int i = 1; i < seg; i++)
{
double t = sa + theta * i / seg;
body.Add((cx + ar * Math.Cos(t), cy + ar * Math.Sin(t)));
}
}
/// <summary>Box-šířka víceřádkového atributu z XRecordu (group 41); 0 = jednořádkový.</summary>
private static double MultilineWidth(AttributeDefinition ad)
{
var xd = ad.XDictionary;
if (xd is null) return 0;
foreach (var obj in xd)
if (obj is XRecord xr)
foreach (var e in xr.Entries)
if (e.Code == 41 && e.Value is double w && w > 0)
return w;
return 0;
}
/// <summary>Šablona má tagy se suffixem (NAZEV_AKCE_001) → párovat bez suffixu, case-insensitive.</summary>
public static string NormalizeTag(string tag) => SuffixRegex().Replace(tag, "").ToUpperInvariant();
[GeneratedRegex(@"_\d+$")]
private static partial Regex SuffixRegex();
private static (HAlign, VAlign) MapMTextAlign(AttachmentPointType a) => a switch
{
AttachmentPointType.TopLeft => (HAlign.Left, VAlign.Top),
AttachmentPointType.TopCenter => (HAlign.Center, VAlign.Top),
AttachmentPointType.TopRight => (HAlign.Right, VAlign.Top),
AttachmentPointType.MiddleLeft => (HAlign.Left, VAlign.Middle),
AttachmentPointType.MiddleCenter => (HAlign.Center, VAlign.Middle),
AttachmentPointType.MiddleRight => (HAlign.Right, VAlign.Middle),
AttachmentPointType.BottomLeft => (HAlign.Left, VAlign.Bottom),
AttachmentPointType.BottomCenter => (HAlign.Center, VAlign.Bottom),
AttachmentPointType.BottomRight => (HAlign.Right, VAlign.Bottom),
_ => (HAlign.Left, VAlign.Top),
};
}
+142
View File
@@ -0,0 +1,142 @@
using System.IO;
using SkiaSharp;
using Rozpisky.Rendering;
namespace Rozpisky.Dxf;
/// <summary>
/// Vykreslí libovolnou kresbu (<see cref="Action{IProfileRenderer}"/>) na A4 do PNG/PDF
/// nebo do PNG bytů pro náhled (ROZPISKY.md §5). Kresba se vystředí a kreslí 1:1; zmenší se
/// jen, když se do tisknutelné plochy nevejde.
/// </summary>
public static class Exporters
{
private const double A4Wmm = 210, A4Hmm = 297;
private const double MmPerInch = 25.4;
private const double PtPerMm = 72.0 / 25.4;
/// <summary>A4 raster do PNG (default 200 dpi). marginMm = 0: rozpiska má v DXF vlastní
/// A4 rámeček (0,0)(210,297), který se mapuje 1:1 na papír (žádný přidaný okraj → vystředěno).</summary>
public static void PngA4(Action<IProfileRenderer> draw, string path, double dpi = 200,
bool landscape = false, double marginMm = 0,
(double X, double Y, double W, double H)? frame = null)
{
double pageWmm = landscape ? A4Hmm : A4Wmm, pageHmm = landscape ? A4Wmm : A4Hmm;
double pxPerMm = dpi / MmPerInch;
int wpx = (int)Math.Round(pageWmm * pxPerMm), hpx = (int)Math.Round(pageHmm * pxPerMm);
using var bmp = new SKBitmap(wpx, hpx);
using var canvas = new SKCanvas(bmp);
canvas.Clear(SKColors.White);
RenderPage(canvas, wpx, hpx, pageWmm, pageHmm, pxPerMm, marginMm, draw, minStrokePx: 1, frame);
using var img = SKImage.FromBitmap(bmp);
using var data = img.Encode(SKEncodedImageFormat.Png, 100);
using var fs = File.Create(path);
data.SaveTo(fs);
}
/// <summary>A4 vektor do PDF (1:1). marginMm = 0: DXF už obsahuje vlastní A4 rámeček, mapuje se
/// 1:1 na stránku, takže rozpiska vyjde vystředěná na papír přesně dle návrhu (žádné zmenšení).</summary>
public static void PdfA4(Action<IProfileRenderer> draw, string path,
bool landscape = false, double marginMm = 0,
(double X, double Y, double W, double H)? frame = null)
{
double pageWmm = landscape ? A4Hmm : A4Wmm, pageHmm = landscape ? A4Wmm : A4Hmm;
double wpt = pageWmm * PtPerMm, hpt = pageHmm * PtPerMm;
using var fs = File.Create(path);
using var doc = SKDocument.CreatePdf(fs);
var canvas = doc.BeginPage((float)wpt, (float)hpt);
RenderPage(canvas, wpt, hpt, pageWmm, pageHmm, PtPerMm, marginMm, draw, minStrokePx: 0, frame);
doc.EndPage();
doc.Close();
}
/// <summary>Živý náhled na plátno (SKElement): vystředí a vyplní referenční obdélník (rámeček
/// šablony, jinak rozsah kresby) do pixelového boxu jako „výchozí fit" (zoom=1), na to navrch
/// aplikuje uživatelský zoom a posun v px. Kresba je vektorová → ostrá v každém přiblížení.</summary>
public static void RenderView(SKCanvas canvas, int wpx, int hpx, Action<IProfileRenderer> draw,
(double X, double Y, double W, double H)? frame,
double zoom, double panXpx, double panYpx, double marginPx = 14)
{
canvas.Clear(SKColors.White);
double bMinX, bMinY, bW, bH;
if (frame is { } f)
{
(bMinX, bMinY, bW, bH) = (f.X, f.Y, f.W, f.H);
}
else
{
var b = Bounds(draw);
if (b.IsEmpty) return;
(bMinX, bMinY, bW, bH) = (b.MinX, b.MinY, b.Width, b.Height);
}
if (bW <= 0 || bH <= 0) return;
double basePxPerMm = Math.Min((wpx - 2 * marginPx) / bW, (hpx - 2 * marginPx) / bH);
// Uživatelský zoom/posun: canvas se transformuje před přehráním kresby, takže se škáluje
// vše včetně tloušťky čar a velikosti glyfů → Skia je rasterizuje ostře v cílovém měřítku.
canvas.Save();
canvas.Translate((float)panXpx, (float)panYpx);
canvas.Scale((float)zoom, (float)zoom);
RenderInto(canvas, wpx, hpx, bMinX, bMinY, bW, bH, basePxPerMm, draw, minStrokePx: 1);
canvas.Restore();
}
// --- jádro ---
private static BoundsRenderer Bounds(Action<IProfileRenderer> draw)
{
var b = new BoundsRenderer();
draw(b);
return b;
}
/// <summary>A4 stránka: referenční obdélník (A4 rámeček ze šablony, jinak rozsah kresby) se mapuje
/// 1:1 a vystředí na papír; zmenší se jen když se nevejde do plochy bez okrajů.</summary>
private static void RenderPage(SKCanvas canvas, double wpx, double hpx,
double pageWmm, double pageHmm, double pxPerMm, double marginMm,
Action<IProfileRenderer> draw, double minStrokePx,
(double X, double Y, double W, double H)? frame)
{
double bMinX, bMinY, bW, bH;
if (frame is { } f)
{
(bMinX, bMinY, bW, bH) = (f.X, f.Y, f.W, f.H);
}
else
{
var b = Bounds(draw);
if (b.IsEmpty) return;
(bMinX, bMinY, bW, bH) = (b.MinX, b.MinY, b.Width, b.Height);
}
double availWmm = pageWmm - 2 * marginMm, availHmm = pageHmm - 2 * marginMm;
double scale = 1.0;
if (bW > availWmm || bH > availHmm)
scale = Math.Min(availWmm / bW, availHmm / bH);
RenderInto(canvas, (int)Math.Round(wpx), (int)Math.Round(hpx), bMinX, bMinY, bW, bH, pxPerMm * scale, draw, minStrokePx);
}
/// <summary>Vystředí referenční obdélník do plátna a přehraje kresbu (mm → px, osa Y nahoru).
/// Co přesahuje obdélník (např. přetečený text), se ořízne na hraně plátna.</summary>
private static void RenderInto(SKCanvas canvas, int wpx, int hpx,
double bMinX, double bMinY, double bWidth, double bHeight,
double pxPerDrawingMm, Action<IProfileRenderer> draw, double minStrokePx)
{
double drawWpx = bWidth * pxPerDrawingMm, drawHpx = bHeight * pxPerDrawingMm;
double offX = (wpx - drawWpx) / 2.0;
double offYfromBottom = (hpx - drawHpx) / 2.0;
SKPoint Map(double x, double y) => new(
(float)(offX + (x - bMinX) * pxPerDrawingMm),
(float)(hpx - (offYfromBottom + (y - bMinY) * pxPerDrawingMm)));
using var r = new SkiaProfileRenderer(canvas, Map, pxPerDrawingMm, minStrokePx);
draw(r);
}
}
+131
View File
@@ -0,0 +1,131 @@
using System.Text;
using Rozpisky.Models;
namespace Rozpisky.Dxf;
/// <summary>
/// Generátor strojově čitelného <b>47-pozičního kódu</b> přílohy („kód pod rozpiskou", manuál SŽ
/// kap. 3.1.5 a 3.2.13.2.8). Kód má pevnou délku 47 znaků složenou ze 6 skupin oddělených znakem
/// <c>_</c> na pevných pozicích. Skládá se ze strukturovaných polí přílohy a projektu; každá pozice,
/// pro kterou není podklad, se vyplní znakem <c>X</c> žádná pozice nezůstává prázdná.
/// <para>Rozložení (1-based pozice → obsah):</para>
/// <list type="bullet">
/// <item>111 Označení investora (S-kód), 12 = <c>_</c></item>
/// <item>1316 Stupeň dokumentace, 17 = <c>_</c></item>
/// <item>1821 Část (CAST) + řada = číslice z dílčí části (DILCI_CAST bez písmen a teček);
/// např. CAST <c>B</c> + DILCI_CAST <c>B.7</c> → <c>B7</c>, CAST <c>D</c> + <c>D110</c> → <c>D110</c>, 22 = <c>_</c></item>
/// <item>2324 předpona objektu (SO/PS) + 2531 číslo objektu (řada+úsek+řazení), 32 = <c>_</c></item>
/// <item>3334 podobjekt, 35 = <c>_</c></item>
/// <item>3637 typ přílohy (D1…), 38 = <c>_</c>, 39 část přílohy, 40 = <c>_</c></item>
/// <item>4143 číslo přílohy, 44 = <c>_</c></item>
/// <item>45 typ revize (P/0/V) + 4647 pořadí revize</item>
/// </list>
/// Sdružené objekty (SK/PK, kap. 3.3) a další okrajové případy zatím nejsou řešené kód se skládá
/// přímočaře z dostupných hodnot.
/// </summary>
public static class KodPrilohy
{
/// <summary>Pevná délka kódu (počet pozic / buněk K1K47).</summary>
public const int Delka = 47;
private const char Vypln = 'X';
/// <summary>Sestaví 47-znakový kód přílohy z údajů projektu a řádku přílohy.</summary>
public static string Sestav(Projekt p, Priloha r)
{
var obj = ObjektOznaceni.Rozpad(r.CisloObjektu);
var (revTyp, revCislo) = RozpadRevize(r.R1);
var sb = new StringBuilder(Delka);
Pole(sb, p.Skod, 11); // 111 investor / S-kód
sb.Append('_'); // 12
Pole(sb, p.StupenDokumentace, 4); // 1316 stupeň dokumentace
sb.Append('_'); // 17
Pole(sb, ObjektOznaceni.Alfanum(r.OznaceniCasti) + Cislice(r.OznaceniDilciCasti), 4); // 1821 část (CAST) + řada z DILCI_CAST bez písmen/teček
sb.Append('_'); // 22
Pole(sb, obj.Prefix, 2); // 2324 SO/PS
Pole(sb, obj.Cislo + obj.Usek + obj.Razeni, 7); // 2531 číslo objektu
sb.Append('_'); // 32
Pole(sb, obj.Podobjekt, 2); // 3334 podobjekt
sb.Append('_'); // 35
Pole(sb, ObjektOznaceni.Alfanum(r.TypPrilohy), 2); // 3637 typ přílohy (D1…)
sb.Append('_'); // 38
Pole(sb, r.DilciPriloha, 1); // 39 část přílohy
sb.Append('_'); // 40
Pole(sb, r.PoradiPrilohy, 3); // 4143 číslo přílohy
sb.Append('_'); // 44
sb.Append(revTyp); // 45 typ revize
Pole(sb, revCislo, 2); // 4647 pořadí revize
return sb.ToString();
}
/// <summary>Kód „seznamu příloh“ objektu (manuál SŽ kap. 5.1.11): pozice 134 jsou shodné s kódem
/// rozpisky objektu (S-kód, stupeň, část/řada, objekt, podobjekt), pozice 3543 nahrazeny pevně
/// <c>_XX_X_001</c> (seznam = „příloha 001“ bez typu/dokumentu) a zbytek (4447) je revize.
/// <paramref name="reprezentant"/> = libovolná příloha objektu (pozice 134 jsou objektové).
/// <paramref name="revizeKod"/> = zvolená revize seznamu (např. „V00“); null → revize z reprezentanta.</summary>
public static string SestavSeznam(Projekt p, Priloha reprezentant, string? revizeKod = null)
=> SestavObjektovy(p, reprezentant, "_XX_X_001", revizeKod);
/// <summary>Kód „rozpisky na spisové desky objektu“ (manuál SŽ): pozice 134 shodné s kódem
/// rozpisky objektu, pozice 3543 nahrazeny pevně <c>_XX_X_000</c> (desky = „příloha 000“ bez
/// typu/dokumentu) a zbytek (4447) je revize. Shoduje se s názvem souboru/rozvržení desek
/// (<c>objekt_[podobjekt]_XX_X_000</c>, viz <see cref="Models.Priloha.JmenoRozvrzeniAuto"/>)
/// typ <c>XX</c> je literální placeholder a zůstává v obou.
/// <paramref name="reprezentant"/> = libovolná příloha objektu; <paramref name="revizeKod"/> =
/// zvolená revize, null → revize z reprezentanta.</summary>
public static string SestavDesky(Projekt p, Priloha reprezentant, string? revizeKod = null)
=> SestavObjektovy(p, reprezentant, "_XX_X_000", revizeKod);
/// <summary>Sdílený základ objektových kódů (seznam/desky): vezme 47-znakový kód reprezentanta
/// (pozice 134 = objektové, 4447 = revize) a nahradí prostřední segment 3543 hodnotou
/// <paramref name="stred"/> (např. <c>_XX_X_001</c> / <c>_XX_X_000</c>). <paramref name="revizeKod"/>
/// null → revize zůstane z reprezentanta, jinak se přepočítá přes <see cref="RozpadRevize"/>.</summary>
private static string SestavObjektovy(Projekt p, Priloha reprezentant, string stred, string? revizeKod)
{
var full = Sestav(p, reprezentant); // 47 znaků (revize z reprezentanta)
if (revizeKod is null)
return full[..34] + stred + full[43..]; // 134 | stred | 4447 revize
var (typ, cislo) = RozpadRevize(revizeKod);
var sb = new StringBuilder(full[..34]); // 134
sb.Append(stred); // 3543
sb.Append('_'); // 44
sb.Append(typ); // 45
Pole(sb, cislo, 2); // 4647
return sb.ToString();
}
/// <summary>Ponechá jen číslice (zahodí písmena, tečky i další znaky) použití pro řadu z pole
/// DILCI_CAST do kódu K18K21: <c>B.7</c> → <c>7</c>, <c>D110</c> → <c>110</c>.</summary>
private static string Cislice(string? s)
=> string.IsNullOrEmpty(s) ? "" : new string(s.Where(char.IsDigit).ToArray());
/// <summary>Vloží hodnotu do pole pevné šířky: ořízne přebytek, chybějící pozice doplní <c>X</c>.</summary>
private static void Pole(StringBuilder sb, string? hodnota, int sirka)
{
var s = (hodnota ?? "").Trim();
if (s.Length > sirka) s = s[..sirka];
sb.Append(s);
for (int i = s.Length; i < sirka; i++) sb.Append(Vypln);
}
/// <summary>Z kódu revize (např. <c>R1</c>) odvodí typ verze (P/0/V) a její pořadí.
/// První znak je typ, zbytek je číslo revize. Bez revize → <c>X</c> a prázdné číslo.
/// Např. <c>V00</c> → (<c>V</c>, <c>00</c>), <c>002</c> → (<c>0</c>, <c>02</c>).</summary>
private static (char Typ, string Cislo) RozpadRevize(string? revKod)
{
string rev = (revKod ?? "").Trim();
if (rev.Length == 0) return (Vypln, "");
char typ = rev[0] switch
{
'P' or 'V' or '0' => rev[0],
_ when char.IsDigit(rev[0]) => '0',
_ => char.ToUpperInvariant(rev[0]),
};
// Číslo revize = číslice ZA typovým znakem (vedoucí znak je typ, ne součást čísla).
string cislo = new string(rev.Skip(1).Where(char.IsDigit).ToArray());
return (typ, cislo);
}
}
+65
View File
@@ -0,0 +1,65 @@
using System.IO;
using Rozpisky.Models;
namespace Rozpisky.Dxf;
/// <summary>
/// Sestaví mapu „hladina placeholderu → absolutní cesta k DXF" pro jednu rozpisku.
/// Pravidla (kap. 4.3 manuálu SŽ):
/// <list type="bullet">
/// <item><c>Rozpiska_logo1/2</c> financující instituce (dotační vlevo / státní vpravo),
/// projektové, soubor ze složky <c>loga\</c> vedle .exe,</item>
/// <item><c>Rozpiska_logo3</c> logo investora (číselník Organizace dle <c>Projekt.Investor</c>),</item>
/// <item><c>Rozpiska_logo4</c> logo zhotovitele díla (číselník Firmy dle <c>Projekt.ZhotovitelSOD</c>),</item>
/// <item><c>Rozpiska_logo5</c> logo zhotovitele objektu (číselník Firmy dle <c>Priloha.ZhotovitelCasti</c>),</item>
/// <item><c>Rozpiska_Orientační schéma</c> DXF u přílohy (<c>Priloha.SchemaSoubor</c>), relativně k .rzp.</item>
/// </list>
/// Položky bez vyplněného / neexistujícího souboru se vynechají.
/// </summary>
public static class LogaResolver
{
/// <summary>Složka s logy dodávanými s programem (vedle .exe).</summary>
public static string LogaDir => Path.Combine(AppContext.BaseDirectory, "loga");
/// <param name="rzpDir">Složka aktuálního souboru .rzp (pro relativní cestu k schématu); null = neuloženo.</param>
public static IReadOnlyDictionary<string, string> Resolve(Projekt p, Priloha r, Ciselniky c, string? rzpDir)
{
var d = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
PridejZeSlozky(d, "Rozpiska_logo1", p.Logo1Soubor);
PridejZeSlozky(d, "Rozpiska_logo2", p.Logo2Soubor);
PridejZeSlozky(d, "Rozpiska_logo3", OrganizaceLogo(c, p.Investor));
PridejZeSlozky(d, "Rozpiska_logo4", FirmaLogo(c, p.ZhotovitelSOD));
PridejZeSlozky(d, "Rozpiska_logo5", FirmaLogo(c, r.ZhotovitelCasti));
var schema = ResolveSchema(r.SchemaSoubor, rzpDir);
if (schema is not null) d["Rozpiska_Orientační schéma"] = schema;
return d;
}
private static void PridejZeSlozky(IDictionary<string, string> d, string hladina, string? soubor)
{
if (string.IsNullOrWhiteSpace(soubor)) return;
var cesta = Path.IsPathRooted(soubor) ? soubor : Path.Combine(LogaDir, soubor);
if (File.Exists(cesta)) d[hladina] = cesta;
}
/// <summary>Cesta ke schématu: absolutní beze změny, jinak relativně ke složce .rzp.</summary>
private static string? ResolveSchema(string? soubor, string? rzpDir)
{
if (string.IsNullOrWhiteSpace(soubor)) return null;
if (Path.IsPathRooted(soubor)) return File.Exists(soubor) ? soubor : null;
if (string.IsNullOrEmpty(rzpDir)) return null;
var cesta = Path.Combine(rzpDir, soubor);
return File.Exists(cesta) ? cesta : null;
}
private static string? FirmaLogo(Ciselniky c, string? nazev)
=> string.IsNullOrWhiteSpace(nazev) ? null
: c.Firmy.FirstOrDefault(f => string.Equals(f.Nazev, nazev, StringComparison.OrdinalIgnoreCase))?.Logo;
private static string? OrganizaceLogo(Ciselniky c, string? nazev)
=> string.IsNullOrWhiteSpace(nazev) ? null
: c.Organizace.FirstOrDefault(o => string.Equals(o.Nazev, nazev, StringComparison.OrdinalIgnoreCase))?.Logo;
}
+155
View File
@@ -0,0 +1,155 @@
using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json;
using Rozpisky.Data;
namespace Rozpisky.Dxf;
/// <summary>
/// Jeden řádek mapování: hodnota z modelu (<see cref="Zdroj"/>) → DXF atribut (<see cref="Tag"/>).
/// <para><see cref="Zdroj"/> má tyto tvary (rozlišuje je <see cref="RozpiskaValues"/>):</para>
/// <list type="bullet">
/// <item><c>Projekt.Stavba</c> vlastnost z údajů projektu,</item>
/// <item><c>Priloha.NazevObjektu</c> vlastnost z řádku přílohy,</item>
/// <item><c>Revize:R1.Datum</c> číslo revize z <c>Priloha.R1</c> se dohledá v tabulce revizí
/// a vrátí se její <c>Datum</c> / <c>Popis</c>,</item>
/// <item><c>Firma:ZhotovitelCasti.Adresa</c> název firmy z <c>Priloha.ZhotovitelCasti</c> se dohledá
/// v číselníku Firmy a vrátí se <c>Adresa</c> / <c>Telefon</c> / <c>Email</c>,</item>
/// <item><c>Cast:OznaceniCasti.Nazev</c> označení z <c>Priloha.OznaceniCasti</c> se dohledá
/// v číselníku Části a vrátí se <c>Nazev</c>.</item>
/// </list>
/// <see cref="Prefix"/> se předřadí neprázdné hodnotě (např. <c>"T: "</c>), <see cref="Transform"/>
/// aplikuje pojmenovanou úpravu (zatím <c>"FormatA4"</c> = číslo → <c>„4 x A4"</c>).
/// <see cref="Tag"/> je kanonický (bez suffixu <c>_NNN</c>); párování se suffixem řeší
/// <see cref="DxfTemplate.NormalizeTag"/>.
/// </summary>
public sealed class MapPole
{
public string Tag { get; set; } = "";
public string Zdroj { get; set; } = "";
public string? Prefix { get; set; }
public string? Transform { get; set; }
/// <summary>Hodnota dosazená, když je zdroj prázdný (nevyplněný). Slouží pro pole, která dle
/// manuálu SŽ (kap. 4.1.7) nesmí zůstat prázdná a vyplní se pomlčkou „—". Bez nastavení se
/// prázdné pole nevykreslí. Prefix ani Transform se na výchozí hodnotu neaplikují.</summary>
public string? Vychozi { get; set; }
}
/// <summary>
/// Načítání mapování „pole programu → DXF atribut“ z JSON souboru <b>vedle .exe</b> (portable),
/// aby šlo upravit bez překladu (např. když se změní tagy v šabloně). Stejný princip jako
/// <see cref="Data.CiselnikyStore"/>. Soubor se <b>dodává</b>; <see cref="Vychozi"/> je jen
/// nouzová záloha, kdyby chyběl/byl poškozený.
/// </summary>
public static class MapovaniStore
{
public const string SouborNazev = "mapovani.json";
private static readonly JsonSerializerOptions Opts = new()
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
/// <summary>Výchozí umístění: soubor vedle .exe (portable).</summary>
public static string DefaultPath => Path.Combine(AppContext.BaseDirectory, SouborNazev);
private static IReadOnlyList<MapPole>? _cache;
/// <summary>Mapování načtené z <c>mapovani.json</c> vedle .exe; když chybí/je poškozený,
/// vrátí vestavěný <see cref="Vychozi"/>. Výsledek se cachuje (config se za běhu nemění)
/// po ruční editaci souboru zavolej <see cref="Reload"/>.</summary>
public static IReadOnlyList<MapPole> Mapovani => _cache ??= Load();
/// <summary>Zahodí cache další přístup k <see cref="Mapovani"/> načte soubor znovu.</summary>
public static void Reload() => _cache = null;
private static IReadOnlyList<MapPole> Load()
{
try
{
if (File.Exists(DefaultPath))
{
var data = JsonSerializer.Deserialize<Dto>(File.ReadAllText(DefaultPath), Opts);
if (data?.Pole is { Count: > 0 }) return data.Pole;
// Soubor existuje, ale nenese žádné pole („null" / prázdný seznam) → stejné
// zacházení jako s rozbitým JSON, viz catch níže.
JsonConfigFile.ZalohujAOhlasPoskozeny(DefaultPath);
}
}
catch (Exception ex) when (ex is IOException or JsonException or UnauthorizedAccessException)
{
// Poškozený / nečitelný soubor → fallback na vestavěné mapování. Soubor se nepřepisuje,
// ale exporty by tiše jely s jiným mapováním než uživatelovou (rozbitou) úpravou
// proto záloha + upozornění při každém startu, dokud soubor neopraví/nesmaže.
JsonConfigFile.ZalohujAOhlasPoskozeny(DefaultPath, ex);
}
return Vychozi();
}
private sealed class Dto
{
public List<MapPole> Pole { get; set; } = new();
}
/// <summary>Vestavěné výchozí mapování záloha pro případ smazaného/poškozeného
/// <c>mapovani.json</c>. Zdroj pravdy je dodávaný soubor; tahle metoda ho jen zrcadlí.</summary>
public static IReadOnlyList<MapPole> Vychozi()
{
var l = new List<MapPole>
{
// Projektová pole (list ÚDAJE)
new() { Tag = "NAZEV_AKCE", Zdroj = "Projekt.Stavba" },
new() { Tag = "SO_HIP", Zdroj = "Projekt.HIP", Vychozi = "—" },
new() { Tag = "OBJEDNATEL_NAZEV", Zdroj = "Projekt.Investor" },
new() { Tag = "OBJEDNATEL_ADRESA", Zdroj = "Projekt.InvestorAdresa" },
new() { Tag = "OBJEDNATEL_ZASTUPCE_NAZEV", Zdroj = "Projekt.SlozkaSZ" },
new() { Tag = "OBJEDNATEL_ZASTUPCE_ADRESA",Zdroj = "Projekt.SlozkaSZAdresa" },
new() { Tag = "S_KOD", Zdroj = "Projekt.Skod" },
new() { Tag = "ZAK_CISLO", Zdroj = "Projekt.CisloZakazky" },
new() { Tag = "STUP_DOK", Zdroj = "Projekt.StupenDokumentace" },
new() { Tag = "DATUM", Zdroj = "Projekt.SmluvniDatumZpracovani" },
new() { Tag = "ZHOTOVITEL_DILA_NAZEV", Zdroj = "Projekt.ZhotovitelSOD" },
new() { Tag = "ZHOTOVITEL_DILA_ADRESA", Zdroj = "Projekt.AdresaSOD" },
new() { Tag = "ZHOTOVITEL_DILA_TELEFON", Zdroj = "Projekt.TelSOD", Prefix = "T: " },
new() { Tag = "ZHOTOVITEL_DILA_EMAIL", Zdroj = "Projekt.EmailSOD", Prefix = "E: " },
// Řádková pole (list SEZNAM)
new() { Tag = "CAST", Zdroj = "Priloha.OznaceniCasti" },
new() { Tag = "CAST_NAZEV", Zdroj = "Cast:OznaceniCasti.Nazev" },
new() { Tag = "OBJEKT_NAZEV", Zdroj = "Priloha.NazevObjektu", Vychozi = "—" },
new() { Tag = "RADA", Zdroj = "Priloha.Rada", Vychozi = "—" },
new() { Tag = "USEK", Zdroj = "Priloha.Usek", Vychozi = "—" },
new() { Tag = "RAZENI", Zdroj = "Priloha.Razeni", Vychozi = "—" },
new() { Tag = "PODOBJEKT", Zdroj = "Priloha.Podobjekt", Vychozi = "—" },
new() { Tag = "DILCI_CAST", Zdroj = "Priloha.OznaceniDilciCasti", Vychozi = "—" },
new() { Tag = "TYP", Zdroj = "Priloha.Typ", Vychozi = "—" },
new() { Tag = "CISLO_PRILOHY", Zdroj = "Priloha.PoradiPrilohy", Vychozi = "—" },
new() { Tag = "PRILOHA", Zdroj = "Priloha.NazevPrilohy", Vychozi = "—" },
new() { Tag = "PRILOHA_CAST", Zdroj = "Priloha.NazevDilciCastiPrilohy", Vychozi = "—" },
new() { Tag = "SO_VYPRACOVAL_PRILOHA", Zdroj = "Priloha.Vypracoval" },
new() { Tag = "SO_ZODP_PROJEKTANT", Zdroj = "Priloha.OdpovednyProjektant" },
new() { Tag = "SO_SPECIALISTA", Zdroj = "Priloha.Specialista", Vychozi = "—" },
new() { Tag = "MERITKO", Zdroj = "Priloha.Meritko", Vychozi = "—" },
new() { Tag = "FORMATY", Zdroj = "Priloha.Formaty", Transform = "FormatA4", Vychozi = "—" },
new() { Tag = "KRAJ", Zdroj = "Priloha.Kraj" },
new() { Tag = "KATASTRALNI_UZEMI", Zdroj = "Priloha.KatastralniUzemi" },
new() { Tag = "TUDU", Zdroj = "Priloha.Tudu" },
new() { Tag = "ZHOTOVITEL_OBJEKTU_NAZEV", Zdroj = "Priloha.ZhotovitelCasti" },
new() { Tag = "ZHOTOVITEL_OBJEKTU_ADRESA", Zdroj = "Firma:ZhotovitelCasti.Adresa" },
new() { Tag = "ZHOTOVITEL_OBJEKTU_TELEFON", Zdroj = "Firma:ZhotovitelCasti.Telefon", Prefix = "T: " },
new() { Tag = "ZHOTOVITEL_OBJEKTU_EMAIL", Zdroj = "Firma:ZhotovitelCasti.Email", Prefix = "E: " },
};
// Revizní pole R1R4: číslo + kdo kontroloval z přílohy, datum + popis z tabulky revizí.
for (int i = 1; i <= 4; i++)
{
l.Add(new() { Tag = $"REVIZE{i}CISLO", Zdroj = $"Priloha.R{i}" });
l.Add(new() { Tag = $"SO_KONTROLOVAL{i}", Zdroj = $"Priloha.KontrolovalR{i}" });
l.Add(new() { Tag = $"REVIZE{i}DAT", Zdroj = $"Revize:R{i}.Datum" });
l.Add(new() { Tag = $"REVIZE{i}", Zdroj = $"Revize:R{i}.Popis" });
}
return l;
}
}
+91
View File
@@ -0,0 +1,91 @@
using System.Collections.Concurrent;
using System.IO;
using Rozpisky.Models;
using Rozpisky.Rendering;
namespace Rozpisky.Dxf;
/// <summary>
/// Spojuje DXF šablonu, mapování hodnot a exportéry do jednoho místa pro UI
/// (náhled, tisk). Šablona se načte jednou a cachuje.
/// </summary>
public sealed class RozpiskaRenderer
{
private readonly DxfTemplate _tpl;
public RozpiskaRenderer(string sablonaPath) => _tpl = new DxfTemplate(ResolvePath(sablonaPath));
/// <summary>Najde šablonu: absolutní/relativní k pracovní složce, jinak vedle .exe.</summary>
public static string ResolvePath(string path)
=> File.Exists(path) ? path : Path.Combine(AppContext.BaseDirectory, Path.GetFileName(path));
/// <summary>A4 rámeček šablony (mm) referenční obdélník pro vystředění náhledu i exportů.</summary>
public (double X, double Y, double W, double H)? SheetFrame => _tpl.SheetFrame;
/// <summary>Rozměr jedné buňky pro dlaždicový DXF export přednostně celkový <c>!!NOPRINT</c>
/// rámeček, jinak A4 rámeček, jinak rozsah kresby.</summary>
public (double X, double Y, double W, double H)? CellFrame => _tpl.FrameBounds ?? _tpl.SheetFrame ?? _tpl.Bounds;
// Načtené fragmenty (loga / schémata) podle cesty šablony jsou immutable, lze sdílet.
private static readonly ConcurrentDictionary<string, DxfTemplate?> FragmentCache = new(StringComparer.OrdinalIgnoreCase);
/// <summary>Vyprázdní cache fragmentů volá se při načtení/založení projektu, aby cache nerostla
/// donekonečna a aby se mezitím změněné DXF soubory (loga, schémata) načetly znovu.</summary>
public static void VycistiCache() => FragmentCache.Clear();
/// <summary>Sestaví kresbu rozpisky (vektorové příkazy) pro libovolný cíl náhled/PNG/PDF/DXF.
/// <paramref name="loga"/> = mapa „hladina placeholderu → absolutní cesta k DXF" (logo/schéma);
/// každý fragment se fitne (uniformní scale + vystředění) do svého rámečku.
/// <paramref name="ramecekA4"/> = true → přidá i <c>!!NOPRINT</c> rámeček (hranice papíru A4)
/// pro DXF export, aby šlo z CADu rozpisku případně vytisknout. Fragmenty (loga) svůj NOPRINT
/// vodicí rámeček nikdy nekreslí.</summary>
public Action<IProfileRenderer> BuildDraw(Projekt p, Priloha r, IEnumerable<Revize> revize, Ciselniky c,
bool vyplnene, IReadOnlyDictionary<string, string>? loga = null,
bool ramecekA4 = false)
{
var values = vyplnene ? RozpiskaValues.Build(p, r, revize, c) : null;
return g =>
{
_tpl.Replay(g, values, ramecekA4);
if (loga is { Count: > 0 }) KresliFragmenty(g, loga);
};
}
/// <summary>Vykreslí každý DXF fragment do jeho rámečku: uniformní zmenšení tak, aby se vešel,
/// a vystředění. Chybějící rámeček/soubor/prázdná kresba se tiše přeskočí.</summary>
private void KresliFragmenty(IProfileRenderer g, IReadOnlyDictionary<string, string> loga)
{
foreach (var (hladina, cesta) in loga)
{
if (string.IsNullOrWhiteSpace(cesta)) continue;
if (!_tpl.Placeholders.TryGetValue(hladina, out var f)) continue;
if (f.W <= 0 || f.H <= 0) continue;
var frag = NactiFragment(cesta);
if (frag is null) continue;
// Reference pro fit: přednostně vodicí rámeček !!NOPRINT (návrhové umístění uvnitř loga,
// poměr i okraje řeší autor → fit 1:1), jinak bbox kresby (s 5% rezervou).
bool maRamecek = frag.FrameBounds is { };
if ((maRamecek ? frag.FrameBounds : frag.Bounds) is not { } cb || cb.W <= 0 || cb.H <= 0) continue;
double s = Math.Min(f.W / cb.W, f.H / cb.H) * (maRamecek ? 1.0 : 0.95);
double ox = f.X + f.W / 2 - s * (cb.X + cb.W / 2);
double oy = f.Y + f.H / 2 - s * (cb.Y + cb.H / 2);
frag.Replay(new TransformRenderer(g, s, ox, oy));
}
}
private static DxfTemplate? NactiFragment(string cesta) => FragmentCache.GetOrAdd(cesta, c =>
{
try { return File.Exists(c) ? new DxfTemplate(c) : null; }
catch { return null; } // poškozený/nečitelný DXF fragment → bez loga
});
public void ExportPdf(Projekt p, Priloha r, IEnumerable<Revize> revize, Ciselniky c, string path,
IReadOnlyDictionary<string, string>? loga = null)
=> Exporters.PdfA4(BuildDraw(p, r, revize, c, vyplnene: true, loga), path, frame: _tpl.SheetFrame);
public void ExportPng(Projekt p, Priloha r, IEnumerable<Revize> revize, Ciselniky c, string path,
IReadOnlyDictionary<string, string>? loga = null)
=> Exporters.PngA4(BuildDraw(p, r, revize, c, vyplnene: true, loga), path, frame: _tpl.SheetFrame);
}
+122
View File
@@ -0,0 +1,122 @@
using System.Collections.Concurrent;
using System.Reflection;
using Rozpisky.Models;
namespace Rozpisky.Dxf;
/// <summary>
/// Sestaví slovník „tag DXF → hodnota" pro jednu rozpisku z údajů projektu, řádku přílohy a
/// sdílených číselníků. Mapování (které pole programu plní který atribut) <b>není v kódu</b>
/// čte se z dodávaného <c>mapovani.json</c> vedle .exe přes <see cref="MapovaniStore"/>, hodnoty
/// se dotahují reflexí. Klíče jsou kanonické (bez suffixu _NNN) párování řeší
/// <see cref="DxfTemplate.NormalizeTag"/>.
/// </summary>
public static class RozpiskaValues
{
public static Dictionary<string, string> Build(Projekt p, Priloha r, IEnumerable<Revize> revize, Ciselniky c)
{
// Tabulky pro dohledání (číslo revize → řádek, název firmy → firma, označení → část).
var revLookup = ToLookup(revize, x => x.Cislo);
var firmaLookup = ToLookup(c.Firmy, f => f.Nazev);
var castLookup = ToLookup(c.Casti, x => x.Oznaceni);
var v = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var m in MapovaniStore.Mapovani)
{
if (string.IsNullOrWhiteSpace(m.Tag) || string.IsNullOrWhiteSpace(m.Zdroj)) continue;
var val = Resolve(m.Zdroj, p, r, revLookup, firmaLookup, castLookup);
if (string.IsNullOrEmpty(val))
{
// Pole, která dle manuálu nesmí zůstat prázdná (kap. 4.1.7), dostanou „—"; ostatní se přeskočí.
if (!string.IsNullOrEmpty(m.Vychozi))
v[DxfTemplate.NormalizeTag(m.Tag)] = m.Vychozi;
continue;
}
val = ApplyTransform(m.Transform, val);
if (!string.IsNullOrEmpty(m.Prefix)) val = m.Prefix + val;
v[DxfTemplate.NormalizeTag(m.Tag)] = val;
}
// Strojový 47-poziční kód (K1K47) „kód pod rozpiskou". Negeneruje se přes mapování
// (skládá se z více polí), ale algoritmicky; každá pozice = jedna buňka K1..K47.
// Desky objektu mají pevný střed _XX_X_000 (typ/dílčí/pořadí se neberou z řádku).
string kod = r.JeDesky ? KodPrilohy.SestavDesky(p, r) : KodPrilohy.Sestav(p, r);
for (int i = 0; i < kod.Length && i < KodPrilohy.Delka; i++)
v[DxfTemplate.NormalizeTag($"K{i + 1}")] = kod[i].ToString();
return v;
}
/// <summary>Vyhodnotí <c>Zdroj</c> z mapování na konkrétní hodnotu. Tvary:
/// <c>Projekt.X</c>, <c>Priloha.Y</c>, a lookup <c>Revize:R1.Datum</c> / <c>Firma:ZhotovitelCasti.Adresa</c>
/// / <c>Cast:OznaceniCasti.Nazev</c> (klíč z vlastnosti přílohy → řádek číselníku → cílová vlastnost).</summary>
private static string? Resolve(string zdroj, Projekt p, Priloha r,
IReadOnlyDictionary<string, Revize> revLookup,
IReadOnlyDictionary<string, Firma> firmaLookup,
IReadOnlyDictionary<string, Cast> castLookup)
{
int colon = zdroj.IndexOf(':');
if (colon > 0)
{
var kind = zdroj[..colon];
var rest = zdroj[(colon + 1)..]; // "ZhotovitelCasti.Adresa"
int dot = rest.IndexOf('.');
if (dot <= 0) return null;
var klic = GetProp(r, rest[..dot]); // hodnota klíče z přílohy
var cilProp = rest[(dot + 1)..];
if (string.IsNullOrWhiteSpace(klic)) return null;
return kind switch
{
"Revize" => revLookup.TryGetValue(klic, out var rev) ? GetProp(rev, cilProp) : null,
"Firma" => firmaLookup.TryGetValue(klic, out var f) ? GetProp(f, cilProp) : null,
"Cast" => castLookup.TryGetValue(klic, out var ca) ? GetProp(ca, cilProp) : null,
_ => null,
};
}
int sep = zdroj.IndexOf('.');
if (sep <= 0) return null;
object? obj = zdroj[..sep] switch
{
"Projekt" => p,
"Priloha" => r,
_ => null,
};
return obj is null ? null : GetProp(obj, zdroj[(sep + 1)..]);
}
/// <summary>Pojmenované úpravy hodnoty. <c>FormatA4</c>: čistě číselný počet → „4 x A4".</summary>
private static string ApplyTransform(string? transform, string val) => transform switch
{
"FormatA4" => IsNumeric(val) ? $"{val.Trim()} x A4" : val,
_ => val,
};
private static bool IsNumeric(string s)
{
s = s.Trim();
return s.Length > 0 && s.All(char.IsDigit);
}
/// <summary>Slovník klíč→první výskyt (case-insensitive), prázdné klíče ignoruje.</summary>
private static Dictionary<string, T> ToLookup<T>(IEnumerable<T> src, Func<T, string> key)
{
var d = new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase);
foreach (var x in src)
{
var k = key(x);
if (!string.IsNullOrWhiteSpace(k)) d.TryAdd(k, x);
}
return d;
}
// Cache PropertyInfo Build se volá pro každý řádek (rozpisku) při exportu.
private static readonly ConcurrentDictionary<(Type, string), PropertyInfo?> PropCache = new();
private static string? GetProp(object obj, string name)
{
var pi = PropCache.GetOrAdd((obj.GetType(), name),
key => key.Item1.GetProperty(key.Item2, BindingFlags.Public | BindingFlags.Instance));
return pi?.GetValue(obj) as string;
}
}
+144
View File
@@ -0,0 +1,144 @@
<Window x:Class="Rozpisky.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Rozpisky"
xmlns:views="clr-namespace:Rozpisky.Views"
xmlns:theming="clr-namespace:Rozpisky.Theming"
mc:Ignorable="d"
Title="{Binding Titulek}" Height="760" Width="1180"
Icon="/Rozpisky.ico"
WindowStartupLocation="CenterScreen"
WindowState="Maximized"
Background="{DynamicResource Brush.Window}"
Foreground="{DynamicResource Brush.Text}">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
</Window.Resources>
<Window.InputBindings>
<KeyBinding Modifiers="Control" Key="N" Command="{Binding NovyProjektCmd}"/>
<KeyBinding Modifiers="Control" Key="O" Command="{Binding OtevritProjektCmd}"/>
<KeyBinding Modifiers="Control" Key="S" Command="{Binding UlozitProjektCmd}"/>
<KeyBinding Modifiers="Control+Shift" Key="S" Command="{Binding UlozitJakoProjektCmd}"/>
<KeyBinding Key="F1" Command="{Binding OtevritNapoveduCmd}"/>
</Window.InputBindings>
<DockPanel>
<!-- Hlavní menu -->
<Menu DockPanel.Dock="Top" Padding="4,2">
<MenuItem Header="Soubor">
<MenuItem Header="Nový projekt" Command="{Binding NovyProjektCmd}" InputGestureText="Ctrl+N"/>
<MenuItem Header="Otevřít projekt…" Command="{Binding OtevritProjektCmd}" InputGestureText="Ctrl+O"/>
<Separator/>
<MenuItem Header="Uložit projekt" Command="{Binding UlozitProjektCmd}" InputGestureText="Ctrl+S"/>
<MenuItem Header="Uložit projekt jako…" Command="{Binding UlozitJakoProjektCmd}" InputGestureText="Ctrl+Shift+S"/>
</MenuItem>
<MenuItem Header="Nápověda">
<MenuItem Header="Nápověda k programu" Command="{Binding OtevritNapoveduCmd}" InputGestureText="F1"/>
<MenuItem Header="Manuál SŽ (PDF)…" Command="{Binding OtevritManualCmd}"/>
<Separator/>
<MenuItem Header="O programu" Command="{Binding OProgramuCmd}"/>
</MenuItem>
</Menu>
<!-- Akční lišta: dvě sekce (Rozpisky | Rozvržení) oddělené svislou čarou.
Grid: řádek 0 = popisky sekcí, řádek 1 = skupiny tlačítek (skupiny obou sekcí leží
ve stejném řádku → jejich hlavičky i tlačítka jsou zarovnané přes oddělovač). -->
<Border DockPanel.Dock="Top" Background="{DynamicResource Brush.TitleBar}" Padding="10,8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Sekce Rozpisky -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="Rozpisky" Style="{StaticResource Text.Caption}" Margin="2,0,0,3"/>
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal">
<HeaderedContentControl Style="{StaticResource Toolbar.Group}" Header="Export PDF">
<StackPanel Orientation="Horizontal">
<Button Content="Výběr" theming:Icon.Glyph="&#xE8A5;" Style="{StaticResource Button.Primary}"
ToolTip="Exportuje vybrané rozpisky do PDF" Command="{Binding ExportVyberPdfCmd}"/>
<Button Content="Vše" theming:Icon.Glyph="&#xE8A5;" Margin="6,0,0,0"
ToolTip="Exportuje všechny rozpisky do PDF" Command="{Binding ExportVsePdfCmd}"/>
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Style="{StaticResource Toolbar.Group}" Header="Export DXF">
<StackPanel Orientation="Horizontal">
<Button Content="Výběr" theming:Icon.Glyph="&#xE70F;"
ToolTip="Exportuje vybrané rozpisky do DXF (zatím neimplementováno)" Command="{Binding ExportVyberDxfCmd}"/>
<Button Content="Vše" theming:Icon.Glyph="&#xE70F;" Margin="6,0,0,0"
ToolTip="Exportuje všechny rozpisky do DXF (zatím neimplementováno)" Command="{Binding ExportVseDxfCmd}"/>
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Style="{StaticResource Toolbar.Group}" Header="Seznam příloh">
<StackPanel Orientation="Horizontal">
<Button Content="Tisk" theming:Icon.Glyph="&#xE749;"
ToolTip="Vytiskne seznam příloh" Command="{Binding TiskSeznamuCmd}"/>
<Button Content="Export" theming:Icon.Glyph="&#xEDE1;" Margin="6,0,0,0"
ToolTip="Exportuje seznam příloh do XLSX" Command="{Binding ExportSeznamuCmd}"/>
</StackPanel>
</HeaderedContentControl>
</StackPanel>
<!-- Oddělovač sekcí (přes oba řádky) -->
<Border Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Width="1"
Background="{DynamicResource Brush.BorderStrong}" VerticalAlignment="Stretch" Margin="6,0,14,2"/>
<!-- Sekce Rozvržení -->
<TextBlock Grid.Row="0" Grid.Column="2" Text="Rozvržení" Style="{StaticResource Text.Caption}" Margin="2,0,0,3"/>
<HeaderedContentControl Grid.Row="1" Grid.Column="2" Style="{StaticResource Toolbar.Group}" Header="Práce s výkresem" Margin="0">
<StackPanel Orientation="Horizontal">
<Button Content="Aktualizovat" theming:Icon.Glyph="&#xE72C;"
ToolTip="Aktualizuje rozvržení" Command="{Binding AktualizujRozvrzeniCmd}"/>
<Button Content="Tisk" theming:Icon.Glyph="&#xE749;" Margin="6,0,0,0"
ToolTip="Vytiskne rozvržení" Command="{Binding VytiskniRozvrzeniCmd}"/>
</StackPanel>
</HeaderedContentControl>
</Grid>
</Border>
<!-- Stavový řádek -->
<StatusBar DockPanel.Dock="Bottom">
<StatusBarItem>
<TextBlock>
<Run Text="Příloh: "/>
<Run Text="{Binding PocetPriloh, Mode=OneWay}" FontWeight="SemiBold"/>
</TextBlock>
</StatusBarItem>
<Separator/>
<StatusBarItem>
<TextBlock Text="{Binding StavovaZprava, Mode=OneWay}" FontWeight="SemiBold"/>
</StatusBarItem>
<StatusBarItem>
<!-- Viditelné jen během dávkového exportu PDF/DXF; dávka se ukončí po aktuální položce. -->
<Button Content="Zrušit export" Padding="8,0" Command="{Binding ZrusitExportCmd}"
Visibility="{Binding ExportBezi, Converter={StaticResource BoolToVis}, Mode=OneWay}"/>
</StatusBarItem>
</StatusBar>
<!-- Sekce aplikace -->
<TabControl x:Name="Zalozky" Margin="8">
<TabItem Header="Údaje projektu">
<views:UdajeProjektuView/>
</TabItem>
<TabItem Header="Seznam příloh">
<views:SeznamPrilohView/>
</TabItem>
<TabItem Header="Revize">
<views:RevizeView/>
</TabItem>
<TabItem Header="Databáze">
<views:CiselnikyView/>
</TabItem>
<TabItem Header="Nastavení">
<views:NastaveniView/>
</TabItem>
</TabControl>
</DockPanel>
</Window>
+38
View File
@@ -0,0 +1,38 @@
using System.Windows;
using System.Windows.Input;
using Rozpisky.ViewModels;
namespace Rozpisky;
/// <summary>
/// Hlavní okno aplikace (ROZHRANI.md §4.1): akční lišta + sekce (Údaje projektu,
/// Seznam příloh, Revize, Číselníky, Nastavení) + stavový řádek.
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var vm = new MainViewModel();
DataContext = vm;
// Alt+1..Alt+5 přepínají záložky (Údaje projektu … Nastavení). Přepínání záložek je čistě
// věc View, proto tady v code-behind, ne přes příkaz VM. Key.D1..Key.D5 jsou sousední hodnoty,
// takže Key.D1 + i odpovídá indexu záložky.
for (int i = 0; i < Zalozky.Items.Count; i++)
{
int index = i;
InputBindings.Add(new KeyBinding(
new RelayCommand(() => Zalozky.SelectedIndex = index),
Key.D1 + i, ModifierKeys.Alt));
}
Closing += (_, e) =>
{
// Neuložené změny: dotaz Ano/Ne/Storno. Storno (nebo zrušené uložení) zavření přeruší.
if (!vm.PotvrditZahozeniZmen()) { e.Cancel = true; return; }
// Uložit číselníky při zavření (zachytí i úpravy v buňkách, které autosave na změnu kolekce neřeší).
vm.UlozCiselniky();
vm.UvolniSoubor(); // pusť exkluzivní zámek .rzp souboru
};
}
}
+81
View File
@@ -0,0 +1,81 @@
using System.Collections.ObjectModel;
namespace Rozpisky.Models;
/// <summary>Organizace (objednatel / složka SŽ) číselník s adresou (ROZHRANI.md §3.4).</summary>
public sealed class Organizace : ObservableObject
{
private string _nazev = "";
private string _adresa = "";
private string _logo = "";
public string Nazev { get => _nazev; set => SetField(ref _nazev, value); }
public string Adresa { get => _adresa; set => SetField(ref _adresa, value); }
/// <summary>Jméno DXF souboru loga ve složce <c>loga\</c> vedle .exe (prázdné = bez loga).</summary>
public string Logo { get => _logo; set => SetField(ref _logo, value); }
public override string ToString() => Nazev;
}
/// <summary>Firma / zhotovitel číselník s adresou a kontakty.</summary>
public sealed class Firma : ObservableObject
{
private string _nazev = "";
private string _adresa = "";
private string _telefon = "";
private string _email = "";
private string _logo = "";
public string Nazev { get => _nazev; set => SetField(ref _nazev, value); }
public string Adresa { get => _adresa; set => SetField(ref _adresa, value); }
public string Telefon { get => _telefon; set => SetField(ref _telefon, value); }
public string Email { get => _email; set => SetField(ref _email, value); }
/// <summary>Jméno DXF souboru loga ve složce <c>loga\</c> vedle .exe (prázdné = bez loga).</summary>
public string Logo { get => _logo; set => SetField(ref _logo, value); }
public override string ToString() => Nazev;
}
/// <summary>Jméno osoby (vypracoval / odpovědný / specialista / kontroloval).</summary>
public sealed class Jmeno : ObservableObject
{
private string _hodnota = "";
public string Hodnota { get => _hodnota; set => SetField(ref _hodnota, value); }
public override string ToString() => Hodnota;
}
/// <summary>Verze / stupeň dokumentace (např. DOS, DPS).</summary>
public sealed class VerzeDokumentace : ObservableObject
{
private string _hodnota = "";
public string Hodnota { get => _hodnota; set => SetField(ref _hodnota, value); }
public override string ToString() => Hodnota;
}
/// <summary>Kraj (název) číselník pro našeptávání sloupce „Kraj" v seznamu příloh.</summary>
public sealed class Kraj : ObservableObject
{
private string _nazev = "";
public string Nazev { get => _nazev; set => SetField(ref _nazev, value); }
public override string ToString() => Nazev;
}
/// <summary>Část / objekt v seznamu (označení + název).</summary>
public sealed class Cast : ObservableObject
{
private string _oznaceni = "";
private string _nazev = "";
public string Oznaceni { get => _oznaceni; set => SetField(ref _oznaceni, value); }
public string Nazev { get => _nazev; set => SetField(ref _nazev, value); }
public override string ToString() => string.IsNullOrEmpty(Oznaceni) ? Nazev : $"{Oznaceni} {Nazev}";
}
/// <summary>
/// Kontejner sdílených číselníků (ROZHRANI.md §3.4). Ve finální aplikaci by žil
/// v externí databázi vedle .exe; zde drží data v paměti.
/// </summary>
public sealed class Ciselniky
{
public ObservableCollection<Organizace> Organizace { get; } = new();
public ObservableCollection<Firma> Firmy { get; } = new();
public ObservableCollection<Jmeno> Jmena { get; } = new();
public ObservableCollection<VerzeDokumentace> VerzeDokumentace { get; } = new();
public ObservableCollection<Cast> Casti { get; } = new();
public ObservableCollection<Kraj> Kraje { get; } = new();
}
+48
View File
@@ -0,0 +1,48 @@
namespace Rozpisky.Models;
/// <summary>
/// Konfigurace exportu / tisku (ROZHRANI.md §3.5). Neřídí pole rozpisky, ale výstup.
/// </summary>
public sealed class Nastaveni : ObservableObject
{
private string _cestaExportu = @"C:\temp";
private string _slozkaExportu = "Rozpisky";
private string _sablonaDxf = "Rozpiska.dxf";
private string _seznamMustr = "SEZNAM.xlsx";
private bool _meritkoAutomaticky = true;
private bool _ltscaleAutomaticky = true;
private bool _upravovatTiskarnu = true;
/// <summary>Kořenová cesta pro export (např. C:\temp).</summary>
public string CestaExportu { get => _cestaExportu; set => SetField(ref _cestaExportu, value); }
/// <summary>Podsložka exportu.</summary>
public string SlozkaExportu { get => _slozkaExportu; set => SetField(ref _slozkaExportu, value); }
/// <summary>Cesta k DXF šabloně rozpisky.</summary>
public string SablonaDxf { get => _sablonaDxf; set => SetField(ref _sablonaDxf, value); }
/// <summary>Cesta k mustru „Seznam příloh“ (.xlsx) vyplňovanému při exportu seznamu.</summary>
public string SeznamMustr { get => _seznamMustr; set => SetField(ref _seznamMustr, value); }
/// <summary>Automatické měřítko (MERITKOAUTOMATICKY) přepínač.</summary>
public bool MeritkoAutomaticky { get => _meritkoAutomaticky; set => SetField(ref _meritkoAutomaticky, value); }
/// <summary>Automatické měřítko typů čar (LTSCALE) přepínač. Řídí, jestli se při tisku do PDF
/// (<c>CadTiskService</c>) LTSCALE per-výkres skutečně <b>použije</b> (přepíše systémovou proměnnou).
/// Samotné vyhodnocení a zápis hodnoty do sloupce v seznamu (při aktualizaci rozvržení) na tomhle
/// přepínači nezávisí běží vždycky, protože nic nemění na výkresu.</summary>
public bool LtscaleAutomaticky { get => _ltscaleAutomaticky; set => SetField(ref _ltscaleAutomaticky, value); }
/// <summary>Upravovat nastavení tiskárny při aktualizaci rozvržení přepínač.</summary>
public bool UpravovatTiskarnu { get => _upravovatTiskarnu; set => SetField(ref _upravovatTiskarnu, value); }
/// <summary>Přepíše hodnoty z jiné instance (zachová databinding živé instance) při načtení projektu.</summary>
public void CopyFrom(Nastaveni o)
{
CestaExportu = o.CestaExportu; SlozkaExportu = o.SlozkaExportu; SablonaDxf = o.SablonaDxf;
SeznamMustr = o.SeznamMustr;
MeritkoAutomaticky = o.MeritkoAutomaticky; LtscaleAutomaticky = o.LtscaleAutomaticky;
UpravovatTiskarnu = o.UpravovatTiskarnu;
}
}
+39
View File
@@ -0,0 +1,39 @@
namespace Rozpisky.Models;
/// <summary>
/// Rozpad čísla objektu (sloupec D, <see cref="Priloha.CisloObjektu"/>) na složky, které popisové
/// pole zobrazuje samostatně (úsek, řazení, podobjekt) i ze kterých se skládá 47-poziční strojový
/// kód. Číslo objektu se píše jako <c>SO131.15.12.01</c> (případně s pomlčkou/podtržítkem/mezerou):
/// první segment je <b>označení objektu</b> = písmenná předpona (<c>SO</c>/<c>PS</c>/<c>SK</c>/<c>PK</c>)
/// + číslo (řada), další segmenty jsou úsek, řazení a podobjekt. Neuvedené složky zůstávají prázdné.
/// </summary>
public readonly record struct ObjektOznaceni(string Prefix, string Cislo, string Usek, string Razeni, string Podobjekt)
{
private static readonly char[] Oddelovace = { '.', '-', '_', ' ', '/' };
/// <summary>Ponechá jen písmena a číslice (odstraní tečky, mezery, pomlčky a další oddělovače).</summary>
public static string Alfanum(string? s)
=> string.IsNullOrEmpty(s) ? "" : new string(s.Where(char.IsLetterOrDigit).ToArray());
/// <summary>Rozloží číslo objektu na složky; prázdný vstup → samé prázdné řetězce (ne null).</summary>
public static ObjektOznaceni Rozpad(string? cisloObjektu)
{
if (string.IsNullOrWhiteSpace(cisloObjektu))
return new("", "", "", "", "");
var seg = cisloObjektu.Split(Oddelovace,
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
// První segment = označení objektu: písmenná předpona + číslo (řada).
string prvni = seg.Length > 0 ? seg[0] : "";
int i = 0;
while (i < prvni.Length && char.IsLetter(prvni[i])) i++;
return new(
Prefix: prvni[..i],
Cislo: prvni[i..],
Usek: seg.Length > 1 ? seg[1] : "",
Razeni: seg.Length > 2 ? seg[2] : "",
Podobjekt: seg.Length > 3 ? seg[3] : "");
}
}
+24
View File
@@ -0,0 +1,24 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Rozpisky.Models;
/// <summary>
/// Minimální báze pro INotifyPropertyChanged (MVVM-lite, bez NuGet závislostí).
/// </summary>
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
/// <summary>Nastaví pole a vyvolá PropertyChanged jen při skutečné změně. Vrací true při změně.</summary>
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? name = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(name);
return true;
}
}
+282
View File
@@ -0,0 +1,282 @@
using System.Text.Json.Serialization;
namespace Rozpisky.Models;
/// <summary>
/// Jeden řádek seznamu příloh = jedna rozpiska (původní list SEZNAM, ROZHRANI.md §3.2).
/// Sloupce AAD; <see cref="JmenoRozvrzeni"/> (sloupec AB) se dopočítává automaticky.
/// </summary>
public sealed class Priloha : ObservableObject
{
private string _oznaceniCasti = ""; // A
private string _oznaceniDilciCasti = ""; // B (řada)
private string _nazevObjektu = ""; // C
private string _cisloObjektu = ""; // D ┐
private string _typPrilohy = ""; // E │ vstupy pro JmenoRozvrzeni
private string _dilciPriloha = ""; // F │
private string _poradiPrilohy = ""; // G ┘
private string _nazevPrilohy = ""; // H
private string _nazevDilciCastiPrilohy = ""; // I
private string _zkratka = ""; // J
private bool _pouzitZkratku; // příznak: přidat zkratku (J) do JmenoRozvrzeni
private string _vypracoval = ""; // K
private string _odpovednyProjektant = ""; // L
private string _specialista = ""; // M
private string _r1 = ""; // N
private string _kontrolovalR1 = ""; // O
private string _r2 = ""; // P
private string _kontrolovalR2 = ""; // Q
private string _r3 = ""; // R
private string _kontrolovalR3 = ""; // S
private string _r4 = ""; // T
private string _kontrolovalR4 = ""; // U
private string _zhotovitelCasti = ""; // V
private string _meritko = ""; // W
private string _formaty = ""; // X
private string _kraj = ""; // Y
private string _katastralniUzemi = "viz textová část"; // Z předvyplněno u nového řádku
private string _tudu = ""; // AA
private string _ltscale = ""; // AC
private string _ctb = ""; // AD
private string _schemaSoubor = ""; // orientační schéma (DXF vedle .rzp)
private bool _jeObjektovaPriloha = true; // true = jméno rozvržení dle manuálu (auto)
private bool _jeDesky; // true = rozpiska na spisové desky objektu (kód _XX_X_000)
private bool _jeRozpiska = true; // true = název PDF dostane příponu _rozpiska
private string _jmenoRozvrzeniRucni = ""; // ruční jméno rozvržení (jen když není objektová)
private bool _jeDuplicitniJmeno; // příznak kolize jména (nastavuje VM)
public string OznaceniCasti { get => _oznaceniCasti; set => SetField(ref _oznaceniCasti, value); }
public string OznaceniDilciCasti { get => _oznaceniDilciCasti; set => SetField(ref _oznaceniDilciCasti, value); }
public string NazevObjektu { get => _nazevObjektu; set => SetField(ref _nazevObjektu, value); }
public string CisloObjektu
{
get => _cisloObjektu;
set
{
if (!SetField(ref _cisloObjektu, value)) return;
OnPropertyChanged(nameof(JmenoRozvrzeni));
OnPropertyChanged(nameof(Rada));
OnPropertyChanged(nameof(Usek));
OnPropertyChanged(nameof(Razeni));
OnPropertyChanged(nameof(Podobjekt));
}
}
public string TypPrilohy
{
get => _typPrilohy;
set { if (SetField(ref _typPrilohy, value)) { OnPropertyChanged(nameof(JmenoRozvrzeni)); OnPropertyChanged(nameof(Typ)); } }
}
public string DilciPriloha
{
get => _dilciPriloha;
set { if (SetField(ref _dilciPriloha, value)) { OnPropertyChanged(nameof(JmenoRozvrzeni)); OnPropertyChanged(nameof(Typ)); } }
}
public string PoradiPrilohy
{
get => _poradiPrilohy;
set { if (SetField(ref _poradiPrilohy, value)) OnPropertyChanged(nameof(JmenoRozvrzeni)); }
}
public string NazevPrilohy { get => _nazevPrilohy; set => SetField(ref _nazevPrilohy, value); }
public string NazevDilciCastiPrilohy { get => _nazevDilciCastiPrilohy; set => SetField(ref _nazevDilciCastiPrilohy, value); }
public string Zkratka
{
get => _zkratka;
set { if (SetField(ref _zkratka, value)) OnPropertyChanged(nameof(JmenoRozvrzeni)); }
}
public string Vypracoval { get => _vypracoval; set => SetField(ref _vypracoval, value); }
public string OdpovednyProjektant { get => _odpovednyProjektant; set => SetField(ref _odpovednyProjektant, value); }
public string Specialista { get => _specialista; set => SetField(ref _specialista, value); }
public string R1 { get => _r1; set => SetField(ref _r1, value); }
public string KontrolovalR1 { get => _kontrolovalR1; set => SetField(ref _kontrolovalR1, value); }
public string R2 { get => _r2; set => SetField(ref _r2, value); }
public string KontrolovalR2 { get => _kontrolovalR2; set => SetField(ref _kontrolovalR2, value); }
public string R3 { get => _r3; set => SetField(ref _r3, value); }
public string KontrolovalR3 { get => _kontrolovalR3; set => SetField(ref _kontrolovalR3, value); }
public string R4 { get => _r4; set => SetField(ref _r4, value); }
public string KontrolovalR4 { get => _kontrolovalR4; set => SetField(ref _kontrolovalR4, value); }
public string ZhotovitelCasti { get => _zhotovitelCasti; set => SetField(ref _zhotovitelCasti, value); }
public string Meritko { get => _meritko; set => SetField(ref _meritko, value); }
public string Formaty { get => _formaty; set => SetField(ref _formaty, value); }
public string Kraj { get => _kraj; set => SetField(ref _kraj, value); }
public string KatastralniUzemi { get => _katastralniUzemi; set => SetField(ref _katastralniUzemi, value); }
public string Tudu { get => _tudu; set => SetField(ref _tudu, value); }
public string Ltscale { get => _ltscale; set => SetField(ref _ltscale, value); }
/// <summary>Styl tisku (plot style sheet) nastavovaný na rozvržení při „Aktualizuj rozvržení".
/// Napiš <b>celý název souboru včetně přípony</b> „.ctb" (např. <c>zwcad.ctb</c>) na rozdíl od
/// původního VBA (které příponu automaticky dopisovalo) se tu nic nedopisuje. Prázdné = použije se
/// výchozí CTB z <c>layout.json</c> (<see cref="Cad.LayoutConfig.VychoziCtb"/>), tam platí totéž.</summary>
public string Ctb { get => _ctb; set => SetField(ref _ctb, value); }
/// <summary>Orientační schéma přílohy cesta k DXF souboru (hladina <c>Rozpiska_Orientační schéma</c>).
/// Absolutní cesta, nebo relativní vůči složce <c>.rzp</c>. Prázdné = bez schématu.</summary>
public string SchemaSoubor { get => _schemaSoubor; set => SetField(ref _schemaSoubor, value); }
/// <summary>Příloha objektu <c>true</c> (výchozí) znamená, že <see cref="JmenoRozvrzeni"/> se
/// striktně generuje dle manuálu a needituje se. <c>false</c> = uživatel si jméno píše ručně
/// (<see cref="JmenoRozvrzeniRucni"/>). Při přepnutí na ruční se prázdné pole předvyplní auto
/// hodnotou. Ukládá se do projektu.</summary>
public bool JeObjektovaPriloha
{
get => _jeObjektovaPriloha;
set
{
if (!SetField(ref _jeObjektovaPriloha, value)) return;
// Přepnutí na ruční režim: předvyplň auto hodnotou, ať uživatel jen dolaďuje.
if (!value && string.IsNullOrEmpty(_jmenoRozvrzeniRucni))
_jmenoRozvrzeniRucni = JmenoRozvrzeniAuto;
OnPropertyChanged(nameof(JmenoRozvrzeni));
}
}
/// <summary>Použít zkratku <c>true</c> znamená, že se k automaticky generovanému
/// <see cref="JmenoRozvrzeni"/> (jen u objektové přílohy, viz <see cref="JeObjektovaPriloha"/>)
/// připojí <c>_</c> + <see cref="Zkratka"/> (sloupec J). Prázdná zkratka nic nepřidá.
/// Ukládá se do projektu.</summary>
public bool PouzitZkratku
{
get => _pouzitZkratku;
set { if (SetField(ref _pouzitZkratku, value)) OnPropertyChanged(nameof(JmenoRozvrzeni)); }
}
/// <summary>Rozpiska na spisové desky objektu (manuál SŽ). <c>true</c> = strojový kód pod rozpiskou
/// dostane pevný střed <c>_XX_X_000</c> (viz <see cref="Dxf.KodPrilohy.SestavDesky"/>) a u objektové
/// přílohy (<see cref="JeObjektovaPriloha"/> = <c>true</c>) je i automatický název rozvržení
/// <c>objekt_[podobjekt]_XX_X_000</c> (viz <see cref="JmenoRozvrzeniAuto"/>).
/// <b>Nevynucuje</b> <see cref="JeObjektovaPriloha"/> lze mít desky s ručním jménem rozvržení
/// (Příloha objektu = <c>false</c>): kód je pak <c>_XX_X_000</c>, ale jméno si píše uživatel.
/// Ukládá se do projektu.</summary>
public bool JeDesky
{
get => _jeDesky;
set
{
if (!SetField(ref _jeDesky, value)) return;
OnPropertyChanged(nameof(JmenoRozvrzeni)); // u objektové přílohy se auto-jméno mění na _XX_X_000
}
}
/// <summary>Rozpiska <c>true</c> (výchozí) znamená, že název exportovaného PDF dostane příponu
/// „_rozpiska". Řídí příponu samostatně, nezávisle na <see cref="JeObjektovaPriloha"/> a
/// <see cref="JeDesky"/> (viz <c>MainViewModel.SestavNazevSouboru</c>). Ukládá se do projektu.</summary>
public bool JeRozpiska { get => _jeRozpiska; set => SetField(ref _jeRozpiska, value); }
/// <summary>Ruční jméno rozvržení platí jen když <see cref="JeObjektovaPriloha"/> je <c>false</c>.
/// Ukládá se do projektu.</summary>
public string JmenoRozvrzeniRucni
{
get => _jmenoRozvrzeniRucni;
set { if (SetField(ref _jmenoRozvrzeniRucni, value)) OnPropertyChanged(nameof(JmenoRozvrzeni)); }
}
/// <summary>Automatické jméno rozvržení / souboru dle manuálu SŽ kap. 2.7.7.9: číslo objektu
/// bez teček, mezer a pomlček, jednotlivé složky (podobjekt, typ + dílčí + pořadí přílohy)
/// oddělené podtržítkem. Např. <c>SO131.45.01</c> + <c>D1</c> + <c>1</c> + <c>001</c> →
/// <c>SO1314501_D1_1_001</c>; u podobjektu <c>SO1314501_01_D1_1_001</c>; u skupiny
/// <c>SK11311_D1_1_001</c>. Prázdné složky se vynechají. Dopočítané pole do projektu se neukládá.</summary>
[JsonIgnore]
public string JmenoRozvrzeniAuto
{
get
{
// Desky objektu = objektový „speciál" (typ XX zůstává v názvu jako literální placeholder), pořadí 000.
// Platí jen když je řádek objektová příloha jinak si jméno píše uživatel ručně.
if (JeDesky) return NazevObjektovehoSpecialu(CisloObjektu, "000");
var o = ObjektOznaceni.Rozpad(CisloObjektu);
string objekt = ObjektOznaceni.Alfanum(o.Prefix + o.Cislo + o.Usek + o.Razeni); // SO + řada + úsek + řazení
// Zkratka (sloupec J) se přidává jen s příznakem PouzitZkratku a bez sanitizace (dle rozhodnutí
// uživatele) prázdná ji .Where níže vynechá (žádné koncové „_"). Neplatné znaky v názvu layoutu
// jsou vědomé riziko (CadLayoutService název nesanitizuje).
string zkratka = PouzitZkratku ? Zkratka.Trim() : "";
var casti = new[] { objekt, ObjektOznaceni.Alfanum(o.Podobjekt), ObjektOznaceni.Alfanum(TypPrilohy), ObjektOznaceni.Alfanum(DilciPriloha), ObjektOznaceni.Alfanum(PoradiPrilohy), zkratka };
return string.Join("_", casti.Where(s => !string.IsNullOrEmpty(s)));
}
}
/// <summary>Název rozvržení/souboru pro objektové „speciály" dle manuálu SŽ: <c>objekt_[podobjekt]_XX_X_NNN</c>
/// typ přílohy (<c>XX</c>) je dle manuálu literální placeholder a v názvu <b>zůstává</b> (viz metadata
/// tabulka kap. 2.7.8 a tabulka adresářové struktury, kde je pro Rozpisku/Seznam příloh objektu uveden
/// jako <c>XX</c>, nikoli vynechán). <paramref name="poradi"/>: <c>000</c> = rozpiska na spisové desky
/// objektu, <c>001</c> = seznam příloh objektu. Shoduje se se středem strojového 47-pozičního kódu
/// <c>_XX_X_NNN</c>, viz <see cref="Dxf.KodPrilohy"/>.</summary>
public static string NazevObjektovehoSpecialu(string? cisloObjektu, string poradi)
{
var o = ObjektOznaceni.Rozpad(cisloObjektu ?? "");
string objekt = ObjektOznaceni.Alfanum(o.Prefix + o.Cislo + o.Usek + o.Razeni);
var casti = new[] { objekt, ObjektOznaceni.Alfanum(o.Podobjekt), "XX", "X", poradi };
return string.Join("_", casti.Where(s => !string.IsNullOrEmpty(s)));
}
/// <summary>Hluboká kopie ukládaných hodnot řádku (pro duplikaci řádku v seznamu). Dopočítaná
/// pole a příznak duplicity se nekopírují jsou <c>[JsonIgnore]</c>, takže round-trip přes JSON
/// je zachová odvozené.</summary>
public Priloha Klon()
=> System.Text.Json.JsonSerializer.Deserialize<Priloha>(System.Text.Json.JsonSerializer.Serialize(this))!;
/// <summary>
/// Jméno rozvržení (sloupec AB) jméno layoutu / základ názvu exportu. Pro objektovou přílohu
/// (<see cref="JeObjektovaPriloha"/>) je to <see cref="JmenoRozvrzeniAuto"/>, jinak ruční hodnota.
/// Dopočítané pole ukládá se jen <see cref="JmenoRozvrzeniRucni"/> a <see cref="JeObjektovaPriloha"/>.
/// </summary>
[JsonIgnore]
public string JmenoRozvrzeni
{
get => JeObjektovaPriloha ? JmenoRozvrzeniAuto : JmenoRozvrzeniRucni;
set { if (!JeObjektovaPriloha) JmenoRozvrzeniRucni = value; }
}
/// <summary>Příznak, že <see cref="JmenoRozvrzeni"/> koliduje s jiným řádkem seznamu nastavuje ho
/// <c>MainViewModel.PrepocitejDuplicity</c>, slouží ke zvýraznění řádku. Dopočítané, neukládá se.</summary>
[JsonIgnore]
public bool JeDuplicitniJmeno { get => _jeDuplicitniJmeno; set => SetField(ref _jeDuplicitniJmeno, value); }
private bool _jeVybrana;
/// <summary>Příznak, že řádek je součástí aktuálního výběru v gridu (nastavuje ho
/// <c>MainViewModel.NastavVyber</c> z <c>DataGrid.SelectedCells</c>). Slouží k jemnému podbarvení
/// celého řádku u vícenásobného výběru. Dopočítané, neukládá se.</summary>
[JsonIgnore]
public bool JeVybrana { get => _jeVybrana; set => SetField(ref _jeVybrana, value); }
/// <summary>Řada objektu = předpona (SO/PS/SK/PK) + číslo řady, dopočítaná z prvního segmentu
/// <see cref="CisloObjektu"/> (např. <c>SO113.13.12.01</c> → <c>SO113</c>). Pole rozpisky
/// <c>RADA</c>. Dopočítané pole do projektu se neukládá.</summary>
[JsonIgnore]
public string Rada
{
get { var o = ObjektOznaceni.Rozpad(CisloObjektu); return o.Prefix + o.Cislo; }
}
/// <summary>Úsek složka označení objektu (s úvodní tečkou), dopočítaná rozpadem
/// <see cref="CisloObjektu"/> (např. <c>SO113.13.12.01</c> → <c>.13</c>). Dopočítané pole.</summary>
[JsonIgnore]
public string Usek => STeckou(ObjektOznaceni.Rozpad(CisloObjektu).Usek);
/// <summary>Řazení složka označení objektu (s úvodní tečkou), dopočítaná rozpadem
/// <see cref="CisloObjektu"/> (např. <c>SO113.13.12.01</c> → <c>.12</c>). Dopočítané pole.</summary>
[JsonIgnore]
public string Razeni => STeckou(ObjektOznaceni.Rozpad(CisloObjektu).Razeni);
/// <summary>Podobjekt složka označení objektu (s úvodní tečkou), dopočítaná rozpadem
/// <see cref="CisloObjektu"/> (např. <c>SO113.13.12.01</c> → <c>.01</c>). Dopočítané pole.</summary>
[JsonIgnore]
public string Podobjekt => STeckou(ObjektOznaceni.Rozpad(CisloObjektu).Podobjekt);
/// <summary>Typ přílohy do rozpisky = „číslo přílohy typ" + <c>.</c> + „číslo přílohy dílčí"
/// (např. <c>D1</c> + <c>1</c> → <c>D1.1</c>). Dopočítané pole do projektu se neukládá.</summary>
[JsonIgnore]
public string Typ
{
get
{
bool t = !string.IsNullOrWhiteSpace(TypPrilohy), d = !string.IsNullOrWhiteSpace(DilciPriloha);
if (t && d) return $"{TypPrilohy}.{DilciPriloha}";
return t ? TypPrilohy : (d ? DilciPriloha : "");
}
}
/// <summary>Předřadí složce čísla objektu tečku (prázdná složka zůstane prázdná).</summary>
private static string STeckou(string s) => string.IsNullOrEmpty(s) ? "" : "." + s;
}
+87
View File
@@ -0,0 +1,87 @@
namespace Rozpisky.Models;
/// <summary>
/// Údaje společné celému projektu (původní excelový list ÚDAJE, ROZHRANI.md §3.1).
/// Část polí (adresy, kontakty) se automaticky dohledává z číselníků náhrada VLOOKUP.
/// </summary>
public sealed class Projekt : ObservableObject
{
private string _stavba = "";
private string _hip = "";
private string _investor = "";
private string _investorAdresa = "";
private string _slozkaSZ = "";
private string _slozkaSZAdresa = "";
private string _skod = "";
private string _cisloZakazky = "";
private string _stupenDokumentace = "";
private string _smluvniDatumZpracovani = "";
private string _zhotovitelSOD = "";
private string _adresaSOD = "";
private string _telSOD = "";
private string _emailSOD = "";
private string _logo1Soubor = "";
private string _logo2Soubor = "";
/// <summary>Název stavby / akce (ruční).</summary>
public string Stavba { get => _stavba; set => SetField(ref _stavba, value); }
/// <summary>Hlavní inženýr projektu (ruční).</summary>
public string HIP { get => _hip; set => SetField(ref _hip, value); }
/// <summary>Objednatel zakázky (číselník Organizace).</summary>
public string Investor { get => _investor; set => SetField(ref _investor, value); }
/// <summary>Adresa objednatele (auto z č. Organizace).</summary>
public string InvestorAdresa { get => _investorAdresa; set => SetField(ref _investorAdresa, value); }
/// <summary>Složka SŽ zástupce objednatele (ruční / číselník).</summary>
public string SlozkaSZ { get => _slozkaSZ; set => SetField(ref _slozkaSZ, value); }
/// <summary>Adresa složky SŽ (auto z č. Organizace).</summary>
public string SlozkaSZAdresa { get => _slozkaSZAdresa; set => SetField(ref _slozkaSZAdresa, value); }
/// <summary>S-kód (číslo smlouvy dle SoD).</summary>
public string Skod { get => _skod; set => SetField(ref _skod, value); }
/// <summary>Číslo zakázky.</summary>
public string CisloZakazky { get => _cisloZakazky; set => SetField(ref _cisloZakazky, value); }
/// <summary>Stupeň dokumentace (číselník Verze dokumentace).</summary>
public string StupenDokumentace { get => _stupenDokumentace; set => SetField(ref _stupenDokumentace, value); }
/// <summary>Smluvní datum zpracování (ruční).</summary>
public string SmluvniDatumZpracovani { get => _smluvniDatumZpracovani; set => SetField(ref _smluvniDatumZpracovani, value); }
/// <summary>Zhotovitel díla dle SoD (číselník Firmy).</summary>
public string ZhotovitelSOD { get => _zhotovitelSOD; set => SetField(ref _zhotovitelSOD, value); }
/// <summary>Adresa zhotovitele (auto z č. Firmy).</summary>
public string AdresaSOD { get => _adresaSOD; set => SetField(ref _adresaSOD, value); }
/// <summary>Telefon zhotovitele (auto z č. Firmy).</summary>
public string TelSOD { get => _telSOD; set => SetField(ref _telSOD, value); }
/// <summary>E-mail zhotovitele (auto z č. Firmy).</summary>
public string EmailSOD { get => _emailSOD; set => SetField(ref _emailSOD, value); }
/// <summary>Logo financující instituce dotační program (vlevo, hladina <c>Rozpiska_logo1</c>).
/// Jméno DXF souboru ve složce <c>loga\</c> vedle .exe; stejné pro všechny rozpisky projektu.</summary>
public string Logo1Soubor { get => _logo1Soubor; set => SetField(ref _logo1Soubor, value); }
/// <summary>Logo financující instituce státní (vpravo, hladina <c>Rozpiska_logo2</c>).
/// Jméno DXF souboru ve složce <c>loga\</c> vedle .exe; stejné pro všechny rozpisky projektu.</summary>
public string Logo2Soubor { get => _logo2Soubor; set => SetField(ref _logo2Soubor, value); }
/// <summary>Přepíše hodnoty z jiné instance (zachová databinding živé instance) při načtení projektu.</summary>
public void CopyFrom(Projekt o)
{
Stavba = o.Stavba; HIP = o.HIP;
Investor = o.Investor; InvestorAdresa = o.InvestorAdresa;
SlozkaSZ = o.SlozkaSZ; SlozkaSZAdresa = o.SlozkaSZAdresa;
Skod = o.Skod; CisloZakazky = o.CisloZakazky;
StupenDokumentace = o.StupenDokumentace; SmluvniDatumZpracovani = o.SmluvniDatumZpracovani;
ZhotovitelSOD = o.ZhotovitelSOD; AdresaSOD = o.AdresaSOD; TelSOD = o.TelSOD; EmailSOD = o.EmailSOD;
Logo1Soubor = o.Logo1Soubor; Logo2Soubor = o.Logo2Soubor;
}
}
+21
View File
@@ -0,0 +1,21 @@
namespace Rozpisky.Models;
/// <summary>
/// Serializovatelný obsah jednoho projektu (zakázky) = interní data ukládaná do souboru
/// `.rzp` (ROZHRANI.md §9). Číselníky se sem NEukládají jsou sdílené mimo projekt.
/// </summary>
public sealed class ProjektDocument
{
/// <summary>Verze formátu souboru (pro budoucí migrace).</summary>
public int Verze { get; set; } = 1;
public Projekt Projekt { get; set; } = new();
public List<Priloha> Prilohy { get; set; } = new();
public List<Revize> Revize { get; set; } = new();
public Nastaveni Nastaveni { get; set; } = new();
/// <summary>Šířky sloupců gridu „Seznam příloh" v pixelech, ve fixním pořadí sloupců podle XAML
/// (přeskládání sloupců je zakázané). UI stav ukládaný spolu s daty; prázdný seznam = výchozí
/// šířky z XAML.</summary>
public List<double> SirkySloupcuSeznamu { get; set; } = new();
}
+21
View File
@@ -0,0 +1,21 @@
namespace Rozpisky.Models;
/// <summary>
/// Řádek tabulky revizí projektu (původní list REVIZE, ROZHRANI.md §3.3).
/// Revize jsou interní data konkrétní zakázky (ukládají se s projektem, ne do číselníků).
/// </summary>
public sealed class Revize : ObservableObject
{
private string _cislo = "";
private string _datum = "";
private string _popis = "";
/// <summary>Kód / číslo verze revize (např. „V00") sloupec REVIZE.</summary>
public string Cislo { get => _cislo; set => SetField(ref _cislo, value); }
/// <summary>Datum revize sloupec DATUM.</summary>
public string Datum { get => _datum; set => SetField(ref _datum, value); }
/// <summary>Popis revize sloupec POPIS.</summary>
public string Popis { get => _popis; set => SetField(ref _popis, value); }
}
+53
View File
@@ -0,0 +1,53 @@
<Window x:Class="Rozpisky.NahledWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:skia="clr-namespace:SkiaSharp.Views.WPF;assembly=SkiaSharp.Views.WPF"
mc:Ignorable="d"
Title="Náhled rozpisky" Height="820" Width="640"
Icon="/Rozpisky.ico"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource Brush.Window}"
Foreground="{DynamicResource Brush.Text}">
<DockPanel Margin="12">
<!-- Záhlaví + přepínač zobrazení -->
<Grid DockPanel.Dock="Top" Margin="0,0,0,10">
<StackPanel>
<TextBlock x:Name="Nadpis" Style="{StaticResource Text.H2}"/>
<TextBlock x:Name="Podnadpis" Style="{StaticResource Text.Caption}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top">
<TextBlock Text="Názvy polí" Style="{StaticResource Text.Caption}" VerticalAlignment="Center" Margin="0,0,8,0"/>
<ToggleButton x:Name="PrepinacZobrazeni" Style="{StaticResource ToggleSwitch}" IsChecked="True"
Checked="Prepinac_Changed" Unchecked="Prepinac_Changed"/>
<TextBlock Text="Vyplněné" Style="{StaticResource Text.Caption}" VerticalAlignment="Center" Margin="8,0,0,0"/>
</StackPanel>
</Grid>
<!-- Akce náhledu -->
<DockPanel DockPanel.Dock="Bottom" Margin="0,10,0,0" LastChildFill="False">
<TextBlock DockPanel.Dock="Left" Style="{StaticResource Text.Caption}" VerticalAlignment="Center"
Text="Kolečko = zoom, tažení = posun, dvojklik = výchozí zobrazení"/>
<Button DockPanel.Dock="Right" Content="Zavřít" Style="{StaticResource Button.Primary}" Margin="8,0,0,0" Click="Zavrit_Click"/>
</DockPanel>
<!-- Kreslicí plocha: pan/zoom přes MatrixTransform na obrázku -->
<Border Background="White"
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="1"
CornerRadius="{DynamicResource Metric.Radius}" ClipToBounds="True">
<Grid x:Name="Plocha" Background="Transparent"
MouseWheel="Plocha_MouseWheel"
MouseLeftButtonDown="Plocha_MouseLeftButtonDown"
MouseLeftButtonUp="Plocha_MouseLeftButtonUp"
MouseMove="Plocha_MouseMove"
MouseRightButtonDown="Plocha_ResetView">
<skia:SKElement x:Name="Nahled" PaintSurface="Nahled_PaintSurface"/>
<TextBlock x:Name="Chyba" Visibility="Collapsed" Foreground="{DynamicResource Brush.Danger}"
TextWrapping="Wrap" TextAlignment="Center" MaxWidth="360"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</Border>
</DockPanel>
</Window>
+153
View File
@@ -0,0 +1,153 @@
using System.Windows;
using System.Windows.Input;
using SkiaSharp;
using SkiaSharp.Views.Desktop;
using Rozpisky.Dxf;
using Rozpisky.Models;
namespace Rozpisky;
/// <summary>
/// Náhled rozpisky (ROZHRANI.md §4.4): vykreslí razítko z DXF šablony engine-em
/// a přepíná zobrazení vyplněné ↔ názvy polí. Export se dělá dávkově z hlavního okna.
/// </summary>
public partial class NahledWindow : Window
{
private readonly Priloha _priloha;
private readonly Projekt _projekt;
private readonly IReadOnlyList<Revize> _revize;
private readonly Ciselniky _ciselniky;
private readonly IReadOnlyDictionary<string, string>? _loga;
private readonly RozpiskaRenderer? _renderer;
public NahledWindow(Priloha priloha, Projekt projekt, IReadOnlyList<Revize> revize, Ciselniky ciselniky, string sablonaPath,
IReadOnlyDictionary<string, string>? loga = null)
{
InitializeComponent();
_priloha = priloha;
_projekt = projekt;
_revize = revize;
_ciselniky = ciselniky;
_loga = loga;
var rozvrzeni = string.IsNullOrWhiteSpace(_priloha.JmenoRozvrzeni) ? "(bez rozvržení)" : _priloha.JmenoRozvrzeni;
Title = $"Náhled {rozvrzeni}";
Nadpis.Text = string.IsNullOrWhiteSpace(_priloha.NazevPrilohy) ? rozvrzeni : _priloha.NazevPrilohy;
Podnadpis.Text = $"{_projekt.Stavba} • rozvržení {rozvrzeni}";
try
{
_renderer = new RozpiskaRenderer(sablonaPath);
}
catch (Exception ex)
{
ZobrazChybu($"Šablonu rozpisky se nepodařilo načíst:\n{RozpiskaRenderer.ResolvePath(sablonaPath)}\n\n{ex.Message}");
}
Loaded += (_, _) => Vykresli();
}
private bool Vyplnene => PrepinacZobrazeni.IsChecked == true;
/// <summary>Požádá plátno o překreslení (vektorově, v aktuálním zoomu/posunu).
/// Null-podmínka: přepínač zobrazení může Vykresli zavolat ještě během InitializeComponent,
/// kdy pole <see cref="Nahled"/> není přiřazené.</summary>
private void Vykresli() => Nahled?.InvalidateVisual();
/// <summary>Vykreslí rozpisku přímo na Skia plátno vektorově, takže zoom je bez ztráty kvality.</summary>
private void Nahled_PaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
var canvas = e.Surface.Canvas;
int w = e.Info.Width, h = e.Info.Height;
// Poměr fyzické pixely / WPF jednotky (DIP) pro převod pozic myši na pixely plátna.
_dpiScale = Nahled.ActualWidth > 0 ? w / Nahled.ActualWidth : 1.0;
if (_renderer is null) { canvas.Clear(SKColors.White); return; }
try
{
var draw = _renderer.BuildDraw(_projekt, _priloha, _revize, _ciselniky, Vyplnene, _loga);
Exporters.RenderView(canvas, w, h, draw, _renderer.SheetFrame, _zoom, _panX, _panY);
}
catch (Exception ex)
{
canvas.Clear(SKColors.White);
Dispatcher.BeginInvoke(() => ZobrazChybu($"Náhled se nepodařilo vykreslit:\n{ex.Message}"));
}
}
private void Prepinac_Changed(object sender, RoutedEventArgs e) => Vykresli();
// ── Pan & zoom náhledu ──────────────────────────────────────────────────
// Stav transformace pro vektorové plátno: zoom (1 = výchozí fit, strop 20×) a posun
// v pixelech plátna. Souřadnice myši (DIP) se násobí _dpiScale na pixely plátna.
private double _zoom = 1.0;
private double _panX, _panY;
private double _dpiScale = 1.0;
private bool _panuje;
private Point _poslednibod;
private void Plocha_MouseWheel(object sender, MouseWheelEventArgs e)
{
double cx = e.GetPosition(Plocha).X * _dpiScale; // pozice kurzoru v px plátna
double cy = e.GetPosition(Plocha).Y * _dpiScale;
double novy = Math.Clamp(_zoom * (e.Delta > 0 ? 1.15 : 1 / 1.15), 1.0, 20.0);
double f = novy / _zoom;
// Posun tak, aby bod pod kurzorem zůstal na místě (screen = pan + zoom·base).
_panX = cx - f * (cx - _panX);
_panY = cy - f * (cy - _panY);
_zoom = novy;
Vykresli();
e.Handled = true;
}
private void Plocha_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ClickCount == 2) { ResetView(); return; }
_panuje = true;
_poslednibod = e.GetPosition(Plocha);
Plocha.CaptureMouse();
Plocha.Cursor = Cursors.SizeAll;
}
private void Plocha_MouseMove(object sender, MouseEventArgs e)
{
if (!_panuje) return;
Point nyni = e.GetPosition(Plocha);
_panX += (nyni.X - _poslednibod.X) * _dpiScale;
_panY += (nyni.Y - _poslednibod.Y) * _dpiScale;
_poslednibod = nyni;
Vykresli();
}
private void Plocha_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_panuje = false;
Plocha.ReleaseMouseCapture();
Plocha.Cursor = Cursors.Arrow;
}
private void Plocha_ResetView(object sender, MouseButtonEventArgs e) => ResetView();
private void ResetView()
{
_zoom = 1.0;
_panX = _panY = 0;
Vykresli();
}
private void ZobrazChybu(string text)
{
Chyba.Text = text;
Chyba.Visibility = Visibility.Visible;
Nahled.Visibility = Visibility.Collapsed;
}
private void Zavrit_Click(object sender, RoutedEventArgs e) => Close();
/// <summary>Escape zavře náhled (drag pan na plátně klávesy nezachytává, takže stačí na okně).</summary>
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.Key == Key.Escape) { Close(); e.Handled = true; return; }
base.OnKeyDown(e);
}
}
+71
View File
@@ -0,0 +1,71 @@
namespace Rozpisky.Rendering;
/// <summary>
/// Nic nekreslí jen sčítá min/max souřadnic (ROZPISKY.md §3). Stejnou kresbou se zjistí
/// rozsah pro vystředění na A4 / „zoom to fit".
/// </summary>
public sealed class BoundsRenderer : IProfileRenderer
{
public double MinX { get; private set; } = double.MaxValue;
public double MinY { get; private set; } = double.MaxValue;
public double MaxX { get; private set; } = double.MinValue;
public double MaxY { get; private set; } = double.MinValue;
public bool IsEmpty => MinX > MaxX;
public double Width => IsEmpty ? 0 : MaxX - MinX;
public double Height => IsEmpty ? 0 : MaxY - MinY;
private void Add(double x, double y)
{
if (x < MinX) MinX = x;
if (y < MinY) MinY = y;
if (x > MaxX) MaxX = x;
if (y > MaxY) MaxY = y;
}
public void Layer(string name, int rgb, double lineWeightMm = 0.25) { }
public void Line(double x1, double y1, double x2, double y2) { Add(x1, y1); Add(x2, y2); }
public void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false)
{
foreach (var (x, y) in pts) Add(x, y);
}
public void Circle(double cx, double cy, double r) { Add(cx - r, cy - r); Add(cx + r, cy + r); }
public void Stroke(Contour c) => AddContour(c);
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)
{
// Hrubý odhad rozsahu (na vystředění stačí kotva + přibližná výška/šířka), víceřádkově.
var segs = (text ?? "").Replace("\\P", "\n").Replace('\r', '\n').Split('\n');
int longest = 0;
foreach (var s in segs) if (s.Length > longest) longest = s.Length;
double wf = widthFactor > 0 ? widthFactor : 1;
double w = longest * heightMm * 0.6 * wf
+ heightMm * Math.Abs(Math.Tan(style.ObliqueDeg * Math.PI / 180)); // přesah sklonu
double blockH = segs.Length * heightMm * 1.4;
// Kotva dle zarovnání: box roste od kotvy směrem daným H/V (dřív jen doprava nahoru).
double dx = h switch { HAlign.Center => -w / 2, HAlign.Right => -w, _ => 0 };
double dy = v switch { VAlign.Top => -blockH, VAlign.Middle => -blockH / 2, _ => 0 };
Add(x + dx, y + dy);
Add(x + dx + w, y + dy + blockH);
}
public void Fill(IReadOnlyList<Contour> loops)
{
foreach (var loop in loops) AddContour(loop);
}
/// <summary>Započítá obrys oblouky navzorkuje na body (na bbox stačí hrubší tolerance).</summary>
private void AddContour(Contour c)
{
var pts = new List<(double X, double Y)>();
foreach (var e in c.Edges) e.AppendPoints(pts, maxSagitta: 0.1);
foreach (var (x, y) in pts) Add(x, y);
if (c.Edges.Count > 0) { Add(c.Edges[^1].EX, c.Edges[^1].EY); } // poslední koncový bod
}
}
+296
View File
@@ -0,0 +1,296 @@
using System.IO;
using ACadSharp;
using ACadSharp.Entities;
using CSMath;
using AcLayer = ACadSharp.Tables.Layer;
using AcTextStyle = ACadSharp.Tables.TextStyle;
using FontFlags = ACadSharp.Tables.FontFlags;
namespace Rozpisky.Rendering;
/// <summary>
/// <see cref="IProfileRenderer"/>, který místo rasterizace staví <b>ACadSharp entity</b> do jednoho
/// <see cref="CadDocument"/> pro export rozpisek do DXF. Implementuje celý kontrakt rozhraní
/// (6 primitiv), takže má stejné pokrytí jako vykreslovací engine: složitější DXF entity (spline,
/// oblouk, hatch) jsou už ve fázi čtení (<c>DxfTemplate.Replay</c>) převedené na čáry/polylinie/výplně.
/// <para>Zachovává <b>barvu, tloušťku čáry a styl/zarovnání písma</b> přesně podle zdroje stav se
/// přebírá z <see cref="Layer"/> (barva + tloušťka pro každou entitu) a z parametru <c>style</c>
/// metody <see cref="Text"/> (font). Souřadnice jsou ve výkresových mm, osa Y nahoru shodně s DXF.
/// Jednořádkový text → <c>TEXT</c>, víceřádkový (<c>maxWidthMm &gt; 0</c>) → <c>MTEXT</c>.</para>
/// </summary>
public sealed class DxfProfileRenderer : IProfileRenderer
{
private readonly CadDocument _doc = new();
private readonly Dictionary<string, AcLayer> _vrstvy = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, AcTextStyle> _styly = new(StringComparer.OrdinalIgnoreCase);
private AcLayer _aktualniVrstva;
private Color _aktualniBarva;
private LineWeightType _aktualniTloustka = LineWeightType.Default;
public DxfProfileRenderer()
{
if (!_doc.Layers.TryGetValue(AcLayer.DefaultName, out _aktualniVrstva!))
{
_aktualniVrstva = new AcLayer(AcLayer.DefaultName);
_doc.Layers.Add(_aktualniVrstva);
}
_vrstvy[_aktualniVrstva.Name] = _aktualniVrstva;
_aktualniBarva = new Color(0, 0, 0);
}
/// <summary>Hotový výkres se všemi rozpiskami (pro <c>DxfWriter.Write</c>).</summary>
public CadDocument Document => _doc;
public void Layer(string name, int rgb, double lineWeightMm = 0.25)
{
if (string.IsNullOrEmpty(name)) name = AcLayer.DefaultName;
if (!_vrstvy.TryGetValue(name, out var vrstva))
{
if (!_doc.Layers.TryGetValue(name, out vrstva!))
{
vrstva = new AcLayer(name) { Color = RgbNaColor(rgb) };
_doc.Layers.Add(vrstva);
}
_vrstvy[name] = vrstva;
}
_aktualniVrstva = vrstva;
_aktualniBarva = RgbNaColor(rgb); // barva entity dle zdroje (ByLayer už rozbalil DxfTemplate)
_aktualniTloustka = SnapTloustka(lineWeightMm);
}
public void Line(double x1, double y1, double x2, double y2)
=> Pridej(new Line(new XYZ(x1, y1, 0), new XYZ(x2, y2, 0)));
public void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false)
{
if (pts.Count == 0) return;
Pridej(new LwPolyline(pts.Select(p => new XY(p.X, p.Y))) { IsClosed = closed });
}
public void Circle(double cx, double cy, double r)
{
if (r <= 0) return;
Pridej(new Circle(new XYZ(cx, cy, 0), r));
}
public void Stroke(Contour c)
{
var edges = c.Edges;
if (edges.Count == 0) return;
// Samostatný oblouk → věrná entita ARC (bulge polylinie by u velkého rozsahu byla nepřesná).
if (edges.Count == 1 && edges[0].Kind == EdgeKind.Arc && !c.Closed)
{
var e = edges[0];
// ARC se kreslí proti směru hodin start→end; pro záporný rozsah (CW) prohodíme meze.
double start = e.Sweep >= 0 ? e.A0 : e.A0 + e.Sweep;
double end = e.Sweep >= 0 ? e.A0 + e.Sweep : e.A0;
Pridej(new Arc(new XYZ(e.Cx, e.Cy, 0), e.R, start, end));
return;
}
// Jinak LwPolyline s bulge: oblouk → bulge = tan(Sweep/4), úsečka → 0.
var lw = new LwPolyline { IsClosed = c.Closed };
foreach (var e in edges)
lw.Vertices.Add(new LwPolyline.Vertex(new XY(e.SX, e.SY))
{ Bulge = e.Kind == EdgeKind.Arc ? Math.Tan(e.Sweep / 4) : 0 });
if (!c.Closed)
lw.Vertices.Add(new LwPolyline.Vertex(new XY(edges[^1].EX, edges[^1].EY)));
Pridej(lw);
}
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)
{
if (string.IsNullOrEmpty(text) || heightMm <= 0) return;
var styl = StylProEntitu(style);
if (maxWidthMm > 0)
{
var mtext = new MText
{
Value = text.Replace("\r\n", "\\P").Replace("\n", "\\P"),
InsertPoint = new XYZ(x, y, 0),
Height = heightMm,
RectangleWidth = maxWidthMm,
AttachmentPoint = MapAttach(h, v),
};
if (styl is not null) mtext.Style = styl;
Pridej(mtext);
}
else
{
var t = new TextEntity
{
Value = text,
InsertPoint = new XYZ(x, y, 0),
AlignmentPoint = new XYZ(x, y, 0),
Height = heightMm,
Rotation = rotationDeg * Math.PI / 180.0,
ObliqueAngle = style.ObliqueDeg * Math.PI / 180.0,
WidthFactor = widthFactor <= 0 ? 1 : widthFactor,
HorizontalAlignment = MapH(h),
VerticalAlignment = MapV(v),
};
if (styl is not null) t.Style = styl;
Pridej(t);
}
}
public void Fill(IReadOnlyList<Contour> loops)
{
if (loops.Count == 0) return;
try
{
var hatch = new Hatch { IsSolid = true, Pattern = new HatchPattern("SOLID") };
foreach (var loop in loops)
{
if (loop.Edges.Count == 0) continue;
var path = new Hatch.BoundaryPath();
if (loop.Edges.All(e => e.Kind == EdgeKind.Line))
{
// Samé úsečky → kompaktní polylinie (jako dřív).
path.Edges.Add(new Hatch.BoundaryPath.Polyline(
loop.Edges.Select(e => new XYZ(e.SX, e.SY, 0)), true));
}
else
{
// Obsahuje oblouky → věrné hranové edge (úsečka / kruhový oblouk).
foreach (var e in loop.Edges) path.Edges.Add(HranaNaBoundary(e));
}
hatch.Paths.Add(path);
}
if (hatch.Paths.Count == 0) return;
Pridej(hatch);
}
catch
{
// Záloha: výplň nešla sestavit → alespoň obrys smyček.
foreach (var loop in loops) Stroke(loop with { Closed = true });
}
}
/// <summary>Převede analytickou hranu obrysu na DXF hraniční edge šrafy (úsečka / kruhový oblouk).
/// U oblouku: záporný rozsah (CW) se v DXF vyjádří <c>CounterClockWise=false</c>, kde čtečka úhly
/// zrcadlí přes osu X proto pro CW meze <c>A0</c> / <c>(A0+Sweep)</c>, znormalizované do [0,2π).</summary>
private static Hatch.BoundaryPath.Edge HranaNaBoundary(Edge e)
{
if (e.Kind == EdgeKind.Line)
return new Hatch.BoundaryPath.Line { Start = new XY(e.SX, e.SY), End = new XY(e.EX, e.EY) };
bool ccw = e.Sweep >= 0;
double sa = Norm(ccw ? e.A0 : -e.A0);
double ea = Norm(ccw ? e.A0 + e.Sweep : -(e.A0 + e.Sweep));
if (ea <= sa) ea += 2 * Math.PI;
return new Hatch.BoundaryPath.Arc
{
Center = new XY(e.Cx, e.Cy),
Radius = e.R,
StartAngle = sa,
EndAngle = ea,
CounterClockWise = ccw,
};
}
private static double Norm(double a)
{
a %= 2 * Math.PI;
return a < 0 ? a + 2 * Math.PI : a;
}
// ── pomocné ────────────────────────────────────────────────────────────
private void Pridej(Entity e)
{
e.Layer = _aktualniVrstva;
e.Color = _aktualniBarva;
e.LineWeight = _aktualniTloustka;
_doc.Entities.Add(e);
}
/// <summary>Najde/vytvoří DXF textový styl pro daný vzhled písma (font soubor + řez). Null = bez
/// stylu (použije se výchozí styl dokumentu).</summary>
private AcTextStyle? StylProEntitu(TextStyle s)
{
// Soubor fontu: přednostně .ttf z předlohy, jinak odvozeno z rodiny písma.
string? file = s.FontFile ?? (string.IsNullOrWhiteSpace(s.FontFamily) ? null : s.FontFamily + ".ttf");
if (file is null && string.IsNullOrWhiteSpace(s.FontFamily)) return null;
string klic = $"{file}|{s.FontFamily}|{(s.Bold ? "B" : "")}{(s.Italic ? "I" : "")}|{s.ObliqueDeg}";
if (_styly.TryGetValue(klic, out var styl)) return styl;
var ff = FontFlags.Regular;
if (s.Bold) ff |= FontFlags.Bold;
if (s.Italic) ff |= FontFlags.Italic;
string nazev = UnikatniNazevStylu(s.FontFamily ?? Path.GetFileNameWithoutExtension(file) ?? "Styl", s.Bold, s.Italic);
// Oblique i na stylu TEXT ho nese sám (group 51), ale MTEXT sklon přebírá jen ze stylu.
styl = new AcTextStyle(nazev) { Filename = file ?? "", TrueType = ff, ObliqueAngle = s.ObliqueDeg * Math.PI / 180.0 };
_doc.TextStyles.Add(styl);
_styly[klic] = styl;
return styl;
}
private string UnikatniNazevStylu(string zaklad, bool bold, bool italic)
{
var sb = new System.Text.StringBuilder();
foreach (var c in zaklad) sb.Append(char.IsLetterOrDigit(c) ? c : '_');
if (bold) sb.Append("_B");
if (italic) sb.Append("_I");
string nazev = sb.ToString();
if (string.IsNullOrEmpty(nazev)) nazev = "Styl";
string kandidat = nazev;
for (int i = 1; _doc.TextStyles.Contains(kandidat); i++) kandidat = $"{nazev}_{i}";
return kandidat;
}
private static Color RgbNaColor(int rgb)
=> new((byte)((rgb >> 16) & 0xFF), (byte)((rgb >> 8) & 0xFF), (byte)(rgb & 0xFF));
/// <summary>Standardní tloušťky čar (1/100 mm) dle DXF; vstupní mm se přichytí k nejbližší.</summary>
private static readonly int[] StdTloustky =
{ 0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158, 200, 211 };
private static LineWeightType SnapTloustka(double mm)
{
if (mm <= 0) return LineWeightType.Default;
int h = (int)Math.Round(mm * 100);
int best = StdTloustky[0], bestDiff = int.MaxValue;
foreach (var s in StdTloustky)
{
int d = Math.Abs(s - h);
if (d < bestDiff) { bestDiff = d; best = s; }
}
return (LineWeightType)best;
}
private static TextHorizontalAlignment MapH(HAlign h) => h switch
{
HAlign.Center => TextHorizontalAlignment.Center,
HAlign.Right => TextHorizontalAlignment.Right,
_ => TextHorizontalAlignment.Left,
};
private static TextVerticalAlignmentType MapV(VAlign v) => v switch
{
VAlign.Top => TextVerticalAlignmentType.Top,
VAlign.Middle => TextVerticalAlignmentType.Middle,
VAlign.Bottom => TextVerticalAlignmentType.Bottom,
_ => TextVerticalAlignmentType.Baseline,
};
private static AttachmentPointType MapAttach(HAlign h, VAlign v) => (v, h) switch
{
(VAlign.Top, HAlign.Left) => AttachmentPointType.TopLeft,
(VAlign.Top, HAlign.Center) => AttachmentPointType.TopCenter,
(VAlign.Top, HAlign.Right) => AttachmentPointType.TopRight,
(VAlign.Middle, HAlign.Left) => AttachmentPointType.MiddleLeft,
(VAlign.Middle, HAlign.Center) => AttachmentPointType.MiddleCenter,
(VAlign.Middle, HAlign.Right) => AttachmentPointType.MiddleRight,
(_, HAlign.Center) => AttachmentPointType.BottomCenter,
(_, HAlign.Right) => AttachmentPointType.BottomRight,
_ => AttachmentPointType.BottomLeft,
};
}
+108
View File
@@ -0,0 +1,108 @@
namespace Rozpisky.Rendering;
/// <summary>Vodorovné zarovnání textu.</summary>
public enum HAlign { Left, Center, Right }
/// <summary>Svislé zarovnání textu.</summary>
public enum VAlign { Baseline, Bottom, Middle, Top }
/// <summary>
/// Vzhled písma přenesený z DXF textového stylu (ROZPISKY.md §3): rodina fontu + řez,
/// volitelně přímo soubor fontu (.ttf) pro věrný řez. <c>default</c> = fallback font rendereru.
/// <paramref name="ObliqueDeg"/> = sklon znaků ve stupních (DXF oblique angle, group 51 entity /
/// group 50 stylu); kladný = naklonění doprava, 0 = vzpřímeně.
/// </summary>
public readonly record struct TextStyle(string? FontFamily = null, bool Bold = false, bool Italic = false,
string? FontFile = null, double ObliqueDeg = 0);
/// <summary>Druh hrany obrysu: úsečka nebo kruhový oblouk. Elipsy a spliny se do obrysu vkládají
/// jako jemná řada úseček (viz <see cref="Edge"/>), takže renderery vystačí se dvěma případy.</summary>
public enum EdgeKind : byte { Line, Arc }
/// <summary>
/// Jedna hrana obrysu ve výkresových mm (WCS, osa Y nahoru). Nese <b>analytický</b> tvar
/// úsečku nebo kruhový oblouk aby renderery kreslily hladké křivky (Skia <c>ArcTo</c>, DXF
/// <c>ARC</c>/hatch oblouk) místo lomené čáry. Počátek i konec jsou uložené explicitně kvůli
/// řetězení a otáčení hran ve smyčce (hatch). Oblouk: střed <c>(Cx,Cy)</c>, poloměr <c>R</c>,
/// počáteční úhel <c>A0</c> a <b>znaménkový</b> rozsah <c>Sweep</c> v radiánech (kladný = proti
/// směru hodin); bod(θ) = střed + R·(cos θ, sin θ), θ ∈ [A0, A0+Sweep].
/// </summary>
public readonly record struct Edge(
EdgeKind Kind, double SX, double SY, double EX, double EY,
double Cx = 0, double Cy = 0, double R = 0, double A0 = 0, double Sweep = 0)
{
public static Edge Line(double sx, double sy, double ex, double ey)
=> new(EdgeKind.Line, sx, sy, ex, ey);
public static Edge Arc(double sx, double sy, double ex, double ey,
double cx, double cy, double r, double a0, double sweep)
=> new(EdgeKind.Arc, sx, sy, ex, ey, cx, cy, r, a0, sweep);
/// <summary>Otočí hranu (prohodí start/konec; u oblouku obrátí směr rozsahu).</summary>
public Edge Reversed() => Kind == EdgeKind.Line
? this with { SX = EX, SY = EY, EX = SX, EY = SY }
: this with { SX = EX, SY = EY, EX = SX, EY = SY, A0 = A0 + Sweep, Sweep = -Sweep };
/// <summary>Bod oblouku pro úhel θ (radiány).</summary>
public (double X, double Y) PointAt(double theta) => (Cx + R * Math.Cos(theta), Cy + R * Math.Sin(theta));
}
/// <summary>Obrys = zřetězené hrany (konec jedné = počátek další). <paramref name="Closed"/> = true →
/// vyplnitelná/uzavřená smyčka (poslední hrana se vrací k počátku prvního).</summary>
public readonly record struct Contour(IReadOnlyList<Edge> Edges, bool Closed);
/// <summary>Pomůcky pro převod obrysů na body jen pro <b>odhad rozsahu</b> (bbox) a nouzové
/// zálohy; skutečná kresba (Skia/DXF) zůstává analytická (hladké oblouky).</summary>
public static class ContourGeometry
{
/// <summary>Navzorkuje hranu na body včetně počátečního (koncový přidá navazující hrana / uzávěr).
/// Oblouk se dělí tak, aby výška úsečky (sagitta) nepřekročila <paramref name="maxSagitta"/> mm.</summary>
public static void AppendPoints(this Edge e, List<(double X, double Y)> pts, double maxSagitta = 0.05)
{
pts.Add((e.SX, e.SY));
if (e.Kind == EdgeKind.Line || e.R <= 0) return;
int n = ArcSegments(Math.Abs(e.Sweep), e.R, maxSagitta);
for (int i = 1; i < n; i++) pts.Add(e.PointAt(e.A0 + e.Sweep * i / n));
}
/// <summary>Počet dílků oblouku (poloměr <paramref name="r"/>, rozsah <paramref name="sweep"/> rad),
/// aby sagitta ≤ <paramref name="maxSagitta"/>: δ = 2·acos(1tol/r), n = ⌈|sweep|/δ⌉.</summary>
public static int ArcSegments(double sweep, double r, double maxSagitta)
{
if (r <= 0 || sweep <= 0) return 1;
double ratio = 1 - maxSagitta / r;
double delta = ratio <= -1 ? Math.PI : 2 * Math.Acos(Math.Max(ratio, -1));
if (delta < 1e-6) return 1;
return Math.Max(1, (int)Math.Ceiling(sweep / delta));
}
}
/// <summary>
/// Kreslicí rozhraní (ROZPISKY.md §3). Vše ve výkresových milimetrech, osa Y nahoru
/// (jako v CAD/DXF). Tatáž kresba teče na obrazovku, do PNG i do PDF liší se jen
/// implementace (Skia / bounds).
/// </summary>
public interface IProfileRenderer
{
/// <summary>Nastaví aktuální hladinu/styl kresby. <paramref name="rgb"/> = barva 0xRRGGBB
/// (rozbalená z DXF, ByLayer i index/true-color), tloušťka v mm. Bílá se kreslí černě
/// (bílá na bílém papíře by zmizela).</summary>
void Layer(string name, int rgb, double lineWeightMm = 0.25);
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);
/// <summary>Vykreslí (obrysově) obrys se zachovanými kruhovými oblouky LwPolyline/Polyline2D
/// s bulge, samostatný ARC apod. Hladké v každém přiblížení i v PDF.</summary>
void Stroke(Contour c);
/// <param name="widthFactor">Horizontální měřítko písma (DXF width factor); 1 = beze změny.</param>
/// <param name="maxWidthMm">Šířka pro zalomení víceřádkového textu v mm; 0 = bez zalomení (jen explicitní zlomy).</param>
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);
/// <summary>Vyplní oblast danou jednou či více uzavřenými obrysy (even-odd → vnořené smyčky
/// tvoří díry). Obrysy nesou analytické oblouky (viz <see cref="Contour"/>), takže výplně z
/// DXF HATCH např. loga mají hladké okraje v každém přiblížení i v PDF.</summary>
void Fill(IReadOnlyList<Contour> loops);
}
+285
View File
@@ -0,0 +1,285 @@
using System.IO;
using SkiaSharp;
namespace Rozpisky.Rendering;
/// <summary>
/// Kreslí na <see cref="SKCanvas"/> (ROZPISKY.md §3). Dostane funkci <c>map</c>
/// (mm → bod plátna, řeší i převrácení osy Y) a <c>pxPerMm</c>. Text se kreslí vzpřímeně.
/// Barva se přebírá z DXF (hladina/entita); bílá (barva 7) se kreslí černě, aby na bílém
/// papíře nezmizela. Statická geometrie rozpisky je bílá → černá; barevné jsou typicky loga.
/// </summary>
public sealed class SkiaProfileRenderer : IProfileRenderer, IDisposable
{
private readonly SKCanvas _canvas;
private readonly Func<double, double, SKPoint> _map;
private readonly double _pxPerMm;
private readonly float _minStrokePx;
/// <summary>Afinní matice mm → bod plátna, odvozená vzorkováním <see cref="_map"/> ve třech bodech
/// (mapování je scale + převrácení Y + posun). Slouží ke kreslení analytických oblouků: <see cref="SKPath"/>
/// se sestaví ve výkresových mm (osa Y nahoru, standardní úhly proti směru hodin) a pak se transformuje
/// touto maticí <c>ArcTo</c> tak zůstane hladké v každém přiblížení i ve vektorovém PDF.</summary>
private readonly SKMatrix _mm2px;
private readonly SKPaint _stroke;
private readonly SKPaint _text;
private readonly Dictionary<(string Family, int Weight, bool Italic), SKTypeface> _typefaces = new();
private readonly string _fontsDir = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
private double _lineWeightMm = 0.25;
/// <summary>Font použitý, když DXF styl rodinu neuvádí (např. SHX styly).</summary>
private const string FallbackFamily = "Arial";
public SkiaProfileRenderer(SKCanvas canvas, Func<double, double, SKPoint> map, double pxPerMm, double minStrokePx = 0f)
{
_canvas = canvas;
_map = map;
_pxPerMm = pxPerMm;
_minStrokePx = (float)minStrokePx;
// Afinní koeficienty z map(0,0), map(1,0), map(0,1) map je scale + Y-flip + posun.
var o = map(0, 0); var px = map(1, 0); var py = map(0, 1);
_mm2px = new SKMatrix(
px.X - o.X, py.X - o.X, o.X, // ScaleX, SkewX, TransX
px.Y - o.Y, py.Y - o.Y, o.Y, // SkewY, ScaleY, TransY
0, 0, 1);
_stroke = new SKPaint
{
IsAntialias = true,
Color = SKColors.Black,
Style = SKPaintStyle.Stroke,
StrokeCap = SKStrokeCap.Round,
StrokeJoin = SKStrokeJoin.Round,
};
_text = new SKPaint
{
IsAntialias = true,
Color = SKColors.Black,
Style = SKPaintStyle.Fill,
};
}
/// <summary>Vrátí (a cachuje) typeface pro daný DXF styl: nejdřív přímo ze souboru fontu
/// (přesný řez, např. verdanab.ttf ve Fonts), pak podle rodiny+řezu, jinak fallback.</summary>
private SKTypeface Typeface(TextStyle style)
{
// 1) Přesný font ze souboru, na který odkazuje DXF styl (.ttf v systémové složce Fonts).
if (!string.IsNullOrWhiteSpace(style.FontFile))
{
var fileKey = ("\0file:" + style.FontFile, 0, false);
if (_typefaces.TryGetValue(fileKey, out var tff)) return tff;
var path = Path.Combine(_fontsDir, style.FontFile!);
var loaded = File.Exists(path) ? SKTypeface.FromFile(path) : null;
if (loaded is not null) { _typefaces[fileKey] = loaded; return loaded; }
}
// 2) Podle názvu rodiny + řezu. Z názvu odštěpíme slovo váhy (Black/Bold/Light…), protože
// např. „Arial Black" není v systému samostatná rodina, ale rodina „Arial" s vahou 900.
string family = string.IsNullOrWhiteSpace(style.FontFamily) ? FallbackFamily : style.FontFamily!.Trim();
int weight = style.Bold ? (int)SKFontStyleWeight.Bold : (int)SKFontStyleWeight.Normal;
(family, weight) = OddelVahu(family, weight);
var key = (family, weight, style.Italic);
if (!_typefaces.TryGetValue(key, out var tf))
{
tf = SKTypeface.FromFamilyName(
family,
(SKFontStyleWeight)weight,
SKFontStyleWidth.Normal,
style.Italic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright)
?? SKTypeface.Default;
_typefaces[key] = tf;
}
return tf;
}
/// <summary>Známá slova váhy řezu na konci názvu fontu → číselná váha (DirectWrite/Skia).</summary>
private static readonly Dictionary<string, int> Vahy = new(StringComparer.OrdinalIgnoreCase)
{
["thin"] = 100, ["hairline"] = 100, ["extralight"] = 200, ["ultralight"] = 200, ["light"] = 300,
["regular"] = 400, ["normal"] = 400, ["book"] = 400, ["medium"] = 500,
["semibold"] = 600, ["demibold"] = 600, ["bold"] = 700, ["extrabold"] = 800, ["ultrabold"] = 800,
["black"] = 900, ["heavy"] = 900,
};
/// <summary>Odštěpí z názvu rodiny závěrečné slovo váhy (a „Arial Black" → rodina „Arial", váha 900).
/// Pokud žádné nenajde, vrátí název i vstupní váhu beze změny.</summary>
private static (string Family, int Weight) OddelVahu(string family, int defaultWeight)
{
int sp = family.LastIndexOf(' ');
if (sp > 0 && Vahy.TryGetValue(family[(sp + 1)..], out int w))
return (family[..sp].Trim(), w);
return (family, defaultWeight);
}
private float StrokePx => Math.Max((float)(_lineWeightMm * _pxPerMm), _minStrokePx);
public void Layer(string name, int rgb, double lineWeightMm = 0.25)
{
_lineWeightMm = lineWeightMm;
byte r = (byte)((rgb >> 16) & 0xFF), g = (byte)((rgb >> 8) & 0xFF), b = (byte)(rgb & 0xFF);
if (r == 255 && g == 255 && b == 255) r = g = b = 0; // bílá na bílém papíře → černá
var c = new SKColor(r, g, b);
_stroke.Color = c;
_text.Color = c;
}
public void Line(double x1, double y1, double x2, double y2)
{
_stroke.StrokeWidth = StrokePx;
_canvas.DrawLine(_map(x1, y1), _map(x2, y2), _stroke);
}
public void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false)
{
if (pts.Count < 2) return;
using var path = new SKPath();
var p0 = _map(pts[0].X, pts[0].Y);
path.MoveTo(p0);
for (int i = 1; i < pts.Count; i++)
{
var p = _map(pts[i].X, pts[i].Y);
path.LineTo(p);
}
if (closed) path.Close();
_stroke.StrokeWidth = StrokePx;
_canvas.DrawPath(path, _stroke);
}
public void Circle(double cx, double cy, double r)
{
var c = _map(cx, cy);
_stroke.StrokeWidth = StrokePx;
_canvas.DrawCircle(c, (float)(r * _pxPerMm), _stroke);
}
public void Stroke(Contour c)
{
if (c.Edges.Count == 0) return;
using var path = new SKPath();
AppendContour(path, c);
path.Transform(_mm2px);
_stroke.StrokeWidth = StrokePx;
_canvas.DrawPath(path, _stroke);
}
/// <summary>Přidá obrys do <see cref="SKPath"/> ve výkresových mm (bez transformace): úsečky
/// <c>LineTo</c>, kruhové oblouky <c>ArcTo</c> (oval + počáteční úhel + znaménkový rozsah).
/// V mm-prostoru je osa Y nahoru, takže kladný rozsah = proti směru hodin shodně s DXF.</summary>
private static void AppendContour(SKPath path, Contour c)
{
var e0 = c.Edges[0];
path.MoveTo((float)e0.SX, (float)e0.SY);
foreach (var e in c.Edges)
{
if (e.Kind == EdgeKind.Line || e.R <= 0)
{
path.LineTo((float)e.EX, (float)e.EY);
}
else
{
var oval = new SKRect((float)(e.Cx - e.R), (float)(e.Cy - e.R), (float)(e.Cx + e.R), (float)(e.Cy + e.R));
double a0deg = e.A0 * 180 / Math.PI, sweepDeg = e.Sweep * 180 / Math.PI;
// ArcTo se sweepem ±360° je degenerovaný (start == konec → nenakreslí nic plný kruh
// z jediné obloukové hrany „propadne"). Rozdělíme na části ≤180°, aby to platilo vždy.
int parts = Math.Max(1, (int)Math.Ceiling(Math.Abs(sweepDeg) / 180.0 - 1e-9));
double step = sweepDeg / parts;
for (int k = 0; k < parts; k++) path.ArcTo(oval, (float)(a0deg + step * k), (float)step, false);
}
}
if (c.Closed) path.Close();
}
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)
{
if (string.IsNullOrEmpty(text)) return;
var paint = _text;
paint.Typeface = Typeface(style);
paint.TextScaleX = widthFactor > 0 ? (float)widthFactor : 1f; // DXF width factor (horizontální měřítko)
// DXF oblique angle = sklon znaků (kurzíva náklonem). Skew na paintu (ne na plátně) nemění
// šířky glyfů (MeasureText), takže zarovnání i zalomení zůstávají korektní. Glyph space má
// Y dolů → záporné znaménko = naklonění doprava. Reset na 0 nutný paint je sdílený.
paint.TextSkewX = style.ObliqueDeg != 0 ? -(float)Math.Tan(style.ObliqueDeg * Math.PI / 180) : 0f;
// DXF výška textu = výška velkých písmen (cap height); TextSize (em) je o něco větší.
// Změříme reálný poměr cap/em zvoleného fontu, ať výška sedí i pro jiné fonty než Arial.
float capPx = (float)(heightMm * _pxPerMm);
paint.TextSize = capPx;
float capAtEm = Math.Abs(paint.FontMetrics.CapHeight);
paint.TextSize = capAtEm > 0 ? capPx * capPx / capAtEm : capPx / 0.72f;
// Víceřádkový text: explicitní zlomy (\n, \P) + volitelné zalomení na šířku.
var lines = Wrap(text, paint, (float)(maxWidthMm * _pxPerMm));
float stepPx = paint.FontSpacing; // doporučené řádkování zvoleného fontu
int n = lines.Count;
// Svislé zarovnání celého bloku → baseline prvního řádku v lokálních souřadnicích.
float base0 = v switch
{
VAlign.Top => capPx,
VAlign.Middle => capPx / 2f - (n - 1) * stepPx / 2f,
_ => -(n - 1) * stepPx, // Baseline i Bottom: kotva na posledním řádku
};
var p = _map(x, y);
_canvas.Save();
_canvas.Translate(p.X, p.Y);
if (rotationDeg != 0) _canvas.RotateDegrees(-(float)rotationDeg); // plátno má Y dolů
for (int i = 0; i < n; i++)
{
float w = paint.MeasureText(lines[i]);
float dx = h switch { HAlign.Center => -w / 2f, HAlign.Right => -w, _ => 0f };
_canvas.DrawText(lines[i], dx, base0 + i * stepPx, paint);
}
_canvas.Restore();
}
/// <summary>Rozloží text na řádky: explicitní zlomy (\n, \P) + volitelné greedy zalomení na šířku v px.</summary>
private static List<string> Wrap(string text, SKPaint paint, float maxWidthPx)
{
var segments = text.Replace("\\P", "\n").Replace("\r\n", "\n").Replace('\r', '\n').Split('\n');
var lines = new List<string>();
foreach (var seg in segments)
{
if (maxWidthPx <= 0 || paint.MeasureText(seg) <= maxWidthPx) { lines.Add(seg); continue; }
string cur = "";
foreach (var word in seg.Split(' '))
{
string trial = cur.Length == 0 ? word : cur + " " + word;
if (cur.Length == 0 || paint.MeasureText(trial) <= maxWidthPx) cur = trial;
else { lines.Add(cur); cur = word; }
}
lines.Add(cur);
}
return lines;
}
public void Fill(IReadOnlyList<Contour> loops)
{
if (loops.Count == 0) return;
using var path = new SKPath { FillType = SKPathFillType.EvenOdd };
bool any = false;
foreach (var loop in loops)
{
if (loop.Edges.Count == 0) continue;
AppendContour(path, loop); // každá smyčka = nový subpath → even-odd díry
any = true;
}
if (!any) return;
path.Transform(_mm2px);
_canvas.DrawPath(path, _text); // _text = černá výplň (Style=Fill)
}
public void Dispose()
{
_stroke.Dispose();
_text.Dispose();
foreach (var tf in _typefaces.Values)
if (tf != SKTypeface.Default) tf.Dispose();
}
}
+53
View File
@@ -0,0 +1,53 @@
namespace Rozpisky.Rendering;
/// <summary>
/// Dekorátor <see cref="IProfileRenderer"/>, který na všechny souřadnice aplikuje
/// uniformní měřítko + posun: <c>X' = Ox + S·X</c>, <c>Y' = Oy + S·Y</c>. 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í <c>·S</c>; hladina/barva propadá beze změny.
/// </summary>
public sealed class TransformRenderer : IProfileRenderer
{
private readonly IProfileRenderer _inner;
private readonly double _s, _ox, _oy;
public TransformRenderer(IProfileRenderer inner, double scale, double offsetX, double offsetY)
{
_inner = inner;
_s = scale;
_ox = offsetX;
_oy = offsetY;
}
private double X(double x) => _ox + _s * x;
private double Y(double y) => _oy + _s * y;
public void Layer(string name, int rgb, double lineWeightMm = 0.25)
=> _inner.Layer(name, rgb, lineWeightMm * _s);
public void Line(double x1, double y1, double x2, double y2)
=> _inner.Line(X(x1), Y(y1), X(x2), Y(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);
public void Circle(double cx, double cy, double r)
=> _inner.Circle(X(cx), Y(cy), r * _s);
public void Stroke(Contour c) => _inner.Stroke(new Contour(c.Edges.Select(T).ToList(), c.Closed));
/// <summary>Přemapuje hranu: body scale+offset, poloměr ·S, úhly beze změny (transformace je
/// uniformní scale + posun, bez rotace/zrcadlení).</summary>
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 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);
public void Fill(IReadOnlyList<Contour> loops)
=> _inner.Fill(loops.Select(c => new Contour(c.Edges.Select(T).ToList(), c.Closed)).ToList());
}
+42
View File
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<!-- Ikona spustitelného souboru (taskbar, Průzkumník). -->
<ApplicationIcon>Rozpisky.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.105.0" />
<PackageReference Include="SkiaSharp" Version="2.88.8" />
<PackageReference Include="SkiaSharp.Views.WPF" Version="2.88.8" />
<PackageReference Include="ACadSharp" Version="3.6.29" />
</ItemGroup>
<ItemGroup>
<!-- Ikona aplikace jako prostředek (pro Window.Icon v titulkovém pruhu). -->
<Resource Include="Rozpisky.ico" />
</ItemGroup>
<ItemGroup>
<!-- DXF šablona rozpisky (leží v kořeni solution) kopíruje se vedle .exe. -->
<None Include="..\Rozpiska.dxf" Link="Rozpiska.dxf" CopyToOutputDirectory="PreserveNewest" />
<!-- Mustr „Seznam příloh“ (Otevřená verze .xlsx) šablona vyplňovaná při exportu seznamu. -->
<None Include="..\SEZNAM.xlsx" Link="SEZNAM.xlsx" CopyToOutputDirectory="PreserveNewest" />
<!-- Závazný manuál SŽ (PDF) kopíruje se vedle .exe pro otevření z nabídky Nápověda. -->
<None Include="..\SZ_SM011_P10_Manual_v6.pdf" Link="SZ_SM011_P10_Manual_v6.pdf" CopyToOutputDirectory="PreserveNewest" />
<!-- Výchozí číselníky (data z původního Excelu) dodávají se vedle .exe.
Update (ne Include): .json je už implicitní None item. PreserveNewest = uživatelovy
úpravy v output složce se při rebuildu nepřepíšou (přepíše jen novější zdroj). -->
<None Update="ciselniky.json" CopyToOutputDirectory="PreserveNewest" />
<!-- Mapování „pole programu → DXF atribut“ (RozpiskaValues přes reflexi). Editovatelné vedle .exe. -->
<None Update="mapovani.json" CopyToOutputDirectory="PreserveNewest" />
<!-- Loga dodávaná s programem (DXF) kopírují se do složky loga\ vedle .exe. -->
<None Include="loga\**\*" CopyToOutputDirectory="PreserveNewest" LinkBase="loga" />
</ItemGroup>
</Project>
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+46
View File
@@ -0,0 +1,46 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- ============ DARK ============ -->
<!-- Pozadí -->
<SolidColorBrush x:Key="Brush.Window" Color="#2B2B2B"/>
<SolidColorBrush x:Key="Brush.Panel" Color="#333333"/>
<SolidColorBrush x:Key="Brush.Elevated" Color="#3C3C3C"/>
<SolidColorBrush x:Key="Brush.Input" Color="#1E1E1E"/>
<SolidColorBrush x:Key="Brush.Hover" Color="#454545"/>
<SolidColorBrush x:Key="Brush.Active" Color="#505050"/>
<SolidColorBrush x:Key="Brush.Selected" Color="#0E639C"/>
<SolidColorBrush x:Key="Brush.SelectedSoft" Color="#264B5E"/>
<!-- Tlumené podbarvení řádku, na kterém stojí aktivní buňka (jen indikace pozice; vybraná
buňka zůstává výraznější přes Brush.SelectedSoft). -->
<SolidColorBrush x:Key="Brush.CurrentRow" Color="#20333D"/>
<SolidColorBrush x:Key="Brush.TitleBar" Color="#252525"/>
<!-- Overlay (hover/press jednotně přes všechny varianty) -->
<SolidColorBrush x:Key="Brush.HoverOverlay" Color="#16FFFFFF"/>
<SolidColorBrush x:Key="Brush.PressOverlay" Color="#28FFFFFF"/>
<!-- Okraje -->
<SolidColorBrush x:Key="Brush.Border" Color="#4A4A4A"/>
<SolidColorBrush x:Key="Brush.BorderStrong" Color="#5A5A5A"/>
<SolidColorBrush x:Key="Brush.BorderSubtle" Color="#3A3A3A"/>
<!-- Text -->
<SolidColorBrush x:Key="Brush.Text" Color="#E6E6E6"/>
<SolidColorBrush x:Key="Brush.TextSecondary" Color="#9A9A9A"/>
<SolidColorBrush x:Key="Brush.TextDisabled" Color="#6A6A6A"/>
<SolidColorBrush x:Key="Brush.TextOnAccent" Color="#FFFFFF"/>
<!-- Akcent + stavové -->
<SolidColorBrush x:Key="Brush.Accent" Color="#1B9CE6"/>
<SolidColorBrush x:Key="Brush.AccentHover" Color="#3AAEF0"/>
<SolidColorBrush x:Key="Brush.AccentPress" Color="#1481C2"/>
<SolidColorBrush x:Key="Brush.Success" Color="#4CAF50"/>
<SolidColorBrush x:Key="Brush.Warning" Color="#E0A521"/>
<SolidColorBrush x:Key="Brush.Danger" Color="#E05B4B"/>
<SolidColorBrush x:Key="Brush.DangerHover" Color="#EC7264"/>
<!-- Tlumené červené pozadí (např. zvýraznění chybného/duplicitního řádku v seznamu) -->
<SolidColorBrush x:Key="Brush.DangerSubtle" Color="#5A2E2A"/>
</ResourceDictionary>
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=System.Runtime">
<!-- Typografie -->
<FontFamily x:Key="Font.Family">Segoe UI</FontFamily>
<FontFamily x:Key="Font.Icon">Segoe MDL2 Assets</FontFamily>
<sys:Double x:Key="Metric.IconSize">14</sys:Double>
<sys:Double x:Key="Metric.FontSize">12</sys:Double>
<sys:Double x:Key="Metric.FontSizeSm">11</sys:Double>
<sys:Double x:Key="Metric.FontSizeH">13</sys:Double>
<sys:Double x:Key="Metric.FontSizeH1">18</sys:Double>
<sys:Double x:Key="Metric.FontSizeH2">15</sys:Double>
<!-- Metriky -->
<sys:Double x:Key="Metric.ControlHeight">26</sys:Double>
<sys:Double x:Key="Metric.RowHeight">24</sys:Double>
<CornerRadius x:Key="Metric.Radius">3</CornerRadius>
<CornerRadius x:Key="Metric.RadiusSm">2</CornerRadius>
<Thickness x:Key="Metric.PaddingControl">8,0</Thickness>
<Thickness x:Key="Metric.PaddingButton">14,0</Thickness>
</ResourceDictionary>
+46
View File
@@ -0,0 +1,46 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
namespace Rozpisky.Theming;
/// <summary>
/// Připojená vlastnost na editovatelný <see cref="ComboBox"/> v <c>CellEditingTemplate</c> DataGridu:
/// při vstupu do editace (F2/dvojklik) přesune fokus rovnou do editačního pole ComboBoxu. WPF totiž
/// při zahájení editace šablonovaného sloupce nechá klávesový fokus na <see cref="DataGridCell"/>,
/// ne na prvku uvnitř šablony bez tohoto by uživatel musel kliknout ještě jednou, aby mohl psát
/// nebo listovat nabídkou. Nasazuje se hromadně přes styl <c>ComboBox.Cell</c> (Controls.xaml).
/// </summary>
public static class CellEditFocus
{
public static readonly DependencyProperty FocusOnLoadProperty =
DependencyProperty.RegisterAttached("FocusOnLoad", typeof(bool), typeof(CellEditFocus),
new PropertyMetadata(false, OnFocusOnLoadChanged));
public static void SetFocusOnLoad(DependencyObject o, bool value) => o.SetValue(FocusOnLoadProperty, value);
public static bool GetFocusOnLoad(DependencyObject o) => (bool)o.GetValue(FocusOnLoadProperty);
private static void OnFocusOnLoadChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is not ComboBox combo || !(bool)e.NewValue) return;
// Editační ComboBox se v šablonovaném sloupci vytváří znovu při každém vstupu do editace,
// takže Loaded spolehlivě padne pokaždé při zahájení editace buňky.
combo.Loaded += (_, _) =>
// Fokus až po doběhnutí editační logiky DataGridu (ta jinak fokus vrátí na buňku),
// proto přes Dispatcher na Input prioritě.
combo.Dispatcher.BeginInvoke(DispatcherPriority.Input, new Action(() =>
{
// U editovatelného ComboBoxu míří fokus do PART_EditableTextBox; vyber i text,
// aby šel rovnou přepsat. Fallback na Focus() ComboBoxu, není-li šablona dosažitelná.
if (combo.Template?.FindName("PART_EditableTextBox", combo) is TextBox tb)
{
tb.Focus();
tb.SelectAll();
}
else
{
combo.Focus();
}
}));
}
}
+393
View File
@@ -0,0 +1,393 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
namespace Rozpisky.Theming;
/// <summary>
/// Excelovský „fill handle" pro <see cref="DataGrid"/>: čtvereček v pravém dolním rohu výběru
/// buněk, tažením se hodnoty výběru rozkopírují do cílové oblasti (vzor se opakuje, žádné
/// dopočítávání řad). Zapíná se per-grid: <c>theming:GridFill.EnableFillHandle="True"</c>.
/// Záměrně NENÍ v globálním stylu DataGridu — mění chování, ne jen vzhled, takže je opt-in.
///
/// Předpoklady a omezení (viz README, sekce Fill handle):
/// - výběr buněk vyžaduje SelectionUnit="Cell" nebo "CellOrRowHeader",
/// - hodnoty se čtou/zapisují přes ClipboardContentBinding sloupce
/// (OnCopyingCellClipboardContent / OnPastingCellClipboardContent) — tedy s konverzí bindingu,
/// bez vstupu do edit módu a bez CellEditEnding událostí,
/// - read-only sloupce a grid se přeskakují; chyba konverze buňku tiše vynechá,
/// - undo není (DataGrid nemá transakce; případně doplnit zapamatování původních hodnot).
///
/// Rozšíření oproti theme kitu: dvojklik na úchyt vyplní vzor výběru až do posledního řádku.
/// </summary>
public static class GridFill
{
public static readonly DependencyProperty EnableFillHandleProperty =
DependencyProperty.RegisterAttached("EnableFillHandle", typeof(bool), typeof(GridFill),
new PropertyMetadata(false, OnEnableChanged));
public static void SetEnableFillHandle(DependencyObject o, bool value) => o.SetValue(EnableFillHandleProperty, value);
public static bool GetEnableFillHandle(DependencyObject o) => (bool)o.GetValue(EnableFillHandleProperty);
// interní: drží instanci adorneru, ať se při opakovaném Loaded (přepnutí tabu) neduplikuje
private static readonly DependencyProperty AdornerProperty =
DependencyProperty.RegisterAttached("Adorner", typeof(FillHandleAdorner), typeof(GridFill));
private static void OnEnableChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is not DataGrid grid) return;
if ((bool)e.NewValue)
{
grid.Loaded += OnGridLoaded;
if (grid.IsLoaded) Attach(grid);
}
else
{
grid.Loaded -= OnGridLoaded;
Detach(grid);
}
}
private static void OnGridLoaded(object sender, RoutedEventArgs e) => Attach((DataGrid)sender);
private static void Attach(DataGrid grid)
{
// Adorner layer po znovu-načtení (tab) může být jiný → starý adorner vždy sundat a založit nový.
Detach(grid);
var layer = AdornerLayer.GetAdornerLayer(grid);
if (layer == null) return; // chybí AdornerDecorator v okně — bez něj fill handle nelze vykreslit
var adorner = new FillHandleAdorner(grid);
grid.SetValue(AdornerProperty, adorner);
layer.Add(adorner);
}
private static void Detach(DataGrid grid)
{
if (grid.GetValue(AdornerProperty) is not FillHandleAdorner adorner) return;
grid.ClearValue(AdornerProperty);
adorner.Unhook();
if (adorner.Parent is AdornerLayer layer) layer.Remove(adorner);
}
}
/// <summary>
/// Vykresluje úchyt a čárkovaný náhled cílové oblasti; obsluhuje tažení myší.
/// Hit-testovatelný je jen vykreslený úchyt, zbytek gridu zůstává adornerem nedotčený.
/// </summary>
internal sealed class FillHandleAdorner : Adorner
{
private readonly DataGrid _grid;
private Rect _handleRect = Rect.Empty; // kde byl úchyt naposledy vykreslen (pro hit-test kliknutí)
private bool _dragging;
private bool _vertical; // směr tažení: true = po řádcích, false = po sloupcích
private int _target = -1; // index cílového řádku (vertical) / display-sloupce (horizontal)
public FillHandleAdorner(DataGrid grid) : base(grid)
{
_grid = grid;
Cursor = Cursors.Cross; // platí jen nad vykresleným úchytem — jinde adorner není hit-testovatelný
_grid.SelectedCellsChanged += OnGridChanged;
_grid.SizeChanged += OnGridSizeChanged;
_grid.PreviewKeyDown += OnGridKeyDown;
_grid.AddHandler(ScrollViewer.ScrollChangedEvent, (RoutedEventHandler)OnGridChanged);
}
public void Unhook()
{
_grid.SelectedCellsChanged -= OnGridChanged;
_grid.SizeChanged -= OnGridSizeChanged;
_grid.PreviewKeyDown -= OnGridKeyDown;
_grid.RemoveHandler(ScrollViewer.ScrollChangedEvent, (RoutedEventHandler)OnGridChanged);
}
private void OnGridChanged(object? sender, EventArgs e) => InvalidateVisual();
private void OnGridSizeChanged(object sender, SizeChangedEventArgs e) => InvalidateVisual();
private void OnGridKeyDown(object sender, KeyEventArgs e)
{
if (_dragging && e.Key == Key.Escape) { CancelDrag(); e.Handled = true; }
}
// ---------------------------------------------------------------- kreslení
protected override void OnRender(DrawingContext dc)
{
_handleRect = Rect.Empty;
var bounds = EffectiveBounds();
if (bounds is not var (r0, r1, c0, c1) || r1 < 0) return;
var accent = _grid.TryFindResource("Brush.Accent") as Brush ?? Brushes.DodgerBlue;
if (_dragging && _target >= 0)
{
var (t0, t1, u0, u1) = _vertical
? (Math.Min(r0, _target), Math.Max(r1, _target), c0, c1)
: (r0, r1, Math.Min(c0, _target), Math.Max(c1, _target));
if (RangeRect(t0, t1, u0, u1) is Rect preview)
{
var pen = new Pen(accent, 1.5) { DashStyle = new DashStyle(new double[] { 3, 2 }, 0) };
dc.DrawRectangle(null, pen, preview);
}
}
// Úchyt kotvíme na pravém dolním rohu výběru (Excel), ale je-li ten roh odscrollovaný pryč
// (široký grid, výběr celého řádku), spadneme na aktivní buňku — ta je vždy realizovaná,
// takže úchyt zůstane vidět stejně jako v předchozí verzi.
if ((CellRect(r1, c1) ?? CurrentCellRect()) is Rect corner)
{
_handleRect = new Rect(corner.Right - 3.5, corner.Bottom - 3.5, 7, 7);
// Průhledné halo: adorner je hit-testovatelný jen tam, kde něco vykreslil, a 7px
// čtvereček by byl moc malý klikací cíl. Brushes.Transparent hit-test registruje.
var halo = _handleRect; halo.Inflate(3, 3);
dc.DrawRectangle(Brushes.Transparent, null, halo);
var outline = _grid.TryFindResource("Brush.Window") as Brush ?? Brushes.Black;
dc.DrawRectangle(accent, new Pen(outline, 1), _handleRect);
}
}
// ---------------------------------------------------------------- myš
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (_handleRect.IsEmpty) return;
var hit = _handleRect; hit.Inflate(3, 3); // trochu benevolentní zásah, 7px čtvereček je malý cíl
if (!hit.Contains(e.GetPosition(_grid))) return;
_grid.CommitEdit(DataGridEditingUnit.Row, true);
// Dvojklik: vyplnit vzor výběru až do posledního řádku (jako v Excelu). První klik dvojkliku
// sice zahájí tažení, ale bez pohybu zůstane _target = -1, takže se při uvolnění nic nezapíše.
if (e.ClickCount == 2)
{
Commit(vertical: true, target: _grid.Items.Count - 1);
e.Handled = true;
return;
}
_dragging = true;
_target = -1;
CaptureMouse();
e.Handled = true;
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (!_dragging) return;
var pos = e.GetPosition(_grid);
var bounds = EffectiveBounds();
if (bounds is not var (r0, r1, c0, c1) || r1 < 0) { CancelDrag(); return; }
// Autoscroll u okrajů — virtualizované řádky se tím postupně realizují a tažení může pokračovat.
if (FindScrollViewer() is { } sv)
{
if (pos.Y > _grid.ActualHeight - 12) sv.LineDown();
else if (pos.Y < 30) sv.LineUp(); // ~výška hlavičky
if (pos.X > _grid.ActualWidth - 12) sv.LineRight();
else if (pos.X < 12) sv.LineLeft();
}
var row = RowAt(pos);
if (row == null) { InvalidateVisual(); return; } // mimo řádky: nech poslední cíl
int r = row.GetIndex();
int? c = ColumnAt(row, pos.X);
// Zámek směru podle toho, kterým směrem uživatel vyjel dál za hranici výběru (jako Excel).
int dRow = r > r1 ? r - r1 : r < r0 ? r0 - r : 0;
int dCol = 0;
if (c is int ci) dCol = ci > c1 ? ci - c1 : ci < c0 ? c0 - ci : 0;
if (dRow >= dCol) { _vertical = true; _target = r; }
else { _vertical = false; _target = c!.Value; }
InvalidateVisual();
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
if (!_dragging) return;
bool vertical = _vertical;
int target = _target;
_dragging = false;
ReleaseMouseCapture();
InvalidateVisual();
if (target >= 0) Commit(vertical, target);
e.Handled = true;
}
protected override void OnLostMouseCapture(MouseEventArgs e)
{
if (_dragging) CancelDrag();
}
private void CancelDrag()
{
_dragging = false;
_target = -1;
if (IsMouseCaptured) ReleaseMouseCapture();
InvalidateVisual();
}
// ---------------------------------------------------------------- zápis hodnot
private void Commit(bool vertical, int target)
{
var bounds = EffectiveBounds();
if (bounds is not var (r0, r1, c0, c1) || r1 < 0 || _grid.IsReadOnly) return;
var cols = ColumnsByDisplay();
if (vertical)
{
int srcRows = r1 - r0 + 1;
for (int r = Math.Min(r0, target); r <= Math.Max(r1, target); r++)
{
if (r >= r0 && r <= r1) continue; // zdrojová oblast
var item = _grid.Items[r];
if (item == CollectionView.NewItemPlaceholder) continue;
var src = _grid.Items[r0 + Mod(r - r0, srcRows)];
for (int c = c0; c <= c1; c++)
CopyCell(srcColumn: cols[c], srcItem: src, dstColumn: cols[c], dstItem: item);
}
}
else
{
int srcCols = c1 - c0 + 1;
for (int c = Math.Min(c0, target); c <= Math.Max(c1, target); c++)
{
if (c >= c0 && c <= c1) continue;
var srcCol = cols[c0 + Mod(c - c0, srcCols)];
for (int r = r0; r <= r1; r++)
{
var item = _grid.Items[r];
if (item == CollectionView.NewItemPlaceholder) continue;
CopyCell(srcColumn: srcCol, srcItem: item, dstColumn: cols[c], dstItem: item);
}
}
}
}
private static void CopyCell(DataGridColumn srcColumn, object srcItem, DataGridColumn dstColumn, object dstItem)
{
if (dstColumn.IsReadOnly) return;
try
{
// Clipboard-binding cesta: čte i zapisuje přes binding sloupce včetně konverze typů,
// funguje bez vstupu buňky do edit módu. Sloupec bez ClipboardContentBinding
// (např. holý template sloupec) v tichosti nic neudělá.
dstColumn.OnPastingCellClipboardContent(dstItem, srcColumn.OnCopyingCellClipboardContent(srcItem));
}
catch
{
// nekonvertibilní hodnota (např. text do čísla při horizontálním tažení) → buňku vynech
}
}
private static int Mod(int a, int m) => ((a % m) + m) % m;
// ---------------------------------------------------------------- geometrie a hledání
/// <summary>Obálka výběru; když nic není vybrané, spadne na aktivní (current) buňku, aby úchyt
/// a tažení fungovaly i pro jednu buňku bez explicitního výběru (jako předchozí verze).</summary>
private (int r0, int r1, int c0, int c1)? EffectiveBounds()
{
if (SelectionBounds() is { } b) return b;
return CurrentCellIndex() is (int r, int c) ? (r, r, c, c) : null;
}
/// <summary>Index (řádek, DisplayIndex sloupce) aktivní buňky, nebo null.</summary>
private (int r, int c)? CurrentCellIndex()
{
var info = _grid.CurrentCell;
if (!info.IsValid || info.Column == null || info.Item == null) return null;
int r = _grid.Items.IndexOf(info.Item);
return r < 0 ? null : (r, info.Column.DisplayIndex);
}
/// <summary>Obdélník aktivní buňky v souřadnicích gridu; null pro nerealizovanou.</summary>
private Rect? CurrentCellRect()
=> CurrentCellIndex() is (int r, int c) ? CellRect(r, c) : null;
/// <summary>Obálka výběru: min/max index řádku a min/max DisplayIndex sloupce, nebo null.</summary>
private (int r0, int r1, int c0, int c1)? SelectionBounds()
{
var cells = _grid.SelectedCells;
if (cells.Count == 0) return null;
int r0 = int.MaxValue, r1 = -1, c0 = int.MaxValue, c1 = -1;
var rowIndex = new Dictionary<object, int>(); // Items.IndexOf je O(n), memoizuj per položka
foreach (var cell in cells)
{
if (!cell.IsValid || cell.Column == null || cell.Item == null) continue;
if (!rowIndex.TryGetValue(cell.Item, out int r))
rowIndex[cell.Item] = r = _grid.Items.IndexOf(cell.Item);
if (r < 0) continue;
if (r < r0) r0 = r;
if (r > r1) r1 = r;
int c = cell.Column.DisplayIndex;
if (c < c0) c0 = c;
if (c > c1) c1 = c;
}
return r1 < 0 ? null : (r0, r1, c0, c1);
}
private List<DataGridColumn> ColumnsByDisplay() => _grid.Columns.OrderBy(c => c.DisplayIndex).ToList();
/// <summary>Obdélník buňky v souřadnicích gridu; null pro nerealizovanou (odscrollovanou) buňku.</summary>
private Rect? CellRect(int rowIndex, int displayCol)
{
if (rowIndex < 0 || rowIndex >= _grid.Items.Count) return null;
if (_grid.ItemContainerGenerator.ContainerFromIndex(rowIndex) is not DataGridRow row) return null;
var cols = ColumnsByDisplay();
if (displayCol < 0 || displayCol >= cols.Count) return null;
if (cols[displayCol].GetCellContent(row)?.Parent is not DataGridCell cell) return null;
return new Rect(cell.TranslatePoint(new Point(0, 0), _grid), new Size(cell.ActualWidth, cell.ActualHeight));
}
/// <summary>Obálkový obdélník oblasti; skládá se z realizovaných rohů, jinak null.</summary>
private Rect? RangeRect(int r0, int r1, int c0, int c1)
{
Rect? acc = null;
foreach (var r in new[] { CellRect(r0, c0), CellRect(r0, c1), CellRect(r1, c0), CellRect(r1, c1) })
if (r is Rect rect) acc = acc is Rect a ? Rect.Union(a, rect) : rect;
return acc;
}
private DataGridRow? RowAt(Point p)
{
var d = _grid.InputHitTest(p) as DependencyObject;
while (d != null && d is not DataGridRow)
d = d is Visual or System.Windows.Media.Media3D.Visual3D
? VisualTreeHelper.GetParent(d) : LogicalTreeHelper.GetParent(d);
// pojistka proti vnořenému DataGridu: řádek musí patřit tomuhle gridu
return d is DataGridRow row && ItemsControl.ItemsControlFromItemContainer(row) == _grid ? row : null;
}
/// <summary>DisplayIndex sloupce pod souřadnicí X — přes obdélníky buněk daného řádku.</summary>
private int? ColumnAt(DataGridRow row, double x)
{
var cols = ColumnsByDisplay();
for (int c = 0; c < cols.Count; c++)
{
if (cols[c].GetCellContent(row)?.Parent is not DataGridCell cell) continue;
double left = cell.TranslatePoint(new Point(0, 0), _grid).X;
if (x >= left && x <= left + cell.ActualWidth) return c;
}
return null;
}
private ScrollViewer? FindScrollViewer()
{
for (var queue = new Queue<DependencyObject>(new DependencyObject[] { _grid }); queue.Count > 0;)
{
var d = queue.Dequeue();
if (d is ScrollViewer sv) return sv;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(d); i++)
queue.Enqueue(VisualTreeHelper.GetChild(d, i));
}
return null;
}
}
+45
View File
@@ -0,0 +1,45 @@
using System.Windows;
using System.Windows.Controls;
namespace Rozpisky.Theming;
/// <summary>
/// Připojená vlastnost: na <see cref="DataGrid"/> obarví editační prvky textových sloupců do
/// tmavého motivu. WPF totiž editoru <see cref="DataGridTextColumn"/> nepředá implicitní styl
/// TextBoxu sloupec má vlastní <b>nenulový</b> výchozí <c>EditingElementStyle</c> (systémově bílý),
/// takže ho musíme přepsat. Nastavuje se hromadně přes globální styl DataGridu (<c>Controls.xaml</c>),
/// takže platí pro všechny mřížky bez zásahu do jednotlivých sloupců.
/// </summary>
public static class GridTheme
{
public static readonly DependencyProperty ThemeEditorsProperty =
DependencyProperty.RegisterAttached("ThemeEditors", typeof(bool), typeof(GridTheme),
new PropertyMetadata(false, OnThemeEditorsChanged));
public static void SetThemeEditors(DependencyObject o, bool value) => o.SetValue(ThemeEditorsProperty, value);
public static bool GetThemeEditors(DependencyObject o) => (bool)o.GetValue(ThemeEditorsProperty);
private static void OnThemeEditorsChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is not DataGrid grid || !(bool)e.NewValue) return;
// Sloupce z XAML existují až po načtení; aplikuj tedy po Loaded (nebo hned, je-li už načteno).
grid.Loaded += (_, _) => Apply(grid);
if (grid.IsLoaded) Apply(grid);
// Sloupce přidané za běhu (po Loaded) by jinak zůstaly bílé.
grid.Columns.CollectionChanged += (_, _) => { if (grid.IsLoaded) Apply(grid); };
}
private static void Apply(DataGrid grid)
{
if (grid.TryFindResource("DataGridCell.Editor.TextBox") is not Style style) return;
foreach (var col in grid.Columns)
if (col is DataGridTextColumn text
// Null-check nestačí: výchozí EditingElementStyle je nenulový (systémově bílý).
// Rozlišením je lokální hodnota defaultní styl lokální hodnotou není, takže
// ReadLocalValue == UnsetValue ⇒ nikdo si vlastní editor nenastavil a smíme přepsat.
&& text.ReadLocalValue(DataGridBoundColumn.EditingElementStyleProperty) == DependencyProperty.UnsetValue)
{
text.EditingElementStyle = style;
}
}
}
+18
View File
@@ -0,0 +1,18 @@
using System.Windows;
namespace Rozpisky.Theming;
/// <summary>
/// Připojená vlastnost: glyph ze Segoe MDL2 Assets zobrazený před textem tlačítka.
/// Vykresluje ho šablona Buttonu v <c>Controls.xaml</c>; bez nastaveného glyphu se
/// ikona vůbec nezobrazí, takže stávající tlačítka zůstávají beze změny.
/// </summary>
public static class Icon
{
public static readonly DependencyProperty GlyphProperty =
DependencyProperty.RegisterAttached("Glyph", typeof(string), typeof(Icon),
new PropertyMetadata(null));
public static void SetGlyph(DependencyObject o, string? value) => o.SetValue(GlyphProperty, value);
public static string? GetGlyph(DependencyObject o) => (string?)o.GetValue(GlyphProperty);
}
+23
View File
@@ -0,0 +1,23 @@
using System.Windows;
namespace Rozpisky.ViewModels;
/// <summary>
/// Freezable, který „propašuje" DataContext (VM) i tam, kde není vizuální strom
/// hlavně do sloupců <see cref="System.Windows.Controls.DataGrid"/>. Sloupce s našeptáváním
/// (editovatelné ComboBoxy plněné z číselníků) se přes něj dostanou na <c>Ciselniky</c>.
/// Vzor: proxy v Resources s <c>Data="{Binding}"</c>, pak <c>Source={StaticResource ...}</c>.
/// </summary>
public sealed class BindingProxy : Freezable
{
protected override Freezable CreateInstanceCore() => new BindingProxy();
public object? Data
{
get => GetValue(DataProperty);
set => SetValue(DataProperty, value);
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register(nameof(Data), typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
using System.Windows.Input;
namespace Rozpisky.ViewModels;
/// <summary>Jednoduchá ICommand implementace pro MVVM-lite (bez NuGet závislostí).</summary>
public sealed class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool>? _canExecute;
public RelayCommand(Action execute, Func<bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
public void Execute(object? parameter) => _execute();
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
+87
View File
@@ -0,0 +1,87 @@
<UserControl x:Class="Rozpisky.Views.CiselnikyView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:Rozpisky.ViewModels"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="450">
<UserControl.Resources>
<!-- Vlastní CollectionViewSource pro každou mřížku: CanUserAddRows přidává „NewItemPlaceholder"
jen do TÉTO view, ne do sdílené výchozí view kolekce. Comboboxy jinde (vázané přímo na
Ciselniky.*) tak v nabídce placeholder nemají. Source přes BindingProxy (Resources nemají DataContext). -->
<vm:BindingProxy x:Key="Proxy" Data="{Binding}"/>
<CollectionViewSource x:Key="cvsOrganizace" Source="{Binding Data.Ciselniky.Organizace, Source={StaticResource Proxy}}"/>
<CollectionViewSource x:Key="cvsFirmy" Source="{Binding Data.Ciselniky.Firmy, Source={StaticResource Proxy}}"/>
<CollectionViewSource x:Key="cvsJmena" Source="{Binding Data.Ciselniky.Jmena, Source={StaticResource Proxy}}"/>
<CollectionViewSource x:Key="cvsVerze" Source="{Binding Data.Ciselniky.VerzeDokumentace, Source={StaticResource Proxy}}"/>
<CollectionViewSource x:Key="cvsCasti" Source="{Binding Data.Ciselniky.Casti, Source={StaticResource Proxy}}"/>
<CollectionViewSource x:Key="cvsKraje" Source="{Binding Data.Ciselniky.Kraje, Source={StaticResource Proxy}}"/>
</UserControl.Resources>
<DockPanel Margin="12">
<Grid DockPanel.Dock="Top" Margin="0,0,0,8">
<StackPanel VerticalAlignment="Center">
<TextBlock Text="Databáze" Style="{StaticResource Text.H2}"/>
<TextBlock Style="{StaticResource Text.Caption}"
Text="Sdílená data napříč projekty (organizace, firmy, jména, verze, části, kraje)."/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Content="+ Přidat záznam" Style="{StaticResource Button.Primary}"
ToolTip="Přidá nový záznam do aktivní tabulky databáze" Click="PridatZaznam_Click"/>
<Button Content="Odebrat záznam" Style="{StaticResource Button.Danger}" Margin="8,0,0,0"
ToolTip="Odebere vybraný záznam z aktivní tabulky databáze" Click="OdebratZaznam_Click"/>
</StackPanel>
</Grid>
<TabControl x:Name="Zalozky">
<TabItem Header="Organizace">
<DataGrid ItemsSource="{Binding Source={StaticResource cvsOrganizace}}" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Název" Binding="{Binding Nazev}" Width="240"/>
<DataGridTextColumn Header="Adresa" Binding="{Binding Adresa}" Width="*"/>
<DataGridTextColumn Header="Logo (soubor)" Binding="{Binding Logo}" Width="160"/>
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem Header="Firmy">
<DataGrid ItemsSource="{Binding Source={StaticResource cvsFirmy}}" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Firma" Binding="{Binding Nazev}" Width="220"/>
<DataGridTextColumn Header="Adresa" Binding="{Binding Adresa}" Width="*"/>
<DataGridTextColumn Header="Telefon" Binding="{Binding Telefon}" Width="140"/>
<DataGridTextColumn Header="E-mail" Binding="{Binding Email}" Width="160"/>
<DataGridTextColumn Header="Logo (soubor)" Binding="{Binding Logo}" Width="160"/>
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem Header="Jména">
<DataGrid ItemsSource="{Binding Source={StaticResource cvsJmena}}" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Jméno" Binding="{Binding Hodnota}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem Header="Verze dokumentace">
<DataGrid ItemsSource="{Binding Source={StaticResource cvsVerze}}" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Verze" Binding="{Binding Hodnota}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem Header="Části">
<DataGrid ItemsSource="{Binding Source={StaticResource cvsCasti}}" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Označení" Binding="{Binding Oznaceni}" Width="100"/>
<DataGridTextColumn Header="Název" Binding="{Binding Nazev}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem Header="Kraje">
<DataGrid ItemsSource="{Binding Source={StaticResource cvsKraje}}" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Kraj" Binding="{Binding Nazev}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</TabItem>
</TabControl>
</DockPanel>
</UserControl>
+34
View File
@@ -0,0 +1,34 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace Rozpisky.Views;
public partial class CiselnikyView : UserControl
{
public CiselnikyView() => InitializeComponent();
/// <summary>DataGrid aktivní záložky (obsah každé záložky je přímo <see cref="DataGrid"/>).</summary>
private DataGrid? AktivniGrid() => (Zalozky.SelectedItem as TabItem)?.Content as DataGrid;
/// <summary>Přidá nový (prázdný) záznam do číselníku aktivní záložky. Typ položky odvodí
/// <see cref="IEditableCollectionView.AddNew"/> z kolekce, takže funguje pro všechny záložky
/// bez ohledu na typ řádku.</summary>
private void PridatZaznam_Click(object sender, RoutedEventArgs e)
{
if (AktivniGrid() is not { } grid || grid.Items is not IEditableCollectionView view || !view.CanAddNew) return;
if (view.IsAddingNew) view.CommitNew(); // ukonči případnou rozdělanou transakci placeholderu
var novy = view.AddNew();
view.CommitNew();
grid.SelectedItem = novy;
grid.ScrollIntoView(novy);
}
/// <summary>Odebere vybraný záznam z číselníku aktivní záložky.</summary>
private void OdebratZaznam_Click(object sender, RoutedEventArgs e)
{
if (AktivniGrid() is not { } grid || grid.SelectedItem is not { } vybrany) return;
if (grid.Items is IEditableCollectionView view && view.CanRemove)
view.Remove(vybrany);
}
}
+42
View File
@@ -0,0 +1,42 @@
<Window x:Class="Rozpisky.Views.LayoutLogWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Title="Průběh" Width="640" Height="420"
Icon="/Rozpisky.ico"
ShowInTaskbar="False"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource Brush.Window}"
Foreground="{DynamicResource Brush.Text}">
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock x:Name="Nadpis" Grid.Row="0" Style="{StaticResource Text.H2}" Margin="0,0,0,8"/>
<ListBox x:Name="Seznam" Grid.Row="1" ItemsSource="{Binding}"
Background="{DynamicResource Brush.Window}"
Foreground="{DynamicResource Brush.Text}"
BorderBrush="{DynamicResource Brush.BorderStrong}" BorderThickness="1"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock TextWrapping="Wrap" Text="{Binding Text}"
Foreground="{Binding Brush}" FontFamily="Consolas"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
<Button Content="Kopírovat" Click="Kopirovat_Click"/>
<Button x:Name="ZrusitBtn" Content="Zrušit" Style="{StaticResource Button.Danger}" Margin="8,0,0,0" Click="Zrusit_Click"/>
<Button Content="Zavřít" Style="{StaticResource Button.Primary}" Margin="8,0,0,0"
IsDefault="True" IsCancel="True" Click="Zavrit_Click"/>
</StackPanel>
</Grid>
</Window>
+94
View File
@@ -0,0 +1,94 @@
using System.Collections.ObjectModel;
using System.Threading;
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
using Rozpisky.Cad;
namespace Rozpisky.Views;
/// <summary>
/// Nemodální log okno pro dávkové CAD akce (aktualizace rozvržení, tisk do PDF) každý krok/chyba
/// se zapíše jako řádek, nic se neztratí v jednom souhrnném MessageBoxu. Otevírá se na začátku akce
/// (<see cref="Window.Show"/>, ne <c>ShowDialog</c>) a zůstává otevřené i po dokončení. Tlačítko
/// „Zrušit" nastaví <see cref="Zruseno"/>, které se kontroluje mezi jednotlivými kroky dávky.
/// </summary>
public partial class LayoutLogWindow : Window, ILayoutLog
{
private readonly ObservableCollection<LogRadek> _radky = new();
private readonly CancellationTokenSource _cts = new();
public bool Zruseno => _cts.IsCancellationRequested;
public CancellationToken Token => _cts.Token;
public LayoutLogWindow(string titulek)
{
InitializeComponent();
Title = titulek;
Nadpis.Text = titulek;
Seznam.DataContext = _radky;
_radky.CollectionChanged += (_, _) =>
{
// Nesmí volat ScrollIntoView synchronně tady ListBox zpracovává tu samou CollectionChanged
// notifikaci přes svůj ListCollectionView (aktualizuje ItemContainerGenerator). Synchronní
// vynucení layoutu uprostřed toho rozhodí jeho interní počítadlo (InvalidOperationException
// "Generator_Inconsistent"). Odložit na Background nechá ListBox notifikaci nejdřív dokončit.
if (_radky.Count == 0) return;
var posledni = _radky[^1];
Dispatcher.BeginInvoke(() => Seznam.ScrollIntoView(posledni), DispatcherPriority.Background);
};
}
public void Info(string zprava) => Pridej(zprava, chyba: false);
public void Chyba(string zprava) => Pridej(zprava, chyba: true);
private void Pridej(string zprava, bool chyba)
{
void Zapis() => _radky.Add(new LogRadek(zprava, chyba));
if (Dispatcher.CheckAccess()) Zapis();
else Dispatcher.Invoke(Zapis);
}
/// <summary>Po zrušení se tlačítko deaktivuje další klik nic nemění, dávka doběhne aktuální krok
/// a smyčka se ukončí sama (kontroluje <see cref="Zruseno"/>).</summary>
private void Zrusit_Click(object sender, RoutedEventArgs e)
{
_cts.Cancel();
ZrusitBtn.IsEnabled = false;
Info("Zrušeno uživatelem dokončuji aktuální krok…");
}
private void Zavrit_Click(object sender, RoutedEventArgs e) => Close();
/// <summary>Zavření okna uprostřed běžící dávky ji zároveň zruší jinak by běžela dál „naslepo“
/// bez viditelného logu a bez možnosti ji zastavit.</summary>
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
base.OnClosing(e);
_cts.Cancel();
}
/// <summary>Zkopíruje celý log (všechny řádky, bez barev) do schránky, ať jde chybové hlášky/diagnostiku
/// snadno vložit jinam (e-mail, chat).</summary>
private void Kopirovat_Click(object sender, RoutedEventArgs e)
{
if (_radky.Count == 0) return;
string text = string.Join(Environment.NewLine, _radky.Select(r => r.Text));
// Schránku může zrovna držet jiný proces (CLIPBRD_E_CANT_OPEN) nesmí to shodit aplikaci.
try { Clipboard.SetDataObject(text, copy: false); }
catch (Exception ex) { Chyba($"Kopírování do schránky selhalo: {ex.Message}"); }
}
private sealed class LogRadek
{
public string Text { get; }
public Brush Brush { get; }
public LogRadek(string text, bool chyba)
{
Text = (chyba ? "✗ " : " ") + text;
Brush = (Brush)(Application.Current.TryFindResource(chyba ? "Brush.Danger" : "Brush.Text") ?? System.Windows.Media.Brushes.Gray);
}
}
}
+147
View File
@@ -0,0 +1,147 @@
<Window x:Class="Rozpisky.Views.NapovedaWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:theming="clr-namespace:Rozpisky.Theming"
mc:Ignorable="d"
Title="Nápověda Rozpisky" Height="820" Width="760"
Icon="/Rozpisky.ico"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource Brush.Window}"
Foreground="{DynamicResource Brush.Text}">
<Window.Resources>
<!-- Odsazení nadpisů sekcí a odstavců (jednotný rytmus textu). -->
<Style x:Key="Napoveda.Nadpis" TargetType="{x:Type TextBlock}" BasedOn="{StaticResource Text.H2}">
<Setter Property="Margin" Value="0,18,0,6"/>
</Style>
<Style x:Key="Napoveda.Odstavec" TargetType="{x:Type TextBlock}">
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="Margin" Value="0,0,0,6"/>
</Style>
<Style x:Key="Napoveda.Polozka" TargetType="{x:Type TextBlock}">
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="Margin" Value="14,0,0,4"/>
</Style>
</Window.Resources>
<DockPanel>
<!-- Spodní lišta akcí -->
<Border DockPanel.Dock="Bottom" Background="{DynamicResource Brush.TitleBar}" Padding="16,10">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Otevřít PDF manuál" theming:Icon.Glyph="&#xEC50;"
Style="{StaticResource Button.Subtle}"
ToolTip="Otevře originální manuál SŽ (SZ_SM011_P10_Manual_v6.pdf) ve výchozím prohlížeči PDF"
Command="{Binding OtevritManualCmd}"/>
<Button Content="Zavřít" Style="{StaticResource Button.Primary}" Margin="8,0,0,0"
IsCancel="True" IsDefault="True"/>
</StackPanel>
</Border>
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="24,20,24,20">
<StackPanel MaxWidth="680" HorizontalAlignment="Left">
<TextBlock Style="{StaticResource Text.H1}" Text="Nápověda k programu Rozpisky"/>
<TextBlock Style="{StaticResource Text.Caption}" Margin="0,2,0,0"
Text="Generátor výkresových rohových razítek (rozpisek)"/>
<!-- 1. K čemu program slouží -->
<TextBlock Style="{StaticResource Napoveda.Nadpis}" Text="K čemu program slouží"/>
<TextBlock Style="{StaticResource Napoveda.Odstavec}">
<Run Text="Rozpisky jsou samostatná (standalone) náhrada původního excelového nástroje "/>
<Run Text="„Rozpisky 1.0.8"/><Run Text="&#x201C;"/>
<Run Text=" pro generování rohových razítek výkresů. Program nevyžaduje AutoCAD."/>
</TextBlock>
<TextBlock Style="{StaticResource Napoveda.Odstavec}">
<Run Text="Princip: jednou vyplníte "/><Run FontWeight="SemiBold" Text="Údaje projektu"/>
<Run Text=" (společné všem výkresům) a postupně plníte "/>
<Run FontWeight="SemiBold" Text="Seznam příloh"/>
<Run Text=" jeden řádek = jeden výkres = jedna rozpiska. Program každý řádek spojí s údaji projektu, naplní pole šablony a umí rozpisku zobrazit (náhled), vyexportovat i vytisknout."/>
</TextBlock>
<!-- 2. Rychlý začátek -->
<TextBlock Style="{StaticResource Napoveda.Nadpis}" Text="Rychlý začátek"/>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="1."/><Run Text=" Soubor → Nový projekt."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="2."/><Run Text=" Na záložce Údaje projektu vyplňte společné údaje (stavba, objednatel, zhotovitel…). Výběr z číselníku automaticky doplní adresy a kontakty."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="3."/><Run Text=" Na záložce Seznam příloh přidávejte řádky (jeden výkres = jeden řádek) a vyplňujte je přímo v tabulce."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="4."/><Run Text=" Horní lištou vyexportujte rozpisky do PDF/DXF a projekt uložte (Ctrl+S)."/></TextBlock>
<!-- 3. Přehled záložek -->
<TextBlock Style="{StaticResource Napoveda.Nadpis}" Text="Přehled záložek"/>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Údaje projektu"/><Run Text=" formulář s údaji společnými celému projektu. Výběr objednatele / zhotovitele z číselníku automaticky doplní adresu a kontakty."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Seznam příloh"/><Run Text=" hlavní pracovní plocha. Tabulka, kde jeden řádek = jedna rozpiska; vybraná pole nabízejí výběr z číselníků. Sloupec „Jméno rozvržení“ se generuje poloautomaticky (řízeno polem „Příloha objektu“); zaškrtnutím „Použít zkratku“ se k automaticky generovanému jménu připojí „_“ a zkratka přílohy (jen u objektové přílohy, ne u desek objektu); řádky se shodným jménem (kolize názvu exportu) se zvýrazní."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Revize"/><Run Text=" tabulka revizí projektu (číslo/kód verze, datum, popis, kdo kontroloval). Revize jsou interní data zakázky a ukládají se spolu s projektem."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Číselníky"/><Run Text=" správa sdílených hodnot (organizace, firmy, jména, profese, verze dokumentace, části). Číselníky žijí vedle programu a sdílí se napříč projekty; nabídka se navíc doplňuje z používání."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Nastavení"/><Run Text=" cesty exportu, šablona rozpisky, volba CAD systému a parametry tisku."/></TextBlock>
<!-- 4. Akce v horní liště -->
<TextBlock Style="{StaticResource Napoveda.Nadpis}" Text="Akce v horní liště"/>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Export PDF (Výběr / Vše)"/><Run Text=" vyexportuje vybrané, resp. všechny rozpisky do PDF do struktury složek pod kořenovou cestou exportu (Nastavení)."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Export DXF (Výběr / Vše)"/><Run Text=" vyexportuje rozpisky do jednoho DXF, poskládané do mřížky vedle sebe."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Seznam příloh (Tisk / Export)"/><Run Text=" vytiskne seznam příloh objektu do PDF, resp. vyexportuje ho do XLSX (vedle souboru projektu)."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Rozvržení (Aktualizovat / Tisk)"/><Run Text=" práce s rozvržením výkresu přes připojený CAD (aktualizace hlavičky a tisk rozvržení)."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Odstavec}" Margin="14,4,0,6">
<Run Text="Průběh dávkových akcí ukazuje stavový řádek dole; běžící export lze přerušit tlačítkem "/>
<Run FontWeight="SemiBold" Text="Zrušit export"/><Run Text="."/>
</TextBlock>
<!-- 5. Klávesové zkratky -->
<TextBlock Style="{StaticResource Napoveda.Nadpis}" Text="Klávesové zkratky"/>
<TextBlock Style="{StaticResource Napoveda.Odstavec}" Margin="0,2,0,4"><Run FontWeight="SemiBold" Text="Práce se souborem projektu"/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Ctrl+N"/><Run Text=" Nový projekt."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Ctrl+O"/><Run Text=" Otevřít projekt."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Ctrl+S"/><Run Text=" Uložit projekt."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Ctrl+Shift+S"/><Run Text=" Uložit projekt jako…"/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="F1"/><Run Text=" Otevře tuto nápovědu."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Odstavec}" Margin="0,8,0,4"><Run FontWeight="SemiBold" Text="Přepínání záložek"/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Alt+1"/><Run Text=" Údaje projektu, "/><Run FontWeight="SemiBold" Text="Alt+2"/><Run Text=" Seznam příloh, "/><Run FontWeight="SemiBold" Text="Alt+3"/><Run Text=" Revize, "/><Run FontWeight="SemiBold" Text="Alt+4"/><Run Text=" Databáze, "/><Run FontWeight="SemiBold" Text="Alt+5"/><Run Text=" Nastavení."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Odstavec}" Margin="0,8,0,4"><Run FontWeight="SemiBold" Text="Práce v tabulkách (Seznam příloh, Revize, Databáze)"/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="F2"/><Run Text=" zahájí editaci vybrané buňky (buňka se začne editovat i tím, že rovnou začnete psát)."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Enter"/><Run Text=" potvrdí zadání a přejde o řádek níž."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Esc"/><Run Text=" zruší rozeditovanou hodnotu buňky (a probíhající tažení úchytu, viz níže)."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Tab / Shift+Tab"/><Run Text=" přeskočí na další / předchozí buňku; "/><Run FontWeight="SemiBold" Text="šipky"/><Run Text=" pohyb mezi buňkami."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Ctrl+C / Ctrl+V"/><Run Text=" zkopíruje / vloží obsah buněk (i více najednou)."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Delete"/><Run Text=" v Seznamu příloh vymaže obsah vybraných textových buněk (jako v Excelu); needitovatelné sloupce („Jméno rozvržení“) a zaškrtávátka nechá být. V Databázi s vybraným řádkem (přes hlavičku) smaže celý řádek."/></TextBlock>
<!-- 6. Rychlé vyplňování tabulky úchyt (fill handle) -->
<TextBlock Style="{StaticResource Napoveda.Nadpis}" Text="Rychlé vyplňování tabulky (úchyt)"/>
<TextBlock Style="{StaticResource Napoveda.Odstavec}">
<Run Text="V Seznamu příloh se buňky vybírají jako v Excelu: klik do buňky a "/>
<Run FontWeight="SemiBold" Text="tažení"/>
<Run Text=" vybere obdélníkovou oblast, Ctrl+klik / Shift+klik výběr rozšíří a celý řádek se vybere kliknutím na jeho hlavičku vlevo. V pravém dolním rohu výběru je malý modrý čtvereček "/>
<Run FontWeight="SemiBold" Text="úchyt"/><Run Text=" (fill handle)."/>
</TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Tažení úchytu"/><Run Text=" chyťte čtvereček myší a táhněte dolů (nebo do stran). Hodnoty vybraných buněk se rozkopírují do celé přejeté oblasti; vzor výběru se přitom opakuje (žádné dopočítávání číselných řad). Směr (po řádcích, nebo po sloupcích) se zamkne podle toho, kterým směrem vyjedete dál za výběr. Cílová oblast se během tažení ukazuje čárkovaně; tažení zrušíte klávesou Esc."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Dvojklik na úchyt"/><Run Text=" vyplní vzor výběru automaticky až do posledního řádku tabulky (nemusíte táhnout ručně)."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Odstavec}" Margin="14,0,0,6">
<Run Text="Kopírují se jen hodnoty, které v cílovém sloupci dávají smysl; needitovatelné sloupce (např. „Jméno rozvržení“) se přeskočí. Vyplnění úchytem "/>
<Run FontWeight="SemiBold" Text="nelze vzít zpět"/><Run Text=" (není Ctrl+Z), proto před hromadným tažením raději projekt uložte."/>
</TextBlock>
<!-- 7. Soubory vedle programu -->
<TextBlock Style="{StaticResource Napoveda.Nadpis}" Text="Soubory vedle programu"/>
<TextBlock Style="{StaticResource Napoveda.Odstavec}">
<Run Text="Program používá několik souborů uložených ve stejné složce jako spustitelný soubor. Lze je upravovat, aby si úpravy zachovaly i po aktualizaci programu:"/>
</TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="ciselniky.json"/><Run Text=" sdílené číselníky."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="mapovani.json"/><Run Text=" mapování polí programu na atributy DXF šablony."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="cad.json"/><Run Text=" seznam podporovaných CAD systémů a poslední volba."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="Rozpiska.dxf"/><Run Text=" DXF šablona rozpisky, "/><Run FontWeight="SemiBold" Text="SEZNAM.xlsx"/><Run Text=" mustr seznamu příloh, složka "/><Run FontWeight="SemiBold" Text="loga\"/><Run Text=" loga institucí (DXF)."/></TextBlock>
<TextBlock Style="{StaticResource Napoveda.Polozka}"><Run FontWeight="SemiBold" Text="SZ_SM011_P10_Manual_v6.pdf"/><Run Text=" originální závazný manuál SŽ (viz níže)."/></TextBlock>
<!-- 8. Kde hledat víc -->
<TextBlock Style="{StaticResource Napoveda.Nadpis}" Text="Kde hledat víc"/>
<TextBlock Style="{StaticResource Napoveda.Odstavec}">
<Run Text="Autoritativním zdrojem pro význam jednotlivých polí rozpisky, kódové označení přílohy a pravidla vyplnění popisového pole je "/>
<Run FontWeight="SemiBold" Text="originální manuál SŽ"/>
<Run Text=". Otevřete ho tlačítkem "/><Run FontWeight="SemiBold" Text="„Otevřít PDF manuál"/><Run Text="&#x201C;"/>
<Run Text=" dole v tomto okně (nebo z nabídky Nápověda → Manuál SŽ)."/>
</TextBlock>
</StackPanel>
</ScrollViewer>
</DockPanel>
</Window>
+14
View File
@@ -0,0 +1,14 @@
using System.Windows;
namespace Rozpisky.Views;
/// <summary>Vestavěná nápověda k programu (rolovatelné okno se sekcemi). Text je statický v XAML;
/// tlačítko „Otevřít PDF manuál" binduje na <c>OtevritManualCmd</c> z <see cref="ViewModels.MainViewModel"/>
/// (DataContext se nastavuje při otevírání okna).</summary>
public partial class NapovedaWindow : Window
{
public NapovedaWindow()
{
InitializeComponent();
}
}
+109
View File
@@ -0,0 +1,109 @@
<UserControl x:Class="Rozpisky.Views.NastaveniView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignWidth="700">
<UserControl.Resources>
<Style TargetType="TextBlock" x:Key="FieldLabel" BasedOn="{StaticResource Text.Caption}">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="0,0,10,8"/>
</Style>
<Thickness x:Key="Inp">0,0,0,8</Thickness>
</UserControl.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="16">
<StackPanel MaxWidth="640" HorizontalAlignment="Left">
<TextBlock Text="Nastavení" Style="{StaticResource Text.H1}" Margin="0,0,0,4"/>
<TextBlock Style="{StaticResource Text.Caption}"
Text="Konfigurace exportu a tisku. Neřídí pole rozpisky, ale výstup."/>
<GroupBox Header="EXPORT">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Kořenová cesta exportu" Style="{StaticResource FieldLabel}"/>
<TextBox Grid.Row="0" Grid.Column="1" Margin="{StaticResource Inp}"
Text="{Binding Nastaveni.CestaExportu, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Podsložka exportu" Style="{StaticResource FieldLabel}"/>
<TextBox Grid.Row="1" Grid.Column="1" Margin="{StaticResource Inp}"
Text="{Binding Nastaveni.SlozkaExportu, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="DXF šablona" Style="{StaticResource FieldLabel}"/>
<TextBox Grid.Row="2" Grid.Column="1" Margin="{StaticResource Inp}"
Text="{Binding Nastaveni.SablonaDxf, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</GroupBox>
<GroupBox Header="CAD">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="CAD systém" Style="{StaticResource FieldLabel}"/>
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Margin="{StaticResource Inp}">
<ComboBox Width="200" ItemsSource="{Binding PodporovaneCady}"
DisplayMemberPath="Nazev" SelectedItem="{Binding VybranyCad}"/>
<Button Content="Připojit" Margin="10,0,0,0" Padding="10,0"
Command="{Binding PripojitCadCmd}"/>
</StackPanel>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Stav" Style="{StaticResource FieldLabel}"/>
<TextBlock Grid.Row="1" Grid.Column="1" Margin="{StaticResource Inp}"
VerticalAlignment="Center" TextWrapping="Wrap"
Text="{Binding CadStav}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="ProgID (ručně)" Style="{StaticResource FieldLabel}"/>
<StackPanel Grid.Row="2" Grid.Column="1" Margin="{StaticResource Inp}">
<TextBox Width="260" HorizontalAlignment="Left"
IsEnabled="{Binding JeRucniCad}"
Text="{Binding RucniProgId, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Style="{StaticResource Text.Caption}" TextWrapping="Wrap" Margin="0,4,0,0"
Text="Aktivní jen v režimu Manuálně (výběr nahoře). Zadej verzově konkrétní ProgID, např. ZWCAD.Application.2023 pomůže, když se CAD přes obecný název nenapojí."/>
</StackPanel>
</Grid>
</GroupBox>
<GroupBox Header="VÝKRES / TISK">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Automatické měřítko" Style="{StaticResource FieldLabel}"/>
<ToggleButton Grid.Row="0" Grid.Column="1" Style="{StaticResource ToggleSwitch}"
HorizontalAlignment="Left" Margin="{StaticResource Inp}"
IsChecked="{Binding Nastaveni.MeritkoAutomaticky}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="LTSCALE automaticky" Style="{StaticResource FieldLabel}"/>
<ToggleButton Grid.Row="1" Grid.Column="1" Style="{StaticResource ToggleSwitch}"
HorizontalAlignment="Left" Margin="{StaticResource Inp}"
IsChecked="{Binding Nastaveni.LtscaleAutomaticky}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Upravovat tiskárnu" Style="{StaticResource FieldLabel}"/>
<ToggleButton Grid.Row="2" Grid.Column="1" Style="{StaticResource ToggleSwitch}"
HorizontalAlignment="Left" Margin="{StaticResource Inp}"
IsChecked="{Binding Nastaveni.UpravovatTiskarnu}"/>
</Grid>
</GroupBox>
</StackPanel>
</ScrollViewer>
</UserControl>
+8
View File
@@ -0,0 +1,8 @@
using System.Windows.Controls;
namespace Rozpisky.Views;
public partial class NastaveniView : UserControl
{
public NastaveniView() => InitializeComponent();
}
+33
View File
@@ -0,0 +1,33 @@
<UserControl x:Class="Rozpisky.Views.RevizeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="400">
<DockPanel Margin="12">
<Grid DockPanel.Dock="Top" Margin="0,0,0,8">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Revize" Style="{StaticResource Text.H2}" VerticalAlignment="Center"/>
<TextBlock Style="{StaticResource Text.Caption}" VerticalAlignment="Center" Margin="12,0,0,0"
Text="Interní data zakázky (ukládají se s projektem)."/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="+ Přidat revizi" Style="{StaticResource Button.Primary}"
Command="{Binding PridejReviziCmd}"/>
<Button Content="Smazat revizi" Style="{StaticResource Button.Danger}" Margin="8,0,0,0"
Command="{Binding SmazReviziCmd}"/>
</StackPanel>
</Grid>
<DataGrid ItemsSource="{Binding Revize}"
SelectedItem="{Binding SelectedRevize}"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Revize" Binding="{Binding Cislo}" Width="90"/>
<DataGridTextColumn Header="Datum" Binding="{Binding Datum}" Width="130"/>
<DataGridTextColumn Header="Popis" Binding="{Binding Popis}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</UserControl>
+8
View File
@@ -0,0 +1,8 @@
using System.Windows.Controls;
namespace Rozpisky.Views;
public partial class RevizeView : UserControl
{
public RevizeView() => InitializeComponent();
}
+327
View File
@@ -0,0 +1,327 @@
<UserControl x:Class="Rozpisky.Views.SeznamPrilohView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:Rozpisky.ViewModels"
xmlns:beh="clr-namespace:Rozpisky.Behaviors"
xmlns:theming="clr-namespace:Rozpisky.Theming"
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="500">
<UserControl.Resources>
<!-- Přenese VM do sloupců DataGridu (mimo vizuální strom) → našeptávání z číselníků. -->
<vm:BindingProxy x:Key="Proxy" Data="{Binding}"/>
</UserControl.Resources>
<DockPanel Margin="12">
<!-- Mini-toolbar nad mřížkou -->
<Grid DockPanel.Dock="Top" Margin="0,0,0,8">
<StackPanel Orientation="Horizontal" VerticalAlignment="Bottom" Margin="0,0,0,6">
<TextBlock Text="Seznam příloh" Style="{StaticResource Text.H2}" VerticalAlignment="Center"/>
<TextBlock Style="{StaticResource Text.Caption}" VerticalAlignment="Center" Margin="12,0,0,0"
Text="Jeden řádek = jedna rozpiska. Náhled přes tlačítko. Pořadí mění šipky. Jména a zhotovitel se našeptávají z číselníků."/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<HeaderedContentControl Style="{StaticResource Toolbar.Group}" Header="Náhled">
<StackPanel Orientation="Horizontal">
<Button Content="Náhled" theming:Icon.Glyph="&#xE890;" Style="{StaticResource Button.Primary}"
ToolTip="Zobrazí náhled rozpisky vybraného řádku"
Command="{Binding NahledPrilohyCmd}"/>
<Button Content="Schéma…" theming:Icon.Glyph="&#xE8B9;" Margin="6,0,0,0"
ToolTip="Vybrat DXF orientačního schématu pro vybraný řádek"
Command="{Binding VyberSchemaCmd}"/>
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Style="{StaticResource Toolbar.Group}" Header="Pořadí">
<StackPanel Orientation="Horizontal">
<Button theming:Icon.Glyph="&#xE74A;"
ToolTip="Posunout řádek nahoru" Style="{StaticResource Button.Icon}"
Command="{Binding PosunPrilohuNahoruCmd}"/>
<Button theming:Icon.Glyph="&#xE74B;"
ToolTip="Posunout řádek dolů" Style="{StaticResource Button.Icon}" Margin="4,0,0,0"
Command="{Binding PosunPrilohuDoluCmd}"/>
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Style="{StaticResource Toolbar.Group}" Header="Řádky" Margin="0">
<StackPanel Orientation="Horizontal">
<Button Content="Přidat" theming:Icon.Glyph="&#xE710;"
ToolTip="Přidá nový řádek na konec seznamu"
Command="{Binding PridejPrilohuCmd}"/>
<Button Content="Duplikovat" theming:Icon.Glyph="&#xE8C8;"
ToolTip="Vloží kopii vybraného řádku hned pod něj" Margin="6,0,0,0"
Command="{Binding DuplikujPrilohuCmd}"/>
<Button Content="Smazat" theming:Icon.Glyph="&#xE74D;" Style="{StaticResource Button.Danger}" Margin="6,0,0,0"
Command="{Binding SmazPrilohuCmd}"/>
</StackPanel>
</HeaderedContentControl>
</StackPanel>
</Grid>
<!-- CanUserSortColumns=False: pořadí řádků = pořadí příloh, řadí se jen šipkami v toolbaru. -->
<!-- SelectionUnit=CellOrRowHeader: klik vybírá buňky (kvůli fill handle), celé řádky přes
hlavičku řádku. SelectedItem SCHVÁLNĚ NENÍ bindovaný: v režimu výběru buněk by zpětná
vazba VM→grid při každém kliku rozšířila výběr jedné buňky na celý řádek (a úchyt fill
handle by zmizel za pravý okraj). Obousměrnou synchronizaci SelectedPriloha řeší
code-behind (Grid_SelectedCellsChanged ↔ Vm_PropertyChanged) s guardem proti smyčce. -->
<DataGrid x:Name="Grid"
ItemsSource="{Binding Prilohy}"
SelectionMode="Extended"
SelectionUnit="CellOrRowHeader"
SelectedCellsChanged="Grid_SelectedCellsChanged"
CanUserSortColumns="False"
CanUserReorderColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
HeadersVisibility="All"
RowHeaderWidth="16"
theming:GridFill.EnableFillHandle="True"
beh:GridDelete.Enabled="True"
beh:GridPaste.Enabled="True">
<!-- Zvýraznění řádku s duplicitním jménem rozvržení (kolize názvu exportu). -->
<DataGrid.RowStyle>
<Style TargetType="DataGridRow" BasedOn="{StaticResource {x:Type DataGridRow}}">
<Style.Triggers>
<!-- Jemné podbarvení všech řádků aktuálního výběru (i vícenásobného tahem). Řídí
ho JeVybrana (nastavuje VM.NastavVyber z DataGrid.SelectedCells), ne jediný
SelectedPriloha tím se zvýrazní celý výběr a nezaostává o jeden klik. Před
duplicitním triggerem schválně: kolize jména zůstane výraznější, i když je
řádek vybraný. Vybraná buňka (SelectedSoft) je nad tím. -->
<DataTrigger Binding="{Binding JeVybrana}" Value="True">
<Setter Property="Background" Value="{DynamicResource Brush.CurrentRow}"/>
</DataTrigger>
<DataTrigger Binding="{Binding JeDuplicitniJmeno}" Value="True">
<Setter Property="Background" Value="{DynamicResource Brush.DangerSubtle}"/>
<Setter Property="ToolTip" Value="Duplicitní jméno rozvržení koliduje s jiným řádkem"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<!-- Sloupce AAD přesně dle původního excelového listu SEZNAM -->
<DataGrid.Columns>
<!-- DataGridTemplateColumn (ne DataGridCheckBoxColumn) záměrně: vestavěný checkbox sloupec
vyžaduje první klik na výběr buňky a až druhý na přepnutí. Checkbox v šabloně je plně
interaktivní i mimo editační režim buňky, takže stačí jeden klik. -->
<DataGridTemplateColumn Width="70" ClipboardContentBinding="{Binding JeObjektovaPriloha}">
<DataGridTemplateColumn.Header>
<TextBlock Text="Příloha objektu" TextWrapping="Wrap"
ToolTip="Zaškrtnuto = jméno rozvržení se generuje automaticky dle manuálu (needituje se)"/>
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding JeObjektovaPriloha, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="70" ClipboardContentBinding="{Binding JeDesky}">
<DataGridTemplateColumn.Header>
<TextBlock Text="Desky" TextWrapping="Wrap"
ToolTip="Zaškrtnuto = rozpiska na spisové desky objektu jen strojový kód pod rozpiskou dostane pevný střed _XX_X_000; jméno rozvržení tím neovlivní (řídí ho sloupec „Příloha objektu“)"/>
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding JeDesky, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="70" ClipboardContentBinding="{Binding JeRozpiska}">
<DataGridTemplateColumn.Header>
<TextBlock Text="Rozpiska" TextWrapping="Wrap"
ToolTip="Zaškrtnuto = název exportovaného PDF dostane příponu „_rozpiska“"/>
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding JeRozpiska, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Označení části" Binding="{Binding OznaceniCasti}" Width="60"/>
<DataGridTextColumn Header="Označení dílčí části/řady" Binding="{Binding OznaceniDilciCasti}" Width="90"/>
<DataGridTextColumn Header="Název objektu/dílčí části" Binding="{Binding NazevObjektu}" Width="200"/>
<DataGridTextColumn Header="Číslo objektu/komplexu" Binding="{Binding CisloObjektu}" Width="90"/>
<DataGridTextColumn Header="Číslo přílohy Typ" Binding="{Binding TypPrilohy}" Width="80"/>
<DataGridTextColumn Header="Číslo přílohy dílčí" Binding="{Binding DilciPriloha}" Width="80"/>
<DataGridTextColumn Header="Číslo přílohy pořadí" Binding="{Binding PoradiPrilohy}" Width="80"/>
<DataGridTextColumn Header="Název přílohy" Binding="{Binding NazevPrilohy}" Width="160"/>
<DataGridTextColumn Header="Název dílčí části přílohy" Binding="{Binding NazevDilciCastiPrilohy}" Width="160"/>
<DataGridTextColumn Header="Zkratka přílohy" Binding="{Binding Zkratka}" Width="80"/>
<DataGridTemplateColumn Width="70" ClipboardContentBinding="{Binding PouzitZkratku}">
<DataGridTemplateColumn.Header>
<TextBlock Text="Použít zkratku" TextWrapping="Wrap"
ToolTip="Zaškrtnuto = k automaticky generovanému jménu rozvržení (jen u objektové přílohy) se přidá „_“ + zkratka přílohy"/>
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding PouzitZkratku, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- Vypracoval našeptává z číselníku Jména (volný text povolen). -->
<DataGridTemplateColumn Header="Vypracoval" Width="140" ClipboardContentBinding="{Binding Vypracoval}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate><TextBlock Text="{Binding Vypracoval}" Margin="4,0" VerticalAlignment="Center"/></DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox Style="{StaticResource ComboBox.Cell}"
IsEditable="True" IsTextSearchEnabled="True" StaysOpenOnEdit="True"
ItemsSource="{Binding Data.Ciselniky.Jmena, Source={StaticResource Proxy}}"
TextSearch.TextPath="Hodnota"
Text="{Binding Vypracoval, UpdateSourceTrigger=LostFocus}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Odpovědný projektant" Width="150" ClipboardContentBinding="{Binding OdpovednyProjektant}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate><TextBlock Text="{Binding OdpovednyProjektant}" Margin="4,0" VerticalAlignment="Center"/></DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox Style="{StaticResource ComboBox.Cell}"
IsEditable="True" IsTextSearchEnabled="True" StaysOpenOnEdit="True"
ItemsSource="{Binding Data.Ciselniky.Jmena, Source={StaticResource Proxy}}"
TextSearch.TextPath="Hodnota"
Text="{Binding OdpovednyProjektant, UpdateSourceTrigger=LostFocus}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Specialista" Width="140" ClipboardContentBinding="{Binding Specialista}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate><TextBlock Text="{Binding Specialista}" Margin="4,0" VerticalAlignment="Center"/></DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox Style="{StaticResource ComboBox.Cell}"
IsEditable="True" IsTextSearchEnabled="True" StaysOpenOnEdit="True"
ItemsSource="{Binding Data.Ciselniky.Jmena, Source={StaticResource Proxy}}"
TextSearch.TextPath="Hodnota"
Text="{Binding Specialista, UpdateSourceTrigger=LostFocus}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="R1" Binding="{Binding R1}" Width="50"/>
<DataGridTemplateColumn Header="Kontrola" Width="150" ClipboardContentBinding="{Binding KontrolovalR1}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate><TextBlock Text="{Binding KontrolovalR1}" Margin="4,0" VerticalAlignment="Center"/></DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox Style="{StaticResource ComboBox.Cell}"
IsEditable="True" IsTextSearchEnabled="True" StaysOpenOnEdit="True"
ItemsSource="{Binding Data.Ciselniky.Jmena, Source={StaticResource Proxy}}"
TextSearch.TextPath="Hodnota"
Text="{Binding KontrolovalR1, UpdateSourceTrigger=LostFocus}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="R2" Binding="{Binding R2}" Width="50"/>
<DataGridTemplateColumn Header="Revize 2" Width="150" ClipboardContentBinding="{Binding KontrolovalR2}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate><TextBlock Text="{Binding KontrolovalR2}" Margin="4,0" VerticalAlignment="Center"/></DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox Style="{StaticResource ComboBox.Cell}"
IsEditable="True" IsTextSearchEnabled="True" StaysOpenOnEdit="True"
ItemsSource="{Binding Data.Ciselniky.Jmena, Source={StaticResource Proxy}}"
TextSearch.TextPath="Hodnota"
Text="{Binding KontrolovalR2, UpdateSourceTrigger=LostFocus}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="R3" Binding="{Binding R3}" Width="50"/>
<DataGridTemplateColumn Header="Revize 3" Width="150" ClipboardContentBinding="{Binding KontrolovalR3}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate><TextBlock Text="{Binding KontrolovalR3}" Margin="4,0" VerticalAlignment="Center"/></DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox Style="{StaticResource ComboBox.Cell}"
IsEditable="True" IsTextSearchEnabled="True" StaysOpenOnEdit="True"
ItemsSource="{Binding Data.Ciselniky.Jmena, Source={StaticResource Proxy}}"
TextSearch.TextPath="Hodnota"
Text="{Binding KontrolovalR3, UpdateSourceTrigger=LostFocus}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="R4" Binding="{Binding R4}" Width="50"/>
<DataGridTemplateColumn Header="Revize 4" Width="150" ClipboardContentBinding="{Binding KontrolovalR4}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate><TextBlock Text="{Binding KontrolovalR4}" Margin="4,0" VerticalAlignment="Center"/></DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox Style="{StaticResource ComboBox.Cell}"
IsEditable="True" IsTextSearchEnabled="True" StaysOpenOnEdit="True"
ItemsSource="{Binding Data.Ciselniky.Jmena, Source={StaticResource Proxy}}"
TextSearch.TextPath="Hodnota"
Text="{Binding KontrolovalR4, UpdateSourceTrigger=LostFocus}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<!-- Zhotovitel části našeptává z číselníku Firmy. -->
<DataGridTemplateColumn Header="Zhotovitel části" Width="160" ClipboardContentBinding="{Binding ZhotovitelCasti}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate><TextBlock Text="{Binding ZhotovitelCasti}" Margin="4,0" VerticalAlignment="Center"/></DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox Style="{StaticResource ComboBox.Cell}"
IsEditable="True" IsTextSearchEnabled="True" StaysOpenOnEdit="True"
ItemsSource="{Binding Data.Ciselniky.Firmy, Source={StaticResource Proxy}}"
TextSearch.TextPath="Nazev"
Text="{Binding ZhotovitelCasti, UpdateSourceTrigger=LostFocus}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Měřítko" Binding="{Binding Meritko}" Width="70"/>
<DataGridTextColumn Header="Formát x A4" Binding="{Binding Formaty}" Width="75"/>
<!-- Kraj našeptává z číselníku Kraje (volný text povolen). -->
<DataGridTemplateColumn Header="Kraj" Width="120" ClipboardContentBinding="{Binding Kraj}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate><TextBlock Text="{Binding Kraj}" Margin="4,0" VerticalAlignment="Center"/></DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox Style="{StaticResource ComboBox.Cell}"
IsEditable="True" IsTextSearchEnabled="True" StaysOpenOnEdit="True"
ItemsSource="{Binding Data.Ciselniky.Kraje, Source={StaticResource Proxy}}"
TextSearch.TextPath="Nazev"
Text="{Binding Kraj, UpdateSourceTrigger=LostFocus}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Katastrální území" Binding="{Binding KatastralniUzemi}" Width="130"/>
<DataGridTextColumn Header="TUDU" Binding="{Binding Tudu}" Width="80"/>
<!-- Jméno rozvržení: pro objektovou přílohu generované a needitovatelné (TextBox IsReadOnly),
jinak ruční text. -->
<DataGridTemplateColumn Header="Jméno rozvržení" Width="150" ClipboardContentBinding="{Binding JmenoRozvrzeni}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate><TextBlock Text="{Binding JmenoRozvrzeni}" Margin="4,0" VerticalAlignment="Center"/></DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding JmenoRozvrzeni, UpdateSourceTrigger=LostFocus}"
IsReadOnly="{Binding JeObjektovaPriloha}"
Style="{StaticResource DataGridCell.Editor.TextBox}"/>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="LTSCALE" Binding="{Binding Ltscale}" Width="80"/>
<DataGridTextColumn Header="CTB" Binding="{Binding Ctb}" Width="120"/>
<DataGridTextColumn Header="Orientační schéma (DXF)" Binding="{Binding SchemaSoubor}" Width="180"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</UserControl>
+143
View File
@@ -0,0 +1,143 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Rozpisky.ViewModels;
namespace Rozpisky.Views;
public partial class SeznamPrilohView : UserControl
{
private MainViewModel? _vm;
// Guard proti smyčce mezi výběrem buněk v gridu a SelectedPriloha ve VM: když jeden směr
// synchronizace běží, druhý se přeskočí, aby se klik do buňky nerozšířil na celý řádek.
private bool _syncing;
// Guard při programovém promítání uložených šířek do sloupců: bez něj by změna šířky každého
// sloupce vyvolala zpětné hlášení do VM s neúplným (mezistavovým) seznamem a „ušpinila" projekt.
private bool _obnovujiSirky;
public SeznamPrilohView()
{
InitializeComponent();
// Ruční roztažení sloupce (přeskládání je zakázané) → nahlásíme šířky do VM, ať se uloží do
// .rzp spolu s daty. Sloupce jsou z XAML, takže existují hned po InitializeComponent.
var deskriptor = DependencyPropertyDescriptor.FromProperty(DataGridColumn.WidthProperty, typeof(DataGridColumn));
foreach (var sloupec in Grid.Columns)
deskriptor.AddValueChanged(sloupec, SloupecSirkaZmenena);
// Náhled už neotvírá dvojklik, ale tlačítko (NahledPrilohyCmd → událost NahledPozadovan).
// Okno otvíráme tady, protože VM nezná Window/Owner. DataContext je při Loaded hotový.
Loaded += (_, _) =>
{
OtmavRohoveTlacitko();
if (DataContext is not MainViewModel vm || ReferenceEquals(vm, _vm)) return;
if (_vm is not null)
{
_vm.NahledPozadovan -= OtevriNahled;
_vm.PropertyChanged -= Vm_PropertyChanged;
_vm.SirkySloupcuObnoveny -= ObnovSirkySloupcu;
}
_vm = vm;
_vm.NahledPozadovan += OtevriNahled;
_vm.PropertyChanged += Vm_PropertyChanged;
_vm.SirkySloupcuObnoveny += ObnovSirkySloupcu;
ObnovSirkySloupcu(); // promítni šířky již načteného projektu
};
}
/// <summary>Šířka sloupce k uložení: hodnota v pixelech (sloupce mají v XAML absolutní šířky, po
/// ručním roztažení zůstávají absolutní), s bezpečným fallbackem na skutečnou šířku.</summary>
private static double SirkaSloupce(DataGridColumn c) => c.Width.IsAbsolute ? c.Width.Value : c.ActualWidth;
/// <summary>Uživatel roztáhl sloupec → pošli aktuální šířky všech sloupců do VM (uloží se s daty).
/// Při programové obnově přeskočíme, ať mezistav nešpiní dirty flag.</summary>
private void SloupecSirkaZmenena(object? sender, EventArgs e)
{
if (_vm is null || _obnovujiSirky) return;
_vm.NastavSirkySloupcuSeznamu(Grid.Columns.Select(SirkaSloupce));
}
/// <summary>Promítne uložené šířky (VM.SirkySloupcuSeznamu) do sloupců gridu. Prázdný seznam
/// (nový projekt / starší .rzp) nechá výchozí šířky z XAML.</summary>
private void ObnovSirkySloupcu()
{
if (_vm is null) return;
var sirky = _vm.SirkySloupcuSeznamu;
_obnovujiSirky = true;
try
{
for (int i = 0; i < Grid.Columns.Count && i < sirky.Count; i++)
Grid.Columns[i].Width = new DataGridLength(sirky[i]);
}
finally { _obnovujiSirky = false; }
}
/// <summary>Přiřadí rohovému „vybrat vše" tlačítku gridu tmavý styl. Systémová šablona DataGridu mu
/// dává vlastní styl (implicitní Button styl ho nepřebije), proto ho hledáme ve vizuálním stromě a
/// styl mu nastavíme přímo. Jediný <see cref="Button"/> ve stromě gridu je právě tenhle roh (buňky
/// mají CheckBox/ComboBox/TextBox, scrollbary RepeatButton), takže stačí najít první.</summary>
private void OtmavRohoveTlacitko()
{
if (Grid.FindResource("DataGrid.CornerButton") is not Style styl) return;
if (NajdiPotomka<Button>(Grid) is { } roh) roh.Style = styl;
}
private static T? NajdiPotomka<T>(DependencyObject koren) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(koren); i++)
{
var d = VisualTreeHelper.GetChild(koren, i);
if (d is T hit) return hit;
if (NajdiPotomka<T>(d) is { } vnoreny) return vnoreny;
}
return null;
}
/// <summary>Se SelectionUnit=CellOrRowHeader vybírá běžný klik/tah/Ctrl+klik buňky, ne celé
/// řádky, takže se dávkový výběr (smazání, export výběru, rozvržení) odvozuje z dotčených řádků
/// SelectedCells, ne z SelectedItems (to plní jen výběr přes hlavičku řádku). Zároveň se odsud
/// dopočítává SelectedPriloha (řádek pro toolbar/náhled) z naposledy přidané buňky; guard brání
/// zpětnému rozšíření kliku na celý řádek přes Vm_PropertyChanged.</summary>
private void Grid_SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
{
if (_vm is null || _syncing) return;
// Vícenásobný výběr řádků pro dávkové akce = všechny řádky dotčené výběrem buněk (distinct).
_vm.NastavVyber(Grid.SelectedCells.Select(c => c.Item).OfType<Models.Priloha>().Distinct());
// Aktivní řádek (toolbar/náhled) čteme z CurrentCell, ne z e.AddedCells: při zúžení
// vícenásobného výběru jedním klikem nemusí nová buňka v AddedCells vůbec být, takže
// SelectedPriloha (a dřív i zvýraznění) „zaostával" o jeden klik. Fallback na AddedCells
// pro případ, že by CurrentCell nemířila na řádek.
var priloha = Grid.CurrentCell.Item as Models.Priloha
?? e.AddedCells.Select(c => c.Item).OfType<Models.Priloha>().FirstOrDefault();
if (priloha is null) return;
_syncing = true;
try { _vm.SelectedPriloha = priloha; }
finally { _syncing = false; }
}
/// <summary>Programová změna výběru z VM (přidání/duplikace/posun řádku) → přesune výběr i aktivní
/// buňku v gridu a odscrolluje na ni. Guardem odfiltruje vlastní změny pocházející z kliku do buňky.</summary>
private void Vm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(MainViewModel.SelectedPriloha) || _syncing) return;
if (_vm?.SelectedPriloha is not { } priloha) return;
_syncing = true;
try
{
Grid.SelectedItem = priloha;
Grid.ScrollIntoView(priloha);
if (Grid.Columns.Count > 0) Grid.CurrentCell = new DataGridCellInfo(priloha, Grid.Columns[0]);
}
finally { _syncing = false; }
}
/// <summary>Otevře náhled rozpisky pro vybranou přílohu (ROZHRANI.md §4.4).</summary>
private void OtevriNahled()
{
if (_vm?.SelectedPriloha is not { } priloha) return;
new NahledWindow(priloha, _vm.Projekt, _vm.Revize.ToList(), _vm.Ciselniky, _vm.Nastaveni.SablonaDxf, _vm.ResolveLoga(priloha))
{ Owner = Window.GetWindow(this) }.ShowDialog();
}
}
+166
View File
@@ -0,0 +1,166 @@
<UserControl x:Class="Rozpisky.Views.UdajeProjektuView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignWidth="700">
<UserControl.Resources>
<Thickness x:Key="Lbl">0,0,10,6</Thickness>
<Thickness x:Key="Inp">0,0,0,6</Thickness>
<Style TargetType="TextBlock" x:Key="FieldLabel" BasedOn="{StaticResource Text.Caption}">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="{StaticResource Lbl}"/>
</Style>
</UserControl.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="16">
<StackPanel MaxWidth="760" HorizontalAlignment="Left">
<TextBlock Text="Údaje projektu" Style="{StaticResource Text.H1}" Margin="0,0,0,4"/>
<TextBlock Style="{StaticResource Text.Caption}"
Text="Hodnoty společné všem rozpiskám projektu. Výběr z číselníku automaticky doplní adresy a kontakty."/>
<!-- Identifikace zakázky -->
<GroupBox Header="IDENTIFIKACE ZAKÁZKY">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Název stavby / akce" Style="{StaticResource FieldLabel}"/>
<TextBox Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="4" Margin="{StaticResource Inp}"
Text="{Binding Projekt.Stavba, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="S-kód (SoD)" Style="{StaticResource FieldLabel}"/>
<TextBox Grid.Row="1" Grid.Column="1" Margin="{StaticResource Inp}"
Text="{Binding Projekt.Skod, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Grid.Row="1" Grid.Column="3" Text="Číslo zakázky" Style="{StaticResource FieldLabel}"/>
<TextBox Grid.Row="1" Grid.Column="4" Margin="{StaticResource Inp}"
Text="{Binding Projekt.CisloZakazky, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Stupeň dokumentace" Style="{StaticResource FieldLabel}"/>
<ComboBox Grid.Row="2" Grid.Column="1" Margin="{StaticResource Inp}"
ItemsSource="{Binding Ciselniky.VerzeDokumentace}"
SelectedItem="{Binding SelectedStupen}"/>
<TextBlock Grid.Row="2" Grid.Column="3" Text="Smluvní datum zpracování" Style="{StaticResource FieldLabel}"/>
<TextBox Grid.Row="2" Grid.Column="4" Margin="{StaticResource Inp}"
Text="{Binding Projekt.SmluvniDatumZpracovani, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</GroupBox>
<!-- Objednatel -->
<GroupBox Header="OBJEDNATEL">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Objednatel" Style="{StaticResource FieldLabel}"/>
<ComboBox Grid.Row="0" Grid.Column="1" Margin="{StaticResource Inp}"
ItemsSource="{Binding Ciselniky.Organizace}"
SelectedItem="{Binding SelectedInvestor}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Adresa objednatele" Style="{StaticResource FieldLabel}"/>
<TextBox Grid.Row="1" Grid.Column="1" Margin="{StaticResource Inp}" IsReadOnly="True"
Text="{Binding Projekt.InvestorAdresa}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Hlavní inženýr projektu" Style="{StaticResource FieldLabel}"/>
<ComboBox Grid.Row="2" Grid.Column="1" Margin="{StaticResource Inp}"
IsEditable="True" IsTextSearchEnabled="True" StaysOpenOnEdit="True"
ItemsSource="{Binding Ciselniky.Jmena}" TextSearch.TextPath="Hodnota"
Text="{Binding Projekt.HIP, UpdateSourceTrigger=LostFocus}"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="Složka SŽ (zástupce)" Style="{StaticResource FieldLabel}"/>
<ComboBox Grid.Row="3" Grid.Column="1" Margin="{StaticResource Inp}"
ItemsSource="{Binding Ciselniky.Organizace}"
SelectedItem="{Binding SelectedSlozkaSZ}"/>
<TextBlock Grid.Row="4" Grid.Column="0" Text="Adresa složky SŽ" Style="{StaticResource FieldLabel}"/>
<TextBox Grid.Row="4" Grid.Column="1" Margin="{StaticResource Inp}" IsReadOnly="True"
Text="{Binding Projekt.SlozkaSZAdresa}"/>
</Grid>
</GroupBox>
<!-- Zhotovitel díla -->
<GroupBox Header="ZHOTOVITEL DÍLA (dle SoD)">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Zhotovitel" Style="{StaticResource FieldLabel}"/>
<ComboBox Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="4" Margin="{StaticResource Inp}"
ItemsSource="{Binding Ciselniky.Firmy}"
SelectedItem="{Binding SelectedZhotovitelSOD}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Adresa" Style="{StaticResource FieldLabel}"/>
<TextBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="4" Margin="{StaticResource Inp}" IsReadOnly="True"
Text="{Binding Projekt.AdresaSOD}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Telefon" Style="{StaticResource FieldLabel}"/>
<TextBox Grid.Row="2" Grid.Column="1" Margin="{StaticResource Inp}" IsReadOnly="True"
Text="{Binding Projekt.TelSOD}"/>
<TextBlock Grid.Row="2" Grid.Column="3" Text="E-mail" Style="{StaticResource FieldLabel}"/>
<TextBox Grid.Row="2" Grid.Column="4" Margin="{StaticResource Inp}" IsReadOnly="True"
Text="{Binding Projekt.EmailSOD}"/>
</Grid>
</GroupBox>
<!-- Loga financujících institucí (stejná pro všechny rozpisky projektu) -->
<GroupBox Header="LOGA FINANCUJÍCÍCH INSTITUCÍ (nepovinné)">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="5" Style="{StaticResource Text.Caption}" Margin="0,0,0,6"
Text="Loga dotačních programů (vlevo) a státních institucí (vpravo). DXF soubory ze složky loga\ vedle programu."/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Logo dotační (vlevo)" Style="{StaticResource FieldLabel}"/>
<ComboBox Grid.Row="1" Grid.Column="1" Margin="{StaticResource Inp}"
IsEditable="True"
ItemsSource="{Binding DostupnaLoga}"
Text="{Binding Projekt.Logo1Soubor, UpdateSourceTrigger=LostFocus}"/>
<TextBlock Grid.Row="1" Grid.Column="3" Text="Logo státní (vpravo)" Style="{StaticResource FieldLabel}"/>
<ComboBox Grid.Row="1" Grid.Column="4" Margin="{StaticResource Inp}"
IsEditable="True"
ItemsSource="{Binding DostupnaLoga}"
Text="{Binding Projekt.Logo2Soubor, UpdateSourceTrigger=LostFocus}"/>
</Grid>
</GroupBox>
</StackPanel>
</ScrollViewer>
</UserControl>
+8
View File
@@ -0,0 +1,8 @@
using System.Windows.Controls;
namespace Rozpisky.Views;
public partial class UdajeProjektuView : UserControl
{
public UdajeProjektuView() => InitializeComponent();
}
+32
View File
@@ -0,0 +1,32 @@
<Window x:Class="Rozpisky.Views.VyberObjektuWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Title="Výběr objektu" SizeToContent="Height" Width="460"
Icon="/Rozpisky.ico"
ResizeMode="NoResize" ShowInTaskbar="False"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource Brush.Window}"
Foreground="{DynamicResource Brush.Text}">
<StackPanel Margin="20">
<TextBlock Style="{StaticResource Text.H2}" Text="Vyberte objekt pro seznam příloh"/>
<TextBlock Style="{StaticResource Text.Caption}" TextWrapping="Wrap" Margin="0,8,0,0"
Text="Seznam příloh se vytvoří pro zvolený objekt a uloží vedle souboru projektu."/>
<TextBlock Text="Objekt" Style="{StaticResource Text.Caption}" Margin="0,12,0,2"/>
<ComboBox x:Name="Combo"/>
<StackPanel x:Name="RevizePanel">
<TextBlock Text="Revize do kódu seznamu" Style="{StaticResource Text.Caption}" Margin="0,12,0,2"/>
<ComboBox x:Name="ComboRevize"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,20,0,0">
<Button Content="Zrušit" Style="{StaticResource Button.Subtle}" IsCancel="True" Click="Zrusit_Click"/>
<Button Content="OK" Style="{StaticResource Button.Primary}" Margin="8,0,0,0"
IsDefault="True" Click="Ok_Click"/>
</StackPanel>
</StackPanel>
</Window>
+45
View File
@@ -0,0 +1,45 @@
using System.Windows;
namespace Rozpisky.Views;
/// <summary>
/// Modální výběr objektu (a volitelně revize) pro seznam příloh. Combobox čísel objektů + OK/Zrušit;
/// při exportu navíc combobox revize do kódu seznamu. Vrací zvolené číslo objektu (<see cref="Vybrane"/>)
/// a revizi (<see cref="VybranaRevize"/>).
/// </summary>
public partial class VyberObjektuWindow : Window
{
/// <summary>Zvolené číslo objektu (null, dokud uživatel nepotvrdí).</summary>
public string? Vybrane { get; private set; }
/// <summary>Zvolená revize do kódu seznamu (null = nebyla nabídnuta / nevybrána).</summary>
public string? VybranaRevize { get; private set; }
public VyberObjektuWindow(IEnumerable<string> cislaObjektu, IEnumerable<string>? revize = null)
{
InitializeComponent();
Combo.ItemsSource = cislaObjektu.ToList();
Combo.SelectedIndex = 0;
var revizeList = revize?.ToList();
if (revizeList is { Count: > 0 })
{
ComboRevize.ItemsSource = revizeList;
ComboRevize.SelectedIndex = 0;
}
else
{
RevizePanel.Visibility = Visibility.Collapsed; // tisk seznamu revizi neřeší
}
}
private void Ok_Click(object sender, RoutedEventArgs e)
{
if (Combo.SelectedItem is not string cislo) return; // nic nevybráno → zůstaň
Vybrane = cislo;
VybranaRevize = ComboRevize.SelectedItem as string;
DialogResult = true;
}
private void Zrusit_Click(object sender, RoutedEventArgs e) => DialogResult = false;
}
+24
View File
@@ -0,0 +1,24 @@
<Window x:Class="Rozpisky.Views.ZamcenySouborWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Title="Soubor je používán" SizeToContent="Height" Width="480"
Icon="/Rozpisky.ico"
ResizeMode="NoResize" ShowInTaskbar="False"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource Brush.Window}"
Foreground="{DynamicResource Brush.Text}">
<StackPanel Margin="20">
<TextBlock Style="{StaticResource Text.H2}" Text="Soubor je používán jiným programem"/>
<TextBlock x:Name="Zprava" Style="{StaticResource Text.Caption}" TextWrapping="Wrap" Margin="0,8,0,0"/>
<TextBlock Style="{StaticResource Text.Caption}" TextWrapping="Wrap" Margin="0,12,0,0"
Text="Zavřete soubor v programu, který ho používá, a zvolte Pokračovat. Přerušit akci zastaví."/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,20,0,0">
<Button Content="Přerušit" Style="{StaticResource Button.Subtle}" IsCancel="True" Click="Prerusit_Click"/>
<Button Content="Pokračovat" Style="{StaticResource Button.Primary}" Margin="8,0,0,0"
IsDefault="True" Click="Pokracovat_Click"/>
</StackPanel>
</StackPanel>
</Window>
@@ -0,0 +1,21 @@
using System.Windows;
namespace Rozpisky.Views;
/// <summary>
/// Modální dotaz při dávkovém exportu, když cílové PDF drží jiný program (prohlížeč PDF).
/// „Pokračovat" (DialogResult = true) = uživatel soubor zavřel, zkus zápis znovu;
/// „Přerušit" (false) = zastav celý export.
/// </summary>
public partial class ZamcenySouborWindow : Window
{
public ZamcenySouborWindow(string cesta)
{
InitializeComponent();
Zprava.Text = $"Soubor nelze přepsat, protože je otevřený v jiném programu:\n{cesta}";
}
private void Pokracovat_Click(object sender, RoutedEventArgs e) => DialogResult = true;
private void Prerusit_Click(object sender, RoutedEventArgs e) => DialogResult = false;
}
+53
View File
@@ -0,0 +1,53 @@
using System.Runtime.InteropServices;
namespace Rozpisky.Xlsx;
/// <summary>
/// Převod .xlsx → PDF přes nainstalovaný MS Excel (COM interop, pozdní vazba přes <c>dynamic</c>,
/// takže není potřeba PIA reference). Výstup vzhledově přesně odpovídá sešitu. Vyhodí výjimku, když
/// Excel není k dispozici nebo převod selže; COM objekty vždy uvolní, aby nezůstal viset Excel.exe.
/// </summary>
public static class ExcelToPdf
{
private const int XlTypePDF = 0;
public static void Convert(string xlsxPath, string pdfPath)
{
var excelType = Type.GetTypeFromProgID("Excel.Application")
?? throw new InvalidOperationException(
"MS Excel není nainstalovaný (COM třída „Excel.Application“ nenalezena).");
dynamic? excel = null, workbooks = null, wb = null;
try
{
excel = Activator.CreateInstance(excelType)
?? throw new InvalidOperationException("MS Excel se nepodařilo spustit.");
excel.Visible = false;
excel.DisplayAlerts = false;
workbooks = excel.Workbooks;
// ReadOnly: sešit se jen čte (export do PDF), a hlavně otevření nekoliduje se sešitem
// zrovna otevřeným v Excelu uživatele.
wb = workbooks.Open(xlsxPath, ReadOnly: true);
wb.ExportAsFixedFormat(XlTypePDF, pdfPath);
}
finally
{
if (wb is not null)
{
// Close ve finally: když ExportAsFixedFormat vyhodí výjimku, sešit se musí zavřít
// i tak, jinak by úklid závisel jen na Quit(). Vlastní try Close nesmí zamaskovat
// původní výjimku ani přeskočit uvolnění COM objektů níže.
try { wb.Close(false); } catch { /* už zavřen / Excel v nečekaném stavu */ }
Marshal.FinalReleaseComObject(wb);
}
if (workbooks is not null) Marshal.FinalReleaseComObject(workbooks);
if (excel is not null)
{
try { excel.Quit(); } catch { /* už ukončen */ }
Marshal.FinalReleaseComObject(excel);
}
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}
+119
View File
@@ -0,0 +1,119 @@
using ClosedXML.Excel;
using Rozpisky.Models;
namespace Rozpisky.Xlsx;
/// <summary>
/// Vyplní mustr „Seznam příloh" (Otevřená verze .xlsx) daty jednoho objektu a uloží jako nový soubor.
/// Zachovává formátování mustru píše jen do buněk a vkládá řádky příloh (ClosedXML). Členění příloh
/// dle manuálu SŽ (kap. 2.3 / 5.3.5): D.1 se dělí na 1/2/3, D.2 a D.3 se uvádějí celé pod hlavní část.
/// </summary>
public static class SeznamExporter
{
// Sloupce mustru (1-based): A=Část (typ), B=Číslo přílohy, C=Název přílohy, E=Dílčí název,
// H=neobsazeno, I=Měřítko, J=Poslední revize.
private const int SlA = 1, SlB = 2, SlC = 3, SlE = 5, SlH = 8, SlI = 9, SlJ = 10;
public static void Export(string mustrPath, string cilPath, Projekt p, string cisloObjektu,
IReadOnlyList<Priloha> prilohyObjektu, string kodSeznamu)
{
using var wb = new XLWorkbook(mustrPath);
var ws = wb.Worksheet(1);
// ── Hlavička (pevné buňky; rozbité externí pojmenované buňky mustru ignorujeme) ──
var nazevObjektu = prilohyObjektu.Select(x => x.NazevObjektu)
.FirstOrDefault(s => !string.IsNullOrWhiteSpace(s)) ?? "";
ws.Cell("A2").Value = nazevObjektu;
ws.Cell("J2").Value = cisloObjektu;
ws.Cell("D7").Value = p.Stavba; // přepíše rozbitý vzorec „=stavba"
ws.Cell("D9").Value = p.Skod;
ws.Cell("F9").Value = p.StupenDokumentace;
if (DateTime.TryParse(p.SmluvniDatumZpracovani, out var datum))
ws.Cell("J9").Value = datum; // zachová formát data v buňce
else
ws.Cell("J9").Value = p.SmluvniDatumZpracovani;
// ── Kódová buňka pod tabulkou: ClosedXML ji při vkládání řádků ztratí, tak si zapamatuj její
// styl (do políčka mimo tabulku, nad vkládáním), buňku v mustru smaž a kód dopíšeme až nakonec.
var stylZachrana = ws.Cell(1, 26); // Z1 nad tabulkou, vkládání řádků (>=12) ji neposune
foreach (var c in ws.Column(SlA).CellsUsed())
if (c.GetString().Contains("_001_", StringComparison.Ordinal))
{
stylZachrana.Style = c.Style;
c.Clear(XLClearOptions.All);
break;
}
// ── Tabulka zdola nahoru, ať vkládané řádky neposouvají ještě nezpracované kotvy ──
VyplnTyp(ws, "D.3", prilohyObjektu.Where(x => Typ(x) == "D3"));
VyplnTyp(ws, "D.2", prilohyObjektu.Where(x => Typ(x) == "D2"));
VyplnTyp(ws, "D.1.3", prilohyObjektu.Where(x => Typ(x) == "D1" && Dilci(x) == "3"));
VyplnTyp(ws, "D.1.2", prilohyObjektu.Where(x => Typ(x) == "D1" && Dilci(x) == "2"));
VyplnTyp(ws, "D.1.1", prilohyObjektu.Where(x => Typ(x) == "D1" && Dilci(x) == "1"));
// ── Výsledný kód do buňky pod tabulkou (vynech jeden prázdný řádek pod posledním řádkem) ──
int kodRadek = (ws.LastRowUsed()?.RowNumber() ?? 11) + 2;
var kodCell = ws.Cell(kodRadek, SlA);
kodCell.Style = stylZachrana.Style;
kodCell.Value = kodSeznamu;
stylZachrana.Clear(XLClearOptions.All); // ukliď pomocné políčko
wb.SaveAs(cilPath);
}
/// <summary>Vyplní jeden typ přílohy (D.1.1 … D.3): každá příloha jde na <b>vlastní nový řádek</b>
/// pod nadpis (i jednodokumentové D.1.1/D.1.3). Prázdný typ → „neobsazeno".</summary>
private static void VyplnTyp(IXLWorksheet ws, string label, IEnumerable<Priloha> prilohy)
{
int anchor = NajdiRadek(ws, label);
if (anchor < 0) return;
var list = prilohy.OrderBy(Poradi).ToList();
if (list.Count == 0)
{
ws.Cell(anchor, SlH).Value = "neobsazeno";
return;
}
ws.Cell(anchor, SlH).Clear(XLClearOptions.Contents); // smaž případné „neobsazeno" z mustru
VlozRadky(ws, anchor, list);
}
/// <summary>Vloží pod nadpisový řádek po jednom řádku na každou přílohu
/// (B=číslo | C=název přílohy | E=dílčí název | I=měřítko | J=revize). Styl (vč. ohraničení) se
/// kopíruje <b>po buňkách</b> z řádku o řádek níž (prázdný „tělový" řádek mustru), ne z nadpisu
/// styl na úroveň celého řádku by u smíšeně formátovaných buněk ohraničení setřel.</summary>
private static void VlozRadky(IXLWorksheet ws, int anchor, IReadOnlyList<Priloha> list)
{
if (list.Count == 0) return;
ws.Row(anchor).InsertRowsBelow(list.Count);
int refRow = anchor + 1 + list.Count; // původní „tělový" řádek pod blokem (má ohraničení)
for (int i = 0; i < list.Count; i++)
{
int r = anchor + 1 + i;
for (int col = SlA; col <= SlJ; col++)
ws.Cell(r, col).Style = ws.Cell(refRow, col).Style; // formát + ohraničení „o řádek níž"
var pr = list[i];
ws.Cell(r, SlB).Value = pr.PoradiPrilohy; // číslo přílohy 001/002 (text → úvodní nuly)
ws.Cell(r, SlC).Value = pr.NazevPrilohy; // název přílohy
ws.Cell(r, SlE).Value = pr.NazevDilciCastiPrilohy; // dílčí název přílohy
ws.Cell(r, SlI).Value = pr.Meritko;
ws.Cell(r, SlJ).Value = Revize(pr);
}
}
/// <summary>Najde řádek typové kotvy podle textu ve sloupci „Část" (A).</summary>
private static int NajdiRadek(IXLWorksheet ws, string label)
{
foreach (var c in ws.Column(SlA).CellsUsed())
if (string.Equals(c.GetString().Trim(), label, StringComparison.OrdinalIgnoreCase))
return c.Address.RowNumber;
return -1;
}
// ── pole přílohy ─────────────────────────────────────────────────────────
private static string Typ(Priloha p) => ObjektOznaceni.Alfanum(p.TypPrilohy).ToUpperInvariant(); // „D.1" → „D1"
private static string Dilci(Priloha p) => (p.DilciPriloha ?? "").Trim();
private static int Poradi(Priloha p) => int.TryParse(p.PoradiPrilohy, out var n) ? n : 0;
private static string Revize(Priloha p) => (p.R1 ?? "").Trim();
}
+347
View File
@@ -0,0 +1,347 @@
{
"Organizace": [
{
"Nazev": "Správa železnic, státní organizace",
"Adresa": "Dlážděná 1003/7, 110 00 Praha 1",
"Logo": "sprava_zeleznic.dxf"
},
{
"Nazev": "Stavební správa východ",
"Adresa": "Nerudova 1, 779 00 Olomouc",
"Logo": ""
},
{
"Nazev": "Stavební správa západ",
"Adresa": "Ke Štvanici 656/3, 186 00 Praha 8",
"Logo": ""
},
{
"Nazev": "Správa železniční geodézie",
"Adresa": "Václavkova 169/1, 160 00 Praha 6",
"Logo": ""
},
{
"Nazev": "Oblastní ředitelství Brno",
"Adresa": "Kounicova 26, 611 43 Brno",
"Logo": ""
},
{
"Nazev": "Oblastní ředitelství Hradec Králové",
"Adresa": "U Fotochemy 259, 501 01 Hradec Králové",
"Logo": ""
},
{
"Nazev": "Oblastní ředitelství Olomouc",
"Adresa": "Nerudova 773/1, 779 00 Olomouc",
"Logo": ""
},
{
"Nazev": "Oblastní ředitelství Plzeň",
"Adresa": "Sušická 1168/23, 326 00 Plzeň",
"Logo": ""
},
{
"Nazev": "Oblastní ředitelství Praha",
"Adresa": "Partyzánská 24, 170 00 Praha 7",
"Logo": ""
},
{
"Nazev": "Oblastní ředitelství Ústí nad Labem",
"Adresa": "Železničářská 1386/31, 400 03 Ústí nad Labem",
"Logo": ""
}
],
"Firmy": [
{
"Nazev": "EXprojekt s.r.o.",
"Adresa": "Heršpická 758/13, 619 00 Brno",
"Telefon": "+420 533 312 000",
"Email": "info@exprojekt.cz",
"Logo": "exprojekt.dxf"
},
{
"Nazev": "Signal Projekt s.r.o.",
"Adresa": "Vídeňská 55, 639 00 Brno",
"Telefon": "+420 543 233 962",
"Email": "projekce@signalprojekt.cz",
"Logo": "signalprojekt.dxf"
},
{
"Nazev": "MORAVIA CONSULT Olomouc a.s.",
"Adresa": "Legionářská 1085/8, 779 00 Olomouc",
"Telefon": "+420 585 570 444",
"Email": "moravia@moravia.cz",
"Logo": "mco.dxf"
},
{
"Nazev": "TESIA speciální technické práce s.r.o.",
"Adresa": "Luční 2435/17, Žabovřesky, 616 00 Brno",
"Telefon": "+420 739 573 422",
"Email": "sekretariat@tesia.cz",
"Logo": "tesia.dxf"
},
{
"Nazev": "SUDOP BRNO, spol. s r.o.",
"Adresa": "Kounicova 26, 611 36 Brno",
"Telefon": "+420 972 625 804",
"Email": "sudop@sudop-brno.cz",
"Logo": "sudopBrno.dxf"
},
{
"Nazev": "GEOSTAR, spol. s r. o.",
"Adresa": "Tuřanka 240/111, Slatina, 627 00 Brno",
"Telefon": "+420 545 221 218",
"Email": "geostar@geostar.cz",
"Logo": ""
},
{
"Nazev": "Společnost pro Al Če",
"Adresa": "EXprojekt s.r.o., Heršpická 758/13, 619 00 Brno",
"Telefon": "+420 533 312 000",
"Email": "info@exprojekt.cz",
"Logo": "exprojekt+mco.dxf"
},
{
"Nazev": "Ecological Consulting a.s.",
"Adresa": "Legionářská 1085/8, 779 00 Olomouc",
"Telefon": "+420 720 856 269",
"Email": "ecological@ecological.cz",
"Logo": ""
},
{
"Nazev": "Výzkumný Ústav Železniční, a.s.",
"Adresa": "Novodvorská 1698/138b, 142 00 Praha",
"Telefon": "+420 241 493 135",
"Email": "marketing@cdvuz.cz",
"Logo": ""
},
{
"Nazev": "Výskumný ústav dopravný, a.s.",
"Adresa": "Rosinská cesta 3339/9, 010 08 Žilina, Slovensko",
"Telefon": "+421 415 686 111",
"Email": "info@vud.sk",
"Logo": ""
}
],
"Jmena": [
{
"Hodnota": "Bc. Jan Cabal"
},
{
"Hodnota": "Ing. David Rose"
},
{
"Hodnota": "Ing. Dominik Mojžíšek"
},
{
"Hodnota": "Ing. Ivan Drajčík"
},
{
"Hodnota": "Ing. Ivana Havlíková, Ph.D."
},
{
"Hodnota": "Ing. Jan Maleňák"
},
{
"Hodnota": "Ing. Jaroslav Šmíd"
},
{
"Hodnota": "Ing. Josef Marek"
},
{
"Hodnota": "Ing. Kamil Dvořák"
},
{
"Hodnota": "Ing. Kateřina Koudelová"
},
{
"Hodnota": "Ing. Lucie Puczoková"
},
{
"Hodnota": "Ing. Martin Chaloupka"
},
{
"Hodnota": "Ing. Martina Bolješiková"
},
{
"Hodnota": "Ing. Monika Buganová"
},
{
"Hodnota": "Ing. Ondřej Čech"
},
{
"Hodnota": "Ing. Pavel Odehnal"
},
{
"Hodnota": "Ing. Pavlína Vondrušková"
},
{
"Hodnota": "Ing. Petr Libosvár"
},
{
"Hodnota": "Ing. Radek Šíp"
},
{
"Hodnota": "Ing. Róbert Tomov"
},
{
"Hodnota": "Ing. Roman König"
},
{
"Hodnota": "Ing. Sára Sobková"
},
{
"Hodnota": "Ing. Stanislav Sabo"
},
{
"Hodnota": "Ing. Tomáš Pospíšil"
},
{
"Hodnota": "Ing. Vojtěch Bureš"
},
{
"Hodnota": "Jaroslav Soldátek"
},
{
"Hodnota": "Jaroslava Urbánková"
},
{
"Hodnota": "Mgr. Martina Fialová, Ph.D."
},
{
"Hodnota": "Mgr. Zuzana Indráková"
},
{
"Hodnota": "Ing. Arch. Zuzana Solničková"
},
{
"Hodnota": "Ing. Monika Hajduk"
},
{
"Hodnota": "Mgr. Josef Víšek"
},
{
"Hodnota": "Mgr. Michaela Adamcová"
},
{
"Hodnota": "Ing. Dominik Mojžíšek, Ing. Petr Libosvár"
},
{
"Hodnota": "Ing. Petr Čech"
},
{
"Hodnota": "Mgr. Michal Lachman"
},
{
"Hodnota": "dle příloh"
},
{
"Hodnota": "Bc. Jiří Tuscher"
},
{
"Hodnota": "Ing. Jaromír Cápal"
}
],
"VerzeDokumentace": [
{
"Hodnota": "DOS"
},
{
"Hodnota": "DUR"
},
{
"Hodnota": "DSP"
},
{
"Hodnota": "PDPS"
},
{
"Hodnota": "DPS"
},
{
"Hodnota": "RDS"
}
],
"Casti": [
{
"Oznaceni": "A",
"Nazev": "Průvodní list"
},
{
"Oznaceni": "B",
"Nazev": "Souhrnná technická zpráva"
},
{
"Oznaceni": "C",
"Nazev": "Situační výkresy"
},
{
"Oznaceni": "D",
"Nazev": "Dokumentace objektů"
},
{
"Oznaceni": "E",
"Nazev": "Dokladová část pro správní řízení"
},
{
"Oznaceni": "N",
"Nazev": "Doklady objednatele"
},
{
"Oznaceni": "O",
"Nazev": "Fyzická ochrana objektů"
},
{
"Oznaceni": "P",
"Nazev": "Podklady pro vypracování dokumentace"
},
{
"Oznaceni": "R",
"Nazev": "Náklady stavby"
}
],
"Kraje": [
{
"Nazev": "Hlavní město Praha"
},
{
"Nazev": "Středočeský"
},
{
"Nazev": "Jihočeský"
},
{
"Nazev": "Plzeňský"
},
{
"Nazev": "Karlovarský"
},
{
"Nazev": "Ústecký"
},
{
"Nazev": "Liberecký"
},
{
"Nazev": "Královéhradecký"
},
{
"Nazev": "Pardubický"
},
{
"Nazev": "Kraj Vysočina"
},
{
"Nazev": "Jihomoravský"
},
{
"Nazev": "Olomoucký"
},
{
"Nazev": "Zlínský"
},
{
"Nazev": "Moravskoslezský"
}
]
}
+18
View File
@@ -0,0 +1,18 @@
Loga dodávaná s programem
=========================
Do této složky patří DXF soubory log, která se vkládají do rozpisky.
Použití:
- Loga financujících institucí (dotační/státní) se vybírají v aplikaci na kartě
„Údaje projektu“ nabídka obsahuje DXF soubory z této složky.
- Loga firem/organizací (zhotovitel díla, zhotovitel objektu, investor) se přiřadí
v „Číselnících“ vyplněním sloupce „Logo (soubor)“ = jméno DXF souboru z této složky.
Formát:
- Soubor je obyčejný DXF s vektorovou kresbou loga (čáry, polylinie, texty).
- Logo se do vymezeného rámečku v rozpisce automaticky zmenší (uniformně) a vystředí,
takže na konkrétní velikosti kresby v DXF nezáleží rozhoduje jen poměr stran.
Orientační schéma je naopak unikátní pro každou přílohu a NEUKLÁDÁ se sem
vybírá se přímo u řádku v „Seznamu příloh“ (tlačítko „Schéma…“) a leží vedle .rzp.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+16738
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More