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