Files
web-cad/frontend/src/services/hatchPatternService.ts
T

155 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 };
}