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