1use core::f64::consts::FRAC_PI_6;
96use std::collections::BTreeSet;
97
98use super::smooth::capped;
99use super::{Editable, NONE, Scope, index, prev};
100use crate::bvh::Bvh;
101use crate::curvature::in_parallel;
102use ogeom_core::{OgeomResult, ogeom_bail};
103use ogeom_math::{Point, Vector};
104use ogeom_topo::Triangulation;
105
106pub const OFFSET_EDGE_ANGLE: f64 = FRAC_PI_6;
109
110pub const OFFSET_FARTHEST: f64 = 3.0;
112
113const SLIVER: f64 = 16.0;
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub struct OffsetReport {
123 pub merged: usize,
125 pub eased: usize,
127 pub capped: usize,
130 pub folded: usize,
133}
134
135#[derive(Clone, Copy)]
137struct Placed {
138 step: Vector,
139 rank: u8,
140 free: Vector,
143 capped: bool,
144}
145
146#[derive(Default)]
148struct Sector {
149 faces: Vec<(Vector, f64)>,
150}
151
152impl Sector {
153 fn weight(&self) -> f64 {
154 self.faces.iter().map(|&(_, a)| a).sum()
155 }
156
157 fn normal(&self) -> Option<Vector> {
158 let sum = self
159 .faces
160 .iter()
161 .fold(Vector::new(0.0, 0.0, 0.0), |s, &(n, a)| s + n * a);
162 unit(sum)
163 }
164}
165
166fn unit(v: Vector) -> Option<Vector> {
167 let length = v.magnitude();
168 (length > 0.0 && length.is_finite()).then(|| v / length)
169}
170
171impl Editable {
172 pub fn offset(&mut self, distance: f64, region: Option<&[bool]>) -> OgeomResult<OffsetReport> {
187 if !distance.is_finite() {
188 ogeom_bail!(Numeric, "an offset of {distance} is not a distance");
189 }
190 let n = self.positions.len();
191 if let Some(region) = region
192 && region.len() != n
193 {
194 ogeom_bail!(
195 Range,
196 "a region of {} entries for {n} vertices",
197 region.len()
198 );
199 }
200 let mut report = OffsetReport {
201 merged: 0,
202 eased: 0,
203 capped: 0,
204 folded: 0,
205 };
206 if distance == 0.0 {
207 return Ok(report);
208 }
209 let faces_before = self.corners.len();
210 let area: Vec<Vector> = (0..faces_before).map(|f| self.area_vector(f)).collect();
211 let normal: Vec<Option<Vector>> = area
212 .iter()
213 .zip(&self.live)
214 .map(|(&a, &live)| if live { unit(a) } else { None })
215 .collect();
216 let was_sliver: Vec<bool> = (0..faces_before)
217 .map(|f| {
218 let [a, b, c] = self.corners[f].map(|v| self.positions[v as usize]);
219 shape(a, b, c, area[f]) == Shape::Sliver
220 })
221 .collect();
222 let moves: Vec<bool> = self
223 .corners
224 .iter()
225 .zip(&self.live)
226 .map(|(c, &live)| live && region.is_none_or(|r| c.iter().all(|&v| r[v as usize])))
227 .collect();
228
229 let candidates: Vec<u32> = (0..n)
230 .filter(|&v| self.out[v] != NONE && region.is_none_or(|r| r[v]))
231 .map(index)
232 .collect();
233 let placed = in_parallel(&candidates, |v| self.place(v, distance, &normal, &moves));
234 if region.is_some() {
235 self.raise_walls(&candidates, &moves)?;
236 }
237
238 let before = self.positions.clone();
239 let stuck = self.non_manifold();
240 let mut rank = vec![0_u8; self.positions.len()];
241 let mut movable = vec![false; self.positions.len()];
242 let mut sliding: Vec<(u32, Placed)> = Vec::new();
243 for (&v, p) in candidates.iter().zip(&placed) {
244 let k = v as usize;
245 if p.rank > 0 {
246 self.positions[k] += p.step;
247 rank[k] = p.rank;
248 movable[k] = true;
249 report.capped += usize::from(p.capped);
250 if p.rank < 3 {
251 sliding.push((v, *p));
252 }
253 }
254 }
255 for v in stuck {
256 movable[v as usize] = false;
257 }
258 self.rejoin_crowded_edges();
259
260 let pushed = self.push_back(&before, faces_before, &normal, distance, &sliding)?;
261 report.eased = pushed.len();
262 for (v, to, r) in pushed {
263 self.positions[v as usize] = to;
264 rank[v as usize] = r;
265 }
266
267 let folded = |me: &Self, f: usize| {
268 f < faces_before && me.live[f] && !was_sliver[f] && me.folded(f, area[f])
269 };
270 let mut work: BTreeSet<u32> = (0..faces_before)
271 .filter(|&f| folded(self, f))
272 .map(index)
273 .collect();
274 while let Some(f) = work.pop_first() {
275 if !folded(self, f as usize) {
276 continue;
277 }
278 if let Some(w) = self.repair(f, &rank, &movable, &area) {
279 report.merged += 1;
280 work.extend(
281 self.fan(w)
282 .into_iter()
283 .map(|h| h / 3)
284 .filter(|&g| (g as usize) < faces_before),
285 );
286 }
287 }
288 report.folded = (0..faces_before).filter(|&f| folded(self, f)).count();
289 Ok(report)
290 }
291
292 fn area_vector(&self, f: usize) -> Vector {
294 let [a, b, c] = self.corners[f].map(|v| self.positions[v as usize]);
295 (b - a).cross(c - a)
296 }
297
298 fn fan(&self, v: u32) -> Vec<u32> {
300 let start = self.out[v as usize];
301 let mut fan = Vec::new();
302 if start == NONE {
303 return fan;
304 }
305 let mut h = start;
306 loop {
307 fan.push(h);
308 h = self.twin[prev(h) as usize];
309 if h == NONE || h == start {
310 return fan;
311 }
312 }
313 }
314
315 fn place(&self, v: u32, d: f64, normal: &[Option<Vector>], moves: &[bool]) -> Placed {
317 let still = Placed {
318 step: Vector::new(0.0, 0.0, 0.0),
319 rank: 0,
320 free: Vector::new(0.0, 0.0, 0.0),
321 capped: false,
322 };
323 let fan = self.fan(v);
324 let closed = fan.first().is_some_and(|&h| self.twin[h as usize] != NONE);
325 let p = self.positions[v as usize];
326 let cos_edge = OFFSET_EDGE_ANGLE.cos();
327 let mut sectors: Vec<Sector> = vec![Sector::default()];
328 let mut parted_at_start = !closed;
331 let mut last: Option<Vector> = None;
332 let mut first: Option<Vector> = None;
333 for (i, &h) in fan.iter().enumerate() {
334 let f = (h / 3) as usize;
335 if !moves[f] {
336 if i == 0 {
337 parted_at_start = true;
338 }
339 if !sectors.last().is_some_and(|s| s.faces.is_empty()) {
340 sectors.push(Sector::default());
341 }
342 last = None;
343 continue;
344 }
345 let Some(nf) = normal[f] else { continue };
346 if last.is_some_and(|m| m.dot(nf) < cos_edge)
347 && !sectors.last().is_some_and(|s| s.faces.is_empty())
348 {
349 sectors.push(Sector::default());
350 }
351 if first.is_none() && sectors.len() == 1 {
352 first = Some(nf);
353 }
354 last = Some(nf);
355 let [a, b] = [self.head(h), self.tail(prev(h))].map(|w| self.positions[w as usize] - p);
356 let angle = a.cross(b).magnitude().atan2(a.dot(b));
357 if let Some(s) = sectors.last_mut() {
358 s.faces.push((nf, angle));
359 }
360 }
361 sectors.retain(|s| !s.faces.is_empty());
362 if closed
363 && !parted_at_start
364 && sectors.len() > 1
365 && let (Some(m), Some(nf)) = (last, first)
366 && m.dot(nf) >= cos_edge
367 && let Some(tail) = sectors.pop()
368 {
369 sectors[0].faces.extend(tail.faces);
370 }
371
372 let groups: Vec<(Vector, f64)> = sectors
373 .iter()
374 .filter_map(|s| s.normal().map(|m| (m, s.weight())))
375 .collect();
376 let Some(&(heaviest, _)) = groups.iter().reduce(|a, b| if b.1 > a.1 { b } else { a })
377 else {
378 return still;
379 };
380 let mut basis = vec![heaviest];
381 let mut taken = vec![heaviest];
382 while basis.len() < 3 {
383 let across = groups
384 .iter()
385 .filter(|&&(m, _)| taken.iter().all(|&t| m.dot(t) < cos_edge))
386 .map(|&(m, _)| (m, basis.iter().fold(m, |r, &e| r - e * r.dot(e))))
387 .reduce(|a, b| {
388 if b.1.magnitude() > a.1.magnitude() {
389 b
390 } else {
391 a
392 }
393 });
394 match across.and_then(|(m, r)| unit(r).map(|e| (m, e))) {
395 Some((m, e)) => {
396 taken.push(m);
397 basis.push(e);
398 }
399 None => break,
400 }
401 }
402
403 let faces = sectors.iter().flat_map(|s| s.faces.iter());
404 let k = basis.len();
405 let mut m = [[0.0; 3]; 3];
406 let mut r = [0.0; 3];
407 for &(nf, a) in faces {
408 let mut c = [0.0; 3];
409 for (ci, &e) in c.iter_mut().zip(&basis) {
410 *ci = nf.dot(e);
411 }
412 for i in 0..k {
413 r[i] += a * d * c[i];
414 for j in 0..k {
415 m[i][j] += a * c[i] * c[j];
416 }
417 }
418 }
419 let Some(alpha) = solve(m, r, k) else {
420 return still;
421 };
422 let step = basis
423 .iter()
424 .zip(alpha)
425 .fold(Vector::new(0.0, 0.0, 0.0), |s, (&e, t)| s + e * t);
426 if !step.magnitude().is_finite() {
427 return still;
428 }
429 let (to, held) = capped(p, p + step, OFFSET_FARTHEST * d.abs());
430 let free = match k {
431 1 => Some(basis[0]),
432 2 => unit(basis[0].cross(basis[1])),
433 _ => None,
434 };
435 Placed {
436 step: to - p,
437 rank: u8::try_from(k).unwrap_or(u8::MAX),
438 free: free.unwrap_or(Vector::new(0.0, 0.0, 0.0)),
439 capped: held,
440 }
441 }
442
443 fn raise_walls(&mut self, candidates: &[u32], moves: &[bool]) -> OgeomResult<()> {
448 let mut foot = vec![NONE; self.positions.len()];
449 let mut parted: Vec<u32> = Vec::new();
450 for &v in candidates {
451 let fan = self.fan(v);
452 let mixed = fan.iter().any(|&h| moves[(h / 3) as usize])
453 && fan.iter().any(|&h| !moves[(h / 3) as usize]);
454 if mixed {
455 parted.push(v);
456 }
457 }
458 if parted.is_empty() {
459 return Ok(());
460 }
461 let walls: Vec<u32> = parted
462 .iter()
463 .flat_map(|&v| self.fan(v))
464 .filter(|&h| {
465 let g = self.twin[h as usize];
466 moves[(h / 3) as usize] && g != NONE && !moves[(g / 3) as usize]
467 })
468 .collect();
469 let added = parted.len().saturating_add(self.positions.len());
470 if added >= NONE as usize || (self.corners.len() + 2 * walls.len()) * 3 >= NONE as usize {
471 ogeom_bail!(Range, "a wall past u32 numbering");
472 }
473 let mut points: Vec<u32> = parted.iter().map(|&v| self.point[v as usize]).collect();
474 points.sort_unstable();
475 points.dedup();
476 let mut faces = self.about(&points);
477 for &v in &parted {
478 let w = self.add_vertex(self.positions[v as usize]);
479 self.copy_sources(w, v);
480 foot[v as usize] = w;
481 points.push(w);
482 }
483 for &f in &faces {
484 if !moves[f as usize] {
485 for c in &mut self.corners[f as usize] {
486 let w = foot[*c as usize];
487 if w != NONE {
488 *c = w;
489 }
490 }
491 }
492 }
493 for h in walls {
494 let (a, b) = (self.tail(h), self.head(h));
495 let (fa, fb) = (foot[a as usize], foot[b as usize]);
496 faces.push(self.add_face([fa, fb, b]));
497 faces.push(self.add_face([fa, b, a]));
498 }
499 faces.sort_unstable();
500 points.sort_unstable();
501 self.relink(&Scope::Points(points), &faces);
502 Ok(())
503 }
504
505 fn push_back(
513 &self,
514 before: &[Point],
515 faces_before: usize,
516 normal: &[Option<Vector>],
517 d: f64,
518 sliding: &[(u32, Placed)],
519 ) -> OgeomResult<Vec<(u32, Point, u8)>> {
520 if sliding.is_empty() {
521 return Ok(Vec::new());
522 }
523 let mut faces: Vec<u32> = Vec::new();
524 let mut triangles: Vec<[u32; 3]> = Vec::new();
525 for (f, n) in normal.iter().enumerate().take(faces_before) {
526 if n.is_some() {
527 faces.push(index(f));
528 triangles.push(self.corners[f]);
529 }
530 }
531 let mesh = Triangulation {
532 positions: before.to_vec(),
533 triangles,
534 ..Triangulation::new()
535 };
536 let bvh = Bvh::build(&mesh)?;
537 let reach = d.abs();
538 let least = OFFSET_EDGE_ANGLE.sin();
539 let movers: Vec<u32> = sliding.iter().map(|&(v, _)| v).collect();
540 let pushed = in_parallel(&movers, |v| {
541 let slot = movers.binary_search(&v).ok()?;
542 let placed = sliding[slot].1;
543 let start = before[v as usize];
544 let placed_at = self.positions[v as usize];
545 let mut x = placed_at;
546 let mut rank = placed.rank;
547 let mut free = placed.free;
548 let mut moved = false;
549 while rank < 3 {
550 let across = |nf: Vector| {
554 let slant = if rank == 1 {
555 (1.0 - nf.dot(free).powi(2)).max(0.0).sqrt()
556 } else {
557 nf.dot(free).abs()
558 };
559 slant >= least
560 };
561 let side = |f: usize, nf: Vector, at: Point| {
565 nf.dot(at - before[self.corners[f][0] as usize])
566 };
567 let Some(hit) = bvh.nearest_where(placed_at, reach, |t| {
568 let f = faces[t as usize] as usize;
569 normal[f].is_some_and(|nf| across(nf) && side(f, nf, placed_at) * d > 0.0)
570 }) else {
571 break;
572 };
573 if hit.distance >= reach {
574 break;
575 }
576 let f = faces[hit.triangle as usize] as usize;
577 let Some(nf) = normal[f] else { break };
578 let side = side(f, nf, x);
579 let along = if rank == 1 {
580 nf - free * nf.dot(free)
581 } else {
582 free
583 };
584 let Some(along) = unit(along) else { break };
585 let t = (d - side) / nf.dot(along);
586 let to = x + along * t;
587 if !t.is_finite() || (to - start).magnitude() > OFFSET_FARTHEST * reach {
588 break;
589 }
590 x = to;
591 moved = true;
592 if rank == 1 {
593 let Some(line) = unit(free.cross(nf)) else {
594 break;
595 };
596 free = line;
597 }
598 rank += 1;
599 }
600 moved.then_some((v, x, rank))
601 });
602 Ok(pushed.into_iter().flatten().collect())
603 }
604
605 fn folded(&self, f: usize, was: Vector) -> bool {
608 let [a, b, c] = self.corners[f].map(|v| self.positions[v as usize]);
609 shape(a, b, c, was) != Shape::Kept
610 }
611
612 fn repair(&mut self, f: u32, rank: &[u8], movable: &[bool], area: &[Vector]) -> Option<u32> {
616 let corners = self.corners[f as usize];
617 let mut from: Vec<u32> = corners
618 .iter()
619 .copied()
620 .filter(|&v| movable[v as usize])
621 .collect();
622 from.sort_by(|&a, &b| rank[a as usize].cmp(&rank[b as usize]).then(b.cmp(&a)));
623 for &u in &from {
624 let p = self.positions[u as usize];
625 let mut to: Vec<u32> = from.iter().copied().filter(|&w| w != u).collect();
626 to.sort_by(|&a, &b| {
627 rank[b as usize]
628 .cmp(&rank[a as usize])
629 .then(
630 self.positions[a as usize]
631 .distance(p)
632 .total_cmp(&self.positions[b as usize].distance(p)),
633 )
634 .then(a.cmp(&b))
635 });
636 for w in to {
637 if self.collapse(u, w, area) {
638 return Some(w);
639 }
640 }
641 }
642 None
643 }
644
645 fn collapse(&mut self, u: u32, w: u32, area: &[Vector]) -> bool {
648 let lone = |me: &Self, v: u32| {
649 me.ring(me.point[v as usize])
650 .into_iter()
651 .all(|c| c == v || me.out[c as usize] == NONE)
652 };
653 if !lone(self, u) || !lone(self, w) {
654 return false;
655 }
656 let (fan_u, fan_w) = (self.fan(u), self.fan(w));
657 let on_rim = |fan: &[u32]| fan.first().is_some_and(|&h| self.twin[h as usize] == NONE);
658 let neighbours = |fan: &[u32]| {
659 let mut n: Vec<u32> = fan
660 .iter()
661 .flat_map(|&h| [self.head(h), self.tail(prev(h))])
662 .collect();
663 n.sort_unstable();
664 n.dedup();
665 n
666 };
667 let link = |fan: &[u32]| {
668 let mut l: Vec<(u32, u32)> = fan
669 .iter()
670 .map(|&h| {
671 let (a, b) = (self.head(h), self.tail(prev(h)));
672 (a.min(b), a.max(b))
673 })
674 .collect();
675 l.sort_unstable();
676 l
677 };
678 let shared: Vec<u32> = fan_u
679 .iter()
680 .copied()
681 .filter(|&h| self.corners[(h / 3) as usize].contains(&w))
682 .collect();
683 if shared.is_empty() || shared.len() > 2 {
684 return false;
685 }
686 if on_rim(&fan_u) && shared.len() != 1 {
687 return false;
688 }
689 let mut across: Vec<u32> = shared
690 .iter()
691 .flat_map(|&h| self.corners[(h / 3) as usize])
692 .filter(|&c| c != u && c != w)
693 .collect();
694 across.sort_unstable();
695 across.dedup();
696 let (nu, nw) = (neighbours(&fan_u), neighbours(&fan_w));
697 let common: Vec<u32> = nu
698 .iter()
699 .copied()
700 .filter(|c| nw.binary_search(c).is_ok())
701 .collect();
702 if common != across {
703 return false;
704 }
705 let lw = link(&fan_w);
706 if link(&fan_u).iter().any(|e| lw.binary_search(e).is_ok()) {
707 return false;
708 }
709
710 let p = self.positions[w as usize];
711 for &h in &fan_u {
712 let f = (h / 3) as usize;
713 if f >= area.len() || shared.contains(&h) {
714 continue;
715 }
716 let [a, b] = [self.head(h), self.tail(prev(h))].map(|v| self.positions[v as usize]);
717 if shape(p, a, b, area[f]) == Shape::Turned {
718 return false;
719 }
720 }
721
722 let mut points: Vec<u32> = [u, w]
723 .into_iter()
724 .chain(across.iter().copied())
725 .map(|v| self.point[v as usize])
726 .collect();
727 points.sort_unstable();
728 points.dedup();
729 let around = self.about(&points);
730 for &h in &shared {
731 let g = h / 3;
732 self.live[g as usize] = false;
733 for e in 3 * g..3 * g + 3 {
734 self.twin[e as usize] = NONE;
735 }
736 }
737 let kept: Vec<u32> = around
738 .into_iter()
739 .filter(|&g| self.live[g as usize])
740 .collect();
741 self.absorb_sources(w, u);
742 for &g in &kept {
743 for c in &mut self.corners[g as usize] {
744 if *c == u {
745 *c = w;
746 }
747 }
748 }
749 self.relink(&Scope::Points(points), &kept);
750 true
751 }
752}
753
754#[derive(Debug, Clone, Copy, PartialEq, Eq)]
756enum Shape {
757 Kept,
759 Turned,
761 Sliver,
763}
764
765fn shape(a: Point, b: Point, c: Point, was: Vector) -> Shape {
768 let now = (b - a).cross(c - a);
769 let longest = (b - a)
770 .magnitude()
771 .max((c - b).magnitude())
772 .max((a - c).magnitude());
773 let size = [a, b, c]
774 .iter()
775 .flat_map(|p| [p.x.abs(), p.y.abs(), p.z.abs()])
776 .fold(0.0, f64::max);
777 if now.magnitude() <= SLIVER * f64::EPSILON * size * longest {
778 Shape::Sliver
779 } else if now.dot(was) < OFFSET_EDGE_ANGLE.cos() * now.magnitude() * was.magnitude() {
780 Shape::Turned
781 } else {
782 Shape::Kept
783 }
784}
785
786fn solve(mut m: [[f64; 3]; 3], mut r: [f64; 3], k: usize) -> Option<[f64; 3]> {
789 for col in 0..k {
790 let pivot = (col..k).max_by(|&a, &b| m[a][col].abs().total_cmp(&m[b][col].abs()))?;
791 if m[pivot][col] == 0.0 {
792 return None;
793 }
794 m.swap(col, pivot);
795 r.swap(col, pivot);
796 for row in col + 1..k {
797 let s = m[row][col] / m[col][col];
798 let above = m[col];
799 for (mj, &aj) in m[row].iter_mut().zip(&above).take(k).skip(col) {
800 *mj -= s * aj;
801 }
802 r[row] -= s * r[col];
803 }
804 }
805 let mut x = [0.0; 3];
806 for row in (0..k).rev() {
807 let mut s = r[row];
808 for j in row + 1..k {
809 s -= m[row][j] * x[j];
810 }
811 x[row] = s / m[row][row];
812 }
813 Some(x)
814}