Skip to main content

ogeom_fillet/
chamfer_corner.rs

1//! Where three bevels meet at a vertex: a point or a facet.
2
3use crate::Chamfer;
4use crate::chamfer::{Legs, legs};
5use crate::support::planar_face;
6use ogeom_algo::Built;
7use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
8use ogeom_math::{Point, Vector};
9use ogeom_topo::{Model, Shape};
10
11/// How a chamfer request closes a vertex where three or more of its bevels
12/// meet.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ChamferCorner {
15    /// The bevels mitre into one point, by wedges built on the solid as given
16    /// and applied together.
17    Point,
18    /// Each bevel stops at the plane through the points where two bevels'
19    /// contact lines cross on the face they share, and the corner is the
20    /// polygon there: a triangle at a corner of three bevels. What
21    /// [`chamfer_edges_with`](crate::chamfer_edges_with) builds.
22    Facet,
23}
24
25/// [`chamfer_edges_with`](crate::chamfer_edges_with), the vertices where
26/// three or more of its bevels meet closed as `corner` says.
27///
28/// [`ChamferCorner::Facet`] is exactly
29/// [`chamfer_edges_with`](crate::chamfer_edges_with): on a solid bounded by
30/// planes the blend engine closes each such vertex, convex, concave or
31/// mixed, with the polygon through its faces' setback corners. Elsewhere,
32/// and past what the engine's setbacks hold, each such vertex is a convex corner of three
33/// planes, each pair of its bevels sharing one face there: on that face the
34/// two bevels' contact lines cross at a point, and the three points span
35/// the facet's plane. The mitred bevels meet at the point their three planes
36/// share, and the tetrahedron between it and the three points, whose sides
37/// lie on the bevels, is cut away: each bevel ends on the facet, and a box's
38/// corner of three bevels `d` gains the triangle cutting each edge at `2d`
39/// from the corner, its point's tetrahedron `d³ / 12` taken off.
40///
41/// At a vertex of `k > 3` bevels, every face there between two of them in
42/// turn round it, the `k` points where consecutive bevels' contact lines
43/// cross must lie on one plane, within the confusion distance, for a facet
44/// through them all to exist: as at a pyramid's apex beveled alike. The
45/// solid cut away is then the convex piece on the vertex's side of that
46/// plane and the solid's side of every bevel's plane, whose faces are the
47/// facet and the bevels' parts past it.
48///
49/// # Errors
50///
51/// As [`chamfer_edges_with`](crate::chamfer_edges_with), and for
52/// [`ChamferCorner::Facet`]
53/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) where
54/// a vertex of three or more of the request's bevels holds one that is not
55/// a straight convex edge between planes, two in turn round it that share
56/// no face, facet points that do not stand on the edges' own lengths, or,
57/// past three, facet points off one plane.
58pub fn chamfer_edges_cornered(
59    model: &mut Model,
60    solid: &Shape,
61    specs: &[(Shape, Chamfer)],
62    corner: ChamferCorner,
63    tol: Tolerances,
64) -> OgeomResult<Built> {
65    if specs.is_empty() {
66        ogeom_bail!(Construction, "a chain of no edges bevels nothing");
67    }
68    let (specs, _) = crate::chamfer::without_seams(model, solid, specs, tol)?;
69    if specs.is_empty() {
70        return Ok(crate::chamfer::unchanged(solid));
71    }
72    let specs = &specs[..];
73    if corner == ChamferCorner::Point {
74        return crate::chamfer::chamfer_edges_mitred(model, solid, specs, tol);
75    }
76    // A solid bounded by planes is chamfered by the blend engine in one
77    // pass: every face set back, every vertex its facet.
78    if let Some(built) = crate::chamfer::engine_chamfer(model, solid, specs, tol)? {
79        return Ok(built);
80    }
81    let cuts = facets(model, solid, specs, tol)?;
82    let mut built = crate::chamfer::chamfer_edges_mitred(model, solid, specs, tol)?;
83    for cut in cuts {
84        let faces = match cut {
85            Cut::Tetrahedron(corners) => tetrahedron(model, corners, tol)?,
86            Cut::Polytope(polygons) => {
87                let mut faces = Vec::with_capacity(polygons.len());
88                for (ring, outward) in &polygons {
89                    faces.push(planar_face(model, ring, *outward, tol)?);
90                }
91                faces
92            }
93        };
94        let step = crate::support::apply_wedge(model, &built.shape, None, &faces, false, tol)?;
95        built = Built {
96            shape: step.shape.clone(),
97            history: built.history.then(&step.history),
98        };
99    }
100    Ok(built)
101}
102
103/// What a faceted corner sheds.
104enum Cut {
105    /// At a corner of three bevels: the bevels' shared point, then the
106    /// three facet points.
107    Tetrahedron([Point; 4]),
108    /// At a corner of more: the convex piece's faces, each a ring of
109    /// points with its outward normal.
110    Polytope(Vec<(Vec<Point>, Vector)>),
111}
112
113/// The piece each vertex of three or more bevels sheds.
114fn facets(
115    model: &Model,
116    solid: &Shape,
117    specs: &[(Shape, Chamfer)],
118    tol: Tolerances,
119) -> OgeomResult<Vec<Cut>> {
120    let conf = tol.confusion();
121    let mut ends: Vec<[ogeom_topo::TShapeId; 2]> = Vec::with_capacity(specs.len());
122    let mut points: Vec<(ogeom_topo::TShapeId, Point)> = Vec::new();
123    for (edge, _) in specs {
124        let Some((a, b)) = ogeom_algo::edge_vertices(model, edge)? else {
125            ogeom_bail!(Construction, "a chamfered edge has no vertices");
126        };
127        for v in [&a, &b] {
128            let Some(data) = model.node(v).and_then(|n| n.data().as_vertex()) else {
129                ogeom_bail!(Dangling, "a vertex is not in this model");
130            };
131            points.push((v.node(), v.transform(model.datums())?.apply(data.point)));
132        }
133        ends.push([a.node(), b.node()]);
134    }
135    let mut vertices: Vec<ogeom_topo::TShapeId> = Vec::new();
136    for pair in &ends {
137        for v in pair {
138            if !vertices.contains(v) && ends.iter().filter(|e| e.contains(v)).count() >= 3 {
139                vertices.push(*v);
140            }
141        }
142    }
143    let mut out = Vec::with_capacity(vertices.len());
144    for v in vertices {
145        let at: Vec<usize> = (0..specs.len()).filter(|&i| ends[i].contains(&v)).collect();
146        let [i, j, k] = at.as_slice() else {
147            out.push(Cut::Polytope(polygon_corner(
148                model, solid, specs, &at, v, &points, tol,
149            )?));
150            continue;
151        };
152        // Each bevel's seat, its legs' distances, and its edge leaving the
153        // vertex.
154        let mut bevels = Vec::with_capacity(3);
155        for &n in [i, j, k] {
156            let (edge, spec) = &specs[n];
157            let Legs::Straight(seat, distances) = legs(model, solid, edge, spec, tol)? else {
158                ogeom_bail!(
159                    Construction,
160                    "a facet closes a corner of bevels on straight edges between planes"
161                );
162            };
163            if !seat.convex {
164                ogeom_bail!(Construction, "a facet closes a convex corner");
165            }
166            // The seat's end standing at the vertex.
167            let Some(&(_, at)) = points.iter().find(|(id, _)| *id == v) else {
168                ogeom_bail!(Dangling, "a vertex is not in this model");
169            };
170            let at_start = seat.start.distance(at) <= seat.end.distance(at);
171            let (vertex, away) = if at_start {
172                (seat.start, seat.along)
173            } else {
174                (seat.end, -seat.along)
175            };
176            bevels.push((seat, distances, vertex, away));
177        }
178        // A bevel's plane: through its contact lines, normal to them both.
179        let plane = |n: usize| -> OgeomResult<(Point, Vector)> {
180            let (seat, d, _, _) = &bevels[n];
181            let a = seat.start + seat.leg(0, tol)? * d[0];
182            let b = seat.start + seat.leg(1, tol)? * d[1];
183            Ok((a, seat.along.cross(b - a)))
184        };
185        let planes = [plane(0)?, plane(1)?, plane(2)?];
186        let Some(point) = meet(&planes) else {
187            ogeom_bail!(Construction, "the corner's three bevels share no point");
188        };
189        // On the face two bevels share, where their contact lines cross.
190        let mut feet = Vec::with_capacity(3);
191        for (m, n) in [(0, 1), (1, 2), (2, 0)] {
192            let (sm, dm, vm, um) = &bevels[m];
193            let (sn, dn, vn, un) = &bevels[n];
194            let shared: Vec<(usize, usize)> = (0..2)
195                .flat_map(|a| (0..2).map(move |b| (a, b)))
196                .filter(|&(a, b)| sm.faces[a].node() == sn.faces[b].node())
197                .collect();
198            let [(a, b)] = shared.as_slice() else {
199                ogeom_bail!(
200                    Construction,
201                    "two bevels of a faceted corner share no single face"
202                );
203            };
204            let pm = *vm + sm.leg(*a, tol)? * dm[*a];
205            let pn = *vn + sn.leg(*b, tol)? * dn[*b];
206            let Some((s, t, gap)) = crossing(pm, *um, pn, *un) else {
207                ogeom_bail!(
208                    Construction,
209                    "two bevels of a faceted corner run parallel on their face"
210                );
211            };
212            // The crossing stands on both edges' own lengths, and the lines
213            // cross on the face.
214            let reach = |seat: &crate::support::Seat| seat.start.distance(seat.end);
215            if gap > conf * 10.0
216                || s <= conf
217                || t <= conf
218                || s >= reach(sm) - conf
219                || t >= reach(sn) - conf
220            {
221                ogeom_bail!(
222                    Construction,
223                    "the facet's point on a face stands off the edges' lengths"
224                );
225            }
226            feet.push(pm + *um * s);
227        }
228        // The point is on the vertex's side of the facet.
229        let normal = (feet[1] - feet[0]).cross(feet[2] - feet[0]);
230        let side = |p: Point| normal.dot(p - feet[0]);
231        let vertex = bevels[0].2;
232        let depth = side(point) / normal.magnitude();
233        if depth.abs() <= conf || depth.signum() != side(vertex).signum() {
234            ogeom_bail!(
235                Construction,
236                "the bevels' shared point stands on the facet's far side"
237            );
238        }
239        out.push(Cut::Tetrahedron([point, feet[0], feet[1], feet[2]]));
240    }
241    Ok(out)
242}
243
244/// A bevel's seat at vertex `v`, its legs' distances, its end at the
245/// vertex and its direction away from it.
246type Bevel = (crate::support::Seat, [f64; 2], Point, Vector);
247
248/// The faces of the convex piece a vertex of the `at` bevels, more than
249/// three, sheds: on the vertex's side of the plane through the points
250/// where consecutive bevels' contact lines cross on the face they share,
251/// and on the solid's side of every bevel's plane.
252fn polygon_corner(
253    model: &Model,
254    solid: &Shape,
255    specs: &[(Shape, Chamfer)],
256    at: &[usize],
257    v: ogeom_topo::TShapeId,
258    points: &[(ogeom_topo::TShapeId, Point)],
259    tol: Tolerances,
260) -> OgeomResult<Vec<(Vec<Point>, Vector)>> {
261    let conf = tol.confusion();
262    let Some(&(_, vertex)) = points.iter().find(|(id, _)| *id == v) else {
263        ogeom_bail!(Dangling, "a vertex is not in this model");
264    };
265    let mut bevels: Vec<Bevel> = Vec::with_capacity(at.len());
266    for &n in at {
267        let (edge, spec) = &specs[n];
268        let Legs::Straight(seat, distances) = legs(model, solid, edge, spec, tol)? else {
269            ogeom_bail!(
270                Construction,
271                "a facet closes a corner of bevels on straight edges between planes"
272            );
273        };
274        if !seat.convex {
275            ogeom_bail!(Construction, "a facet closes a convex corner");
276        }
277        let at_start = seat.start.distance(vertex) <= seat.end.distance(vertex);
278        let (end, away) = if at_start {
279            (seat.start, seat.along)
280        } else {
281            (seat.end, -seat.along)
282        };
283        bevels.push((seat, distances, end, away));
284    }
285    // The face two bevels share, as each one's leg index.
286    let shared = |m: usize, n: usize| -> Option<(usize, usize)> {
287        let pairs: Vec<(usize, usize)> = (0..2)
288            .flat_map(|a| (0..2).map(move |b| (a, b)))
289            .filter(|&(a, b)| bevels[m].0.faces[a].node() == bevels[n].0.faces[b].node())
290            .collect();
291        match pairs.as_slice() {
292            [one] => Some(*one),
293            _ => None,
294        }
295    };
296    // The bevels in turn round the vertex, each sharing a face with the
297    // next.
298    let mut ring = vec![0];
299    while ring.len() < bevels.len() {
300        let last = ring[ring.len() - 1];
301        let Some(next) =
302            (0..bevels.len()).find(|&n| !ring.contains(&n) && shared(last, n).is_some())
303        else {
304            ogeom_bail!(
305                Construction,
306                "two bevels in turn round a faceted corner share no single face"
307            );
308        };
309        ring.push(next);
310    }
311    if shared(ring[ring.len() - 1], ring[0]).is_none() {
312        ogeom_bail!(
313            Construction,
314            "two bevels in turn round a faceted corner share no single face"
315        );
316    }
317    // Where consecutive bevels' contact lines cross on their face.
318    let reach = |seat: &crate::support::Seat| seat.start.distance(seat.end);
319    let mut feet = Vec::with_capacity(ring.len());
320    for w in 0..ring.len() {
321        let (m, n) = (ring[w], ring[(w + 1) % ring.len()]);
322        let Some((a, b)) = shared(m, n) else {
323            ogeom_bail!(
324                Construction,
325                "two bevels in turn round a faceted corner share no single face"
326            );
327        };
328        let (sm, dm, vm, um) = &bevels[m];
329        let (sn, dn, vn, un) = &bevels[n];
330        let pm = *vm + sm.leg(a, tol)? * dm[a];
331        let pn = *vn + sn.leg(b, tol)? * dn[b];
332        let Some((s, t, gap)) = crossing(pm, *um, pn, *un) else {
333            ogeom_bail!(
334                Construction,
335                "two bevels of a faceted corner run parallel on their face"
336            );
337        };
338        if gap > conf * 10.0
339            || s <= conf
340            || t <= conf
341            || s >= reach(sm) - conf
342            || t >= reach(sn) - conf
343        {
344            ogeom_bail!(
345                Construction,
346                "the facet's point on a face stands off the edges' lengths"
347            );
348        }
349        feet.push(pm + *um * s);
350    }
351    // The facet's plane, by Newell's sum, and every point on it.
352    let mut normal = Vector::ZERO;
353    for w in 0..feet.len() {
354        let (p, q) = (
355            feet[w] - Point::ORIGIN,
356            feet[(w + 1) % feet.len()] - Point::ORIGIN,
357        );
358        normal += p.cross(q);
359    }
360    let size = normal.magnitude();
361    if size.is_nan() || size <= 0.0 {
362        ogeom_bail!(Construction, "the facet's points span no plane");
363    }
364    let normal = normal / size;
365    let centre = Point::ORIGIN
366        + feet
367            .iter()
368            .fold(Vector::ZERO, |sum, p| sum + (*p - Point::ORIGIN))
369            / f64::from(u32::try_from(feet.len()).unwrap_or(u32::MAX));
370    let off = feet
371        .iter()
372        .fold(0.0_f64, |worst, p| worst.max(normal.dot(*p - centre).abs()));
373    if off > conf {
374        ogeom_bail!(
375            Construction,
376            "the {} facet points of a corner stand {off:.3e} off one plane",
377            feet.len()
378        );
379    }
380    // The bounding planes as `(point, normal)`, each normal facing out of
381    // the piece: the facet's away from the vertex, each bevel's toward it.
382    let toward = if normal.dot(vertex - centre) > 0.0 {
383        1.0
384    } else {
385        -1.0
386    };
387    let mut planes: Vec<(Point, Vector)> = vec![(centre, normal * -toward)];
388    for (seat, d, _, _) in &bevels {
389        let a = seat.start + seat.leg(0, tol)? * d[0];
390        let b = seat.start + seat.leg(1, tol)? * d[1];
391        let mut n = seat.along.cross(b - a);
392        if n.dot(vertex - a) < 0.0 {
393            n = -n;
394        }
395        planes.push((a, n / n.magnitude()));
396    }
397    // The piece's corners: every three planes' shared point inside all.
398    let inside = |p: Point| planes.iter().all(|(q, n)| n.dot(p - *q) <= conf * 10.0);
399    let mut corners: Vec<Point> = Vec::new();
400    for x in 0..planes.len() {
401        for y in x + 1..planes.len() {
402            for z in y + 1..planes.len() {
403                let Some(p) = meet(&[planes[x], planes[y], planes[z]]) else {
404                    continue;
405                };
406                if inside(p) && !corners.iter().any(|c| c.distance(p) <= conf * 10.0) {
407                    corners.push(p);
408                }
409            }
410        }
411    }
412    let middle = Point::ORIGIN
413        + corners
414            .iter()
415            .fold(Vector::ZERO, |sum, p| sum + (*p - Point::ORIGIN))
416            / f64::from(u32::try_from(corners.len().max(1)).unwrap_or(u32::MAX));
417    // Each plane's face: the corners on it, in turn round its normal.
418    let mut faces = Vec::with_capacity(planes.len());
419    for (q, n) in &planes {
420        let mut on: Vec<Point> = corners
421            .iter()
422            .copied()
423            .filter(|p| n.dot(*p - *q).abs() <= conf * 10.0)
424            .collect();
425        if on.len() < 3 {
426            ogeom_bail!(
427                Construction,
428                "a faceted corner's piece has a plane touching it at fewer than three corners"
429            );
430        }
431        let mid = Point::ORIGIN
432            + on.iter()
433                .fold(Vector::ZERO, |sum, p| sum + (*p - Point::ORIGIN))
434                / f64::from(u32::try_from(on.len()).unwrap_or(u32::MAX));
435        let x = (on[0] - mid) / (on[0] - mid).magnitude();
436        let y = n.cross(x);
437        on.sort_by(|a, b| {
438            let angle = |p: &Point| (*p - mid).dot(y).atan2((*p - mid).dot(x));
439            angle(a).total_cmp(&angle(b))
440        });
441        let outward = if n.dot(mid - middle) >= 0.0 { *n } else { -*n };
442        faces.push((on, outward));
443    }
444    Ok(faces)
445}
446
447/// The point three planes, each a point and a normal, share.
448fn meet(planes: &[(Point, Vector); 3]) -> Option<Point> {
449    let [(p0, n0), (p1, n1), (p2, n2)] = *planes;
450    let det = n0.dot(n1.cross(n2));
451    let scale = n0.magnitude() * n1.magnitude() * n2.magnitude();
452    if det.abs() <= 1e-12 * scale {
453        return None;
454    }
455    let (d0, d1, d2) = (
456        n0.dot(p0 - Point::ORIGIN),
457        n1.dot(p1 - Point::ORIGIN),
458        n2.dot(p2 - Point::ORIGIN),
459    );
460    let v = (n1.cross(n2) * d0 + n2.cross(n0) * d1 + n0.cross(n1) * d2) / det;
461    Some(Point::ORIGIN + v)
462}
463
464/// Where the lines `p + s u` and `q + t v` come closest: `s`, `t` and the
465/// gap there; `None` for parallel lines.
466fn crossing(p: Point, u: Vector, q: Point, v: Vector) -> Option<(f64, f64, f64)> {
467    let w = p - q;
468    let (a, b, c) = (u.dot(u), u.dot(v), v.dot(v));
469    let (d, e) = (u.dot(w), v.dot(w));
470    let den = a.mul_add(c, -b * b);
471    if den.abs() <= 1e-12 * a * c {
472        return None;
473    }
474    let s = b.mul_add(e, -c * d) / den;
475    let t = a.mul_add(e, -b * d) / den;
476    Some((s, t, (p + u * s).distance(q + v * t)))
477}
478
479/// The four faces of the tetrahedron on `corners`, each facing out.
480fn tetrahedron(model: &mut Model, corners: [Point; 4], tol: Tolerances) -> OgeomResult<Vec<Shape>> {
481    let centroid = Point::ORIGIN
482        + (corners
483            .iter()
484            .fold(Vector::ZERO, |sum, p| sum + (*p - Point::ORIGIN))
485            / 4.0);
486    let mut faces = Vec::with_capacity(4);
487    for skip in 0..4 {
488        let tri: Vec<Point> = (0..4).filter(|&n| n != skip).map(|n| corners[n]).collect();
489        let mut normal = (tri[1] - tri[0]).cross(tri[2] - tri[0]);
490        if normal.dot(tri[0] - centroid) < 0.0 {
491            normal = -normal;
492        }
493        let normal = normal / normal.magnitude();
494        faces.push(planar_face(model, &tri, normal, tol)?);
495    }
496    Ok(faces)
497}