Files
Rozpisky/Rozpisky/Cad/CadTiskService.cs
T
marekandClaude Opus 5 3c0e30bf35 Uživatelská data do %APPDATA%\Rozpisky místo vedle .exe
Vše, co píše uživatel, míří přes novou třídu UzivatelskaData do profilu
uživatele: cad.json, layout.json, error.log, tisk_diag.log. Dosud se
zapisovalo vedle .exe, takže .exe v Program Files (složka jen pro čtení)
nebo přeinstalace novou verzí uživatele o data připravily.

Dodávané podklady (šablona, mustr, loga, mapovani.json) se dál čtou vedle
.exe – ty se naopak při každé instalaci obnovují. Hranice mezi tím, co se
instaluje, a tím, co patří uživateli, vede přesně tudy.

Portable režim zůstává: soubor portable.txt vedle .exe vrátí staré chování
(běh z USB, vývoj s jednou složkou).

Migrace: Migrace.PrenesStaraData() při startu přenese cad.json a layout.json
ze staré polohy, pokud v cíli ještě nejsou; originál označí příponou
.migrovano. Nic se nemaže a nic novějšího se nepřepíše.

Číselníky přijdou na řadu samostatně (rozpad na dodávanou a uživatelskou
vrstvu).

Build 0 chyb / 0 varování, 36 testů zelených.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 10:39:25 +02:00

221 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = Data.UzivatelskaData.Cesta("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>
/// v uživatelské složce.
/// 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(Data.UzivatelskaData.Cesta("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 */ }
}
}