1use crate::mass_properties::{GAUSS_X, GAUSS_W};
31use crate::{KernelRefusal, KernelStage, OrRefuse};
32use crate::offset_carve::{carve_folded_trim_scanned, dropped_sweep as carve_dropped_sweep, CarveOutcome};
33use crate::offset_regularity::{scan_offset_regularity, Certainty, RegularityScan, ScanBudget, TrimRegion};
34use crate::sweep_topology::parameter_line;
35use crate::topology::{
36 BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
37};
38use crate::image_curve::{affine_image_curve, image_curve_pair};
39use crate::{make_arc, make_line, NurbsCurve, NurbsSurface, Vec3};
40
41#[path = "thicken/band.rs"]
42pub(crate) mod band;
43
44#[path = "thicken/periodic.rs"]
45mod periodic;
46
47pub(crate) const FOLD_FACTOR: f64 = 1e-6;
53
54fn ensure_offsets_regular(
69 surface: &NurbsSurface,
70 loops: &[Vec<NurbsCurve>],
71 distances: &[f64],
72) -> Result<(), KernelRefusal> {
73 let region = TrimRegion::from_pcurve_loops(surface, loops)
74 .map_err(|error| error.with_message(|error| format!("thickenSheet: {error}")))?;
75 let mut scan = scan_offset_regularity(surface, ®ion, distances, FOLD_FACTOR, ScanBudget::SHEET)
76 .map_err(|error| error.with_message(|error| format!("thickenSheet: {error}")))?;
77 ensure_scan_regular(surface, ®ion, distances, &mut scan)
78}
79
80fn ensure_scan_regular(
84 surface: &NurbsSurface,
85 region: &TrimRegion,
86 distances: &[f64],
87 scan: &mut RegularityScan,
88) -> Result<(), KernelRefusal> {
89 match scan.certify(surface, region, distances, FOLD_FACTOR) {
90 Certainty::Regular => Ok(()),
91 Certainty::Folded(worst) => Err(fold_refusal(surface, worst)),
92 Certainty::Uncertain {
93 unresolved,
94 budget_exhausted,
95 least,
96 } => Err(uncertain_refusal(scan, unresolved, budget_exhausted, least)),
97 }
98}
99
100fn uncertain_refusal(
105 scan: &RegularityScan,
106 unresolved: usize,
107 budget_exhausted: bool,
108 least: Option<crate::offset_regularity::FoldSample>,
109) -> KernelRefusal {
110 let because = match (unresolved, budget_exhausted) {
111 (0, _) => "the refinement budget ran out with prominent dips of the fold factor still \
112 unexplored"
113 .to_string(),
114 (cells, false) => format!(
115 "{cells} cell(s) were still risky at the scan's finest spacing — the fold factor \
116 sits within a hair of the level over an area no sampling can decide"
117 ),
118 (cells, true) => format!(
119 "{cells} cell(s) were still risky at the scan's finest spacing and the refinement \
120 budget ran out"
121 ),
122 };
123 let least = least
124 .map(|least| {
125 format!(
126 "; the least fold factor seen is {:.3e} at (u={:.6}, v={:.6}), concave curvature \
127 radius {:.6} against the offset distance {:.6}",
128 least.factor,
129 least.u,
130 least.v,
131 least.radius(),
132 least.displacement.abs()
133 )
134 })
135 .unwrap_or_default();
136 KernelRefusal::unsupported(KernelStage::Refine, "thicken_regularity_uncertain", format!(
137 "thickenSheet: the offset cannot be certified regular over the selected trim — none of \
138 {} samples collapsed, but {because}{least}",
139 scan.sampled
140 ))
141}
142
143fn fold_refusal(surface: &NurbsSurface, worst: crate::offset_regularity::FoldSample) -> KernelRefusal {
145 let where_3d = match surface.evaluate(worst.u, worst.v) {
146 Ok(point) => format!(
147 " — 3D ({:.6}, {:.6}, {:.6})",
148 point.x, point.y, point.z
149 ),
150 Err(_) => String::new(),
151 };
152 KernelRefusal::unsupported(KernelStage::Refine, "thicken_fold_inside_trim", format!(
153 "thickenSheet: offset by {:.6} self-intersects — inside the selected trim the sheet's \
154 concave curvature radius {:.6} at (u={:.6}, v={:.6}){} is not larger than the offset \
155 distance (fold factor {:.3e})",
156 worst.displacement,
157 worst.radius(),
158 worst.u,
159 worst.v,
160 where_3d,
161 worst.factor
162 ))
163}
164
165fn validate_trim_loops(
172 loops: &[Vec<NurbsCurve>],
173 uv_tolerance: f64,
174 minimum_area: f64,
175) -> Result<(), KernelRefusal> {
176 for (loop_index, loop_curves) in loops.iter().enumerate() {
177 let count = loop_curves.len();
178 let mut starts = Vec::with_capacity(count);
179 let mut ends = Vec::with_capacity(count);
180 for curve in loop_curves {
181 let [q0, q1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
182 starts.push(curve.evaluate(q0).or_refuse(KernelStage::Refine, "evaluate")?);
183 ends.push(curve.evaluate(q1).or_refuse(KernelStage::Refine, "evaluate")?);
184 }
185 for index in 0..count {
186 let next_index = (index + 1) % count;
187 let gap = planar_gap(ends[index], starts[next_index]);
188 if gap > uv_tolerance {
189 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_open_loop", format!(
190 "thickenSheet: loop {loop_index} is open — pcurve {index} ends at \
191 (u={:.6}, v={:.6}) but pcurve {next_index} starts at (u={:.6}, v={:.6}) \
192 (parameter-space gap {gap:.3e})",
193 ends[index].x, ends[index].y, starts[next_index].x, starts[next_index].y
194 )));
195 }
196 }
197 if count == 1 {
198 let [q0, q1] = loop_curves[0].domain().or_refuse(KernelStage::Refine, "domain")?;
199 let middle = loop_curves[0].evaluate((q0 + q1) * 0.5).or_refuse(KernelStage::Refine, "evaluate")?;
200 if planar_gap(middle, starts[0]) <= uv_tolerance {
201 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_degenerate_loop", format!(
202 "thickenSheet: loop {loop_index} is degenerate (zero parameter-space extent)"
203 )));
204 }
205 } else {
206 for index in 0..count {
207 if planar_gap(ends[index], starts[index]) <= uv_tolerance {
208 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_pinched_loop", format!(
209 "thickenSheet: pcurve {index} of loop {loop_index} closes on itself \
210 inside a multi-curve loop (pinched loop)"
211 )));
212 }
213 }
214 }
215 let mut area = 0.0;
216 for curve in loop_curves {
217 area += pcurve_signed_area(curve)?;
218 }
219 if loop_index == 0 {
220 if area <= minimum_area {
221 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_outer_winding", format!(
222 "thickenSheet: outer loop must run counter-clockwise in (u, v) \
223 (signed area {area:.3e})"
224 )));
225 }
226 } else if area >= -minimum_area {
227 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_hole_winding", format!(
228 "thickenSheet: hole loop {loop_index} must run clockwise in (u, v) \
229 (signed area {area:.3e})"
230 )));
231 }
232 }
233 Ok(())
234}
235
236fn offset_sheet(
259 surface: &NurbsSurface,
260 loops: &[Vec<NurbsCurve>],
261 distance: f64,
262) -> Result<NurbsSurface, KernelRefusal> {
263 if distance == 0.0 {
264 return Ok(surface.clone());
265 }
266 let carrier = FaceRecord {
267 id: 1,
268 surface: surface.clone(),
269 same_sense: true,
270 loops: loops
271 .iter()
272 .enumerate()
273 .map(|(loop_index, loop_curves)| LoopRecord {
274 id: loop_index as u64 + 1,
275 coedges: loop_curves
276 .iter()
277 .enumerate()
278 .map(|(index, pcurve)| CoedgeRecord {
279 id: index as u64 + 1,
283 edge_id: index as u64 + 1,
284 forward: true,
285 pcurve: pcurve.clone(),
286 })
287 .collect(),
288 })
289 .collect(),
290 name: None,
291 };
292 let (offset, lane) = crate::offset::offset_surface_with_lane(
293 &carrier,
294 -distance,
295 &crate::CarrierExtension::uniform(0.0),
296 )?;
297 if matches!(lane, crate::OffsetSurfaceLane::Reparameterised) {
298 return Err(KernelRefusal::unsupported(KernelStage::Refine, "thicken_reparameterised_carrier", format!(
299 "thickenSheet: the offset carrier for distance {distance:.6} was re-parameterized \
300 (the apex-cone pinch retrim pulled a sample row back to the offset's own apex), so \
301 the trim's pcurves no longer name the pointwise offset of the selected region — the \
302 selected trim reaches past the pinch and offsetting only its own sub-domain is not \
303 yet supported"
304 )));
305 }
306 Ok(offset)
307}
308
309fn ruled_wall(bottom: &NurbsCurve, top: &NurbsCurve) -> Result<NurbsSurface, KernelRefusal> {
315 if bottom.degree != top.degree
316 || bottom.knots.len() != top.knots.len()
317 || bottom
318 .knots
319 .iter()
320 .zip(&top.knots)
321 .any(|(a, b)| (a - b).abs() > 1e-12)
322 || bottom
323 .control_points
324 .iter()
325 .zip(&top.control_points)
326 .any(|(a, b)| (a.w - b.w).abs() > 1e-9)
327 {
328 return Err(KernelRefusal::internal(KernelStage::Refine, "thicken_basis_mismatch", "thickenSheet: internal error — offset sheet basis mismatch"));
329 }
330 let rows = bottom
331 .control_points
332 .iter()
333 .zip(&top.control_points)
334 .map(|(b, t)| vec![*b, *t])
335 .collect();
336 NurbsSurface::new(
337 bottom.degree,
338 1,
339 bottom.knots.clone(),
340 vec![0.0, 0.0, 1.0, 1.0],
341 rows,
342 )
343 .or_refuse(KernelStage::Refine, "new")
344}
345
346
347fn pcurve_signed_area(curve: &NurbsCurve) -> Result<f64, KernelRefusal> {
352 let [q0, q1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
353 let mut breaks = vec![q0];
354 for &knot in &curve.knots {
355 if knot > q0 + 1e-12 && knot < q1 - 1e-12 && (knot - breaks[breaks.len() - 1]).abs() > 1e-12
356 {
357 breaks.push(knot);
358 }
359 }
360 breaks.push(q1);
361 let mut area = 0.0;
362 for pair in breaks.windows(2) {
363 let half = (pair[1] - pair[0]) * 0.5;
364 let middle = (pair[1] + pair[0]) * 0.5;
365 for index in 0..GAUSS_X.len() {
366 let derivatives = curve.derivatives(middle + half * GAUSS_X[index], 1).or_refuse(KernelStage::Refine, "derivatives")?;
367 let point = derivatives[0];
368 let tangent = derivatives[1];
369 area += GAUSS_W[index] * half * 0.5 * (point.x * tangent.y - point.y * tangent.x);
370 }
371 }
372 Ok(area)
373}
374
375fn planar_gap(first: Vec3, second: Vec3) -> f64 {
378 let du = first.x - second.x;
379 let dv = first.y - second.y;
380 (du * du + dv * dv).sqrt()
381}
382
383struct BoundaryImages {
387 bottom: NurbsCurve,
388 top: NurbsCurve,
389 t0: f64,
390 t1: f64,
391 dir: bool,
393}
394
395fn boundary_images(
412 base_affine: bool,
413 bottom: &NurbsSurface,
414 top: &NurbsSurface,
415 pcurve: &NurbsCurve,
416 eps_u: f64,
417 eps_v: f64,
418 fit_tolerance: f64,
419) -> Result<BoundaryImages, KernelRefusal> {
420 if base_affine {
421 let [q0, q1] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
422 return Ok(BoundaryImages {
423 bottom: affine_image_curve(bottom, pcurve).or_refuse(KernelStage::Refine, "affine_image_curve")?,
424 top: affine_image_curve(top, pcurve).or_refuse(KernelStage::Refine, "affine_image_curve")?,
425 t0: q0,
426 t1: q1,
427 dir: true,
428 });
429 }
430 if pcurve.degree == 1 && pcurve.control_points.len() == 2 {
431 let first = pcurve.control_points[0];
432 let second = pcurve.control_points[1];
433 if (first.w - second.w).abs() <= 1e-12 {
434 let (ua, va) = (first.x / first.w, first.y / first.w);
435 let (ub, vb) = (second.x / second.w, second.y / second.w);
436 if (ua - ub).abs() <= eps_u && (va - vb).abs() > eps_v {
437 let u_constant = (ua + ub) * 0.5;
438 return Ok(BoundaryImages {
439 bottom: bottom.iso_curve_u(u_constant).or_refuse(KernelStage::Refine, "iso_curve_u")?,
440 top: top.iso_curve_u(u_constant).or_refuse(KernelStage::Refine, "iso_curve_u")?,
441 t0: va.min(vb),
442 t1: va.max(vb),
443 dir: vb > va,
444 });
445 }
446 if (va - vb).abs() <= eps_v && (ua - ub).abs() > eps_u {
447 let v_constant = (va + vb) * 0.5;
448 return Ok(BoundaryImages {
449 bottom: bottom.iso_curve_v(v_constant).or_refuse(KernelStage::Refine, "iso_curve_v")?,
450 top: top.iso_curve_v(v_constant).or_refuse(KernelStage::Refine, "iso_curve_v")?,
451 t0: ua.min(ub),
452 t1: ua.max(ub),
453 dir: ub > ua,
454 });
455 }
456 }
457 }
458 let (bottom_image, top_image) =
461 image_curve_pair(bottom, top, pcurve, fit_tolerance, "thickenSheet").or_refuse(KernelStage::Refine, "image_curve_pair")?;
462 let forward = bottom_image.t0 <= bottom_image.t1;
463 Ok(BoundaryImages {
464 bottom: bottom_image.curve,
465 top: top_image.curve,
466 t0: bottom_image.t0.min(bottom_image.t1),
467 t1: bottom_image.t0.max(bottom_image.t1),
468 dir: forward,
469 })
470}
471
472pub fn thicken_trimmed_sheet(
519 surface: &NurbsSurface,
520 loops: &[Vec<NurbsCurve>],
521 thickness: f64,
522 symmetric: bool,
523) -> Result<Vec<BrepSolid>, KernelRefusal> {
524 if !thickness.is_finite() || thickness.abs() <= 1e-12 {
525 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_thickness", "thickenSheet: thickness must be a nonzero finite value"));
526 }
527 if loops.is_empty() || loops.iter().any(|loop_curves| loop_curves.is_empty()) {
528 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_no_loop", "thickenSheet: at least one non-empty pcurve loop is required"));
529 }
530 let (distance_bottom, distance_top) = if symmetric {
531 (-thickness.abs() * 0.5, thickness.abs() * 0.5)
532 } else if thickness > 0.0 {
533 (0.0, thickness)
534 } else {
535 (thickness, 0.0)
536 };
537 match band::recognize(surface, loops, distance_bottom, distance_top)? {
550 Some(band::Recognized::Full(recognized)) => return Ok(vec![band::build(&recognized)?]),
551 Some(band::Recognized::Partial(partial)) => return Ok(band::build_partial(&partial)?),
554 None => {}
555 }
556 if let Some(solid) = periodic::build(surface, loops, distance_bottom, distance_top)? {
557 return Ok(vec![solid]);
558 }
559 let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
560 let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
561 let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
562 let mut trim_min = [f64::INFINITY; 2];
570 let mut trim_max = [f64::NEG_INFINITY; 2];
571 if closed_u || closed_v {
572 for control in loops.iter().flatten().flat_map(|curve| &curve.control_points) {
573 let point = control.point().or_refuse(KernelStage::Collect, "point")?;
574 for (axis, value) in [point.x, point.y].into_iter().enumerate() {
575 trim_min[axis] = trim_min[axis].min(value);
576 trim_max[axis] = trim_max[axis].max(value);
577 }
578 }
579 }
580 let in_open_chart = |axis: usize, lo: f64, hi: f64| {
581 let epsilon = 1e-7 * (hi - lo);
582 trim_min[axis] >= lo - epsilon
583 && trim_max[axis] <= hi + epsilon
584 && trim_max[axis] - trim_min[axis] < hi - lo - 2.0 * epsilon
585 };
586 if (closed_u && !in_open_chart(0, u0, u1))
587 || (closed_v && !in_open_chart(1, v0, v1))
588 {
589 return Err(KernelRefusal::unsupported(KernelStage::Collect, "thicken_closed_sheet", "thickenSheet: closed sheets are not supported (split the patch at its seam first)"));
590 }
591 let uv_tolerance = 1e-7 * (u1 - u0).max(v1 - v0);
592 let minimum_area = 1e-10 * (u1 - u0) * (v1 - v0);
593 validate_trim_loops(loops, uv_tolerance, minimum_area)?;
598 let (carved, carve_scan) = match carve_folded_trim_scanned(
606 surface,
607 loops,
608 &[distance_bottom, distance_top],
609 FOLD_FACTOR,
610 ScanBudget::SHEET,
611 ) {
612 Ok(CarveOutcome { carved, scan, region }) => (carved, Some((scan, region))),
613 Err(why) => {
614 ensure_offsets_regular(surface, loops, &[distance_bottom, distance_top]).map_err(|refusal| {
621 refusal.with_message(|refusal| format!("{refusal}; the fold could not be carved out either: {why}"))
622 })?;
623 return Err(why.with_message(|why| format!("thickenSheet: {why}")));
624 }
625 };
626 let regions: Vec<Vec<Vec<NurbsCurve>>> = match &carved {
627 Some(trim) => {
628 let mut regions = Vec::with_capacity(trim.kept.len());
629 for piece in &trim.kept {
630 let kept = piece
631 .iter()
632 .map(|carved_loop| carved_loop.pcurves.clone())
633 .collect::<Vec<_>>();
634 validate_trim_loops(&kept, uv_tolerance, minimum_area)?;
639 regions.push(kept);
640 }
641 regions
642 }
643 None => {
644 match carve_scan {
649 Some((mut scan, region)) => {
650 ensure_scan_regular(surface, ®ion, &[distance_bottom, distance_top], &mut scan)?
651 }
652 None => ensure_offsets_regular(surface, loops, &[distance_bottom, distance_top])?,
653 }
654 vec![loops.to_vec()]
655 }
656 };
657 let mut bodies = Vec::with_capacity(regions.len());
664 for region_loops in ®ions {
665 bodies.push(thicken_one_region(
666 surface,
667 region_loops,
668 distance_bottom,
669 distance_top,
670 )?);
671 }
672
673 if let Some(trim) = &carved {
692 for swept in carve_dropped_sweep(
693 surface,
694 &trim.dropped,
695 &[distance_bottom, distance_top],
696 ScanBudget::SHEET,
697 )? {
698 if bodies.iter().any(|body| {
699 crate::classify_point(swept.point, body, uv_tolerance)
700 .map(|classification| classification.class != crate::PointClass::Out)
701 .unwrap_or(false)
702 }) {
703 continue;
704 }
705 return Err(KernelRefusal::unsupported(KernelStage::Refine, "thicken_fold_drop_sweeps_material", format!(
706 "thickenSheet: the fold covers part of the selected trim, and the part that \
707 would be dropped sweeps material the result does not contain — the normal at \
708 (u={:.6}, v={:.6}) reaches ({:.6}, {:.6}, {:.6}) at {:.0}% of the {:.6} offset, \
709 which is outside every body built from the {} kept piece(s). A carve may drop a \
710 part of the trim only when that part's own normals sweep material the kept \
711 construction contains; here they leave it, so the kept pieces bound less than \
712 the sheet sweeps and their volume is a form fitted to the construction. \
713 Refused rather than measured against it",
714 swept.uv[0],
715 swept.uv[1],
716 swept.point.x,
717 swept.point.y,
718 swept.point.z,
719 swept.fraction * 100.0,
720 swept.displacement,
721 bodies.len()
722 )));
723 }
724 }
725 Ok(bodies)
726}
727
728fn thicken_one_region(
735 surface: &NurbsSurface,
736 loops: &[Vec<NurbsCurve>],
737 distance_bottom: f64,
738 distance_top: f64,
739) -> Result<BrepSolid, KernelRefusal> {
740 let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
741 let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
742
743 let seam = periodic::seams(surface, loops)?;
744 let mut vertex_welds = Vec::new();
745 let mut edge_welds = Vec::new();
746 let bottom = offset_sheet(surface, loops, distance_bottom)?;
747 let top = offset_sheet(surface, loops, distance_top)?;
748 let (bottom, top) = crate::offset::unify_offset_sheet_bases(&bottom, &top)?;
752 let eps_u = 1e-9 * (u1 - u0);
753 let eps_v = 1e-9 * (v1 - v0);
754 let base_affine = surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?;
755 let sheet_points = bottom
762 .control_points
763 .iter()
764 .flatten()
765 .map(|control| control.point().or_refuse(KernelStage::Refine, "point"))
766 .collect::<Result<Vec<_>, KernelRefusal>>()?;
767 let sheet_scale = crate::model_scale(sheet_points.iter().copied());
768 let fit_tolerance =
769 crate::KernelTolerances::for_scale(crate::model_scale(sheet_points), 1e-7).intersection_fit;
770
771 let mut vertices: Vec<VertexRecord> = Vec::new();
772 let mut edges: Vec<EdgeRecord> = Vec::new();
773 let mut faces: Vec<FaceRecord> = Vec::new();
774 let mut top_cap_loops: Vec<LoopRecord> = Vec::new();
775 let mut bottom_cap_loops: Vec<LoopRecord> = Vec::new();
776 let mut bottom_junction_points: Vec<Vec3> = Vec::new();
777 let mut next_id = 1u64;
778
779 for loop_curves in loops.iter() {
780 let count = loop_curves.len();
781
782 let mut starts = Vec::with_capacity(count);
785 for curve in loop_curves {
786 let [q0, _] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
787 starts.push(curve.evaluate(q0).or_refuse(KernelStage::Refine, "evaluate")?);
788 }
789
790 let pole_band = 1e-9 * sheet_scale.max(1.0);
797 let mut pole = vec![false; count];
798 for (index, curve) in loop_curves.iter().enumerate() {
799 let [q0, q1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
800 let mut images = Vec::with_capacity(5);
801 for fraction in [0.0, 0.25, 0.5, 0.75, 1.0] {
802 let uv = curve.evaluate(q0 + (q1 - q0) * fraction).or_refuse(KernelStage::Refine, "evaluate")?;
803 images.push(surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Refine, "evaluate")?);
804 }
805 pole[index] = images.iter().all(|point| point.sub(images[0]).length() <= pole_band);
806 }
807 if pole.iter().all(|is_pole| *is_pole) {
808 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_degenerate_boundary", "thickenSheet: sheet boundary is degenerate (every boundary curve is a pole)"));
809 }
810 let mut junction_of: Vec<usize> = (0..count).collect();
813 let first_real = (0..count).find(|&j| !pole[(j + count - 1) % count]).unwrap_or(0);
814 for step in 1..count {
815 let j = (first_real + step) % count;
816 let previous = (j + count - 1) % count;
817 if pole[previous] {
818 junction_of[j] = junction_of[previous];
819 }
820 }
821
822 let mut bottom_vertex_ids = vec![0u64; count];
824 let mut top_vertex_ids = vec![0u64; count];
825 let mut bottom_points = vec![Vec3::default(); count];
826 let mut top_points = vec![Vec3::default(); count];
827 for (junction, start) in starts.iter().enumerate() {
828 if junction_of[junction] != junction {
829 continue;
830 }
831 let bottom_point = bottom.evaluate(start.x, start.y).or_refuse(KernelStage::Refine, "evaluate")?;
832 let top_point = top.evaluate(start.x, start.y).or_refuse(KernelStage::Refine, "evaluate")?;
833 vertices.push(VertexRecord {
834 id: next_id,
835 point: bottom_point,
836 });
837 bottom_vertex_ids[junction] = next_id;
838 next_id += 1;
839 vertices.push(VertexRecord {
840 id: next_id,
841 point: top_point,
842 });
843 top_vertex_ids[junction] = next_id;
844 next_id += 1;
845 bottom_points[junction] = bottom_point;
846 top_points[junction] = top_point;
847 if seam.is_none() || (start.x - u0).abs() <= eps_u {
850 bottom_junction_points.push(bottom_point);
851 }
852 }
853 for junction in 0..count {
854 let owner = junction_of[junction];
855 bottom_vertex_ids[junction] = bottom_vertex_ids[owner];
856 top_vertex_ids[junction] = top_vertex_ids[owner];
857 bottom_points[junction] = bottom_points[owner];
858 top_points[junction] = top_points[owner];
859 }
860
861 let mut images = Vec::with_capacity(count);
863 for curve in loop_curves {
864 images.push(boundary_images(
865 base_affine,
866 &bottom,
867 &top,
868 curve,
869 eps_u,
870 eps_v,
871 fit_tolerance,
872 )?);
873 }
874 let mut bottom_edge_ids = Vec::with_capacity(count);
875 let mut top_edge_ids = Vec::with_capacity(count);
876 for (index, image) in images.iter().enumerate() {
877 let next_index = (index + 1) % count;
878 let (start_j, end_j) = if image.dir {
879 (index, next_index)
880 } else {
881 (next_index, index)
882 };
883 edges.push(EdgeRecord {
884 id: next_id,
885 curve: image.bottom.clone(),
886 t0: image.t0,
887 t1: image.t1,
888 start_vertex_id: bottom_vertex_ids[start_j],
889 end_vertex_id: bottom_vertex_ids[end_j],
890 degenerate: pole[index],
891 name: None,
892 });
893 bottom_edge_ids.push(next_id);
894 next_id += 1;
895 edges.push(EdgeRecord {
896 id: next_id,
897 curve: image.top.clone(),
898 t0: image.t0,
899 t1: image.t1,
900 start_vertex_id: top_vertex_ids[start_j],
901 end_vertex_id: top_vertex_ids[end_j],
902 degenerate: pole[index],
903 name: None,
904 });
905 top_edge_ids.push(next_id);
906 next_id += 1;
907 }
908 let mut vertical_edge_ids = vec![0u64; count];
909 for junction in 0..count {
910 if junction_of[junction] != junction {
911 continue;
912 }
913 edges.push(EdgeRecord {
914 id: next_id,
915 curve: make_line(bottom_points[junction], top_points[junction]).or_refuse(KernelStage::Refine, "make_line")?,
916 t0: 0.0,
917 t1: 1.0,
918 start_vertex_id: bottom_vertex_ids[junction],
919 end_vertex_id: top_vertex_ids[junction],
920 degenerate: false,
921 name: None,
922 });
923 vertical_edge_ids[junction] = next_id;
924 next_id += 1;
925 }
926 for junction in 0..count {
927 vertical_edge_ids[junction] = vertical_edge_ids[junction_of[junction]];
928 }
929
930 if let Some([first, second]) = seam {
931 for fraction in 0..=16 {
934 let t = images[first].t0
935 + (images[first].t1 - images[first].t0) * fraction as f64 / 16.0;
936 for (a, b) in [
937 (&images[first].bottom, &images[second].bottom),
938 (&images[first].top, &images[second].top),
939 ] {
940 if a.evaluate(t).or_refuse(KernelStage::Sew, "evaluate")?
941 .sub(b.evaluate(t).or_refuse(KernelStage::Sew, "evaluate")?)
942 .length() > fit_tolerance
943 {
944 return Err(KernelRefusal::unsupported(KernelStage::Sew, "thicken_offset_seam", "thickenSheet: offset meridians do not close within the fit tolerance"));
945 }
946 }
947 }
948 edge_welds.push((bottom_edge_ids[second], bottom_edge_ids[first]));
949 edge_welds.push((top_edge_ids[second], top_edge_ids[first]));
950 for i in 0..count {
951 for j in i + 1..count {
952 if (starts[i].y - starts[j].y).abs() <= eps_v {
953 vertex_welds.push((bottom_vertex_ids[j], bottom_vertex_ids[i]));
954 vertex_welds.push((top_vertex_ids[j], top_vertex_ids[i]));
955 edge_welds.push((vertical_edge_ids[j], vertical_edge_ids[i]));
956 }
957 }
958 }
959 }
960
961 for (index, image) in images.iter().enumerate() {
971 if seam.is_some_and(|indices| indices.contains(&index)) || pole[index] {
972 continue;
973 }
974 let next_index = (index + 1) % count;
975 let wall = ruled_wall(&image.bottom, &image.top)?;
976 let (s_start, s_end) = if image.dir {
977 (image.t0, image.t1)
978 } else {
979 (image.t1, image.t0)
980 };
981 let mut coedges = Vec::with_capacity(4);
982 for (edge_id, forward, pcurve) in [
983 (
984 bottom_edge_ids[index],
985 image.dir,
986 parameter_line(s_start, 0.0, s_end, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
987 ),
988 (
989 vertical_edge_ids[next_index],
990 true,
991 parameter_line(s_end, 0.0, s_end, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
992 ),
993 (
994 top_edge_ids[index],
995 !image.dir,
996 parameter_line(s_end, 1.0, s_start, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
997 ),
998 (
999 vertical_edge_ids[index],
1000 false,
1001 parameter_line(s_start, 1.0, s_start, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1002 ),
1003 ] {
1004 coedges.push(CoedgeRecord {
1005 id: next_id,
1006 edge_id,
1007 forward,
1008 pcurve,
1009 });
1010 next_id += 1;
1011 }
1012 let loop_id = next_id;
1013 next_id += 1;
1014 faces.push(FaceRecord {
1015 id: next_id,
1016 surface: wall,
1017 same_sense: image.dir,
1018 loops: vec![LoopRecord {
1019 id: loop_id,
1020 coedges,
1021 }],
1022 name: None,
1023 });
1024 next_id += 1;
1025 }
1026
1027 let mut top_coedges = Vec::with_capacity(count);
1029 for (index, image) in images.iter().enumerate() {
1030 top_coedges.push(CoedgeRecord {
1031 id: next_id,
1032 edge_id: top_edge_ids[index],
1033 forward: image.dir,
1034 pcurve: loop_curves[index].clone(),
1035 });
1036 next_id += 1;
1037 }
1038 top_cap_loops.push(LoopRecord {
1039 id: next_id,
1040 coedges: top_coedges,
1041 });
1042 next_id += 1;
1043 let mut bottom_coedges = Vec::with_capacity(count);
1044 for index in (0..count).rev() {
1045 bottom_coedges.push(CoedgeRecord {
1046 id: next_id,
1047 edge_id: bottom_edge_ids[index],
1048 forward: !images[index].dir,
1049 pcurve: loop_curves[index].reversed().or_refuse(KernelStage::Refine, "reversed")?,
1050 });
1051 next_id += 1;
1052 }
1053 bottom_cap_loops.push(LoopRecord {
1054 id: next_id,
1055 coedges: bottom_coedges,
1056 });
1057 next_id += 1;
1058 }
1059
1060 let scale = crate::model_scale(bottom_junction_points.iter().copied());
1068 for first in 0..bottom_junction_points.len() {
1069 for second in first + 1..bottom_junction_points.len() {
1070 if bottom_junction_points[first]
1071 .sub(bottom_junction_points[second])
1072 .length()
1073 <= 1e-7 * scale
1074 {
1075 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_degenerate_boundary", "thickenSheet: sheet boundary is degenerate (coincident junction vertices)"));
1076 }
1077 }
1078 }
1079
1080 faces.push(FaceRecord {
1083 id: next_id,
1084 surface: top,
1085 same_sense: true,
1086 loops: top_cap_loops,
1087 name: None,
1088 });
1089 next_id += 1;
1090 faces.push(FaceRecord {
1091 id: next_id,
1092 surface: bottom,
1093 same_sense: false,
1094 loops: bottom_cap_loops,
1095 name: None,
1096 });
1097 next_id += 1;
1098
1099 for edge in &mut edges {
1102 for (from, to) in &vertex_welds {
1103 if edge.start_vertex_id == *from {
1104 edge.start_vertex_id = *to;
1105 }
1106 if edge.end_vertex_id == *from {
1107 edge.end_vertex_id = *to;
1108 }
1109 }
1110 }
1111 for coedge in faces.iter_mut()
1112 .flat_map(|f| &mut f.loops)
1113 .flat_map(|l| &mut l.coedges)
1114 {
1115 for (from, to) in &edge_welds {
1116 if coedge.edge_id == *from {
1117 coedge.edge_id = *to;
1118 }
1119 }
1120 }
1121 vertices.retain(|v| !vertex_welds.iter().any(|(from, _)| *from == v.id));
1122 edges.retain(|e| !edge_welds.iter().any(|(from, _)| *from == e.id));
1123 let shell_id = next_id;
1124 let solid = BrepSolid {
1125 mass_properties_cache: Default::default(),
1126 cells: Default::default(),
1127 id: next_id + 1,
1128 vertices,
1129 edges,
1130 shells: vec![ShellRecord {
1131 id: shell_id,
1132 faces,
1133 }],
1134 genus: loops.len() as i64 - 1 + i64::from(seam.is_some()),
1135 kind: crate::BodyKind::Solid,
1136 };
1137 let issues = solid.validate();
1138 if !issues.is_empty() {
1139 return Err(KernelRefusal::internal(KernelStage::Validate, "thicken_validate", format!(
1140 "thickenSheet: assembled solid failed validation: {issues:?}"
1141 )));
1142 }
1143 let volume = crate::solid_signed_volume(&solid).or_refuse(KernelStage::Refine, "solid_signed_volume")?;
1144 if volume <= 0.0 {
1145 return Err(KernelRefusal::internal(KernelStage::Validate, "thicken_orientation", format!(
1146 "thickenSheet: internal orientation error (signed volume {volume})"
1147 )));
1148 }
1149 crate::accept_sound(solid, "thickenSheet")
1157}
1158
1159pub fn thicken_face_sheet(
1173 surface: &NurbsSurface,
1174 thickness: f64,
1175 symmetric: bool,
1176) -> Result<Vec<BrepSolid>, KernelRefusal> {
1177 if !thickness.is_finite() || thickness.abs() <= 1e-12 {
1178 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_thickness", "thickenSheet: thickness must be a nonzero finite value"));
1179 }
1180 let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1181 let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1182 let rectangle = vec![
1183 parameter_line(u0, v0, u1, v0).or_refuse(KernelStage::Refine, "parameter_line")?,
1184 parameter_line(u1, v0, u1, v1).or_refuse(KernelStage::Refine, "parameter_line")?,
1185 parameter_line(u1, v1, u0, v1).or_refuse(KernelStage::Refine, "parameter_line")?,
1186 parameter_line(u0, v1, u0, v0).or_refuse(KernelStage::Refine, "parameter_line")?,
1187 ];
1188 thicken_trimmed_sheet(surface, &[rectangle], thickness, symmetric)
1189}
1190