1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ChamferCorner {
15 Point,
18 Facet,
23}
24
25pub 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 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
103enum Cut {
105 Tetrahedron([Point; 4]),
108 Polytope(Vec<(Vec<Point>, Vector)>),
111}
112
113fn 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 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 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 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 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 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 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
244type Bevel = (crate::support::Seat, [f64; 2], Point, Vector);
247
248fn 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 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 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 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 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 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 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 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
447fn 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
464fn 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
479fn 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}