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(
81 edge,
82 &first_mate,
83 &second_mate,
84 radius_at,
85 overshoot_fraction,
86 )?;
87 let parameters = station_parameters(&stations);
88 let rows = fit_open_rows(&stations, ¶meters, chamfer)?;
89 let mut ends = Vec::with_capacity(2);
90 let mut in_range = true;
91 for (vertex, at_start) in [(edge.start_vertex_id, true), (edge.end_vertex_id, false)] {
92 let (end, side_in_range) = resolve_free_end(
93 solid,
94 edge_id,
95 &first_mate,
96 &second_mate,
97 &rows,
98 vertex,
99 at_start,
100 )?;
101 if debug {
102 eprintln!(
103 " [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}",
104 end.end_face_id,
105 end.first_edge_id,
106 end.first_edge_parameter,
107 end.second_edge_id,
108 end.second_edge_parameter,
109 end.cr_parameter,
110 end.cs_parameter,
111 );
112 }
113 in_range &= side_in_range;
114 ends.push(end);
115 }
116 Ok((rows, ends, in_range))
117 };
118
119 let seeded_fraction: Option<f64> = (|| {
129 let span = edge.t1 - edge.t0;
130 let mut length = 0.0f64;
131 let mut previous = edge.curve.evaluate(edge.t0).ok()?;
132 for index in 1..=16 {
133 let t = edge.t0 + span * index as f64 / 16.0;
134 let point = edge.curve.evaluate(t).ok()?;
135 length += point.sub(previous).length();
136 previous = point;
137 }
138 if !(length > 0.0) || !length.is_finite() {
139 return None;
140 }
141 let mid_t = edge.t0 + span * 0.5;
142 let uv1 = edge_uv_on_face(first_mate.coedge, edge, mid_t).ok()?;
143 let uv2 = edge_uv_on_face(second_mate.coedge, edge, mid_t).ok()?;
144 let n1 = raw_normal(&first_mate.face.surface, uv1[0], uv1[1]).ok()?;
145 let n2 = raw_normal(&second_mate.face.surface, uv2[0], uv2[1]).ok()?;
146 let cos_alpha =
147 (rho1.signum() * rho2.signum() * n1.dot(n2)).clamp(-1.0, 1.0);
148 let tan_half = ((1.0 - cos_alpha).max(0.0) / (1.0 + cos_alpha).max(1e-9)).sqrt();
151 let end_radius = radius_at(edge.t0).abs().max(radius_at(edge.t1).abs());
152 let fraction = (1.5 * end_radius * tan_half / length).clamp(0.08, 0.45);
153 if !fraction.is_finite() || (fraction - 0.08).abs() < 1e-12 {
154 return None;
155 }
156 Some(fraction)
157 })();
158
159 let mut chosen: Option<(FittedRows, Vec<EndSurgery>)> = None;
166 let mut last_error: Option<String> = None;
167 if let Some(fraction) = seeded_fraction {
168 match compute(fraction) {
169 Ok((rows, ends, true)) => chosen = Some((rows, ends)),
170 Ok(_) => {}
171 Err(error) => last_error = Some(error),
172 }
173 }
174 if chosen.is_none() {
175 for &overshoot_fraction in &[0.08f64, 0.16, 0.28, 0.45] {
176 match compute(overshoot_fraction) {
177 Ok((rows, ends, in_range)) => {
178 let fallback = chosen.is_none();
179 if in_range {
180 chosen = Some((rows, ends));
181 break;
182 } else if fallback {
183 chosen = Some((rows, ends));
184 }
185 }
186 Err(error) => last_error = Some(error),
187 }
188 }
189 }
190 let (rows, ends) = chosen.ok_or_else(|| {
191 last_error.unwrap_or_else(|| "blend: open march failed at every overshoot".into())
192 })?;
193
194 let [start_end, finish_end] = match <[EndSurgery; 2]>::try_from(ends) {
195 Ok(pair) => pair,
196 Err(_) => return Err("blend: open surgery needs exactly two ends".into()),
197 };
198 let mut result = solid.clone();
199 let mut take_id = fresh_id_source(solid);
200 build_open_surgery(
201 solid,
202 &mut result,
203 &mut take_id,
204 edge,
205 &first_mate,
206 &second_mate,
207 rows,
208 [EndPlan::Free(start_end), EndPlan::Free(finish_end)],
209 name,
210 )?;
211 prune_orphan_vertices(&mut result);
212 Ok(result)
213}
214
215pub(in crate::blend) fn fresh_id_source(solid: &BrepSolid) -> impl FnMut() -> u64 {
219 let mut next_id = solid
220 .vertices
221 .iter()
222 .map(|vertex| vertex.id)
223 .chain(solid.edges.iter().map(|edge| edge.id))
224 .chain(
225 solid
226 .shells
227 .iter()
228 .flat_map(|shell| &shell.faces)
229 .flat_map(|face| {
230 face.loops
231 .iter()
232 .map(|loop_record| loop_record.id)
233 .chain(face.loops.iter().flat_map(|loop_record| {
234 loop_record.coedges.iter().map(|coedge| coedge.id)
235 }))
236 .chain(std::iter::once(face.id))
237 }),
238 )
239 .max()
240 .unwrap_or(0)
241 + 1;
242 move || {
243 let id = next_id;
244 next_id += 1;
245 id
246 }
247}
248
249pub(in crate::blend) fn build_open_surgery(
261 solid: &BrepSolid,
262 result: &mut BrepSolid,
263 take_id: &mut dyn FnMut() -> u64,
264 edge: &EdgeRecord,
265 first: &BlendMate,
266 second: &BlendMate,
267 rows: FittedRows,
268 ends: [EndPlan; 2],
269 name: Option<&str>,
270) -> Result<SewnStripe, String> {
271 let [start_end, finish_end] = ends;
272
273 let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, String> {
275 let (_, tail) = row.split(a)?;
276 let (middle, _) = tail.split(b)?;
277 Ok(middle)
278 };
279 let kind = |plan: &EndPlan| match plan {
280 EndPlan::Free(_) => "free",
281 EndPlan::Corner(_) => "corner",
282 EndPlan::Miter(_) => "miter",
283 EndPlan::Cap(_) => "cap",
284 };
285 let describe_trim = |error: String| {
286 format!(
287 "{error} (edge {} rows trimmed for a {} start at cr {:.6}/cs {:.6} and a {} finish \
288 at cr {:.6}/cs {:.6})",
289 edge.id,
290 kind(&start_end),
291 start_end.cr_parameter(),
292 start_end.cs_parameter(),
293 kind(&finish_end),
294 finish_end.cr_parameter(),
295 finish_end.cs_parameter()
296 )
297 };
298 let cr = trim_row(&rows.cr, start_end.cr_parameter(), finish_end.cr_parameter())
299 .map_err(describe_trim)?;
300 let cr_pcurve = trim_row(
301 &rows.cr_pcurve,
302 start_end.cr_parameter(),
303 finish_end.cr_parameter(),
304 )?;
305 let cs = trim_row(&rows.cs, start_end.cs_parameter(), finish_end.cs_parameter())
306 .map_err(describe_trim)?;
307 let cs_pcurve = trim_row(
308 &rows.cs_pcurve,
309 start_end.cs_parameter(),
310 finish_end.cs_parameter(),
311 )?;
312
313 let cr_domain = cr.domain()?;
314 let cs_domain = cs.domain()?;
315 let old_start = edge.start_vertex_id;
316 let old_finish = edge.end_vertex_id;
317
318 let classify =
326 |boundary_id: u64, boundary_param: f64, corner: u64| -> Result<RimResolution, String> {
327 let boundary = result
328 .edges
329 .iter()
330 .find(|candidate| candidate.id == boundary_id)
331 .ok_or("blend: end boundary edge missing during surgery")?;
332 let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
333 let far_vertex = if boundary.start_vertex_id == corner {
334 Some((boundary.end_vertex_id, boundary.t1))
335 } else if boundary.end_vertex_id == corner {
336 Some((boundary.start_vertex_id, boundary.t0))
337 } else {
338 None
339 };
340 if let Some((far_id, _far_t)) = far_vertex {
341 let far_point = result
352 .vertices
353 .iter()
354 .find(|candidate| candidate.id == far_id)
355 .map(|candidate| candidate.point)
356 .ok_or("blend: consumed boundary far vertex missing")?;
357 let near_point = result
358 .vertices
359 .iter()
360 .find(|candidate| candidate.id == corner)
361 .map(|candidate| candidate.point)
362 .ok_or("blend: boundary corner vertex missing")?;
363 let crossing = boundary.curve.evaluate_extended(boundary_param)?;
364 let extent = far_point.sub(near_point).length();
365 let consumed_band = (1e-6 * (1.0 + extent)).max(1e-7);
366 let _ = span;
367 if crossing.sub(far_point).length() <= consumed_band {
368 if !result
370 .vertices
371 .iter()
372 .any(|candidate| candidate.id == far_id)
373 {
374 return Err("blend: consumed boundary far vertex missing".into());
375 }
376 return Ok(RimResolution::Consumed(far_id));
377 }
378 }
379 Ok(RimResolution::Fresh)
380 };
381 let resolve_side = |plan: &EndPlan,
385 side_first: bool,
386 corner: u64|
387 -> Result<RimResolution, String> {
388 if let Some(vertex) = plan.planned_rim(side_first) {
389 return Ok(RimResolution::Corner(vertex));
390 }
391 match plan {
392 EndPlan::Free(free) => classify(
393 if side_first {
394 free.first_edge_id
395 } else {
396 free.second_edge_id
397 },
398 if side_first {
399 free.first_edge_parameter
400 } else {
401 free.second_edge_parameter
402 },
403 corner,
404 ),
405 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => {
406 Err("blend: planned end without rim vertices".into())
407 }
408 }
409 };
410 let start_first = resolve_side(&start_end, true, old_start)?;
411 let start_second = resolve_side(&start_end, false, old_start)?;
412 let finish_first = resolve_side(&finish_end, true, old_finish)?;
413 let finish_second = resolve_side(&finish_end, false, old_finish)?;
414
415 let first_use_forward = first.coedge.forward;
418 let blend_cr_forward = !first_use_forward;
419
420 let (sew_first, sew_second) = match (start_end.free(), finish_end.free()) {
435 (Some(start_free), Some(finish_free)) => (
436 detect_row_coincidence(
437 solid,
438 first,
439 edge.id,
440 start_free.first_edge_id,
441 finish_free.first_edge_id,
442 start_first.is_consumed(),
443 start_first.existing().unwrap_or(0),
444 finish_first.is_consumed(),
445 finish_first.existing().unwrap_or(0),
446 &cr,
447 blend_cr_forward,
448 )?,
449 detect_row_coincidence(
450 solid,
451 second,
452 edge.id,
453 start_free.second_edge_id,
454 finish_free.second_edge_id,
455 start_second.is_consumed(),
456 start_second.existing().unwrap_or(0),
457 finish_second.is_consumed(),
458 finish_second.existing().unwrap_or(0),
459 &cs,
460 !blend_cr_forward,
461 )?,
462 ),
463 _ => (None, None),
464 };
465 let (sew_first, sew_second): (Option<RowSew>, Option<RowSew>) =
469 if first.face.id == second.face.id {
470 (None, None)
471 } else {
472 (sew_first, sew_second)
473 };
474
475 let mut resolve = |rim: &RimResolution, point: Result<Vec3, String>| -> Result<u64, String> {
478 match rim.existing() {
479 Some(id) => Ok(id),
480 None => {
481 let id = take_id();
482 result.vertices.push(VertexRecord { id, point: point? });
483 Ok(id)
484 }
485 }
486 };
487 let w1a = resolve(&start_first, cr.evaluate(cr_domain[0]))?;
488 let w1b = resolve(&finish_first, cr.evaluate(cr_domain[1]))?;
489 let w2a = resolve(&start_second, cs.evaluate(cs_domain[0]))?;
490 let w2b = resolve(&finish_second, cs.evaluate(cs_domain[1]))?;
491
492 let cr_edge_id = match &sew_first {
496 Some(sew) => sew.edge_id,
497 None => {
498 let id = take_id();
499 result.edges.push(EdgeRecord {
500 id,
501 curve: cr.clone(),
502 t0: cr_domain[0],
503 t1: cr_domain[1],
504 start_vertex_id: w1a,
505 end_vertex_id: w1b,
506 degenerate: false,
507 name: None,
508 });
509 id
510 }
511 };
512 let cs_edge_id = match &sew_second {
513 Some(sew) => sew.edge_id,
514 None => {
515 let id = take_id();
516 result.edges.push(EdgeRecord {
517 id,
518 curve: cs.clone(),
519 t0: cs_domain[0],
520 t1: cs_domain[1],
521 start_vertex_id: w2a,
522 end_vertex_id: w2b,
523 degenerate: false,
524 name: None,
525 });
526 id
527 }
528 };
529 let mut commit_end_edge = |plan: &EndPlan,
534 first_rim: u64,
535 second_rim: u64|
536 -> Result<Option<u64>, String> {
537 match plan {
538 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
539 EndPlan::Free(free) => {
540 let id = take_id();
541 let domain = free.transverse_curve.domain()?;
542 result.edges.push(EdgeRecord {
543 id,
544 curve: free.transverse_curve.clone(),
545 t0: domain[0],
546 t1: domain[1],
547 start_vertex_id: first_rim,
548 end_vertex_id: second_rim,
549 degenerate: false,
550 name: None,
551 });
552 Ok(Some(id))
553 }
554 }
555 };
556 let transverse_a_id = commit_end_edge(&start_end, w1a, w2a)?;
557 let transverse_b_id = commit_end_edge(&finish_end, w1b, w2b)?;
558
559 for (mate, new_edge_id, pcurve_forward, sewn) in [
564 (first, cr_edge_id, &cr_pcurve, sew_first.is_some()),
565 (second, cs_edge_id, &cs_pcurve, sew_second.is_some()),
566 ] {
567 if sewn {
568 continue;
569 }
570 let face = result
571 .shells
572 .iter_mut()
573 .flat_map(|shell| &mut shell.faces)
574 .find(|face| face.id == mate.face.id)
575 .ok_or("blend: mate face lost during surgery")?;
576 let loop_record = &mut face.loops[mate.loop_index];
577 let position = loop_record
578 .coedges
579 .iter()
580 .position(|coedge| coedge.edge_id == edge.id)
581 .ok_or("blend: edge coedge lost during surgery")?;
582 let old_forward = loop_record.coedges[position].forward;
583 loop_record.coedges[position] = CoedgeRecord {
584 id: loop_record.coedges[position].id,
585 edge_id: new_edge_id,
586 forward: old_forward,
587 pcurve: if old_forward {
588 pcurve_forward.clone()
589 } else {
590 pcurve_forward.reversed()?
591 },
592 };
593 }
594
595 for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
600 let EndPlan::Cap(cap) = plan else {
601 continue;
602 };
603 for (mate, rail_edge_id, leg, rim) in [
604 (first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
605 (second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
606 ] {
607 let face = result
608 .shells
609 .iter_mut()
610 .flat_map(|shell| &mut shell.faces)
611 .find(|face| face.id == mate.face.id)
612 .ok_or("blend: mate face lost during cap splice")?;
613 let loop_record = &mut face.loops[mate.loop_index];
614 let count = loop_record.coedges.len();
615 let rail_at = loop_record
616 .coedges
617 .iter()
618 .position(|coedge| coedge.edge_id == rail_edge_id)
619 .ok_or("blend: rail coedge lost during cap splice")?;
620 let rail_forward = loop_record.coedges[rail_at].forward;
626 let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
627 let (insert_at, forward, pcurve) = if end_is_traversal_end {
628 (rail_at + 1, true, leg.1.clone())
630 } else {
631 (rail_at, false, leg.1.reversed()?)
633 };
634 let _ = (count, rim);
635 loop_record.coedges.insert(
636 insert_at,
637 CoedgeRecord {
638 id: take_id(),
639 edge_id: leg.0,
640 forward,
641 pcurve,
642 },
643 );
644 }
645 }
646
647 let mut consumed_edges: Vec<u64> = Vec::new();
653 let start_free = start_end.free();
654 let finish_free = finish_end.free();
655 for (mate, sewn, sides) in [
656 (
657 first,
658 sew_first.is_some(),
659 [
660 (&start_first, start_free.map(|free| free.first_edge_id)),
661 (&finish_first, finish_free.map(|free| free.first_edge_id)),
662 ],
663 ),
664 (
665 second,
666 sew_second.is_some(),
667 [
668 (&start_second, start_free.map(|free| free.second_edge_id)),
669 (&finish_second, finish_free.map(|free| free.second_edge_id)),
670 ],
671 ),
672 ] {
673 for (cross, boundary_id) in sides {
674 if !cross.is_consumed() {
675 continue;
676 }
677 let Some(boundary_id) = boundary_id else {
678 continue;
679 };
680 if !sewn {
681 let face = result
682 .shells
683 .iter_mut()
684 .flat_map(|shell| &mut shell.faces)
685 .find(|face| face.id == mate.face.id)
686 .ok_or("blend: mate face lost during surgery")?;
687 face.loops[mate.loop_index]
688 .coedges
689 .retain(|coedge| coedge.edge_id != boundary_id);
690 }
691 if !consumed_edges.contains(&boundary_id) {
692 consumed_edges.push(boundary_id);
693 }
694 }
695 }
696 for sew in [&sew_first, &sew_second].into_iter().flatten() {
698 for id in &sew.collapsed_edges {
699 if !consumed_edges.contains(id) {
700 consumed_edges.push(*id);
701 }
702 }
703 }
704
705 let traversal_vertices = |result: &BrepSolid, coedge: &CoedgeRecord| -> Option<(u64, u64)> {
710 let edge = result
711 .edges
712 .iter()
713 .find(|candidate| candidate.id == coedge.edge_id)?;
714 Some(if coedge.forward {
715 (edge.start_vertex_id, edge.end_vertex_id)
716 } else {
717 (edge.end_vertex_id, edge.start_vertex_id)
718 })
719 };
720 let mut end_plans = Vec::with_capacity(2);
724 for (end, transverse_id, corner, first_consumed, second_consumed) in [
725 (
726 start_end.free(),
727 transverse_a_id,
728 old_start,
729 start_first.is_consumed(),
730 start_second.is_consumed(),
731 ),
732 (
733 finish_end.free(),
734 transverse_b_id,
735 old_finish,
736 finish_first.is_consumed(),
737 finish_second.is_consumed(),
738 ),
739 ] {
740 let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
741 continue;
742 };
743 let face = result
744 .shells
745 .iter()
746 .flat_map(|shell| &shell.faces)
747 .find(|face| face.id == end.end_face_id)
748 .ok_or("blend: end face lost during surgery")?;
749 let mut located = None;
750 'search: for (loop_index, loop_record) in face.loops.iter().enumerate() {
751 let count = loop_record.coedges.len();
752 for index in 0..count {
753 let this = &loop_record.coedges[index];
754 let next = &loop_record.coedges[(index + 1) % count];
755 let this_pair = (this.edge_id, next.edge_id);
756 let matches_pair = this_pair == (end.first_edge_id, end.second_edge_id)
757 || this_pair == (end.second_edge_id, end.first_edge_id);
758 if !matches_pair {
759 continue;
760 }
761 let Some((_, this_end)) = traversal_vertices(&result, this) else {
762 continue;
763 };
764 let Some((next_start, _)) = traversal_vertices(&result, next) else {
765 continue;
766 };
767 if this_end == corner && next_start == corner {
768 located = Some((
771 loop_index,
772 this.id,
773 next.id,
774 this.edge_id == end.first_edge_id,
775 ));
776 break 'search;
777 }
778 }
779 }
780 let Some((loop_index, x_coedge_id, y_coedge_id, x_is_first)) = located else {
781 return Err("blend: end-face corner (adjacent boundary coedges) not found".into());
782 };
783 end_plans.push((
784 end.end_face_id,
785 transverse_id,
786 end.transverse_end_pcurve.clone(),
787 loop_index,
788 x_coedge_id,
789 y_coedge_id,
790 x_is_first,
791 first_consumed,
792 second_consumed,
793 ));
794 }
795
796 for (rim_resolution, boundary, corner, rim) in [
799 (
800 &start_first,
801 start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
802 old_start,
803 w1a,
804 ),
805 (
806 &start_second,
807 start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
808 old_start,
809 w2a,
810 ),
811 (
812 &finish_first,
813 finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
814 old_finish,
815 w1b,
816 ),
817 (
818 &finish_second,
819 finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
820 old_finish,
821 w2b,
822 ),
823 ] {
824 if !rim_resolution.trims_boundary() {
825 continue;
826 }
827 let Some((boundary_id, boundary_param)) = boundary else {
828 continue;
829 };
830 trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
831 }
832
833 for (
836 end_face,
837 transverse_id,
838 transverse_end_pcurve,
839 loop_index,
840 x_coedge_id,
841 y_coedge_id,
842 x_is_first,
843 first_consumed,
844 second_consumed,
845 ) in end_plans
846 {
847 let forward = x_is_first;
848 let pcurve = if forward {
849 transverse_end_pcurve
850 } else {
851 transverse_end_pcurve.reversed()?
852 };
853 let x_consumed = if x_is_first {
854 first_consumed
855 } else {
856 second_consumed
857 };
858 let y_consumed = if x_is_first {
859 second_consumed
860 } else {
861 first_consumed
862 };
863 let coedge_id = take_id();
864 let mut transverse_coedge = Some(CoedgeRecord {
865 id: coedge_id,
866 edge_id: transverse_id,
867 forward,
868 pcurve,
869 });
870 let face = result
871 .shells
872 .iter_mut()
873 .flat_map(|shell| &mut shell.faces)
874 .find(|face| face.id == end_face)
875 .ok_or("blend: end face lost during surgery")?;
876 let loop_record = &mut face.loops[loop_index];
877 let count = loop_record.coedges.len();
878 let seg_index = loop_record
881 .coedges
882 .iter()
883 .position(|coedge| coedge.id == x_coedge_id)
884 .ok_or("blend: end-face corner coedge lost during surgery")?;
885 let y_index = (seg_index + 1) % count;
886 if loop_record.coedges[y_index].id != y_coedge_id {
887 return Err("blend: end-face corner pair no longer adjacent".into());
888 }
889 let mut rebuilt = Vec::with_capacity(count + 1);
890 for index in 0..count {
891 if index == seg_index {
892 if !x_consumed {
893 rebuilt.push(loop_record.coedges[index].clone());
894 }
895 rebuilt.push(transverse_coedge.take().expect("transverse spliced once"));
896 } else if index == y_index {
897 if !y_consumed {
898 rebuilt.push(loop_record.coedges[index].clone());
899 }
900 } else {
901 rebuilt.push(loop_record.coedges[index].clone());
902 }
903 }
904 loop_record.coedges = rebuilt;
905 }
906
907 let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
909 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
910 let station_uv = cr_pcurve.evaluate(mid_u)?;
911 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
912 let out1 = if first.face.same_sense {
913 n1
914 } else {
915 n1.scale(-1.0)
916 };
917 let same_sense = blend_normal.dot(out1) >= 0.0;
918 let loop_id = take_id();
919 let cr_forward = sew_first
927 .as_ref()
928 .map_or(blend_cr_forward, |sew| sew.forward);
929 let cs_forward = sew_second
930 .as_ref()
931 .map_or(!blend_cr_forward, |sew| sew.forward);
932 let mut end_slot = |plan: &EndPlan,
942 free_edge_id: Option<u64>,
943 natural_forward: bool|
944 -> Result<Vec<CoedgeRecord>, String> {
945 match plan {
946 EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
947 id: take_id(),
948 edge_id: end.arc_edge_id,
949 forward: true,
950 pcurve: end.arc_blend_pcurve.clone(),
951 }]),
952 EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
953 id: take_id(),
954 edge_id: end.arc_edge_id,
955 forward: true,
956 pcurve: end.arc_blend_pcurve.clone(),
957 }]),
958 EndPlan::Miter(end) => end
959 .edges
960 .iter()
961 .map(|(edge_id, forward, pcurve)| {
962 Ok(CoedgeRecord {
963 id: take_id(),
964 edge_id: *edge_id,
965 forward: *forward,
966 pcurve: pcurve.clone(),
967 })
968 })
969 .collect(),
970 EndPlan::Free(free) => {
971 let edge_id =
972 free_edge_id.ok_or("blend: free end without its transverse edge")?;
973 Ok(vec![if natural_forward {
974 CoedgeRecord {
975 id: take_id(),
976 edge_id,
977 forward: true,
978 pcurve: free.transverse_blend_pcurve.clone(),
979 }
980 } else {
981 CoedgeRecord {
982 id: take_id(),
983 edge_id,
984 forward: false,
985 pcurve: free.transverse_blend_pcurve.reversed()?,
986 }
987 }])
988 }
989 }
990 };
991 let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
992 let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
993 let coedges = if blend_cr_forward {
994 let mut coedges = vec![CoedgeRecord {
995 id: take_id(),
996 edge_id: cr_edge_id,
997 forward: cr_forward,
998 pcurve: crate::sweep_topology::parameter_line(
999 cr_domain[0],
1000 0.0,
1001 cr_domain[1],
1002 0.0,
1003 )?,
1004 }];
1005 coedges.extend(finish_slot);
1006 coedges.push(CoedgeRecord {
1007 id: take_id(),
1008 edge_id: cs_edge_id,
1009 forward: cs_forward,
1010 pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
1011 });
1012 coedges.extend(start_slot);
1013 coedges
1014 } else {
1015 let mut coedges = vec![CoedgeRecord {
1016 id: take_id(),
1017 edge_id: cr_edge_id,
1018 forward: cr_forward,
1019 pcurve: crate::sweep_topology::parameter_line(
1020 cr_domain[1],
1021 0.0,
1022 cr_domain[0],
1023 0.0,
1024 )?,
1025 }];
1026 coedges.extend(start_slot);
1027 coedges.push(CoedgeRecord {
1028 id: take_id(),
1029 edge_id: cs_edge_id,
1030 forward: cs_forward,
1031 pcurve: crate::sweep_topology::parameter_line(
1032 cs_domain[0],
1033 1.0,
1034 cs_domain[1],
1035 1.0,
1036 )?,
1037 });
1038 coedges.extend(finish_slot);
1039 coedges
1040 };
1041 let blend_face_id = take_id();
1042 let mut blend_face = FaceRecord {
1043 id: blend_face_id,
1044 surface: rows.surface,
1045 same_sense,
1046 loops: vec![LoopRecord {
1047 id: loop_id,
1048 coedges,
1049 }],
1050 name: name.map(|value| value.to_string()),
1051 };
1052 if rows.exact_extrusion {
1053 transpose_face(&mut blend_face)?;
1054 }
1055 let shell_index = result
1056 .shells
1057 .iter()
1058 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
1059 .ok_or("blend: mate shell lost during surgery")?;
1060 for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
1065 if sew.is_some() {
1066 for shell in &mut result.shells {
1067 shell.faces.retain(|face| face.id != mate.face.id);
1068 }
1069 }
1070 }
1071 result.shells[shell_index].faces.push(blend_face);
1072
1073 result.edges.retain(|candidate| candidate.id != edge.id);
1074 result
1077 .edges
1078 .retain(|candidate| !consumed_edges.contains(&candidate.id));
1079 Ok(SewnStripe {
1080 cr_edge_id,
1081 cs_edge_id,
1082 blend_face_id,
1083 })
1084}
1085
1086pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), String> {
1092 let surface = &face.surface;
1093 let rows_u = surface.control_points.len();
1094 let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
1095 let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
1096 for row in &surface.control_points {
1097 for (j, point) in row.iter().enumerate() {
1098 transposed[j].push(*point);
1099 }
1100 }
1101 face.surface = NurbsSurface::new(
1102 surface.degree_v,
1103 surface.degree_u,
1104 surface.knots_v.clone(),
1105 surface.knots_u.clone(),
1106 transposed,
1107 )?;
1108 for loop_record in &mut face.loops {
1109 for coedge in &mut loop_record.coedges {
1110 for control in &mut coedge.pcurve.control_points {
1111 std::mem::swap(&mut control.x, &mut control.y);
1112 }
1113 }
1114 }
1115 face.same_sense = !face.same_sense;
1116 Ok(())
1117}
1118
1119pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
1124 let used: rustc_hash::FxHashSet<u64> = result
1125 .edges
1126 .iter()
1127 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
1128 .collect();
1129 result
1130 .vertices
1131 .retain(|candidate| used.contains(&candidate.id));
1132}
1133
1134fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, String> {
1135 crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0)
1136}