Switch v Replay neměl default větev: INSERT, ELLIPSE, POINT, SOLID, 3DFACE a POLYLINE3D propadly bez akce a bez záznamu. V dodávaných souborech žádná z nich není, ale uživatel si přidává vlastní loga – to nejhorší, co se mu může stát, je logo, které se nevykreslí a nikdo mu to neřekne. - INSERT se rozvine rekurzivně (vztažný bod, měřítko os, natočení), s ochranou proti bloku vkládajícímu sám sebe. Uvnitř bloku se hladina „0" a barva ByBlock berou od vkládající entity – proto jde stejný blok vložit dvakrát a pokaždé v jiné barvě. - TransformRenderer umí celou afinní transformaci, ne jen měřítko a posun. Oblouk zůstane obloukem jen u podobnosti bez zrcadlení; jinak by z kružnice byla elipsa, kterou kreslicí rozhraní nezná, tak se rozloží na lomenou čáru. - ELLIPSE, POINT, SOLID, 3DFACE, POLYLINE3D se kreslí. U SOLID pozor na prohozený 3. a 4. roh, jinak vyjdou přesýpací hodiny. - Zbytek se počítá do DxfTemplate.NezpracovaneEntity a souhrn exportu (PDF i DXF) ho vypíše. Export se kvůli tomu nezastaví. - Spline se dělí podle délky (dřív pevných 64 dílků – málo pro dlouhé křivky, zbytečně moc pro krátké). - Verze výstupního DXF se zafixovala na AC1027 (jako zdrojová šablona). Dřív ji určoval default ACadSharp, který může upgrade balíčku tiše změnit. Šablona i všech 9 log projdou s prázdným počítadlem (test). Render log do PNG je pixel po pixelu shodný s předchozím stavem. 114 testů zelených. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
114 lines
6.1 KiB
C#
114 lines
6.1 KiB
C#
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
|
||
});
|
||
|
||
/// <summary>Prvky vložených DXF (log, schémat), které kreslítko neumí – po jednom řádku na
|
||
/// soubor, např. <c>„nase_logo.dxf: DIMENSION ×3, LEADER ×1"</c>. Prázdné = vykreslí se všechno.
|
||
/// <para>Uživatel si přidává vlastní loga; bez tohohle by v nich chybějící prvky prostě zmizely
|
||
/// a nikdo by se to nedozvěděl. Nekreslí se nic – kresba jde do <see cref="BoundsRenderer"/>,
|
||
/// jen aby se sesbírala statistika.</para></summary>
|
||
public static IReadOnlyList<string> NepodporovanePrvky(IEnumerable<string> cestyKFragmentum)
|
||
{
|
||
var hlaseni = new List<string>();
|
||
foreach (var cesta in cestyKFragmentum.Where(c => !string.IsNullOrWhiteSpace(c)).Distinct(StringComparer.OrdinalIgnoreCase))
|
||
{
|
||
var frag = NactiFragment(cesta);
|
||
if (frag is null) continue;
|
||
|
||
frag.Replay(new BoundsRenderer());
|
||
if (frag.NezpracovaneEntity.Count == 0) continue;
|
||
|
||
var vypis = string.Join(", ", frag.NezpracovaneEntity.OrderBy(p => p.Key).Select(p => $"{p.Key} ×{p.Value}"));
|
||
hlaseni.Add($"{Path.GetFileName(cesta)}: {vypis}");
|
||
}
|
||
return hlaseni;
|
||
}
|
||
|
||
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);
|
||
}
|