1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use super::*;
3
4pub fn blend_open_edge(
7 solid: &BrepSolid,
8 edge_id: u64,
9 radius: f64,
10 chamfer: bool,
11 name: Option<&str>,
12) -> Result<BrepSolid, KernelRefusal> {
13 if !(radius > 0.0) || !radius.is_finite() {
14 return Err(KernelRefusal::input(KernelStage::Collect, "radius", "blend: radius must be positive"));
15 }
16 blend_open_edge_impl(solid, edge_id, &|_| radius, chamfer, name)
17}
18
19pub(super) fn blend_open_edge_impl(
20 solid: &BrepSolid,
21 edge_id: u64,
22 radius_at: &dyn Fn(f64) -> f64,
23 chamfer: bool,
24 name: Option<&str>,
25) -> Result<BrepSolid, KernelRefusal> {
26 let edge = solid
27 .edges
28 .iter()
29 .find(|edge| edge.id == edge_id)
30 .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
31 if edge.start_vertex_id == edge.end_vertex_id {
32 return Err(KernelRefusal::input(KernelStage::Collect, "open_edge", "blend: blend_open_edge requires an open edge"));
33 }
34 let (first_face, first_loop, first_coedge) = locate_mate(solid, edge_id, None)?;
35 let (second_face, second_loop, second_coedge) =
36 locate_mate(solid, edge_id, Some((first_face.id, first_loop)))?;
37 let mid_radius = radius_at(edge.t0 + (edge.t1 - edge.t0) * 0.5);
38 let (rho1, rho2) = signed_radii(
39 edge,
40 first_face,
41 first_coedge,
42 second_face,
43 second_coedge,
44 mid_radius,
45 )?;
46 let first_mate = BlendMate {
47 face: first_face,
48 coedge: first_coedge,
49 loop_index: first_loop,
50 rho: rho1,
51 };
52 let second_mate = BlendMate {
53 face: second_face,
54 coedge: second_coedge,
55 loop_index: second_loop,
56 rho: rho2,
57 };
58 let debug = std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok();
66 if debug {
67 let sp = edge.curve.evaluate(edge.t0);
68 let ep = edge.curve.evaluate(edge.t1);
69 eprintln!(
70 "OPEN edge {} v{}->v{} first_face {} second_face {} p0={:?} p1={:?}",
71 edge.id,
72 edge.start_vertex_id,
73 edge.end_vertex_id,
74 first_face.id,
75 second_face.id,
76 sp,
77 ep
78 );
79 }
80 let compute = |overshoot_fraction: f64| -> Result<(FittedRows, Vec<EndSurgery>, bool), KernelRefusal> {
81 let (stations, vertex_indices) = march_open_stations(
85 edge,
86 &first_mate,
87 &second_mate,
88 radius_at,
89 overshoot_fraction,
90 )?;
91 let parameters = station_parameters(&stations);
92 let rows = fit_open_rows(
93 &stations,
94 ¶meters,
95 chamfer,
96 Some(vertex_indices),
97 extrusion_direction(edge, &first_mate, &second_mate),
98 )?;
99 let mut ends = Vec::with_capacity(2);
100 let mut in_range = true;
101 for (vertex, at_start) in [(edge.start_vertex_id, true), (edge.end_vertex_id, false)] {
102 let (end, side_in_range) = resolve_free_end(
103 solid,
104 edge_id,
105 &first_mate,
106 &second_mate,
107 &rows,
108 vertex,
109 at_start,
110 )?;
111 if debug {
112 eprintln!(
113 " [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}",
114 end.end_face_id,
115 end.first_edge_id,
116 end.first_edge_parameter,
117 end.second_edge_id,
118 end.second_edge_parameter,
119 end.cr_parameter,
120 end.cs_parameter,
121 );
122 }
123 in_range &= side_in_range;
124 ends.push(end);
125 }
126 Ok((rows, ends, in_range))
127 };
128
129 let seeded_fraction: Option<f64> = (|| {
139 let span = edge.t1 - edge.t0;
140 let mut length = 0.0f64;
141 let mut previous = edge.curve.evaluate(edge.t0).ok()?;
142 for index in 1..=16 {
143 let t = edge.t0 + span * index as f64 / 16.0;
144 let point = edge.curve.evaluate(t).ok()?;
145 length += point.sub(previous).length();
146 previous = point;
147 }
148 if !(length > 0.0) || !length.is_finite() {
149 return None;
150 }
151 let mid_t = edge.t0 + span * 0.5;
152 let uv1 = edge_uv_on_face(first_mate.coedge, edge, mid_t).ok()?;
153 let uv2 = edge_uv_on_face(second_mate.coedge, edge, mid_t).ok()?;
154 let n1 = raw_normal(&first_mate.face.surface, uv1[0], uv1[1]).ok()?;
155 let n2 = raw_normal(&second_mate.face.surface, uv2[0], uv2[1]).ok()?;
156 let cos_alpha =
157 (rho1.signum() * rho2.signum() * n1.dot(n2)).clamp(-1.0, 1.0);
158 let tan_half = ((1.0 - cos_alpha).max(0.0) / (1.0 + cos_alpha).max(1e-9)).sqrt();
161 let end_radius = radius_at(edge.t0).abs().max(radius_at(edge.t1).abs());
162 let fraction = (1.5 * end_radius * tan_half / length).clamp(0.08, 0.45);
163 if !fraction.is_finite() || (fraction - 0.08).abs() < 1e-12 {
164 return None;
165 }
166 Some(fraction)
167 })();
168
169 let mut chosen: Option<(FittedRows, Vec<EndSurgery>)> = None;
176 let mut last_error: Option<KernelRefusal> = None;
177 if let Some(fraction) = seeded_fraction {
178 match compute(fraction) {
179 Ok((rows, ends, true)) => chosen = Some((rows, ends)),
180 Ok(_) => {}
181 Err(error) => last_error = Some(error),
182 }
183 }
184 if chosen.is_none() {
185 for &overshoot_fraction in &[0.08f64, 0.16, 0.28, 0.45] {
186 match compute(overshoot_fraction) {
187 Ok((rows, ends, in_range)) => {
188 let fallback = chosen.is_none();
189 if in_range {
190 chosen = Some((rows, ends));
191 break;
192 } else if fallback {
193 chosen = Some((rows, ends));
194 }
195 }
196 Err(error) => last_error = Some(error),
197 }
198 }
199 }
200 let (rows, ends) = chosen.ok_or_else(|| {
201 last_error.unwrap_or_else(|| {
202 KernelRefusal::internal(
203 KernelStage::Refine,
204 "open_march",
205 "blend: open march failed at every overshoot",
206 )
207 })
208 })?;
209
210 let [start_end, finish_end] = match <[EndSurgery; 2]>::try_from(ends) {
211 Ok(pair) => pair,
212 Err(_) => return Err(KernelRefusal::internal(KernelStage::Sew, "open_surgery_ends", "blend: open surgery needs exactly two ends")),
213 };
214 let mut crossings = SpokeCrossings::default();
218 crossings.note(start_end.escalated || finish_end.escalated);
219 let mut result = solid.clone();
220 let mut take_id = fresh_id_source(solid);
221 let sewn = build_open_surgery(
222 solid,
223 &mut result,
224 &mut take_id,
225 edge,
226 &first_mate,
227 &second_mate,
228 rows,
229 [EndPlan::Free(start_end), EndPlan::Free(finish_end)],
230 name,
231 )?;
232 prune_orphan_vertices(&mut result);
233 check_snap_closure(solid, &result, sewn.snap)?;
234 crossings.gate(result)
235}
236
237pub(in crate::blend) fn fresh_id_source(solid: &BrepSolid) -> impl FnMut() -> u64 {
242 let mut next_id = solid
243 .vertices
244 .iter()
245 .map(|vertex| vertex.id)
246 .chain(solid.edges.iter().map(|edge| edge.id))
247 .chain(
248 solid
249 .shells
250 .iter()
251 .flat_map(|shell| &shell.faces)
252 .flat_map(|face| {
253 face.loops
254 .iter()
255 .map(|loop_record| loop_record.id)
256 .chain(face.loops.iter().flat_map(|loop_record| {
257 loop_record.coedges.iter().map(|coedge| coedge.id)
258 }))
259 .chain(std::iter::once(face.id))
260 }),
261 )
262 .max()
263 .unwrap_or(0)
264 + 1;
265 move || {
266 let id = next_id;
267 next_id += 1;
268 id
269 }
270}
271
272pub(in crate::blend) fn build_open_surgery(
284 solid: &BrepSolid,
285 result: &mut BrepSolid,
286 take_id: &mut dyn FnMut() -> u64,
287 edge: &EdgeRecord,
288 first: &BlendMate,
289 second: &BlendMate,
290 rows: FittedRows,
291 ends: [EndPlan; 2],
292 name: Option<&str>,
293) -> Result<SewnStripe, KernelRefusal> {
294 let [start_end, finish_end] = ends;
295
296 let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, KernelRefusal> {
303 let [low, high] = row.domain().or_refuse(KernelStage::Refine, "domain")?;
304 let tail = if a == low { row.clone() } else { row.split(a).or_refuse(KernelStage::Refine, "split")?.1 };
305 if b == high {
306 return Ok(tail);
307 }
308 let (middle, _) = tail.split(b).or_refuse(KernelStage::Refine, "split")?;
309 Ok(middle)
310 };
311 let kind = |plan: &EndPlan| match plan {
312 EndPlan::Free(_) => "free",
313 EndPlan::Corner(_) => "corner",
314 EndPlan::Miter(_) => "miter",
315 EndPlan::Cap(_) => "cap",
316 };
317 let describe_trim = |error: KernelRefusal| {
318 error.with_message(|error| {
319 format!(
320 "{error} (edge {} rows trimmed for a {} start at cr {:.6}/cs {:.6} and a {} finish \
321 at cr {:.6}/cs {:.6})",
322 edge.id,
323 kind(&start_end),
324 start_end.cr_parameter(),
325 start_end.cs_parameter(),
326 kind(&finish_end),
327 finish_end.cr_parameter(),
328 finish_end.cs_parameter()
329 )
330 })
331 };
332 let band = consumed_band(solid);
342 let collapses = |row: &NurbsCurve, a: f64, b: f64| -> Result<bool, KernelRefusal> {
343 let from = row.evaluate(a).or_refuse(KernelStage::Refine, "evaluate")?;
344 Ok(row.evaluate(b).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band
345 && row.evaluate(0.5 * (a + b)).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band)
346 };
347 let cr_collapsed = collapses(&rows.cr, start_end.cr_parameter(), finish_end.cr_parameter())?;
348 let cs_collapsed = collapses(&rows.cs, start_end.cs_parameter(), finish_end.cs_parameter())?;
349 if cr_collapsed || cs_collapsed {
350 let unsupported = if start_end.free().is_some() && finish_end.free().is_some() {
351 Some("neither end is a corner")
352 } else if first.face.id == second.face.id {
353 Some("both rails lie on one face")
354 } else if matches!(start_end, EndPlan::Cap(_)) || matches!(finish_end, EndPlan::Cap(_)) {
355 Some("a capped end has no corner to share the point with")
356 } else {
357 None
358 };
359 if let Some(reason) = unsupported {
360 return Err(KernelRefusal::unsupported(KernelStage::Sew, super::RAIL_COLLAPSE_WHAT, format!(
361 "{RAIL_COLLAPSE_UNSUPPORTED} edge {}'s rail shrinks to a point and {reason}",
362 edge.id
363 )));
364 }
365 }
366 let trim_rail = |collapsed: bool,
367 row: &NurbsCurve,
368 pcurve: &NurbsCurve,
369 a: f64,
370 b: f64|
371 -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
372 if collapsed {
373 return Ok(None);
374 }
375 Ok(Some((trim_row(row, a, b).map_err(describe_trim)?, trim_row(pcurve, a, b)?)))
376 };
377 let cr_rail = trim_rail(
378 cr_collapsed,
379 &rows.cr,
380 &rows.cr_pcurve,
381 start_end.cr_parameter(),
382 finish_end.cr_parameter(),
383 )?;
384 let cs_rail = trim_rail(
385 cs_collapsed,
386 &rows.cs,
387 &rows.cs_pcurve,
388 start_end.cs_parameter(),
389 finish_end.cs_parameter(),
390 )?;
391 let cr_domain = match &cr_rail {
393 Some((cr, _)) => cr.domain().or_refuse(KernelStage::Refine, "domain")?,
394 None => [start_end.cr_parameter(), finish_end.cr_parameter()],
395 };
396 let cs_domain = match &cs_rail {
397 Some((cs, _)) => cs.domain().or_refuse(KernelStage::Refine, "domain")?,
398 None => [start_end.cs_parameter(), finish_end.cs_parameter()],
399 };
400 let cr_at = |parameter: f64| match &cr_rail {
402 Some((cr, _)) => cr.evaluate(parameter),
403 None => rows.cr.evaluate(parameter),
404 };
405 let cs_at = |parameter: f64| match &cs_rail {
406 Some((cs, _)) => cs.evaluate(parameter),
407 None => rows.cs.evaluate(parameter),
408 };
409 let old_start = edge.start_vertex_id;
410 let old_finish = edge.end_vertex_id;
411
412 let classify =
423 |boundary_id: u64, boundary_param: f64, corner: u64| -> Result<RimResolution, KernelRefusal> {
424 let boundary = result
425 .edges
426 .iter()
427 .find(|candidate| candidate.id == boundary_id)
428 .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_boundary_edge", "blend: end boundary edge missing during surgery"))?;
429 let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
430 let far_vertex = if boundary.start_vertex_id == corner {
431 Some((boundary.end_vertex_id, boundary.t1, boundary.t0))
432 } else if boundary.end_vertex_id == corner {
433 Some((boundary.start_vertex_id, boundary.t0, boundary.t1))
434 } else {
435 None
436 };
437 if let Some((far_id, far_t, near_t)) = far_vertex {
438 let far_point = result
449 .vertices
450 .iter()
451 .find(|candidate| candidate.id == far_id)
452 .map(|candidate| candidate.point)
453 .ok_or(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"))?;
454 let crossing = boundary.curve.evaluate_extended(boundary_param).or_refuse(KernelStage::Refine, "evaluate_extended")?;
455 let _ = span;
456 let past_far = crossing.sub(far_point).length();
457 if past_far <= band {
458 if !result
460 .vertices
461 .iter()
462 .any(|candidate| candidate.id == far_id)
463 {
464 return Err(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"));
465 }
466 return Ok(RimResolution::Consumed(far_id));
467 }
468 if (boundary_param - far_t) * (far_t - near_t) > 0.0 {
481 return Err(KernelRefusal::unsupported(KernelStage::Sew, "blend_wider_than_face", format!(
482 "{BLEND_WIDER_THAN_FACE} its rail crosses edge {boundary_id} {past_far:.3e} \
483 past that edge's far vertex, and a rail within {band:.3e} of it is the \
484 most this surgery snaps onto the edge"
485 )));
486 }
487 }
488 Ok(RimResolution::Fresh)
489 };
490 let resolve_side = |plan: &EndPlan,
494 side_first: bool,
495 corner: u64|
496 -> Result<RimResolution, KernelRefusal> {
497 if let Some(vertex) = plan.planned_rim(side_first) {
498 return Ok(RimResolution::Corner(vertex));
499 }
500 match plan {
501 EndPlan::Free(free) => classify(
502 if side_first {
503 free.first_edge_id
504 } else {
505 free.second_edge_id
506 },
507 if side_first {
508 free.first_edge_parameter
509 } else {
510 free.second_edge_parameter
511 },
512 corner,
513 ),
514 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => {
515 Err(KernelRefusal::internal(KernelStage::Sew, "rim_vertices", "blend: planned end without rim vertices"))
516 }
517 }
518 };
519 let start_first = resolve_side(&start_end, true, old_start)?;
520 let start_second = resolve_side(&start_end, false, old_start)?;
521 let finish_first = resolve_side(&finish_end, true, old_finish)?;
522 let finish_second = resolve_side(&finish_end, false, old_finish)?;
523
524 let first_use_forward = first.coedge.forward;
527 let blend_cr_forward = !first_use_forward;
528
529 let (sew_first, sew_second) = match (start_end.free(), finish_end.free(), &cr_rail, &cs_rail) {
544 (Some(start_free), Some(finish_free), Some((cr, _)), Some((cs, _))) => (
545 detect_row_coincidence(
546 solid,
547 first,
548 edge.id,
549 start_free.first_edge_id,
550 finish_free.first_edge_id,
551 start_first.is_consumed(),
552 start_first.existing().unwrap_or(0),
553 finish_first.is_consumed(),
554 finish_first.existing().unwrap_or(0),
555 cr,
556 blend_cr_forward,
557 band,
558 )?,
559 detect_row_coincidence(
560 solid,
561 second,
562 edge.id,
563 start_free.second_edge_id,
564 finish_free.second_edge_id,
565 start_second.is_consumed(),
566 start_second.existing().unwrap_or(0),
567 finish_second.is_consumed(),
568 finish_second.existing().unwrap_or(0),
569 cs,
570 !blend_cr_forward,
571 band,
572 )?,
573 ),
574 _ => (None, None),
575 };
576 let (sew_first, sew_second): (Option<RowSew>, Option<RowSew>) =
580 if first.face.id == second.face.id {
581 (None, None)
582 } else {
583 (sew_first, sew_second)
584 };
585
586 let mut resolve = |rim: &RimResolution, point: Result<Vec3, String>| -> Result<u64, KernelRefusal> {
589 match rim.existing() {
590 Some(id) => Ok(id),
591 None => {
592 let id = take_id();
593 result.vertices.push(VertexRecord { id, point: point.or_refuse(KernelStage::Refine, "point")? });
594 Ok(id)
595 }
596 }
597 };
598 let w1a = resolve(&start_first, cr_at(cr_domain[0]))?;
599 let w1b = resolve(&finish_first, cr_at(cr_domain[1]))?;
600 let w2a = resolve(&start_second, cs_at(cs_domain[0]))?;
601 let w2b = resolve(&finish_second, cs_at(cs_domain[1]))?;
602 let mut snap = [&sew_first, &sew_second]
605 .into_iter()
606 .flatten()
607 .fold(0.0f64, |worst, sew| worst.max(sew.deviation));
608 for (rim, vertex, row_end) in [
609 (&start_first, w1a, cr_at(cr_domain[0]).or_refuse(KernelStage::Refine, "cr_at")?),
610 (&finish_first, w1b, cr_at(cr_domain[1]).or_refuse(KernelStage::Refine, "cr_at")?),
611 (&start_second, w2a, cs_at(cs_domain[0]).or_refuse(KernelStage::Refine, "cs_at")?),
612 (&finish_second, w2b, cs_at(cs_domain[1]).or_refuse(KernelStage::Refine, "cs_at")?),
613 ] {
614 if !rim.is_consumed() {
615 continue;
616 }
617 let point = result
618 .vertices
619 .iter()
620 .find(|candidate| candidate.id == vertex)
621 .ok_or(KernelRefusal::internal(KernelStage::Sew, "rim_vertex", "blend: consumed rim vertex missing"))?
622 .point;
623 snap = snap.max(row_end.sub(point).length());
624 }
625
626 let cr_edge_id = match (&sew_first, &cr_rail) {
631 (Some(sew), _) => Some(sew.edge_id),
632 (None, None) => None,
633 (None, Some((cr, _))) => {
634 let id = take_id();
635 result.edges.push(EdgeRecord {
636 id,
637 curve: cr.clone(),
638 t0: cr_domain[0],
639 t1: cr_domain[1],
640 start_vertex_id: w1a,
641 end_vertex_id: w1b,
642 degenerate: false,
643 name: None,
644 });
645 Some(id)
646 }
647 };
648 let cs_edge_id = match (&sew_second, &cs_rail) {
649 (Some(sew), _) => Some(sew.edge_id),
650 (None, None) => None,
651 (None, Some((cs, _))) => {
652 let id = take_id();
653 result.edges.push(EdgeRecord {
654 id,
655 curve: cs.clone(),
656 t0: cs_domain[0],
657 t1: cs_domain[1],
658 start_vertex_id: w2a,
659 end_vertex_id: w2b,
660 degenerate: false,
661 name: None,
662 });
663 Some(id)
664 }
665 };
666 let mut commit_end_edge = |plan: &EndPlan,
671 first_rim: u64,
672 second_rim: u64|
673 -> Result<Option<u64>, KernelRefusal> {
674 match plan {
675 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
676 EndPlan::Free(free) => {
677 let id = take_id();
678 let domain = free.transverse_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
679 result.edges.push(EdgeRecord {
680 id,
681 curve: free.transverse_curve.clone(),
682 t0: domain[0],
683 t1: domain[1],
684 start_vertex_id: first_rim,
685 end_vertex_id: second_rim,
686 degenerate: false,
687 name: None,
688 });
689 Ok(Some(id))
690 }
691 }
692 };
693 let transverse_a_id = commit_end_edge(&start_end, w1a, w2a)?;
694 let transverse_b_id = commit_end_edge(&finish_end, w1b, w2b)?;
695
696 for (mate, rail, sewn) in [
703 (first, cr_edge_id.zip(cr_rail.as_ref()), sew_first.is_some()),
704 (second, cs_edge_id.zip(cs_rail.as_ref()), sew_second.is_some()),
705 ] {
706 if sewn {
707 continue;
708 }
709 let face = result
710 .shells
711 .iter_mut()
712 .flat_map(|shell| &mut shell.faces)
713 .find(|face| face.id == mate.face.id)
714 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
715 let loop_record = &mut face.loops[mate.loop_index];
716 let position = loop_record
717 .coedges
718 .iter()
719 .position(|coedge| coedge.edge_id == edge.id)
720 .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
721 let Some((new_edge_id, (_, pcurve_forward))) = rail else {
722 loop_record.coedges.remove(position);
723 continue;
724 };
725 let old_forward = loop_record.coedges[position].forward;
726 loop_record.coedges[position] = CoedgeRecord {
727 id: loop_record.coedges[position].id,
728 edge_id: new_edge_id,
729 forward: old_forward,
730 pcurve: if old_forward {
731 pcurve_forward.clone()
732 } else {
733 pcurve_forward.reversed().or_refuse(KernelStage::Refine, "reversed")?
734 },
735 };
736 }
737
738 for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
743 let EndPlan::Cap(cap) = plan else {
744 continue;
745 };
746 for (mate, rail_edge_id, leg, rim) in [
747 (first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
748 (second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
749 ] {
750 let rail_edge_id = rail_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "end_rail", "blend: a capped end lost its rail"))?;
752 let face = result
753 .shells
754 .iter_mut()
755 .flat_map(|shell| &mut shell.faces)
756 .find(|face| face.id == mate.face.id)
757 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during cap splice"))?;
758 let loop_record = &mut face.loops[mate.loop_index];
759 let count = loop_record.coedges.len();
760 let rail_at = loop_record
761 .coedges
762 .iter()
763 .position(|coedge| coedge.edge_id == rail_edge_id)
764 .ok_or(KernelRefusal::internal(KernelStage::Sew, "rail_coedge", "blend: rail coedge lost during cap splice"))?;
765 let rail_forward = loop_record.coedges[rail_at].forward;
771 let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
772 let (insert_at, forward, pcurve) = if end_is_traversal_end {
773 (rail_at + 1, true, leg.1.clone())
775 } else {
776 (rail_at, false, leg.1.reversed().or_refuse(KernelStage::Refine, "reversed")?)
778 };
779 let _ = (count, rim);
780 loop_record.coedges.insert(
781 insert_at,
782 CoedgeRecord {
783 id: take_id(),
784 edge_id: leg.0,
785 forward,
786 pcurve,
787 },
788 );
789 }
790 }
791
792 let mut consumed_edges: Vec<u64> = Vec::new();
798 let start_free = start_end.free();
799 let finish_free = finish_end.free();
800 for (mate, sewn, sides) in [
801 (
802 first,
803 sew_first.is_some(),
804 [
805 (&start_first, start_free.map(|free| free.first_edge_id)),
806 (&finish_first, finish_free.map(|free| free.first_edge_id)),
807 ],
808 ),
809 (
810 second,
811 sew_second.is_some(),
812 [
813 (&start_second, start_free.map(|free| free.second_edge_id)),
814 (&finish_second, finish_free.map(|free| free.second_edge_id)),
815 ],
816 ),
817 ] {
818 for (cross, boundary_id) in sides {
819 if !cross.is_consumed() {
820 continue;
821 }
822 let Some(boundary_id) = boundary_id else {
823 continue;
824 };
825 if !sewn {
826 let face = result
827 .shells
828 .iter_mut()
829 .flat_map(|shell| &mut shell.faces)
830 .find(|face| face.id == mate.face.id)
831 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
832 face.loops[mate.loop_index]
833 .coedges
834 .retain(|coedge| coedge.edge_id != boundary_id);
835 }
836 if !consumed_edges.contains(&boundary_id) {
837 consumed_edges.push(boundary_id);
838 }
839 }
840 }
841 for sew in [&sew_first, &sew_second].into_iter().flatten() {
843 for id in &sew.collapsed_edges {
844 if !consumed_edges.contains(id) {
845 consumed_edges.push(*id);
846 }
847 }
848 }
849
850 let mut end_plans = Vec::with_capacity(2);
859 for (end, transverse_id, corner, first_consumed, second_consumed) in [
860 (
861 start_end.free(),
862 transverse_a_id,
863 old_start,
864 start_first.is_consumed(),
865 start_second.is_consumed(),
866 ),
867 (
868 finish_end.free(),
869 transverse_b_id,
870 old_finish,
871 finish_first.is_consumed(),
872 finish_second.is_consumed(),
873 ),
874 ] {
875 let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
876 continue;
877 };
878 let face = result
879 .shells
880 .iter()
881 .flat_map(|shell| &shell.faces)
882 .find(|face| face.id == end.end_face_id)
883 .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_face", "blend: end face lost during surgery"))?;
884 let Some(located) = locate_end_corner(
887 &result,
888 face,
889 corner,
890 end.first_edge_id,
891 end.second_edge_id,
892 ) else {
893 return Err(KernelRefusal::internal(KernelStage::Sew, "end_face_corner", "blend: end-face corner (adjacent boundary coedges) not found"));
894 };
895 for pole in &located.pole_edge_ids {
898 if !consumed_edges.contains(pole) {
899 consumed_edges.push(*pole);
900 }
901 }
902 end_plans.push((
903 end.end_face_id,
904 transverse_id,
905 end.transverse_end_pcurve.clone(),
906 located,
907 first_consumed,
908 second_consumed,
909 ));
910 }
911
912 for (rim_resolution, boundary, corner, rim) in [
915 (
916 &start_first,
917 start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
918 old_start,
919 w1a,
920 ),
921 (
922 &start_second,
923 start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
924 old_start,
925 w2a,
926 ),
927 (
928 &finish_first,
929 finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
930 old_finish,
931 w1b,
932 ),
933 (
934 &finish_second,
935 finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
936 old_finish,
937 w2b,
938 ),
939 ] {
940 if !rim_resolution.trims_boundary() {
941 continue;
942 }
943 let Some((boundary_id, boundary_param)) = boundary else {
944 continue;
945 };
946 trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
947 }
948
949 for (end_face, transverse_id, transverse_end_pcurve, located, first_consumed, second_consumed) in
952 end_plans
953 {
954 let forward = located.x_is_first;
955 let pcurve = if forward {
956 transverse_end_pcurve
957 } else {
958 transverse_end_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
959 };
960 let (x_consumed, y_consumed) = if located.x_is_first {
961 (first_consumed, second_consumed)
962 } else {
963 (second_consumed, first_consumed)
964 };
965 let transverse_coedge = CoedgeRecord {
966 id: take_id(),
967 edge_id: transverse_id,
968 forward,
969 pcurve,
970 };
971 splice_end_corner(
972 result,
973 end_face,
974 &located,
975 transverse_coedge,
976 x_consumed,
977 y_consumed,
978 )?;
979 }
980
981 let (mid_u, station_uv) = match (&cr_rail, &cs_rail) {
986 (Some((_, cr_pcurve)), _) => {
987 let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
988 (mid_u, cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
989 }
990 (None, Some(_)) => {
991 let mid_u = (cs_domain[0] + cs_domain[1]) * 0.5;
992 (mid_u, rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
993 }
994 (None, None) => (cr_domain[0], rows.cr_pcurve.evaluate(cr_domain[0]).or_refuse(KernelStage::Refine, "evaluate")?),
995 };
996 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
997 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
998 let out1 = if first.face.same_sense {
999 n1
1000 } else {
1001 n1.scale(-1.0)
1002 };
1003 let same_sense = blend_normal.dot(out1) >= 0.0;
1004 let loop_id = take_id();
1005 let cr_forward = sew_first
1013 .as_ref()
1014 .map_or(blend_cr_forward, |sew| sew.forward);
1015 let cs_forward = sew_second
1016 .as_ref()
1017 .map_or(!blend_cr_forward, |sew| sew.forward);
1018 let mut end_slot = |plan: &EndPlan,
1028 free_edge_id: Option<u64>,
1029 natural_forward: bool|
1030 -> Result<Vec<CoedgeRecord>, KernelRefusal> {
1031 match plan {
1032 EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
1033 id: take_id(),
1034 edge_id: end.arc_edge_id,
1035 forward: true,
1036 pcurve: end.arc_blend_pcurve.clone(),
1037 }]),
1038 EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
1039 id: take_id(),
1040 edge_id: end.arc_edge_id,
1041 forward: true,
1042 pcurve: end.arc_blend_pcurve.clone(),
1043 }]),
1044 EndPlan::Miter(end) => end
1045 .edges
1046 .iter()
1047 .map(|(edge_id, forward, pcurve)| {
1048 Ok(CoedgeRecord {
1049 id: take_id(),
1050 edge_id: *edge_id,
1051 forward: *forward,
1052 pcurve: pcurve.clone(),
1053 })
1054 })
1055 .collect(),
1056 EndPlan::Free(free) => {
1057 let edge_id =
1058 free_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "transverse_edge", "blend: free end without its transverse edge"))?;
1059 Ok(vec![if natural_forward {
1060 CoedgeRecord {
1061 id: take_id(),
1062 edge_id,
1063 forward: true,
1064 pcurve: free.transverse_blend_pcurve.clone(),
1065 }
1066 } else {
1067 CoedgeRecord {
1068 id: take_id(),
1069 edge_id,
1070 forward: false,
1071 pcurve: free.transverse_blend_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?,
1072 }
1073 }])
1074 }
1075 }
1076 };
1077 let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
1078 let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
1079 let coedges = if blend_cr_forward {
1082 let mut coedges = Vec::new();
1083 if let Some(cr_edge_id) = cr_edge_id {
1084 coedges.push(CoedgeRecord {
1085 id: take_id(),
1086 edge_id: cr_edge_id,
1087 forward: cr_forward,
1088 pcurve: crate::sweep_topology::parameter_line(
1089 cr_domain[0],
1090 0.0,
1091 cr_domain[1],
1092 0.0,
1093 ).or_refuse(KernelStage::Refine, "parameter_line")?,
1094 });
1095 }
1096 coedges.extend(finish_slot);
1097 if let Some(cs_edge_id) = cs_edge_id {
1098 coedges.push(CoedgeRecord {
1099 id: take_id(),
1100 edge_id: cs_edge_id,
1101 forward: cs_forward,
1102 pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
1103 });
1104 }
1105 coedges.extend(start_slot);
1106 coedges
1107 } else {
1108 let mut coedges = Vec::new();
1109 if let Some(cr_edge_id) = cr_edge_id {
1110 coedges.push(CoedgeRecord {
1111 id: take_id(),
1112 edge_id: cr_edge_id,
1113 forward: cr_forward,
1114 pcurve: crate::sweep_topology::parameter_line(
1115 cr_domain[1],
1116 0.0,
1117 cr_domain[0],
1118 0.0,
1119 ).or_refuse(KernelStage::Refine, "parameter_line")?,
1120 });
1121 }
1122 coedges.extend(start_slot);
1123 if let Some(cs_edge_id) = cs_edge_id {
1124 coedges.push(CoedgeRecord {
1125 id: take_id(),
1126 edge_id: cs_edge_id,
1127 forward: cs_forward,
1128 pcurve: crate::sweep_topology::parameter_line(
1129 cs_domain[0],
1130 1.0,
1131 cs_domain[1],
1132 1.0,
1133 ).or_refuse(KernelStage::Refine, "parameter_line")?,
1134 });
1135 }
1136 coedges.extend(finish_slot);
1137 coedges
1138 };
1139 let blend_face_id = take_id();
1140 let mut blend_face = FaceRecord {
1141 id: blend_face_id,
1142 surface: rows.surface,
1143 same_sense,
1144 loops: vec![LoopRecord {
1145 id: loop_id,
1146 coedges,
1147 }],
1148 name: name.map(|value| value.to_string()),
1149 };
1150 if rows.exact_extrusion {
1151 transpose_face(&mut blend_face)?;
1152 }
1153 let shell_index = result
1154 .shells
1155 .iter()
1156 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
1157 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
1158 for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
1163 if sew.is_some() {
1164 for shell in &mut result.shells {
1165 shell.faces.retain(|face| face.id != mate.face.id);
1166 }
1167 }
1168 }
1169 result.shells[shell_index].faces.push(blend_face);
1170
1171 result.edges.retain(|candidate| candidate.id != edge.id);
1172 result
1175 .edges
1176 .retain(|candidate| !consumed_edges.contains(&candidate.id));
1177 let mut identified = Vec::new();
1179 for (collapsed, from, to) in [(cr_collapsed, w1a, w1b), (cs_collapsed, w2a, w2b)] {
1180 if collapsed && from != to {
1181 identified.push((from, to));
1182 }
1183 }
1184 Ok(SewnStripe {
1185 cr_edge_id,
1186 cs_edge_id,
1187 blend_face_id,
1188 consumed: [sew_first.is_some(), sew_second.is_some()],
1189 snap,
1190 identified,
1191 })
1192}
1193
1194pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), KernelRefusal> {
1200 let surface = &face.surface;
1201 let rows_u = surface.control_points.len();
1202 let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
1203 let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
1204 for row in &surface.control_points {
1205 for (j, point) in row.iter().enumerate() {
1206 transposed[j].push(*point);
1207 }
1208 }
1209 face.surface = NurbsSurface::new(
1210 surface.degree_v,
1211 surface.degree_u,
1212 surface.knots_v.clone(),
1213 surface.knots_u.clone(),
1214 transposed,
1215 ).or_refuse(KernelStage::Refine, "new")?;
1216 for loop_record in &mut face.loops {
1217 for coedge in &mut loop_record.coedges {
1218 for control in &mut coedge.pcurve.control_points {
1219 std::mem::swap(&mut control.x, &mut control.y);
1220 }
1221 }
1222 }
1223 face.same_sense = !face.same_sense;
1224 Ok(())
1225}
1226
1227pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
1232 let used: rustc_hash::FxHashSet<u64> = result
1233 .edges
1234 .iter()
1235 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
1236 .collect();
1237 result
1238 .vertices
1239 .retain(|candidate| used.contains(&candidate.id));
1240}
1241
1242fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
1243 crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")
1244}
1245