/** * 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 }; }