1use crate::{KernelRefusal, KernelStage, OrRefuse};
31use crate::offset_carve::{carve_folded_trim_scanned, dropped_sweep as carve_dropped_sweep, CarveOutcome};
32use crate::offset_regularity::{scan_offset_regularity, Certainty, RegularityScan, ScanBudget, TrimRegion};
33use crate::sweep_topology::parameter_line;
34use crate::topology::{
35 BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
36};
37use crate::image_curve::{affine_image_curve, image_curve_pair};
38use crate::{make_arc, make_line, NurbsCurve, NurbsSurface, Vec3};
39
40#[path = "thicken/band.rs"]
41pub(crate) mod band;
42
43#[path = "thicken/periodic.rs"]
44mod periodic;
45
46pub(crate) const FOLD_FACTOR: f64 = 1e-6;
52
53fn ensure_offsets_regular(
68 surface: &NurbsSurface,
69 loops: &[Vec<NurbsCurve>],
70 distances: &[f64],
71) -> Result<(), KernelRefusal> {
72 let region = TrimRegion::from_pcurve_loops(surface, loops)
73 .map_err(|error| error.with_message(|error| format!("thickenSheet: {error}")))?;
74 let mut scan = scan_offset_regularity(surface, ®ion, distances, FOLD_FACTOR, ScanBudget::SHEET)
75 .map_err(|error| error.with_message(|error| format!("thickenSheet: {error}")))?;
76 ensure_scan_regular(surface, ®ion, distances, &mut scan)
77}
78
79fn ensure_scan_regular(
83 surface: &NurbsSurface,
84 region: &TrimRegion,
85 distances: &[f64],
86 scan: &mut RegularityScan,
87) -> Result<(), KernelRefusal> {
88 match scan.certify(surface, region, distances, FOLD_FACTOR) {
89 Certainty::Regular => Ok(()),
90 Certainty::Folded(worst) => Err(fold_refusal(surface, worst)),
91 Certainty::Uncertain {
92 unresolved,
93 budget_exhausted,
94 least,
95 } => Err(uncertain_refusal(scan, unresolved, budget_exhausted, least)),
96 }
97}
98
99fn uncertain_refusal(
104 scan: &RegularityScan,
105 unresolved: usize,
106 budget_exhausted: bool,
107 least: Option<crate::offset_regularity::FoldSample>,
108) -> KernelRefusal {
109 let because = match (unresolved, budget_exhausted) {
110 (0, _) => "the refinement budget ran out with prominent dips of the fold factor still \
111 unexplored"
112 .to_string(),
113 (cells, false) => format!(
114 "{cells} cell(s) were still risky at the scan's finest spacing — the fold factor \
115 sits within a hair of the level over an area no sampling can decide"
116 ),
117 (cells, true) => format!(
118 "{cells} cell(s) were still risky at the scan's finest spacing and the refinement \
119 budget ran out"
120 ),
121 };
122 let least = least
123 .map(|least| {
124 format!(
125 "; the least fold factor seen is {:.3e} at (u={:.6}, v={:.6}), concave curvature \
126 radius {:.6} against the offset distance {:.6}",
127 least.factor,
128 least.u,
129 least.v,
130 least.radius(),
131 least.displacement.abs()
132 )
133 })
134 .unwrap_or_default();
135 KernelRefusal::unsupported(KernelStage::Refine, "thicken_regularity_uncertain", format!(
136 "thickenSheet: the offset cannot be certified regular over the selected trim — none of \
137 {} samples collapsed, but {because}{least}",
138 scan.sampled
139 ))
140}
141
142fn fold_refusal(surface: &NurbsSurface, worst: crate::offset_regularity::FoldSample) -> KernelRefusal {
144 let where_3d = match surface.evaluate(worst.u, worst.v) {
145 Ok(point) => format!(
146 " — 3D ({:.6}, {:.6}, {:.6})",
147 point.x, point.y, point.z
148 ),
149 Err(_) => String::new(),
150 };
151 KernelRefusal::unsupported(KernelStage::Refine, "thicken_fold_inside_trim", format!(
152 "thickenSheet: offset by {:.6} self-intersects — inside the selected trim the sheet's \
153 concave curvature radius {:.6} at (u={:.6}, v={:.6}){} is not larger than the offset \
154 distance (fold factor {:.3e})",
155 worst.displacement,
156 worst.radius(),
157 worst.u,
158 worst.v,
159 where_3d,
160 worst.factor
161 ))
162}
163
164fn validate_trim_loops(
171 loops: &[Vec<NurbsCurve>],
172 uv_tolerance: f64,
173 minimum_area: f64,
174) -> Result<(), KernelRefusal> {
175 for (loop_index, loop_curves) in loops.iter().enumerate() {
176 let count = loop_curves.len();
177 let mut starts = Vec::with_capacity(count);
178 let mut ends = Vec::with_capacity(count);
179 for curve in loop_curves {
180 let [q0, q1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
181 starts.push(curve.evaluate(q0).or_refuse(KernelStage::Refine, "evaluate")?);
182 ends.push(curve.evaluate(q1).or_refuse(KernelStage::Refine, "evaluate")?);
183 }
184 for index in 0..count {
185 let next_index = (index + 1) % count;
186 let gap = planar_gap(ends[index], starts[next_index]);
187 if gap > uv_tolerance {
188 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_open_loop", format!(
189 "thickenSheet: loop {loop_index} is open — pcurve {index} ends at \
190 (u={:.6}, v={:.6}) but pcurve {next_index} starts at (u={:.6}, v={:.6}) \
191 (parameter-space gap {gap:.3e})",
192 ends[index].x, ends[index].y, starts[next_index].x, starts[next_index].y
193 )));
194 }
195 }
196 if count == 1 {
197 let [q0, q1] = loop_curves[0].domain().or_refuse(KernelStage::Refine, "domain")?;
198 let middle = loop_curves[0].evaluate((q0 + q1) * 0.5).or_refuse(KernelStage::Refine, "evaluate")?;
199 if planar_gap(middle, starts[0]) <= uv_tolerance {
200 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_degenerate_loop", format!(
201 "thickenSheet: loop {loop_index} is degenerate (zero parameter-space extent)"
202 )));
203 }
204 } else {
205 for index in 0..count {
206 if planar_gap(ends[index], starts[index]) <= uv_tolerance {
207 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_pinched_loop", format!(
208 "thickenSheet: pcurve {index} of loop {loop_index} closes on itself \
209 inside a multi-curve loop (pinched loop)"
210 )));
211 }
212 }
213 }
214 let mut area = 0.0;
215 for curve in loop_curves {
216 area += pcurve_signed_area(curve)?;
217 }
218 if loop_index == 0 {
219 if area <= minimum_area {
220 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_outer_winding", format!(
221 "thickenSheet: outer loop must run counter-clockwise in (u, v) \
222 (signed area {area:.3e})"
223 )));
224 }
225 } else if area >= -minimum_area {
226 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_hole_winding", format!(
227 "thickenSheet: hole loop {loop_index} must run clockwise in (u, v) \
228 (signed area {area:.3e})"
229 )));
230 }
231 }
232 Ok(())
233}
234
235fn offset_sheet(
258 surface: &NurbsSurface,
259 loops: &[Vec<NurbsCurve>],
260 distance: f64,
261) -> Result<NurbsSurface, KernelRefusal> {
262 if distance == 0.0 {
263 return Ok(surface.clone());
264 }
265 let carrier = FaceRecord {
266 id: 1,
267 surface: surface.clone(),
268 same_sense: true,
269 loops: loops
270 .iter()
271 .enumerate()
272 .map(|(loop_index, loop_curves)| LoopRecord {
273 id: loop_index as u64 + 1,
274 coedges: loop_curves
275 .iter()
276 .enumerate()
277 .map(|(index, pcurve)| CoedgeRecord {
278 id: index as u64 + 1,
282 edge_id: index as u64 + 1,
283 forward: true,
284 pcurve: pcurve.clone(),
285 })
286 .collect(),
287 })
288 .collect(),
289 name: None,
290 };
291 let (offset, lane) = crate::offset::offset_surface_with_lane(
292 &carrier,
293 -distance,
294 &crate::CarrierExtension::uniform(0.0),
295 )?;
296 if matches!(lane, crate::OffsetSurfaceLane::Reparameterised) {
297 return Err(KernelRefusal::unsupported(KernelStage::Refine, "thicken_reparameterised_carrier", format!(
298 "thickenSheet: the offset carrier for distance {distance:.6} was re-parameterized \
299 (the apex-cone pinch retrim pulled a sample row back to the offset's own apex), so \
300 the trim's pcurves no longer name the pointwise offset of the selected region — the \
301 selected trim reaches past the pinch and offsetting only its own sub-domain is not \
302 yet supported"
303 )));
304 }
305 Ok(offset)
306}
307
308fn ruled_wall(bottom: &NurbsCurve, top: &NurbsCurve) -> Result<NurbsSurface, KernelRefusal> {
314 if bottom.degree != top.degree
315 || bottom.knots.len() != top.knots.len()
316 || bottom
317 .knots
318 .iter()
319 .zip(&top.knots)
320 .any(|(a, b)| (a - b).abs() > 1e-12)
321 || bottom
322 .control_points
323 .iter()
324 .zip(&top.control_points)
325 .any(|(a, b)| (a.w - b.w).abs() > 1e-9)
326 {
327 return Err(KernelRefusal::internal(KernelStage::Refine, "thicken_basis_mismatch", "thickenSheet: internal error — offset sheet basis mismatch"));
328 }
329 let rows = bottom
330 .control_points
331 .iter()
332 .zip(&top.control_points)
333 .map(|(b, t)| vec![*b, *t])
334 .collect();
335 NurbsSurface::new(
336 bottom.degree,
337 1,
338 bottom.knots.clone(),
339 vec![0.0, 0.0, 1.0, 1.0],
340 rows,
341 )
342 .or_refuse(KernelStage::Refine, "new")
343}
344
345const GAUSS_X: [f64; 8] = [
346 -0.9602898564975363,
347 -0.7966664774136267,
348 -0.525532409916329,
349 -0.18343464249564978,
350 0.18343464249564978,
351 0.525532409916329,
352 0.7966664774136267,
353 0.9602898564975363,
354];
355const GAUSS_W: [f64; 8] = [
356 0.10122853629037669,
357 0.22238103445337445,
358 0.31370664587788727,
359 0.362683783378362,
360 0.362683783378362,
361 0.31370664587788727,
362 0.22238103445337445,
363 0.10122853629037669,
364];
365
366fn pcurve_signed_area(curve: &NurbsCurve) -> Result<f64, KernelRefusal> {
371 let [q0, q1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
372 let mut breaks = vec![q0];
373 for &knot in &curve.knots {
374 if knot > q0 + 1e-12 && knot < q1 - 1e-12 && (knot - breaks[breaks.len() - 1]).abs() > 1e-12
375 {
376 breaks.push(knot);
377 }
378 }
379 breaks.push(q1);
380 let mut area = 0.0;
381 for pair in breaks.windows(2) {
382 let half = (pair[1] - pair[0]) * 0.5;
383 let middle = (pair[1] + pair[0]) * 0.5;
384 for index in 0..GAUSS_X.len() {
385 let derivatives = curve.derivatives(middle + half * GAUSS_X[index], 1).or_refuse(KernelStage::Refine, "derivatives")?;
386 let point = derivatives[0];
387 let tangent = derivatives[1];
388 area += GAUSS_W[index] * half * 0.5 * (point.x * tangent.y - point.y * tangent.x);
389 }
390 }
391 Ok(area)
392}
393
394fn planar_gap(first: Vec3, second: Vec3) -> f64 {
397 let du = first.x - second.x;
398 let dv = first.y - second.y;
399 (du * du + dv * dv).sqrt()
400}
401
402struct BoundaryImages {
406 bottom: NurbsCurve,
407 top: NurbsCurve,
408 t0: f64,
409 t1: f64,
410 dir: bool,
412}
413
414fn boundary_images(
431 base_affine: bool,
432 bottom: &NurbsSurface,
433 top: &NurbsSurface,
434 pcurve: &NurbsCurve,
435 eps_u: f64,
436 eps_v: f64,
437 fit_tolerance: f64,
438) -> Result<BoundaryImages, KernelRefusal> {
439 if base_affine {
440 let [q0, q1] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
441 return Ok(BoundaryImages {
442 bottom: affine_image_curve(bottom, pcurve).or_refuse(KernelStage::Refine, "affine_image_curve")?,
443 top: affine_image_curve(top, pcurve).or_refuse(KernelStage::Refine, "affine_image_curve")?,
444 t0: q0,
445 t1: q1,
446 dir: true,
447 });
448 }
449 if pcurve.degree == 1 && pcurve.control_points.len() == 2 {
450 let first = pcurve.control_points[0];
451 let second = pcurve.control_points[1];
452 if (first.w - second.w).abs() <= 1e-12 {
453 let (ua, va) = (first.x / first.w, first.y / first.w);
454 let (ub, vb) = (second.x / second.w, second.y / second.w);
455 if (ua - ub).abs() <= eps_u && (va - vb).abs() > eps_v {
456 let u_constant = (ua + ub) * 0.5;
457 return Ok(BoundaryImages {
458 bottom: bottom.iso_curve_u(u_constant).or_refuse(KernelStage::Refine, "iso_curve_u")?,
459 top: top.iso_curve_u(u_constant).or_refuse(KernelStage::Refine, "iso_curve_u")?,
460 t0: va.min(vb),
461 t1: va.max(vb),
462 dir: vb > va,
463 });
464 }
465 if (va - vb).abs() <= eps_v && (ua - ub).abs() > eps_u {
466 let v_constant = (va + vb) * 0.5;
467 return Ok(BoundaryImages {
468 bottom: bottom.iso_curve_v(v_constant).or_refuse(KernelStage::Refine, "iso_curve_v")?,
469 top: top.iso_curve_v(v_constant).or_refuse(KernelStage::Refine, "iso_curve_v")?,
470 t0: ua.min(ub),
471 t1: ua.max(ub),
472 dir: ub > ua,
473 });
474 }
475 }
476 }
477 let (bottom_image, top_image) =
480 image_curve_pair(bottom, top, pcurve, fit_tolerance, "thickenSheet").or_refuse(KernelStage::Refine, "image_curve_pair")?;
481 let forward = bottom_image.t0 <= bottom_image.t1;
482 Ok(BoundaryImages {
483 bottom: bottom_image.curve,
484 top: top_image.curve,
485 t0: bottom_image.t0.min(bottom_image.t1),
486 t1: bottom_image.t0.max(bottom_image.t1),
487 dir: forward,
488 })
489}
490
491pub fn thicken_trimmed_sheet(
538 surface: &NurbsSurface,
539 loops: &[Vec<NurbsCurve>],
540 thickness: f64,
541 symmetric: bool,
542) -> Result<Vec<BrepSolid>, KernelRefusal> {
543 if !thickness.is_finite() || thickness.abs() <= 1e-12 {
544 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_thickness", "thickenSheet: thickness must be a nonzero finite value"));
545 }
546 if loops.is_empty() || loops.iter().any(|loop_curves| loop_curves.is_empty()) {
547 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_no_loop", "thickenSheet: at least one non-empty pcurve loop is required"));
548 }
549 let (distance_bottom, distance_top) = if symmetric {
550 (-thickness.abs() * 0.5, thickness.abs() * 0.5)
551 } else if thickness > 0.0 {
552 (0.0, thickness)
553 } else {
554 (thickness, 0.0)
555 };
556 match band::recognize(surface, loops, distance_bottom, distance_top)? {
569 Some(band::Recognized::Full(recognized)) => return Ok(vec![band::build(&recognized)?]),
570 Some(band::Recognized::Partial(partial)) => return Ok(band::build_partial(&partial)?),
573 None => {}
574 }
575 if let Some(solid) = periodic::build(surface, loops, distance_bottom, distance_top)? {
576 return Ok(vec![solid]);
577 }
578 let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
579 let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
580 let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
581 let mut trim_min = [f64::INFINITY; 2];
589 let mut trim_max = [f64::NEG_INFINITY; 2];
590 if closed_u || closed_v {
591 for control in loops.iter().flatten().flat_map(|curve| &curve.control_points) {
592 let point = control.point().or_refuse(KernelStage::Collect, "point")?;
593 for (axis, value) in [point.x, point.y].into_iter().enumerate() {
594 trim_min[axis] = trim_min[axis].min(value);
595 trim_max[axis] = trim_max[axis].max(value);
596 }
597 }
598 }
599 let in_open_chart = |axis: usize, lo: f64, hi: f64| {
600 let epsilon = 1e-7 * (hi - lo);
601 trim_min[axis] >= lo - epsilon
602 && trim_max[axis] <= hi + epsilon
603 && trim_max[axis] - trim_min[axis] < hi - lo - 2.0 * epsilon
604 };
605 if (closed_u && !in_open_chart(0, u0, u1))
606 || (closed_v && !in_open_chart(1, v0, v1))
607 {
608 return Err(KernelRefusal::unsupported(KernelStage::Collect, "thicken_closed_sheet", "thickenSheet: closed sheets are not supported (split the patch at its seam first)"));
609 }
610 let uv_tolerance = 1e-7 * (u1 - u0).max(v1 - v0);
611 let minimum_area = 1e-10 * (u1 - u0) * (v1 - v0);
612 validate_trim_loops(loops, uv_tolerance, minimum_area)?;
617 let (carved, carve_scan) = match carve_folded_trim_scanned(
625 surface,
626 loops,
627 &[distance_bottom, distance_top],
628 FOLD_FACTOR,
629 ScanBudget::SHEET,
630 ) {
631 Ok(CarveOutcome { carved, scan, region }) => (carved, Some((scan, region))),
632 Err(why) => {
633 ensure_offsets_regular(surface, loops, &[distance_bottom, distance_top]).map_err(|refusal| {
640 refusal.with_message(|refusal| format!("{refusal}; the fold could not be carved out either: {why}"))
641 })?;
642 return Err(why.with_message(|why| format!("thickenSheet: {why}")));
643 }
644 };
645 let regions: Vec<Vec<Vec<NurbsCurve>>> = match &carved {
646 Some(trim) => {
647 let mut regions = Vec::with_capacity(trim.kept.len());
648 for piece in &trim.kept {
649 let kept = piece
650 .iter()
651 .map(|carved_loop| carved_loop.pcurves.clone())
652 .collect::<Vec<_>>();
653 validate_trim_loops(&kept, uv_tolerance, minimum_area)?;
658 regions.push(kept);
659 }
660 regions
661 }
662 None => {
663 match carve_scan {
668 Some((mut scan, region)) => {
669 ensure_scan_regular(surface, ®ion, &[distance_bottom, distance_top], &mut scan)?
670 }
671 None => ensure_offsets_regular(surface, loops, &[distance_bottom, distance_top])?,
672 }
673 vec![loops.to_vec()]
674 }
675 };
676 let mut bodies = Vec::with_capacity(regions.len());
683 for region_loops in ®ions {
684 bodies.push(thicken_one_region(
685 surface,
686 region_loops,
687 distance_bottom,
688 distance_top,
689 )?);
690 }
691
692 if let Some(trim) = &carved {
711 for swept in carve_dropped_sweep(
712 surface,
713 &trim.dropped,
714 &[distance_bottom, distance_top],
715 ScanBudget::SHEET,
716 )? {
717 if bodies.iter().any(|body| {
718 crate::classify_point(swept.point, body, uv_tolerance)
719 .map(|classification| classification.class != crate::PointClass::Out)
720 .unwrap_or(false)
721 }) {
722 continue;
723 }
724 return Err(KernelRefusal::unsupported(KernelStage::Refine, "thicken_fold_drop_sweeps_material", format!(
725 "thickenSheet: the fold covers part of the selected trim, and the part that \
726 would be dropped sweeps material the result does not contain — the normal at \
727 (u={:.6}, v={:.6}) reaches ({:.6}, {:.6}, {:.6}) at {:.0}% of the {:.6} offset, \
728 which is outside every body built from the {} kept piece(s). A carve may drop a \
729 part of the trim only when that part's own normals sweep material the kept \
730 construction contains; here they leave it, so the kept pieces bound less than \
731 the sheet sweeps and their volume is a form fitted to the construction. \
732 Refused rather than measured against it",
733 swept.uv[0],
734 swept.uv[1],
735 swept.point.x,
736 swept.point.y,
737 swept.point.z,
738 swept.fraction * 100.0,
739 swept.displacement,
740 bodies.len()
741 )));
742 }
743 }
744 Ok(bodies)
745}
746
747fn thicken_one_region(
754 surface: &NurbsSurface,
755 loops: &[Vec<NurbsCurve>],
756 distance_bottom: f64,
757 distance_top: f64,
758) -> Result<BrepSolid, KernelRefusal> {
759 let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
760 let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
761
762 let seam = periodic::seams(surface, loops)?;
763 let mut vertex_welds = Vec::new();
764 let mut edge_welds = Vec::new();
765 let bottom = offset_sheet(surface, loops, distance_bottom)?;
766 let top = offset_sheet(surface, loops, distance_top)?;
767 let (bottom, top) = crate::offset::unify_offset_sheet_bases(&bottom, &top)?;
771 let eps_u = 1e-9 * (u1 - u0);
772 let eps_v = 1e-9 * (v1 - v0);
773 let base_affine = surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")?;
774 let sheet_points = bottom
781 .control_points
782 .iter()
783 .flatten()
784 .map(|control| control.point().or_refuse(KernelStage::Refine, "point"))
785 .collect::<Result<Vec<_>, KernelRefusal>>()?;
786 let fit_tolerance =
787 crate::KernelTolerances::for_scale(crate::model_scale(sheet_points), 1e-7).intersection_fit;
788
789 let mut vertices: Vec<VertexRecord> = Vec::new();
790 let mut edges: Vec<EdgeRecord> = Vec::new();
791 let mut faces: Vec<FaceRecord> = Vec::new();
792 let mut top_cap_loops: Vec<LoopRecord> = Vec::new();
793 let mut bottom_cap_loops: Vec<LoopRecord> = Vec::new();
794 let mut bottom_junction_points: Vec<Vec3> = Vec::new();
795 let mut next_id = 1u64;
796
797 for loop_curves in loops.iter() {
798 let count = loop_curves.len();
799
800 let mut starts = Vec::with_capacity(count);
803 for curve in loop_curves {
804 let [q0, _] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
805 starts.push(curve.evaluate(q0).or_refuse(KernelStage::Refine, "evaluate")?);
806 }
807
808 let mut bottom_vertex_ids = Vec::with_capacity(count);
810 let mut top_vertex_ids = Vec::with_capacity(count);
811 let mut bottom_points = Vec::with_capacity(count);
812 let mut top_points = Vec::with_capacity(count);
813 for start in &starts {
814 let bottom_point = bottom.evaluate(start.x, start.y).or_refuse(KernelStage::Refine, "evaluate")?;
815 let top_point = top.evaluate(start.x, start.y).or_refuse(KernelStage::Refine, "evaluate")?;
816 vertices.push(VertexRecord {
817 id: next_id,
818 point: bottom_point,
819 });
820 bottom_vertex_ids.push(next_id);
821 next_id += 1;
822 vertices.push(VertexRecord {
823 id: next_id,
824 point: top_point,
825 });
826 top_vertex_ids.push(next_id);
827 next_id += 1;
828 bottom_points.push(bottom_point);
829 top_points.push(top_point);
830 if seam.is_none() || (start.x - u0).abs() <= eps_u {
833 bottom_junction_points.push(bottom_point);
834 }
835 }
836
837 let mut images = Vec::with_capacity(count);
839 for curve in loop_curves {
840 images.push(boundary_images(
841 base_affine,
842 &bottom,
843 &top,
844 curve,
845 eps_u,
846 eps_v,
847 fit_tolerance,
848 )?);
849 }
850 let mut bottom_edge_ids = Vec::with_capacity(count);
851 let mut top_edge_ids = Vec::with_capacity(count);
852 for (index, image) in images.iter().enumerate() {
853 let next_index = (index + 1) % count;
854 let (start_j, end_j) = if image.dir {
855 (index, next_index)
856 } else {
857 (next_index, index)
858 };
859 edges.push(EdgeRecord {
860 id: next_id,
861 curve: image.bottom.clone(),
862 t0: image.t0,
863 t1: image.t1,
864 start_vertex_id: bottom_vertex_ids[start_j],
865 end_vertex_id: bottom_vertex_ids[end_j],
866 degenerate: false,
867 name: None,
868 });
869 bottom_edge_ids.push(next_id);
870 next_id += 1;
871 edges.push(EdgeRecord {
872 id: next_id,
873 curve: image.top.clone(),
874 t0: image.t0,
875 t1: image.t1,
876 start_vertex_id: top_vertex_ids[start_j],
877 end_vertex_id: top_vertex_ids[end_j],
878 degenerate: false,
879 name: None,
880 });
881 top_edge_ids.push(next_id);
882 next_id += 1;
883 }
884 let mut vertical_edge_ids = Vec::with_capacity(count);
885 for junction in 0..count {
886 edges.push(EdgeRecord {
887 id: next_id,
888 curve: make_line(bottom_points[junction], top_points[junction]).or_refuse(KernelStage::Refine, "make_line")?,
889 t0: 0.0,
890 t1: 1.0,
891 start_vertex_id: bottom_vertex_ids[junction],
892 end_vertex_id: top_vertex_ids[junction],
893 degenerate: false,
894 name: None,
895 });
896 vertical_edge_ids.push(next_id);
897 next_id += 1;
898 }
899
900 if let Some([first, second]) = seam {
901 for fraction in 0..=16 {
904 let t = images[first].t0
905 + (images[first].t1 - images[first].t0) * fraction as f64 / 16.0;
906 for (a, b) in [
907 (&images[first].bottom, &images[second].bottom),
908 (&images[first].top, &images[second].top),
909 ] {
910 if a.evaluate(t).or_refuse(KernelStage::Sew, "evaluate")?
911 .sub(b.evaluate(t).or_refuse(KernelStage::Sew, "evaluate")?)
912 .length() > fit_tolerance
913 {
914 return Err(KernelRefusal::unsupported(KernelStage::Sew, "thicken_offset_seam", "thickenSheet: offset meridians do not close within the fit tolerance"));
915 }
916 }
917 }
918 edge_welds.push((bottom_edge_ids[second], bottom_edge_ids[first]));
919 edge_welds.push((top_edge_ids[second], top_edge_ids[first]));
920 for i in 0..count {
921 for j in i + 1..count {
922 if (starts[i].y - starts[j].y).abs() <= eps_v {
923 vertex_welds.push((bottom_vertex_ids[j], bottom_vertex_ids[i]));
924 vertex_welds.push((top_vertex_ids[j], top_vertex_ids[i]));
925 edge_welds.push((vertical_edge_ids[j], vertical_edge_ids[i]));
926 }
927 }
928 }
929 }
930
931 for (index, image) in images.iter().enumerate() {
941 if seam.is_some_and(|indices| indices.contains(&index)) {
942 continue;
943 }
944 let next_index = (index + 1) % count;
945 let wall = ruled_wall(&image.bottom, &image.top)?;
946 let (s_start, s_end) = if image.dir {
947 (image.t0, image.t1)
948 } else {
949 (image.t1, image.t0)
950 };
951 let mut coedges = Vec::with_capacity(4);
952 for (edge_id, forward, pcurve) in [
953 (
954 bottom_edge_ids[index],
955 image.dir,
956 parameter_line(s_start, 0.0, s_end, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
957 ),
958 (
959 vertical_edge_ids[next_index],
960 true,
961 parameter_line(s_end, 0.0, s_end, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
962 ),
963 (
964 top_edge_ids[index],
965 !image.dir,
966 parameter_line(s_end, 1.0, s_start, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
967 ),
968 (
969 vertical_edge_ids[index],
970 false,
971 parameter_line(s_start, 1.0, s_start, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
972 ),
973 ] {
974 coedges.push(CoedgeRecord {
975 id: next_id,
976 edge_id,
977 forward,
978 pcurve,
979 });
980 next_id += 1;
981 }
982 let loop_id = next_id;
983 next_id += 1;
984 faces.push(FaceRecord {
985 id: next_id,
986 surface: wall,
987 same_sense: image.dir,
988 loops: vec![LoopRecord {
989 id: loop_id,
990 coedges,
991 }],
992 name: None,
993 });
994 next_id += 1;
995 }
996
997 let mut top_coedges = Vec::with_capacity(count);
999 for (index, image) in images.iter().enumerate() {
1000 top_coedges.push(CoedgeRecord {
1001 id: next_id,
1002 edge_id: top_edge_ids[index],
1003 forward: image.dir,
1004 pcurve: loop_curves[index].clone(),
1005 });
1006 next_id += 1;
1007 }
1008 top_cap_loops.push(LoopRecord {
1009 id: next_id,
1010 coedges: top_coedges,
1011 });
1012 next_id += 1;
1013 let mut bottom_coedges = Vec::with_capacity(count);
1014 for index in (0..count).rev() {
1015 bottom_coedges.push(CoedgeRecord {
1016 id: next_id,
1017 edge_id: bottom_edge_ids[index],
1018 forward: !images[index].dir,
1019 pcurve: loop_curves[index].reversed().or_refuse(KernelStage::Refine, "reversed")?,
1020 });
1021 next_id += 1;
1022 }
1023 bottom_cap_loops.push(LoopRecord {
1024 id: next_id,
1025 coedges: bottom_coedges,
1026 });
1027 next_id += 1;
1028 }
1029
1030 let scale = crate::model_scale(bottom_junction_points.iter().copied());
1038 for first in 0..bottom_junction_points.len() {
1039 for second in first + 1..bottom_junction_points.len() {
1040 if bottom_junction_points[first]
1041 .sub(bottom_junction_points[second])
1042 .length()
1043 <= 1e-7 * scale
1044 {
1045 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_degenerate_boundary", "thickenSheet: sheet boundary is degenerate (coincident junction vertices)"));
1046 }
1047 }
1048 }
1049
1050 faces.push(FaceRecord {
1053 id: next_id,
1054 surface: top,
1055 same_sense: true,
1056 loops: top_cap_loops,
1057 name: None,
1058 });
1059 next_id += 1;
1060 faces.push(FaceRecord {
1061 id: next_id,
1062 surface: bottom,
1063 same_sense: false,
1064 loops: bottom_cap_loops,
1065 name: None,
1066 });
1067 next_id += 1;
1068
1069 for edge in &mut edges {
1072 for (from, to) in &vertex_welds {
1073 if edge.start_vertex_id == *from {
1074 edge.start_vertex_id = *to;
1075 }
1076 if edge.end_vertex_id == *from {
1077 edge.end_vertex_id = *to;
1078 }
1079 }
1080 }
1081 for coedge in faces.iter_mut()
1082 .flat_map(|f| &mut f.loops)
1083 .flat_map(|l| &mut l.coedges)
1084 {
1085 for (from, to) in &edge_welds {
1086 if coedge.edge_id == *from {
1087 coedge.edge_id = *to;
1088 }
1089 }
1090 }
1091 vertices.retain(|v| !vertex_welds.iter().any(|(from, _)| *from == v.id));
1092 edges.retain(|e| !edge_welds.iter().any(|(from, _)| *from == e.id));
1093 let shell_id = next_id;
1094 let solid = BrepSolid {
1095 mass_properties_cache: Default::default(),
1096 cells: Default::default(),
1097 id: next_id + 1,
1098 vertices,
1099 edges,
1100 shells: vec![ShellRecord {
1101 id: shell_id,
1102 faces,
1103 }],
1104 genus: loops.len() as i64 - 1 + i64::from(seam.is_some()),
1105 kind: crate::BodyKind::Solid,
1106 };
1107 let issues = solid.validate();
1108 if !issues.is_empty() {
1109 return Err(KernelRefusal::internal(KernelStage::Validate, "thicken_validate", format!(
1110 "thickenSheet: assembled solid failed validation: {issues:?}"
1111 )));
1112 }
1113 let volume = crate::solid_signed_volume(&solid).or_refuse(KernelStage::Refine, "solid_signed_volume")?;
1114 if volume <= 0.0 {
1115 return Err(KernelRefusal::internal(KernelStage::Validate, "thicken_orientation", format!(
1116 "thickenSheet: internal orientation error (signed volume {volume})"
1117 )));
1118 }
1119 crate::accept_sound(solid, "thickenSheet")
1127}
1128
1129pub fn thicken_face_sheet(
1143 surface: &NurbsSurface,
1144 thickness: f64,
1145 symmetric: bool,
1146) -> Result<Vec<BrepSolid>, KernelRefusal> {
1147 if !thickness.is_finite() || thickness.abs() <= 1e-12 {
1148 return Err(KernelRefusal::input(KernelStage::Collect, "thicken_thickness", "thickenSheet: thickness must be a nonzero finite value"));
1149 }
1150 let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1151 let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1152 let rectangle = vec![
1153 parameter_line(u0, v0, u1, v0).or_refuse(KernelStage::Refine, "parameter_line")?,
1154 parameter_line(u1, v0, u1, v1).or_refuse(KernelStage::Refine, "parameter_line")?,
1155 parameter_line(u1, v1, u0, v1).or_refuse(KernelStage::Refine, "parameter_line")?,
1156 parameter_line(u0, v1, u0, v0).or_refuse(KernelStage::Refine, "parameter_line")?,
1157 ];
1158 thicken_trimmed_sheet(surface, &[rectangle], thickness, symmetric)
1159}
1160