Skip to main content

brep_kernel/csg/
edge_split.rs

1use 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
60/// Apply the boundary-edge partition produced by imprint construction.
61///
62/// The original edge curve and parameter range semantics are preserved; only
63/// coedge p-curves are restricted to traversal-aligned subcurves.
64pub 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
72/// [`apply_edge_splits`] plus the identity ledger: original edge id → the
73/// minted sub-edge ids that replaced it. Consumers keyed on ORIGINAL edge
74/// ids (the fragment-selection barrier set) must remap through it, since the
75/// split solid's coedges reference the minted ids.
76pub 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            // First piece keeps the source name; later pieces get the
146            // deterministic `_1`, `_2` split suffixes the application uses.
147            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    // id -> source edge, built once. The per-coedge `solid.edges.iter().find`
175    // below was O(edges) inside the face/loop/coedge loops. `solid` is the
176    // immutable source, so the lookup returns the identical record.
177    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}