Skip to main content

brep_kernel/construction/
extrude_up_to.rs

1//! An extrude that ends on a face of another solid, and a solid's separate
2//! pieces.
3//!
4//! [`extrude_profile_up_to_face`] slides a profile along a direction until it
5//! meets a chosen face of a target solid: the prism run on past the target,
6//! less the target, and of what is left the piece that stands on the profile.
7//! Its far end is then the target's own surface, trimmed by the prism's walls,
8//! so the result fits the target exactly. An end that would land on any other
9//! face of the target is refused by name.
10use crate::topology::ShellRecord;
11use crate::{boolean_operation, extrude_profile_brep, project_point_to_surface, solid_signed_volume, BooleanOperation, BooleanOptions, BrepSolid, KernelRefusal, KernelStage, NurbsCurve, OrRefuse, Vec3};
12
13/// The prefix of every refusal of this operation.
14pub const EXTRUDE_UP_TO_REFUSAL: &str = "extrudeUpToFace";
15
16fn input(what: &'static str, message: String) -> KernelRefusal {
17    KernelRefusal::input(KernelStage::Collect, what, format!("{EXTRUDE_UP_TO_REFUSAL}: {message}"))
18}
19
20fn refuse(what: &'static str, message: String) -> KernelRefusal {
21    KernelRefusal::unsupported(KernelStage::Select, what, format!("{EXTRUDE_UP_TO_REFUSAL}: {message}"))
22}
23
24/// The separate pieces of `solid`: one solid per shell that bounds material,
25/// each with the shells of the hollows inside it. A solid of one piece comes
26/// back as itself.
27pub fn solid_pieces(solid: &BrepSolid) -> Result<Vec<BrepSolid>, KernelRefusal> {
28    if solid.shells.len() <= 1 {
29        return Ok(vec![solid.clone()]);
30    }
31    let alone = |shells: Vec<ShellRecord>| -> BrepSolid {
32        let mut edge_ids = std::collections::HashSet::new();
33        for coedge in shells.iter().flat_map(|shell| &shell.faces).flat_map(|face| &face.loops).flat_map(|record| &record.coedges) {
34            edge_ids.insert(coedge.edge_id);
35        }
36        let edges: Vec<_> = solid.edges.iter().filter(|edge| edge_ids.contains(&edge.id)).cloned().collect();
37        let vertex_ids: std::collections::HashSet<u64> = edges.iter().flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id]).collect();
38        let vertices: Vec<_> = solid.vertices.iter().filter(|vertex| vertex_ids.contains(&vertex.id)).cloned().collect();
39        // V − E + F − H = 2·shells − 2g, poles' degenerate edges not counted
40        let live = edges.iter().filter(|edge| !edge.degenerate).count() as i64;
41        let faces = shells.iter().map(|shell| shell.faces.len()).sum::<usize>() as i64;
42        let rings = shells.iter().flat_map(|shell| &shell.faces).map(|face| face.loops.len().saturating_sub(1)).sum::<usize>() as i64;
43        let twice = 2 * shells.len() as i64 - (vertices.len() as i64 - live + faces - rings);
44        BrepSolid { mass_properties_cache: Default::default(), id: solid.id, vertices, edges, shells, genus: (twice / 2).max(0), kind: solid.kind, cells: Default::default() }
45    };
46    // a shell that bounds material has a positive volume; a hollow's is negative
47    let mut outers: Vec<(Vec<ShellRecord>, BrepSolid)> = Vec::new();
48    let mut hollows = Vec::new();
49    for shell in &solid.shells {
50        let single = alone(vec![shell.clone()]);
51        let volume = solid_signed_volume(&single).or_refuse(KernelStage::Select, "solidPieces.volume")?;
52        if volume > 0.0 {
53            outers.push((vec![shell.clone()], single));
54        } else {
55            hollows.push((shell.clone(), single));
56        }
57    }
58    for (shell, single) in hollows {
59        // a hollow belongs to the piece that holds one of its vertices
60        let probe = single.vertices.first().map(|vertex| vertex.point).ok_or_else(|| refuse("pieces_hollow", "a hollow shell has no vertex".into()))?;
61        let mut home = None;
62        for (index, (_, outer)) in outers.iter().enumerate() {
63            let class = crate::classify_point(probe, outer, 1e-9).or_refuse(KernelStage::Select, "solidPieces.classify")?;
64            if matches!(class.class, crate::PointClass::In) {
65                home = Some(index);
66                break;
67            }
68        }
69        let index = home.ok_or_else(|| refuse("pieces_hollow", "a hollow shell lies in no piece".into()))?;
70        outers[index].0.push(shell);
71    }
72    Ok(outers.into_iter().map(|(shells, _)| alone(shells)).collect())
73}
74
75/// The prism on `profile` (a closed loop of curves in a plane, or one closed
76/// curve) slid along `direction` until it meets the face `face_id` of
77/// `target`, on which it ends.
78///
79/// The profile must lie clear of the target's material where it starts (on
80/// its boundary is clear), and the prism must reach the face all round: where
81/// part of it would run past the face's edge onto another face of the target,
82/// or miss the target, the operation refuses.
83pub fn extrude_profile_up_to_face(profile: &[NurbsCurve], direction: Vec3, target: &BrepSolid, face_id: u64) -> Result<BrepSolid, KernelRefusal> {
84    let along = direction.normalized().map_err(|_| input("direction", "direction must be non-zero".into()))?;
85    let face = target
86        .shells
87        .iter()
88        .flat_map(|shell| &shell.faces)
89        .find(|face| face.id == face_id)
90        .ok_or_else(|| input("face", format!("the target has no face {face_id}")))?;
91    let start = profile
92        .first()
93        .and_then(|curve| curve.control_points.first())
94        .map(|pole| Vec3::new(pole.x / pole.w, pole.y / pole.w, pole.z / pole.w))
95        .ok_or_else(|| input("profile", "the profile is empty".into()))?;
96    // far enough to pass every point of the target
97    let poles = target.shells.iter().flat_map(|shell| &shell.faces).flat_map(|face| face.surface.control_points.iter().flatten());
98    let (mut reach, mut size) = (0.0f64, 0.0f64);
99    for pole in poles {
100        let arm = Vec3::new(pole.x / pole.w, pole.y / pole.w, pole.z / pole.w).sub(start);
101        reach = reach.max(arm.dot(along));
102        size = size.max(arm.length());
103    }
104    if reach <= 0.0 {
105        return Err(refuse("misses", "the target lies behind the profile".into()));
106    }
107    let prism = extrude_profile_brep(profile, along, reach + 0.25 * size)?;
108    let left = boolean_operation(&prism, target, BooleanOperation::Subtract, &BooleanOptions::default())?;
109    let tolerance = 1e-7 * size.max(1.0);
110    let at_start = |piece: &BrepSolid| piece.vertices.iter().any(|vertex| vertex.point.sub(start).dot(along).abs() <= tolerance);
111    let mut standing: Vec<BrepSolid> = solid_pieces(&left)?.into_iter().filter(at_start).collect();
112    let piece = match standing.len() {
113        1 => standing.remove(0),
114        0 => return Err(refuse("buried", "the profile starts inside the target".into())),
115        _ => return Err(refuse("split", "the target cuts the prism into several pieces at the profile".into())),
116    };
117    // every face of the piece is the profile's own, a wall along the direction, or on the chosen
118    // face's surface: a face on any other surface is another face of the target
119    for end in piece.shells.iter().flat_map(|shell| &shell.faces) {
120        let [u0, u1] = end.surface.domain_u().or_refuse(KernelStage::Select, "extrudeUpToFace.domain")?;
121        let [v0, v1] = end.surface.domain_v().or_refuse(KernelStage::Select, "extrudeUpToFace.domain")?;
122        let mut kinds = [true, true, true];
123        for (a, b) in [(0.5, 0.5), (0.25, 0.3), (0.7, 0.8)] {
124            let (u, v) = (u0 + (u1 - u0) * a, v0 + (v1 - v0) * b);
125            let point = end.surface.evaluate(u, v).or_refuse(KernelStage::Select, "extrudeUpToFace.point")?;
126            let normal = end.surface.normal(u, v).or_refuse(KernelStage::Select, "extrudeUpToFace.normal")?;
127            let foot = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Select, "extrudeUpToFace.project")?;
128            kinds[0] &= point.sub(start).dot(along).abs() <= tolerance && normal.cross(along).length() <= 1e-9;
129            kinds[1] &= normal.dot(along).abs() <= 1e-9;
130            kinds[2] &= foot.distance <= tolerance;
131        }
132        if !kinds.iter().any(|kind| *kind) {
133            return Err(refuse("other_face", format!("the prism meets another face of the target before face {face_id}")));
134        }
135    }
136    let far_end = reach + 0.25 * size;
137    for vertex in &piece.vertices {
138        let height = vertex.point.sub(start).dot(along);
139        if height.abs() <= tolerance {
140            continue;
141        }
142        if (height - far_end).abs() <= tolerance {
143            return Err(refuse("misses", "part of the prism passes the target without meeting the face".into()));
144        }
145        let foot = project_point_to_surface(&face.surface, vertex.point).or_refuse(KernelStage::Select, "extrudeUpToFace.project")?;
146        if foot.distance > tolerance {
147            return Err(refuse("other_face", format!("the prism meets another face of the target before face {face_id}")));
148        }
149    }
150    Ok(piece)
151}
152