1use super::*;
2
3pub fn blend_open_edge(
6 solid: &BrepSolid,
7 edge_id: u64,
8 radius: f64,
9 chamfer: bool,
10 name: Option<&str>,
11) -> Result<BrepSolid, String> {
12 if !(radius > 0.0) || !radius.is_finite() {
13 return Err("blend: radius must be positive".into());
14 }
15 blend_open_edge_impl(solid, edge_id, &|_| radius, chamfer, name)
16}
17
18pub(super) fn blend_open_edge_impl(
19 solid: &BrepSolid,
20 edge_id: u64,
21 radius_at: &dyn Fn(f64) -> f64,
22 chamfer: bool,
23 name: Option<&str>,
24) -> Result<BrepSolid, String> {
25 let edge = solid
26 .edges
27 .iter()
28 .find(|edge| edge.id == edge_id)
29 .ok_or_else(|| format!("blend: edge {edge_id} not found"))?;
30 if edge.start_vertex_id == edge.end_vertex_id {
31 return Err("blend: blend_open_edge requires an open edge".into());
32 }
33 let (first_face, first_loop, first_coedge) = locate_mate(solid, edge_id, None)?;
34 let (second_face, second_loop, second_coedge) =
35 locate_mate(solid, edge_id, Some((first_face.id, first_loop)))?;
36 let mid_radius = radius_at(edge.t0 + (edge.t1 - edge.t0) * 0.5);
37 let (rho1, rho2) = signed_radii(
38 edge,
39 first_face,
40 first_coedge,
41 second_face,
42 second_coedge,
43 mid_radius,
44 )?;
45 let first_mate = BlendMate {
46 face: first_face,
47 coedge: first_coedge,
48 loop_index: first_loop,
49 rho: rho1,
50 };
51 let second_mate = BlendMate {
52 face: second_face,
53 coedge: second_coedge,
54 loop_index: second_loop,
55 rho: rho2,
56 };
57 let debug = std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok();
65 if debug {
66 let sp = edge.curve.evaluate(edge.t0);
67 let ep = edge.curve.evaluate(edge.t1);
68 eprintln!(
69 "OPEN edge {} v{}->v{} first_face {} second_face {} p0={:?} p1={:?}",
70 edge.id,
71 edge.start_vertex_id,
72 edge.end_vertex_id,
73 first_face.id,
74 second_face.id,
75 sp,
76 ep
77 );
78 }
79 let compute = |overshoot_fraction: f64| -> Result<(FittedRows, Vec<EndSurgery>, bool), String> {
80 let (stations, _) = march_open_stations(
83 edge,
84 &first_mate,
85 &second_mate,
86 radius_at,
87 overshoot_fraction,
88 )?;
89 let parameters = station_parameters(&stations);
90 let rows = fit_open_rows(&stations, ¶meters, chamfer)?;
91 let mut ends = Vec::with_capacity(2);
92 let mut in_range = true;
93 for (vertex, at_start) in [(edge.start_vertex_id, true), (edge.end_vertex_id, false)] {
94 let (end, side_in_range) = resolve_free_end(
95 solid,
96 edge_id,
97 &first_mate,
98 &second_mate,
99 &rows,
100 vertex,
101 at_start,
102 )?;
103 if debug {
104 eprintln!(
105 " [os {overshoot_fraction:.2}] end v{vertex} at_start={at_start} end_face={} first_boundary={}(t={:.4}) second_boundary={}(t={:.4}) cr={:.4} cs={:.4} in_range={side_in_range}",
106 end.end_face_id,
107 end.first_edge_id,
108 end.first_edge_parameter,
109 end.second_edge_id,
110 end.second_edge_parameter,
111 end.cr_parameter,
112 end.cs_parameter,
113 );
114 }
115 in_range &= side_in_range;
116 ends.push(end);
117 }
118 Ok((rows, ends, in_range))
119 };
120
121 let seeded_fraction: Option<f64> = (|| {
131 let span = edge.t1 - edge.t0;
132 let mut length = 0.0f64;
133 let mut previous = edge.curve.evaluate(edge.t0).ok()?;
134 for index in 1..=16 {
135 let t = edge.t0 + span * index as f64 / 16.0;
136 let point = edge.curve.evaluate(t).ok()?;
137 length += point.sub(previous).length();
138 previous = point;
139 }
140 if !(length > 0.0) || !length.is_finite() {
141 return None;
142 }
143 let mid_t = edge.t0 + span * 0.5;
144 let uv1 = edge_uv_on_face(first_mate.coedge, edge, mid_t).ok()?;
145 let uv2 = edge_uv_on_face(second_mate.coedge, edge, mid_t).ok()?;
146 let n1 = raw_normal(&first_mate.face.surface, uv1[0], uv1[1]).ok()?;
147 let n2 = raw_normal(&second_mate.face.surface, uv2[0], uv2[1]).ok()?;
148 let cos_alpha =
149 (rho1.signum() * rho2.signum() * n1.dot(n2)).clamp(-1.0, 1.0);
150 let tan_half = ((1.0 - cos_alpha).max(0.0) / (1.0 + cos_alpha).max(1e-9)).sqrt();
153 let end_radius = radius_at(edge.t0).abs().max(radius_at(edge.t1).abs());
154 let fraction = (1.5 * end_radius * tan_half / length).clamp(0.08, 0.45);
155 if !fraction.is_finite() || (fraction - 0.08).abs() < 1e-12 {
156 return None;
157 }
158 Some(fraction)
159 })();
160
161 let mut chosen: Option<(FittedRows, Vec<EndSurgery>)> = None;
168 let mut last_error: Option<String> = None;
169 if let Some(fraction) = seeded_fraction {
170 match compute(fraction) {
171 Ok((rows, ends, true)) => chosen = Some((rows, ends)),
172 Ok(_) => {}
173 Err(error) => last_error = Some(error),
174 }
175 }
176 if chosen.is_none() {
177 for &overshoot_fraction in &[0.08f64, 0.16, 0.28, 0.45] {
178 match compute(overshoot_fraction) {
179 Ok((rows, ends, in_range)) => {
180 let fallback = chosen.is_none();
181 if in_range {
182 chosen = Some((rows, ends));
183 break;
184 } else if fallback {
185 chosen = Some((rows, ends));
186 }
187 }
188 Err(error) => last_error = Some(error),
189 }
190 }
191 }
192 let (rows, ends) = chosen.ok_or_else(|| {
193 last_error.unwrap_or_else(|| "blend: open march failed at every overshoot".into())
194 })?;
195
196 let [start_end, finish_end] = match <[EndSurgery; 2]>::try_from(ends) {
197 Ok(pair) => pair,
198 Err(_) => return Err("blend: open surgery needs exactly two ends".into()),
199 };
200 let mut crossings = SpokeCrossings::default();
204 crossings.note(start_end.escalated || finish_end.escalated);
205 let mut result = solid.clone();
206 let mut take_id = fresh_id_source(solid);
207 build_open_surgery(
208 solid,
209 &mut result,
210 &mut take_id,
211 edge,
212 &first_mate,
213 &second_mate,
214 rows,
215 [EndPlan::Free(start_end), EndPlan::Free(finish_end)],
216 name,
217 )?;
218 prune_orphan_vertices(&mut result);
219 crossings.gate(result)
220}
221
222pub(in crate::blend) fn fresh_id_source(solid: &BrepSolid) -> impl FnMut() -> u64 {
227 let mut next_id = solid
228 .vertices
229 .iter()
230 .map(|vertex| vertex.id)
231 .chain(solid.edges.iter().map(|edge| edge.id))
232 .chain(
233 solid
234 .shells
235 .iter()
236 .flat_map(|shell| &shell.faces)
237 .flat_map(|face| {
238 face.loops
239 .iter()
240 .map(|loop_record| loop_record.id)
241 .chain(face.loops.iter().flat_map(|loop_record| {
242 loop_record.coedges.iter().map(|coedge| coedge.id)
243 }))
244 .chain(std::iter::once(face.id))
245 }),
246 )
247 .max()
248 .unwrap_or(0)
249 + 1;
250 move || {
251 let id = next_id;
252 next_id += 1;
253 id
254 }
255}
256
257pub(in crate::blend) fn build_open_surgery(
269 solid: &BrepSolid,
270 result: &mut BrepSolid,
271 take_id: &mut dyn FnMut() -> u64,
272 edge: &EdgeRecord,
273 first: &BlendMate,
274 second: &BlendMate,
275 rows: FittedRows,
276 ends: [EndPlan; 2],
277 name: Option<&str>,
278) -> Result<SewnStripe, String> {
279 let [start_end, finish_end] = ends;
280
281 let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, String> {
283 let (_, tail) = row.split(a)?;
284 let (middle, _) = tail.split(b)?;
285 Ok(middle)
286 };
287 let kind = |plan: &EndPlan| match plan {
288 EndPlan::Free(_) => "free",
289 EndPlan::Corner(_) => "corner",
290 EndPlan::Miter(_) => "miter",
291 EndPlan::Cap(_) => "cap",
292 };
293 let describe_trim = |error: String| {
294 format!(
295 "{error} (edge {} rows trimmed for a {} start at cr {:.6}/cs {:.6} and a {} finish \
296 at cr {:.6}/cs {:.6})",
297 edge.id,
298 kind(&start_end),
299 start_end.cr_parameter(),
300 start_end.cs_parameter(),
301 kind(&finish_end),
302 finish_end.cr_parameter(),
303 finish_end.cs_parameter()
304 )
305 };
306 let cr = trim_row(&rows.cr, start_end.cr_parameter(), finish_end.cr_parameter())
307 .map_err(describe_trim)?;
308 let cr_pcurve = trim_row(
309 &rows.cr_pcurve,
310 start_end.cr_parameter(),
311 finish_end.cr_parameter(),
312 )?;
313 let cs = trim_row(&rows.cs, start_end.cs_parameter(), finish_end.cs_parameter())
314 .map_err(describe_trim)?;
315 let cs_pcurve = trim_row(
316 &rows.cs_pcurve,
317 start_end.cs_parameter(),
318 finish_end.cs_parameter(),
319 )?;
320
321 let cr_domain = cr.domain()?;
322 let cs_domain = cs.domain()?;
323 let old_start = edge.start_vertex_id;
324 let old_finish = edge.end_vertex_id;
325
326 let classify =
334 |boundary_id: u64, boundary_param: f64, corner: u64| -> Result<RimResolution, String> {
335 let boundary = result
336 .edges
337 .iter()
338 .find(|candidate| candidate.id == boundary_id)
339 .ok_or("blend: end boundary edge missing during surgery")?;
340 let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
341 let far_vertex = if boundary.start_vertex_id == corner {
342 Some((boundary.end_vertex_id, boundary.t1))
343 } else if boundary.end_vertex_id == corner {
344 Some((boundary.start_vertex_id, boundary.t0))
345 } else {
346 None
347 };
348 if let Some((far_id, _far_t)) = far_vertex {
349 let far_point = result
360 .vertices
361 .iter()
362 .find(|candidate| candidate.id == far_id)
363 .map(|candidate| candidate.point)
364 .ok_or("blend: consumed boundary far vertex missing")?;
365 let near_point = result
366 .vertices
367 .iter()
368 .find(|candidate| candidate.id == corner)
369 .map(|candidate| candidate.point)
370 .ok_or("blend: boundary corner vertex missing")?;
371 let crossing = boundary.curve.evaluate_extended(boundary_param)?;
372 let extent = far_point.sub(near_point).length();
373 let consumed_band = (1e-6 * (1.0 + extent)).max(1e-7);
374 let _ = span;
375 if crossing.sub(far_point).length() <= consumed_band {
376 if !result
378 .vertices
379 .iter()
380 .any(|candidate| candidate.id == far_id)
381 {
382 return Err("blend: consumed boundary far vertex missing".into());
383 }
384 return Ok(RimResolution::Consumed(far_id));
385 }
386 }
387 Ok(RimResolution::Fresh)
388 };
389 let resolve_side = |plan: &EndPlan,
393 side_first: bool,
394 corner: u64|
395 -> Result<RimResolution, String> {
396 if let Some(vertex) = plan.planned_rim(side_first) {
397 return Ok(RimResolution::Corner(vertex));
398 }
399 match plan {
400 EndPlan::Free(free) => classify(
401 if side_first {
402 free.first_edge_id
403 } else {
404 free.second_edge_id
405 },
406 if side_first {
407 free.first_edge_parameter
408 } else {
409 free.second_edge_parameter
410 },
411 corner,
412 ),
413 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => {
414 Err("blend: planned end without rim vertices".into())
415 }
416 }
417 };
418 let start_first = resolve_side(&start_end, true, old_start)?;
419 let start_second = resolve_side(&start_end, false, old_start)?;
420 let finish_first = resolve_side(&finish_end, true, old_finish)?;
421 let finish_second = resolve_side(&finish_end, false, old_finish)?;
422
423 let first_use_forward = first.coedge.forward;
426 let blend_cr_forward = !first_use_forward;
427
428 let (sew_first, sew_second) = match (start_end.free(), finish_end.free()) {
443 (Some(start_free), Some(finish_free)) => (
444 detect_row_coincidence(
445 solid,
446 first,
447 edge.id,
448 start_free.first_edge_id,
449 finish_free.first_edge_id,
450 start_first.is_consumed(),
451 start_first.existing().unwrap_or(0),
452 finish_first.is_consumed(),
453 finish_first.existing().unwrap_or(0),
454 &cr,
455 blend_cr_forward,
456 )?,
457 detect_row_coincidence(
458 solid,
459 second,
460 edge.id,
461 start_free.second_edge_id,
462 finish_free.second_edge_id,
463 start_second.is_consumed(),
464 start_second.existing().unwrap_or(0),
465 finish_second.is_consumed(),
466 finish_second.existing().unwrap_or(0),
467 &cs,
468 !blend_cr_forward,
469 )?,
470 ),
471 _ => (None, None),
472 };
473 let (sew_first, sew_second): (Option<RowSew>, Option<RowSew>) =
477 if first.face.id == second.face.id {
478 (None, None)
479 } else {
480 (sew_first, sew_second)
481 };
482
483 let mut resolve = |rim: &RimResolution, point: Result<Vec3, String>| -> Result<u64, String> {
486 match rim.existing() {
487 Some(id) => Ok(id),
488 None => {
489 let id = take_id();
490 result.vertices.push(VertexRecord { id, point: point? });
491 Ok(id)
492 }
493 }
494 };
495 let w1a = resolve(&start_first, cr.evaluate(cr_domain[0]))?;
496 let w1b = resolve(&finish_first, cr.evaluate(cr_domain[1]))?;
497 let w2a = resolve(&start_second, cs.evaluate(cs_domain[0]))?;
498 let w2b = resolve(&finish_second, cs.evaluate(cs_domain[1]))?;
499
500 let cr_edge_id = match &sew_first {
504 Some(sew) => sew.edge_id,
505 None => {
506 let id = take_id();
507 result.edges.push(EdgeRecord {
508 id,
509 curve: cr.clone(),
510 t0: cr_domain[0],
511 t1: cr_domain[1],
512 start_vertex_id: w1a,
513 end_vertex_id: w1b,
514 degenerate: false,
515 name: None,
516 });
517 id
518 }
519 };
520 let cs_edge_id = match &sew_second {
521 Some(sew) => sew.edge_id,
522 None => {
523 let id = take_id();
524 result.edges.push(EdgeRecord {
525 id,
526 curve: cs.clone(),
527 t0: cs_domain[0],
528 t1: cs_domain[1],
529 start_vertex_id: w2a,
530 end_vertex_id: w2b,
531 degenerate: false,
532 name: None,
533 });
534 id
535 }
536 };
537 let mut commit_end_edge = |plan: &EndPlan,
542 first_rim: u64,
543 second_rim: u64|
544 -> Result<Option<u64>, String> {
545 match plan {
546 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
547 EndPlan::Free(free) => {
548 let id = take_id();
549 let domain = free.transverse_curve.domain()?;
550 result.edges.push(EdgeRecord {
551 id,
552 curve: free.transverse_curve.clone(),
553 t0: domain[0],
554 t1: domain[1],
555 start_vertex_id: first_rim,
556 end_vertex_id: second_rim,
557 degenerate: false,
558 name: None,
559 });
560 Ok(Some(id))
561 }
562 }
563 };
564 let transverse_a_id = commit_end_edge(&start_end, w1a, w2a)?;
565 let transverse_b_id = commit_end_edge(&finish_end, w1b, w2b)?;
566
567 for (mate, new_edge_id, pcurve_forward, sewn) in [
572 (first, cr_edge_id, &cr_pcurve, sew_first.is_some()),
573 (second, cs_edge_id, &cs_pcurve, sew_second.is_some()),
574 ] {
575 if sewn {
576 continue;
577 }
578 let face = result
579 .shells
580 .iter_mut()
581 .flat_map(|shell| &mut shell.faces)
582 .find(|face| face.id == mate.face.id)
583 .ok_or("blend: mate face lost during surgery")?;
584 let loop_record = &mut face.loops[mate.loop_index];
585 let position = loop_record
586 .coedges
587 .iter()
588 .position(|coedge| coedge.edge_id == edge.id)
589 .ok_or("blend: edge coedge lost during surgery")?;
590 let old_forward = loop_record.coedges[position].forward;
591 loop_record.coedges[position] = CoedgeRecord {
592 id: loop_record.coedges[position].id,
593 edge_id: new_edge_id,
594 forward: old_forward,
595 pcurve: if old_forward {
596 pcurve_forward.clone()
597 } else {
598 pcurve_forward.reversed()?
599 },
600 };
601 }
602
603 for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
608 let EndPlan::Cap(cap) = plan else {
609 continue;
610 };
611 for (mate, rail_edge_id, leg, rim) in [
612 (first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
613 (second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
614 ] {
615 let face = result
616 .shells
617 .iter_mut()
618 .flat_map(|shell| &mut shell.faces)
619 .find(|face| face.id == mate.face.id)
620 .ok_or("blend: mate face lost during cap splice")?;
621 let loop_record = &mut face.loops[mate.loop_index];
622 let count = loop_record.coedges.len();
623 let rail_at = loop_record
624 .coedges
625 .iter()
626 .position(|coedge| coedge.edge_id == rail_edge_id)
627 .ok_or("blend: rail coedge lost during cap splice")?;
628 let rail_forward = loop_record.coedges[rail_at].forward;
634 let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
635 let (insert_at, forward, pcurve) = if end_is_traversal_end {
636 (rail_at + 1, true, leg.1.clone())
638 } else {
639 (rail_at, false, leg.1.reversed()?)
641 };
642 let _ = (count, rim);
643 loop_record.coedges.insert(
644 insert_at,
645 CoedgeRecord {
646 id: take_id(),
647 edge_id: leg.0,
648 forward,
649 pcurve,
650 },
651 );
652 }
653 }
654
655 let mut consumed_edges: Vec<u64> = Vec::new();
661 let start_free = start_end.free();
662 let finish_free = finish_end.free();
663 for (mate, sewn, sides) in [
664 (
665 first,
666 sew_first.is_some(),
667 [
668 (&start_first, start_free.map(|free| free.first_edge_id)),
669 (&finish_first, finish_free.map(|free| free.first_edge_id)),
670 ],
671 ),
672 (
673 second,
674 sew_second.is_some(),
675 [
676 (&start_second, start_free.map(|free| free.second_edge_id)),
677 (&finish_second, finish_free.map(|free| free.second_edge_id)),
678 ],
679 ),
680 ] {
681 for (cross, boundary_id) in sides {
682 if !cross.is_consumed() {
683 continue;
684 }
685 let Some(boundary_id) = boundary_id else {
686 continue;
687 };
688 if !sewn {
689 let face = result
690 .shells
691 .iter_mut()
692 .flat_map(|shell| &mut shell.faces)
693 .find(|face| face.id == mate.face.id)
694 .ok_or("blend: mate face lost during surgery")?;
695 face.loops[mate.loop_index]
696 .coedges
697 .retain(|coedge| coedge.edge_id != boundary_id);
698 }
699 if !consumed_edges.contains(&boundary_id) {
700 consumed_edges.push(boundary_id);
701 }
702 }
703 }
704 for sew in [&sew_first, &sew_second].into_iter().flatten() {
706 for id in &sew.collapsed_edges {
707 if !consumed_edges.contains(id) {
708 consumed_edges.push(*id);
709 }
710 }
711 }
712
713 let mut end_plans = Vec::with_capacity(2);
722 for (end, transverse_id, corner, first_consumed, second_consumed) in [
723 (
724 start_end.free(),
725 transverse_a_id,
726 old_start,
727 start_first.is_consumed(),
728 start_second.is_consumed(),
729 ),
730 (
731 finish_end.free(),
732 transverse_b_id,
733 old_finish,
734 finish_first.is_consumed(),
735 finish_second.is_consumed(),
736 ),
737 ] {
738 let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
739 continue;
740 };
741 let face = result
742 .shells
743 .iter()
744 .flat_map(|shell| &shell.faces)
745 .find(|face| face.id == end.end_face_id)
746 .ok_or("blend: end face lost during surgery")?;
747 let Some(located) = locate_end_corner(
750 &result,
751 face,
752 corner,
753 end.first_edge_id,
754 end.second_edge_id,
755 ) else {
756 return Err("blend: end-face corner (adjacent boundary coedges) not found".into());
757 };
758 for pole in &located.pole_edge_ids {
761 if !consumed_edges.contains(pole) {
762 consumed_edges.push(*pole);
763 }
764 }
765 end_plans.push((
766 end.end_face_id,
767 transverse_id,
768 end.transverse_end_pcurve.clone(),
769 located,
770 first_consumed,
771 second_consumed,
772 ));
773 }
774
775 for (rim_resolution, boundary, corner, rim) in [
778 (
779 &start_first,
780 start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
781 old_start,
782 w1a,
783 ),
784 (
785 &start_second,
786 start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
787 old_start,
788 w2a,
789 ),
790 (
791 &finish_first,
792 finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
793 old_finish,
794 w1b,
795 ),
796 (
797 &finish_second,
798 finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
799 old_finish,
800 w2b,
801 ),
802 ] {
803 if !rim_resolution.trims_boundary() {
804 continue;
805 }
806 let Some((boundary_id, boundary_param)) = boundary else {
807 continue;
808 };
809 trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
810 }
811
812 for (end_face, transverse_id, transverse_end_pcurve, located, first_consumed, second_consumed) in
815 end_plans
816 {
817 let forward = located.x_is_first;
818 let pcurve = if forward {
819 transverse_end_pcurve
820 } else {
821 transverse_end_pcurve.reversed()?
822 };
823 let (x_consumed, y_consumed) = if located.x_is_first {
824 (first_consumed, second_consumed)
825 } else {
826 (second_consumed, first_consumed)
827 };
828 let transverse_coedge = CoedgeRecord {
829 id: take_id(),
830 edge_id: transverse_id,
831 forward,
832 pcurve,
833 };
834 splice_end_corner(
835 result,
836 end_face,
837 &located,
838 transverse_coedge,
839 x_consumed,
840 y_consumed,
841 )?;
842 }
843
844 let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
846 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
847 let station_uv = cr_pcurve.evaluate(mid_u)?;
848 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
849 let out1 = if first.face.same_sense {
850 n1
851 } else {
852 n1.scale(-1.0)
853 };
854 let same_sense = blend_normal.dot(out1) >= 0.0;
855 let loop_id = take_id();
856 let cr_forward = sew_first
864 .as_ref()
865 .map_or(blend_cr_forward, |sew| sew.forward);
866 let cs_forward = sew_second
867 .as_ref()
868 .map_or(!blend_cr_forward, |sew| sew.forward);
869 let mut end_slot = |plan: &EndPlan,
879 free_edge_id: Option<u64>,
880 natural_forward: bool|
881 -> Result<Vec<CoedgeRecord>, String> {
882 match plan {
883 EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
884 id: take_id(),
885 edge_id: end.arc_edge_id,
886 forward: true,
887 pcurve: end.arc_blend_pcurve.clone(),
888 }]),
889 EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
890 id: take_id(),
891 edge_id: end.arc_edge_id,
892 forward: true,
893 pcurve: end.arc_blend_pcurve.clone(),
894 }]),
895 EndPlan::Miter(end) => end
896 .edges
897 .iter()
898 .map(|(edge_id, forward, pcurve)| {
899 Ok(CoedgeRecord {
900 id: take_id(),
901 edge_id: *edge_id,
902 forward: *forward,
903 pcurve: pcurve.clone(),
904 })
905 })
906 .collect(),
907 EndPlan::Free(free) => {
908 let edge_id =
909 free_edge_id.ok_or("blend: free end without its transverse edge")?;
910 Ok(vec![if natural_forward {
911 CoedgeRecord {
912 id: take_id(),
913 edge_id,
914 forward: true,
915 pcurve: free.transverse_blend_pcurve.clone(),
916 }
917 } else {
918 CoedgeRecord {
919 id: take_id(),
920 edge_id,
921 forward: false,
922 pcurve: free.transverse_blend_pcurve.reversed()?,
923 }
924 }])
925 }
926 }
927 };
928 let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
929 let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
930 let coedges = if blend_cr_forward {
931 let mut coedges = vec![CoedgeRecord {
932 id: take_id(),
933 edge_id: cr_edge_id,
934 forward: cr_forward,
935 pcurve: crate::sweep_topology::parameter_line(
936 cr_domain[0],
937 0.0,
938 cr_domain[1],
939 0.0,
940 )?,
941 }];
942 coedges.extend(finish_slot);
943 coedges.push(CoedgeRecord {
944 id: take_id(),
945 edge_id: cs_edge_id,
946 forward: cs_forward,
947 pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
948 });
949 coedges.extend(start_slot);
950 coedges
951 } else {
952 let mut coedges = vec![CoedgeRecord {
953 id: take_id(),
954 edge_id: cr_edge_id,
955 forward: cr_forward,
956 pcurve: crate::sweep_topology::parameter_line(
957 cr_domain[1],
958 0.0,
959 cr_domain[0],
960 0.0,
961 )?,
962 }];
963 coedges.extend(start_slot);
964 coedges.push(CoedgeRecord {
965 id: take_id(),
966 edge_id: cs_edge_id,
967 forward: cs_forward,
968 pcurve: crate::sweep_topology::parameter_line(
969 cs_domain[0],
970 1.0,
971 cs_domain[1],
972 1.0,
973 )?,
974 });
975 coedges.extend(finish_slot);
976 coedges
977 };
978 let blend_face_id = take_id();
979 let mut blend_face = FaceRecord {
980 id: blend_face_id,
981 surface: rows.surface,
982 same_sense,
983 loops: vec![LoopRecord {
984 id: loop_id,
985 coedges,
986 }],
987 name: name.map(|value| value.to_string()),
988 };
989 if rows.exact_extrusion {
990 transpose_face(&mut blend_face)?;
991 }
992 let shell_index = result
993 .shells
994 .iter()
995 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
996 .ok_or("blend: mate shell lost during surgery")?;
997 for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
1002 if sew.is_some() {
1003 for shell in &mut result.shells {
1004 shell.faces.retain(|face| face.id != mate.face.id);
1005 }
1006 }
1007 }
1008 result.shells[shell_index].faces.push(blend_face);
1009
1010 result.edges.retain(|candidate| candidate.id != edge.id);
1011 result
1014 .edges
1015 .retain(|candidate| !consumed_edges.contains(&candidate.id));
1016 Ok(SewnStripe {
1017 cr_edge_id,
1018 cs_edge_id,
1019 blend_face_id,
1020 })
1021}
1022
1023pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), String> {
1029 let surface = &face.surface;
1030 let rows_u = surface.control_points.len();
1031 let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
1032 let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
1033 for row in &surface.control_points {
1034 for (j, point) in row.iter().enumerate() {
1035 transposed[j].push(*point);
1036 }
1037 }
1038 face.surface = NurbsSurface::new(
1039 surface.degree_v,
1040 surface.degree_u,
1041 surface.knots_v.clone(),
1042 surface.knots_u.clone(),
1043 transposed,
1044 )?;
1045 for loop_record in &mut face.loops {
1046 for coedge in &mut loop_record.coedges {
1047 for control in &mut coedge.pcurve.control_points {
1048 std::mem::swap(&mut control.x, &mut control.y);
1049 }
1050 }
1051 }
1052 face.same_sense = !face.same_sense;
1053 Ok(())
1054}
1055
1056pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
1061 let used: rustc_hash::FxHashSet<u64> = result
1062 .edges
1063 .iter()
1064 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
1065 .collect();
1066 result
1067 .vertices
1068 .retain(|candidate| used.contains(&candidate.id));
1069}
1070
1071fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, String> {
1072 crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0)
1073}
1074
1075