Skip to main content

brep_kernel/construction/sweep_topology/
extrude.rs

1use super::*;
2
3pub(super) fn translated_curve(curve: &NurbsCurve, delta: Vec3) -> Result<NurbsCurve, KernelRefusal> {
4    NurbsCurve::new(
5        curve.degree,
6        curve.knots.clone(),
7        curve
8            .control_points
9            .iter()
10            .map(|point| Vec4 {
11                x: point.x + point.w * delta.x,
12                y: point.y + point.w * delta.y,
13                z: point.z + point.w * delta.z,
14                w: point.w,
15            })
16            .collect(),
17    )
18    .or_refuse(KernelStage::Fragment, "curve_new")
19}
20
21pub(crate) fn profile_area(
22    curves: &[NurbsCurve],
23    origin: Vec3,
24    x_axis: Vec3,
25    y_axis: Vec3,
26) -> Result<f64, String> {
27    let mut area = 0.0;
28    for curve in curves {
29        let [start, end] = curve.domain()?;
30        let mut previous = curve.evaluate(start)?;
31        for index in 1..=64 {
32            let point = curve.evaluate(start + (end - start) * index as f64 / 64.0)?;
33            let a = previous.sub(origin);
34            let b = point.sub(origin);
35            area += 0.5 * (a.dot(x_axis) * b.dot(y_axis) - b.dot(x_axis) * a.dot(y_axis));
36            previous = point;
37        }
38    }
39    Ok(area)
40}
41
42pub fn extrude_profile_brep(
43    input_curves: &[NurbsCurve],
44    direction: Vec3,
45    distance: f64,
46) -> Result<BrepSolid, KernelRefusal> {
47    if input_curves.len() < 2 {
48        return Err(KernelRefusal::input(
49            KernelStage::Collect,
50            "profile_count",
51            "profile needs at least 2 curves",
52        ));
53    }
54    if distance.abs() <= 1e-12 {
55        return Err(KernelRefusal::input(
56            KernelStage::Collect,
57            "distance",
58            "extrudeSolid: distance must be non-zero",
59        ));
60    }
61    let displacement = direction
62        .normalized()
63        .or_input(KernelStage::Collect, "direction")?
64        .scale(distance);
65    let tolerance = 1e-6;
66    let mut curves: Vec<NurbsCurve> = input_curves
67        .iter()
68        .map(|curve| {
69            NurbsCurve::new(
70                curve.degree,
71                curve.knots.clone(),
72                curve.control_points.clone(),
73            )
74        })
75        .collect::<Result<_, _>>().or_refuse(KernelStage::Fragment, "collect")?;
76
77    let mut samples = Vec::new();
78    for (index, curve) in curves.iter().enumerate() {
79        let [start, end] = curve.domain().or_refuse(KernelStage::Fragment, "domain")?;
80        let next = &curves[(index + 1) % curves.len()];
81        let next_start = next.domain().or_refuse(KernelStage::Fragment, "domain")?[0];
82        if curve
83            .evaluate(end).or_refuse(KernelStage::Fragment, "evaluate")?
84            .sub(next.evaluate(next_start).or_refuse(KernelStage::Fragment, "evaluate")?)
85            .length()
86            > tolerance
87        {
88            return Err(KernelRefusal::input(
89                KernelStage::Collect,
90                "profile_closed",
91                format!("profile not closed at curve {index}"),
92            ));
93        }
94        for sample in 0..16 {
95            samples.push(curve.evaluate(start + (end - start) * sample as f64 / 16.0).or_refuse(KernelStage::Fragment, "evaluate")?);
96        }
97    }
98    let mut normal = crate::polygon::newell_normal(&samples);
99    // A zero Newell normal is a profile with no enclosed area: the caller's.
100    normal = normal.normalized().or_input(KernelStage::Collect, "profile_area")?;
101    if normal.dot(displacement) < 0.0 {
102        normal = normal.scale(-1.0);
103    }
104    if normal.dot(displacement.normalized().or_refuse(KernelStage::Fragment, "normalized")?).abs() < 0.1 {
105        return Err(KernelRefusal::input(
106            KernelStage::Classify,
107            "sliver",
108            "extrudeSolid: sweep direction is nearly parallel to profile plane",
109        ));
110    }
111    let origin = samples[0];
112    if samples
113        .iter()
114        .any(|point| point.sub(origin).dot(normal).abs() > tolerance * 100.0)
115    {
116        return Err(KernelRefusal::input(
117            KernelStage::Collect,
118            "profile_planar",
119            "extrudeSolid: profile is not planar",
120        ));
121    }
122    let x_axis = normal.perpendicular().or_refuse(KernelStage::Fragment, "perpendicular")?;
123    let y_axis = normal.cross(x_axis).normalized().or_refuse(KernelStage::Fragment, "normalized")?;
124    let reversed_winding = profile_area(&curves, origin, x_axis, y_axis).or_refuse(KernelStage::Fragment, "profile_area")? < 0.0;
125    if reversed_winding {
126        curves = curves
127            .iter()
128            .rev()
129            .map(NurbsCurve::reversed)
130            .collect::<Result<_, _>>().or_refuse(KernelStage::Fragment, "collect")?;
131    }
132
133    let count = curves.len();
134    let points: Vec<Vec3> = curves
135        .iter()
136        .map(|curve| curve.domain().and_then(|domain| curve.evaluate(domain[0])))
137        .collect::<Result<_, _>>().or_refuse(KernelStage::Fragment, "collect")?;
138    let mut vertices = Vec::with_capacity(count * 2);
139    for (index, point) in points.iter().enumerate() {
140        vertices.push(VertexRecord {
141            id: index as u64 + 1,
142            point: *point,
143        });
144    }
145    for (index, point) in points.iter().enumerate() {
146        vertices.push(VertexRecord {
147            id: (count + index) as u64 + 1,
148            point: point.add(displacement),
149        });
150    }
151
152    let mut edges = Vec::with_capacity(count * 3);
153    for (index, curve) in curves.iter().enumerate() {
154        let [start, end] = curve.domain().or_refuse(KernelStage::Fragment, "domain")?;
155        edges.push(EdgeRecord {
156            id: 10 + index as u64,
157            curve: curve.clone(),
158            t0: start,
159            t1: end,
160            start_vertex_id: index as u64 + 1,
161            end_vertex_id: ((index + 1) % count) as u64 + 1,
162            degenerate: false,
163            name: None,
164        });
165        edges.push(EdgeRecord {
166            id: 10 + count as u64 + index as u64,
167            curve: translated_curve(curve, displacement)?,
168            t0: start,
169            t1: end,
170            start_vertex_id: (count + index) as u64 + 1,
171            end_vertex_id: (count + (index + 1) % count) as u64 + 1,
172            degenerate: false,
173            name: None,
174        });
175        edges.push(EdgeRecord {
176            id: 10 + 2 * count as u64 + index as u64,
177            curve: make_line(points[index], points[index].add(displacement)).or_refuse(KernelStage::Fragment, "make_line")?,
178            t0: 0.0,
179            t1: 1.0,
180            start_vertex_id: index as u64 + 1,
181            end_vertex_id: (count + index) as u64 + 1,
182            degenerate: false,
183            name: None,
184        });
185    }
186
187    let mut next_id = 1000_u64;
188    let mut faces = Vec::with_capacity(count + 2);
189    for (index, curve) in curves.iter().enumerate() {
190        let [start, end] = curve.domain().or_refuse(KernelStage::Fragment, "domain")?;
191        let coedges = vec![
192            CoedgeRecord {
193                id: next_id,
194                edge_id: 10 + index as u64,
195                forward: true,
196                pcurve: parameter_line(start, 0.0, end, 0.0).or_refuse(KernelStage::Fragment, "parameter_line")?,
197            },
198            CoedgeRecord {
199                id: next_id + 1,
200                edge_id: 10 + 2 * count as u64 + ((index + 1) % count) as u64,
201                forward: true,
202                pcurve: parameter_line(end, 0.0, end, 1.0).or_refuse(KernelStage::Fragment, "parameter_line")?,
203            },
204            CoedgeRecord {
205                id: next_id + 2,
206                edge_id: 10 + count as u64 + index as u64,
207                forward: false,
208                pcurve: parameter_line(end, 1.0, start, 1.0).or_refuse(KernelStage::Fragment, "parameter_line")?,
209            },
210            CoedgeRecord {
211                id: next_id + 3,
212                edge_id: 10 + 2 * count as u64 + index as u64,
213                forward: false,
214                pcurve: parameter_line(start, 1.0, start, 0.0).or_refuse(KernelStage::Fragment, "parameter_line")?,
215            },
216        ];
217        next_id += 4;
218        faces.push(FaceRecord {
219            id: next_id + 1,
220            surface: make_extrusion(curve, displacement).or_refuse(KernelStage::Fragment, "make_extrusion")?,
221            same_sense: true,
222            loops: vec![LoopRecord {
223                id: next_id,
224                coedges,
225            }],
226            name: None,
227        });
228        next_id += 2;
229    }
230    if reversed_winding {
231        // The winding normalization reversed the curve loop above.  Callers
232        // name side faces by INPUT-curve order (the app stamps names onto
233        // kernel faces in listed order), so a silently reordered face list
234        // permutes every wall name — an arc's wall ends up carrying a
235        // neighboring line's name.  Emit side faces in input order.
236        faces.reverse();
237    }
238
239    let mut min_x = f64::INFINITY;
240    let mut min_y = f64::INFINITY;
241    let mut max_x = f64::NEG_INFINITY;
242    let mut max_y = f64::NEG_INFINITY;
243    for point in &samples {
244        let delta = point.sub(origin);
245        min_x = min_x.min(delta.dot(x_axis));
246        max_x = max_x.max(delta.dot(x_axis));
247        min_y = min_y.min(delta.dot(y_axis));
248        max_y = max_y.max(delta.dot(y_axis));
249    }
250    let padding = (max_x - min_x).max(max_y - min_y) * 0.05 + 1e-6;
251    let width = max_x - min_x + 2.0 * padding;
252    let height = max_y - min_y + 2.0 * padding;
253    let bottom_origin = origin
254        .add(x_axis.scale(min_x - padding))
255        .add(y_axis.scale(min_y - padding));
256    let mut bottom_coedges = Vec::with_capacity(count);
257    for index in (0..count).rev() {
258        bottom_coedges.push(CoedgeRecord {
259            id: next_id,
260            edge_id: 10 + index as u64,
261            forward: false,
262            pcurve: curve_to_plane_parameters(&curves[index], bottom_origin, x_axis, y_axis).or_refuse(KernelStage::Fragment, "plane_parameters")?
263                .reversed().or_refuse(KernelStage::Fragment, "reversed")?,
264        });
265        next_id += 1;
266    }
267    faces.push(FaceRecord {
268        id: next_id + 1,
269        surface: make_plane(bottom_origin, x_axis, y_axis, width, height).or_refuse(KernelStage::Fragment, "make_plane")?,
270        same_sense: false,
271        loops: vec![LoopRecord {
272            id: next_id,
273            coedges: bottom_coedges,
274        }],
275        name: None,
276    });
277    next_id += 2;
278
279    let top_origin = bottom_origin.add(displacement);
280    let mut top_coedges = Vec::with_capacity(count);
281    for (index, curve) in curves.iter().enumerate() {
282        let translated = translated_curve(curve, displacement)?;
283        top_coedges.push(CoedgeRecord {
284            id: next_id,
285            edge_id: 10 + count as u64 + index as u64,
286            forward: true,
287            pcurve: curve_to_plane_parameters(&translated, top_origin, x_axis, y_axis).or_refuse(KernelStage::Fragment, "plane_parameters")?,
288        });
289        next_id += 1;
290    }
291    faces.push(FaceRecord {
292        id: next_id + 1,
293        surface: make_plane(top_origin, x_axis, y_axis, width, height).or_refuse(KernelStage::Fragment, "make_plane")?,
294        same_sense: true,
295        loops: vec![LoopRecord {
296            id: next_id,
297            coedges: top_coedges,
298        }],
299        name: None,
300    });
301    next_id += 2;
302
303    let solid = BrepSolid {
304        mass_properties_cache: Default::default(),
305        id: next_id + 1,
306        vertices,
307        edges,
308        shells: vec![ShellRecord { id: next_id, faces }],
309        genus: 0,
310    };
311    let issues = solid.validate();
312    if issues.is_empty() {
313        Ok(solid)
314    } else {
315        Err(KernelRefusal::internal(
316            KernelStage::Validate,
317            "validate",
318            format!("Rust extrusion builder produced invalid topology: {issues:?}"),
319        ))
320    }
321}