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