1use ogeom_algo::Built;
15use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
16use ogeom_geom::SurfaceGeometry;
17use ogeom_math::{Point, Vector};
18use ogeom_topo::{Model, NodeData, Shape, ShapeType, explore_unique};
19
20use crate::fillet::seated_fillet;
21use crate::support::Seat;
22
23pub fn blend_faces(
41 model: &mut Model,
42 solid: &Shape,
43 a: &Shape,
44 b: &Shape,
45 radius: f64,
46 tol: Tolerances,
47) -> OgeomResult<Built> {
48 if !radius.is_finite() || radius <= tol.confusion() {
49 ogeom_bail!(Construction, "a blend of radius {radius} rounds nothing");
50 }
51 let (plane_a, normal_a) = planar_face_of(model, a, tol)?;
52 let (plane_b, normal_b) = planar_face_of(model, b, tol)?;
53
54 let along = normal_a.cross(normal_b);
57 let magnitude = along.magnitude();
58 if magnitude <= tol.angular() {
59 ogeom_bail!(
60 Construction,
61 "the two faces are parallel; there is no corner between them"
62 );
63 }
64 let along = along / magnitude;
65 let seed = meet(plane_a, normal_a, plane_b, normal_b, along, tol)?;
66
67 let span = |face: &Shape| -> OgeomResult<(f64, f64)> {
70 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
71 for vertex in explore_unique(model, face, ShapeType::Vertex)? {
72 let Some(point) = model
73 .node(&vertex)
74 .and_then(|n| n.data().as_vertex().map(|v| v.point))
75 else {
76 continue;
77 };
78 let placed = vertex.transform(model.datums())?.apply(point);
79 let t = (placed - seed).dot(along);
80 lo = lo.min(t);
81 hi = hi.max(t);
82 }
83 if !lo.is_finite() {
84 ogeom_bail!(Construction, "a face with no vertices seats nothing");
85 }
86 Ok((lo, hi))
87 };
88 let (a0, a1) = span(a)?;
89 let (b0, b1) = span(b)?;
90 let (lo, hi) = (a0.max(b0), a1.min(b1));
91 if hi - lo <= tol.confusion() {
92 ogeom_bail!(
93 Construction,
94 "the two faces do not both reach the line their planes meet on, \
95 so there is no stretch to seat a blend over"
96 );
97 }
98
99 let unit = |v: Vector| -> OgeomResult<Vector> {
107 let m = v.magnitude();
108 if m <= tol.angular() {
109 ogeom_bail!(Construction, "the faces meet too sharply to seat a blend");
110 }
111 Ok(v / m)
112 };
113 let middle = seed + along * f64::midpoint(lo, hi);
114 let step = (hi - lo).min(radius) * 1e-3 + tol.confusion();
115 let mut convex = true;
116 for side in [unit(normal_a - normal_b)?, unit(normal_b - normal_a)?] {
117 if matches!(
118 ogeom_algo::classify_in_solid_exact(model, solid, middle + side * step, tol)?,
119 ogeom_algo::Containment::In
120 ) {
121 convex = false;
122 }
123 }
124
125 let seat = Seat {
126 start: seed + along * lo,
127 end: seed + along * hi,
128 along,
129 normals: [normal_a, normal_b],
130 faces: [a.clone(), b.clone()],
131 convex,
132 };
133 seated_fillet(model, solid, &seat, radius, None, tol)
134}
135
136fn planar_face_of(model: &Model, face: &Shape, tol: Tolerances) -> OgeomResult<(Point, Vector)> {
138 let Some(node) = model.node(face) else {
139 ogeom_bail!(Dangling, "face is not in this model");
140 };
141 let NodeData::Face(data) = node.data() else {
142 ogeom_bail!(Construction, "expected a face");
143 };
144 let Some(SurfaceGeometry::Plane(plane)) = model.geometry().surface(data.surface) else {
145 ogeom_bail!(
146 Construction,
147 "a face-face blend between curved supports needs the marching \
148 seat; this is the planar form"
149 );
150 };
151 let placement = face.transform(model.datums())?;
152 let origin = placement.apply(plane.plane().frame().origin());
153 let mut normal = placement.apply_vector(plane.plane().normal().vector());
154 if face.orientation() == ogeom_topo::Orientation::Reversed {
155 normal = -normal;
156 }
157 let magnitude = normal.magnitude();
158 if magnitude <= tol.angular() {
159 ogeom_bail!(Construction, "a face with no normal faces nothing");
160 }
161 Ok((origin, normal / magnitude))
162}
163
164fn meet(
168 origin_a: Point,
169 normal_a: Vector,
170 origin_b: Point,
171 normal_b: Vector,
172 along: Vector,
173 tol: Tolerances,
174) -> OgeomResult<Point> {
175 let rows = [normal_a, normal_b, along];
176 let rhs = [
177 normal_a.dot(origin_a.to_vector()),
178 normal_b.dot(origin_b.to_vector()),
179 along.dot(Point::midpoint(origin_a, origin_b).to_vector()),
180 ];
181 let det = rows[0].dot(rows[1].cross(rows[2]));
182 if det.abs() <= tol.confusion() {
183 ogeom_bail!(Construction, "the two planes do not meet in a line");
184 }
185 let c0 = rows[1].cross(rows[2]);
188 let c1 = rows[2].cross(rows[0]);
189 let c2 = rows[0].cross(rows[1]);
190 let v = (c0 * rhs[0] + c1 * rhs[1] + c2 * rhs[2]) / det;
191 Ok(Point::ORIGIN + v)
192}