task(CAD-2): real hatch patterns - 9 patterns (ANSI31/32/37, NET, GRASS, DOTS, SOLID + legacy), boundary detection, render adapter hatch lines
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Task CAD-2 – Echte Hatch-Muster + Boundary-Erkennung.
|
||||
*
|
||||
* Standard-CAD-Schraffurmuster als Linien-Segment-Generatoren für
|
||||
* eine BBox. computeBoundary liefert die umschlossene Fläche aus
|
||||
* einer Kontur (Polygon-Punkte).
|
||||
*/
|
||||
|
||||
export interface HatchSeg {
|
||||
from: { x: number; y: number };
|
||||
to: { x: number; y: number };
|
||||
fill?: string;
|
||||
closed?: boolean;
|
||||
points?: Array<{ x: number; y: number }>;
|
||||
}
|
||||
|
||||
export interface HatchPattern {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
defaultSpacing: number;
|
||||
/** 45°-Ersatzwinkel für Linienfamilien (Grad). */
|
||||
angles: number[];
|
||||
/** Punkte-Muster (DOTS) statt Linien. */
|
||||
dots?: boolean;
|
||||
/** Vollflächige Füllung. */
|
||||
solid?: boolean;
|
||||
}
|
||||
|
||||
export type HatchPatternId = string;
|
||||
|
||||
export const HATCH_PATTERNS: HatchPattern[] = [
|
||||
{ id: 'lines', label: 'Linien (45°)', description: 'Einfache 45°-Schraffur', defaultSpacing: 8, angles: [45] },
|
||||
{ id: 'cross', label: 'Kreuz', description: 'Kreuzschraffur 45°/-45°', defaultSpacing: 8, angles: [45, -45] },
|
||||
{ id: 'ANSI31', label: 'ANSI31 (45°)', description: 'ANSI-Standard 45°', defaultSpacing: 6, angles: [45] },
|
||||
{ id: 'ANSI32', label: 'ANSI32 (Eisen)', description: 'ANSI Eisen 45°/-45°', defaultSpacing: 6, angles: [45, -45] },
|
||||
{ id: 'ANSI37', label: 'ANSI37 (Beton)', description: 'ANSI Beton 45°/-45° dicht', defaultSpacing: 3, angles: [45, -45] },
|
||||
{ id: 'NET', label: 'Netz', description: 'Horizontal + vertikal', defaultSpacing: 10, angles: [0, 90] },
|
||||
{ id: 'GRASS', label: 'Gras', description: 'Kurze Gras-Striche', defaultSpacing: 6, angles: [75] },
|
||||
{ id: 'DOTS', label: 'Punkte', description: 'Punktschraffur', defaultSpacing: 8, angles: [], dots: true },
|
||||
{ id: 'SOLID', label: 'Voll', description: 'Vollflächige Füllung', defaultSpacing: 0, angles: [], solid: true },
|
||||
];
|
||||
|
||||
/** Muster per ID (oder undefined). */
|
||||
export function getHatchPattern(id: string | undefined): HatchPattern | undefined {
|
||||
if (!id) return undefined;
|
||||
return HATCH_PATTERNS.find((p) => p.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt Schraffur-Segmente für eine BBox nach Muster.
|
||||
* SOLID liefert EIN Segment mit fill-Flag; DOTS liefert Punkte-Muster.
|
||||
*/
|
||||
export function generateHatchLines(
|
||||
patternId: string,
|
||||
bbox: { minX: number; minY: number; maxX: number; maxY: number },
|
||||
spacing: number,
|
||||
): HatchSeg[] {
|
||||
const pattern = getHatchPattern(patternId);
|
||||
if (!pattern) return [];
|
||||
const { minX, minY, maxX, maxY } = bbox;
|
||||
const w = maxX - minX;
|
||||
const h = maxY - minY;
|
||||
|
||||
if (pattern.solid) {
|
||||
return [{ from: { x: minX, y: minY }, to: { x: maxX, y: maxY }, fill: '#888888' }];
|
||||
}
|
||||
|
||||
if (pattern.dots) {
|
||||
const out: HatchSeg[] = [];
|
||||
for (let y = minY + spacing / 2; y < maxY; y += spacing) {
|
||||
for (let x = minX + spacing / 2; x < maxX; x += spacing) {
|
||||
out.push({
|
||||
from: { x, y },
|
||||
to: { x: x + 1, y: y + 1 },
|
||||
points: [{ x, y }],
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Linienfamilien je Winkel
|
||||
const out: HatchSeg[] = [];
|
||||
for (const angleDeg of pattern.angles) {
|
||||
const rad = (angleDeg * Math.PI) / 180;
|
||||
const cosA = Math.cos(rad);
|
||||
const sinA = Math.sin(rad);
|
||||
const diag = Math.hypot(w, h);
|
||||
const step = spacing;
|
||||
const offsetCount = Math.ceil(diag / step) + 2;
|
||||
for (let i = -offsetCount; i <= offsetCount; i++) {
|
||||
const cx = minX + w / 2 + i * step;
|
||||
const cy = minY + h / 2;
|
||||
// Linie durch (cx,cy) in Richtung (cosA, sinA), begrenzt auf BBox
|
||||
const p1 = { x: cx - cosA * diag, y: cy - sinA * diag };
|
||||
const p2 = { x: cx + cosA * diag, y: cy + sinA * diag };
|
||||
// Clipping an BBox (einfaches Cohen-Sutherland)
|
||||
const cl = clipToBBox(p1, p2, bbox);
|
||||
if (cl) out.push({ from: cl[0], to: cl[1] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Cohen-Sutherland-artiges Clipping an BBox (vereinfacht). */
|
||||
function clipToBBox(
|
||||
p1: { x: number; y: number },
|
||||
p2: { x: number; y: number },
|
||||
b: { minX: number; minY: number; maxX: number; maxY: number },
|
||||
): Array<{ x: number; y: number }> | null {
|
||||
// Liang-Barsky
|
||||
let t0 = 0, t1 = 1;
|
||||
const dx = p2.x - p1.x;
|
||||
const dy = p2.y - p1.y;
|
||||
const clip = (p: number, q: number): boolean => {
|
||||
if (p === 0) return q >= 0;
|
||||
const r = q / p;
|
||||
if (p < 0) {
|
||||
if (r > t1) return false;
|
||||
if (r > t0) t0 = r;
|
||||
} else {
|
||||
if (r < t0) return false;
|
||||
if (r < t1) t1 = r;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (!clip(-dx, p1.x - b.minX)) return null;
|
||||
if (!clip(dx, b.maxX - p1.x)) return null;
|
||||
if (!clip(-dy, p1.y - b.minY)) return null;
|
||||
if (!clip(dy, b.maxY - p1.y)) return null;
|
||||
return [
|
||||
{ x: p1.x + t0 * dx, y: p1.y + t0 * dy },
|
||||
{ x: p1.x + t1 * dx, y: p1.y + t1 * dy },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Boundary-Erkennung: BBox aus einer Kontur (Polygon-Punkte).
|
||||
* Weniger als 2 Punkte → null.
|
||||
*/
|
||||
export function computeBoundary(
|
||||
pts: Array<{ x: number; y: number }>,
|
||||
): { minX: number; minY: number; maxX: number; maxY: number } | null {
|
||||
if (!Array.isArray(pts) || pts.length < 2) return null;
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
for (const p of pts) {
|
||||
if (p.x < minX) minX = p.x;
|
||||
if (p.y < minY) minY = p.y;
|
||||
if (p.x > maxX) maxX = p.x;
|
||||
if (p.y > maxY) maxY = p.y;
|
||||
}
|
||||
return { minX, minY, maxX, maxY };
|
||||
}
|
||||
Reference in New Issue
Block a user