1pub const BOUNDARY_TOL_MM: f64 = 1.0;
27
28const DEGENERATE_AREA_REL: f64 = 1e-12;
31
32pub fn signed_area(pts: &[[f64; 2]]) -> f64 {
37 shoelace(pts.len(), |i| pts[i])
38}
39
40fn shoelace(n: usize, xy: impl Fn(usize) -> [f64; 2]) -> f64 {
45 if n < 3 {
46 return 0.0;
47 }
48 let mut sum = 0.0;
49 for i in 0..n {
50 let a = xy(i);
51 let b = xy((i + 1) % n);
52 sum += a[0] * b[1] - b[0] * a[1];
53 }
54 sum / 2.0
55}
56
57pub fn area(pts: &[[f64; 2]]) -> f64 {
59 signed_area(pts).abs()
60}
61
62pub fn area_xy(pts: &[[f64; 3]]) -> f64 {
67 shoelace(pts.len(), |i| [pts[i][0], pts[i][1]]).abs()
68}
69
70pub fn area_3d(pts: &[[f64; 3]]) -> f64 {
77 if pts.len() < 3 {
78 return 0.0;
79 }
80 let n = pts.len();
81 let mut normal = [0.0_f64; 3];
82 for i in 0..n {
83 let (p0, p1) = (pts[i], pts[(i + 1) % n]);
84 let c = super::vec3::cross(p0, p1);
85 normal = super::vec3::add(normal, c);
86 }
87 0.5 * super::vec3::norm(normal)
88}
89
90pub fn centroid(pts: &[[f64; 2]]) -> [f64; 2] {
108 let vertex_mean = || {
109 if pts.is_empty() {
110 return [0.0, 0.0];
111 }
112 let n = pts.len() as f64;
113 [
114 pts.iter().map(|p| p[0]).sum::<f64>() / n,
115 pts.iter().map(|p| p[1]).sum::<f64>() / n,
116 ]
117 };
118 if pts.len() < 3 {
119 return vertex_mean();
120 }
121 let a = signed_area(pts);
122 if a.abs() <= max_extent_sq(pts) * DEGENERATE_AREA_REL {
123 return vertex_mean();
124 }
125 let mut cx = 0.0;
126 let mut cy = 0.0;
127 for (i, p0) in pts.iter().enumerate() {
128 let p1 = pts[(i + 1) % pts.len()];
129 let cross = p0[0] * p1[1] - p1[0] * p0[1];
130 cx += (p0[0] + p1[0]) * cross;
131 cy += (p0[1] + p1[1]) * cross;
132 }
133 let six_a = 6.0 * a;
134 [cx / six_a, cy / six_a]
135}
136
137pub fn bounding_box(pts: &[[f64; 2]]) -> ([f64; 2], [f64; 2]) {
139 if pts.is_empty() {
140 return ([0.0, 0.0], [0.0, 0.0]);
141 }
142 let (mut min_x, mut max_x) = (f64::INFINITY, f64::NEG_INFINITY);
143 let (mut min_y, mut max_y) = (f64::INFINITY, f64::NEG_INFINITY);
144 for p in pts {
145 min_x = min_x.min(p[0]);
146 max_x = max_x.max(p[0]);
147 min_y = min_y.min(p[1]);
148 max_y = max_y.max(p[1]);
149 }
150 ([min_x, min_y], [max_x, max_y])
151}
152
153fn max_extent_sq(pts: &[[f64; 2]]) -> f64 {
155 let (lo, hi) = bounding_box(pts);
156 let extent = (hi[0] - lo[0]).max(hi[1] - lo[1]);
157 extent * extent
158}
159
160pub fn point_segment_dist(p: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
162 point_segment_dist_sq(p, a, b).sqrt()
163}
164
165pub fn point_segment_dist_sq(p: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
169 let ab = [b[0] - a[0], b[1] - a[1]];
170 let len2 = ab[0] * ab[0] + ab[1] * ab[1];
171 let t = if len2 <= f64::EPSILON {
172 0.0
173 } else {
174 (((p[0] - a[0]) * ab[0] + (p[1] - a[1]) * ab[1]) / len2).clamp(0.0, 1.0)
175 };
176 let q = [a[0] + t * ab[0], a[1] + t * ab[1]];
177 let (dx, dy) = (p[0] - q[0], p[1] - q[1]);
178 dx * dx + dy * dy
179}
180
181pub fn on_boundary(poly: &[[f64; 2]], p: [f64; 2]) -> bool {
184 let n = poly.len();
185 if n < 2 {
186 return false;
187 }
188 let tol2 = BOUNDARY_TOL_MM * BOUNDARY_TOL_MM;
189 (0..n).any(|i| point_segment_dist_sq(p, poly[i], poly[(i + 1) % n]) <= tol2)
190}
191
192pub fn contains_excluding_boundary(poly: &[[f64; 2]], p: [f64; 2]) -> bool {
201 if poly.len() < 3 || on_boundary(poly, p) {
202 return false;
203 }
204 ray_crossing(poly, p)
205}
206
207pub fn contains_including_boundary(poly: &[[f64; 2]], p: [f64; 2]) -> bool {
212 if poly.len() < 3 {
213 return false;
214 }
215 on_boundary(poly, p) || ray_crossing(poly, p)
216}
217
218pub fn contains_by_ray_crossing(poly: &[[f64; 2]], p: [f64; 2]) -> bool {
229 if poly.len() < 3 {
230 return false;
231 }
232 ray_crossing(poly, p)
233}
234
235fn ray_crossing(poly: &[[f64; 2]], p: [f64; 2]) -> bool {
237 let n = poly.len();
238 let mut inside = false;
239 let mut j = n - 1;
240 for i in 0..n {
241 let (a, b) = (poly[i], poly[j]);
242 if (a[1] > p[1]) != (b[1] > p[1]) {
243 let x = (b[0] - a[0]) * (p[1] - a[1]) / (b[1] - a[1]) + a[0];
244 if p[0] < x {
245 inside = !inside;
246 }
247 }
248 j = i;
249 }
250 inside
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 fn ccw_square(side: f64) -> Vec<[f64; 2]> {
259 vec![[0.0, 0.0], [side, 0.0], [side, side], [0.0, side]]
260 }
261
262 #[test]
263 fn signed_area_is_positive_for_ccw_and_negative_for_cw() {
264 let ccw = ccw_square(10.0);
265 let mut cw = ccw.clone();
266 cw.reverse();
267 assert_eq!(signed_area(&ccw), 100.0);
268 assert_eq!(signed_area(&cw), -100.0);
269 assert_eq!(area(&cw), 100.0);
270 }
271
272 #[test]
273 fn signed_area_of_degenerate_input_is_zero() {
274 assert_eq!(signed_area(&[]), 0.0);
275 assert_eq!(signed_area(&[[0.0, 0.0], [1.0, 1.0]]), 0.0);
276 }
277
278 #[test]
279 fn area_xy_projects_and_area_3d_keeps_true_area() {
280 let pts = [
282 [0.0, 0.0, 0.0],
283 [1000.0, 0.0, 0.0],
284 [1000.0, 0.0, 1000.0],
285 [0.0, 0.0, 1000.0],
286 ];
287 assert_eq!(area_xy(&pts), 0.0);
288 assert!((area_3d(&pts) - 1_000_000.0).abs() < 1e-6);
289 }
290
291 #[test]
292 fn centroid_of_offset_rectangle_is_its_center() {
293 let rect = [
294 [100.0, 200.0],
295 [300.0, 200.0],
296 [300.0, 600.0],
297 [100.0, 600.0],
298 ];
299 let c = centroid(&rect);
300 assert!((c[0] - 200.0).abs() < 1e-9);
301 assert!((c[1] - 400.0).abs() < 1e-9);
302 }
303
304 #[test]
306 fn centroid_of_l_shape_differs_from_vertex_mean() {
307 let l = [
308 [0.0, 0.0],
309 [200.0, 0.0],
310 [200.0, 100.0],
311 [100.0, 100.0],
312 [100.0, 200.0],
313 [0.0, 200.0],
314 ];
315 let c = centroid(&l);
316 assert!((c[0] - 250.0 / 3.0).abs() < 1e-9, "{c:?}");
318 assert!((c[1] - 250.0 / 3.0).abs() < 1e-9, "{c:?}");
319 let mean_x = l.iter().map(|p| p[0]).sum::<f64>() / l.len() as f64;
320 assert!((c[0] - mean_x).abs() > 1.0, "単純平均と一致してはならない");
321 }
322
323 #[test]
327 fn centroid_of_collinear_points_falls_back_to_vertex_mean() {
328 let collinear = [
329 [1234.5, 6789.0],
330 [11234.5, 6789.0],
331 [21234.5, 6789.0],
332 [31234.5, 6789.0],
333 ];
334 let c = centroid(&collinear);
335 let mean_x = collinear.iter().map(|p| p[0]).sum::<f64>() / 4.0;
336 assert!(
337 c[0].is_finite() && c[1].is_finite(),
338 "発散してはならない: {c:?}"
339 );
340 assert!((c[0] - mean_x).abs() < 1e-6, "{c:?}");
341 assert!((c[1] - 6789.0).abs() < 1e-6, "{c:?}");
342 }
343
344 #[test]
345 fn centroid_of_fewer_than_three_points_is_vertex_mean() {
346 assert_eq!(centroid(&[]), [0.0, 0.0]);
347 assert_eq!(centroid(&[[2.0, 4.0]]), [2.0, 4.0]);
348 assert_eq!(centroid(&[[0.0, 0.0], [2.0, 4.0]]), [1.0, 2.0]);
349 }
350
351 #[test]
352 fn point_segment_dist_clamps_to_the_segment_ends() {
353 let (a, b) = ([0.0, 0.0], [10.0, 0.0]);
354 assert!((point_segment_dist([5.0, 3.0], a, b) - 3.0).abs() < 1e-12);
355 assert!((point_segment_dist([14.0, 3.0], a, b) - 5.0).abs() < 1e-12);
357 assert!((point_segment_dist([3.0, 4.0], a, a) - 5.0).abs() < 1e-12);
359 }
360
361 #[test]
362 fn point_segment_dist_sq_is_the_square_of_the_distance() {
363 let (p, a, b) = ([5.0, 3.0], [0.0, 0.0], [10.0, 0.0]);
364 let d = point_segment_dist(p, a, b);
365 assert!((point_segment_dist_sq(p, a, b) - d * d).abs() < 1e-12);
366 }
367
368 #[test]
370 fn the_three_containment_rules_differ_only_on_the_boundary_band() {
371 let sq = ccw_square(1000.0);
372 let inner = [500.0, 500.0];
373 let outer = [1500.0, 500.0];
374 let on_band = [500.0, 0.5];
376
377 for p in [inner, outer, on_band] {
378 let excl = contains_excluding_boundary(&sq, p);
379 let incl = contains_including_boundary(&sq, p);
380 let ray = contains_by_ray_crossing(&sq, p);
381 match p {
382 p if p == inner => assert!(excl && incl && ray, "内部は 3 つとも真"),
383 p if p == outer => assert!(!excl && !incl && !ray, "外部は 3 つとも偽"),
384 _ => {
385 assert!(!excl, "辺上は除外側で偽");
386 assert!(incl, "辺上は包含側で真");
387 assert!(ray, "レイキャストは帯を見ないので内部として真");
388 }
389 }
390 }
391 }
392
393 #[test]
394 fn containment_of_fewer_than_three_points_is_false() {
395 let seg = [[0.0, 0.0], [10.0, 0.0]];
396 assert!(!contains_excluding_boundary(&seg, [5.0, 0.0]));
397 assert!(!contains_including_boundary(&seg, [5.0, 0.0]));
398 assert!(!contains_by_ray_crossing(&seg, [5.0, 0.0]));
399 }
400
401 #[test]
402 fn bounding_box_spans_the_point_cloud() {
403 let pts = [[3.0, -1.0], [-2.0, 5.0], [1.0, 2.0]];
404 assert_eq!(bounding_box(&pts), ([-2.0, -1.0], [3.0, 5.0]));
405 assert_eq!(bounding_box(&[]), ([0.0, 0.0], [0.0, 0.0]));
406 }
407
408 #[test]
409 fn on_boundary_covers_vertices_and_edges() {
410 let sq = ccw_square(1000.0);
411 assert!(on_boundary(&sq, [0.0, 0.0]), "頂点");
412 assert!(on_boundary(&sq, [500.0, 0.0]), "辺上");
413 assert!(on_boundary(&sq, [500.0, 0.9]), "帯の内側");
414 assert!(!on_boundary(&sq, [500.0, 1.1]), "帯の外側");
415 assert!(!on_boundary(&[[0.0, 0.0]], [0.0, 0.0]), "頂点 2 個未満は偽");
416 }
417}