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>
221 lines
11 KiB
C#
221 lines
11 KiB
C#
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 */ }
|
||
}
|
||
}
|