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