Files
Rozpisky/Rozpisky/Dxf/RozpiskaRenderer.cs
T
marekandClaude Opus 5 a900acc7bb Entity: rozvinout bloky, doplnit elipsy a hlásit, co program neumí
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>
2026-09-07 14:46:02 +02:00

114 lines
6.1 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.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);
}