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>
143 lines
6.4 KiB
C#
143 lines
6.4 KiB
C#
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);
|
||
}
|
||
}
|