1use brepkit_math::aabb::Aabb3;
4use brepkit_math::vec::{Point2, Point3, Vec3};
5use brepkit_topology::Topology;
6use brepkit_topology::edge::EdgeCurve;
7use brepkit_topology::face::{FaceId, FaceSurface};
8
9use crate::CheckError;
10
11#[must_use]
15pub fn polygon_normal(verts: &[Point3]) -> Vec3 {
16 let mut nx = 0.0;
17 let mut ny = 0.0;
18 let mut nz = 0.0;
19 let n = verts.len();
20 for i in 0..n {
21 let j = (i + 1) % n;
22 let vi = verts[i];
23 let vj = verts[j];
24 nx += (vi.y() - vj.y()) * (vi.z() + vj.z());
25 ny += (vi.z() - vj.z()) * (vi.x() + vj.x());
26 nz += (vi.x() - vj.x()) * (vi.y() + vj.y());
27 }
28 let len = (nx.mul_add(nx, ny.mul_add(ny, nz * nz))).sqrt();
29 if len < 1e-30 {
30 Vec3::new(0.0, 0.0, 1.0)
31 } else {
32 Vec3::new(nx / len, ny / len, nz / len)
33 }
34}
35
36pub const CLOSED_CURVE_SAMPLES: usize = 32;
38
39pub const OPEN_CURVE_SAMPLES: usize = 32;
42
43#[must_use]
47pub fn sample_edge_curve(curve: &EdgeCurve, n: usize) -> Vec<Point3> {
48 match curve {
49 EdgeCurve::Circle(c) => (0..n)
50 .map(|i| {
51 let t = std::f64::consts::TAU * (i as f64) / (n as f64);
52 c.evaluate(t)
53 })
54 .collect(),
55 EdgeCurve::Ellipse(e) => (0..n)
56 .map(|i| {
57 let t = std::f64::consts::TAU * (i as f64) / (n as f64);
58 e.evaluate(t)
59 })
60 .collect(),
61 EdgeCurve::NurbsCurve(nc) => {
62 let (u0, u1) = nc.domain();
63 let start_pt = nc.evaluate(u0);
64 let end_pt = nc.evaluate(u1);
65 let is_closed = (start_pt - end_pt).length() < 1e-6;
66 let divisor = if is_closed { n } else { n - 1 };
67 (0..n)
68 .map(|i| {
69 let t = u0 + (u1 - u0) * (i as f64) / (divisor as f64);
70 nc.evaluate(t)
71 })
72 .collect()
73 }
74 EdgeCurve::Line => vec![],
75 }
76}
77
78pub fn face_polygon(topo: &Topology, face_id: FaceId) -> Result<Vec<Point3>, CheckError> {
85 let face = topo.face(face_id)?;
86 wire_polygon(topo, face.outer_wire())
87}
88
89pub fn wire_polygon(
101 topo: &Topology,
102 wire_id: brepkit_topology::wire::WireId,
103) -> Result<Vec<Point3>, CheckError> {
104 let wire = topo.wire(wire_id)?;
105 let mut pts: Vec<Point3> = Vec::new();
106 let mut prev_end: Option<brepkit_topology::vertex::VertexId> = None;
107
108 for oe in wire.edges() {
109 let edge = topo.edge(oe.edge())?;
110 let curve = edge.curve();
111 let start_vid = edge.start();
112 let end_vid = edge.end();
113 let forward = match prev_end {
114 Some(pe) if start_vid == pe && end_vid != pe => true,
115 Some(pe) if end_vid == pe && start_vid != pe => false,
116 _ if start_vid == end_vid => oe.is_forward(),
120 _ => match pts.last() {
127 Some(&last) => {
128 let sp = topo.vertex(start_vid)?.point();
129 let ep = topo.vertex(end_vid)?.point();
130 (sp - last).length_squared() <= (ep - last).length_squared()
131 }
132 None => oe.is_forward(),
133 },
134 };
135 let is_closed_edge = start_vid == end_vid
136 && matches!(
137 curve,
138 EdgeCurve::Circle(_) | EdgeCurve::Ellipse(_) | EdgeCurve::NurbsCurve(_)
139 );
140 if is_closed_edge {
141 let seam_pt = topo.vertex(start_vid)?.point();
145 #[allow(clippy::cast_precision_loss)]
149 let params = |n: usize, period: f64| -> Vec<f64> {
150 if forward {
151 (0..n).map(|i| period * (i as f64) / (n as f64)).collect()
152 } else {
153 (1..=n)
154 .rev()
155 .map(|i| period * (i as f64) / (n as f64))
156 .collect()
157 }
158 };
159 let sampled: Vec<Point3> = match curve {
160 EdgeCurve::Circle(c) => {
161 let t0 = c.project(seam_pt);
162 params(CLOSED_CURVE_SAMPLES, std::f64::consts::TAU)
163 .into_iter()
164 .map(|dt| c.evaluate(t0 + dt))
165 .collect()
166 }
167 EdgeCurve::Ellipse(e) => {
168 let t0 = e.project(seam_pt);
169 params(CLOSED_CURVE_SAMPLES, std::f64::consts::TAU)
170 .into_iter()
171 .map(|dt| e.evaluate(t0 + dt))
172 .collect()
173 }
174 EdgeCurve::NurbsCurve(nc) => {
175 let (u0, u1) = nc.domain();
176 let span = u1 - u0;
177 if span.is_finite() && span > 0.0 {
178 let t0 = nurbs_seam_parameter(nc, seam_pt, u0, u1);
179 params(CLOSED_CURVE_SAMPLES, span)
180 .into_iter()
181 .map(|dt| nc.evaluate(u0 + (t0 - u0 + dt).rem_euclid(span)))
182 .collect()
183 } else {
184 let mut s = sample_edge_curve(curve, CLOSED_CURVE_SAMPLES);
185 if !forward {
186 s.reverse();
187 }
188 s
189 }
190 }
191 EdgeCurve::Line => vec![],
192 };
193 pts.extend(sampled);
194 prev_end = Some(start_vid);
195 } else if matches!(curve, EdgeCurve::Line) {
196 let vid = if forward { start_vid } else { end_vid };
197 pts.push(topo.vertex(vid)?.point());
198 prev_end = Some(if forward { end_vid } else { start_vid });
199 } else {
200 let sp = topo.vertex(start_vid)?.point();
209 let ep = topo.vertex(end_vid)?.point();
210 let (t0, t1) = curve.domain_with_endpoints(sp, ep);
211 let n = OPEN_CURVE_SAMPLES;
212 #[allow(clippy::cast_precision_loss)]
213 let mut seq: Vec<Point3> = (0..=n)
214 .map(|i| {
215 curve.evaluate_with_endpoints(t0 + (t1 - t0) * (i as f64) / (n as f64), sp, ep)
216 })
217 .collect();
218 let traversal_start = if forward { sp } else { ep };
224 if (seq[0] - traversal_start).length_squared()
225 > (seq[n] - traversal_start).length_squared()
226 {
227 seq.reverse();
228 }
229 seq.pop();
230 pts.extend(seq);
231 prev_end = Some(if forward { end_vid } else { start_vid });
232 }
233 }
234
235 Ok(pts)
236}
237
238pub fn expand_aabb_for_surface(aabb: &mut Aabb3, surface: &FaceSurface) {
245 match surface {
246 FaceSurface::Sphere(s) => {
247 let c = s.center();
248 let r = s.radius();
249 aabb_include(aabb, Point3::new(c.x() - r, c.y() - r, c.z() - r));
250 aabb_include(aabb, Point3::new(c.x() + r, c.y() + r, c.z() + r));
251 }
252 FaceSurface::Cylinder(c) => {
253 let origin = c.origin();
254 let axis = c.axis();
255 let r = c.radius();
256 let rx = r * (1.0 - axis.x() * axis.x()).max(0.0).sqrt();
257 let ry = r * (1.0 - axis.y() * axis.y()).max(0.0).sqrt();
258 let rz = r * (1.0 - axis.z() * axis.z()).max(0.0).sqrt();
259 for corner in [aabb.min, aabb.max] {
260 let rel = Vec3::new(
261 corner.x() - origin.x(),
262 corner.y() - origin.y(),
263 corner.z() - origin.z(),
264 );
265 let t = axis.dot(rel);
266 let coa = Point3::new(
267 origin.x() + axis.x() * t,
268 origin.y() + axis.y() * t,
269 origin.z() + axis.z() * t,
270 );
271 aabb_include(aabb, Point3::new(coa.x() - rx, coa.y() - ry, coa.z() - rz));
272 aabb_include(aabb, Point3::new(coa.x() + rx, coa.y() + ry, coa.z() + rz));
273 }
274 }
275 FaceSurface::Torus(t) => {
276 let c = t.center();
277 let outer_r = t.major_radius() + t.minor_radius();
278 let axis = t.z_axis();
279 let axial_offset = Vec3::new(
280 axis.x() * t.minor_radius(),
281 axis.y() * t.minor_radius(),
282 axis.z() * t.minor_radius(),
283 );
284 aabb_include(
285 aabb,
286 Point3::new(
287 c.x() - outer_r + axial_offset.x().min(0.0),
288 c.y() - outer_r + axial_offset.y().min(0.0),
289 c.z() - outer_r + axial_offset.z().min(0.0),
290 ),
291 );
292 aabb_include(
293 aabb,
294 Point3::new(
295 c.x() + outer_r + axial_offset.x().max(0.0),
296 c.y() + outer_r + axial_offset.y().max(0.0),
297 c.z() + outer_r + axial_offset.z().max(0.0),
298 ),
299 );
300 }
301 FaceSurface::Nurbs(nurbs) => {
302 let (u_min, u_max) = nurbs.domain_u();
303 let (v_min, v_max) = nurbs.domain_v();
304 let n_samples = 8;
305 for iu in 0..=n_samples {
306 let u = u_min + (u_max - u_min) * (iu as f64) / (n_samples as f64);
307 for iv in 0..=n_samples {
308 let v = v_min + (v_max - v_min) * (iv as f64) / (n_samples as f64);
309 aabb_include(aabb, nurbs.evaluate(u, v));
310 }
311 }
312 }
313 FaceSurface::Plane { .. } | FaceSurface::Cone(_) => {}
314 }
315}
316
317fn aabb_include(aabb: &mut Aabb3, p: Point3) {
319 *aabb = aabb.union(Aabb3 { min: p, max: p });
320}
321
322const SEAM_COINCIDENT_SQ: f64 = 1e-14;
325
326fn nurbs_seam_parameter(
332 nc: &brepkit_math::nurbs::curve::NurbsCurve,
333 seam_pt: Point3,
334 u0: f64,
335 u1: f64,
336) -> f64 {
337 if (nc.evaluate(u0) - seam_pt).length_squared() <= SEAM_COINCIDENT_SQ {
338 return u0;
339 }
340 brepkit_math::nurbs::projection::project_point_to_curve(nc, seam_pt, 1e-9)
341 .map_or(u0, |proj| proj.parameter.clamp(u0, u1))
342}
343
344fn expand_aabb_for_curve(aabb: &mut Aabb3, curve: &EdgeCurve) {
353 match curve {
354 EdgeCurve::Line => {}
355 EdgeCurve::Circle(c) => {
356 let cen = c.center();
357 let r = c.radius();
358 let (u, v) = (c.u_axis(), c.v_axis());
359 let ext = [
360 r * u.x().hypot(v.x()),
361 r * u.y().hypot(v.y()),
362 r * u.z().hypot(v.z()),
363 ];
364 aabb_include(
365 aabb,
366 Point3::new(cen.x() - ext[0], cen.y() - ext[1], cen.z() - ext[2]),
367 );
368 aabb_include(
369 aabb,
370 Point3::new(cen.x() + ext[0], cen.y() + ext[1], cen.z() + ext[2]),
371 );
372 }
373 EdgeCurve::Ellipse(e) => {
374 let cen = e.center();
375 let (a, b) = (e.semi_major(), e.semi_minor());
376 let (u, v) = (e.u_axis(), e.v_axis());
377 let ext = [
378 (a * u.x()).hypot(b * v.x()),
379 (a * u.y()).hypot(b * v.y()),
380 (a * u.z()).hypot(b * v.z()),
381 ];
382 aabb_include(
383 aabb,
384 Point3::new(cen.x() - ext[0], cen.y() - ext[1], cen.z() - ext[2]),
385 );
386 aabb_include(
387 aabb,
388 Point3::new(cen.x() + ext[0], cen.y() + ext[1], cen.z() + ext[2]),
389 );
390 }
391 EdgeCurve::NurbsCurve(nc) => {
392 for &p in nc.control_points() {
393 aabb_include(aabb, p);
394 }
395 }
396 }
397}
398
399pub fn face_aabb(topo: &Topology, face_id: FaceId) -> Result<Aabb3, CheckError> {
408 let face = topo.face(face_id)?;
409 let wire = topo.wire(face.outer_wire())?;
410 let mut points = Vec::new();
411 for oe in wire.edges() {
412 let edge = topo.edge(oe.edge())?;
413 points.push(topo.vertex(edge.start())?.point());
414 points.push(topo.vertex(edge.end())?.point());
415 }
416 let mut aabb = Aabb3::try_from_points(points.iter().copied())
417 .ok_or_else(|| CheckError::ClassificationFailed("face has no vertices".into()))?;
418 for oe in wire.edges() {
419 let edge = topo.edge(oe.edge())?;
420 expand_aabb_for_curve(&mut aabb, edge.curve());
421 }
422 expand_aabb_for_surface(&mut aabb, face.surface());
423 Ok(aabb)
424}
425
426#[must_use]
429pub fn point_in_polygon_3d(point: &Point3, polygon: &[Point3], normal: &Vec3) -> bool {
430 use brepkit_math::predicates::point_in_polygon;
431
432 let ax = normal.x().abs();
433 let ay = normal.y().abs();
434 let az = normal.z().abs();
435
436 let (proj_pt, proj_poly): (Point2, Vec<Point2>) = if az >= ax && az >= ay {
437 (
438 Point2::new(point.x(), point.y()),
439 polygon.iter().map(|p| Point2::new(p.x(), p.y())).collect(),
440 )
441 } else if ay >= ax {
442 (
443 Point2::new(point.x(), point.z()),
444 polygon.iter().map(|p| Point2::new(p.x(), p.z())).collect(),
445 )
446 } else {
447 (
448 Point2::new(point.y(), point.z()),
449 polygon.iter().map(|p| Point2::new(p.y(), p.z())).collect(),
450 )
451 };
452
453 point_in_polygon(proj_pt, &proj_poly)
454}
455
456#[cfg(test)]
457mod tests {
458 #![allow(clippy::unwrap_used, clippy::expect_used)]
459
460 use super::*;
461 use brepkit_geometry::convert::curve_to_nurbs::circle_to_nurbs;
462 use brepkit_math::curves::Circle3D;
463 use brepkit_topology::edge::Edge;
464 use brepkit_topology::vertex::Vertex;
465 use brepkit_topology::wire::{OrientedEdge, Wire};
466
467 #[test]
468 fn wire_polygon_anchors_closed_nurbs_rim_at_seam_vertex() {
469 let radius = 2.0;
470 let circle =
471 Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), radius).unwrap();
472 let nurbs = circle_to_nurbs(&circle, 0.0, std::f64::consts::TAU).unwrap();
473 let seam_pt = circle.evaluate(1.1);
475
476 let mut topo = Topology::new();
477 let v = topo.add_vertex(Vertex::new(seam_pt, 1e-7));
478 let e = topo.add_edge(Edge::new(v, v, EdgeCurve::NurbsCurve(nurbs)));
479 let wire = topo.add_wire(Wire::new(vec![OrientedEdge::new(e, true)], true).unwrap());
480
481 let pts = wire_polygon(&topo, wire).unwrap();
482 assert_eq!(pts.len(), CLOSED_CURVE_SAMPLES);
483 assert!(
484 (pts[0] - seam_pt).length() < 1e-6,
485 "first sample {:?} not anchored at seam {:?}",
486 pts[0],
487 seam_pt
488 );
489 for p in &pts {
490 assert!(
491 (p.x().hypot(p.y()) - radius).abs() < 1e-9,
492 "sample off the rim circle: {p:?}"
493 );
494 }
495 }
496}