1use super::*;
2
3pub fn extrude_profile_brep_draft(
25 profile: &[NurbsCurve],
26 direction: Vec3,
27 distance: f64,
28 draft_angle_rad: f64,
29 name: Option<&str>,
30) -> Result<BrepSolid, String> {
31 let _ = name;
34 let tolerance = 1e-6;
35 if profile.len() < 2 {
36 return Err("draftExtrude: profile needs at least 2 curves forming a closed loop".into());
37 }
38 if distance.abs() <= 1e-12 {
39 return Err("draftExtrude: distance must be non-zero".into());
40 }
41 let axis = direction
42 .normalized()
43 .map_err(|_| "draftExtrude: direction is degenerate".to_string())?;
44
45 let mut samples = Vec::new();
47 for (index, curve) in profile.iter().enumerate() {
48 let [start, end] = curve.domain()?;
49 let next = &profile[(index + 1) % profile.len()];
50 let next_start = next.domain()?[0];
51 if curve
52 .evaluate(end)?
53 .sub(next.evaluate(next_start)?)
54 .length()
55 > tolerance
56 {
57 return Err(format!(
58 "draftExtrude: profile is not closed at curve {index}"
59 ));
60 }
61 for sample in 0..16 {
62 samples.push(curve.evaluate(start + (end - start) * sample as f64 / 16.0)?);
63 }
64 }
65 let normal = crate::polygon::newell_normal(&samples);
66 let centroid = samples.iter().fold(Vec3::default(), |sum, &point| sum.add(point));
67 let np = normal
68 .normalized()
69 .map_err(|_| "draftExtrude: profile is degenerate (zero enclosed area)".to_string())?;
70 let origin = centroid.scale(1.0 / samples.len() as f64);
71 if samples
72 .iter()
73 .any(|point| point.sub(origin).dot(np).abs() > tolerance * 100.0)
74 {
75 return Err("draftExtrude: profile is not planar".into());
76 }
77 if np.dot(axis).abs() < 0.999 {
79 return Err(
80 "draftExtrude: extrude direction must be parallel to the profile normal".into(),
81 );
82 }
83
84 let displacement = axis.scale(distance);
91 let zh = displacement.normalized()?;
92 let height = displacement.length();
93 let x_axis = zh.perpendicular()?;
94 let y_axis = zh.cross(x_axis).normalized()?;
95 let mut curves: Vec<NurbsCurve> = profile.to_vec();
96 let reversed_winding = profile_area(&curves, origin, x_axis, y_axis)? < 0.0;
97 if reversed_winding {
98 curves = curves
99 .iter()
100 .rev()
101 .map(NurbsCurve::reversed)
102 .collect::<Result<_, _>>()?;
103 }
104 let signed_d = distance * draft_angle_rad.tan();
105
106 let segs = classify_profile_segments(&curves, zh).map_err(|e| format!("draftExtrude: {e}"))?;
111 let count = segs.len();
112 let mut bottom_junctions = Vec::with_capacity(count);
113 let mut top_junctions = Vec::with_capacity(count);
114 let mut mid_junctions = Vec::with_capacity(count);
115 for index in 0..count {
116 let prev = &segs[(index + count - 1) % count];
117 let next = &segs[index];
118 bottom_junctions.push(offset_junction(prev, next, zh, 0.0).map_err(|e| format!("draftExtrude: {e}"))?);
119 top_junctions.push(
120 offset_junction(prev, next, zh, signed_d)
121 .map_err(|e| format!("draftExtrude: {e}"))?
122 .add(displacement),
123 );
124 mid_junctions.push(
125 offset_junction(prev, next, zh, signed_d * 0.5)
126 .map_err(|e| format!("draftExtrude: {e}"))?
127 .add(displacement.scale(0.5)),
128 );
129 }
130
131 let mut side_curves = Vec::with_capacity(count);
136 for index in 0..count {
137 side_curves.push(junction_edge_curve(
138 &segs[(index + count - 1) % count],
139 &segs[index],
140 bottom_junctions[index],
141 mid_junctions[index],
142 top_junctions[index],
143 zh,
144 height,
145 signed_d,
146 )?);
147 }
148
149 enum WallSurface {
155 Plane { origin: Vec3, ex: Vec3, ey: Vec3 },
157 Cone,
159 }
160 let mut wall_surfaces = Vec::with_capacity(count);
161 let mut wall_kinds = Vec::with_capacity(count);
162 let mut bottom_curves = Vec::with_capacity(count);
163 let mut top_curves = Vec::with_capacity(count);
164 for index in 0..count {
165 let next_index = (index + 1) % count;
166 let a0 = bottom_junctions[index];
167 let a1 = bottom_junctions[next_index];
168 let b0 = top_junctions[index];
169 let b1 = top_junctions[next_index];
170 match &segs[index] {
171 SegGeom::Line { dir, .. } => {
172 let up = b0.sub(a0);
173 let ey = up
174 .sub(dir.scale(up.dot(*dir)))
175 .normalized()
176 .map_err(|_| "draftExtrude: wall plane frame is degenerate".to_string())?;
177 let mut points = vec![a0, a1, b0, b1];
181 for side in [&side_curves[index], &side_curves[next_index]] {
182 for control in &side.control_points {
183 points.push(control.point()?);
184 }
185 }
186 let mut min_x = f64::INFINITY;
187 let mut min_y = f64::INFINITY;
188 let mut max_x = f64::NEG_INFINITY;
189 let mut max_y = f64::NEG_INFINITY;
190 for point in &points {
191 let delta = point.sub(a0);
192 min_x = min_x.min(delta.dot(*dir));
193 max_x = max_x.max(delta.dot(*dir));
194 min_y = min_y.min(delta.dot(ey));
195 max_y = max_y.max(delta.dot(ey));
196 }
197 let padding = (max_x - min_x).max(max_y - min_y) * 0.05 + 1e-6;
198 let patch_origin = a0
199 .add(dir.scale(min_x - padding))
200 .add(ey.scale(min_y - padding));
201 wall_surfaces.push(make_plane(
202 patch_origin,
203 *dir,
204 ey,
205 max_x - min_x + 2.0 * padding,
206 max_y - min_y + 2.0 * padding,
207 )?);
208 wall_kinds.push(WallSurface::Plane {
209 origin: patch_origin,
210 ex: *dir,
211 ey,
212 });
213 bottom_curves.push(make_line(a0, a1)?);
214 top_curves.push(make_line(b0, b1)?);
215 }
216 SegGeom::Arc {
217 center,
218 radius,
219 turn,
220 arc_normal,
221 ..
222 } => {
223 let in_plane = |p: Vec3| {
224 let rel = p.sub(*center);
225 rel.sub(zh.scale(rel.dot(zh)))
226 };
227 let ax = in_plane(a0).normalized()?;
228 let ay = arc_normal.cross(ax).normalized()?;
229 let angle_near = |p: Vec3, near: f64| {
230 let ve = in_plane(p);
231 let mut angle = ve.dot(ay).atan2(ve.dot(ax));
232 while angle < near - std::f64::consts::PI {
233 angle += std::f64::consts::TAU;
234 }
235 while angle > near + std::f64::consts::PI {
236 angle -= std::f64::consts::TAU;
237 }
238 angle
239 };
240 let mut sweep = in_plane(a1).dot(ay).atan2(in_plane(a1).dot(ax));
241 if sweep <= 1e-9 {
242 sweep += std::f64::consts::TAU;
243 }
244 let phi0 = angle_near(b0, 0.0);
245 let phi1 = angle_near(b1, sweep);
246 let theta_lo = 0.0_f64.min(phi0);
247 let theta_hi = sweep.max(phi1);
248 if theta_hi - theta_lo > std::f64::consts::TAU {
249 return Err(
250 "draftExtrude: a drafted arc's trimmed window exceeds a full circle".into(),
251 );
252 }
253 let r_offset = radius - signed_d * turn;
254 if r_offset <= tolerance {
255 return Err(
256 "draftExtrude: offset: distance is too large — a concave arc collapses"
257 .into(),
258 );
259 }
260 let row_bottom = make_arc(*center, ax, ay, *radius, theta_lo, theta_hi)?;
261 let row_top =
262 make_arc(center.add(displacement), ax, ay, r_offset, theta_lo, theta_hi)?;
263 wall_surfaces.push(ruled_between(&row_bottom, &row_top)?);
264 wall_kinds.push(WallSurface::Cone);
265 bottom_curves.push(arc_window_subrange(&row_bottom, a0, a1)?);
266 top_curves.push(arc_window_subrange(&row_top, b0, b1)?);
267 }
268 }
269 }
270
271 let mut vertices = Vec::with_capacity(2 * count);
276 for (index, point) in bottom_junctions.iter().enumerate() {
277 vertices.push(VertexRecord {
278 id: index as u64 + 1,
279 point: *point,
280 });
281 }
282 for (index, point) in top_junctions.iter().enumerate() {
283 vertices.push(VertexRecord {
284 id: (count + index) as u64 + 1,
285 point: *point,
286 });
287 }
288 let mut edges = Vec::with_capacity(3 * count);
289 for index in 0..count {
290 let [b_start, b_end] = bottom_curves[index].domain()?;
291 edges.push(EdgeRecord {
292 id: 10 + index as u64,
293 curve: bottom_curves[index].clone(),
294 t0: b_start,
295 t1: b_end,
296 start_vertex_id: index as u64 + 1,
297 end_vertex_id: ((index + 1) % count) as u64 + 1,
298 degenerate: false,
299 name: None,
300 });
301 let [t_start, t_end] = top_curves[index].domain()?;
302 edges.push(EdgeRecord {
303 id: 10 + count as u64 + index as u64,
304 curve: top_curves[index].clone(),
305 t0: t_start,
306 t1: t_end,
307 start_vertex_id: (count + index) as u64 + 1,
308 end_vertex_id: (count + (index + 1) % count) as u64 + 1,
309 degenerate: false,
310 name: None,
311 });
312 let [s_start, s_end] = side_curves[index].domain()?;
313 edges.push(EdgeRecord {
314 id: 10 + 2 * count as u64 + index as u64,
315 curve: side_curves[index].clone(),
316 t0: s_start,
317 t1: s_end,
318 start_vertex_id: index as u64 + 1,
319 end_vertex_id: (count + index) as u64 + 1,
320 degenerate: false,
321 name: None,
322 });
323 }
324
325 let mut next_id = 1000_u64;
326 let mut faces = Vec::with_capacity(count + 2);
327 for index in 0..count {
328 let next_index = (index + 1) % count;
329 let surface = &wall_surfaces[index];
330 let (pc_bottom, pc_side_up, pc_top, pc_side_down) = match &wall_kinds[index] {
333 WallSurface::Plane { origin, ex, ey } => (
334 curve_to_plane_parameters(&bottom_curves[index], *origin, *ex, *ey)?,
335 curve_to_plane_parameters(&side_curves[next_index], *origin, *ex, *ey)?,
336 curve_to_plane_parameters(&top_curves[index], *origin, *ex, *ey)?.reversed()?,
337 curve_to_plane_parameters(&side_curves[index], *origin, *ex, *ey)?.reversed()?,
338 ),
339 WallSurface::Cone => {
340 let [b0, b1] = bottom_curves[index].domain()?;
341 let [t0, t1] = top_curves[index].domain()?;
342 (
343 parameter_line(b0, 0.0, b1, 0.0)?,
344 build_pcurve_on_surface(surface, &side_curves[next_index])?,
345 parameter_line(t1, 1.0, t0, 1.0)?,
346 build_pcurve_on_surface(surface, &side_curves[index])?.reversed()?,
347 )
348 }
349 };
350 let coedges = vec![
351 CoedgeRecord {
352 id: next_id,
353 edge_id: 10 + index as u64,
354 forward: true,
355 pcurve: pc_bottom,
356 },
357 CoedgeRecord {
358 id: next_id + 1,
359 edge_id: 10 + 2 * count as u64 + next_index as u64,
360 forward: true,
361 pcurve: pc_side_up,
362 },
363 CoedgeRecord {
364 id: next_id + 2,
365 edge_id: 10 + count as u64 + index as u64,
366 forward: false,
367 pcurve: pc_top,
368 },
369 CoedgeRecord {
370 id: next_id + 3,
371 edge_id: 10 + 2 * count as u64 + index as u64,
372 forward: false,
373 pcurve: pc_side_down,
374 },
375 ];
376 next_id += 4;
377 faces.push(FaceRecord {
378 id: next_id + 1,
379 surface: wall_surfaces[index].clone(),
380 same_sense: true,
381 loops: vec![LoopRecord {
382 id: next_id,
383 coedges,
384 }],
385 name: None,
386 });
387 next_id += 2;
388 }
389 if reversed_winding {
390 faces.reverse();
393 }
394
395 let mut cap = |curves: &[NurbsCurve],
397 edge_base: u64,
398 forward: bool,
399 next_id: &mut u64|
400 -> Result<FaceRecord, String> {
401 let mut min_x = f64::INFINITY;
402 let mut min_y = f64::INFINITY;
403 let mut max_x = f64::NEG_INFINITY;
404 let mut max_y = f64::NEG_INFINITY;
405 let mut plane_point = None;
406 for curve in curves {
407 let [start, end] = curve.domain()?;
408 for sample in 0..=16 {
409 let point = curve.evaluate(start + (end - start) * sample as f64 / 16.0)?;
410 let anchor = *plane_point.get_or_insert(point);
411 let delta = point.sub(anchor);
412 min_x = min_x.min(delta.dot(x_axis));
413 max_x = max_x.max(delta.dot(x_axis));
414 min_y = min_y.min(delta.dot(y_axis));
415 max_y = max_y.max(delta.dot(y_axis));
416 }
417 }
418 let anchor = plane_point.ok_or("draftExtrude: cap has no boundary samples")?;
419 let padding = (max_x - min_x).max(max_y - min_y) * 0.05 + 1e-6;
420 let cap_origin = anchor
421 .add(x_axis.scale(min_x - padding))
422 .add(y_axis.scale(min_y - padding));
423 let mut coedges = Vec::with_capacity(curves.len());
424 if forward {
425 for (index, curve) in curves.iter().enumerate() {
426 coedges.push(CoedgeRecord {
427 id: *next_id,
428 edge_id: edge_base + index as u64,
429 forward: true,
430 pcurve: curve_to_plane_parameters(curve, cap_origin, x_axis, y_axis)?,
431 });
432 *next_id += 1;
433 }
434 } else {
435 for index in (0..curves.len()).rev() {
436 coedges.push(CoedgeRecord {
437 id: *next_id,
438 edge_id: edge_base + index as u64,
439 forward: false,
440 pcurve: curve_to_plane_parameters(&curves[index], cap_origin, x_axis, y_axis)?
441 .reversed()?,
442 });
443 *next_id += 1;
444 }
445 }
446 let loop_id = *next_id;
447 let face_id = *next_id + 1;
448 *next_id += 2;
449 Ok(FaceRecord {
450 id: face_id,
451 surface: make_plane(
452 cap_origin,
453 x_axis,
454 y_axis,
455 max_x - min_x + 2.0 * padding,
456 max_y - min_y + 2.0 * padding,
457 )?,
458 same_sense: forward,
459 loops: vec![LoopRecord {
460 id: loop_id,
461 coedges,
462 }],
463 name: None,
464 })
465 };
466 faces.push(cap(&bottom_curves, 10, false, &mut next_id)?);
467 faces.push(cap(&top_curves, 10 + count as u64, true, &mut next_id)?);
468
469 let solid = BrepSolid {
470 id: next_id + 1,
471 vertices,
472 edges,
473 shells: vec![ShellRecord { id: next_id, faces }],
474 genus: 0,
475 };
476 let issues = solid.validate();
477 if issues.is_empty() {
478 Ok(solid)
479 } else {
480 Err(format!(
481 "Rust draft-extrude builder produced invalid topology: {issues:?}"
482 ))
483 }
484}