1use ogeom_algo::{Across, Built, CurveDef, History, NetworkOptions, PatchDef, PatchSide};
28use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
29use ogeom_geom::{Continuity, Curve, Curve3d as _};
30use ogeom_math::{Point, Vector};
31use ogeom_topo::{Model, Shape, ShapeType, explore_unique};
32
33use crate::fill_n::{FillBoundary, Region, make_filling_n_region, make_filling_n_within};
34use crate::setback::{
35 CornerEnd, Setbacks, bevel_end, corner_edges, filler_inside, point_of, round_end, trim_conic,
36};
37
38const ALONG: u32 = 32;
40const SEED: u32 = 40;
42const FULL: [f64; 3] = [1.6, 4.0 / 3.0, 2.0];
53const AT_Q: f64 = 1e-7;
56const TURNS: u32 = 64;
57
58pub fn round_vertex_setback_sharp(
96 model: &mut Model,
97 solid: &Shape,
98 vertex: &Shape,
99 sharp: &Shape,
100 radius: f64,
101 setbacks: &Setbacks,
102 tol: Tolerances,
103) -> OgeomResult<Built> {
104 let mut own: Vec<Shape> = vec![vertex.clone(), sharp.clone()];
105 own.extend(setbacks.per_edge.iter().map(|(e, _)| e.clone()));
106 ogeom_algo::on_own_nodes(model, solid, &own, |model, solid, parts| {
107 let named: Vec<(Shape, f64)> = parts[2..]
108 .iter()
109 .zip(&setbacks.per_edge)
110 .map(|(e, (_, s))| (e.clone(), *s))
111 .collect();
112 let own = Setbacks {
113 per_edge: named,
114 default: setbacks.default,
115 continuity: setbacks.continuity,
116 };
117 sharp_own(model, solid, &parts[0], &parts[1], radius, &own, tol)
118 })
119}
120
121struct Side {
124 curve: Curve,
125 range: (f64, f64),
126}
127
128impl Side {
129 fn at(&self, f: f64, tol: Tolerances) -> OgeomResult<Point> {
130 let (lo, hi) = self.range;
131 self.curve.point_at((hi - lo).mul_add(f, lo), tol)
132 }
133
134 fn tangent(&self, f: f64, tol: Tolerances) -> OgeomResult<Vector> {
136 let (lo, hi) = self.range;
137 let d = self.curve.d1_at((hi - lo).mul_add(f, lo), tol)? * (hi - lo).signum();
138 d.normalized(tol)
139 }
140
141 fn def(&self) -> CurveDef {
142 CurveDef::Curve {
143 curve: self.curve.clone(),
144 range: self.range,
145 }
146 }
147}
148
149#[allow(clippy::too_many_lines, reason = "one corner, assembled end to end")]
152fn sharp_own(
153 model: &mut Model,
154 solid: &Shape,
155 vertex: &Shape,
156 sharp: &Shape,
157 radius: f64,
158 setbacks: &Setbacks,
159 tol: Tolerances,
160) -> OgeomResult<Built> {
161 if !radius.is_finite() || radius <= tol.confusion() {
162 ogeom_bail!(Construction, "a blend radius must be a positive distance");
163 }
164 if setbacks.continuity != Continuity::G1 {
165 ogeom_bail!(
166 Construction,
167 "a setback corner with an edge left sharp meets its bands and faces \
168 at G1, not {:?}",
169 setbacks.continuity
170 );
171 }
172 let frame = crate::corner::corner_frame(model, solid, vertex, radius, tol)?;
173 let n = frame.d.len();
174 let (edges, reach) = corner_edges(model, solid, vertex, &frame, tol)?;
175 let Some(ks) = edges.iter().position(|e| e.is_same(sharp)) else {
176 ogeom_bail!(
177 Construction,
178 "the edge left sharp does not leave the vertex"
179 );
180 };
181 if n != 3 {
182 ogeom_bail!(
183 Construction,
184 "a setback corner with an edge left sharp closes a vertex of three \
185 edges; one of {n} is still owed; see docs/CAPABILITIES.md, \
186 fillet.setback-corners"
187 );
188 }
189 let setback: Vec<f64> = edges
190 .iter()
191 .map(|e| {
192 setbacks
193 .per_edge
194 .iter()
195 .find(|(named, _)| named.is_same(e))
196 .map_or(setbacks.default, |(_, s)| *s)
197 })
198 .collect();
199 for k in 0..n {
200 let s = setback[k];
201 if !s.is_finite() || s <= tol.confusion() {
202 ogeom_bail!(Construction, "a setback of {s} is not a positive distance");
203 }
204 let own = (frame.far - frame.corner).dot(frame.d[k].vector());
205 if k == ks && s <= own + tol.confusion() {
206 ogeom_bail!(
207 Construction,
208 "the edge left sharp is cut at {s}, inside the bands' own \
209 corner, which reaches {own} along it; cut it farther from \
210 the vertex"
211 );
212 }
213 if k != ks && s < own - tol.confusion() {
214 ogeom_bail!(
215 Construction,
216 "a setback of {s} stops the band inside the ball's own corner, \
217 which reaches {own} along its edge"
218 );
219 }
220 if s >= reach[k] - tol.confusion() {
221 ogeom_bail!(
222 Construction,
223 "a setback of {s} reaches past its edge, {} long",
224 reach[k]
225 );
226 }
227 }
228 let rounded: Vec<Shape> = (0..n)
229 .filter(|&k| k != ks)
230 .map(|k| edges[k].clone())
231 .collect();
232 let banded = crate::fillet::fillet_edges(model, solid, &rounded, radius, tol)?;
233 let kinds = vec![CornerEnd::Round; n];
234 close_sharp_corner(
235 model, solid, banded, vertex, &frame, &setback, ks, &kinds, tol,
236 )
237}
238
239#[allow(clippy::too_many_lines, reason = "one corner, assembled end to end")]
249#[allow(clippy::too_many_arguments, reason = "one corner and its reading")]
250pub(crate) fn close_sharp_corner(
251 model: &mut Model,
252 solid: &Shape,
253 banded: Built,
254 vertex: &Shape,
255 frame: &crate::corner::CornerFrame,
256 setback: &[f64],
257 ks: usize,
258 kinds: &[CornerEnd],
259 tol: Tolerances,
260) -> OgeomResult<Built> {
261 let n = frame.d.len();
262 let mut walls: Vec<(Vector, Point)> = frame.m.iter().map(|mk| (*mk, frame.corner)).collect();
265 let within: Vec<(Vector, Point)> = frame.m.iter().map(|mk| (-*mk, frame.corner)).collect();
268 let sections: Vec<(Vector, Point)> = (0..n)
269 .map(|k| {
270 let dk = frame.d[k].vector();
271 (-dk, frame.corner + dk * setback[k])
272 })
273 .collect();
274 walls.extend(sections.iter().copied());
275 let cell = crate::corner::convex_block(model, &walls, tol)?;
276 let cut = ogeom_bool::cut(model, &banded.shape, &cell, tol)?;
277 let planes: Vec<(Vector, Point)> = frame.m.iter().map(|mk| (*mk, frame.corner)).collect();
278 crate::setback::guard_cell(
279 model,
280 solid,
281 &banded.shape,
282 &cut.shape,
283 vertex,
284 &planes,
285 &walls,
286 tol,
287 )?;
288 let history = banded.history.then(&cut.history);
289 let near = tol.confusion() * 100.0;
290 let on_plane = |p: Point, (normal, at): (Vector, Point)| normal.dot(p - at).abs() <= near;
291 let mut kept: Vec<Shape> = Vec::new();
292 for face in explore_unique(model, &cut.shape, ShapeType::Face)? {
293 let data = model.node(&face).and_then(|n| n.data().as_face());
294 let planar = data
295 .and_then(|d| model.geometry().surface(d.surface))
296 .is_some_and(|s| matches!(s, ogeom_geom::SurfaceGeometry::Plane(_)));
297 let on_section = planar && {
298 let (origin, outward) = ogeom_algo::face_normal(model, &face, tol)?;
299 sections.iter().any(|section| {
300 outward.cross(section.0).magnitude() <= tol.angular() * 10.0
301 && on_plane(origin, *section)
302 })
303 };
304 if !on_section {
305 kept.push(face);
306 }
307 }
308
309 let q_point = sections[ks].1;
311 let Some(vq) = explore_unique(model, &cut.shape, ShapeType::Vertex)?
312 .into_iter()
313 .find(|v| point_of(model, v).is_ok_and(|p| p.distance(q_point) <= near))
314 else {
315 ogeom_bail!(
316 Construction,
317 "the cut does not leave the sharp edge at its setback"
318 );
319 };
320 let mut arcs: Vec<Option<(Shape, Shape)>> = Vec::with_capacity(n);
321 for (k, section) in sections.iter().enumerate() {
322 arcs.push(if k == ks {
323 None
324 } else if kinds[k] == CornerEnd::Bevel {
325 Some(bevel_end(model, &kept, frame, *section, &walls, tol)?)
326 } else {
327 Some(round_end(model, &kept, k, *section, &walls, tol)?)
328 });
329 }
330 let host = |t: usize| -> (Vector, Point) { (frame.m[frame.ring[t]], frame.corner) };
331 let end_on = |model: &Model, k: usize, plane: (Vector, Point)| -> OgeomResult<(Shape, Point)> {
332 let Some((arc, _)) = &arcs[k] else {
333 return Ok((vq.clone(), q_point));
334 };
335 let Some((a, b)) = ogeom_algo::edge_vertices(model, arc)? else {
336 ogeom_bail!(Construction, "a band's end arc has no vertices");
337 };
338 for v in [a, b] {
339 let p = point_of(model, &v)?;
340 if on_plane(p, plane) {
341 return Ok((v, p));
342 }
343 }
344 ogeom_bail!(Construction, "a band's end arc does not reach its face");
345 };
346
347 let mut trims: Vec<Trim> = Vec::with_capacity(n);
352 let mut fillers: Vec<Shape> = Vec::with_capacity(n);
353 for k in 0..n {
354 let next = (k + 1) % n;
355 let plane = host(next);
356 let (v0, p0) = end_on(model, k, plane)?;
357 let (v1, p1) = end_on(model, next, plane)?;
358 let Some(q) = crate::corner::planes_meet(&plane, §ions[k], §ions[next]) else {
359 ogeom_bail!(
360 Construction,
361 "two section planes do not meet on the face between them"
362 );
363 };
364 let into = |from: (usize, Point), to: (usize, Point)| -> OgeomResult<Vector> {
365 if from.0 == ks {
366 let aim = to.1 - frame.d[to.0].vector() * (setback[to.0] / 2.0);
367 Ok((aim - from.1).normalized(tol)?)
368 } else {
369 Ok(-frame.d[from.0].vector())
370 }
371 };
372 let (conic, apex) = trim_conic(
373 (p0, into((k, p0), (next, p1))?),
374 (p1, into((next, p1), (k, p0))?),
375 tol,
376 )?;
377 let edges = trim_edges(model, &conic, (&v0, &v1), k == ks || next == ks, tol)?;
378 let filler = filler_inside(
379 model,
380 edges.clone(),
381 apex,
382 (&v0, p0),
383 (&v1, p1),
384 q,
385 plane,
386 tol,
387 )?;
388 trims.push(Trim {
389 curve: conic,
390 edges,
391 normal: plane.0,
392 filler: filler.clone(),
393 });
394 fillers.push(filler);
395 }
396 let (r1, r2) = ((ks + 1) % n, (ks + 2) % n);
397
398 let (Some(arc1), Some(arc2)) = (&arcs[r1], &arcs[r2]) else {
399 ogeom_bail!(Construction, "a rounded edge has no end arc");
400 };
401 let round = |k: usize| kinds[k] == CornerEnd::Round;
404 let (at_q, patch) = close_through_q(
405 model,
406 [&trims[ks], &trims[r1], &trims[r2]],
407 [arc1, arc2],
408 [
409 round(r1),
410 round(r1),
411 round(r1) && round(r2),
412 round(r2),
413 round(r2),
414 ],
415 &Held {
416 within: &within,
417 seam: &[],
418 region: None,
419 },
420 tol,
421 )?;
422 let mut faces = kept;
423 faces.extend(fillers);
424 sew_corner(model, faces, (at_q, patch), vertex, history, tol)
425}
426
427pub(crate) fn close_through_q(
439 model: &mut Model,
440 [out, middle, back]: [&Trim; 3],
441 [arc1, arc2]: [&(Shape, Shape); 2],
442 rest: [bool; 5],
443 held: &Held<'_>,
444 tol: Tolerances,
445) -> OgeomResult<(Shape, Shape)> {
446 let Held {
447 within,
448 seam,
449 region,
450 } = *held;
451 let out_half = Side {
455 curve: out.curve.clone(),
456 range: (0.0, 0.5),
457 };
458 let back_half = Side {
459 curve: back.curve.clone(),
460 range: (0.5, 1.0),
461 };
462 let across = |normal: Vector, along: Vector, towards: Vector| -> OgeomResult<Vector> {
463 let d = normal.cross(along).normalized(tol)?;
464 Ok(if d.dot(towards) >= 0.0 { d } else { -d })
465 };
466 let fit = crate::marched::band_fit_target(tol);
467 let attempt = |model: &mut Model, full: f64| -> OgeomResult<(Shape, Shape)> {
470 let arc = |d: f64, w: f64| -> OgeomResult<Point> {
475 let (from, to) = (out_half.at(d, tol)?, back_half.at(1.0 - d, tol)?);
476 let reach0 = from.distance(to) * full;
477 let leave = across(out.normal, out_half.tangent(d, tol)?, to - from)?;
478 let leave = turned_within(leave, out.normal, from, reach0, seam, tol)?;
479 let arrive = -across(back.normal, back_half.tangent(1.0 - d, tol)?, from - to)?;
480 let arrive = -turned_within(-arrive, back.normal, to, reach0, seam, tol)?;
481 let mut reach = reach0;
486 for (normal, on) in seam {
487 let lift = normal.dot(leave);
488 if lift > 0.0 {
489 reach = reach.min(3.0 * (-normal.dot(from - *on)).max(0.0) / lift);
490 }
491 let lift = -normal.dot(arrive);
492 if lift > 0.0 {
493 reach = reach.min(3.0 * (-normal.dot(to - *on)).max(0.0) / lift);
494 }
495 }
496 let (w2, w3) = (w * w, w * w * w);
497 let (h00, h10) = (2.0 * w3 - 3.0 * w2 + 1.0, w3 - 2.0 * w2 + w);
498 let (h01, h11) = (-2.0 * w3 + 3.0 * w2, w3 - w2);
499 Ok(Point::ORIGIN
500 + (from - Point::ORIGIN) * h00
501 + leave * (reach * h10)
502 + (to - Point::ORIGIN) * h01
503 + arrive * (reach * h11))
504 };
505 let between: Vec<Point> = (0..=ALONG)
506 .map(|i| arc(1.0, f64::from(i) / f64::from(ALONG)))
507 .collect::<OgeomResult<_>>()?;
508 let mut cloud: Vec<Point> = Vec::new();
509 for i in 1..=SEED {
510 let d = f64::from(i) / f64::from(SEED);
511 for j in 0..=SEED {
512 cloud.push(arc(d, f64::from(j) / f64::from(SEED))?);
513 }
514 }
515 let index = ogeom_mesh::PointIndex::build(&cloud)?;
516 let curves = vec![out_half.def(), CurveDef::Polyline(between), back_half.def()];
517 let side = |curve: usize| {
518 Some(PatchSide {
519 curve,
520 reversed: false,
521 })
522 };
523 let patches = vec![PatchDef {
524 sides: [side(0), side(1), side(2), None],
525 }];
526 let options = NetworkOptions {
530 tolerance: fit,
531 smoothing: fit * fit,
532 creases: Vec::new(),
533 spans: 2,
534 most: 16,
535 held: vec![0, 2],
536 across: vec![
537 (0, Across::Plane(out.normal)),
538 (2, Across::Plane(back.normal)),
539 ],
540 };
541 let network = ogeom_algo::fit_network(model, &patches, &curves, &index, &options, tol)?;
542 let surface = &network.surfaces[0];
546 let limit = |w: f64| -> OgeomResult<Vector> {
547 let (du, dw) = ogeom_geom::Surface::d1_at(surface, AT_Q, w, tol)?;
548 du.cross(dw * (1.0 / AT_Q)).normalized(tol)
551 };
552 let first = limit(0.0)?;
553 let mut turned = 0.0_f64;
554 for i in 1..=TURNS {
555 let now = limit(f64::from(i) / f64::from(TURNS))?
556 .dot(first)
557 .clamp(-1.0, 1.0)
558 .acos();
559 if now < turned - fit {
560 ogeom_bail!(
561 NotDone,
562 "the patch at the sharp edge turns its normal back by {} there",
563 turned - now
564 );
565 }
566 turned = turned.max(now);
567 }
568 let at_q = network.faces[0].clone();
569 let past = past_planes(model, &at_q, within, tol)?;
572 if past > fit {
573 ogeom_bail!(
574 NotDone,
575 "the patch at the sharp edge stands {past} past a face of the vertex"
576 );
577 }
578 let side = |edge: &Shape, support: &Shape, tangent: bool| FillBoundary {
583 edge: edge.clone(),
584 support: Some(support.clone()),
585 continuity: if tangent {
586 Continuity::G1
587 } else {
588 Continuity::C0
589 },
590 };
591 let boundary = vec![
592 side(&out.edges[1], &out.filler, rest[0]),
593 side(&arc1.0, &arc1.1, rest[1]),
594 side(&middle.edges[0], &middle.filler, rest[2]),
595 side(&arc2.0, &arc2.1, rest[3]),
596 side(&back.edges[0], &back.filler, rest[4]),
597 side(&network.edges[1], &at_q, true),
598 ];
599 let filled = match region {
602 Some(region) => make_filling_n_region(model, &boundary, &[], fit, region, tol)?,
603 None => make_filling_n_within(model, &boundary, &[], fit, within, tol)?,
604 };
605 Ok((at_q, filled.built.shape))
606 };
607 let mut made = None;
608 for (round, full) in FULL.iter().enumerate() {
609 match attempt(model, *full) {
610 Ok(both) => {
611 made = Some(both);
612 break;
613 }
614 Err(ogeom_core::OgeomError::NotDone(_)) if round + 1 < FULL.len() => {}
615 Err(e) => return Err(e),
616 }
617 }
618 let Some(both) = made else {
619 ogeom_bail!(NotDone, "no fullness was tried");
620 };
621 Ok(both)
622}
623
624pub(crate) fn sew_corner(
628 model: &mut Model,
629 mut faces: Vec<Shape>,
630 (at_q, patch): (Shape, Shape),
631 vertex: &Shape,
632 history: History,
633 tol: Tolerances,
634) -> OgeomResult<Built> {
635 let fit = crate::marched::band_fit_target(tol);
636 faces.push(at_q.clone());
637 faces.push(patch.clone());
638 let sewn = ogeom_algo::sew_within(model, &faces, fit, tol)?;
639 if sewn.shells.len() != 1 || !ogeom_algo::is_shell_closed(model, &sewn.shells[0])? {
640 ogeom_bail!(
641 Construction,
642 "the setback corner's faces did not close: {} shells",
643 sewn.shells.len()
644 );
645 }
646 let rounded = model.add_solid(std::slice::from_ref(&sewn.shells[0]))?;
647 let surface_of = |model: &Model, face: &Shape| {
650 model
651 .node(face)
652 .and_then(|n| n.data().as_face())
653 .map(|d| d.surface)
654 };
655 let mut history: History = history;
656 for made in [&at_q, &patch] {
657 let wanted = surface_of(model, made);
658 let Some(face) = explore_unique(model, &rounded, ShapeType::Face)?
659 .into_iter()
660 .find(|f| wanted.is_some() && surface_of(model, f) == wanted)
661 else {
662 ogeom_bail!(
663 Construction,
664 "a patch of the corner is not in the sewn solid"
665 );
666 };
667 history.generate(vertex, face);
668 }
669 Ok(Built::new(rounded, history))
670}
671
672pub(crate) fn trim_edges(
675 model: &mut Model,
676 conic: &Curve,
677 (v0, v1): (&Shape, &Shape),
678 split: bool,
679 tol: Tolerances,
680) -> OgeomResult<Vec<Shape>> {
681 let pieces: &[(f64, f64)] = if split {
682 &[(0.0, 0.5), (0.5, 1.0)]
683 } else {
684 &[(0.0, 1.0)]
685 };
686 let mut ends = vec![v0.clone()];
687 if split {
688 ends.push(ogeom_algo::make_vertex(model, conic.point_at(0.5, tol)?).shape);
689 }
690 ends.push(v1.clone());
691 let mut edges = Vec::with_capacity(pieces.len());
692 for (piece, range) in pieces.iter().enumerate() {
693 edges.push(
694 ogeom_algo::make_edge_between(
695 model,
696 conic.clone(),
697 *range,
698 &ends[piece],
699 &ends[piece + 1],
700 tol,
701 )?
702 .shape,
703 );
704 }
705 Ok(edges)
706}
707
708#[derive(Clone, Copy)]
713pub(crate) struct Held<'a> {
714 pub within: &'a [(Vector, Point)],
718 pub seam: &'a [(Vector, Point)],
721 pub region: Option<Region<'a>>,
724}
725
726pub(crate) struct Trim {
727 pub curve: Curve,
728 pub edges: Vec<Shape>,
729 pub normal: Vector,
730 pub filler: Shape,
731}
732
733fn past_planes(
737 model: &Model,
738 face: &Shape,
739 within: &[(Vector, Point)],
740 tol: Tolerances,
741) -> OgeomResult<f64> {
742 if within.is_empty() {
744 return Ok(f64::NEG_INFINITY);
745 }
746 let deflection = ogeom_mesh::Deflection {
747 chord: crate::marched::band_fit_target(tol) * 0.25,
748 ..ogeom_mesh::Deflection::default()
749 };
750 let mesh = ogeom_mesh::triangulate_face(model, face, deflection, tol)?;
751 Ok(mesh
752 .positions
753 .iter()
754 .flat_map(|p| within.iter().map(move |(n, at)| n.dot(*p - *at)))
755 .fold(f64::NEG_INFINITY, f64::max))
756}
757
758fn turned_within(
765 dir: Vector,
766 face: Vector,
767 from: Point,
768 reach: f64,
769 seam: &[(Vector, Point)],
770 tol: Tolerances,
771) -> OgeomResult<Vector> {
772 let mut d = dir;
773 if reach <= tol.confusion() {
774 return Ok(d);
775 }
776 for (normal, on) in seam {
777 let room = 3.0 * (-normal.dot(from - *on)).max(0.0) / reach;
778 let a = d.dot(*normal);
779 if a <= room {
780 continue;
781 }
782 let side = face.cross(d).normalized(tol)?;
783 let side = if side.dot(*normal) < 0.0 { side } else { -side };
784 let b = side.dot(*normal);
785 let r = a.hypot(b);
786 let turn = b.atan2(a) + (room / r).min(1.0).acos();
787 if !(0.0..=core::f64::consts::FRAC_PI_2).contains(&turn) {
788 return Ok(dir);
789 }
790 d = d * turn.cos() + side * turn.sin();
791 }
792 Ok(d)
793}