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