1use crate::asym::{RoundProfile, conic_pieces, legs, straight_seat};
17use crate::law::{Profile, RadiusLaw};
18use crate::support::{face_from_edges, segment_between};
19use ogeom_algo::{Built, make_edge_between, make_vertex};
20use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
21use ogeom_geom::{BSplineCurve, BSplineSurface, Curve, PlaneSurface, SurfaceGeometry};
22use ogeom_math::bspline::Weighted;
23use ogeom_math::{ControlGrid, Direction, KnotVector, Plane, Point, Point2, Vector};
24use ogeom_topo::{Model, Shape, ShapeType, explore_unique};
25
26#[allow(
53 clippy::too_many_arguments,
54 reason = "fillet_edge_distances' own, with laws for the distances"
55)]
56pub fn fillet_edge_distances_variable(
57 model: &mut Model,
58 solid: &Shape,
59 edge: &Shape,
60 face: &Shape,
61 on_face: &RadiusLaw,
62 on_other: &RadiusLaw,
63 profile: RoundProfile,
64 tol: Tolerances,
65) -> OgeomResult<Built> {
66 let laws = [
67 Profile::new(on_face, false, tol)?,
68 Profile::new(on_other, false, tol)?,
69 ];
70 if laws.iter().all(|law| law.level(tol)) {
71 let [a, b] = [laws[0].at(0.0).0, laws[1].at(0.0).0];
72 return crate::fillet_edge_distances(model, solid, edge, face, a, b, profile, tol);
73 }
74 ogeom_algo::on_own_nodes(
75 model,
76 solid,
77 &[edge.clone(), face.clone()],
78 |model, solid, parts| {
79 let mut built = varying(model, solid, &parts[0], &parts[1], &laws, profile, tol)?;
80 crate::support::credit_new_faces(model, solid, &parts[0], &mut built)?;
81 Ok(built)
82 },
83 )
84}
85
86const FIT_STATIONS: u32 = 16;
89
90fn varying(
93 model: &mut Model,
94 solid: &Shape,
95 edge: &Shape,
96 face: &Shape,
97 laws: &[Profile; 2],
98 profile: RoundProfile,
99 tol: Tolerances,
100) -> OgeomResult<Built> {
101 let (seat, named) = straight_seat(model, solid, edge, face, tol)?;
102 let law = |i: usize| if i == named { &laws[0] } else { &laws[1] };
104 let f = legs(&seat, tol)?;
105 let w = profile.weight(f[0].dot(f[1]).clamp(-1.0, 1.0))?;
106 square_ends(model, solid, edge, &seat, tol)?;
107 let length = seat.start.distance(seat.end);
108 let crease = |s: f64| seat.start + seat.along * (length * s);
109 for k in 0..=FIT_STATIONS {
110 let s = f64::from(k) / f64::from(FIT_STATIONS);
111 for (i, (leg, host)) in f.iter().zip(&seat.faces).enumerate() {
112 let distance = law(i).at(s).0;
113 let contact = crease(s) + *leg * distance;
114 if !crate::support::on_face_or_boundary(model, solid, host, contact, tol)? {
115 ogeom_bail!(
116 Construction,
117 "an asymmetric round sets back {distance} along a face of the edge, at \
118 {s:.3} of it, past that face's side; see docs/CAPABILITIES.md, \
119 fillet.asymmetric-rounds"
120 );
121 }
122 }
123 }
124
125 let sign = if seat.convex { 1.0 } else { -1.0 };
126 let VaryingNet {
127 first,
128 second,
129 rows,
130 weight,
131 u_knots,
132 v_knots,
133 surface,
134 } = varying_net(&seat, [law(0), law(1)], f, w, tol)?;
135 let (nu, nv) = (rows.len(), rows[0].len());
136 let surface_id = model.geometry_mut().add_surface(surface.clone());
137
138 let apex = [seat.start, seat.end];
139 let contact = |end: usize, side: usize| {
140 rows[if end == 0 { 0 } else { nu - 1 }][if side == 0 { 0 } else { nv - 1 }]
141 };
142 let ca = [contact(0, 0), contact(1, 0)];
143 let cb = [contact(0, 1), contact(1, 1)];
144 let va = [
145 make_vertex(model, ca[0]).shape,
146 make_vertex(model, ca[1]).shape,
147 ];
148 let vb = [
149 make_vertex(model, cb[0]).shape,
150 make_vertex(model, cb[1]).shape,
151 ];
152 let vapex = [
153 make_vertex(model, apex[0]).shape,
154 make_vertex(model, apex[1]).shape,
155 ];
156
157 let rail =
160 |model: &mut Model, column: usize, ends: &[Shape; 2], row_v: f64| -> OgeomResult<Shape> {
161 let control = rows.iter().map(|r| r[column]).collect::<Vec<_>>();
162 let curve = Curve::BSpline(BSplineCurve::new(
163 KnotVector::new(u_knots.clone(), 3)?,
164 control,
165 tol,
166 )?);
167 let built = make_edge_between(model, curve, (0.0, 1.0), &ends[0], &ends[1], tol)?.shape;
168 let pcurve = ogeom_geom::BSpline2d::new(
169 KnotVector::new(vec![0.0, 0.0, 1.0, 1.0], 1)?,
170 vec![Point2::new(0.0, row_v), Point2::new(1.0, row_v)],
171 tol,
172 )?;
173 ogeom_algo::attach_pcurve(
174 model,
175 &built,
176 pcurve.into(),
177 surface_id,
178 ogeom_topo::Location::identity(),
179 (0.0, 1.0),
180 )?;
181 Ok(built)
182 };
183 let rail_a = rail(model, 0, &va, 0.0)?;
184 let rail_b = rail(model, nv - 1, &vb, 1.0)?;
185
186 let arc = |model: &mut Model, end: usize| -> OgeomResult<Shape> {
189 let r = &rows[if end == 0 { 0 } else { nu - 1 }];
190 let control = r
191 .iter()
192 .enumerate()
193 .map(|(j, q)| Weighted::new(*q, if j % 2 == 1 { weight } else { 1.0 }, tol))
194 .collect::<OgeomResult<Vec<_>>>()?;
195 let curve = Curve::BSpline(BSplineCurve::rational(
196 KnotVector::new(v_knots.clone(), 2)?,
197 control,
198 )?);
199 let built = make_edge_between(model, curve, (0.0, 1.0), &va[end], &vb[end], tol)?.shape;
200 #[allow(clippy::cast_precision_loss, reason = "0 or 1")]
201 let column = ogeom_geom::Line2d::over(
202 ogeom_math::Axis2::new(Point2::new(end as f64, 0.0), ogeom_math::Direction2::Y),
203 0.0,
204 1.0,
205 )?;
206 ogeom_algo::attach_pcurve(
207 model,
208 &built,
209 column.into(),
210 surface_id,
211 ogeom_topo::Location::identity(),
212 (0.0, 1.0),
213 )?;
214 Ok(built)
215 };
216 let arc0 = arc(model, 0)?;
217 let arc1 = arc(model, 1)?;
218
219 let band = {
221 use ogeom_geom::Surface as _;
222 let wire = ogeom_algo::make_wire(
223 model,
224 &[
225 rail_a.clone(),
226 arc1.clone(),
227 rail_b.reversed(),
228 arc0.reversed(),
229 ],
230 tol,
231 )?
232 .shape;
233 let face =
234 ogeom_algo::make_face_on(model, surface_id, std::slice::from_ref(&wire), tol)?.shape;
235 let middle = surface.point_at(0.5, 0.5, tol)?;
236 let (du, dv) = surface.d1_at(0.5, 0.5, tol)?;
237 if du.cross(dv).dot(middle - crease(0.5)) >= 0.0 {
238 face
239 } else {
240 face.reversed()
241 }
242 };
243
244 let crease_edge = segment_between(model, (&vapex[0], apex[0]), (&vapex[1], apex[1]), tol)?;
245 let leg = |model: &mut Model,
248 side: usize,
249 rail: &Shape,
250 contacts: &[Point; 2],
251 ends: &[Shape; 2]|
252 -> OgeomResult<Shape> {
253 let outward = seat.normals[side] * sign;
254 let plane = Plane::through(apex[0], Direction::new(outward, tol)?);
255 let reach =
256 (length + contacts[0].distance(apex[0]) + contacts[1].distance(apex[1]) + 1.0) * 2.0;
257 let surface = PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?;
258 let at0 = segment_between(model, (&vapex[0], apex[0]), (&ends[0], contacts[0]), tol)?;
259 let at1 = segment_between(model, (&ends[1], contacts[1]), (&vapex[1], apex[1]), tol)?;
260 let edges = vec![at0, rail.clone(), at1, crease_edge.reversed()];
261 let turn = (contacts[0] - apex[0]).cross(apex[1] - apex[0]);
264 let edges: Vec<Shape> = if turn.dot(outward) < 0.0 {
265 edges.iter().rev().map(Shape::reversed).collect()
266 } else {
267 edges
268 };
269 face_from_edges(model, surface.into(), &edges, tol)
270 };
271 let leg_a = leg(model, first, &rail_a, &ca, &va)?;
272 let leg_b = leg(model, second, &rail_b, &cb, &vb)?;
273
274 let cap = |model: &mut Model, end: usize, arc: &Shape, outward: Vector| -> OgeomResult<Shape> {
275 let plane = Plane::through(apex[end], Direction::new(outward, tol)?);
276 let reach = (apex[end].distance(ca[end]) + apex[end].distance(cb[end]) + 1.0) * 2.0;
277 let surface = PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?;
278 let edges = vec![
279 segment_between(model, (&vapex[end], apex[end]), (&va[end], ca[end]), tol)?,
280 arc.clone(),
281 segment_between(model, (&vb[end], cb[end]), (&vapex[end], apex[end]), tol)?,
282 ];
283 let turn = (ca[end] - apex[end]).cross(cb[end] - apex[end]);
284 let edges: Vec<Shape> = if turn.dot(outward) < 0.0 {
285 edges.iter().rev().map(Shape::reversed).collect()
286 } else {
287 edges
288 };
289 face_from_edges(model, surface.into(), &edges, tol)
290 };
291 let cap0 = cap(model, 0, &arc0, -seat.along)?;
292 let cap1 = cap(model, 1, &arc1, seat.along)?;
293
294 let faces = [leg_a, leg_b, cap0, cap1, band];
295 let wedge = crate::support::wedge_solid(model, &faces, false, tol)?;
296 crate::support::apply_wedge_solid(model, solid, Some(edge), &wedge, !seat.convex, tol)
297}
298
299pub(crate) struct VaryingNet {
304 pub first: usize,
306 pub second: usize,
308 pub rows: Vec<Vec<Point>>,
310 pub weight: f64,
312 pub u_knots: Vec<f64>,
314 pub v_knots: Vec<f64>,
316 pub surface: SurfaceGeometry,
318}
319
320pub(crate) fn varying_net(
324 seat: &crate::support::Seat,
325 laws: [&Profile; 2],
326 f: [Vector; 2],
327 w: f64,
328 tol: Tolerances,
329) -> OgeomResult<VaryingNet> {
330 let length = seat.start.distance(seat.end);
331 let crease = |s: f64| seat.start + seat.along * (length * s);
332 let (first, second) = if seat
335 .along
336 .dot(seat.normals[0].cross(seat.normals[1]))
337 .is_sign_positive()
338 {
339 (0, 1)
340 } else {
341 (1, 0)
342 };
343
344 let pieces = [laws[first].pieces(tol), laws[second].pieces(tol)];
347 let mut breaks: Vec<f64> = pieces
348 .iter()
349 .flat_map(|p| p.iter().flat_map(|(a, b, _)| [*a, *b]))
350 .collect();
351 breaks.sort_by(f64::total_cmp);
352 breaks.dedup_by(|a, b| (*a - *b).abs() <= tol.parametric());
353 let windowed = |which: usize, a: f64, b: f64| -> [f64; 4] {
354 let middle = 0.5 * (a + b);
355 let &(x0, x1, bezier) = pieces[which]
356 .iter()
357 .find(|(x0, x1, _)| *x0 <= middle && middle <= *x1)
358 .unwrap_or(&pieces[which][0]);
359 bezier_window(bezier, (x0, x1), (a, b))
360 };
361 let mut along: Vec<(f64, f64, Point)> = Vec::new();
364 for (k, span) in breaks.windows(2).enumerate() {
365 let (a, b) = (span[0], span[1]);
366 let (da, db) = (windowed(0, a, b), windowed(1, a, b));
367 for i in usize::from(k > 0)..4 {
368 #[allow(clippy::cast_precision_loss, reason = "a Bezier index")]
369 let t = a + (b - a) * i as f64 / 3.0;
370 along.push((da[i], db[i], crease(t)));
371 }
372 }
373 let mut u_knots = vec![0.0; 4];
374 for &k in &breaks[1..breaks.len() - 1] {
375 u_knots.extend([k; 3]);
376 }
377 u_knots.extend([1.0; 4]);
378
379 let row = |(da, db, apex): (f64, f64, Point)| {
382 conic_pieces([apex + f[first] * da, apex, apex + f[second] * db], w)
383 };
384 let (_, weight, v_knots) = row(along[0]);
385 let rows: Vec<Vec<Point>> = along.iter().map(|&c| row(c).0).collect();
386 let (nu, nv) = (rows.len(), rows[0].len());
387 let mut net = Vec::with_capacity(nu * nv);
388 for r in &rows {
389 for (j, q) in r.iter().enumerate() {
390 net.push(Weighted::new(
391 *q,
392 if j % 2 == 1 { weight } else { 1.0 },
393 tol,
394 )?);
395 }
396 }
397 let surface: SurfaceGeometry = BSplineSurface::rational(
398 KnotVector::new(u_knots.clone(), 3)?,
399 KnotVector::new(v_knots.clone(), 2)?,
400 ControlGrid::new(net, nu, nv)?,
401 )?
402 .into();
403 Ok(VaryingNet {
404 first,
405 second,
406 rows,
407 weight,
408 u_knots,
409 v_knots,
410 surface,
411 })
412}
413
414fn bezier_window(bezier: [f64; 4], (x0, x1): (f64, f64), (a, b): (f64, f64)) -> [f64; 4] {
417 let h = x1 - x0;
418 let (ta, tb) = ((a - x0) / h, (b - x0) / h);
419 let blossom = |t: [f64; 3]| -> f64 {
422 let mut p = bezier;
423 for (level, &x) in t.iter().enumerate() {
424 for i in 0..3 - level {
425 p[i] = p[i] * (1.0 - x) + p[i + 1] * x;
426 }
427 }
428 p[0]
429 };
430 [
431 blossom([ta, ta, ta]),
432 blossom([ta, ta, tb]),
433 blossom([ta, tb, tb]),
434 blossom([tb, tb, tb]),
435 ]
436}
437
438fn square_ends(
443 model: &Model,
444 solid: &Shape,
445 edge: &Shape,
446 seat: &crate::support::Seat,
447 tol: Tolerances,
448) -> OgeomResult<()> {
449 let Some((a, b)) = ogeom_algo::edge_vertices(model, edge)? else {
450 return Ok(());
451 };
452 for face in explore_unique(model, solid, ShapeType::Face)? {
453 if seat.faces.iter().any(|f| f.is_same(&face)) {
454 continue;
455 }
456 let vertices = explore_unique(model, &face, ShapeType::Vertex)?;
457 let meets = vertices.iter().any(|v| {
458 crate::support::same_occurrence(model, v, &a, tol)
459 || crate::support::same_occurrence(model, v, &b, tol)
460 });
461 if !meets {
462 continue;
463 }
464 let square = match face_normal(model, &face, tol)? {
465 Some(normal) => normal.cross(seat.along).magnitude() <= tol.angular(),
466 None => false,
467 };
468 if !square {
469 ogeom_bail!(
470 Construction,
471 "an asymmetric round whose distances vary is capped square at its edge's \
472 ends, and an end of this edge meets a face not square to it; see \
473 docs/CAPABILITIES.md, fillet.asymmetric-rounds"
474 );
475 }
476 }
477 Ok(())
478}
479
480fn face_normal(model: &Model, face: &Shape, tol: Tolerances) -> OgeomResult<Option<Vector>> {
483 use ogeom_geom::Transformable as _;
484 let Some(data) = model.node(face).and_then(|n| n.data().as_face()) else {
485 return Ok(None);
486 };
487 let Some(surface) = model.geometry().surface(data.surface) else {
488 return Ok(None);
489 };
490 let placed = surface
491 .clone()
492 .transformed(&face.transform(model.datums())?, tol)?;
493 Ok(match placed {
494 SurfaceGeometry::Plane(p) => Some(p.plane().normal().vector()),
495 _ => None,
496 })
497}