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:
@@ -277,7 +277,7 @@ function rotate(v: Vec2, rad: number): Vec2 {
|
||||
* mit `PX_TO_M`, ergeben sich die Strichlängen in Modell-Metern (skalieren mit
|
||||
* dem Zoom wie die Kachel selbst — genau wie die SVG-Schraffur).
|
||||
*/
|
||||
const DASH_MM_TO_M = (1 / 0.13) * PX_TO_M;
|
||||
export const DASH_MM_TO_M = (1 / 0.13) * PX_TO_M;
|
||||
|
||||
/**
|
||||
* Zerlegt zusammenhängende Läufe entlang eines Strichmusters (`dash`, in mm-
|
||||
@@ -379,6 +379,14 @@ export function buildHatchRuns(poly: Vec2[], hatch: HatchRender): HatchRun[] {
|
||||
|
||||
const scale = hatch.scale > 1e-6 ? hatch.scale : 1;
|
||||
|
||||
// Random-Vektor-Untermodus (Kies/Splitt): kurze, deterministisch gestreute
|
||||
// Striche statt der regelmäßigen Parallel-/Kreuzschar. Bild-Schraffuren
|
||||
// (kind==="image") werden hier NICHT als Vektorlinien erzeugt — sie fallen im
|
||||
// Aufrufer auf eine neutrale Füllung zurück (RScene/GL kennen keine Textur).
|
||||
if (hatch.kind !== 'image' && hatch.lines === 'random') {
|
||||
return buildRandomHatchRuns(poly, hatch, scale);
|
||||
}
|
||||
|
||||
if (pattern === 'insulation') {
|
||||
return insulationRuns(poly, hatch, scale);
|
||||
}
|
||||
@@ -486,3 +494,131 @@ function insulationRuns(poly: Vec2[], hatch: HatchRender, scale: number): HatchR
|
||||
}
|
||||
return runs;
|
||||
}
|
||||
|
||||
// ── Random-Vektor-Schraffur (deterministisch) ────────────────────────────────
|
||||
|
||||
/**
|
||||
* Deterministischer 32-bit-PRNG (mulberry32). Gleicher Seed ⇒ gleiche Folge —
|
||||
* KEIN `Math.random()`: die Random-Schraffur muss über Re-Renders/Print
|
||||
* reproduzierbar sein (sonst flackert der Splitt und der Druck ist nicht
|
||||
* deterministisch).
|
||||
*/
|
||||
function mulberry32(seed: number): () => number {
|
||||
let a = seed >>> 0;
|
||||
return () => {
|
||||
a |= 0;
|
||||
a = (a + 0x6d2b79f5) | 0;
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Faltet mehrere Fließkomma-Kennwerte zu einem stabilen 32-bit-Seed. Die Werte
|
||||
* werden gequantelt (·1000, gerundet), damit numerisches Rauschen den Seed nicht
|
||||
* verschiebt — gleiche Fläche/gleiche Hatch-Parameter ⇒ gleicher Seed ⇒ gleiche
|
||||
* Streuung.
|
||||
*/
|
||||
function hashSeed(...vals: number[]): number {
|
||||
let h = 0x811c9dc5;
|
||||
for (const v of vals) {
|
||||
const q = Math.round(v * 1000) | 0;
|
||||
h ^= q & 0xffff;
|
||||
h = Math.imul(h, 0x01000193);
|
||||
h ^= (q >>> 16) & 0xffff;
|
||||
h = Math.imul(h, 0x01000193);
|
||||
}
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Streut kurze Striche gleichmäßig-zufällig (aber DETERMINISTISCH per `seed`) über
|
||||
* ein Polygon: je Zelle des `cell`-Rasters ein Strich zufälliger Lage, Länge
|
||||
* (~`strokeLen`) und Orientierung (um `baseAngleRad` ± π), auf das Polygon
|
||||
* geclippt ({@link clipSegmentToPolygon}, konkav-fähig). Einheiten-agnostisch —
|
||||
* der Aufrufer gibt `strokeLen`/`cell` in der Ziel-Koordinateneinheit an
|
||||
* (Modell-Meter bzw. Papier-mm). Ausgabe = 2-Punkt-Läufe.
|
||||
*/
|
||||
export function scatterStrokes(
|
||||
poly: Vec2[],
|
||||
strokeLen: number,
|
||||
cell: number,
|
||||
baseAngleRad: number,
|
||||
seed: number,
|
||||
): HatchRun[] {
|
||||
if (poly.length < 3 || strokeLen <= 1e-9 || cell <= 1e-9) return [];
|
||||
const bb = bbox(poly);
|
||||
const w = bb.maxX - bb.minX;
|
||||
const h = bb.maxY - bb.minY;
|
||||
if (!(w > 0) || !(h > 0)) return [];
|
||||
const nx = Math.max(1, Math.round(w / cell));
|
||||
const ny = Math.max(1, Math.round(h / cell));
|
||||
const count = Math.min(20000, Math.max(4, nx * ny));
|
||||
const rnd = mulberry32(seed);
|
||||
const runs: HatchRun[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const cx = bb.minX + rnd() * w;
|
||||
const cy = bb.minY + rnd() * h;
|
||||
// Orientierung frei (± π um die Basisrichtung), Länge um ±40 % variiert.
|
||||
const ang = baseAngleRad + (rnd() * 2 - 1) * Math.PI;
|
||||
const half = (strokeLen * 0.5) * (0.6 + 0.8 * rnd());
|
||||
const dx = Math.cos(ang) * half;
|
||||
const dy = Math.sin(ang) * half;
|
||||
const a: Vec2 = { x: cx - dx, y: cy - dy };
|
||||
const b: Vec2 = { x: cx + dx, y: cy + dy };
|
||||
for (const seg of clipSegmentToPolygon(a, b, poly)) runs.push([seg.a, seg.b]);
|
||||
}
|
||||
return runs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Random-Vektor-Schraffur eines Polygons in Modell-Metern (Kies/Splitt). Dichte
|
||||
* und Strichlänge sind — wie die Parallelschar — an die Kachelgröße
|
||||
* (`8·scale` px) gekoppelt, damit `scale` die Streudichte steuert. Der Seed
|
||||
* stammt aus der Flächen-Kennung (gerundete Bounding-Box) plus den Hatch-
|
||||
* Parametern → über Re-Renders/Print reproduzierbar.
|
||||
*/
|
||||
export function buildRandomHatchRuns(poly: Vec2[], hatch: HatchRender, scale?: number): HatchRun[] {
|
||||
const s = scale ?? (hatch.scale > 1e-6 ? hatch.scale : 1);
|
||||
const cell = 8 * s * PX_TO_M; // Meter, wie der Parallel-Schar-Abstand
|
||||
const strokeLen = cell * 1.1; // kurzer Strich ~ Kachelmaß
|
||||
const bb = bbox(poly);
|
||||
const seed = hashSeed(bb.minX, bb.minY, bb.maxX - bb.minX, bb.maxY - bb.minY, hatch.angle, s);
|
||||
const base = toModelAngleRad(hatch.angle);
|
||||
return scatterStrokes(poly, strokeLen, cell, base, seed);
|
||||
}
|
||||
|
||||
// ── Zickzack-/Wellen-Linie ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Tesselliert die Strecke [a,b] zu einem Zickzack (Dreieckswelle) mit `amplitude`
|
||||
* (Ausschlag quer, Einheit wie a/b) und `wavelength` (Periodenlänge längs). Rein
|
||||
* geometrisch, einheiten-agnostisch: der Aufrufer übergibt amplitude/wavelength
|
||||
* in der Zielkoordinaten-Einheit. Die Auslenkung folgt dem Muster [0,+A,0,−A] je
|
||||
* Viertel-Wellenlänge; Start liegt auf der Achse, das Ende exakt auf `b`.
|
||||
*/
|
||||
export function zigzagPoints(a: Vec2, b: Vec2, amplitude: number, wavelength: number): Vec2[] {
|
||||
const dx = b.x - a.x;
|
||||
const dy = b.y - a.y;
|
||||
const L = Math.hypot(dx, dy);
|
||||
if (L < 1e-12 || wavelength <= 1e-9 || amplitude <= 0) return [a, b];
|
||||
const ux = dx / L;
|
||||
const uy = dy / L;
|
||||
const nx = -uy; // linke Normale (Ausschlagachse)
|
||||
const ny = ux;
|
||||
const quarter = wavelength / 4;
|
||||
const steps = Math.max(1, Math.round(L / quarter));
|
||||
const tri = [0, 1, 0, -1];
|
||||
const pts: Vec2[] = [];
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = Math.min(L, i * quarter);
|
||||
const off = amplitude * tri[i % 4];
|
||||
pts.push({ x: a.x + ux * t + nx * off, y: a.y + uy * t + ny * off });
|
||||
}
|
||||
const last = pts[pts.length - 1];
|
||||
if (Math.abs(last.x - b.x) > 1e-9 || Math.abs(last.y - b.y) > 1e-9) {
|
||||
pts.push({ x: b.x, y: b.y });
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user