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