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