Skip to main content

brep_kernel/construction/sweep_topology/
extrude.rs

1use super::*;
2
3pub(crate) fn parameter_line(u0: f64, v0: f64, u1: f64, v1: f64) -> Result<NurbsCurve, String> {
4    make_line(Vec3::new(u0, v0, 0.0), Vec3::new(u1, v1, 0.0))
5}
6
7fn translated_curve(curve: &NurbsCurve, delta: Vec3) -> Result<NurbsCurve, String> {
8    NurbsCurve::new(
9        curve.degree,
10        curve.knots.clone(),
11        curve
12            .control_points
13            .iter()
14            .map(|point| Vec4 {
15                x: point.x + point.w * delta.x,
16                y: point.y + point.w * delta.y,
17                z: point.z + point.w * delta.z,
18                w: point.w,
19            })
20            .collect(),
21    )
22}
23
24pub(crate) fn curve_to_plane_parameters(
25    curve: &NurbsCurve,
26    origin: Vec3,
27    x_axis: Vec3,
28    y_axis: Vec3,
29) -> Result<NurbsCurve, String> {
30    NurbsCurve::new(
31        curve.degree,
32        curve.knots.clone(),
33        curve
34            .control_points
35            .iter()
36            .map(|point| {
37                let euclidean = Vec3::new(point.x / point.w, point.y / point.w, point.z / point.w);
38                let delta = euclidean.sub(origin);
39                Vec4 {
40                    x: delta.dot(x_axis) * point.w,
41                    y: delta.dot(y_axis) * point.w,
42                    z: 0.0,
43                    w: point.w,
44                }
45            })
46            .collect(),
47    )
48}
49
50pub(crate) fn profile_area(
51    curves: &[NurbsCurve],
52    origin: Vec3,
53    x_axis: Vec3,
54    y_axis: Vec3,
55) -> Result<f64, String> {
56    let mut area = 0.0;
57    for curve in curves {
58        let [start, end] = curve.domain()?;
59        let mut previous = curve.evaluate(start)?;
60        for index in 1..=64 {
61            let point = curve.evaluate(start + (end - start) * index as f64 / 64.0)?;
62            let a = previous.sub(origin);
63            let b = point.sub(origin);
64            area += 0.5 * (a.dot(x_axis) * b.dot(y_axis) - b.dot(x_axis) * a.dot(y_axis));
65            previous = point;
66        }
67    }
68    Ok(area)
69}
70
71pub fn extrude_profile_brep(
72    input_curves: &[NurbsCurve],
73    direction: Vec3,
74    distance: f64,
75) -> Result<BrepSolid, String> {
76    if input_curves.len() < 2 {
77        return Err("profile needs at least 2 curves".into());
78    }
79    if distance.abs() <= 1e-12 {
80        return Err("extrudeSolid: distance must be non-zero".into());
81    }
82    let displacement = direction.normalized()?.scale(distance);
83    let tolerance = 1e-6;
84    let mut curves: Vec<NurbsCurve> = input_curves
85        .iter()
86        .map(|curve| {
87            NurbsCurve::new(
88                curve.degree,
89                curve.knots.clone(),
90                curve.control_points.clone(),
91            )
92        })
93        .collect::<Result<_, _>>()?;
94
95    let mut samples = Vec::new();
96    for (index, curve) in curves.iter().enumerate() {
97        let [start, end] = curve.domain()?;
98        let next = &curves[(index + 1) % curves.len()];
99        let next_start = next.domain()?[0];
100        if curve
101            .evaluate(end)?
102            .sub(next.evaluate(next_start)?)
103            .length()
104            > tolerance
105        {
106            return Err(format!("profile not closed at curve {index}"));
107        }
108        for sample in 0..16 {
109            samples.push(curve.evaluate(start + (end - start) * sample as f64 / 16.0)?);
110        }
111    }
112    let mut normal = Vec3::default();
113    for index in 0..samples.len() {
114        let point = samples[index];
115        let next = samples[(index + 1) % samples.len()];
116        normal.x += (point.y - next.y) * (point.z + next.z);
117        normal.y += (point.z - next.z) * (point.x + next.x);
118        normal.z += (point.x - next.x) * (point.y + next.y);
119    }
120    normal = normal.normalized()?;
121    if normal.dot(displacement) < 0.0 {
122        normal = normal.scale(-1.0);
123    }
124    if normal.dot(displacement.normalized()?).abs() < 0.1 {
125        return Err("extrudeSolid: sweep direction is nearly parallel to profile plane".into());
126    }
127    let origin = samples[0];
128    if samples
129        .iter()
130        .any(|point| point.sub(origin).dot(normal).abs() > tolerance * 100.0)
131    {
132        return Err("extrudeSolid: profile is not planar".into());
133    }
134    let x_axis = normal.perpendicular()?;
135    let y_axis = normal.cross(x_axis).normalized()?;
136    let reversed_winding = profile_area(&curves, origin, x_axis, y_axis)? < 0.0;
137    if reversed_winding {
138        curves = curves
139            .iter()
140            .rev()
141            .map(NurbsCurve::reversed)
142            .collect::<Result<_, _>>()?;
143    }
144
145    let count = curves.len();
146    let points: Vec<Vec3> = curves
147        .iter()
148        .map(|curve| curve.domain().and_then(|domain| curve.evaluate(domain[0])))
149        .collect::<Result<_, _>>()?;
150    let mut vertices = Vec::with_capacity(count * 2);
151    for (index, point) in points.iter().enumerate() {
152        vertices.push(VertexRecord {
153            id: index as u64 + 1,
154            point: *point,
155        });
156    }
157    for (index, point) in points.iter().enumerate() {
158        vertices.push(VertexRecord {
159            id: (count + index) as u64 + 1,
160            point: point.add(displacement),
161        });
162    }
163
164    let mut edges = Vec::with_capacity(count * 3);
165    for (index, curve) in curves.iter().enumerate() {
166        let [start, end] = curve.domain()?;
167        edges.push(EdgeRecord {
168            id: 10 + index as u64,
169            curve: curve.clone(),
170            t0: start,
171            t1: end,
172            start_vertex_id: index as u64 + 1,
173            end_vertex_id: ((index + 1) % count) as u64 + 1,
174            degenerate: false,
175            name: None,
176        });
177        edges.push(EdgeRecord {
178            id: 10 + count as u64 + index as u64,
179            curve: translated_curve(curve, displacement)?,
180            t0: start,
181            t1: end,
182            start_vertex_id: (count + index) as u64 + 1,
183            end_vertex_id: (count + (index + 1) % count) as u64 + 1,
184            degenerate: false,
185            name: None,
186        });
187        edges.push(EdgeRecord {
188            id: 10 + 2 * count as u64 + index as u64,
189            curve: make_line(points[index], points[index].add(displacement))?,
190            t0: 0.0,
191            t1: 1.0,
192            start_vertex_id: index as u64 + 1,
193            end_vertex_id: (count + index) as u64 + 1,
194            degenerate: false,
195            name: None,
196        });
197    }
198
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()?;
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)?,
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)?,
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)?,
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)?,
227            },
228        ];
229        next_id += 4;
230        faces.push(FaceRecord {
231            id: next_id + 1,
232            surface: make_extrusion(curve, displacement)?,
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)?
275                .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)?,
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)?,
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)?,
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        id: next_id + 1,
317        vertices,
318        edges,
319        shells: vec![ShellRecord { id: next_id, faces }],
320        genus: 0,
321    };
322    let issues = solid.validate();
323    if issues.is_empty() {
324        Ok(solid)
325    } else {
326        Err(format!(
327            "Rust extrusion builder produced invalid topology: {issues:?}"
328        ))
329    }
330}