1use super::debug_enabled;
21use super::pipeline::{offset_body, Body};
22use super::{OffsetFaceRole, OffsetShellFaceImageRecord, OffsetShellResultRecord};
23use crate::classification::PolygonClass;
24use crate::topology::{BrepSolid, EdgeRecord, FaceRecord};
25use crate::{
26 boolean_operation, fillet_edges, project_point_to_surface, BooleanOperation, BooleanOptions,
27 KernelRefusal, KernelStage, OrRefuse, RefusalClass, Vec2, Vec3,
28};
29use rustc_hash::FxHashMap as HashMap;
30use serde::{Deserialize, Serialize};
31
32#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
34#[serde(rename_all = "lowercase")]
35pub enum OffsetCorners {
36 #[default]
37 Sharp,
38 Rounded,
41}
42
43pub fn offset_solid(
46 source: &BrepSolid,
47 distance: f64,
48 corners: OffsetCorners,
49) -> Result<OffsetShellResultRecord, KernelRefusal> {
50 if corners == OffsetCorners::Rounded && distance > 0.0 {
51 if let Some(grown) = grown_across_reflex_edges(source, distance) {
52 return Ok(grown);
53 }
54 }
55 let sharp = match offset_body(source, &[], -distance, Body::Whole, "offset_solid") {
56 Ok(sharp) => sharp,
57 Err(error) => {
58 if corners == OffsetCorners::Rounded {
59 if let Some(grown) = grown_by_pieces(source, distance) {
60 return Ok(grown);
61 }
62 }
63 return Err(error);
64 }
65 };
66 match corners {
67 OffsetCorners::Sharp => Ok(sharp),
68 OffsetCorners::Rounded => match round_offset_joins(sharp, distance) {
69 Ok(rounded) => Ok(rounded),
70 Err(error) => match crate::blend::convex_parallel::convex_polyhedron_parallel(source, distance) {
73 Ok(Some((solid, images))) if solid.validate().is_empty() => {
74 let face_images = images
75 .into_iter()
76 .map(|image| OffsetShellFaceImageRecord { role: OffsetFaceRole::Offset, source_face_id: image.unwrap_or(0) })
77 .collect();
78 Ok(OffsetShellResultRecord { solid, face_images })
79 }
80 _ => Err(error),
81 },
82 },
83 }
84}
85
86fn grow_by_pieces_enabled() -> bool {
88 switch!("BREP_OFFSET_GROW_BY_PIECES", true)
89}
90
91
92fn grown_by_pieces(source: &BrepSolid, distance: f64) -> Option<OffsetShellResultRecord> {
115 if !grow_by_pieces_enabled() {
116 return None;
117 }
118 match grown_by_pieces_impl(source, distance) {
119 Ok(result) => result,
120 Err(error) => {
121 os_debug!("grow by pieces declined: {error}");
122 None
123 }
124 }
125}
126
127fn grown_by_pieces_impl(source: &BrepSolid, distance: f64) -> Result<Option<OffsetShellResultRecord>, KernelRefusal> {
128 if distance < 0.0 {
129 return shrunk_by_pieces(source, -distance);
130 }
131 let Some(sets) = thin_pair_planes(source, distance, true)? else {
132 return Ok(None);
133 };
134 let mut answer = None;
135 for planes in &sets {
136 match grown_union(source, planes, distance) {
137 Ok(Some(solid)) => {
138 answer = Some(solid);
139 break;
140 }
141 Ok(None) => {}
142 Err(error) => os_debug!("grow by pieces: {} plane(s) declined: {error}", planes.len()),
143 }
144 }
145 let Some(solid) = answer else {
146 return Ok(None);
147 };
148 let face_images = solid
149 .shells
150 .iter()
151 .flat_map(|shell| &shell.faces)
152 .map(|_| OffsetShellFaceImageRecord { role: OffsetFaceRole::Offset, source_face_id: 0 })
153 .collect();
154 Ok(Some(OffsetShellResultRecord { solid, face_images }))
155}
156
157fn round_offset_joins(
165 sharp: OffsetShellResultRecord,
166 distance: f64,
167) -> Result<OffsetShellResultRecord, KernelRefusal> {
168 let wanted_convex = distance > 0.0;
169 let midpoints = sharp_join_midpoints(&sharp.solid, wanted_convex)?;
170 if midpoints.is_empty() {
171 return Ok(sharp);
172 }
173 let solid = fillet_edges(&sharp.solid, &midpoints, None, distance.abs(), false, None)
174 .map_err(|error| {
175 error.with_message(|message| {
176 format!(
177 "offset_solid: the rounded joins of the offset body could not be built \
178 ({} edge(s) at radius {:.6}): {message}",
179 midpoints.len(),
180 distance.abs()
181 )
182 })
183 })?;
184 let operands = sharp
185 .solid
186 .shells
187 .iter()
188 .flat_map(|shell| &shell.faces)
189 .zip(&sharp.face_images)
190 .map(|(face, image)| (face, image.clone()))
191 .collect::<Vec<_>>();
192 let cap = OffsetShellFaceImageRecord {
193 role: OffsetFaceRole::Offset,
194 source_face_id: 0,
195 };
196 let face_images = provenance_by_surface(&solid, &operands, Some(&cap))?;
197 Ok(OffsetShellResultRecord { solid, face_images })
198}
199
200fn provenance_by_surface(
208 result: &BrepSolid,
209 operands: &[(&FaceRecord, OffsetShellFaceImageRecord)],
210 unmatched: Option<&OffsetShellFaceImageRecord>,
211) -> Result<Vec<OffsetShellFaceImageRecord>, KernelRefusal> {
212 let band = 1e-6 * crate::solid_scale(result).max(1.0);
213 let mut images = Vec::new();
214 for face in result.shells.iter().flat_map(|shell| &shell.faces) {
215 let seeds = super::source_seeds(result, face.id)?;
216 let seed = seeds[0];
217 let point = face.surface.evaluate(seed.x, seed.y).or_refuse(KernelStage::Sew, "evaluate")?;
218 let mut found = None;
219 for (operand, image) in operands {
220 let Ok(foot) = project_point_to_surface(&operand.surface, point) else { continue };
221 if foot.distance > band {
222 continue;
223 }
224 let inside = super::parameter_point_in_face(operand, Vec2 { x: foot.u, y: foot.v }, 1e-8)
225 .is_ok_and(|class| class != PolygonClass::Outside);
226 if inside {
227 found = Some(image.clone());
228 break;
229 }
230 }
231 match found.or_else(|| unmatched.cloned()) {
232 Some(image) => images.push(image),
233 None => {
234 return Err(KernelRefusal::internal(
235 KernelStage::Sew,
236 "offset_shell_provenance_lost",
237 format!(
238 "offset_shell: result face {} at ({:.4}, {:.4}, {:.4}) lies on no operand face",
239 face.id, point.x, point.y, point.z
240 ),
241 ))
242 }
243 }
244 }
245 Ok(images)
246}
247
248fn sharp_join_midpoints(solid: &BrepSolid, convex: bool) -> Result<Vec<Vec3>, KernelRefusal> {
253 const TANGENT_COSINE: f64 = 1.0 - 1e-6;
255 let faces = solid
256 .shells
257 .iter()
258 .flat_map(|shell| &shell.faces)
259 .collect::<Vec<_>>();
260 let mut uses = HashMap::<u64, Vec<(&FaceRecord, bool)>>::default();
261 for face in &faces {
262 for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
263 uses.entry(coedge.edge_id).or_default().push((face, coedge.forward));
264 }
265 }
266 let mut midpoints = Vec::new();
267 for edge in &solid.edges {
268 if edge.degenerate {
269 continue;
270 }
271 let Some(pair) = uses.get(&edge.id) else { continue };
272 if pair.len() != 2 || pair[0].0.id == pair[1].0.id {
273 continue;
274 }
275 if edge_is_convex_join(edge, pair[0], pair[1].0, TANGENT_COSINE)? == Some(convex) {
276 midpoints.push(edge.curve.evaluate(0.5 * (edge.t0 + edge.t1)).or_refuse(KernelStage::Classify, "evaluate")?);
277 }
278 }
279 Ok(midpoints)
280}
281
282fn edge_is_convex_join(
288 edge: &EdgeRecord,
289 (first, first_forward): (&FaceRecord, bool),
290 second: &FaceRecord,
291 tangent_cosine: f64,
292) -> Result<Option<bool>, KernelRefusal> {
293 let t = 0.5 * (edge.t0 + edge.t1);
294 let derivatives = edge.curve.derivatives(t, 1).or_refuse(KernelStage::Classify, "derivatives")?;
295 let point = derivatives[0];
296 let mut tangent = derivatives[1].normalized().or_refuse(KernelStage::Classify, "normalized")?;
297 if !first_forward {
298 tangent = tangent.scale(-1.0);
299 }
300 let normal_a = outward_normal_at(first, point)?;
301 let normal_b = outward_normal_at(second, point)?;
302 if normal_a.dot(normal_b) >= tangent_cosine {
303 return Ok(None);
304 }
305 Ok(Some(normal_b.dot(normal_a.cross(tangent)) < 0.0))
306}
307
308fn outward_normal_at(face: &FaceRecord, point: Vec3) -> Result<Vec3, KernelRefusal> {
309 let foot = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Classify, "project_point_to_surface")?;
310 super::face_normal(face, foot.u, foot.v)
311}
312
313pub fn offset_shell_by_boolean(
330 source: &BrepSolid,
331 opening_face_ids: &[u64],
332 distance: f64,
333) -> Result<OffsetShellResultRecord, KernelRefusal> {
334 if opening_face_ids.is_empty() {
335 return Err(KernelRefusal::input(
336 KernelStage::Collect,
337 "offset_shell_no_opening",
338 "offset_shell: at least one opening face is required",
339 ));
340 }
341 let body = offset_body(source, opening_face_ids, distance, Body::Whole, "offset_shell_by_boolean")
342 .or_else(|error| {
343 if !direct_refusal_takes_the_boolean_lane(&error) {
344 return Err(error);
345 }
346 let result = super::joint_offset::build(source, opening_face_ids, distance);
347 if let Err(reason) = &result {
348 os_debug!("joint offset refused: {reason}");
349 }
350 result.map_err(|_| error)
351 })?;
352 let (kept, removed) = if distance > 0.0 { (source, &body.solid) } else { (&body.solid, source) };
353 let solid = boolean_operation(kept, removed, BooleanOperation::Subtract, &BooleanOptions::default())
354 .map_err(|error| {
355 error.with_message(|message| {
356 format!("offset_shell: the boolean lane's difference could not be built: {message}")
357 })
358 })?;
359 let mut operands = body
362 .solid
363 .shells
364 .iter()
365 .flat_map(|shell| &shell.faces)
366 .zip(&body.face_images)
367 .map(|(face, image)| (face, image.clone()))
368 .collect::<Vec<_>>();
369 operands.extend(source.shells.iter().flat_map(|shell| &shell.faces).map(|face| {
370 (
371 face,
372 OffsetShellFaceImageRecord {
373 role: OffsetFaceRole::Source,
374 source_face_id: face.id,
375 },
376 )
377 }));
378 let face_images = provenance_by_surface(&solid, &operands, None)?;
379 Ok(OffsetShellResultRecord { solid, face_images })
380}
381
382pub fn offset_shell_closed(source: &BrepSolid, distance: f64) -> Result<OffsetShellResultRecord, KernelRefusal> {
394 let body = offset_body(source, &[], distance, Body::Whole, "offset_shell_closed")?;
395 let (kept, removed) = if distance > 0.0 { (source, &body.solid) } else { (&body.solid, source) };
396 let solid = boolean_operation(kept, removed, BooleanOperation::Subtract, &BooleanOptions::default())
397 .map_err(|error| {
398 error.with_message(|message| format!("offset_shell: the closed hollow's difference could not be built: {message}"))
399 })?;
400 let volume = |solid: &BrepSolid| {
401 crate::solid_mass_properties(solid)
402 .map(|properties| properties.volume)
403 .or_refuse(KernelStage::Validate, "solid_mass_properties")
404 };
405 let expected = volume(kept)? - volume(removed)?;
406 let measured = volume(&solid)?;
407 if solid.shells.len() != 2 || !(expected > 0.0) || !((measured - expected).abs() <= 1e-9 * expected) {
408 return Err(KernelRefusal::internal(
409 KernelStage::Validate,
410 "offset_shell_closed_hollow",
411 format!(
412 "offset_shell: the closed hollow is not the band between its operands ({} shells, volume {measured} against {expected})",
413 solid.shells.len()
414 ),
415 ));
416 }
417 let mut operands = body
418 .solid
419 .shells
420 .iter()
421 .flat_map(|shell| &shell.faces)
422 .zip(&body.face_images)
423 .map(|(face, image)| (face, image.clone()))
424 .collect::<Vec<_>>();
425 operands.extend(source.shells.iter().flat_map(|shell| &shell.faces).map(|face| {
426 (face, OffsetShellFaceImageRecord { role: OffsetFaceRole::Source, source_face_id: face.id })
427 }));
428 let face_images = provenance_by_surface(&solid, &operands, None)?;
429 Ok(OffsetShellResultRecord { solid, face_images })
430}
431
432pub(super) fn direct_refusal_takes_the_boolean_lane(refusal: &KernelRefusal) -> bool {
441 match &refusal.class {
442 RefusalClass::UnsupportedGeometry { what } | RefusalClass::Internal { what } => {
443 what.starts_with("offset_shell_")
444 }
445 RefusalClass::NonIntegralGenus { .. } => {
446 refusal.message.starts_with("offset_shell")
447 || (bare_genus_takes_the_boolean_lane() && refusal.message.starts_with("boolean assembly:"))
448 }
449 RefusalClass::DegenerateArrangement { .. } => refusal.message.starts_with("offset_shell"),
450 _ => false,
451 }
452}
453
454fn bare_genus_takes_the_boolean_lane() -> bool {
463 crate::switch!("BREP_OFFSET_BARE_GENUS_BOOLEAN_LANE", true)
464}
465
466fn thin_pair_planes(solid: &BrepSolid, amount: f64, grow: bool) -> Result<Option<Vec<Vec<(Vec3, Vec3)>>>, KernelRefusal> {
477 let band = 1e-9 * crate::solid_scale(solid).max(1.0);
478 let mut faces: Vec<(Vec3, Vec3)> = Vec::new();
480 for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
481 let Some((point, normal)) = super::rim_welds::planar_surface_frame(&face.surface, band)? else {
482 continue;
483 };
484 let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
485 let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
486 let Ok(outward) = super::face_normal(face, 0.5 * (u0 + u1), 0.5 * (v0 + v1)) else {
487 continue;
488 };
489 faces.push((point, if outward.dot(normal) < 0.0 { normal.scale(-1.0) } else { normal }));
490 }
491 let mut directions: Vec<Vec3> = Vec::new();
492 let mut pairs: Vec<(usize, usize)> = Vec::new();
493 for (index, (point, normal)) in faces.iter().enumerate() {
494 for (offset, (other, other_normal)) in faces[index + 1..].iter().enumerate() {
495 if normal.dot(*other_normal) > -1.0 + 1e-12 {
496 continue;
497 }
498 let gap = other.sub(*point).dot(*normal);
502 let thin = if grow { gap > band && gap < 2.0 * amount } else { gap < -band && -gap < 2.0 * amount };
503 if thin {
504 pairs.push((index, index + 1 + offset));
505 if !directions.iter().any(|axis| axis.dot(*normal).abs() >= 1.0 - 1e-12) {
506 directions.push(*normal);
507 }
508 }
509 }
510 }
511 if directions.is_empty() {
512 return Ok(None);
513 }
514 let push = |planes: &mut Vec<(Vec3, Vec3)>, point: Vec3, normal: Vec3| {
515 let seen = planes
516 .iter()
517 .any(|(other, axis)| axis.dot(normal).abs() >= 1.0 - 1e-12 && point.sub(*other).dot(*axis).abs() <= band);
518 if !seen {
519 planes.push((point, normal));
520 }
521 };
522 let mut parallel: Vec<(Vec3, Vec3)> = Vec::new();
523 for (point, normal) in &faces {
524 if directions.iter().any(|axis| axis.dot(*normal).abs() >= 1.0 - 1e-12) {
525 push(&mut parallel, *point, *normal);
526 }
527 }
528 let mut paired: Vec<(Vec3, Vec3)> = Vec::new();
529 for (first, second) in &pairs {
530 push(&mut paired, faces[*first].0, faces[*first].1);
531 push(&mut paired, faces[*second].0, faces[*second].1);
532 }
533 let mut sets = Vec::new();
534 if parallel.len() <= 8 {
535 sets.push(parallel.clone());
536 }
537 if paired.len() <= 8 && paired.len() < parallel.len() {
538 sets.push(paired);
539 }
540 Ok((!sets.is_empty()).then_some(sets))
541}
542
543fn cut_by_planes(solid: &BrepSolid, planes: &[(Vec3, Vec3)]) -> Vec<BrepSolid> {
545 let mut pieces = vec![solid.clone()];
546 for (point, normal) in planes {
547 let mut next = Vec::with_capacity(pieces.len() * 2);
548 for piece in pieces {
549 match crate::split_solid_by_plane(&piece, *point, *normal) {
550 Ok((below, above)) => {
551 next.push(below);
552 next.push(above);
553 }
554 Err(_) => next.push(piece),
555 }
556 }
557 pieces = next;
558 }
559 pieces
560}
561
562fn shrunk_by_pieces(source: &BrepSolid, amount: f64) -> Result<Option<OffsetShellResultRecord>, KernelRefusal> {
574 let Some(sets) = thin_pair_planes(source, amount, false)? else {
575 return Ok(None);
576 };
577 let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
578 let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
579 for face in source.shells.iter().flat_map(|shell| &shell.faces) {
580 for control in face.surface.control_points.iter().flatten() {
581 let Ok(point) = control.point() else { return Ok(None) };
582 low = Vec3::new(low.x.min(point.x), low.y.min(point.y), low.z.min(point.z));
583 high = Vec3::new(high.x.max(point.x), high.y.max(point.y), high.z.max(point.z));
584 }
585 }
586 let margin = 3.0 * amount;
587 let corner = low.sub(Vec3::new(margin, margin, margin));
588 let size = high.sub(low).add(Vec3::new(2.0 * margin, 2.0 * margin, 2.0 * margin));
589 let frame = crate::make_box_brep(corner, size.x, size.y, size.z).or_refuse(KernelStage::Refine, "make_box_brep")?;
590 let options = BooleanOptions::default();
591 let outside = boolean_operation(&frame, source, BooleanOperation::Subtract, &options)?;
592 let mut answer = None;
593 for planes in &sets {
594 let attempt = grown_union(&outside, planes, amount).and_then(|reach| match reach {
595 Some(reach) => boolean_operation(source, &reach, BooleanOperation::Subtract, &options).map(Some),
596 None => Ok(None),
597 });
598 match attempt {
599 Ok(Some(solid)) if solid.validate().is_empty() => {
600 answer = Some(solid);
601 break;
602 }
603 Ok(_) => {}
604 Err(error) => os_debug!("shrink by pieces: {} plane(s) declined: {error}", planes.len()),
605 }
606 }
607 if answer.is_none() && shrink_by_subtraction_enabled() {
608 for planes in &sets {
609 match subtracted_one_by_one(source, &outside, planes, amount) {
610 Ok(Some(solid)) => {
611 answer = Some(solid);
612 break;
613 }
614 Ok(None) => {}
615 Err(error) => os_debug!("shrink by subtraction: {} plane(s) declined: {error}", planes.len()),
616 }
617 }
618 }
619 let Some(solid) = answer else {
620 return Ok(None);
621 };
622 let face_images = solid
623 .shells
624 .iter()
625 .flat_map(|shell| &shell.faces)
626 .map(|_| OffsetShellFaceImageRecord { role: OffsetFaceRole::Offset, source_face_id: 0 })
627 .collect();
628 Ok(Some(OffsetShellResultRecord { solid, face_images }))
629}
630
631fn grown_union(solid: &BrepSolid, planes: &[(Vec3, Vec3)], amount: f64) -> Result<Option<BrepSolid>, KernelRefusal> {
635 let Some(grown) = grown_pieces(solid, planes, amount)? else {
636 return Ok(None);
637 };
638 let union = crate::boolean_operation_nary(&grown, BooleanOperation::Union)?;
639 Ok(union.validate().is_empty().then_some(union))
640}
641
642fn grown_pieces(solid: &BrepSolid, planes: &[(Vec3, Vec3)], amount: f64) -> Result<Option<Vec<BrepSolid>>, KernelRefusal> {
645 let pieces = cut_by_planes(solid, planes);
646 if pieces.len() < 2 {
647 return Ok(None);
648 }
649 os_debug!("by pieces: {} plane(s) cut the body into {} piece(s)", planes.len(), pieces.len());
650 let mut grown = Vec::with_capacity(pieces.len());
651 for (index, piece) in pieces.iter().enumerate() {
652 let sharp = offset_body(piece, &[], -amount, Body::Whole, "offset_solid_piece")
653 .map_err(|error| error.with_message(|message| format!("piece {index}: {message}")))?;
654 grown.push(round_offset_joins(sharp, amount)?.solid);
655 }
656 Ok(Some(grown))
657}
658
659fn shrink_by_subtraction_enabled() -> bool {
669 switch!("BREP_OFFSET_SHRINK_BY_SUBTRACTION", true)
670}
671
672
673fn subtracted_one_by_one(source: &BrepSolid, outside: &BrepSolid, planes: &[(Vec3, Vec3)], amount: f64) -> Result<Option<BrepSolid>, KernelRefusal> {
674 let Some(grown) = grown_pieces(outside, planes, amount)? else {
675 return Ok(None);
676 };
677 let options = BooleanOptions::default();
678 let mut body = source.clone();
679 for (index, piece) in grown.iter().enumerate() {
680 body = boolean_operation(&body, piece, BooleanOperation::Subtract, &options)
681 .map_err(|error| error.with_message(|message| format!("subtracting grown piece {index}: {message}")))?;
682 }
683 Ok(body.validate().is_empty().then_some(body))
684}
685
686fn reflex_by_pieces_enabled() -> bool {
688 switch!("BREP_OFFSET_REFLEX_BY_PIECES", true)
689}
690
691
692fn grown_across_reflex_edges(source: &BrepSolid, distance: f64) -> Option<OffsetShellResultRecord> {
705 if !reflex_by_pieces_enabled() {
706 return None;
707 }
708 let attempt = || -> Result<Option<OffsetShellResultRecord>, KernelRefusal> {
709 let band = 1e-9 * crate::solid_scale(source).max(1.0);
710 let faces = source.shells.iter().flat_map(|shell| &shell.faces).collect::<Vec<_>>();
711 let mut uses = HashMap::<u64, Vec<(&FaceRecord, bool)>>::default();
712 for face in &faces {
713 for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
714 uses.entry(coedge.edge_id).or_default().push((face, coedge.forward));
715 }
716 }
717 let mut planes: Vec<(Vec3, Vec3)> = Vec::new();
718 for edge in &source.edges {
719 if edge.degenerate {
720 continue;
721 }
722 let Some(pair) = uses.get(&edge.id) else { continue };
723 if pair.len() != 2 || pair[0].0.id == pair[1].0.id {
724 continue;
725 }
726 if edge_is_convex_join(edge, pair[0], pair[1].0, 1.0 - 1e-6)? != Some(false) {
727 continue;
728 }
729 for (face, _) in pair {
730 let Some((point, normal)) = super::rim_welds::planar_surface_frame(&face.surface, band)? else {
731 continue;
732 };
733 let seen = planes
734 .iter()
735 .any(|(other, axis)| axis.dot(normal).abs() >= 1.0 - 1e-12 && point.sub(*other).dot(*axis).abs() <= band);
736 if !seen {
737 planes.push((point, normal));
738 }
739 }
740 }
741 if planes.is_empty() || planes.len() > 8 {
742 return Ok(None);
743 }
744 let mut sets = planes.iter().map(|plane| vec![*plane]).collect::<Vec<_>>();
747 if planes.len() > 1 {
748 sets.push(planes.clone());
749 }
750 for set in &sets {
751 match grown_union(source, set, distance) {
752 Ok(Some(solid)) => {
753 os_debug!("reflex by pieces: {} plane(s)", set.len());
754 let face_images = solid
755 .shells
756 .iter()
757 .flat_map(|shell| &shell.faces)
758 .map(|_| OffsetShellFaceImageRecord { role: OffsetFaceRole::Offset, source_face_id: 0 })
759 .collect();
760 return Ok(Some(OffsetShellResultRecord { solid, face_images }));
761 }
762 Ok(None) => {}
763 Err(error) => os_debug!("reflex by pieces: {} plane(s) declined: {error}", set.len()),
764 }
765 }
766 Ok(None)
767 };
768 match attempt() {
769 Ok(result) => result,
770 Err(error) => {
771 os_debug!("reflex by pieces declined: {error}");
772 None
773 }
774 }
775}