FP_POC / packages /shared /src /rules /geometry.ts
github-actions[bot]
Space-Deploy aus GitHub main
9de795a
Raw History Blame Contribute Delete
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;
}