Schraffur/Linien: Rendering der neuen Typen (Bild, Random, Zickzack)
Bild-Schraffur als getiltes <pattern>/<image> (scaleX/scaleY/rotation) im Live-SVG- und Print-Pfad; GL/WASM/DXF vorerst neutraler Fallback (Folgearbeit, im Code vermerkt). Random-Vektor-Schraffur als deterministische Streu-Striche (mulberry32-Seed aus Flaechen-Bounding-Box, kein Math.random) in allen Pfaden. Zickzack-Linie als getilteter Pfad; LineStyle.kind/zigzag additiv durch die Linien-Emission (generatePlan) bis zu den Renderern durchgereicht. Geteilte Geometrie in glPlanHatch (scatterStrokes/buildRandomHatchRuns/zigzagPoints) — eine Wahrheit fuer Live/Print/GL/DXF. 8 neue Tests.
This commit is contained in:
+114
-13
@@ -23,10 +23,75 @@
|
||||
|
||||
import type { HatchRender, Plan, Primitive } from "../plan/generatePlan";
|
||||
import type { Vec2 } from "../model/types";
|
||||
import { buildRandomHatchRuns, zigzagPoints } from "../plan/glPlan/glPlanHatch";
|
||||
|
||||
/** SVG-Namespace für die programmatisch erzeugten Knoten. */
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
/** Basis-Kachelmaß (mm) einer Bild-Schraffur bei scaleX/scaleY = 1. */
|
||||
const IMG_TILE_MM = 20;
|
||||
|
||||
/** Monoton wachsender Zähler für eindeutige `<pattern>`-IDs im Dokument. */
|
||||
let patternSeq = 0;
|
||||
|
||||
/** Liefert (oder erzeugt) das gemeinsame `<defs>`-Element des SVG. */
|
||||
function ensureDefs(svg: SVGSVGElement): SVGDefsElement {
|
||||
const existing = svg.querySelector("defs");
|
||||
if (existing) return existing as SVGDefsElement;
|
||||
const defs = document.createElementNS(SVG_NS, "defs") as SVGDefsElement;
|
||||
// Vor den Inhalt setzen, damit Referenzen (url(#…)) sicher aufgelöst werden.
|
||||
svg.insertBefore(defs, svg.firstChild);
|
||||
return defs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt ein gekacheltes Bild-`<pattern>` an und liefert dessen ID. `scaleX`/
|
||||
* `scaleY` verzerren die Kachel unabhängig in L×B (preserveAspectRatio="none"),
|
||||
* `rotation` (Grad) dreht das Muster (patternTransform). Einheiten = Papier-mm.
|
||||
*/
|
||||
function ensureImagePattern(
|
||||
svg: SVGSVGElement,
|
||||
image: NonNullable<HatchRender["image"]>,
|
||||
): string {
|
||||
const id = `imgpat-${patternSeq++}`;
|
||||
const w = Math.max(0.5, IMG_TILE_MM * (image.scaleX > 0 ? image.scaleX : 1));
|
||||
const h = Math.max(0.5, IMG_TILE_MM * (image.scaleY > 0 ? image.scaleY : 1));
|
||||
const pattern = document.createElementNS(SVG_NS, "pattern");
|
||||
pattern.setAttribute("id", id);
|
||||
pattern.setAttribute("patternUnits", "userSpaceOnUse");
|
||||
pattern.setAttribute("width", String(round(w)));
|
||||
pattern.setAttribute("height", String(round(h)));
|
||||
if (image.rotation) pattern.setAttribute("patternTransform", `rotate(${image.rotation})`);
|
||||
const img = document.createElementNS(SVG_NS, "image");
|
||||
img.setAttribute("href", image.src);
|
||||
img.setAttribute("x", "0");
|
||||
img.setAttribute("y", "0");
|
||||
img.setAttribute("width", String(round(w)));
|
||||
img.setAttribute("height", String(round(h)));
|
||||
img.setAttribute("preserveAspectRatio", "none");
|
||||
pattern.appendChild(img);
|
||||
ensureDefs(svg).appendChild(pattern);
|
||||
return id;
|
||||
}
|
||||
|
||||
/** `<polyline>`-Knoten (mm-Koordinaten) — für Zickzack-Linien/-Läufe. */
|
||||
function polylineNode(
|
||||
pts: Vec2[],
|
||||
color: string,
|
||||
strokeMm: number,
|
||||
dash: number[] | null | undefined,
|
||||
): SVGElement {
|
||||
const el = document.createElementNS(SVG_NS, "polyline");
|
||||
el.setAttribute("points", pts.map((p) => `${round(p.x)},${round(p.y)}`).join(" "));
|
||||
el.setAttribute("fill", "none");
|
||||
el.setAttribute("stroke", color);
|
||||
el.setAttribute("stroke-width", String(strokeMm));
|
||||
el.setAttribute("stroke-linecap", "round");
|
||||
el.setAttribute("stroke-linejoin", "round");
|
||||
if (dash && dash.length) el.setAttribute("stroke-dasharray", dash.map((d) => round(d)).join(" "));
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard-Stift-Stufen (mm). Beliebige Kategorie-Strichstärken werden auf die
|
||||
* NÄCHSTHÖHERE dieser ISO-nahen Stufen gequantelt, damit der Plan saubere,
|
||||
@@ -160,7 +225,16 @@ function appendPrimitive(
|
||||
case "line": {
|
||||
const a = map(p.a);
|
||||
const b = map(p.b);
|
||||
const ln = lineNode(a, b, p.color ?? "#111111", quantizePen(p.weightMm), p.dash, mmPerM);
|
||||
// Zickzack-Linie (amplitude/wavelength sind Papier-mm = mm-Zielraum) →
|
||||
// Polylinie; sonst gerade (ggf. gestrichelte) Linie wie bisher.
|
||||
const ln = p.zigzag
|
||||
? polylineNode(
|
||||
zigzagPoints(a, b, p.zigzag.amplitude, p.zigzag.wavelength),
|
||||
p.color ?? "#111111",
|
||||
quantizePen(p.weightMm),
|
||||
p.dash,
|
||||
)
|
||||
: lineNode(a, b, p.color ?? "#111111", quantizePen(p.weightMm), p.dash, mmPerM);
|
||||
if (opacity) ln.setAttribute("opacity", opacity);
|
||||
svg.appendChild(ln);
|
||||
break;
|
||||
@@ -195,7 +269,15 @@ function appendPolygon(
|
||||
if (opacity) g.setAttribute("opacity", opacity);
|
||||
|
||||
const pattern = p.hatch.pattern;
|
||||
if (pattern === "none") {
|
||||
if (p.hatch.kind === "image" && p.hatch.image) {
|
||||
// Bild-Schraffur: gekacheltes <pattern> mit <image> (scaleX/scaleY/rotation)
|
||||
// über die Fläche, unabhängig vom `pattern`-Feld. Grundfüllung darunter,
|
||||
// Umriss obenauf.
|
||||
if (p.fill !== "none") g.appendChild(polyNode(ptsAttr, p.fill, "none", 0));
|
||||
const patId = ensureImagePattern(svg, p.hatch.image);
|
||||
g.appendChild(polyNode(ptsAttr, `url(#${patId})`, "none", 0));
|
||||
if (strokeMm > 0) g.appendChild(polyNode(ptsAttr, "none", p.stroke, strokeMm));
|
||||
} else if (pattern === "none") {
|
||||
g.appendChild(polyNode(ptsAttr, p.fill, p.stroke, strokeMm));
|
||||
} else if (pattern === "solid") {
|
||||
g.appendChild(polyNode(ptsAttr, p.hatch.color, p.stroke, strokeMm));
|
||||
@@ -205,17 +287,36 @@ function appendPolygon(
|
||||
if (p.fill !== "none") {
|
||||
g.appendChild(polyNode(ptsAttr, p.fill, "none", 0));
|
||||
}
|
||||
for (const seg of hatchLines(ptsMm, p.hatch, mmPerM)) {
|
||||
g.appendChild(
|
||||
lineNode(
|
||||
seg.a,
|
||||
seg.b,
|
||||
p.hatch.color,
|
||||
quantizePen(p.hatch.lineWeight),
|
||||
p.hatch.dash,
|
||||
mmPerM,
|
||||
),
|
||||
);
|
||||
if (p.hatch.lines === "random") {
|
||||
// Random-Vektor (Kies/Splitt): deterministische Streu-Striche. Wird im
|
||||
// MODELL-Raum erzeugt (identischer Seed/Geometrie wie Bildschirm/GL/PDF)
|
||||
// und dann nach Papier-mm abgebildet.
|
||||
for (const run of buildRandomHatchRuns(p.pts, p.hatch)) {
|
||||
if (run.length < 2) continue;
|
||||
g.appendChild(
|
||||
lineNode(
|
||||
map(run[0]),
|
||||
map(run[run.length - 1]),
|
||||
p.hatch.color,
|
||||
quantizePen(p.hatch.lineWeight),
|
||||
null,
|
||||
mmPerM,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (const seg of hatchLines(ptsMm, p.hatch, mmPerM)) {
|
||||
g.appendChild(
|
||||
lineNode(
|
||||
seg.a,
|
||||
seg.b,
|
||||
p.hatch.color,
|
||||
quantizePen(p.hatch.lineWeight),
|
||||
p.hatch.dash,
|
||||
mmPerM,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (strokeMm > 0) {
|
||||
g.appendChild(polyNode(ptsAttr, "none", p.stroke, strokeMm));
|
||||
|
||||
Reference in New Issue
Block a user