Šrafy: rozvinout vzor na čáry místo plné výplně
Každá šrafa se kreslila jako plná výplň, bez ohledu na vzor. Logo md_sfdi.dxf má 3× ANSI31 (šikmé čáry) – vycházel z něj plný blok. Nový SrafaVzor rozvine definiční čáry vzoru na úsečky: rodina rovnoběžek podle vektoru Offset, ořezaná hranicemi šrafy sudo-lichou paritou (takže díry ve vnořených smyčkách vyjdou správně) a čárkováním z DashLengths. Proti zamrznutí náhledu při vadném měřítku ve zdroji je strop 20 000 úseček na šrafu. Rozhraní: Fill(loops) dostává druhý parametr Vypln. Rastrové renderery kreslí rozvinuté čáry (Skia vzory neumí), DXF export je ignoruje a zapíše zpátky HATCH s původním jménem vzoru, měřítkem a úhlem – v CADu tak zůstane editovatelný. Když se vzor rozvinout nepodaří, plocha zůstane prázdná; zaplácnout ji načerno je horší než ji nevyplnit. Ověřeno i vizuálně: z 9 dodávaných log se změnilo jen md_sfdi, ostatních 8 je po renderu do PNG pixel po pixelu shodných s předchozím stavem. 105 testů zelených. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,9 +50,13 @@ public sealed class RecordingRenderer : IProfileRenderer
|
|||||||
TextStyle style = default, double widthFactor = 1, double maxWidthMm = 0)
|
TextStyle style = default, double widthFactor = 1, double maxWidthMm = 0)
|
||||||
=> Texts.Add(new TextCall(x, y, text, heightMm, h, v, rotationDeg, style, widthFactor, maxWidthMm));
|
=> Texts.Add(new TextCall(x, y, text, heightMm, h, v, rotationDeg, style, widthFactor, maxWidthMm));
|
||||||
|
|
||||||
public void Fill(IReadOnlyList<Contour> loops)
|
/// <summary>Druh výplně u jednotlivých volání Fill (plná / vzor a jeho čáry).</summary>
|
||||||
|
public List<Vypln> Vyplne { get; } = new();
|
||||||
|
|
||||||
|
public void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default)
|
||||||
{
|
{
|
||||||
FillContours.Add(loops);
|
FillContours.Add(loops);
|
||||||
|
Vyplne.Add(vypln);
|
||||||
Fills.Add(loops.Select(Zplosti).ToList());
|
Fills.Add(loops.Select(Zplosti).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
using Rozpisky.Dxf;
|
||||||
|
using Rozpisky.Rendering;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Rozpisky.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Šrafy se vzorem. Logo <c>md_sfdi.dxf</c> má 3× vzor ANSI31 (šikmé čáry); dřív se každá
|
||||||
|
/// šrafa kreslila jako plná výplň, takže z něj byl v náhledu, PNG, PDF i DXF černý blok.
|
||||||
|
/// </summary>
|
||||||
|
public class SrafaVzorTesty
|
||||||
|
{
|
||||||
|
private static RecordingRenderer Prehraj(string logo)
|
||||||
|
{
|
||||||
|
var g = new RecordingRenderer();
|
||||||
|
new DxfTemplate(TestData.Logo(logo)).Replay(g);
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ansi31SeRozvineNaCary()
|
||||||
|
{
|
||||||
|
var g = Prehraj("md_sfdi.dxf");
|
||||||
|
var vzorovane = g.Vyplne.Where(v => !v.JeSolid).ToList();
|
||||||
|
|
||||||
|
Assert.Equal(3, vzorovane.Count);
|
||||||
|
foreach (var v in vzorovane)
|
||||||
|
{
|
||||||
|
Assert.Equal("ANSI31", v.Vzor);
|
||||||
|
Assert.NotNull(v.Cary);
|
||||||
|
Assert.NotEmpty(v.Cary!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CaryLeziUvnitrObrysuSrafy()
|
||||||
|
{
|
||||||
|
var g = Prehraj("md_sfdi.dxf");
|
||||||
|
for (int i = 0; i < g.Vyplne.Count; i++)
|
||||||
|
{
|
||||||
|
if (g.Vyplne[i].JeSolid) continue;
|
||||||
|
var (minX, minY, maxX, maxY) = Bbox(g.Fills[i]);
|
||||||
|
foreach (var u in g.Vyplne[i].Cary!)
|
||||||
|
{
|
||||||
|
Assert.InRange(u.X1, minX - 0.01, maxX + 0.01);
|
||||||
|
Assert.InRange(u.Y1, minY - 0.01, maxY + 0.01);
|
||||||
|
Assert.InRange(u.X2, minX - 0.01, maxX + 0.01);
|
||||||
|
Assert.InRange(u.Y2, minY - 0.01, maxY + 0.01);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CaryAnsi31JsouRovnobezneA45Stupnu()
|
||||||
|
{
|
||||||
|
var g = Prehraj("md_sfdi.dxf");
|
||||||
|
var cary = g.Vyplne.Where(v => !v.JeSolid).SelectMany(v => v.Cary!).ToList();
|
||||||
|
Assert.NotEmpty(cary);
|
||||||
|
|
||||||
|
foreach (var u in cary)
|
||||||
|
{
|
||||||
|
double dx = u.X2 - u.X1, dy = u.Y2 - u.Y1;
|
||||||
|
if (Math.Abs(dx) + Math.Abs(dy) < 1e-9) continue;
|
||||||
|
// ANSI31 je 45° (v tomhle logu 45,007° – tak to má zapsané zdroj).
|
||||||
|
double uhel = Math.Atan2(dy, dx) * 180 / Math.PI;
|
||||||
|
Assert.InRange(uhel, 44.9, 45.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlnaSrafaZustavaPlna()
|
||||||
|
{
|
||||||
|
// Ostatní loga mají výhradně SOLID – nesmí se z nich stát čárová šrafa.
|
||||||
|
var g = Prehraj("exprojekt.dxf");
|
||||||
|
Assert.NotEmpty(g.Vyplne);
|
||||||
|
Assert.All(g.Vyplne, v =>
|
||||||
|
{
|
||||||
|
Assert.True(v.JeSolid);
|
||||||
|
Assert.Null(v.Cary);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VychoziVyplnJePlna()
|
||||||
|
{
|
||||||
|
// Pojistka: `default(Vypln)` musí být plná výplň, jinak by volání Fill(loops) bez
|
||||||
|
// druhého parametru přestalo cokoli kreslit.
|
||||||
|
Assert.True(default(Vypln).JeSolid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BezDefiniceVzoruSeNevrationZadneCary()
|
||||||
|
{
|
||||||
|
var ctverec = new[]
|
||||||
|
{
|
||||||
|
new Contour(new[]
|
||||||
|
{
|
||||||
|
Edge.Line(0, 0, 10, 0), Edge.Line(10, 0, 10, 10),
|
||||||
|
Edge.Line(10, 10, 0, 10), Edge.Line(0, 10, 0, 0),
|
||||||
|
}, Closed: true),
|
||||||
|
};
|
||||||
|
Assert.Empty(SrafaVzor.Cary(null, ctverec));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (double MinX, double MinY, double MaxX, double MaxY) Bbox(
|
||||||
|
IReadOnlyList<IReadOnlyList<(double X, double Y)>> smycky)
|
||||||
|
{
|
||||||
|
double minX = double.MaxValue, minY = double.MaxValue, maxX = double.MinValue, maxY = double.MinValue;
|
||||||
|
foreach (var s in smycky)
|
||||||
|
foreach (var (x, y) in s)
|
||||||
|
{
|
||||||
|
if (x < minX) minX = x;
|
||||||
|
if (y < minY) minY = y;
|
||||||
|
if (x > maxX) maxX = x;
|
||||||
|
if (y > maxY) maxY = y;
|
||||||
|
}
|
||||||
|
return (minX, minY, maxX, maxY);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -128,7 +128,7 @@ public sealed partial class DxfTemplate
|
|||||||
case Hatch hatch:
|
case Hatch hatch:
|
||||||
{
|
{
|
||||||
var loops = HatchSmycky(hatch);
|
var loops = HatchSmycky(hatch);
|
||||||
if (loops.Count > 0) g.Fill(loops);
|
if (loops.Count > 0) g.Fill(loops, VyplnZ(hatch, loops));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +242,21 @@ public sealed partial class DxfTemplate
|
|||||||
return (plain, wf);
|
return (plain, wf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Druh výplně pro HATCH. Plná (SOLID) projde beze změny; u vzoru se vzor rozvine
|
||||||
|
/// na čáry pro rastrové renderery, ale jméno vzoru, měřítko a úhel se nesou dál, aby DXF
|
||||||
|
/// export zapsal zase editovatelnou šrafu. Když se vzor rozvinout nepodaří, plocha zůstane
|
||||||
|
/// prázdná – dřív se v takovém případě kreslila plná výplň a z logotypu byl černý blok.</summary>
|
||||||
|
private static Vypln VyplnZ(Hatch hatch, IReadOnlyList<Contour> smycky)
|
||||||
|
{
|
||||||
|
string vzor = hatch.Pattern?.Name ?? "SOLID";
|
||||||
|
if (hatch.IsSolid || string.IsNullOrWhiteSpace(vzor)
|
||||||
|
|| vzor.Equals("SOLID", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return Vypln.Vzorovana(vzor, hatch.PatternScale, hatch.PatternAngle * RadToDeg,
|
||||||
|
SrafaVzor.Cary(hatch.Pattern, smycky));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Popis hladiny pro kreslítko – celý, ať se dá ve výstupu postavit znovu
|
/// <summary>Popis hladiny pro kreslítko – celý, ať se dá ve výstupu postavit znovu
|
||||||
/// (barva z tabulky hladin, tloušťka, typ čáry, příznak tisku).</summary>
|
/// (barva z tabulky hladin, tloušťka, typ čáry, příznak tisku).</summary>
|
||||||
private static HladinaStav HladinaZ(ACadSharp.Tables.Layer? layer)
|
private static HladinaStav HladinaZ(ACadSharp.Tables.Layer? layer)
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
using ACadSharp.Entities;
|
||||||
|
using Rozpisky.Rendering;
|
||||||
|
|
||||||
|
namespace Rozpisky.Dxf;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rozvine vzor šrafy (ANSI31 a spol.) na čáry. Rastrové renderery vzory neumí – bez tohohle
|
||||||
|
/// se každá šrafa kreslila jako plná výplň a z logotypu <c>md_sfdi.dxf</c> byl černý blok.
|
||||||
|
/// Do DXF se zapisuje původní <c>HATCH</c> se vzorem, tyhle čáry jsou jen pro kresbu.
|
||||||
|
/// </summary>
|
||||||
|
internal static class SrafaVzor
|
||||||
|
{
|
||||||
|
/// <summary>Strop počtu čar na jednu šrafu. Vadné měřítko ve zdroji (nebo šrafa přes celý
|
||||||
|
/// výkres) by jinak vygenerovalo statisíce úseček a náhled by zamrzl.</summary>
|
||||||
|
private const int MaxCar = 20_000;
|
||||||
|
|
||||||
|
/// <summary>Jemnost, s jakou se hranice šrafy zplošťují na mnohoúhelník (mm).</summary>
|
||||||
|
private const double Sagitta = 0.02;
|
||||||
|
|
||||||
|
/// <summary>Čáry vzoru uvnitř zadaných smyček. Prázdné, když vzor není použitelný –
|
||||||
|
/// volající pak plochu nechá bez výplně (lepší než ji zaplácnout načerno).</summary>
|
||||||
|
public static IReadOnlyList<Usecka> Cary(HatchPattern? vzor, IReadOnlyList<Contour> smycky)
|
||||||
|
{
|
||||||
|
var vysledek = new List<Usecka>();
|
||||||
|
if (vzor?.Lines is not { Count: > 0 }) return vysledek;
|
||||||
|
|
||||||
|
var polygony = Zplosti(smycky);
|
||||||
|
if (polygony.Count == 0) return vysledek;
|
||||||
|
|
||||||
|
var (minX, minY, maxX, maxY) = Rozsah(polygony);
|
||||||
|
if (maxX - minX <= 0 || maxY - minY <= 0) return vysledek;
|
||||||
|
|
||||||
|
foreach (var cara in vzor.Lines)
|
||||||
|
{
|
||||||
|
PridejCaryVzoru(cara, polygony, minX, minY, maxX, maxY, vysledek);
|
||||||
|
if (vysledek.Count >= MaxCar) break;
|
||||||
|
}
|
||||||
|
return vysledek;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Jedna definiční čára vzoru = celá rodina rovnoběžek. Posun mezi sousedními
|
||||||
|
/// rovnoběžkami je vektor <c>Offset</c> ve světových souřadnicích: jeho kolmá složka určuje
|
||||||
|
/// rozteč, podélná posouvá čárkování. U ANSI31 vychází přesně kolmý na směr 45°.</summary>
|
||||||
|
private static void PridejCaryVzoru(
|
||||||
|
HatchPattern.Line cara, List<List<(double X, double Y)>> polygony,
|
||||||
|
double minX, double minY, double maxX, double maxY, List<Usecka> vysledek)
|
||||||
|
{
|
||||||
|
double dx = Math.Cos(cara.Angle), dy = Math.Sin(cara.Angle); // směr čáry
|
||||||
|
double nx = -dy, ny = dx; // kolmice
|
||||||
|
|
||||||
|
double ox = cara.Offset.X, oy = cara.Offset.Y;
|
||||||
|
double kolma = ox * nx + oy * ny; // rozteč rovnoběžek
|
||||||
|
if (Math.Abs(kolma) < 1e-9) return; // rovnoběžky by splynuly
|
||||||
|
|
||||||
|
// Kolik kroků pokryje bbox: promítni jeho rohy na kolmici.
|
||||||
|
double zaklad = cara.BasePoint.X * nx + cara.BasePoint.Y * ny;
|
||||||
|
var projekce = new[]
|
||||||
|
{
|
||||||
|
minX * nx + minY * ny, maxX * nx + minY * ny,
|
||||||
|
minX * nx + maxY * ny, maxX * nx + maxY * ny,
|
||||||
|
};
|
||||||
|
double kOd = (projekce.Min() - zaklad) / kolma;
|
||||||
|
double kDo = (projekce.Max() - zaklad) / kolma;
|
||||||
|
if (kOd > kDo) (kOd, kDo) = (kDo, kOd);
|
||||||
|
|
||||||
|
int od = (int)Math.Floor(kOd) - 1, doK = (int)Math.Ceiling(kDo) + 1;
|
||||||
|
if ((long)doK - od > MaxCar) return; // nesmyslně husté měřítko
|
||||||
|
|
||||||
|
var carky = Carky(cara.DashLengths);
|
||||||
|
|
||||||
|
for (int k = od; k <= doK; k++)
|
||||||
|
{
|
||||||
|
double px = cara.BasePoint.X + k * ox, py = cara.BasePoint.Y + k * oy;
|
||||||
|
foreach (var (t1, t2) in Uvnitr(polygony, px, py, dx, dy))
|
||||||
|
foreach (var (u1, u2) in Orez(carky, t1, t2))
|
||||||
|
{
|
||||||
|
vysledek.Add(new Usecka(px + dx * u1, py + dy * u1, px + dx * u2, py + dy * u2));
|
||||||
|
if (vysledek.Count >= MaxCar) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Úseky přímky (parametr <c>t</c> od bodu <c>p</c> ve směru <c>d</c>), které leží
|
||||||
|
/// uvnitř oblasti. Sudo-lichá parita jako u výplně: mezi 1. a 2. průsečíkem je uvnitř,
|
||||||
|
/// mezi 2. a 3. venku – díky tomu vyjdou i díry ve vnořených smyčkách.</summary>
|
||||||
|
private static List<(double Od, double Do)> Uvnitr(
|
||||||
|
List<List<(double X, double Y)>> polygony, double px, double py, double dx, double dy)
|
||||||
|
{
|
||||||
|
var pruseciky = new List<double>();
|
||||||
|
foreach (var polygon in polygony)
|
||||||
|
for (int i = 0; i < polygon.Count; i++)
|
||||||
|
{
|
||||||
|
var a = polygon[i];
|
||||||
|
var b = polygon[(i + 1) % polygon.Count];
|
||||||
|
// Znaménko kolmé vzdálenosti vrcholu od přímky.
|
||||||
|
double sa = (a.X - px) * -dy + (a.Y - py) * dx;
|
||||||
|
double sb = (b.X - px) * -dy + (b.Y - py) * dx;
|
||||||
|
if (sa == sb) continue; // hrana rovnoběžná s přímkou
|
||||||
|
double podil = sa / (sa - sb);
|
||||||
|
// Polouzavřený interval: vrchol sdílený dvěma hranami se započítá jen jednou,
|
||||||
|
// jinak by parita na něm přeskočila a zbytek řádku vyšel obráceně.
|
||||||
|
if (podil <= 0 || podil > 1) continue;
|
||||||
|
double x = a.X + (b.X - a.X) * podil, y = a.Y + (b.Y - a.Y) * podil;
|
||||||
|
pruseciky.Add((x - px) * dx + (y - py) * dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
pruseciky.Sort();
|
||||||
|
var useky = new List<(double, double)>();
|
||||||
|
for (int i = 0; i + 1 < pruseciky.Count; i += 2)
|
||||||
|
if (pruseciky[i + 1] - pruseciky[i] > 1e-9)
|
||||||
|
useky.Add((pruseciky[i], pruseciky[i + 1]));
|
||||||
|
return useky;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Čárkování jedné periody: intervaly, které se kreslí. <c>null</c> = plná čára.
|
||||||
|
/// Nula v definici znamená tečku – kreslí se jako nepatrný úsek.</summary>
|
||||||
|
private static (double Perioda, List<(double Od, double Do)> Kresli)? Carky(IEnumerable<double>? dashe)
|
||||||
|
{
|
||||||
|
var delky = dashe?.ToList();
|
||||||
|
if (delky is not { Count: > 0 }) return null;
|
||||||
|
|
||||||
|
double perioda = delky.Sum(Math.Abs);
|
||||||
|
if (perioda <= 1e-9) return null;
|
||||||
|
|
||||||
|
var kresli = new List<(double, double)>();
|
||||||
|
double t = 0;
|
||||||
|
foreach (var d in delky)
|
||||||
|
{
|
||||||
|
double l = Math.Abs(d) < 1e-9 ? 1e-6 : Math.Abs(d);
|
||||||
|
if (d >= 0) kresli.Add((t, t + l));
|
||||||
|
t += l;
|
||||||
|
}
|
||||||
|
return kresli.Count == 0 ? null : (perioda, kresli);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ořeže úsek přímky čárkováním. Bez čárkování projde beze změny.</summary>
|
||||||
|
private static IEnumerable<(double Od, double Do)> Orez(
|
||||||
|
(double Perioda, List<(double Od, double Do)> Kresli)? carky, double t1, double t2)
|
||||||
|
{
|
||||||
|
if (carky is not { } c) { yield return (t1, t2); yield break; }
|
||||||
|
|
||||||
|
long prvni = (long)Math.Floor(t1 / c.Perioda);
|
||||||
|
long posledni = (long)Math.Ceiling(t2 / c.Perioda);
|
||||||
|
if (posledni - prvni > MaxCar) { yield return (t1, t2); yield break; } // nesmyslně krátká perioda
|
||||||
|
|
||||||
|
for (long i = prvni; i <= posledni; i++)
|
||||||
|
foreach (var (od, doT) in c.Kresli)
|
||||||
|
{
|
||||||
|
double a = Math.Max(t1, i * c.Perioda + od);
|
||||||
|
double b = Math.Min(t2, i * c.Perioda + doT);
|
||||||
|
if (b - a > 1e-9) yield return (a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<List<(double X, double Y)>> Zplosti(IReadOnlyList<Contour> smycky)
|
||||||
|
{
|
||||||
|
var vysledek = new List<List<(double X, double Y)>>();
|
||||||
|
foreach (var smycka in smycky)
|
||||||
|
{
|
||||||
|
var body = new List<(double X, double Y)>();
|
||||||
|
foreach (var hrana in smycka.Edges) hrana.AppendPoints(body, Sagitta);
|
||||||
|
if (body.Count >= 3) vysledek.Add(body);
|
||||||
|
}
|
||||||
|
return vysledek;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (double MinX, double MinY, double MaxX, double MaxY) Rozsah(
|
||||||
|
List<List<(double X, double Y)>> polygony)
|
||||||
|
{
|
||||||
|
double minX = double.MaxValue, minY = double.MaxValue, maxX = double.MinValue, maxY = double.MinValue;
|
||||||
|
foreach (var polygon in polygony)
|
||||||
|
foreach (var (x, y) in polygon)
|
||||||
|
{
|
||||||
|
if (x < minX) minX = x;
|
||||||
|
if (y < minY) minY = y;
|
||||||
|
if (x > maxX) maxX = x;
|
||||||
|
if (y > maxY) maxY = y;
|
||||||
|
}
|
||||||
|
return (minX, minY, maxX, maxY);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ public sealed class BoundsRenderer : IProfileRenderer
|
|||||||
Add(x + dx + w, y + dy + blockH);
|
Add(x + dx + w, y + dy + blockH);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Fill(IReadOnlyList<Contour> loops)
|
public void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default)
|
||||||
{
|
{
|
||||||
foreach (var loop in loops) AddContour(loop);
|
foreach (var loop in loops) AddContour(loop);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,12 +154,20 @@ public sealed class DxfProfileRenderer : IProfileRenderer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Fill(IReadOnlyList<Contour> loops)
|
public void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default)
|
||||||
{
|
{
|
||||||
if (loops.Count == 0) return;
|
if (loops.Count == 0) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var hatch = new Hatch { IsSolid = true, Pattern = new HatchPattern("SOLID") };
|
// Vzor šrafy se zapisuje zpátky jménem, měřítkem a úhlem – v CADu tak zůstane
|
||||||
|
// editovatelný. Rozvinuté čáry (Vypln.Cary) jsou jen pro rastrové renderery.
|
||||||
|
var hatch = new Hatch
|
||||||
|
{
|
||||||
|
IsSolid = vypln.JeSolid,
|
||||||
|
Pattern = new HatchPattern(vypln.JeSolid ? "SOLID" : vypln.Vzor!),
|
||||||
|
PatternScale = vypln.JeSolid || vypln.Meritko <= 0 ? 1 : vypln.Meritko,
|
||||||
|
PatternAngle = vypln.JeSolid ? 0 : vypln.UhelDeg * Math.PI / 180.0,
|
||||||
|
};
|
||||||
foreach (var loop in loops)
|
foreach (var loop in loops)
|
||||||
{
|
{
|
||||||
if (loop.Edges.Count == 0) continue;
|
if (loop.Edges.Count == 0) continue;
|
||||||
|
|||||||
@@ -53,6 +53,28 @@ public readonly record struct Edge(
|
|||||||
public (double X, double Y) PointAt(double theta) => (Cx + R * Math.Cos(theta), Cy + R * Math.Sin(theta));
|
public (double X, double Y) PointAt(double theta) => (Cx + R * Math.Cos(theta), Cy + R * Math.Sin(theta));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Úsečka ve výkresových mm – čára vzoru šrafy.</summary>
|
||||||
|
public readonly record struct Usecka(double X1, double Y1, double X2, double Y2);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Druh výplně plochy. <c>default</c> = plná výplň (SOLID).
|
||||||
|
/// <para>Nesolidní šrafa (ANSI31 apod.) se rastrovým rendererům předává už rozvinutá na
|
||||||
|
/// <paramref name="Cary"/>, protože Skia vzory šraf neumí; DXF export naopak <paramref name="Cary"/>
|
||||||
|
/// ignoruje a zapíše zpátky <c>HATCH</c> s původním jménem vzoru, měřítkem a úhlem, aby zůstal
|
||||||
|
/// v CADu editovatelný. Dřív se každá šrafa kreslila jako plná výplň – z logotypu
|
||||||
|
/// <c>md_sfdi.dxf</c> byl černý blok.</para>
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct Vypln(
|
||||||
|
string? Vzor = null, double Meritko = 1, double UhelDeg = 0, IReadOnlyList<Usecka>? Cary = null)
|
||||||
|
{
|
||||||
|
/// <summary>Plná výplň – buď se vzor neuvádí (<c>default</c>), nebo je to SOLID.</summary>
|
||||||
|
public bool JeSolid => string.IsNullOrEmpty(Vzor) || Vzor.Equals("SOLID", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>Šrafa se vzorem, už rozvinutá na čáry pro rastrové renderery.</summary>
|
||||||
|
public static Vypln Vzorovana(string vzor, double meritko, double uhelDeg, IReadOnlyList<Usecka> cary)
|
||||||
|
=> new(vzor, meritko, uhelDeg, cary);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Obrys = zřetězené hrany (konec jedné = počátek další). <paramref name="Closed"/> = true →
|
/// <summary>Obrys = zřetězené hrany (konec jedné = počátek další). <paramref name="Closed"/> = true →
|
||||||
/// vyplnitelná/uzavřená smyčka (poslední hrana se vrací k počátku prvního).</summary>
|
/// vyplnitelná/uzavřená smyčka (poslední hrana se vrací k počátku prvního).</summary>
|
||||||
public readonly record struct Contour(IReadOnlyList<Edge> Edges, bool Closed);
|
public readonly record struct Contour(IReadOnlyList<Edge> Edges, bool Closed);
|
||||||
@@ -111,6 +133,7 @@ public interface IProfileRenderer
|
|||||||
|
|
||||||
/// <summary>Vyplní oblast danou jednou či více uzavřenými obrysy (even-odd → vnořené smyčky
|
/// <summary>Vyplní oblast danou jednou či více uzavřenými obrysy (even-odd → vnořené smyčky
|
||||||
/// tvoří díry). Obrysy nesou analytické oblouky (viz <see cref="Contour"/>), takže výplně z
|
/// tvoří díry). Obrysy nesou analytické oblouky (viz <see cref="Contour"/>), takže výplně z
|
||||||
/// DXF HATCH – např. loga – mají hladké okraje v každém přiblížení i v PDF.</summary>
|
/// DXF HATCH – např. loga – mají hladké okraje v každém přiblížení i v PDF.
|
||||||
void Fill(IReadOnlyList<Contour> loops);
|
/// <paramref name="vypln"/> říká, čím se plocha vyplní; <c>default</c> = plná.</summary>
|
||||||
|
void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -261,9 +261,21 @@ public sealed class SkiaProfileRenderer : IProfileRenderer, IDisposable
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Fill(IReadOnlyList<Contour> loops)
|
public void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default)
|
||||||
{
|
{
|
||||||
if (loops.Count == 0) return;
|
if (loops.Count == 0) return;
|
||||||
|
|
||||||
|
// Šrafa se vzorem (ANSI31 apod.): Skia vzory neumí, kreslí se rozvinuté čáry. Když se
|
||||||
|
// vzor rozvinout nepodařilo, zůstane plocha prázdná – zaplácnout ji načerno by bylo horší.
|
||||||
|
if (!vypln.JeSolid)
|
||||||
|
{
|
||||||
|
if (vypln.Cary is not { Count: > 0 }) return;
|
||||||
|
_stroke.StrokeWidth = StrokePx;
|
||||||
|
foreach (var u in vypln.Cary)
|
||||||
|
_canvas.DrawLine(_map(u.X1, u.Y1), _map(u.X2, u.Y2), _stroke);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var path = new SKPath { FillType = SKPathFillType.EvenOdd };
|
using var path = new SKPath { FillType = SKPathFillType.EvenOdd };
|
||||||
bool any = false;
|
bool any = false;
|
||||||
foreach (var loop in loops)
|
foreach (var loop in loops)
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ public sealed class TransformRenderer : IProfileRenderer
|
|||||||
TextStyle style = default, double widthFactor = 1, double maxWidthMm = 0)
|
TextStyle style = default, double widthFactor = 1, double maxWidthMm = 0)
|
||||||
=> _inner.Text(X(x), Y(y), text, heightMm * _s, h, v, rotationDeg, style, widthFactor, maxWidthMm * _s);
|
=> _inner.Text(X(x), Y(y), text, heightMm * _s, h, v, rotationDeg, style, widthFactor, maxWidthMm * _s);
|
||||||
|
|
||||||
public void Fill(IReadOnlyList<Contour> loops)
|
public void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default)
|
||||||
=> _inner.Fill(loops.Select(c => new Contour(c.Edges.Select(T).ToList(), c.Closed)).ToList());
|
=> _inner.Fill(
|
||||||
|
loops.Select(c => new Contour(c.Edges.Select(T).ToList(), c.Closed)).ToList(),
|
||||||
|
vypln.Cary is null
|
||||||
|
? vypln
|
||||||
|
: vypln with { Cary = vypln.Cary.Select(u => new Usecka(X(u.X1), Y(u.Y1), X(u.X2), Y(u.Y2))).ToList() });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user