1use std::cmp::Ordering;
25use std::collections::BTreeMap;
26
27use axiolid_contracts::{DisplacementBound, ExecutionOptions, GeomError, GeomResult, Sign};
28use axiolid_core::Point2;
29use axiolid_exact::{Arith, Dyadic};
30
31use crate::arrangement::{
32 arrange, charge, sort_exact, Arrangement, PairBudget, PieceId, Segment, VertexId,
33};
34use crate::points::{inconsistent, ExactPoints, PointId};
35use crate::ring::point_order;
36use crate::trace::HalfEdge;
37
38pub const HOT_POINT_SCRATCH_BYTES: usize = 256;
41
42pub const FRAGMENT_SCRATCH_BYTES: usize = 64;
44
45fn exact(value: f64) -> GeomResult<Dyadic> {
46 Dyadic::try_from_f64(value).ok_or_else(|| GeomError::Degenerate("a cell is not finite".into()))
47}
48
49#[derive(Debug, Clone)]
52struct Cell {
53 center: Point2,
54 lo: [Dyadic; 2],
56 hi: [Dyadic; 2],
58 closed: [bool; 2],
60 half_width: f64,
63 reach: [f64; 4],
66}
67
68fn gaps(value: f64) -> (f64, f64) {
71 let (down, up) = (value.next_down(), value.next_up());
72 match (down.is_finite(), up.is_finite()) {
73 (true, true) => (value - down, up - value),
74 (false, _) => (up - value, up - value),
75 (_, false) => (value - down, value - down),
76 }
77}
78
79fn cell(center: Point2) -> GeomResult<Cell> {
80 let center = Point2::new(center.x + 0.0, center.y + 0.0);
81 let half = exact(0.5)?;
82 let mut lo = [Dyadic::zero(), Dyadic::zero()];
83 let mut hi = [Dyadic::zero(), Dyadic::zero()];
84 let mut closed = [false; 2];
85 let mut reach = [0.0; 4];
86 let mut half_width = 0.0f64;
87 for axis in 0..2 {
88 let c = if axis == 0 { center.x } else { center.y };
89 let (down, up) = gaps(c);
90 let c_exact = exact(c)?;
91 lo[axis] = c_exact.sub(&exact(down)?.mul(&half));
92 hi[axis] = c_exact.add(&exact(up)?.mul(&half));
93 closed[axis] = c.abs().to_bits() & 1 == 0;
94 reach[axis] = c - down;
95 reach[axis + 2] = c + up;
96 let gap = up.max(down);
97 let halved = gap * 0.5;
98 half_width = half_width.max(if halved > 0.0 { halved } else { gap });
99 }
100 Ok(Cell {
101 center,
102 lo,
103 hi,
104 closed,
105 half_width,
106 reach,
107 })
108}
109
110#[derive(Debug, Clone)]
113struct Param {
114 num: Dyadic,
115 den: Dyadic,
116 open: bool,
117}
118
119fn compare_values(a: &Param, b: &Param) -> GeomResult<Ordering> {
120 let sign = a
121 .num
122 .mul(&b.den)
123 .sub(&b.num.mul(&a.den))
124 .sign()
125 .ok_or_else(inconsistent)?;
126 Ok(match sign {
127 Sign::Positive => Ordering::Greater,
128 Sign::Negative => Ordering::Less,
129 _ => Ordering::Equal,
130 })
131}
132
133fn compare_params(a: &Param, b: &Param) -> GeomResult<Ordering> {
135 Ok(compare_values(a, b)?.then(a.open.cmp(&b.open)))
136}
137
138fn meets(from: &[Dyadic; 3], to: &[Dyadic; 3], cell: &Cell) -> GeomResult<Option<Param>> {
142 let mut lower = Param {
143 num: Dyadic::zero(),
144 den: exact(1.0)?,
145 open: false,
146 };
147 let mut upper = Param {
148 num: exact(1.0)?,
149 den: exact(1.0)?,
150 open: false,
151 };
152 for axis in 0..2 {
153 let open = !cell.closed[axis];
154 for (bound, negate) in [(&cell.lo[axis], false), (&cell.hi[axis], true)] {
157 let at = |p: &[Dyadic; 3]| {
158 let value = p[axis].sub(&bound.mul(&p[2]));
159 if negate {
160 value.neg()
161 } else {
162 value
163 }
164 };
165 let (h0, h1) = (at(from), at(to));
166 let satisfied = |h: &Dyadic| -> GeomResult<bool> {
167 Ok(match h.sign().ok_or_else(inconsistent)? {
168 Sign::Positive => true,
169 Sign::Zero => !open,
170 _ => false,
171 })
172 };
173 match (satisfied(&h0)?, satisfied(&h1)?) {
174 (true, true) => {}
175 (false, false) => return Ok(None),
176 (true, false) => {
177 let candidate = Param {
178 num: h0.clone(),
179 den: h0.sub(&h1),
180 open,
181 };
182 match compare_values(&candidate, &upper)? {
185 Ordering::Less => upper = candidate,
186 Ordering::Equal if candidate.open => upper = candidate,
187 _ => {}
188 }
189 }
190 (false, true) => {
191 let candidate = Param {
192 num: h0.neg(),
193 den: h1.sub(&h0),
194 open,
195 };
196 match compare_values(&candidate, &lower)? {
197 Ordering::Greater => lower = candidate,
198 Ordering::Equal if candidate.open => lower = candidate,
199 _ => {}
200 }
201 }
202 }
203 }
204 }
205 Ok(match compare_values(&lower, &upper)? {
207 Ordering::Less => Some(lower),
208 Ordering::Equal if !lower.open && !upper.open => Some(lower),
209 _ => None,
210 })
211}
212
213#[derive(Debug, Clone)]
215pub struct Snapped {
216 pub points: ExactPoints,
219 pub arrangement: Arrangement,
221 pub routes: Vec<Vec<(PieceId, bool)>>,
225 pub displacement: DisplacementBound,
227 pub rerouted: usize,
229}
230
231#[derive(Debug, Clone)]
233pub struct ClassBoundary {
234 pub half_edges: Vec<HalfEdge>,
236 pub survives: Vec<bool>,
239}
240
241fn refused(message: &str) -> GeomError {
242 GeomError::Degenerate(message.into())
243}
244
245pub fn snap_round(
253 points: &ExactPoints,
254 edges: &[Segment],
255 budget: &mut PairBudget,
256 declared_bytes: usize,
257 options: &ExecutionOptions,
258) -> GeomResult<Snapped> {
259 let rounded = |id: PointId| -> GeomResult<Point2> {
261 let at = points.round(id)?;
262 Ok(Point2::new(at.x + 0.0, at.y + 0.0))
263 };
264 let mut centers: Vec<Point2> = Vec::new();
265 for edge in edges {
266 for end in edge.ends {
267 centers.push(rounded(end)?);
268 }
269 }
270 centers.sort_by(point_order);
271 centers.dedup_by(|a, b| point_order(a, b).is_eq());
272 let mut charged = 0usize;
273 let mut store = ExactPoints::with_frame_sign(points.frame_sign());
274 let mut cells = Vec::with_capacity(centers.len());
275 let mut ids = Vec::with_capacity(centers.len());
276 for ¢er in ¢ers {
277 charge(
278 options,
279 declared_bytes,
280 &mut charged,
281 HOT_POINT_SCRATCH_BYTES,
282 )?;
283 let hot = cell(center)?;
284 ids.push(store.push_flat(hot.center)?);
285 cells.push(hot);
286 }
287 let find = |at: Point2| {
288 centers
289 .binary_search_by(|probe| point_order(probe, &at))
290 .map_err(|_| inconsistent())
291 };
292
293 let mut routes: Vec<Vec<usize>> = Vec::with_capacity(edges.len());
295 let mut displacement = 0.0f64;
296 let mut rerouted = 0usize;
297 for edge in edges {
298 options.check_cancelled()?;
299 let [from, to] = edge.ends;
300 let start = find(rounded(from)?)?;
301 let end = find(rounded(to)?)?;
302 if start == end {
303 for (id, at) in [(from, start), (to, end)] {
305 if points.flat(id).is_none() {
306 displacement = displacement.max(cells[at].half_width);
307 }
308 }
309 routes.push(Vec::new());
310 continue;
311 }
312 let (a, b) = (points.bounds(from), points.bounds(to));
313 let reach = [
314 a[0].min(b[0]),
315 a[1].min(b[1]),
316 a[2].max(b[2]),
317 a[3].max(b[3]),
318 ];
319 let first = cells.partition_point(|c| c.reach[2] < reach[0]);
320 let mut found: Vec<(usize, Param)> = Vec::new();
321 for (index, candidate) in cells.iter().enumerate().skip(first) {
322 if candidate.reach[0] > reach[2] {
323 break;
324 }
325 if index == start
326 || index == end
327 || candidate.reach[3] < reach[1]
328 || candidate.reach[1] > reach[3]
329 {
330 continue;
331 }
332 budget.count()?;
333 if let Some(param) = meets(points.exact(from), points.exact(to), candidate)? {
334 found.push((index, param));
335 }
336 }
337 let mut order: Vec<usize> = (0..found.len()).collect();
338 sort_exact(&mut order, |l, r| compare_params(&found[l].1, &found[r].1))?;
339 for pair in order.windows(2) {
340 if compare_params(&found[pair[0]].1, &found[pair[1]].1)?.is_eq() {
341 return Err(inconsistent());
342 }
343 }
344 for (id, at) in [(from, start), (to, end)] {
345 if points.flat(id).is_none() {
346 displacement = displacement.max(cells[at].half_width);
347 }
348 }
349 for &(index, _) in &found {
350 displacement = displacement.max(cells[index].half_width);
351 }
352 rerouted += usize::from(!found.is_empty());
353 let mut route = Vec::with_capacity(found.len() + 2);
354 route.push(start);
355 route.extend(order.iter().map(|&i| found[i].0));
356 route.push(end);
357 routes.push(route);
358 }
359
360 let mut segments = Vec::new();
362 let mut spans = Vec::with_capacity(routes.len());
363 for route in &routes {
364 let first = segments.len();
365 for pair in route.windows(2) {
366 charge(
367 options,
368 declared_bytes,
369 &mut charged,
370 FRAGMENT_SCRATCH_BYTES,
371 )?;
372 segments.push(Segment {
373 ends: [ids[pair[0]], ids[pair[1]]],
374 });
375 }
376 spans.push(first..segments.len());
377 }
378 let arrangement = arrange(
379 &mut store,
380 &segments,
381 budget,
382 declared_bytes.saturating_add(charged),
383 options,
384 )?;
385 if arrangement.crossings != 0 {
386 return Err(refused("snap rounding left two fragments crossing"));
387 }
388 let routes = spans
389 .into_iter()
390 .map(|span| {
391 span.flat_map(|segment| arrangement.paths[segment].iter().copied())
392 .collect()
393 })
394 .collect();
395 Ok(Snapped {
396 points: store,
397 arrangement,
398 routes,
399 displacement: DisplacementBound::new(displacement).ok_or_else(inconsistent)?,
400 rerouted,
401 })
402}
403
404impl Snapped {
405 pub fn point(&self, vertex: VertexId) -> GeomResult<Point2> {
407 self.points
408 .flat(self.arrangement.vertices[vertex.0].point)
409 .ok_or_else(inconsistent)
410 }
411
412 pub fn class_boundary(&self, chain: &[(usize, bool)]) -> GeomResult<ClassBoundary> {
418 let route_of = |&(edge, forward): &(usize, bool)| -> Vec<(PieceId, bool)> {
419 let route = &self.routes[edge];
420 if forward {
421 route.clone()
422 } else {
423 route.iter().rev().map(|&(p, f)| (p, !f)).collect()
424 }
425 };
426 let mut net: BTreeMap<PieceId, i32> = BTreeMap::new();
427 for element in chain {
428 for (piece, forward) in route_of(element) {
429 *net.entry(piece).or_default() += if forward { 1 } else { -1 };
430 }
431 }
432 let mut half_edges = Vec::new();
433 for (&piece, &count) in &net {
434 match count {
435 0 => {}
436 1 => half_edges.push(HalfEdge {
437 piece,
438 forward: true,
439 }),
440 -1 => half_edges.push(HalfEdge {
441 piece,
442 forward: false,
443 }),
444 _ => return Err(refused("snap rounding folded a boundary onto itself")),
445 }
446 }
447 let survives = chain
448 .iter()
449 .map(|element| {
450 route_of(element)
451 .iter()
452 .any(|(piece, _)| net.get(piece).is_some_and(|&count| count != 0))
453 })
454 .collect();
455 Ok(ClassBoundary {
456 half_edges,
457 survives,
458 })
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 fn flat(x: f64, y: f64) -> Point2 {
467 Point2::new(x, y)
468 }
469
470 #[test]
471 fn the_cell_at_one_is_asymmetric_by_a_binade() {
472 let c = cell(flat(1.0, 1.0)).unwrap();
473 let one = exact(1.0).unwrap();
474 assert_eq!(
476 one.sub(&c.lo[0]),
477 exact(2f64.powi(-54)).unwrap(),
478 "lower half-width"
479 );
480 assert_eq!(
481 c.hi[0].sub(&one),
482 exact(2f64.powi(-53)).unwrap(),
483 "upper half-width"
484 );
485 assert_eq!(c.half_width, 2f64.powi(-53));
486 assert!(c.closed[0], "1.0 has an even significand");
487 }
488
489 #[test]
490 fn cells_at_zero_the_smallest_subnormal_and_the_largest_number() {
491 let tiny = f64::from_bits(1);
492 let zero = cell(flat(0.0, -0.0)).unwrap();
493 assert_eq!(zero.center.y.to_bits(), 0, "-0.0 is +0.0");
494 assert_eq!(zero.hi[0], exact(tiny).unwrap().mul(&exact(0.5).unwrap()));
495 assert_eq!(zero.lo[0], zero.hi[0].neg());
496 assert!(zero.closed[0]);
497 assert_eq!(zero.half_width, tiny, "halving underflows");
498
499 let small = cell(flat(tiny, tiny)).unwrap();
500 assert!(!small.closed[0], "the smallest subnormal is odd");
501 assert_eq!(small.lo[0], zero.hi[0]);
502
503 let big = cell(flat(f64::MAX, -f64::MAX)).unwrap();
504 let gap = f64::MAX - f64::MAX.next_down();
505 assert_eq!(
506 big.hi[0].sub(&exact(f64::MAX).unwrap()),
507 exact(gap).unwrap().mul(&exact(0.5).unwrap()),
508 "the missing neighbour mirrors the gap"
509 );
510 assert_eq!(big.reach[2], f64::MAX + gap);
511 }
512
513 fn segment_through(points: &ExactPoints, a: (f64, f64), b: (f64, f64), cell: &Cell) -> bool {
514 let mut store = points.clone();
515 let from = store.push_flat(flat(a.0, a.1)).unwrap();
516 let to = store.push_flat(flat(b.0, b.1)).unwrap();
517 meets(store.exact(from), store.exact(to), cell)
518 .unwrap()
519 .is_some()
520 }
521
522 #[test]
523 fn a_segment_on_a_midpoint_meets_only_the_even_neighbour() {
524 let odd = 1.0 + f64::EPSILON; assert_eq!(1.0f64.to_bits() & 1, 0);
526 assert_eq!(odd.to_bits() & 1, 1);
527 let tie = exact(1.0)
529 .unwrap()
530 .add(&exact(odd).unwrap())
531 .mul(&exact(0.5).unwrap());
532 let (even, odd_cell) = (cell(flat(1.0, 1.0)).unwrap(), cell(flat(odd, 1.0)).unwrap());
533 let two = exact(2.0).unwrap();
535 let line = |y: f64| [tie.mul(&two), exact(y).unwrap().mul(&two), two.clone()];
536 let (p, q) = (line(0.9), line(1.1));
537 assert!(meets(&p, &q, &even).unwrap().is_some());
538 assert!(meets(&p, &q, &odd_cell).unwrap().is_none());
539 }
540
541 #[test]
542 fn a_point_meets_its_own_cell_and_no_neighbour() {
543 let mut points = ExactPoints::new();
544 let at = flat(0.3, -2.5);
545 let id = points.push_flat(at).unwrap();
546 let own = cell(at).unwrap();
547 assert!(meets(points.exact(id), points.exact(id), &own)
548 .unwrap()
549 .is_some());
550 for neighbour in [
551 flat(at.x.next_up(), at.y),
552 flat(at.x.next_down(), at.y),
553 flat(at.x, at.y.next_up()),
554 flat(at.x, at.y.next_down()),
555 ] {
556 let other = cell(neighbour).unwrap();
557 assert!(meets(points.exact(id), points.exact(id), &other)
558 .unwrap()
559 .is_none());
560 }
561 }
562
563 #[test]
564 fn a_segment_through_a_cell_is_found_and_one_beside_it_is_not() {
565 let points = ExactPoints::new();
566 let c = cell(flat(1.0, 1.0)).unwrap();
567 assert!(segment_through(&points, (0.0, 0.0), (2.0, 2.0), &c));
568 assert!(!segment_through(&points, (0.0, 0.5), (2.0, 2.5), &c));
569 }
570
571 struct Lcg(u64);
573
574 impl Lcg {
575 fn next(&mut self) -> u64 {
576 self.0 = self
577 .0
578 .wrapping_mul(6364136223846793005)
579 .wrapping_add(1442695040888963407);
580 self.0 >> 11
581 }
582
583 fn below(&mut self, n: u64) -> u64 {
584 self.next() % n
585 }
586 }
587
588 fn rounded_at(from: &[Dyadic; 3], to: &[Dyadic; 3], num: &Dyadic, den: &Dyadic) -> Point2 {
590 let rest = den.sub(num);
591 let mix = |i: usize| rest.mul(&from[i]).add(&num.mul(&to[i]));
592 let mut store = ExactPoints::new();
593 let id = store.push([mix(0), mix(1), mix(2)]).unwrap();
594 let at = store.round(id).unwrap();
595 Point2::new(at.x + 0.0, at.y + 0.0)
596 }
597
598 #[test]
603 fn meets_agrees_with_rounding_the_points_of_the_segment() {
604 let mut rng = Lcg(0x5eed);
605 let (mut hits, mut misses, mut ties) = (0, 0, 0);
606 let tiny = exact(2f64.powi(-200)).unwrap();
607 for case in 0..5000 {
608 let scale = [1.0, 0.1, 1e-3, 3.7e5, 2.0][rng.below(5) as usize];
609 let center = Point2::new(
610 (rng.next() as f64 / (1u64 << 53) as f64 * 8.0 - 4.0) * scale,
611 (rng.next() as f64 / (1u64 << 53) as f64 * 8.0 - 4.0) * scale,
612 );
613 let center = Point2::new(center.x + 0.0, center.y + 0.0);
614 let cell = cell(center).unwrap();
615 let ulp = |c: f64| exact(c.next_up() - c).unwrap();
616 let end = |rng: &mut Lcg| {
617 let w = [1u64, 3, 5, 7][rng.below(4) as usize];
618 let weight = exact(w as f64).unwrap();
619 let quarter = exact(0.25).unwrap();
620 let offset = |rng: &mut Lcg, c: f64| {
621 let m = rng.below(41) as f64 - 20.0;
622 exact(c)
623 .unwrap()
624 .mul(&weight)
625 .add(&ulp(c).mul(&exact(m).unwrap()).mul(&quarter))
626 };
627 [offset(rng, center.x), offset(rng, center.y), weight.clone()]
628 };
629 let corner = |rng: &mut Lcg| {
632 let pick = |rng: &mut Lcg, axis: usize| {
633 if rng.below(2) == 0 {
634 cell.lo[axis].clone()
635 } else {
636 cell.hi[axis].clone()
637 }
638 };
639 [pick(rng, 0), pick(rng, 1), exact(1.0).unwrap()]
640 };
641 let either = |rng: &mut Lcg| {
642 if rng.below(3) == 0 {
643 corner(rng)
644 } else {
645 end(rng)
646 }
647 };
648 let (from, to) = (either(&mut rng), either(&mut rng));
649 let found = meets(&from, &to, &cell).unwrap();
650 let inside =
651 |num: &Dyadic, den: &Dyadic| rounded_at(&from, &to, num, den) == cell.center;
652 match &found {
653 Some(lower) => {
654 hits += 1;
655 if lower.open {
656 assert!(!inside(&lower.num, &lower.den), "case {case}: open");
657 assert!(
658 inside(&lower.num.add(&lower.den.mul(&tiny)), &lower.den),
659 "case {case}"
660 );
661 } else {
662 assert!(inside(&lower.num, &lower.den), "case {case}: closed");
663 }
664 let before = lower.num.sub(&lower.den.mul(&tiny));
665 if before.sign() == Some(Sign::Positive) {
666 assert!(!inside(&before, &lower.den), "case {case}: before");
667 }
668 ties += usize::from(lower.open);
669 }
670 None => misses += 1,
671 }
672 for k in 0..=16 {
673 let num = exact(f64::from(k)).unwrap();
674 let den = exact(16.0).unwrap();
675 if inside(&num, &den) {
676 let lower = found
677 .as_ref()
678 .unwrap_or_else(|| panic!("case {case}: missed s={k}/16"));
679 let at = Param {
680 num,
681 den,
682 open: false,
683 };
684 assert!(
685 compare_values(lower, &at).unwrap().is_le(),
686 "case {case}: s={k}/16 before the lower end"
687 );
688 }
689 }
690 }
691 assert!(hits > 500 && misses > 500, "{hits} hits, {misses} misses");
692 assert!(ties > 0, "no open lower end was exercised");
693 }
694}