//! Raising a band of a planar face (a plastic part's lip): the band along one of the face's //! boundary loops, a set width into the face, is pushed out along the face's normal. Built //! directly from the body's faces (the band's walls continue the walls below, which a boolean //! union can't join: their faces would coincide). use solidcraft_geom::Vec3; use truck_modeling::{self as mt, builder}; use crate::body::{Body, Solid, from_p3, p3, v3}; use crate::history::{Change, FaceOrigin, FaceProv}; use crate::record::Operand; use crate::{KernelError, Result, guard}; fn fail(m: &str) -> KernelError { KernelError::Failed(format!("raising a band: {m}")) } /// A boundary edge of the face, as its ends and (for an arc) a point halfway. #[derive(Clone, Copy)] struct Piece { a: Vec3, b: Vec3, mid: Option, } /// The planar face of `body` at `at`: its index and outward normal. fn face_at(body: &Body, at: Vec3) -> Result<(usize, Vec3)> { let size = body.size(); let mesh = body.tessellate((size * 1e-3).max(1e-3))?; let near = mesh .triangles .iter() .zip(&mesh.tri_face) .filter_map(|(t, f)| { let [a, b, c] = mesh.tri(t)?; let n = (b - a).cross(c - a).normalized()?; let q = at - n * (at - a).dot(n); let inside = [(a, b), (b, c), (c, a)].iter().all(|(u, v)| (*v - *u).cross(q - *u).dot(n) >= -1e-9); inside.then_some(((at - a).dot(n).abs(), *f as usize)) }) .min_by(|x, y| x.0.total_cmp(&y.0)); let (d, f) = near.ok_or_else(|| fail("no face there"))?; if d > size * 1e-3 { return Err(fail("no face there")); } let face = body.solid.face_iter().nth(f).ok_or_else(|| fail("face"))?; let mt::Surface::Plane(pl) = face.oriented_surface() else { return Err(fail("the face must be planar")) }; let n = pl.normal(); Ok((f, Vec3::new(n.x, n.y, n.z).normalized().ok_or_else(|| fail("normal"))?)) } /// The face's loop moved `w` into the face (to the left of its edges, as a face's boundary has /// its inside on the left), corners mitred where the offsets don't already meet. fn offset_loop(pieces: &[Piece], n: Vec3, w: f64, tol: f64) -> Result> { let left = |a: Vec3, b: Vec3| n.cross(b - a).normalized(); let mut out: Vec = Vec::new(); for p in pieces { out.push(match p.mid { None => { let l = left(p.a, p.b).ok_or_else(|| fail("zero edge"))?; Piece { a: p.a + l * w, b: p.b + l * w, mid: None } } Some(m) => { // Each point moves square to the arc (along its radius), to the left. let (la, lm, lb) = (left(p.a, m).ok_or_else(|| fail("arc"))?, left(p.a, p.b).ok_or_else(|| fail("arc"))?, left(m, p.b).ok_or_else(|| fail("arc"))?); let c = circle_centre(p.a, m, p.b).ok_or_else(|| fail("arc"))?; let radial = |q: Vec3, hint: Vec3| { let r = (q - c).normalized().unwrap_or(hint); if r.dot(hint) >= 0.0 { r } else { -r } }; let _ = (la, lb); Piece { a: p.a + radial(p.a, lm) * w, b: p.b + radial(p.b, lm) * w, mid: Some(m + radial(m, lm) * w) } } }); } // Joints: where the offsets don't meet (sharp corners of lines), the lines are extended or // cut back to their crossing. let k = out.len(); for i in 0..k { let j = (i + 1) % k; let (Some(pi), Some(pj)) = (out.get(i).copied(), out.get(j).copied()) else { continue }; if pi.b.dist(pj.a) < tol { if let Some(x) = out.get_mut(j) { x.a = pi.b; } continue; } match (pi.mid, pj.mid) { (None, None) => { let (d1, d2) = (pi.b - pi.a, pj.b - pj.a); let w0 = pj.a - pi.a; let cr = d1.cross(d2); let den = cr.len2(); if den < 1e-24 { return Err(fail("parallel edges at a corner")); } let t = w0.cross(d2).dot(cr) / den; let x = pi.a + d1 * t; if let Some(e) = out.get_mut(i) { e.b = x; } if let Some(e) = out.get_mut(j) { e.a = x; } } _ => return Err(fail("an arc meeting another edge at a corner")), } } Ok(out) } fn circle_centre(a: Vec3, m: Vec3, b: Vec3) -> Option { let (u, v) = (b - a, m - a); let w = u.cross(v); let d = 2.0 * w.len2(); if d < 1e-300 { return None; } Some(a + (v.cross(w) * u.len2() + w.cross(u) * v.len2()) * (1.0 / d)) } /// Raise the band of the planar face at `at` that runs `width` wide along its outer loop /// (`outside`) or its inner loop, by `height` along the face's normal. pub fn raise_band(body: &Body, at: Vec3, width: f64, height: f64, outside: bool) -> Result { body.require_brep("a lip")?; if !(width > 1e-6 && height > 1e-6 && width.is_finite() && height.is_finite()) { return Err(KernelError::Invalid("the band's width and height must be positive".into())); } if body.solid.boundaries().len() != 1 { return Err(fail("a body in one piece")); } let size = body.size(); let tol = (size * 1e-7).max(1e-9) * 100.0; let (fi, n) = face_at(body, at)?; let faces: Vec = body.solid.face_iter().cloned().collect(); let f = faces.get(fi).ok_or_else(|| fail("face"))?; let wires = f.boundaries(); if wires.len() != 2 { return Err(fail("the face must have an outer and one inner loop (a rim)")); } // Outer loop: turning positively about n. let turn = |w: &mt::Wire| -> f64 { let pts: Vec = w.vertex_iter().map(|v| from_p3(v.point())).collect(); let mut s = Vec3::ZERO; for (k, p) in pts.iter().enumerate() { if let Some(q) = pts.get((k + 1) % pts.len()) { s += p.cross(*q); } } s.dot(n) }; let (w0, w1) = (wires.first().ok_or_else(|| fail("loop"))?, wires.get(1).ok_or_else(|| fail("loop"))?); let (outer, inner) = if turn(w0) >= turn(w1) { (w0, w1) } else { (w1, w0) }; let (along, other) = if outside { (outer, inner) } else { (inner, outer) }; let pieces: Vec = along .edge_iter() .map(|e| { let (a, b) = (from_p3(e.front().point()), from_p3(e.back().point())); match crate::offset::arc_mid(e, size * 1e-6) { Some(None) => Ok(Piece { a, b, mid: None }), Some(Some(m)) => Ok(Piece { a, b, mid: Some(m) }), None => Err(fail("edges must be lines or arcs")), } }) .collect::>()?; let moved = offset_loop(&pieces, n, width, tol)?; guard("lip", || { // The band's other edge, on the face. let verts: Vec = moved.iter().map(|p| builder::vertex(p3(p.a))).collect(); let k = verts.len(); let mut cedges: Vec = Vec::with_capacity(k); for (i, p) in moved.iter().enumerate() { let (Some(a), Some(b)) = (verts.get(i), verts.get((i + 1) % k)) else { return Err(fail("vertex")) }; cedges.push(match p.mid { None => builder::line(a, b), Some(m) => builder::circle_arc(a, b, p3(m)), }); } // The loop's edges and the band's edge beside each, by the loop edge's index in the body. let operand = Operand::new(body); let mut wall_edge: std::collections::HashMap = std::collections::HashMap::new(); for (e, ce) in along.edge_iter().zip(&cedges) { if let Some(i) = operand.edges.get(&e.id()) { wall_edge.insert(e.id(), *i); wall_edge.insert(ce.id(), *i); } } let c: mt::Wire = cedges.into(); // The rest of the face: its other loop, and the band's edge as a loop of its own. // (The band's edge runs the face's way round: a hole when the band follows the inner // loop, the outer loop when it follows the outer.) let rest = mt::Face::try_new(vec![other.clone(), c.clone()], f.oriented_surface()).map_err(|e| fail(&e.to_string()))?; // Walls: the face's loop and the band's edge swept out; the band's top. let up = v3(n * height); let wall_along: mt::Shell = builder::tsweep(along, up); let wall_c: mt::Shell = builder::tsweep(&c, up); let top_of = |sh: &mt::Shell| -> Vec { sh.extract_boundaries().into_iter().filter(|w| w.vertex_iter().all(|v| (from_p3(v.point()) - at).dot(n) > height * 0.5)).collect() }; let flipped = |sh: &mt::Shell, yes: bool| -> mt::Shell { if yes { sh.face_iter().map(|x| x.inverse()).collect::>().into() } else { sh.clone() } }; // History (one operand, the body), face by face as gathered below: the other faces // kept, the face trimmed to what the band leaves, each wall made along the loop // edge it rises from (the band's inner wall along the edge it runs beside), and the // band's top an offset of the face. (Flipping a face keeps its edges.) let history: Option> = crate::record::kept_except(faces.len(), fi) .map(Some) .chain([Some(crate::record::from_face(0, fi, Change::Modified))]) .chain(wall_along.face_iter().chain(wall_c.face_iter()).map(|w| { let edge = w.absolute_boundaries().iter().flatten().find_map(|e| wall_edge.get(&e.id()))?; Some(crate::record::generated(vec![FaceOrigin::Edge { operand: 0, edge: *edge }])) })) .chain([Some(crate::record::generated(vec![FaceOrigin::Offset { operand: 0, face: fi }]))]) .collect(); let mut last = String::new(); for flip in 0..8u32 { let (wa, wc) = (flipped(&wall_along, flip & 1 == 1), flipped(&wall_c, flip & 2 == 2)); let mut tops = top_of(&wa); tops.extend(top_of(&wc)); if tops.len() != 2 { return Err(fail("the band's top")); } // The top closes both walls: its loops run against theirs. let tops: Vec = tops.iter().map(|w| w.inverse()).collect(); let Ok(top) = builder::try_attach_plane(&tops) else { continue }; let top = if flip & 4 == 4 { top.inverse() } else { top }; let mut all: Vec = faces.iter().enumerate().filter(|(i, _)| *i != fi).map(|(_, x)| x.clone()).collect(); all.push(rest.clone()); all.extend(wa.face_iter().cloned()); all.extend(wc.face_iter().cloned()); all.push(top); match Solid::try_new(vec![all.into()]) { Ok(s) => { let b = Body::new(s)?; if b.validity().is_empty() { return crate::record::attach(b, std::slice::from_ref(&operand), history.clone(), Default::default()); } last = b.validity().join("; "); } Err(e) => last = e.to_string(), } } Err(fail(&last)) }) }