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