1use crate::support::edge_curve;
12use ogeom_algo::{Built, History, edge_vertices, make_edge_between, make_vertex, make_wire};
13use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
14use ogeom_geom::Curve3d as _;
15use ogeom_geom::{CircleCurve, Curve};
16use ogeom_math::{Circle, Direction, Frame, Point, Vector};
17use ogeom_topo::{Filter, Model, Orientation, Shape, ShapeType, explore};
18
19pub fn fillet_corner_2d(
34 model: &mut Model,
35 wire: &Shape,
36 vertex: &Shape,
37 radius: f64,
38 tol: Tolerances,
39) -> OgeomResult<Built> {
40 if !radius.is_finite() || radius <= tol.confusion() {
41 ogeom_bail!(Construction, "a fillet of radius {radius} rounds nothing");
42 }
43 let corner = corner_of(model, wire, vertex, tol)?;
44 corner.opening(tol)?;
45 let plane_z = {
52 let n = corner.sides[0].away.cross(corner.sides[1].away);
53 Direction::new(n, tol)?
54 };
55 let frame = Frame::new(
56 corner.point,
57 plane_z,
58 Direction::new(corner.sides[0].away, tol)?,
59 tol,
60 )?;
61 let flat = |p: Point| {
62 let l = frame.to_local(p);
63 ogeom_math::Point2::new(l.x, l.y)
64 };
65 let lift = |q: ogeom_math::Point2| {
66 frame.origin() + frame.x().vector() * q.x + frame.y().vector() * q.y
67 };
68 let loci = |side: &Side| -> OgeomResult<Vec<ogeom_geom::PlanarCurve>> {
69 Ok(match &side.curve {
70 Curve::Line(l) => {
71 let o = flat(l.axis().location);
72 let d = flat(l.axis().location + l.axis().direction.vector()) - o;
73 let d = d / d.magnitude();
74 let n = ogeom_math::Vector2::new(-d.y, d.x);
75 let axis = |shift: f64| {
76 ogeom_math::Direction2::new(d, tol)
77 .map(|dir| ogeom_math::Axis2::new(o + n * shift, dir))
78 };
79 vec![
80 ogeom_geom::Line2d::over(axis(radius)?, -1e6, 1e6)?.into(),
81 ogeom_geom::Line2d::over(axis(-radius)?, -1e6, 1e6)?.into(),
82 ]
83 }
84 Curve::Circle(c) => {
85 let centre = flat(c.circle().centre());
86 let big = c.circle().radius();
87 let mut out: Vec<ogeom_geom::PlanarCurve> = vec![
88 ogeom_geom::Circle2d::new(ogeom_math::Circle2::new(
89 ogeom_math::Frame2::new(centre, ogeom_math::Direction2::X),
90 big + radius,
91 tol,
92 )?)
93 .into(),
94 ];
95 if big - radius > tol.confusion() {
96 out.push(
97 ogeom_geom::Circle2d::new(ogeom_math::Circle2::new(
98 ogeom_math::Frame2::new(centre, ogeom_math::Direction2::X),
99 big - radius,
100 tol,
101 )?)
102 .into(),
103 );
104 }
105 out
106 }
107 _ => ogeom_bail!(Construction, "a corner side is neither line nor arc"),
108 })
109 };
110 let foot = |side: &Side, c2: ogeom_math::Point2| -> Option<(ogeom_math::Point2, f64)> {
113 match &side.curve {
114 Curve::Line(l) => {
115 let o = flat(l.axis().location);
116 let d = flat(l.axis().location + l.axis().direction.vector()) - o;
117 let d = d / d.magnitude();
118 let t = o + d * ((c2 - o).dot(d));
119 let corner2 = flat(side.curve.point_at(side.at, tol).ok()?);
120 let away2 = {
121 let a = flat(lift(corner2) + side.away) - corner2;
122 a / a.magnitude()
123 };
124 Some((t, (t - corner2).dot(away2)))
125 }
126 Curve::Circle(circle) => {
127 let centre = flat(circle.circle().centre());
128 let big = circle.circle().radius();
129 let v = c2 - centre;
130 let m = v.magnitude();
131 if m <= tol.confusion() {
132 return None;
133 }
134 let t = centre + v * (big / m);
135 let corner2 = flat(side.curve.point_at(side.at, tol).ok()?);
136 let a = (corner2 - centre).y.atan2((corner2 - centre).x);
137 let b = (t - centre).y.atan2((t - centre).x);
138 let mut delta = b - a;
139 let tau = core::f64::consts::TAU;
140 while delta > core::f64::consts::PI {
141 delta -= tau;
142 }
143 while delta < -core::f64::consts::PI {
144 delta += tau;
145 }
146 let winding = {
149 let d1 = flat(lift(corner2) + side.away * 1.0) - corner2;
150 let radial = corner2 - centre;
151 (radial.x * d1.y - radial.y * d1.x).signum()
152 };
153 Some((t, delta * winding))
154 }
155 _ => None,
156 }
157 };
158 let corner2 = flat(corner.point);
159 let bis2 = {
160 let a = flat(corner.point + corner.sides[0].away) - corner2;
161 let b = flat(corner.point + corner.sides[1].away) - corner2;
162 let u = a + b;
163 u / u.magnitude()
164 };
165 let mut best: Option<(ogeom_math::Point2, [ogeom_math::Point2; 2], [f64; 2], f64)> = None;
166 for la in loci(&corner.sides[0])? {
167 for lb in loci(&corner.sides[1])? {
168 let found = ogeom_intersect::intersect_curves_2d(
169 &la,
170 &lb,
171 ogeom_intersect::CurveCurveOptions::default(),
172 tol,
173 )?;
174 for crossing in &found.crossings {
175 let c2 = crossing.point;
176 if (c2 - corner2).dot(bis2) <= tol.confusion() {
177 continue;
178 }
179 let (Some((t0, d0)), Some((t1, d1))) =
180 (foot(&corner.sides[0], c2), foot(&corner.sides[1], c2))
181 else {
182 continue;
183 };
184 let room0 = corner.sides[0].room;
186 let room1 = corner.sides[1].room;
187 if d0 <= tol.parametric()
188 || d1 <= tol.parametric()
189 || d0 >= room0 - tol.parametric()
190 || d1 >= room1 - tol.parametric()
191 {
192 continue;
193 }
194 let score = c2.distance(corner2);
195 if best.as_ref().is_none_or(|(_, _, _, held)| score < *held) {
196 best = Some((c2, [t0, t1], [d0, d1], score));
197 }
198 }
199 }
200 }
201 let Some((centre2, feet, deltas, _)) = best else {
202 ogeom_bail!(
203 Construction,
204 "no circle of radius {radius} is tangent to both sides within \
205 their own extents"
206 );
207 };
208 let centre = lift(centre2);
209 let contacts = [lift(feet[0]), lift(feet[1])];
210 let trim_deltas = deltas;
211 let arc = |model: &mut Model, from: &Shape, to: &Shape| -> OgeomResult<Shape> {
212 let w1 = contacts[0] - centre;
213 let w2 = contacts[1] - centre;
214 let z = Direction::new(w1.cross(w2), tol)?;
215 let x = Direction::new(w1, tol)?;
216 let frame = Frame::new(centre, z, x, tol)?;
217 let circle = Circle::new(frame, radius, tol)?;
218 let sweep = w1.cross(w2).magnitude().atan2(w1.dot(w2));
219 Ok(make_edge_between(
220 model,
221 Curve::Circle(CircleCurve::new(circle)),
222 (0.0, sweep),
223 from,
224 to,
225 tol,
226 )?
227 .shape)
228 };
229 rebuild(model, wire, vertex, &corner, trim_deltas, arc, tol)
230}
231
232pub fn chamfer_corner_2d(
242 model: &mut Model,
243 wire: &Shape,
244 vertex: &Shape,
245 first: f64,
246 second: f64,
247 tol: Tolerances,
248) -> OgeomResult<Built> {
249 for distance in [first, second] {
250 if !distance.is_finite() || distance <= tol.confusion() {
251 ogeom_bail!(Construction, "a chamfer of {distance} cuts nothing");
252 }
253 }
254 let corner = corner_of(model, wire, vertex, tol)?;
255 corner.opening(tol)?;
256 let delta = |side: &Side, d: f64| -> f64 {
258 match &side.curve {
259 Curve::Circle(c) => d / c.circle().radius(),
260 _ => d,
261 }
262 };
263 let deltas = [
264 delta(&corner.sides[0], first),
265 delta(&corner.sides[1], second),
266 ];
267 let contacts = [
268 corner.sides[0].curve.point_at(
269 deltas[0].mul_add(corner.sides[0].sense, corner.sides[0].at),
270 tol,
271 )?,
272 corner.sides[1].curve.point_at(
273 deltas[1].mul_add(corner.sides[1].sense, corner.sides[1].at),
274 tol,
275 )?,
276 ];
277 let cut = |model: &mut Model, from: &Shape, to: &Shape| -> OgeomResult<Shape> {
278 let line = ogeom_geom::LineCurve::segment(contacts[0], contacts[1], tol)?;
279 let curve = Curve::Line(line);
280 let domain = ogeom_geom::Curve3d::domain(&curve);
281 Ok(make_edge_between(model, curve, domain, from, to, tol)?.shape)
282 };
283 rebuild(model, wire, vertex, &corner, deltas, cut, tol)
284}
285
286struct Side {
288 index: usize,
290 used: Shape,
292 curve: Curve,
294 away: Vector,
296 at: f64,
298 sense: f64,
300 room: f64,
302 far: Shape,
304}
305
306struct Corner {
309 point: Point,
310 edges: Vec<Shape>,
311 sides: [Side; 2],
312}
313
314impl Corner {
315 fn opening(&self, tol: Tolerances) -> OgeomResult<f64> {
317 let (a, b) = (self.sides[0].away, self.sides[1].away);
318 let angle = a.cross(b).magnitude().atan2(a.dot(b));
319 if angle <= tol.angular() || angle >= core::f64::consts::PI - tol.angular() {
320 ogeom_bail!(
321 Construction,
322 "the corner's edges are collinear; there is no corner to blend"
323 );
324 }
325 Ok(angle)
326 }
327}
328
329fn corner_of(model: &Model, wire: &Shape, vertex: &Shape, tol: Tolerances) -> OgeomResult<Corner> {
332 if model.kind_of(wire)? != ShapeType::Wire {
333 ogeom_bail!(Construction, "a 2D blend rounds a corner of a wire");
334 }
335 let edges = explore(model, wire, Filter::OfType(ShapeType::Edge))?;
336 if edges.len() < 2 {
337 ogeom_bail!(Construction, "a corner needs at least two edges");
338 }
339 let n = edges.len();
340 let mut found: Option<(usize, usize)> = None;
341 for i in 0..n {
342 let j = (i + 1) % n;
343 let Some((_, end)) = edge_vertices(model, &edges[i])? else {
344 continue;
345 };
346 let Some((start, _)) = edge_vertices(model, &edges[j])? else {
347 continue;
348 };
349 if end.node() == vertex.node() && start.node() == vertex.node() {
350 found = Some((i, j));
351 break;
352 }
353 }
354 let Some((i, j)) = found else {
355 ogeom_bail!(
356 Construction,
357 "the vertex is not a corner between two consecutive edges of the \
358 wire"
359 );
360 };
361
362 let point = {
363 let Some(node) = model.node(vertex) else {
364 ogeom_bail!(Dangling, "vertex is not in this model");
365 };
366 let Some(data) = node.data().as_vertex() else {
367 ogeom_bail!(Construction, "vertex node holds no point");
368 };
369 vertex.transform(model.datums())?.apply(data.point)
370 };
371
372 let side = |model: &Model, index: usize, corner_at_end: bool| -> OgeomResult<Side> {
373 let used = edges[index].clone();
374 let (curve, range) = edge_curve(model, &used, tol)?;
375 if !matches!(curve, Curve::Line(_) | Curve::Circle(_)) {
376 ogeom_bail!(
377 Construction,
378 "a corner blend against a free-form side has no closed-form \
379 tangency; lines and arcs do"
380 );
381 }
382 let reversed = used.orientation() == Orientation::Reversed;
385 let at_high = corner_at_end != reversed;
386 let (at, sense, room) = if at_high {
387 (range.1, -1.0, range.1 - range.0)
388 } else {
389 (range.0, 1.0, range.1 - range.0)
390 };
391 let away = {
392 let d = curve.d1_at(at, tol)? * sense;
393 let m = d.magnitude();
394 if m <= tol.confusion() {
395 ogeom_bail!(Construction, "a corner edge is degenerate at its corner");
396 }
397 d / m
398 };
399 let Some((start, end)) = edge_vertices(model, &used)? else {
400 ogeom_bail!(Construction, "a corner edge has no bounding vertices");
401 };
402 let far = if corner_at_end { start } else { end };
403 Ok(Side {
404 index,
405 used,
406 curve,
407 away,
408 at,
409 sense,
410 room,
411 far,
412 })
413 };
414 let sides = [side(model, i, true)?, side(model, j, false)?];
415 Ok(Corner {
416 point,
417 edges,
418 sides,
419 })
420}
421
422fn rebuild(
425 model: &mut Model,
426 wire: &Shape,
427 vertex: &Shape,
428 corner: &Corner,
429 trims: [f64; 2],
430 connector: impl FnOnce(&mut Model, &Shape, &Shape) -> OgeomResult<Shape>,
431 tol: Tolerances,
432) -> OgeomResult<Built> {
433 let mut history = History::new();
434 let mut trimmed: Vec<Shape> = Vec::with_capacity(2);
435 let mut joints: Vec<Shape> = Vec::with_capacity(2);
436 for (side, trim) in corner.sides.iter().zip(trims) {
437 if trim >= side.room - tol.parametric() {
438 ogeom_bail!(
439 Construction,
440 "a trim of {trim} consumes the whole edge; the blend reaches \
441 past the corner's neighbours"
442 );
443 }
444 let (curve, range) = edge_curve(model, &side.used, tol)?;
445 let contact = trim.mul_add(side.sense, side.at);
446 let new_range = if side.sense > 0.0 {
447 (contact, range.1)
448 } else {
449 (range.0, contact)
450 };
451 let joint = make_vertex(model, curve.point_at(contact, tol)?).shape;
452 let (from, to) = if side.sense > 0.0 {
456 (joint.clone(), side.far.clone())
457 } else {
458 (side.far.clone(), joint.clone())
459 };
460 let mut new_edge = make_edge_between(model, curve, new_range, &from, &to, tol)?.shape;
461 if side.used.orientation() == Orientation::Reversed {
462 new_edge = new_edge.reversed();
463 }
464 history.modify(&side.used, new_edge.clone());
465 trimmed.push(new_edge);
466 joints.push(joint);
467 }
468
469 let joined = connector(model, &joints[0], &joints[1])?;
470 history.generate(vertex, joined.clone());
471 history.delete(vertex);
472
473 let mut edges: Vec<Shape> = Vec::with_capacity(corner.edges.len() + 1);
474 for (k, e) in corner.edges.iter().enumerate() {
475 if k == corner.sides[0].index {
476 edges.push(trimmed[0].clone());
477 edges.push(joined.clone());
478 } else if k == corner.sides[1].index {
479 edges.push(trimmed[1].clone());
480 } else {
481 edges.push(e.clone());
482 }
483 }
484 if corner.sides[1].index < corner.sides[0].index {
487 edges.rotate_left(corner.sides[1].index + 1);
488 }
489 let built = make_wire(model, &edges, tol)?;
490 history.modify(wire, built.shape.clone());
491 Ok(Built::new(built.shape, history))
492}