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