brep_kernel/construction/
extrude_up_to.rs1use 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
13pub 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
24pub 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 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 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 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
75pub 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 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 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