1mod chains;
18mod cut_regions;
19mod plane_points;
20mod regions;
21mod request;
22mod ring_nesting;
23
24use std::collections::{BTreeMap, BTreeSet};
25
26use axiolid_contracts::{
27 Backend, BackendDescriptor, BackendId, CancellationGranularity, ExecutionOptions,
28 ExecutionTarget, GeomError, GeomResult, ScratchRequirement, Sign,
29};
30use axiolid_core::{Frame3, Point2, Point3};
31use axiolid_mesh::TriMesh;
32use axiolid_mesh_section_contract::{
33 MeshPlaneSection, PlaneSection, SectionEvidence, SectionLimits, SectionOutcome, SectionRequest,
34};
35
36use crate::orient3::orient3d;
37use crate::orientation::orient2d;
38use chains::assemble_contours;
39
40#[derive(Debug, Default, Clone, Copy)]
42pub struct ScalarSection;
43
44impl ScalarSection {
45 pub const ID: BackendId = BackendId::new("scalar-section");
47
48 #[must_use]
50 pub const fn new() -> Self {
51 Self
52 }
53}
54
55impl Backend for ScalarSection {
56 fn descriptor(&self) -> BackendDescriptor {
57 BackendDescriptor::new(Self::ID, ExecutionTarget::PortableCpu)
58 }
59}
60
61impl MeshPlaneSection for ScalarSection {
62 fn scratch_requirement(&self) -> ScratchRequirement {
63 ScratchRequirement::PerElement {
66 bytes_per_element: 512,
67 }
68 }
69
70 fn request_scratch_requirement(&self) -> ScratchRequirement {
71 ScratchRequirement::PerElement {
86 bytes_per_element: 1536,
87 }
88 }
89
90 fn cancellation_granularity(&self) -> CancellationGranularity {
91 CancellationGranularity::Incremental
92 }
93
94 fn section_request(
95 &self,
96 mesh: &TriMesh,
97 request: &SectionRequest,
98 options: &ExecutionOptions,
99 ) -> GeomResult<PlaneSection> {
100 request::plane_section(mesh, request, options)
101 }
102
103 fn section(
104 &self,
105 mesh: &TriMesh,
106 frame: Frame3,
107 limits: SectionLimits,
108 options: &ExecutionOptions,
109 ) -> GeomResult<SectionOutcome> {
110 options.check_cancelled()?;
111 check_source_limits(mesh, limits)?;
112
113 let plane = ExactSectionPlane::new(frame)?;
114 let classifications = classify_vertices(mesh, plane)?;
115
116 let mut segments = BTreeSet::<Segment>::new();
117 let mut on_plane_edges = BTreeMap::<EdgeKey, Vec<Sign>>::new();
118 for (triangle_index, triangle) in mesh.triangles().enumerate() {
119 options.check_cancelled()?;
120 let vertices = [
121 mesh_index(triangle[0])?,
122 mesh_index(triangle[1])?,
123 mesh_index(triangle[2])?,
124 ];
125 let signs = vertices.map(|index| classifications[index].sign);
126 let zero_count = signs.iter().filter(|&&sign| sign == Sign::Zero).count();
127 match zero_count {
128 3 => {
129 return Err(GeomError::Degenerate(format!(
130 "section plane contains source triangle {triangle_index}; a two-dimensional overlap is not a curve"
131 )))
132 }
133 2 => {
134 let mut zeros = triangle
135 .into_iter()
136 .zip(signs)
137 .filter_map(|(index, sign)| (sign == Sign::Zero).then_some(index));
138 let left = zeros.next().ok_or_else(internal_topology_error)?;
139 let right = zeros.next().ok_or_else(internal_topology_error)?;
140 let third = signs
141 .into_iter()
142 .find(|&sign| sign != Sign::Zero)
143 .ok_or_else(internal_topology_error)?;
144 on_plane_edges
145 .entry(EdgeKey::new(left, right))
146 .or_default()
147 .push(third);
148 }
149 1 => {
150 let zero_corner = (0..3)
151 .find(|&corner| signs[corner] == Sign::Zero)
152 .ok_or_else(internal_topology_error)?;
153 let (first, second) = match zero_corner {
154 0 => (1, 2),
155 1 => (0, 2),
156 2 => (0, 1),
157 _ => return Err(internal_topology_error()),
158 };
159 if opposite(signs[first], signs[second]) {
160 let segment = Segment::new(
161 NodeKey::Vertex(triangle[zero_corner]),
162 NodeKey::Edge(EdgeKey::new(triangle[first], triangle[second])),
163 )?;
164 insert_segment(&mut segments, segment, limits)?;
165 }
166 }
167 0 => {
168 let mut crossing = [None, None];
169 let mut crossing_count = 0usize;
170 for (left, right) in [(0, 1), (1, 2), (2, 0)] {
171 if opposite(signs[left], signs[right]) {
172 if crossing_count >= crossing.len() {
173 return Err(internal_topology_error());
174 }
175 crossing[crossing_count] = Some(NodeKey::Edge(EdgeKey::new(
176 triangle[left],
177 triangle[right],
178 )));
179 crossing_count += 1;
180 }
181 }
182 match (crossing[0], crossing[1]) {
183 (Some(first), Some(second)) => insert_segment(
184 &mut segments,
185 Segment::new(first, second)?,
186 limits,
187 )?,
188 (None, None) => {}
189 _ => return Err(internal_topology_error()),
190 }
191 }
192 _ => return Err(internal_topology_error()),
193 }
194 }
195
196 for (edge, incident_signs) in on_plane_edges {
197 options.check_cancelled()?;
198 if incident_signs.len() != 2 {
199 return Err(GeomError::NotManifold(format!(
200 "on-plane mesh edge {:?} has {} incident triangles",
201 edge,
202 incident_signs.len()
203 )));
204 }
205 if opposite(incident_signs[0], incident_signs[1]) {
206 insert_segment(
207 &mut segments,
208 Segment::new(NodeKey::Vertex(edge.0), NodeKey::Vertex(edge.1))?,
209 limits,
210 )?;
211 }
212 }
213
214 let contours =
215 assemble_contours(mesh, frame, &classifications, &segments, limits, options)?;
216 let output_vertices = contours.iter().map(|contour| contour.points.len()).sum();
217 let evidence =
218 SectionEvidence::input_mesh(mesh.triangle_count(), output_vertices, contours.len());
219 Ok(SectionOutcome::new(frame, contours, evidence))
220 }
221}
222
223#[derive(Debug, Clone, Copy)]
224struct Classification {
225 sign: Sign,
226 distance: f64,
227}
228
229#[derive(Debug, Clone, Copy)]
230struct ExactSectionPlane {
231 origin: Point3,
232 x_point: Point3,
233 y_point: Point3,
234 normal: axiolid_core::Vec3,
235}
236
237impl ExactSectionPlane {
238 fn new(frame: Frame3) -> GeomResult<Self> {
239 let x_point = frame.origin + frame.x;
240 let y_point = frame.origin + frame.y;
241 if !x_point.is_finite()
242 || !y_point.is_finite()
243 || x_point == frame.origin
244 || y_point == frame.origin
245 || x_point == y_point
246 {
247 return Err(GeomError::Degenerate(
248 "section frame cannot resolve a finite affine plane at this coordinate magnitude"
249 .into(),
250 ));
251 }
252 let normal = (x_point - frame.origin).cross(y_point - frame.origin);
253 let normal_length = normal.length();
254 if !normal_length.is_finite() || normal_length == 0.0 {
255 return Err(GeomError::Degenerate(
256 "section affine plane has no finite normal".into(),
257 ));
258 }
259 Ok(Self {
260 origin: frame.origin,
261 x_point,
262 y_point,
263 normal: normal / normal_length,
264 })
265 }
266
267 fn classify(self, point: Point3) -> GeomResult<Classification> {
268 let sign = match orient3d(self.origin, self.x_point, self.y_point, point) {
269 axiolid_contracts::Certified::Certain { sign, .. } => sign,
270 _ => {
271 return Err(GeomError::Degenerate(
272 "certified plane-side predicate returned an uncertain sign".into(),
273 ))
274 }
275 };
276 let distance = self.normal.dot(point - self.origin);
277 if !distance.is_finite() {
278 return Err(GeomError::Degenerate(
279 "section signed distance is not finite".into(),
280 ));
281 }
282 if sign != Sign::Zero && distance == 0.0 {
283 return Err(GeomError::Degenerate(
284 "section interpolation lost a certified nonzero plane offset".into(),
285 ));
286 }
287 Ok(Classification { sign, distance })
288 }
289}
290
291fn classify_vertices(mesh: &TriMesh, plane: ExactSectionPlane) -> GeomResult<Vec<Classification>> {
293 let mut classifications = Vec::new();
294 classifications
295 .try_reserve_exact(mesh.positions.len())
296 .map_err(|_| GeomError::BudgetExceeded { resource: "memory" })?;
297 for &point in &mesh.positions {
298 let classification = plane.classify(point)?;
299 classifications.push(classification);
300 }
301 Ok(classifications)
302}
303
304fn opposite(left: Sign, right: Sign) -> bool {
305 matches!(
306 (left, right),
307 (Sign::Negative, Sign::Positive) | (Sign::Positive, Sign::Negative)
308 )
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
312struct EdgeKey(u32, u32);
313
314impl EdgeKey {
315 fn new(left: u32, right: u32) -> Self {
316 Self(left.min(right), left.max(right))
317 }
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
321enum NodeKey {
322 Vertex(u32),
323 Edge(EdgeKey),
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
327struct Segment(NodeKey, NodeKey);
328
329impl Segment {
330 fn new(left: NodeKey, right: NodeKey) -> GeomResult<Self> {
331 if left == right {
332 return Err(GeomError::Degenerate(
333 "plane section collapsed a segment to one source-topology node".into(),
334 ));
335 }
336 Ok(Self(left.min(right), left.max(right)))
337 }
338}
339
340fn insert_segment(
341 segments: &mut BTreeSet<Segment>,
342 segment: Segment,
343 limits: SectionLimits,
344) -> GeomResult<()> {
345 if segments.contains(&segment) {
346 return Err(GeomError::NotManifold(
347 "two source faces produced the same non-coplanar section segment".into(),
348 ));
349 }
350 if segments.len() >= limits.max_output_vertices {
351 return Err(GeomError::BudgetExceeded {
352 resource: "section output vertices",
353 });
354 }
355 segments.insert(segment);
356 Ok(())
357}
358
359fn mesh_index(index: u32) -> GeomResult<usize> {
360 usize::try_from(index)
361 .map_err(|_| GeomError::InvalidInput("mesh index does not fit usize".into()))
362}
363
364fn node_point(
365 mesh: &TriMesh,
366 classifications: &[Classification],
367 node: NodeKey,
368) -> GeomResult<Point3> {
369 match node {
370 NodeKey::Vertex(index) => Ok(mesh.positions[mesh_index(index)?]),
371 NodeKey::Edge(EdgeKey(left, right)) => {
372 let a = mesh.positions[mesh_index(left)?];
373 let b = mesh.positions[mesh_index(right)?];
374 let da = classifications[mesh_index(left)?].distance.abs();
375 let db = classifications[mesh_index(right)?].distance.abs();
376 if !(da > 0.0 && db > 0.0 && da.is_finite() && db.is_finite()) {
377 return Err(GeomError::Degenerate(
378 "crossing edge has no representable endpoint distance".into(),
379 ));
380 }
381 let t = if da >= db {
382 1.0 / (1.0 + db / da)
383 } else {
384 let ratio = da / db;
385 ratio / (1.0 + ratio)
386 };
387 let point = a + (b - a) * t;
388 if !point.is_finite() {
389 return Err(GeomError::Degenerate(
390 "edge-plane intersection exceeded finite arithmetic".into(),
391 ));
392 }
393 Ok(point)
394 }
395 }
396}
397
398fn simplify_collinear(points: &mut Vec<Point2>, tolerance: f64) {
399 loop {
400 if points.len() <= 3 {
401 return;
402 }
403 let mut removed = false;
404 for index in 0..points.len() {
405 let previous = points[(index + points.len() - 1) % points.len()];
406 let current = points[index];
407 let next = points[(index + 1) % points.len()];
408 let chord = next - previous;
409 let scale = chord.length();
410 let cross = (current - previous).perp_dot(chord).abs();
411 let within_tolerance = scale > 0.0 && cross <= tolerance * scale;
412 if orient2d(previous, current, next).sign() == Some(Sign::Zero) || within_tolerance {
413 points.remove(index);
414 removed = true;
415 break;
416 }
417 }
418 if !removed {
419 return;
420 }
421 }
422}
423
424fn signed_area(points: &[Point2]) -> f64 {
425 let mut twice = 0.0;
426 for index in 0..points.len() {
427 let current = points[index];
428 let next = points[(index + 1) % points.len()];
429 twice += current.x * next.y - current.y * next.x;
430 }
431 twice * 0.5
432}
433
434fn exact_area_sign(points: &[Point2]) -> GeomResult<Sign> {
443 use axiolid_predicates::arithmetic::{expansion_sign, expansion_sum};
444 use axiolid_predicates::two_product;
445
446 let in_range = |value: f64| {
447 let encoded = ((value.to_bits() >> 52) & 0x7ff) as i32;
448 value == 0.0 || (encoded != 0 && (-450..=450).contains(&(encoded - 1023)))
449 };
450 if !points.iter().all(|p| in_range(p.x) && in_range(p.y)) {
451 return Err(GeomError::Degenerate(
452 "section chain coordinates are outside the exact area range".into(),
453 ));
454 }
455 let mut twice = vec![0.0];
456 for index in 0..points.len() {
457 let (current, next) = (points[index], points[(index + 1) % points.len()]);
458 if !(current.is_finite() && next.is_finite()) {
459 return Err(GeomError::Degenerate(
460 "section chain has a non-finite point".into(),
461 ));
462 }
463 let (forward, forward_error) = two_product(current.x, next.y);
464 let (backward, backward_error) = two_product(next.x, current.y);
465 twice = expansion_sum(&twice, &[forward_error, forward]);
466 twice = expansion_sum(&twice, &[-backward_error, -backward]);
467 }
468 Ok(expansion_sign(&twice))
469}
470
471fn point_order(left: &Point2, right: &Point2) -> std::cmp::Ordering {
472 left.x
473 .total_cmp(&right.x)
474 .then_with(|| left.y.total_cmp(&right.y))
475}
476
477fn sequence_order(left: &[Point2], right: &[Point2]) -> std::cmp::Ordering {
479 left.iter()
480 .zip(right)
481 .map(|(a, b)| point_order(a, b))
482 .find(|order| order.is_ne())
483 .unwrap_or_else(|| left.len().cmp(&right.len()))
484}
485
486fn check_source_limits(mesh: &TriMesh, limits: SectionLimits) -> GeomResult<()> {
487 if mesh.positions.len() > limits.max_source_vertices {
488 return Err(GeomError::BudgetExceeded {
489 resource: "section source vertices",
490 });
491 }
492 if mesh.triangle_count() > limits.max_source_triangles {
493 return Err(GeomError::BudgetExceeded {
494 resource: "section source triangles",
495 });
496 }
497 Ok(())
498}
499
500fn internal_topology_error() -> GeomError {
501 GeomError::BackendContractViolation {
502 backend: ScalarSection::ID,
503 detail: "internal section topology state is inconsistent".into(),
504 }
505}
506
507fn project_local(frame: Frame3, world: Point3) -> GeomResult<Point2> {
511 let local = world - frame.origin;
512 let point = Point2::new(local.dot(frame.x), local.dot(frame.y));
513 if point.is_finite() {
514 Ok(point)
515 } else {
516 Err(GeomError::Degenerate(
517 "section projection exceeded finite arithmetic".into(),
518 ))
519 }
520}
521
522fn orient_sign(a: Point2, b: Point2, c: Point2) -> GeomResult<Sign> {
524 orient2d(a, b, c).sign().ok_or_else(|| {
525 GeomError::Degenerate("certified orientation predicate returned an uncertain sign".into())
526 })
527}
528
529fn sign_times(left: Sign, right: Sign) -> Sign {
531 if left == Sign::Zero || right == Sign::Zero {
532 Sign::Zero
533 } else if left == right {
534 Sign::Positive
535 } else {
536 Sign::Negative
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543
544 #[test]
545 fn the_area_sign_is_exact_where_the_float_area_is_wrong() {
546 let points = [
549 Point2::new(100000002.228125, 100000001.5671875),
550 Point2::new(100000001.9375, 100000001.275),
551 Point2::new(100000002.615625, 100000000.5578125),
552 ];
553 assert!(signed_area(&points) < 0.0);
554 assert_eq!(exact_area_sign(&points).expect("sign"), Sign::Positive);
555 let reversed = [points[2], points[1], points[0]];
556 assert_eq!(exact_area_sign(&reversed).expect("sign"), Sign::Negative);
557 let flat = [points[0], points[0], points[1]];
558 assert_eq!(exact_area_sign(&flat).expect("sign"), Sign::Zero);
559 }
560
561 #[test]
562 fn the_area_sign_refuses_coordinates_whose_products_leave_the_range() {
563 let tiny = [
565 Point2::new(-8.036185191855754e-160, -8.029313620019926e-160),
566 Point2::new(-8.036193993089111e-160, -8.028301829579562e-160),
567 Point2::new(-8.019240586512659e-160, -8.033664284419165e-160),
568 ];
569 assert!(matches!(
570 exact_area_sign(&tiny),
571 Err(GeomError::Degenerate(_))
572 ));
573 let huge = [
575 Point2::new(1e200, 1e200),
576 Point2::new(2e200, 1e200),
577 Point2::new(1e200, 3e200),
578 ];
579 assert!(matches!(
580 exact_area_sign(&huge),
581 Err(GeomError::Degenerate(_))
582 ));
583 let large = [
585 Point2::new(1e100, 1e100),
586 Point2::new(2e100, 1e100),
587 Point2::new(1e100, 3e100),
588 ];
589 assert_eq!(exact_area_sign(&large).expect("sign"), Sign::Positive);
590 }
591
592 #[test]
593 fn exact_plane_sign_keeps_a_tiny_nonzero_binary64_offset() {
594 let frame = Frame3 {
595 origin: Point3::ZERO,
596 x: Point3::X,
597 y: Point3::Y,
598 z: Point3::Z,
599 };
600 let plane = ExactSectionPlane::new(frame).expect("resolvable plane");
601 let point = Point3::new(1.0, -1.0, f64::from_bits(1));
602 let certified = orient3d(plane.origin, plane.x_point, plane.y_point, point);
603 assert!(matches!(
604 certified,
605 axiolid_contracts::Certified::Certain {
606 precision: axiolid_contracts::Precision::Exact,
607 ..
608 }
609 ));
610 let positive = plane.classify(point).expect("finite subnormal");
611 assert_ne!(positive.sign, Sign::Zero);
612 }
613}