Spaces:
Running
Running
Download packages/shared/src/rules/geometry.ts from Bryan-HSLU/FP_POC: direct link, hf CLI and curl.
- Browser
- Download file 7.39 kB
-
https://huggingface.co/spaces/Bryan-HSLU/FP_POC/resolve/main/packages/shared/src/rules/geometry.ts
- Command line
-
hf download hf://spaces/Bryan-HSLU/FP_POC/packages/shared/src/rules/geometry.ts
-
curl -L -o geometry.ts https://huggingface.co/spaces/Bryan-HSLU/FP_POC/resolve/main/packages/shared/src/rules/geometry.ts
7.39 kB
| /** | |
| * 2D-Geometrie für den Regel-Interpreter (Grundriss x/z, Meter). | |
| * | |
| * ⚠️ PARITÄT: Dieses Modul ist 1:1 in Python gespiegelt | |
| * (services/engines/src/fp_engines/rules/geometry.py). Jede Änderung hier | |
| * MUSS dort identisch nachgezogen werden – der Paritätstest über die | |
| * goldenen Fixtures erzwingt das. Gleiche Formeln, gleiche Reihenfolge der | |
| * Operationen (IEEE-754-Determinismus). | |
| */ | |
| export type Vec2 = [number, number]; | |
| /** Konvexes Viereck (Footprint/Zone) als 4 Eckpunkte gegen den Uhrzeigersinn. */ | |
| export type Quad = [Vec2, Vec2, Vec2, Vec2]; | |
| /** cos/sin in Grad; Vielfache von 90° exakt (vermeidet Float-Rauschen libm-übergreifend). */ | |
| export function cosDeg(deg: number): number { | |
| const n = ((deg % 360) + 360) % 360; | |
| if (n === 0) return 1; | |
| if (n === 90) return 0; | |
| if (n === 180) return -1; | |
| if (n === 270) return 0; | |
| return Math.cos((deg * Math.PI) / 180); | |
| } | |
| export function sinDeg(deg: number): number { | |
| const n = ((deg % 360) + 360) % 360; | |
| if (n === 0) return 0; | |
| if (n === 90) return 1; | |
| if (n === 180) return 0; | |
| if (n === 270) return -1; | |
| return Math.sin((deg * Math.PI) / 180); | |
| } | |
| /** Rotation um y (y-up, rechtshändig): (x,z) → (x·cos + z·sin, −x·sin + z·cos). */ | |
| export function rotateY(v: Vec2, yawDeg: number): Vec2 { | |
| const c = cosDeg(yawDeg); | |
| const s = sinDeg(yawDeg); | |
| return [v[0] * c + v[1] * s, -v[0] * s + v[1] * c]; | |
| } | |
| /** Blickrichtung der «Front» eines Objekts: lokal +z, rotiert um yaw. */ | |
| export function frontDir(yawDeg: number): Vec2 { | |
| return rotateY([0, 1], yawDeg); | |
| } | |
| /** Footprint-Ecken eines Objekts: Breite w lokal entlang x, Tiefe d lokal entlang z. */ | |
| export function footprint(center: Vec2, w: number, d: number, yawDeg: number): Quad { | |
| const hw = w / 2; | |
| const hd = d / 2; | |
| const local: Quad = [ | |
| [-hw, -hd], | |
| [hw, -hd], | |
| [hw, hd], | |
| [-hw, hd], | |
| ]; | |
| return local.map((p) => { | |
| const r = rotateY(p, yawDeg); | |
| return [center[0] + r[0], center[1] + r[1]] as Vec2; | |
| }) as Quad; | |
| } | |
| /** Achsparalleles Rechteck als Quad. */ | |
| export function rectQuad(xMin: number, zMin: number, xMax: number, zMax: number): Quad { | |
| return [ | |
| [xMin, zMin], | |
| [xMax, zMin], | |
| [xMax, zMax], | |
| [xMin, zMax], | |
| ]; | |
| } | |
| export function distPointToSegment(p: Vec2, a: Vec2, b: Vec2): number { | |
| const abx = b[0] - a[0]; | |
| const abz = b[1] - a[1]; | |
| const apx = p[0] - a[0]; | |
| const apz = p[1] - a[1]; | |
| const len2 = abx * abx + abz * abz; | |
| let t = len2 === 0 ? 0 : (apx * abx + apz * abz) / len2; | |
| if (t < 0) t = 0; | |
| if (t > 1) t = 1; | |
| const dx = p[0] - (a[0] + t * abx); | |
| const dz = p[1] - (a[1] + t * abz); | |
| return Math.sqrt(dx * dx + dz * dz); | |
| } | |
| export function distSegmentToSegment(a1: Vec2, a2: Vec2, b1: Vec2, b2: Vec2): number { | |
| if (segmentsIntersect(a1, a2, b1, b2)) return 0; | |
| return Math.min( | |
| distPointToSegment(a1, b1, b2), | |
| distPointToSegment(a2, b1, b2), | |
| distPointToSegment(b1, a1, a2), | |
| distPointToSegment(b2, a1, a2), | |
| ); | |
| } | |
| function cross(o: Vec2, a: Vec2, b: Vec2): number { | |
| return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); | |
| } | |
| function segmentsIntersect(a1: Vec2, a2: Vec2, b1: Vec2, b2: Vec2): boolean { | |
| const d1 = cross(b1, b2, a1); | |
| const d2 = cross(b1, b2, a2); | |
| const d3 = cross(a1, a2, b1); | |
| const d4 = cross(a1, a2, b2); | |
| if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) { | |
| return true; | |
| } | |
| const onSeg = (o1: Vec2, o2: Vec2, p: Vec2): boolean => | |
| Math.min(o1[0], o2[0]) <= p[0] && | |
| p[0] <= Math.max(o1[0], o2[0]) && | |
| Math.min(o1[1], o2[1]) <= p[1] && | |
| p[1] <= Math.max(o1[1], o2[1]); | |
| if (d1 === 0 && onSeg(b1, b2, a1)) return true; | |
| if (d2 === 0 && onSeg(b1, b2, a2)) return true; | |
| if (d3 === 0 && onSeg(a1, a2, b1)) return true; | |
| if (d4 === 0 && onSeg(a1, a2, b2)) return true; | |
| return false; | |
| } | |
| export function distPointToPolygonBoundary(p: Vec2, poly: Vec2[]): number { | |
| let best = Infinity; | |
| for (let i = 0; i < poly.length; i++) { | |
| const a = poly[i] as Vec2; | |
| const b = poly[(i + 1) % poly.length] as Vec2; | |
| const d = distPointToSegment(p, a, b); | |
| if (d < best) best = d; | |
| } | |
| return best; | |
| } | |
| /** Punkt im (einfachen) Polygon; Punkte AUF dem Rand zählen als innen. */ | |
| export function pointInPolygon(p: Vec2, poly: Vec2[]): boolean { | |
| if (distPointToPolygonBoundary(p, poly) < 1e-9) return true; | |
| let inside = false; | |
| for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { | |
| const pi = poly[i] as Vec2; | |
| const pj = poly[j] as Vec2; | |
| if ( | |
| pi[1] > p[1] !== pj[1] > p[1] && | |
| p[0] < ((pj[0] - pi[0]) * (p[1] - pi[1])) / (pj[1] - pi[1]) + pi[0] | |
| ) { | |
| inside = !inside; | |
| } | |
| } | |
| return inside; | |
| } | |
| /** SAT-Überlappungstiefe zweier konvexer Quads; null wenn getrennt/berührend. */ | |
| export function overlapDepth(a: Quad, b: Quad): number | null { | |
| let minOverlap = Infinity; | |
| for (const quad of [a, b]) { | |
| for (let i = 0; i < 4; i++) { | |
| const p1 = quad[i] as Vec2; | |
| const p2 = quad[(i + 1) % 4] as Vec2; | |
| const ex = p2[0] - p1[0]; | |
| const ez = p2[1] - p1[1]; | |
| const len = Math.sqrt(ex * ex + ez * ez); | |
| if (len === 0) continue; | |
| const nx = -ez / len; | |
| const nz = ex / len; | |
| let aMin = Infinity; | |
| let aMax = -Infinity; | |
| for (const p of a) { | |
| const proj = p[0] * nx + p[1] * nz; | |
| if (proj < aMin) aMin = proj; | |
| if (proj > aMax) aMax = proj; | |
| } | |
| let bMin = Infinity; | |
| let bMax = -Infinity; | |
| for (const p of b) { | |
| const proj = p[0] * nx + p[1] * nz; | |
| if (proj < bMin) bMin = proj; | |
| if (proj > bMax) bMax = proj; | |
| } | |
| const overlap = Math.min(aMax, bMax) - Math.max(aMin, bMin); | |
| if (overlap <= 0) return null; | |
| if (overlap < minOverlap) minOverlap = overlap; | |
| } | |
| } | |
| return minOverlap; | |
| } | |
| /** Echte Mindestdistanz zweier konvexer Quads (0 bei Berührung/Überlappung). */ | |
| export function quadDistance(a: Quad, b: Quad): number { | |
| if (overlapDepth(a, b) !== null) return 0; | |
| let best = Infinity; | |
| for (let i = 0; i < 4; i++) { | |
| for (let j = 0; j < 4; j++) { | |
| const d = distSegmentToSegment( | |
| a[i] as Vec2, | |
| a[(i + 1) % 4] as Vec2, | |
| b[j] as Vec2, | |
| b[(j + 1) % 4] as Vec2, | |
| ); | |
| if (d < best) best = d; | |
| } | |
| } | |
| return best; | |
| } | |
| /** | |
| * Trennung zweier Quads als Marge: positiv = Abstand, negativ = Überlappungstiefe, | |
| * 0 = Berührung. | |
| */ | |
| export function separation(a: Quad, b: Quad): number { | |
| const depth = overlapDepth(a, b); | |
| if (depth !== null) return -depth; | |
| return quadDistance(a, b); | |
| } | |
| /** | |
| * Wie weit ein Quad aus dem Polygon herausragt: 0 = vollständig innen | |
| * (Berührung erlaubt), sonst negative Marge (−max. Austrittstiefe der Ecken). | |
| * v0: Ecken-Heuristik – reicht für Footprints in einfachen Raumpolygonen. | |
| */ | |
| export function containmentViolation(quad: Quad, poly: Vec2[]): number { | |
| let worst = 0; | |
| for (const corner of quad) { | |
| if (!pointInPolygon(corner, poly)) { | |
| const d = distPointToPolygonBoundary(corner, poly); | |
| if (-d < worst) worst = -d; | |
| } | |
| } | |
| return worst; | |
| } | |
| /** Kaufmännisch gerundete cm-Marge (Halbschritt aufwärts, vorzeichensymmetrisch) auf 0.1 cm. */ | |
| export function roundCm(meters: number): number { | |
| const cm = meters * 100; | |
| const sign = cm < 0 ? -1 : 1; | |
| return (sign * Math.floor(Math.abs(cm) * 10 + 0.5)) / 10; | |
| } | |