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 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 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 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}