1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use super::*;
3
4pub fn blend_closed_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_closed_edge_impl(solid, edge_id, &|_| radius, chamfer, name)
17}
18
19pub fn blend_edge_variable(
23 solid: &BrepSolid,
24 edge_id: u64,
25 radii: &[(f64, f64)],
26 chamfer: bool,
27 name: Option<&str>,
28) -> Result<BrepSolid, KernelRefusal> {
29 if radii.is_empty() || radii.iter().any(|(_, radius)| !(*radius > 0.0)) {
30 return Err(KernelRefusal::input(KernelStage::Collect, "radius_stop", "blend: every radius stop must be positive"));
31 }
32 let mut stops = radii.to_vec();
33 stops.sort_by(|a, b| a.0.total_cmp(&b.0));
34 let edge = solid
35 .edges
36 .iter()
37 .find(|edge| edge.id == edge_id)
38 .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
39 let closed = edge.start_vertex_id == edge.end_vertex_id;
40 if closed && (stops[0].1 - stops[stops.len() - 1].1).abs() > 1e-12 {
41 return Err(KernelRefusal::input(KernelStage::Collect, "closed_edge_radii", "blend: closed edges need equal first/last radii"));
42 }
43 let first_radius = stops[0].1;
52 if stops
53 .iter()
54 .all(|(_, radius)| (radius - first_radius).abs() <= 1e-12 * (1.0 + first_radius.abs()))
55 {
56 return if chamfer {
57 crate::fillet::chamfer_edge(solid, edge_id, first_radius, name)
58 } else {
59 crate::fillet::fillet_edge(solid, edge_id, first_radius, name)
60 };
61 }
62 let (t0, t1) = (edge.t0, edge.t1);
63 let radius_at = move |t: f64| -> f64 {
64 let raw = (t - t0) / (t1 - t0);
65 let fraction = if closed {
73 raw.rem_euclid(1.0)
74 } else {
75 raw.clamp(0.0, 1.0)
76 };
77 if fraction <= stops[0].0 {
78 return stops[0].1;
79 }
80 for pair in stops.windows(2) {
81 if fraction <= pair[1].0 {
82 let width = (pair[1].0 - pair[0].0).max(1e-12);
83 let local = (fraction - pair[0].0) / width;
84 return pair[0].1 + (pair[1].1 - pair[0].1) * local;
85 }
86 }
87 stops[stops.len() - 1].1
88 };
89 if closed {
90 blend_closed_edge_impl(solid, edge_id, &radius_at, chamfer, name)
91 } else {
92 blend_open_edge_impl(solid, edge_id, &radius_at, chamfer, name)
93 }
94}
95
96fn blend_closed_edge_impl(
97 solid: &BrepSolid,
98 edge_id: u64,
99 radius_at: &dyn Fn(f64) -> f64,
100 chamfer: bool,
101 name: Option<&str>,
102) -> Result<BrepSolid, KernelRefusal> {
103 let edge = solid
104 .edges
105 .iter()
106 .find(|edge| edge.id == edge_id)
107 .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
108 if edge.start_vertex_id != edge.end_vertex_id {
109 return Err(KernelRefusal::input(KernelStage::Collect, "closed_edge", "blend: general path currently requires a CLOSED edge"));
110 }
111 let (face_a, loop_a, coedge_a) = locate_mate(solid, edge_id, None)?;
112 let (face_b, loop_b, coedge_b) = locate_mate(solid, edge_id, Some((face_a.id, loop_a)))?;
113 if face_a.id == face_b.id && loop_a == loop_b {
114 return Err(KernelRefusal::unsupported(KernelStage::Classify, "seam_edge", "blend: edge is used twice by one loop (seam edge?)"));
115 }
116
117 let seam_a = face_a.loops[loop_a].coedges.len() > 1;
120 let seam_b = face_b.loops[loop_b].coedges.len() > 1;
121 let (first_face, first_loop, first_coedge, second_face, second_loop, second_coedge) =
122 if seam_a || !seam_b {
123 (face_a, loop_a, coedge_a, face_b, loop_b, coedge_b)
124 } else {
125 (face_b, loop_b, coedge_b, face_a, loop_a, coedge_a)
126 };
127 let anchored = first_face.loops[first_loop].coedges.len() > 1;
128 let second_seam = anchored && second_face.loops[second_loop].coedges.len() > 1;
129 let anchor_u = if anchored {
130 Some(first_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?[0])
132 } else {
133 None
134 };
135
136 let mid_radius = radius_at(edge.t0 + (edge.t1 - edge.t0) * 0.5);
138 let (rho1, rho2) = signed_radii(
139 edge,
140 first_face,
141 first_coedge,
142 second_face,
143 second_coedge,
144 mid_radius,
145 )?;
146
147 let first_mate = BlendMate {
148 face: first_face,
149 coedge: first_coedge,
150 loop_index: first_loop,
151 rho: rho1,
152 };
153 let second_mate = BlendMate {
154 face: second_face,
155 coedge: second_coedge,
156 loop_index: second_loop,
157 rho: rho2,
158 };
159 let mid_t = edge.t0 + (edge.t1 - edge.t0) * 0.5;
160 let marched = match march_stations(edge, &first_mate, &second_mate, radius_at, anchor_u) {
175 Ok(stations) => Ok(stations),
176 Err(anchored_error) if anchor_u.is_some() => {
177 march_stations(edge, &first_mate, &second_mate, radius_at, None).map_err(
178 |retry_error| {
179 if crate::blend::is_wall_fold(&retry_error) {
185 retry_error
186 } else {
187 anchored_error
188 }
189 },
190 )
191 }
192 Err(anchored_error) => Err(anchored_error),
193 };
194 let stations = match marched {
195 Ok(stations) => stations,
196 Err(march_error) if crate::blend::is_wall_fold(&march_error) => return Err(march_error),
202 Err(march_error) if crate::blend::is_ball_off_carrier(&march_error) => {
203 return Err(march_error);
210 }
211 Err(march_error) => {
212 return blend_closed_edge_keep(
215 solid,
216 edge,
217 &first_mate,
218 &second_mate,
219 radius_at(mid_t),
220 rho1,
221 chamfer,
222 name,
223 )
224 .or_else(|_| {
225 blend_closed_edge_keep(
226 solid,
227 edge,
228 &second_mate,
229 &first_mate,
230 radius_at(mid_t),
231 rho2,
232 chamfer,
233 name,
234 )
235 })
236 .map_err(|keep_error| {
237 format!("{march_error}; edge-preserving blend also failed: {keep_error}")
238 }).or_refuse(KernelStage::Refine, "map_err");
239 }
240 };
241 let support_exits = |face: &FaceRecord, second_side: bool| -> bool {
245 let stride = (stations.len() / 16).max(1);
249 stations.iter().step_by(stride).any(|station| {
250 let mut uv = if second_side {
251 station.uv2
252 } else {
253 station.uv1
254 };
255 if let Ok((closed_u, closed_v)) = face.surface.closed_directions() {
256 if closed_u {
257 if let Ok([low, high]) = face.surface.domain_u() {
258 uv[0] = low + (uv[0] - low).rem_euclid(high - low);
259 }
260 }
261 if closed_v {
262 if let Ok([low, high]) = face.surface.domain_v() {
263 uv[1] = low + (uv[1] - low).rem_euclid(high - low);
264 }
265 }
266 }
267 crate::parameter_point_in_face(face, crate::Vec2 { x: uv[0], y: uv[1] }, 1e-6)
268 .map(|class| class == crate::PolygonClass::Outside)
269 .unwrap_or(true)
270 })
271 };
272 if support_exits(second_face, true) {
273 return blend_closed_edge_keep(
274 solid,
275 edge,
276 &first_mate,
277 &second_mate,
278 radius_at(mid_t),
279 rho1,
280 chamfer,
281 name,
282 );
283 }
284 if support_exits(first_face, false) {
285 return blend_closed_edge_keep(
286 solid,
287 edge,
288 &second_mate,
289 &first_mate,
290 radius_at(mid_t),
291 rho2,
292 chamfer,
293 name,
294 );
295 }
296 if std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok() {
297 let last = stations.len() - 1;
298 for index in [
299 0usize,
300 1.min(last),
301 2.min(last),
302 last.saturating_sub(1),
303 last,
304 ] {
305 let station = &stations[index];
306 eprintln!(
307 "station {index}: uv1=({:.6},{:.6}) uv2=({:.6},{:.6}) p1=({:.6},{:.6},{:.6}) w={:.6}",
308 station.uv1[0], station.uv1[1], station.uv2[0], station.uv2[1],
309 station.p1.x, station.p1.y, station.p1.z, station.weight,
310 );
311 }
312 }
313 let parameters = station_parameters(&stations);
314 let rows = match exact_closed_revolution_rows(&stations, &first_mate, &second_mate, chamfer) {
315 Some(rows) => rows?,
316 None => fit_closed_rows(&stations, ¶meters, chamfer)?,
317 };
318
319 let second_split = if second_seam {
323 let seam2 = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?[0];
324 let period2 = {
325 let [d0, d1] = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
326 d1 - d0
327 };
328 let u2_first = stations[0].uv2[0];
330 let u2_last = stations[stations.len() - 1].uv2[0];
331 let direction = (u2_last - u2_first).signum();
332 let mut k = ((u2_first - seam2) / period2).ceil();
333 if direction < 0.0 {
334 k = ((u2_first - seam2) / period2).floor();
335 }
336 let target = seam2 + k * period2;
337 let inside =
338 (target - u2_first) * direction > 1e-6 && (u2_last - target) * direction > 1e-6;
339 let aligned = (target - u2_first).abs() <= 1e-6
344 || (target - u2_last).abs() <= 1e-6
345 || ((u2_first - seam2) / period2).fract().abs() <= 1e-9;
346 if !inside && aligned {
347 None
348 } else if !inside {
349 return Err(KernelRefusal::internal(KernelStage::Refine, "seam_crossing_bracket", "blend: second seam crossing not bracketed by the march"));
350 } else {
351 let [fit_low, fit_high] = rows.u_domain;
355 let mut p = fit_low
356 + (fit_high - fit_low) * {
357 let mut seed = 0.5;
359 for pair in 0..stations.len() - 1 {
360 let a = stations[pair].uv2[0];
361 let b = stations[pair + 1].uv2[0];
362 if (target - a) * (target - b) <= 0.0 {
363 let local = (target - a) / (b - a);
364 seed = (parameters[pair]
365 + (parameters[pair + 1] - parameters[pair]) * local)
366 .clamp(0.0, 1.0);
367 break;
368 }
369 }
370 seed
371 };
372 for _ in 0..NEWTON_ITERATIONS {
373 let value = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
374 if value.abs() <= 1e-12 {
375 break;
376 }
377 let step = 1e-8;
378 let probed = rows.cs_pcurve.evaluate(p + step).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
379 let derivative = (probed - value) / step;
380 if derivative.abs() <= 1e-14 {
381 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "seam_crossing_newton", "blend: second seam crossing Newton stalled"));
382 }
383 p -= value / derivative;
384 }
385 let crossing_v = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.y;
386 Some(SecondSeamSplit {
387 fit_parameter: p,
388 crossing_v,
389 period: period2 * direction,
390 })
391 }
392 } else {
393 None
394 };
395
396 build_surgery(
397 solid,
398 edge,
399 &first_mate,
400 &second_mate,
401 rows,
402 second_split,
403 name,
404 )
405}
406
407struct SecondSeamSplit {
410 fit_parameter: f64,
411 crossing_v: f64,
412 period: f64,
414}
415
416fn build_surgery(
419 solid: &BrepSolid,
420 edge: &EdgeRecord,
421 first: &BlendMate,
422 second: &BlendMate,
423 rows: FittedRows,
424 second_split: Option<SecondSeamSplit>,
425 name: Option<&str>,
426) -> Result<BrepSolid, KernelRefusal> {
427 let mut result = solid.clone();
428 let mut take_id = crate::blend::edge::fresh_id_source(solid);
429
430 let [u_start, u_end] = rows.u_domain;
431 let cr_start = rows.cr.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
432 let cs_start = rows.cs.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
433 let vertex1_id = take_id();
434 let vertex2_id = take_id();
435 result.vertices.push(VertexRecord {
436 id: vertex1_id,
437 point: cr_start,
438 });
439 result.vertices.push(VertexRecord {
440 id: vertex2_id,
441 point: cs_start,
442 });
443
444 let cr_edge_id = take_id();
445 result.edges.push(EdgeRecord {
446 id: cr_edge_id,
447 curve: rows.cr.clone(),
448 t0: u_start,
449 t1: u_end,
450 start_vertex_id: vertex1_id,
451 end_vertex_id: vertex1_id,
452 degenerate: false,
453 name: None,
454 });
455 let mut cs_pieces: Vec<(u64, [f64; 2], NurbsCurve)> = Vec::new();
459 let mut second_seam_vertex = vertex2_id;
460 let mut second_seam_point = cs_start;
461 let mut second_crossing_v = 0.0;
462 if let Some(split) = &second_split {
463 let p = split.fit_parameter;
464 second_crossing_v = split.crossing_v;
465 let (cs_a, cs_b) = rows.cs.split(p).or_refuse(KernelStage::Refine, "split")?;
466 let (pc_a, pc_b) = rows.cs_pcurve.split(p).or_refuse(KernelStage::Refine, "split")?;
467 let mut pc_b = pc_b;
469 for point in pc_b.control_points.iter_mut() {
470 point.x -= split.period * point.w;
471 }
472 let w2s = take_id();
473 second_seam_vertex = w2s;
474 second_seam_point = rows.cs.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?;
475 result.vertices.push(VertexRecord {
476 id: w2s,
477 point: second_seam_point,
478 });
479 let edge_a = take_id();
480 result.edges.push(EdgeRecord {
481 id: edge_a,
482 curve: cs_a,
483 t0: u_start,
484 t1: p,
485 start_vertex_id: vertex2_id,
486 end_vertex_id: w2s,
487 degenerate: false,
488 name: None,
489 });
490 cs_pieces.push((edge_a, [u_start, p], pc_a));
491 let edge_b = take_id();
492 result.edges.push(EdgeRecord {
493 id: edge_b,
494 curve: cs_b,
495 t0: p,
496 t1: u_end,
497 start_vertex_id: w2s,
498 end_vertex_id: vertex2_id,
499 degenerate: false,
500 name: None,
501 });
502 cs_pieces.push((edge_b, [p, u_end], pc_b));
503 } else {
504 let cs_edge_id = take_id();
505 result.edges.push(EdgeRecord {
506 id: cs_edge_id,
507 curve: rows.cs.clone(),
508 t0: u_start,
509 t1: u_end,
510 start_vertex_id: vertex2_id,
511 end_vertex_id: vertex2_id,
512 degenerate: false,
513 name: None,
514 });
515 cs_pieces.push((cs_edge_id, [u_start, u_end], rows.cs_pcurve.clone()));
516 }
517 let seam_curve = rows.surface.iso_curve_u(u_start).or_refuse(KernelStage::Refine, "iso_curve_u")?;
518 let [seam_t0, seam_t1] = seam_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
519 let blend_seam_id = take_id();
520 result.edges.push(EdgeRecord {
521 id: blend_seam_id,
522 curve: seam_curve,
523 t0: seam_t0,
524 t1: seam_t1,
525 start_vertex_id: vertex1_id,
526 end_vertex_id: vertex2_id,
527 degenerate: false,
528 name: None,
529 });
530
531 let old_vertex = edge.start_vertex_id;
532 let replace_in_face = |result: &mut BrepSolid,
533 mate: &BlendMate,
534 pieces: &[(u64, [f64; 2], NurbsCurve)],
535 uv_new: [f64; 2]|
536 -> Result<bool, KernelRefusal> {
537 let v_new = uv_new[1];
538 let seam_edge_ends: Vec<(u64, bool, bool)> = result
540 .edges
541 .iter()
542 .filter(|candidate| {
543 candidate.id != edge.id
544 && (candidate.start_vertex_id == old_vertex
545 || candidate.end_vertex_id == old_vertex)
546 })
547 .map(|candidate| {
548 (
549 candidate.id,
550 candidate.start_vertex_id == old_vertex,
551 candidate.end_vertex_id == old_vertex,
552 )
553 })
554 .collect();
555 let seam_edge_ids: Vec<u64> = seam_edge_ends.iter().map(|(id, ..)| *id).collect();
556 let face = result
557 .shells
558 .iter_mut()
559 .flat_map(|shell| &mut shell.faces)
560 .find(|face| face.id == mate.face.id)
561 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
562 let loop_record = &mut face.loops[mate.loop_index];
563 let position = loop_record
564 .coedges
565 .iter()
566 .position(|coedge| coedge.edge_id == edge.id)
567 .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
568 let old_forward = loop_record.coedges[position].forward;
569 let old_seam_v = {
570 let pcurve = &loop_record.coedges[position].pcurve;
571 let [d0, _] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
572 pcurve.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?.y
573 };
574 let mut replacements = Vec::new();
577 let ordered: Vec<&(u64, [f64; 2], NurbsCurve)> = if old_forward {
578 pieces.iter().collect()
579 } else {
580 pieces.iter().rev().collect()
581 };
582 for (index, (edge_id, _, pcurve)) in ordered.iter().enumerate() {
583 replacements.push(CoedgeRecord {
584 id: if index == 0 {
585 loop_record.coedges[position].id
586 } else {
587 0 },
589 edge_id: *edge_id,
590 forward: old_forward,
591 pcurve: if old_forward {
592 pcurve.clone()
593 } else {
594 pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
595 },
596 });
597 }
598 loop_record
599 .coedges
600 .splice(position..=position, replacements);
601 let mut repaired = false;
602 if loop_record.coedges.len() > 1 {
603 for coedge in &mut loop_record.coedges {
609 if pieces
610 .iter()
611 .any(|(edge_id, ..)| *edge_id == coedge.edge_id)
612 || !seam_edge_ids.contains(&coedge.edge_id)
613 {
614 continue;
615 }
616 let controls = &mut coedge.pcurve.control_points;
617 let last_index = controls.len() - 1;
618 let first_u = controls[0].x / controls[0].w;
619 let last_u = controls[last_index].x / controls[last_index].w;
620 let constant_u = (first_u - last_u).abs()
621 <= 1e-9 * (1.0 + first_u.abs().max(last_u.abs()));
622 if constant_u {
623 let first_v = controls[0].y / controls[0].w;
627 let last_v = controls[last_index].y / controls[last_index].w;
628 let target = if (first_v - old_seam_v).abs() < (last_v - old_seam_v).abs() {
629 0
630 } else {
631 last_index
632 };
633 let w = controls[target].w;
634 controls[target].y = v_new * w;
635 continue;
636 }
637 let Some((_, start_is_old, end_is_old)) = seam_edge_ends
646 .iter()
647 .find(|(id, ..)| *id == coedge.edge_id)
648 .copied()
649 else {
650 continue;
651 };
652 if start_is_old == end_is_old {
653 return Err(KernelRefusal::ill_posed(KernelStage::Sew, "neighbour_trim_end", format!(
654 "blend: neighbour edge {} meets the blended edge at both ends; \
655 its trim end is ambiguous",
656 coedge.edge_id
657 )));
658 }
659 let target = if start_is_old == coedge.forward {
662 0
663 } else {
664 last_index
665 };
666 let w = controls[target].w;
667 controls[target].x = uv_new[0] * w;
668 repaired = true;
669 }
670 }
671 Ok(repaired)
672 };
673 let uv1_new = rows.cr_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
674 let v1_new = uv1_new.y;
675 let cr_pieces = vec![(cr_edge_id, [u_start, u_end], rows.cr_pcurve.clone())];
676 let mut repaired = replace_in_face(&mut result, first, &cr_pieces, [uv1_new.x, v1_new])?;
677 let uv2_new = rows.cs_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
678 let v2_new = if second_split.is_some() {
679 second_crossing_v
680 } else {
681 uv2_new.y
682 };
683 repaired |= replace_in_face(&mut result, second, &cs_pieces, [uv2_new.x, v2_new])?;
684
685 let collapse_winding_sphere_cap =
691 |result: &mut BrepSolid, face_id: u64, support_ids: &[u64]| -> Result<(), KernelRefusal> {
692 let face = result
693 .shells
694 .iter_mut()
695 .flat_map(|shell| &mut shell.faces)
696 .find(|face| face.id == face_id)
697 .ok_or(KernelRefusal::internal(KernelStage::Sew, "sphere_carrier", "blend: sphere carrier lost during cap surgery"))?;
698 if !matches!(
699 face.surface.analytic(),
700 Some(crate::AnalyticSurface::Sphere { .. })
701 ) || face.surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")? != (true, false)
702 {
703 return Ok(());
704 }
705 let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
706 let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
707 let period = u1 - u0;
708 let support_loop = face.loops.iter().position(|loop_record| {
709 !loop_record.coedges.is_empty()
710 && loop_record
711 .coedges
712 .iter()
713 .all(|coedge| support_ids.contains(&coedge.edge_id))
714 });
715 let Some(support_loop) = support_loop else {
716 return Ok(());
717 };
718 if face.loops[support_loop].coedges.len() != 1 {
719 return Ok(());
720 }
721 let support = &face.loops[support_loop].coedges[0];
722 let [s0, s1] = support.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
723 let support_start = support.pcurve.evaluate(s0).or_refuse(KernelStage::Refine, "evaluate")?;
724 let support_end = support.pcurve.evaluate(s1).or_refuse(KernelStage::Refine, "evaluate")?;
725 let support_winding = support_end.x - support_start.x;
726 if (support_winding.abs() - period).abs() > 0.05 * period {
727 return Ok(());
728 }
729
730 let mut pole: Option<(usize, CoedgeRecord)> = None;
731 for (loop_index, loop_record) in face.loops.iter().enumerate() {
732 if loop_index == support_loop {
733 continue;
734 }
735 for coedge in &loop_record.coedges {
736 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
737 let start = coedge.pcurve.evaluate(p0).or_refuse(KernelStage::Refine, "evaluate")?;
738 let end = coedge.pcurve.evaluate(p1).or_refuse(KernelStage::Refine, "evaluate")?;
739 let winding = end.x - start.x;
740 let at_pole = (start.y - v0).abs() <= 1e-8 || (start.y - v1).abs() <= 1e-8;
741 if at_pole
742 && (end.y - start.y).abs() <= 1e-8
743 && (winding.abs() - period).abs() <= 0.05 * period
744 && winding * support_winding < 0.0
745 {
746 pole = Some((loop_index, coedge.clone()));
747 break;
748 }
749 }
750 if pole.is_some() {
751 break;
752 }
753 }
754 let Some((pole_loop, pole_coedge)) = pole else {
755 return Ok(());
756 };
757 let support_record = face.loops[support_loop].clone();
758 let pole_id = face.loops[pole_loop].id;
759 face.loops = vec![
760 LoopRecord {
761 id: pole_id,
762 coedges: vec![pole_coedge],
763 },
764 support_record,
765 ];
766 Ok(())
767 };
768 collapse_winding_sphere_cap(&mut result, first.face.id, &[cr_edge_id])?;
769 let cs_edge_ids: Vec<u64> = cs_pieces.iter().map(|(edge_id, ..)| *edge_id).collect();
770 collapse_winding_sphere_cap(&mut result, second.face.id, &cs_edge_ids)?;
771 for shell in &mut result.shells {
773 for face in &mut shell.faces {
774 for loop_record in &mut face.loops {
775 for coedge in &mut loop_record.coedges {
776 if coedge.id == 0 {
777 coedge.id = take_id();
778 }
779 }
780 }
781 }
782 }
783
784 let new_edge_ids: Vec<u64> = std::iter::once(cr_edge_id)
789 .chain(cs_pieces.iter().map(|(edge_id, ..)| *edge_id))
790 .chain(std::iter::once(blend_seam_id))
791 .collect();
792 let first_face_edges: Vec<u64> = first
793 .face
794 .loops
795 .iter()
796 .flat_map(|loop_record| loop_record.coedges.iter().map(|coedge| coedge.edge_id))
797 .collect();
798 for seam_edge in result.edges.iter_mut() {
799 if new_edge_ids.contains(&seam_edge.id) {
800 continue;
801 }
802 if seam_edge.start_vertex_id != old_vertex && seam_edge.end_vertex_id != old_vertex {
803 continue;
804 }
805 let (target_vertex, target_v, target_point) = if first_face_edges.contains(&seam_edge.id) {
806 (vertex1_id, v1_new, cr_start)
807 } else {
808 (second_seam_vertex, v2_new, second_seam_point)
809 };
810 let [c0, c1] = seam_edge.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
811 let clamped = target_v.clamp(c0.min(c1), c0.max(c1));
812 let band = 1e-6 * (1.0 + target_point.length());
821 let split_parameter = if seam_edge.id == edge.id {
822 clamped
828 } else {
829 match seam_edge.curve.evaluate(clamped) {
830 Ok(point) if point.sub(target_point).length() <= band => clamped,
831 _ => {
832 repaired = true;
833 let projection =
834 crate::project_point_to_curve(&seam_edge.curve, target_point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
835 if projection.distance > band {
836 return Err(KernelRefusal::unsupported(KernelStage::Sew, "neighbour_off_support", format!(
837 "blend: neighbour edge {} does not pass through the blend's support \
838 start (off by {:.9}); the trim has no parameter to move to",
839 seam_edge.id, projection.distance
840 )));
841 }
842 projection.u.clamp(c0.min(c1), c0.max(c1))
843 }
844 }
845 };
846 if seam_edge.start_vertex_id == old_vertex {
847 seam_edge.t0 = split_parameter;
848 seam_edge.start_vertex_id = target_vertex;
849 }
850 if seam_edge.end_vertex_id == old_vertex {
851 seam_edge.t1 = split_parameter;
852 seam_edge.end_vertex_id = target_vertex;
853 }
854 }
855
856 let mid_u = (u_start + u_end) * 0.5;
858 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
859 let station_uv = rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?;
860 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
861 let out1 = if first.face.same_sense {
862 n1
863 } else {
864 n1.scale(-1.0)
865 };
866 let same_sense = blend_normal.dot(out1) >= 0.0;
867 let loop_id = take_id();
868 let mut coedges = vec![
869 CoedgeRecord {
870 id: take_id(),
871 edge_id: cr_edge_id,
872 forward: true,
873 pcurve: crate::sweep_topology::parameter_line(u_start, 0.0, u_end, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
874 },
875 CoedgeRecord {
876 id: take_id(),
877 edge_id: blend_seam_id,
878 forward: true,
879 pcurve: crate::sweep_topology::parameter_line(u_end, 0.0, u_end, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
880 },
881 ];
882 for (edge_id, window, _) in cs_pieces.iter().rev() {
883 coedges.push(CoedgeRecord {
884 id: take_id(),
885 edge_id: *edge_id,
886 forward: false,
887 pcurve: crate::sweep_topology::parameter_line(window[1], 1.0, window[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
888 });
889 }
890 coedges.push(CoedgeRecord {
891 id: take_id(),
892 edge_id: blend_seam_id,
893 forward: false,
894 pcurve: crate::sweep_topology::parameter_line(u_start, 1.0, u_start, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
895 });
896 if !same_sense {
906 coedges.reverse();
907 for coedge in &mut coedges {
908 coedge.forward = !coedge.forward;
909 coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
910 }
911 }
912 let blend_face = FaceRecord {
913 id: take_id(),
914 surface: rows.surface,
915 same_sense,
916 loops: vec![LoopRecord {
917 id: loop_id,
918 coedges,
919 }],
920 name: name.map(|value| value.to_string()),
921 };
922 let shell_index = result
923 .shells
924 .iter()
925 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
926 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
927 result.shells[shell_index].faces.push(blend_face);
928
929 let used_edges: std::collections::HashSet<u64> = result
930 .shells
931 .iter()
932 .flat_map(|shell| &shell.faces)
933 .flat_map(|face| &face.loops)
934 .flat_map(|loop_record| &loop_record.coedges)
935 .map(|coedge| coedge.edge_id)
936 .collect();
937 result
938 .edges
939 .retain(|candidate| used_edges.contains(&candidate.id));
940 let used_vertices: std::collections::HashSet<u64> = result
941 .edges
942 .iter()
943 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
944 .collect();
945 result
946 .vertices
947 .retain(|candidate| used_vertices.contains(&candidate.id));
948 if repaired {
955 let problems = result.validate();
956 if !problems.is_empty() {
957 return Err(KernelRefusal::internal(KernelStage::Validate, "neighbour_trim_closure", format!(
958 "blend: the repaired neighbour trim did not close ({} issue(s), first: {})",
959 problems.len(),
960 problems
961 .first()
962 .map(|issue| issue.message.as_str())
963 .unwrap_or("unknown")
964 )));
965 }
966 }
967 Ok(result)
968}