File size: 7,390 Bytes
9de795a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
/**
 * 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;
}