brep_kernel/construction/sweep_topology/
extrude.rs1use 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 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}