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, vertex_indices) = march_open_stations(
84 edge,
85 &first_mate,
86 &second_mate,
87 radius_at,
88 overshoot_fraction,
89 )?;
90 let parameters = station_parameters(&stations);
91 let rows = fit_open_rows(
92 &stations,
93 ¶meters,
94 chamfer,
95 Some(vertex_indices),
96 extrusion_direction(edge, &first_mate, &second_mate),
97 )?;
98 let mut ends = Vec::with_capacity(2);
99 let mut in_range = true;
100 for (vertex, at_start) in [(edge.start_vertex_id, true), (edge.end_vertex_id, false)] {
101 let (end, side_in_range) = resolve_free_end(
102 solid,
103 edge_id,
104 &first_mate,
105 &second_mate,
106 &rows,
107 vertex,
108 at_start,
109 )?;
110 if debug {
111 eprintln!(
112 " [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}",
113 end.end_face_id,
114 end.first_edge_id,
115 end.first_edge_parameter,
116 end.second_edge_id,
117 end.second_edge_parameter,
118 end.cr_parameter,
119 end.cs_parameter,
120 );
121 }
122 in_range &= side_in_range;
123 ends.push(end);
124 }
125 Ok((rows, ends, in_range))
126 };
127
128 let seeded_fraction: Option<f64> = (|| {
138 let span = edge.t1 - edge.t0;
139 let mut length = 0.0f64;
140 let mut previous = edge.curve.evaluate(edge.t0).ok()?;
141 for index in 1..=16 {
142 let t = edge.t0 + span * index as f64 / 16.0;
143 let point = edge.curve.evaluate(t).ok()?;
144 length += point.sub(previous).length();
145 previous = point;
146 }
147 if !(length > 0.0) || !length.is_finite() {
148 return None;
149 }
150 let mid_t = edge.t0 + span * 0.5;
151 let uv1 = edge_uv_on_face(first_mate.coedge, edge, mid_t).ok()?;
152 let uv2 = edge_uv_on_face(second_mate.coedge, edge, mid_t).ok()?;
153 let n1 = raw_normal(&first_mate.face.surface, uv1[0], uv1[1]).ok()?;
154 let n2 = raw_normal(&second_mate.face.surface, uv2[0], uv2[1]).ok()?;
155 let cos_alpha =
156 (rho1.signum() * rho2.signum() * n1.dot(n2)).clamp(-1.0, 1.0);
157 let tan_half = ((1.0 - cos_alpha).max(0.0) / (1.0 + cos_alpha).max(1e-9)).sqrt();
160 let end_radius = radius_at(edge.t0).abs().max(radius_at(edge.t1).abs());
161 let fraction = (1.5 * end_radius * tan_half / length).clamp(0.08, 0.45);
162 if !fraction.is_finite() || (fraction - 0.08).abs() < 1e-12 {
163 return None;
164 }
165 Some(fraction)
166 })();
167
168 let mut chosen: Option<(FittedRows, Vec<EndSurgery>)> = None;
175 let mut last_error: Option<String> = None;
176 if let Some(fraction) = seeded_fraction {
177 match compute(fraction) {
178 Ok((rows, ends, true)) => chosen = Some((rows, ends)),
179 Ok(_) => {}
180 Err(error) => last_error = Some(error),
181 }
182 }
183 if chosen.is_none() {
184 for &overshoot_fraction in &[0.08f64, 0.16, 0.28, 0.45] {
185 match compute(overshoot_fraction) {
186 Ok((rows, ends, in_range)) => {
187 let fallback = chosen.is_none();
188 if in_range {
189 chosen = Some((rows, ends));
190 break;
191 } else if fallback {
192 chosen = Some((rows, ends));
193 }
194 }
195 Err(error) => last_error = Some(error),
196 }
197 }
198 }
199 let (rows, ends) = chosen.ok_or_else(|| {
200 last_error.unwrap_or_else(|| "blend: open march failed at every overshoot".into())
201 })?;
202
203 let [start_end, finish_end] = match <[EndSurgery; 2]>::try_from(ends) {
204 Ok(pair) => pair,
205 Err(_) => return Err("blend: open surgery needs exactly two ends".into()),
206 };
207 let mut crossings = SpokeCrossings::default();
211 crossings.note(start_end.escalated || finish_end.escalated);
212 let mut result = solid.clone();
213 let mut take_id = fresh_id_source(solid);
214 let sewn = build_open_surgery(
215 solid,
216 &mut result,
217 &mut take_id,
218 edge,
219 &first_mate,
220 &second_mate,
221 rows,
222 [EndPlan::Free(start_end), EndPlan::Free(finish_end)],
223 name,
224 )?;
225 prune_orphan_vertices(&mut result);
226 check_snap_closure(solid, &result, sewn.snap)?;
227 crossings.gate(result)
228}
229
230pub(in crate::blend) fn fresh_id_source(solid: &BrepSolid) -> impl FnMut() -> u64 {
235 let mut next_id = solid
236 .vertices
237 .iter()
238 .map(|vertex| vertex.id)
239 .chain(solid.edges.iter().map(|edge| edge.id))
240 .chain(
241 solid
242 .shells
243 .iter()
244 .flat_map(|shell| &shell.faces)
245 .flat_map(|face| {
246 face.loops
247 .iter()
248 .map(|loop_record| loop_record.id)
249 .chain(face.loops.iter().flat_map(|loop_record| {
250 loop_record.coedges.iter().map(|coedge| coedge.id)
251 }))
252 .chain(std::iter::once(face.id))
253 }),
254 )
255 .max()
256 .unwrap_or(0)
257 + 1;
258 move || {
259 let id = next_id;
260 next_id += 1;
261 id
262 }
263}
264
265pub(in crate::blend) fn build_open_surgery(
277 solid: &BrepSolid,
278 result: &mut BrepSolid,
279 take_id: &mut dyn FnMut() -> u64,
280 edge: &EdgeRecord,
281 first: &BlendMate,
282 second: &BlendMate,
283 rows: FittedRows,
284 ends: [EndPlan; 2],
285 name: Option<&str>,
286) -> Result<SewnStripe, String> {
287 let [start_end, finish_end] = ends;
288
289 let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, String> {
296 let [low, high] = row.domain()?;
297 let tail = if a == low { row.clone() } else { row.split(a)?.1 };
298 if b == high {
299 return Ok(tail);
300 }
301 let (middle, _) = tail.split(b)?;
302 Ok(middle)
303 };
304 let kind = |plan: &EndPlan| match plan {
305 EndPlan::Free(_) => "free",
306 EndPlan::Corner(_) => "corner",
307 EndPlan::Miter(_) => "miter",
308 EndPlan::Cap(_) => "cap",
309 };
310 let describe_trim = |error: String| {
311 format!(
312 "{error} (edge {} rows trimmed for a {} start at cr {:.6}/cs {:.6} and a {} finish \
313 at cr {:.6}/cs {:.6})",
314 edge.id,
315 kind(&start_end),
316 start_end.cr_parameter(),
317 start_end.cs_parameter(),
318 kind(&finish_end),
319 finish_end.cr_parameter(),
320 finish_end.cs_parameter()
321 )
322 };
323 let band = consumed_band(solid);
333 let collapses = |row: &NurbsCurve, a: f64, b: f64| -> Result<bool, String> {
334 let from = row.evaluate(a)?;
335 Ok(row.evaluate(b)?.sub(from).length() <= band
336 && row.evaluate(0.5 * (a + b))?.sub(from).length() <= band)
337 };
338 let cr_collapsed = collapses(&rows.cr, start_end.cr_parameter(), finish_end.cr_parameter())?;
339 let cs_collapsed = collapses(&rows.cs, start_end.cs_parameter(), finish_end.cs_parameter())?;
340 if cr_collapsed || cs_collapsed {
341 let unsupported = if start_end.free().is_some() && finish_end.free().is_some() {
342 Some("neither end is a corner")
343 } else if first.face.id == second.face.id {
344 Some("both rails lie on one face")
345 } else if matches!(start_end, EndPlan::Cap(_)) || matches!(finish_end, EndPlan::Cap(_)) {
346 Some("a capped end has no corner to share the point with")
347 } else {
348 None
349 };
350 if let Some(reason) = unsupported {
351 return Err(format!(
352 "{RAIL_COLLAPSE_UNSUPPORTED} edge {}'s rail shrinks to a point and {reason}",
353 edge.id
354 ));
355 }
356 }
357 let trim_rail = |collapsed: bool,
358 row: &NurbsCurve,
359 pcurve: &NurbsCurve,
360 a: f64,
361 b: f64|
362 -> Result<Option<(NurbsCurve, NurbsCurve)>, String> {
363 if collapsed {
364 return Ok(None);
365 }
366 Ok(Some((trim_row(row, a, b).map_err(describe_trim)?, trim_row(pcurve, a, b)?)))
367 };
368 let cr_rail = trim_rail(
369 cr_collapsed,
370 &rows.cr,
371 &rows.cr_pcurve,
372 start_end.cr_parameter(),
373 finish_end.cr_parameter(),
374 )?;
375 let cs_rail = trim_rail(
376 cs_collapsed,
377 &rows.cs,
378 &rows.cs_pcurve,
379 start_end.cs_parameter(),
380 finish_end.cs_parameter(),
381 )?;
382 let cr_domain = match &cr_rail {
384 Some((cr, _)) => cr.domain()?,
385 None => [start_end.cr_parameter(), finish_end.cr_parameter()],
386 };
387 let cs_domain = match &cs_rail {
388 Some((cs, _)) => cs.domain()?,
389 None => [start_end.cs_parameter(), finish_end.cs_parameter()],
390 };
391 let cr_at = |parameter: f64| match &cr_rail {
393 Some((cr, _)) => cr.evaluate(parameter),
394 None => rows.cr.evaluate(parameter),
395 };
396 let cs_at = |parameter: f64| match &cs_rail {
397 Some((cs, _)) => cs.evaluate(parameter),
398 None => rows.cs.evaluate(parameter),
399 };
400 let old_start = edge.start_vertex_id;
401 let old_finish = edge.end_vertex_id;
402
403 let classify =
414 |boundary_id: u64, boundary_param: f64, corner: u64| -> Result<RimResolution, String> {
415 let boundary = result
416 .edges
417 .iter()
418 .find(|candidate| candidate.id == boundary_id)
419 .ok_or("blend: end boundary edge missing during surgery")?;
420 let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
421 let far_vertex = if boundary.start_vertex_id == corner {
422 Some((boundary.end_vertex_id, boundary.t1, boundary.t0))
423 } else if boundary.end_vertex_id == corner {
424 Some((boundary.start_vertex_id, boundary.t0, boundary.t1))
425 } else {
426 None
427 };
428 if let Some((far_id, far_t, near_t)) = far_vertex {
429 let far_point = result
440 .vertices
441 .iter()
442 .find(|candidate| candidate.id == far_id)
443 .map(|candidate| candidate.point)
444 .ok_or("blend: consumed boundary far vertex missing")?;
445 let crossing = boundary.curve.evaluate_extended(boundary_param)?;
446 let _ = span;
447 let past_far = crossing.sub(far_point).length();
448 if past_far <= band {
449 if !result
451 .vertices
452 .iter()
453 .any(|candidate| candidate.id == far_id)
454 {
455 return Err("blend: consumed boundary far vertex missing".into());
456 }
457 return Ok(RimResolution::Consumed(far_id));
458 }
459 if (boundary_param - far_t) * (far_t - near_t) > 0.0 {
472 return Err(format!(
473 "{BLEND_WIDER_THAN_FACE} its rail crosses edge {boundary_id} {past_far:.3e} \
474 past that edge's far vertex, and a rail within {band:.3e} of it is the \
475 most this surgery snaps onto the edge"
476 ));
477 }
478 }
479 Ok(RimResolution::Fresh)
480 };
481 let resolve_side = |plan: &EndPlan,
485 side_first: bool,
486 corner: u64|
487 -> Result<RimResolution, String> {
488 if let Some(vertex) = plan.planned_rim(side_first) {
489 return Ok(RimResolution::Corner(vertex));
490 }
491 match plan {
492 EndPlan::Free(free) => classify(
493 if side_first {
494 free.first_edge_id
495 } else {
496 free.second_edge_id
497 },
498 if side_first {
499 free.first_edge_parameter
500 } else {
501 free.second_edge_parameter
502 },
503 corner,
504 ),
505 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => {
506 Err("blend: planned end without rim vertices".into())
507 }
508 }
509 };
510 let start_first = resolve_side(&start_end, true, old_start)?;
511 let start_second = resolve_side(&start_end, false, old_start)?;
512 let finish_first = resolve_side(&finish_end, true, old_finish)?;
513 let finish_second = resolve_side(&finish_end, false, old_finish)?;
514
515 let first_use_forward = first.coedge.forward;
518 let blend_cr_forward = !first_use_forward;
519
520 let (sew_first, sew_second) = match (start_end.free(), finish_end.free(), &cr_rail, &cs_rail) {
535 (Some(start_free), Some(finish_free), Some((cr, _)), Some((cs, _))) => (
536 detect_row_coincidence(
537 solid,
538 first,
539 edge.id,
540 start_free.first_edge_id,
541 finish_free.first_edge_id,
542 start_first.is_consumed(),
543 start_first.existing().unwrap_or(0),
544 finish_first.is_consumed(),
545 finish_first.existing().unwrap_or(0),
546 cr,
547 blend_cr_forward,
548 band,
549 )?,
550 detect_row_coincidence(
551 solid,
552 second,
553 edge.id,
554 start_free.second_edge_id,
555 finish_free.second_edge_id,
556 start_second.is_consumed(),
557 start_second.existing().unwrap_or(0),
558 finish_second.is_consumed(),
559 finish_second.existing().unwrap_or(0),
560 cs,
561 !blend_cr_forward,
562 band,
563 )?,
564 ),
565 _ => (None, None),
566 };
567 let (sew_first, sew_second): (Option<RowSew>, Option<RowSew>) =
571 if first.face.id == second.face.id {
572 (None, None)
573 } else {
574 (sew_first, sew_second)
575 };
576
577 let mut resolve = |rim: &RimResolution, point: Result<Vec3, String>| -> Result<u64, String> {
580 match rim.existing() {
581 Some(id) => Ok(id),
582 None => {
583 let id = take_id();
584 result.vertices.push(VertexRecord { id, point: point? });
585 Ok(id)
586 }
587 }
588 };
589 let w1a = resolve(&start_first, cr_at(cr_domain[0]))?;
590 let w1b = resolve(&finish_first, cr_at(cr_domain[1]))?;
591 let w2a = resolve(&start_second, cs_at(cs_domain[0]))?;
592 let w2b = resolve(&finish_second, cs_at(cs_domain[1]))?;
593 let mut snap = [&sew_first, &sew_second]
596 .into_iter()
597 .flatten()
598 .fold(0.0f64, |worst, sew| worst.max(sew.deviation));
599 for (rim, vertex, row_end) in [
600 (&start_first, w1a, cr_at(cr_domain[0])?),
601 (&finish_first, w1b, cr_at(cr_domain[1])?),
602 (&start_second, w2a, cs_at(cs_domain[0])?),
603 (&finish_second, w2b, cs_at(cs_domain[1])?),
604 ] {
605 if !rim.is_consumed() {
606 continue;
607 }
608 let point = result
609 .vertices
610 .iter()
611 .find(|candidate| candidate.id == vertex)
612 .ok_or("blend: consumed rim vertex missing")?
613 .point;
614 snap = snap.max(row_end.sub(point).length());
615 }
616
617 let cr_edge_id = match (&sew_first, &cr_rail) {
622 (Some(sew), _) => Some(sew.edge_id),
623 (None, None) => None,
624 (None, Some((cr, _))) => {
625 let id = take_id();
626 result.edges.push(EdgeRecord {
627 id,
628 curve: cr.clone(),
629 t0: cr_domain[0],
630 t1: cr_domain[1],
631 start_vertex_id: w1a,
632 end_vertex_id: w1b,
633 degenerate: false,
634 name: None,
635 });
636 Some(id)
637 }
638 };
639 let cs_edge_id = match (&sew_second, &cs_rail) {
640 (Some(sew), _) => Some(sew.edge_id),
641 (None, None) => None,
642 (None, Some((cs, _))) => {
643 let id = take_id();
644 result.edges.push(EdgeRecord {
645 id,
646 curve: cs.clone(),
647 t0: cs_domain[0],
648 t1: cs_domain[1],
649 start_vertex_id: w2a,
650 end_vertex_id: w2b,
651 degenerate: false,
652 name: None,
653 });
654 Some(id)
655 }
656 };
657 let mut commit_end_edge = |plan: &EndPlan,
662 first_rim: u64,
663 second_rim: u64|
664 -> Result<Option<u64>, String> {
665 match plan {
666 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
667 EndPlan::Free(free) => {
668 let id = take_id();
669 let domain = free.transverse_curve.domain()?;
670 result.edges.push(EdgeRecord {
671 id,
672 curve: free.transverse_curve.clone(),
673 t0: domain[0],
674 t1: domain[1],
675 start_vertex_id: first_rim,
676 end_vertex_id: second_rim,
677 degenerate: false,
678 name: None,
679 });
680 Ok(Some(id))
681 }
682 }
683 };
684 let transverse_a_id = commit_end_edge(&start_end, w1a, w2a)?;
685 let transverse_b_id = commit_end_edge(&finish_end, w1b, w2b)?;
686
687 for (mate, rail, sewn) in [
694 (first, cr_edge_id.zip(cr_rail.as_ref()), sew_first.is_some()),
695 (second, cs_edge_id.zip(cs_rail.as_ref()), sew_second.is_some()),
696 ] {
697 if sewn {
698 continue;
699 }
700 let face = result
701 .shells
702 .iter_mut()
703 .flat_map(|shell| &mut shell.faces)
704 .find(|face| face.id == mate.face.id)
705 .ok_or("blend: mate face lost during surgery")?;
706 let loop_record = &mut face.loops[mate.loop_index];
707 let position = loop_record
708 .coedges
709 .iter()
710 .position(|coedge| coedge.edge_id == edge.id)
711 .ok_or("blend: edge coedge lost during surgery")?;
712 let Some((new_edge_id, (_, pcurve_forward))) = rail else {
713 loop_record.coedges.remove(position);
714 continue;
715 };
716 let old_forward = loop_record.coedges[position].forward;
717 loop_record.coedges[position] = CoedgeRecord {
718 id: loop_record.coedges[position].id,
719 edge_id: new_edge_id,
720 forward: old_forward,
721 pcurve: if old_forward {
722 pcurve_forward.clone()
723 } else {
724 pcurve_forward.reversed()?
725 },
726 };
727 }
728
729 for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
734 let EndPlan::Cap(cap) = plan else {
735 continue;
736 };
737 for (mate, rail_edge_id, leg, rim) in [
738 (first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
739 (second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
740 ] {
741 let rail_edge_id = rail_edge_id.ok_or("blend: a capped end lost its rail")?;
743 let face = result
744 .shells
745 .iter_mut()
746 .flat_map(|shell| &mut shell.faces)
747 .find(|face| face.id == mate.face.id)
748 .ok_or("blend: mate face lost during cap splice")?;
749 let loop_record = &mut face.loops[mate.loop_index];
750 let count = loop_record.coedges.len();
751 let rail_at = loop_record
752 .coedges
753 .iter()
754 .position(|coedge| coedge.edge_id == rail_edge_id)
755 .ok_or("blend: rail coedge lost during cap splice")?;
756 let rail_forward = loop_record.coedges[rail_at].forward;
762 let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
763 let (insert_at, forward, pcurve) = if end_is_traversal_end {
764 (rail_at + 1, true, leg.1.clone())
766 } else {
767 (rail_at, false, leg.1.reversed()?)
769 };
770 let _ = (count, rim);
771 loop_record.coedges.insert(
772 insert_at,
773 CoedgeRecord {
774 id: take_id(),
775 edge_id: leg.0,
776 forward,
777 pcurve,
778 },
779 );
780 }
781 }
782
783 let mut consumed_edges: Vec<u64> = Vec::new();
789 let start_free = start_end.free();
790 let finish_free = finish_end.free();
791 for (mate, sewn, sides) in [
792 (
793 first,
794 sew_first.is_some(),
795 [
796 (&start_first, start_free.map(|free| free.first_edge_id)),
797 (&finish_first, finish_free.map(|free| free.first_edge_id)),
798 ],
799 ),
800 (
801 second,
802 sew_second.is_some(),
803 [
804 (&start_second, start_free.map(|free| free.second_edge_id)),
805 (&finish_second, finish_free.map(|free| free.second_edge_id)),
806 ],
807 ),
808 ] {
809 for (cross, boundary_id) in sides {
810 if !cross.is_consumed() {
811 continue;
812 }
813 let Some(boundary_id) = boundary_id else {
814 continue;
815 };
816 if !sewn {
817 let face = result
818 .shells
819 .iter_mut()
820 .flat_map(|shell| &mut shell.faces)
821 .find(|face| face.id == mate.face.id)
822 .ok_or("blend: mate face lost during surgery")?;
823 face.loops[mate.loop_index]
824 .coedges
825 .retain(|coedge| coedge.edge_id != boundary_id);
826 }
827 if !consumed_edges.contains(&boundary_id) {
828 consumed_edges.push(boundary_id);
829 }
830 }
831 }
832 for sew in [&sew_first, &sew_second].into_iter().flatten() {
834 for id in &sew.collapsed_edges {
835 if !consumed_edges.contains(id) {
836 consumed_edges.push(*id);
837 }
838 }
839 }
840
841 let mut end_plans = Vec::with_capacity(2);
850 for (end, transverse_id, corner, first_consumed, second_consumed) in [
851 (
852 start_end.free(),
853 transverse_a_id,
854 old_start,
855 start_first.is_consumed(),
856 start_second.is_consumed(),
857 ),
858 (
859 finish_end.free(),
860 transverse_b_id,
861 old_finish,
862 finish_first.is_consumed(),
863 finish_second.is_consumed(),
864 ),
865 ] {
866 let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
867 continue;
868 };
869 let face = result
870 .shells
871 .iter()
872 .flat_map(|shell| &shell.faces)
873 .find(|face| face.id == end.end_face_id)
874 .ok_or("blend: end face lost during surgery")?;
875 let Some(located) = locate_end_corner(
878 &result,
879 face,
880 corner,
881 end.first_edge_id,
882 end.second_edge_id,
883 ) else {
884 return Err("blend: end-face corner (adjacent boundary coedges) not found".into());
885 };
886 for pole in &located.pole_edge_ids {
889 if !consumed_edges.contains(pole) {
890 consumed_edges.push(*pole);
891 }
892 }
893 end_plans.push((
894 end.end_face_id,
895 transverse_id,
896 end.transverse_end_pcurve.clone(),
897 located,
898 first_consumed,
899 second_consumed,
900 ));
901 }
902
903 for (rim_resolution, boundary, corner, rim) in [
906 (
907 &start_first,
908 start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
909 old_start,
910 w1a,
911 ),
912 (
913 &start_second,
914 start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
915 old_start,
916 w2a,
917 ),
918 (
919 &finish_first,
920 finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
921 old_finish,
922 w1b,
923 ),
924 (
925 &finish_second,
926 finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
927 old_finish,
928 w2b,
929 ),
930 ] {
931 if !rim_resolution.trims_boundary() {
932 continue;
933 }
934 let Some((boundary_id, boundary_param)) = boundary else {
935 continue;
936 };
937 trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
938 }
939
940 for (end_face, transverse_id, transverse_end_pcurve, located, first_consumed, second_consumed) in
943 end_plans
944 {
945 let forward = located.x_is_first;
946 let pcurve = if forward {
947 transverse_end_pcurve
948 } else {
949 transverse_end_pcurve.reversed()?
950 };
951 let (x_consumed, y_consumed) = if located.x_is_first {
952 (first_consumed, second_consumed)
953 } else {
954 (second_consumed, first_consumed)
955 };
956 let transverse_coedge = CoedgeRecord {
957 id: take_id(),
958 edge_id: transverse_id,
959 forward,
960 pcurve,
961 };
962 splice_end_corner(
963 result,
964 end_face,
965 &located,
966 transverse_coedge,
967 x_consumed,
968 y_consumed,
969 )?;
970 }
971
972 let (mid_u, station_uv) = match (&cr_rail, &cs_rail) {
977 (Some((_, cr_pcurve)), _) => {
978 let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
979 (mid_u, cr_pcurve.evaluate(mid_u)?)
980 }
981 (None, Some(_)) => {
982 let mid_u = (cs_domain[0] + cs_domain[1]) * 0.5;
983 (mid_u, rows.cr_pcurve.evaluate(mid_u)?)
984 }
985 (None, None) => (cr_domain[0], rows.cr_pcurve.evaluate(cr_domain[0])?),
986 };
987 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
988 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
989 let out1 = if first.face.same_sense {
990 n1
991 } else {
992 n1.scale(-1.0)
993 };
994 let same_sense = blend_normal.dot(out1) >= 0.0;
995 let loop_id = take_id();
996 let cr_forward = sew_first
1004 .as_ref()
1005 .map_or(blend_cr_forward, |sew| sew.forward);
1006 let cs_forward = sew_second
1007 .as_ref()
1008 .map_or(!blend_cr_forward, |sew| sew.forward);
1009 let mut end_slot = |plan: &EndPlan,
1019 free_edge_id: Option<u64>,
1020 natural_forward: bool|
1021 -> Result<Vec<CoedgeRecord>, String> {
1022 match plan {
1023 EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
1024 id: take_id(),
1025 edge_id: end.arc_edge_id,
1026 forward: true,
1027 pcurve: end.arc_blend_pcurve.clone(),
1028 }]),
1029 EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
1030 id: take_id(),
1031 edge_id: end.arc_edge_id,
1032 forward: true,
1033 pcurve: end.arc_blend_pcurve.clone(),
1034 }]),
1035 EndPlan::Miter(end) => end
1036 .edges
1037 .iter()
1038 .map(|(edge_id, forward, pcurve)| {
1039 Ok(CoedgeRecord {
1040 id: take_id(),
1041 edge_id: *edge_id,
1042 forward: *forward,
1043 pcurve: pcurve.clone(),
1044 })
1045 })
1046 .collect(),
1047 EndPlan::Free(free) => {
1048 let edge_id =
1049 free_edge_id.ok_or("blend: free end without its transverse edge")?;
1050 Ok(vec![if natural_forward {
1051 CoedgeRecord {
1052 id: take_id(),
1053 edge_id,
1054 forward: true,
1055 pcurve: free.transverse_blend_pcurve.clone(),
1056 }
1057 } else {
1058 CoedgeRecord {
1059 id: take_id(),
1060 edge_id,
1061 forward: false,
1062 pcurve: free.transverse_blend_pcurve.reversed()?,
1063 }
1064 }])
1065 }
1066 }
1067 };
1068 let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
1069 let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
1070 let coedges = if blend_cr_forward {
1073 let mut coedges = Vec::new();
1074 if let Some(cr_edge_id) = cr_edge_id {
1075 coedges.push(CoedgeRecord {
1076 id: take_id(),
1077 edge_id: cr_edge_id,
1078 forward: cr_forward,
1079 pcurve: crate::sweep_topology::parameter_line(
1080 cr_domain[0],
1081 0.0,
1082 cr_domain[1],
1083 0.0,
1084 )?,
1085 });
1086 }
1087 coedges.extend(finish_slot);
1088 if let Some(cs_edge_id) = cs_edge_id {
1089 coedges.push(CoedgeRecord {
1090 id: take_id(),
1091 edge_id: cs_edge_id,
1092 forward: cs_forward,
1093 pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
1094 });
1095 }
1096 coedges.extend(start_slot);
1097 coedges
1098 } else {
1099 let mut coedges = Vec::new();
1100 if let Some(cr_edge_id) = cr_edge_id {
1101 coedges.push(CoedgeRecord {
1102 id: take_id(),
1103 edge_id: cr_edge_id,
1104 forward: cr_forward,
1105 pcurve: crate::sweep_topology::parameter_line(
1106 cr_domain[1],
1107 0.0,
1108 cr_domain[0],
1109 0.0,
1110 )?,
1111 });
1112 }
1113 coedges.extend(start_slot);
1114 if let Some(cs_edge_id) = cs_edge_id {
1115 coedges.push(CoedgeRecord {
1116 id: take_id(),
1117 edge_id: cs_edge_id,
1118 forward: cs_forward,
1119 pcurve: crate::sweep_topology::parameter_line(
1120 cs_domain[0],
1121 1.0,
1122 cs_domain[1],
1123 1.0,
1124 )?,
1125 });
1126 }
1127 coedges.extend(finish_slot);
1128 coedges
1129 };
1130 let blend_face_id = take_id();
1131 let mut blend_face = FaceRecord {
1132 id: blend_face_id,
1133 surface: rows.surface,
1134 same_sense,
1135 loops: vec![LoopRecord {
1136 id: loop_id,
1137 coedges,
1138 }],
1139 name: name.map(|value| value.to_string()),
1140 };
1141 if rows.exact_extrusion {
1142 transpose_face(&mut blend_face)?;
1143 }
1144 let shell_index = result
1145 .shells
1146 .iter()
1147 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
1148 .ok_or("blend: mate shell lost during surgery")?;
1149 for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
1154 if sew.is_some() {
1155 for shell in &mut result.shells {
1156 shell.faces.retain(|face| face.id != mate.face.id);
1157 }
1158 }
1159 }
1160 result.shells[shell_index].faces.push(blend_face);
1161
1162 result.edges.retain(|candidate| candidate.id != edge.id);
1163 result
1166 .edges
1167 .retain(|candidate| !consumed_edges.contains(&candidate.id));
1168 let mut identified = Vec::new();
1170 for (collapsed, from, to) in [(cr_collapsed, w1a, w1b), (cs_collapsed, w2a, w2b)] {
1171 if collapsed && from != to {
1172 identified.push((from, to));
1173 }
1174 }
1175 Ok(SewnStripe {
1176 cr_edge_id,
1177 cs_edge_id,
1178 blend_face_id,
1179 consumed: [sew_first.is_some(), sew_second.is_some()],
1180 snap,
1181 identified,
1182 })
1183}
1184
1185pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), String> {
1191 let surface = &face.surface;
1192 let rows_u = surface.control_points.len();
1193 let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
1194 let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
1195 for row in &surface.control_points {
1196 for (j, point) in row.iter().enumerate() {
1197 transposed[j].push(*point);
1198 }
1199 }
1200 face.surface = NurbsSurface::new(
1201 surface.degree_v,
1202 surface.degree_u,
1203 surface.knots_v.clone(),
1204 surface.knots_u.clone(),
1205 transposed,
1206 )?;
1207 for loop_record in &mut face.loops {
1208 for coedge in &mut loop_record.coedges {
1209 for control in &mut coedge.pcurve.control_points {
1210 std::mem::swap(&mut control.x, &mut control.y);
1211 }
1212 }
1213 }
1214 face.same_sense = !face.same_sense;
1215 Ok(())
1216}
1217
1218pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
1223 let used: rustc_hash::FxHashSet<u64> = result
1224 .edges
1225 .iter()
1226 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
1227 .collect();
1228 result
1229 .vertices
1230 .retain(|candidate| used.contains(&candidate.id));
1231}
1232
1233fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, String> {
1234 crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0)
1235}
1236