Skip to main content

brep_kernel/offset/
thicken_faces.rs

1//! THICKEN a sheet of several faces into one solid.
2//!
3//! Each face is thickened by itself ([`crate::thicken_trimmed_sheet`]: the
4//! face, its offset and the walls ruled between their edges), and the slabs are
5//! united. Where two faces meet along a crease the slabs either overlap (the
6//! crease closes toward the side thickened, and the union trims each moved face
7//! where it meets the other) or leave a gap (the crease opens toward that
8//! side). A gap is filled as the caller asks:
9//!
10//! - [`OffsetCorners::Sharp`]: the moved faces are extended until they meet,
11//!   a prism on the kite between the crease, its two moved images and the line
12//!   the two moved planes share; where three such creases meet in a corner,
13//!   the block out to the point the three moved planes share.
14//! - [`OffsetCorners::Rounded`]: the gap is closed by the cylinder of the
15//!   thickness about the crease, between the two moved faces; where three such
16//!   creases meet in a corner, by the piece of the ball about the corner
17//!   between the three.
18//!
19//! A whole patch of a round cylinder thickened toward its axis by its radius
20//! or more is the wedge between it and its axis, with the like wedge opposite
21//! for what is left over.
22//!
23//! Faces that meet smoothly along a crease (a bend and the flat it leaves)
24//! need no fill. Where a crease closes toward the side thickened at a right
25//! angle or wider, the slabs end on each other or overlap; tighter than a
26//! right angle their square ends are cut off behind the other face. Fills are built where the crease is a straight line along
27//! which both faces keep their normals; any other open crease is refused by
28//! name, as is a corner where the open creases are not exactly three.
29use crate::topology::FaceRecord;
30use crate::{
31    boolean_operation, boolean_operation_nary, extrude_profile_brep, make_arc, make_line, make_sphere_brep_framed, project_point_to_surface,
32    solid_from_polygons, thicken_trimmed_sheet, BooleanOperation, BooleanOptions, BrepSolid, KernelRefusal, KernelStage, NurbsCurve, OffsetCorners,
33    OrRefuse, Vec3,
34};
35
36/// The prefix of every refusal of this operation.
37pub const THICKEN_FACES_REFUSAL: &str = "thickenFaces";
38
39fn input(what: &'static str, message: String) -> KernelRefusal {
40    KernelRefusal::input(KernelStage::Collect, what, format!("{THICKEN_FACES_REFUSAL}: {message}"))
41}
42
43fn refuse(what: &'static str, message: String) -> KernelRefusal {
44    KernelRefusal::unsupported(KernelStage::Classify, what, format!("{THICKEN_FACES_REFUSAL}: {message}"))
45}
46
47/// The sheet a chain of curves sweeps as it slides by `distance` along
48/// `direction`: one face per curve (the curve times the slide), each a sheet
49/// of its own, in the chain's order. Every face's normal is its curve's
50/// direction crossed with the slide's, so the faces of a chain run head to
51/// tail have their normals on one side of the sheet, as
52/// [`thicken_sheet_faces`] needs them; run the chain the other way for the
53/// other side.
54pub fn extrude_curves_to_sheet(curves: &[NurbsCurve], direction: Vec3, distance: f64) -> Result<Vec<BrepSolid>, KernelRefusal> {
55    if curves.is_empty() {
56        return Err(input("curves", "at least one curve is required".into()));
57    }
58    if !distance.is_finite() || distance.abs() <= 1e-12 {
59        return Err(input("distance", "distance must be a nonzero finite value".into()));
60    }
61    let slide = direction.normalized().map_err(|_| input("direction", "direction must be non-zero".into()))?.scale(distance);
62    curves
63        .iter()
64        .map(|curve| {
65            let net = curve
66                .control_points
67                .iter()
68                .map(|pole| vec![*pole, crate::Vec4 { x: pole.x + slide.x * pole.w, y: pole.y + slide.y * pole.w, z: pole.z + slide.z * pole.w, w: pole.w }])
69                .collect();
70            let surface = crate::NurbsSurface::new(curve.degree, 1, curve.knots.clone(), vec![0.0, 0.0, 1.0, 1.0], net).or_refuse(KernelStage::Collect, "thickenFaces.swept_face")?;
71            crate::face_from_surface(&surface)
72        })
73        .collect()
74}
75
76/// One use of an edge by a face: where it runs, and the face's normal there.
77struct Side {
78    face: usize,
79    start: Vec3,
80    middle: Vec3,
81    end: Vec3,
82    /// The face's normal at the start, the middle and the end.
83    normals: [Vec3; 3],
84}
85
86fn face_normal(face: &FaceRecord, point: Vec3) -> Result<Vec3, KernelRefusal> {
87    let foot = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Classify, "thickenFaces.project")?;
88    let normal = face.surface.normal(foot.u, foot.v).or_refuse(KernelStage::Classify, "thickenFaces.normal")?;
89    Ok(if face.same_sense { normal } else { normal.scale(-1.0) })
90}
91
92/// The sheet `faces` thickened by `thickness` along the faces' normals
93/// (negative: against them), its open creases closed as `corners` says.
94///
95/// `faces` are sheets of one face or more ([`crate::face_from_surface`],
96/// [`crate::face_from_surface_trimmed`]) that meet along common edges, their
97/// normals all on one side of the sheet. The result is one solid.
98///
99/// Refuses faces whose normals disagree across a crease, a crease that is not
100/// a straight line with constant normals where it opens toward the thickened
101/// side or closes tighter than a right angle, a corner of other than three
102/// open creases, and whatever the single-face thicken or the union refuses.
103pub fn thicken_sheet_faces(faces: &[BrepSolid], thickness: f64, corners: OffsetCorners) -> Result<BrepSolid, KernelRefusal> {
104    if !thickness.is_finite() || thickness.abs() <= 1e-12 {
105        return Err(input("thickness", "thickness must be a nonzero finite value".into()));
106    }
107    let (t, sign) = (thickness.abs(), thickness.signum());
108    let mut records: Vec<(&BrepSolid, &FaceRecord)> = Vec::new();
109    for body in faces {
110        for face in body.shells.iter().flat_map(|shell| &shell.faces) {
111            records.push((body, face));
112        }
113    }
114    if records.is_empty() {
115        return Err(input("faces", "at least one face is required".into()));
116    }
117    let size = records
118        .iter()
119        .flat_map(|(body, _)| &body.vertices)
120        .map(|vertex| vertex.point.sub(records[0].0.vertices[0].point).length())
121        .fold(t, f64::max);
122    let tolerance = 1e-7 * size;
123    let near = |a: Vec3, b: Vec3| a.sub(b).length() <= tolerance;
124
125    // every use of an edge by a face, as that face's loop runs along it
126    let mut sides = Vec::new();
127    for (index, (body, face)) in records.iter().enumerate() {
128        for coedge in face.loops.iter().flat_map(|record| &record.coedges) {
129            let edge = body.edges.iter().find(|edge| edge.id == coedge.edge_id).ok_or_else(|| input("faces", "a face refers to a missing edge".into()))?;
130            if edge.degenerate {
131                continue;
132            }
133            let at = |fraction: f64| edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction).or_refuse(KernelStage::Classify, "thickenFaces.edge");
134            let (mut start, middle, mut end) = (at(0.0)?, at(0.5)?, at(1.0)?);
135            if !coedge.forward {
136                std::mem::swap(&mut start, &mut end);
137            }
138            let normals = [face_normal(face, start)?, face_normal(face, middle)?, face_normal(face, end)?];
139            sides.push(Side { face: index, start, middle, end, normals });
140        }
141    }
142
143    // the slabs, face by face
144    let mut parts = Vec::new();
145    for (_, face) in &records {
146        let loops: Vec<Vec<NurbsCurve>> = face.loops.iter().map(|record| record.coedges.iter().map(|coedge| coedge.pcurve.clone()).collect()).collect();
147        let along = if face.same_sense { thickness } else { -thickness };
148        match past_its_axis(face, along, tolerance)? {
149            Some(wedges) => parts.extend(wedges),
150            None => parts.extend(thicken_trimmed_sheet(&face.surface, &loops, along, false)?),
151        }
152    }
153
154    // the creases: two faces' uses of one edge, run opposite ways
155    struct Open {
156        faces: [usize; 2],
157        ends: [Vec3; 2],
158        moved: [Vec3; 2],
159    }
160    let mut open = Vec::new();
161    let mut cuts: Vec<BrepSolid> = Vec::new();
162    for (i, a) in sides.iter().enumerate() {
163        for b in sides.iter().skip(i + 1) {
164            if a.face == b.face || !near(a.middle, b.middle) {
165                continue;
166            }
167            if !(near(a.start, b.end) && near(a.end, b.start)) {
168                if near(a.start, b.start) && near(a.end, b.end) {
169                    return Err(input("normals", format!("faces {} and {} have their normals on opposite sides of the sheet", a.face, b.face)));
170                }
171                continue;
172            }
173            let run = a.end.sub(a.start);
174            let tangent = run.normalized().map_err(|_| refuse("crease", "a crease has no length".into()))?;
175            // toward face a's inside, across the crease, and how far face b's moved image leans that way
176            let inward = a.normals[1].cross(tangent);
177            let lean = b.normals[1].scale(sign).dot(inward);
178            let moved = [a.normals[1].scale(sign), b.normals[1].scale(sign)];
179            let straight = a.middle.sub(a.start).cross(tangent).length() <= tolerance;
180            let steady = |side: &Side| side.normals.iter().all(|normal| normal.sub(side.normals[1]).length() <= 1e-9);
181            if lean.abs() <= 1e-9 {
182                // the faces meet smoothly: the slabs' ends are one wall
183                continue;
184            }
185            if lean > 0.0 {
186                // the crease closes toward the thickened side. At a right angle, or wider, the
187                // slabs end on each other or overlap, and the union is the answer. Tighter than a
188                // right angle their square ends stand out behind the other face, and are cut off.
189                let cosine = moved[0].dot(moved[1]);
190                if cosine >= -1e-9 {
191                    continue;
192                }
193                if !straight || !steady(a) || !steady(b) {
194                    return Err(refuse("crease_closing", format!("the crease between faces {} and {} closes toward the thickened side tighter than a right angle and is not a straight line with constant normals", a.face, b.face)));
195                }
196                let inward_b = b.normals[1].cross(tangent.scale(-1.0));
197                for (own, other, into) in [(moved[0], moved[1], inward), (moved[1], moved[0], inward_b)] {
198                    let top = a.start.add(own.scale(t));
199                    let reach = top.add(into.scale(t * -cosine / into.dot(other)));
200                    let line = |from: Vec3, to: Vec3| make_line(from, to).or_refuse(KernelStage::Collect, "thickenFaces.cut_line");
201                    cuts.push(extrude_profile_brep(&[line(a.start, top)?, line(top, reach)?, line(reach, a.start)?], run, run.length())?);
202                }
203                continue;
204            }
205            // b runs the other way: its start is a's end
206            if !straight || !steady(a) || !steady(b) {
207                return Err(refuse("crease_curved", format!("the crease between faces {} and {} opens toward the thickened side and is not a straight line with constant normals", a.face, b.face)));
208            }
209            parts.push(crease_fill(a.start, run, moved, t, corners)?);
210            open.push(Open { faces: [a.face, b.face], ends: [a.start, a.end], moved });
211        }
212    }
213
214    // the corners: points where open creases end together
215    let mut points: Vec<Vec3> = Vec::new();
216    for crease in &open {
217        for end in crease.ends {
218            if !points.iter().any(|point| near(*point, end)) {
219                points.push(end);
220            }
221        }
222    }
223    for point in points {
224        let here: Vec<&Open> = open.iter().filter(|crease| crease.ends.iter().any(|end| near(*end, point))).collect();
225        if here.len() == 1 {
226            continue;
227        }
228        // three creases between three faces, each pair once
229        let mut around: Vec<(usize, Vec3)> = Vec::new();
230        for crease in &here {
231            for (face, moved) in crease.faces.iter().zip(crease.moved) {
232                if !around.iter().any(|(known, _)| known == face) {
233                    around.push((*face, moved));
234                }
235            }
236        }
237        if here.len() != 3 || around.len() != 3 {
238            return Err(refuse("corner", format!("{} open creases between {} faces meet in one corner; exactly three between three faces are built", here.len(), around.len())));
239        }
240        parts.push(corner_fill(point, [around[0].1, around[1].1, around[2].1], t, corners)?);
241    }
242
243    let united = if parts.len() == 1 { parts.remove(0) } else { boolean_operation_nary(&parts, BooleanOperation::Union)? };
244    if cuts.is_empty() {
245        return Ok(united);
246    }
247    let mut operands = vec![united];
248    operands.extend(cuts);
249    boolean_operation_nary(&operands, BooleanOperation::Subtract)
250}
251
252/// A whole patch of a round cylinder (an arc along u, slid straight along v)
253/// thickened toward its axis by its radius or more: every point `p + s·n`
254/// then runs from the patch to the axis and, past it, out the other side. So
255/// the body is the wedge between the patch and its axis and, opposite it, the
256/// like wedge of what the thickness has left over the radius. `None` for any
257/// other face or a thickness short of the axis, which the single-face thicken
258/// takes.
259fn past_its_axis(face: &FaceRecord, along: f64, tolerance: f64) -> Result<Option<Vec<BrepSolid>>, KernelRefusal> {
260    let surface = &face.surface;
261    let stage = KernelStage::Classify;
262    if surface.degree_v != 1 || surface.control_points.first().map(Vec::len) != Some(2) || face.loops.len() != 1 || face.loops[0].coedges.len() != 4 {
263        return Ok(None);
264    }
265    let [u0, u1] = surface.domain_u().or_refuse(stage, "thickenFaces.domain")?;
266    let [v0, v1] = surface.domain_v().or_refuse(stage, "thickenFaces.domain")?;
267    // the face is its surface's whole domain when its loop's corners are the domain's
268    let corners = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
269    for coedge in &face.loops[0].coedges {
270        let [t0, t1] = coedge.pcurve.domain().or_refuse(stage, "thickenFaces.pcurve")?;
271        for t in [t0, t1] {
272            let at = coedge.pcurve.evaluate(t).or_refuse(stage, "thickenFaces.pcurve")?;
273            if !corners.iter().any(|corner| (at.x - corner[0]).abs() + (at.y - corner[1]).abs() <= 1e-9 * ((u1 - u0).abs() + (v1 - v0).abs())) {
274                return Ok(None);
275            }
276        }
277    }
278    let point = |u: f64, v: f64| surface.evaluate(u, v).or_refuse(stage, "thickenFaces.point");
279    let normal = |u: f64, v: f64| surface.normal(u, v).or_refuse(stage, "thickenFaces.normal");
280    let slide = point(u0, v1)?.sub(point(u0, v0)?);
281    // the centre the normals at the two ends of the arc pass through, toward the side thickened
282    let toward = along.signum();
283    let (p0, n0, p1, n1) = (point(u0, v0)?, normal(u0, v0)?.scale(toward), point(u1, v0)?, normal(u1, v0)?.scale(toward));
284    let chord = p1.sub(p0);
285    let spread = n0.sub(n1);
286    if spread.length() <= 1e-9 {
287        return Ok(None);
288    }
289    // p0 + R·n0 = p1 + R·n1
290    let radius = chord.dot(spread) / spread.dot(spread);
291    if !(radius > 0.0) || along.abs() < radius - tolerance {
292        return Ok(None);
293    }
294    let centre = p0.add(n0.scale(radius));
295    const SAMPLES: usize = 16;
296    for i in 0..=SAMPLES {
297        let u = u0 + (u1 - u0) * i as f64 / SAMPLES as f64;
298        for (v, shift) in [(v0, Vec3::new(0.0, 0.0, 0.0)), (v1, slide)] {
299            let focus = point(u, v)?.add(normal(u, v)?.scale(toward * radius));
300            if focus.sub(centre.add(shift)).length() > tolerance {
301                return Ok(None);
302            }
303        }
304    }
305    if slide.cross(n0).length() < (1.0 - 1e-9) * slide.length() {
306        return Ok(None);
307    }
308    // the wedge on the arc scaled about the centre by `factor`, slid along the axis
309    let arc = surface.iso_curve_v(v0).or_refuse(stage, "thickenFaces.arc")?;
310    let wedge = |factor: f64| -> Result<BrepSolid, KernelRefusal> {
311        let scaled = |q: Vec3| centre.add(q.sub(centre).scale(factor));
312        let poles = arc
313            .control_points
314            .iter()
315            .map(|pole| crate::Vec4::from_point(scaled(Vec3::new(pole.x / pole.w, pole.y / pole.w, pole.z / pole.w)), pole.w))
316            .collect();
317        let rim = NurbsCurve::new(arc.degree, arc.knots.clone(), poles).or_refuse(stage, "thickenFaces.wedge_arc")?;
318        let section = vec![
319            make_line(centre, scaled(p0)).or_refuse(stage, "thickenFaces.wedge_side")?,
320            rim,
321            make_line(scaled(p1), centre).or_refuse(stage, "thickenFaces.wedge_side")?,
322        ];
323        extrude_profile_brep(&section, slide, slide.length())
324    };
325    let mut wedges = vec![wedge(1.0)?];
326    let over = along.abs() - radius;
327    if over > tolerance {
328        wedges.push(wedge(-over / radius)?);
329    }
330    Ok(Some(wedges))
331}
332
333/// The fill of an open straight crease from `start` along `run`, between the
334/// two faces' moved images `moved` (unit vectors square to the crease).
335fn crease_fill(start: Vec3, run: Vec3, moved: [Vec3; 2], t: f64, corners: OffsetCorners) -> Result<BrepSolid, KernelRefusal> {
336    let collect = KernelStage::Collect;
337    let (a, b) = (start.add(moved[0].scale(t)), start.add(moved[1].scale(t)));
338    let line = |from: Vec3, to: Vec3| make_line(from, to).or_refuse(collect, "thickenFaces.fill_line");
339    let section = match corners {
340        OffsetCorners::Sharp => {
341            // the point at the thickness from both faces, in the section's plane
342            let apex = start.add(moved[0].add(moved[1]).scale(t / (1.0 + moved[0].dot(moved[1]))));
343            vec![line(start, a)?, line(a, apex)?, line(apex, b)?, line(b, start)?]
344        }
345        OffsetCorners::Rounded => {
346            let across = moved[1].sub(moved[0].scale(moved[1].dot(moved[0]))).normalized().or_refuse(collect, "thickenFaces.fill_frame")?;
347            let angle = moved[0].dot(moved[1]).clamp(-1.0, 1.0).acos();
348            vec![line(start, a)?, make_arc(start, moved[0], across, t, 0.0, angle).or_refuse(collect, "thickenFaces.fill_arc")?, line(b, start)?]
349        }
350    };
351    extrude_profile_brep(&section, run, run.length())
352}
353
354/// The fill of a corner where three open creases meet, between the three
355/// faces' moved images `moved` (unit vectors).
356fn corner_fill(corner: Vec3, moved: [Vec3; 3], t: f64, corners: OffsetCorners) -> Result<BrepSolid, KernelRefusal> {
357    let at = |direction: Vec3, length: f64| corner.add(direction.scale(length));
358    match corners {
359        OffsetCorners::Sharp => {
360            // the point at the thickness from all three faces: moved[i] · x = 1
361            let [a, b, c] = moved;
362            let volume = a.dot(b.cross(c));
363            if volume.abs() <= 1e-9 {
364                return Err(refuse("corner", "the three faces at a corner do not span space".into()));
365            }
366            let far = b.cross(c).add(c.cross(a)).add(a.cross(b)).scale(1.0 / volume);
367            let pair = |i: usize, j: usize| at(moved[i].add(moved[j]), t / (1.0 + moved[i].dot(moved[j])));
368            let tip = |i: usize| at(moved[i], t);
369            let far = at(far, t);
370            let mut faces = Vec::new();
371            for (i, j) in [(0, 1), (1, 2), (2, 0)] {
372                faces.push(vec![corner, tip(i), pair(i, j), tip(j)]);
373            }
374            for (i, j, k) in [(0, 1, 2), (1, 2, 0), (2, 0, 1)] {
375                faces.push(vec![tip(i), pair(i, j), far, pair(i, k)]);
376            }
377            solid_from_polygons(&faces)
378        }
379        OffsetCorners::Rounded => {
380            // the ball about the corner, kept between the three directions: its poles and its
381            // seam turned away from the piece kept
382            let middle = moved[0].add(moved[1]).add(moved[2]).normalized().or_refuse(KernelStage::Collect, "thickenFaces.corner_frame")?;
383            let pole = middle.cross(moved[0]).normalized().or_refuse(KernelStage::Collect, "thickenFaces.corner_frame")?;
384            let ball = make_sphere_brep_framed(corner, t, pole, Some(middle.scale(-1.0))).or_refuse(KernelStage::Collect, "thickenFaces.corner_ball")?;
385            let reach = 4.0 * t;
386            let tips = [at(moved[0], reach), at(moved[1], reach), at(moved[2], reach)];
387            let base = tips[1].sub(tips[0]).cross(tips[2].sub(tips[0]));
388            if (tips[0].sub(corner).dot(base) / base.length()).abs() <= t {
389                return Err(refuse("corner", "the three faces at a corner are too nearly flat for a ball between them".into()));
390            }
391            let wedge = solid_from_polygons(&[
392                vec![corner, tips[0], tips[1]],
393                vec![corner, tips[1], tips[2]],
394                vec![corner, tips[2], tips[0]],
395                vec![tips[0], tips[1], tips[2]],
396            ])?;
397            boolean_operation(&ball, &wedge, BooleanOperation::Intersect, &BooleanOptions::default())
398        }
399    }
400}
401