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