brep_kernel/csg/
edge_split.rs1use crate::imprint::ImprintResultRecord;
2use crate::topology::{BrepSolid, CoedgeRecord, EdgeRecord, VertexRecord};
3use crate::{NurbsCurve, Vec3};
4use rustc_hash::FxHashMap as HashMap;
5
6fn subcurve_by_fraction(
7 curve: &NurbsCurve,
8 start_fraction: f64,
9 end_fraction: f64,
10) -> Result<NurbsCurve, String> {
11 let [start, end] = curve.domain()?;
12 let first = start + (end - start) * start_fraction;
13 let second = start + (end - start) * end_fraction;
14 let epsilon = (1e-9 * (end - start)).max(2e-9);
15 let mut result = curve.clone();
16 if first > start + epsilon && first < end - epsilon {
17 result = result.split(first)?.1;
18 }
19 let domain = result.domain()?;
20 if second < domain[1] - epsilon && second > domain[0] + epsilon {
21 result = result.split(second)?.0;
22 } else if std::env::var("BREP_DEBUG_SUBRANGE").is_ok() && second < domain[1] - epsilon {
23 eprintln!("abnormal subrange skip in edge_split subcurve");
24 }
25 Ok(result)
26}
27
28fn claim_vertex(
29 vertices: &mut Vec<VertexRecord>,
30 imprint: &ImprintResultRecord,
31 point: Vec3,
32 next_id: &mut u64,
33) -> u64 {
34 let tolerance = 1e-5 * (1.0 + point.length());
35 if let Some(vertex) = vertices
36 .iter()
37 .find(|vertex| vertex.point.sub(point).length() <= tolerance)
38 {
39 return vertex.id;
40 }
41 let canonical = imprint
42 .vertices
43 .iter()
44 .filter_map(|vertex| {
45 let distance = vertex.point.sub(point).length();
46 (distance <= tolerance).then_some((vertex.point, distance))
47 })
48 .min_by(|a, b| a.1.total_cmp(&b.1))
49 .map(|candidate| candidate.0)
50 .unwrap_or(point);
51 let id = *next_id;
52 *next_id += 1;
53 vertices.push(VertexRecord {
54 id,
55 point: canonical,
56 });
57 id
58}
59
60pub fn apply_edge_splits(
65 solid: &BrepSolid,
66 operand: u8,
67 imprint: &ImprintResultRecord,
68) -> Result<BrepSolid, String> {
69 apply_edge_splits_with_map(solid, operand, imprint).map(|(result, _)| result)
70}
71
72pub fn apply_edge_splits_with_map(
77 solid: &BrepSolid,
78 operand: u8,
79 imprint: &ImprintResultRecord,
80) -> Result<(BrepSolid, HashMap<u64, Vec<u64>>), String> {
81 let requested = imprint
82 .edge_splits
83 .iter()
84 .filter(|split| split.operand == operand)
85 .map(|split| (split.edge_id, split.parameters.as_slice()))
86 .collect::<HashMap<_, _>>();
87 if requested.is_empty() {
88 return Ok((solid.clone(), HashMap::default()));
89 }
90
91 let mut result = solid.clone();
92 let mut next_vertex_id = result
93 .vertices
94 .iter()
95 .map(|vertex| vertex.id)
96 .max()
97 .unwrap_or(0)
98 + 1;
99 let mut next_edge_id = result.edges.iter().map(|edge| edge.id).max().unwrap_or(0) + 1;
100 let mut next_coedge_id = result
101 .shells
102 .iter()
103 .flat_map(|shell| &shell.faces)
104 .flat_map(|face| &face.loops)
105 .flat_map(|loop_record| &loop_record.coedges)
106 .map(|coedge| coedge.id)
107 .max()
108 .unwrap_or(0)
109 + 1;
110 let mut pieces: HashMap<u64, Vec<EdgeRecord>> = HashMap::default();
111
112 for edge in &solid.edges {
113 let Some(parameters) = requested.get(&edge.id) else {
114 continue;
115 };
116 let parameter_tolerance = (edge.t1 - edge.t0).abs() * 1e-10;
117 let mut partition = parameters
118 .iter()
119 .copied()
120 .filter(|parameter| {
121 *parameter > edge.t0 + parameter_tolerance
122 && *parameter < edge.t1 - parameter_tolerance
123 })
124 .collect::<Vec<_>>();
125 partition.sort_by(f64::total_cmp);
126 partition.dedup_by(|a, b| (*a - *b).abs() <= parameter_tolerance);
127 partition.insert(0, edge.t0);
128 partition.push(edge.t1);
129 if partition.len() <= 2 {
130 continue;
131 }
132 let mut vertex_ids = vec![edge.start_vertex_id];
133 for parameter in &partition[1..partition.len() - 1] {
134 let point = edge.curve.evaluate(*parameter)?;
135 vertex_ids.push(claim_vertex(
136 &mut result.vertices,
137 imprint,
138 point,
139 &mut next_vertex_id,
140 ));
141 }
142 vertex_ids.push(edge.end_vertex_id);
143 let mut edge_pieces = Vec::new();
144 for index in 0..partition.len() - 1 {
145 let name = edge.name.as_ref().map(|name| {
148 if index == 0 {
149 name.clone()
150 } else {
151 format!("{name}_{index}")
152 }
153 });
154 edge_pieces.push(EdgeRecord {
155 id: next_edge_id,
156 curve: edge.curve.clone(),
157 t0: partition[index],
158 t1: partition[index + 1],
159 start_vertex_id: vertex_ids[index],
160 end_vertex_id: vertex_ids[index + 1],
161 degenerate: false,
162 name,
163 });
164 next_edge_id += 1;
165 }
166 pieces.insert(edge.id, edge_pieces);
167 }
168
169 result.edges.retain(|edge| !pieces.contains_key(&edge.id));
170 for edge_pieces in pieces.values() {
171 result.edges.extend(edge_pieces.iter().cloned());
172 }
173
174 let source_edges: HashMap<u64, &EdgeRecord> =
178 solid.edges.iter().map(|edge| (edge.id, edge)).collect();
179
180 for face in result
181 .shells
182 .iter_mut()
183 .flat_map(|shell| shell.faces.iter_mut())
184 {
185 for loop_record in &mut face.loops {
186 let mut rebuilt = Vec::new();
187 for coedge in &loop_record.coedges {
188 let Some(edge_pieces) = pieces.get(&coedge.edge_id) else {
189 rebuilt.push(coedge.clone());
190 continue;
191 };
192 let original = *source_edges
193 .get(&coedge.edge_id)
194 .ok_or_else(|| "apply_edge_splits: missing source edge".to_string())?;
195 let span = original.t1 - original.t0;
196 let indices: Box<dyn Iterator<Item = usize>> = if coedge.forward {
197 Box::new(0..edge_pieces.len())
198 } else {
199 Box::new((0..edge_pieces.len()).rev())
200 };
201 for index in indices {
202 let piece = &edge_pieces[index];
203 let start_fraction = if coedge.forward {
204 (piece.t0 - original.t0) / span
205 } else {
206 (original.t1 - piece.t1) / span
207 };
208 let end_fraction = if coedge.forward {
209 (piece.t1 - original.t0) / span
210 } else {
211 (original.t1 - piece.t0) / span
212 };
213 rebuilt.push(CoedgeRecord {
214 id: next_coedge_id,
215 edge_id: piece.id,
216 forward: coedge.forward,
217 pcurve: subcurve_by_fraction(&coedge.pcurve, start_fraction, end_fraction)?,
218 });
219 next_coedge_id += 1;
220 }
221 }
222 loop_record.coedges = rebuilt;
223 }
224 }
225 let map = pieces
226 .iter()
227 .map(|(source_id, edge_pieces)| {
228 (
229 *source_id,
230 edge_pieces.iter().map(|piece| piece.id).collect(),
231 )
232 })
233 .collect();
234 Ok((result, map))
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::{build_imprints, make_box_brep, ImprintOptions};
241
242 #[test]
243 fn split_edges_keep_each_solid_manifold_and_pcurves_aligned() {
244 let first = make_box_brep(Vec3::default(), 4.0, 4.0, 4.0).unwrap();
245 let second = make_box_brep(Vec3::new(2.0, 1.0, 1.0), 4.0, 4.0, 4.0).unwrap();
246 let imprint = build_imprints(&first, &second, &ImprintOptions::default()).unwrap();
247 let split_first = apply_edge_splits(&first, 0, &imprint).unwrap();
248 let split_second = apply_edge_splits(&second, 1, &imprint).unwrap();
249 assert!(split_first.validate().is_empty());
250 assert!(split_second.validate().is_empty());
251 assert!(
252 split_first.edges.len() > first.edges.len()
253 || split_second.edges.len() > second.edges.len()
254 );
255 }
256}