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) => {
184 return blend_closed_edge_keep(
187 solid,
188 edge,
189 &first_mate,
190 &second_mate,
191 radius_at(mid_t),
192 rho1,
193 chamfer,
194 name,
195 )
196 .or_else(|_| {
197 blend_closed_edge_keep(
198 solid,
199 edge,
200 &second_mate,
201 &first_mate,
202 radius_at(mid_t),
203 rho2,
204 chamfer,
205 name,
206 )
207 })
208 .map_err(|keep_error| {
209 format!("{march_error}; edge-preserving blend also failed: {keep_error}")
210 });
211 }
212 };
213 let support_exits = |face: &FaceRecord, second_side: bool| -> bool {
217 let stride = (stations.len() / 16).max(1);
221 stations.iter().step_by(stride).any(|station| {
222 let mut uv = if second_side {
223 station.uv2
224 } else {
225 station.uv1
226 };
227 if let Ok((closed_u, closed_v)) = face.surface.closed_directions() {
228 if closed_u {
229 if let Ok([low, high]) = face.surface.domain_u() {
230 uv[0] = low + (uv[0] - low).rem_euclid(high - low);
231 }
232 }
233 if closed_v {
234 if let Ok([low, high]) = face.surface.domain_v() {
235 uv[1] = low + (uv[1] - low).rem_euclid(high - low);
236 }
237 }
238 }
239 crate::parameter_point_in_face(face, crate::Vec2 { x: uv[0], y: uv[1] }, 1e-6)
240 .map(|class| class == crate::PolygonClass::Outside)
241 .unwrap_or(true)
242 })
243 };
244 if support_exits(second_face, true) {
245 return blend_closed_edge_keep(
246 solid,
247 edge,
248 &first_mate,
249 &second_mate,
250 radius_at(mid_t),
251 rho1,
252 chamfer,
253 name,
254 );
255 }
256 if support_exits(first_face, false) {
257 return blend_closed_edge_keep(
258 solid,
259 edge,
260 &second_mate,
261 &first_mate,
262 radius_at(mid_t),
263 rho2,
264 chamfer,
265 name,
266 );
267 }
268 if std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok() {
269 let last = stations.len() - 1;
270 for index in [
271 0usize,
272 1.min(last),
273 2.min(last),
274 last.saturating_sub(1),
275 last,
276 ] {
277 let station = &stations[index];
278 eprintln!(
279 "station {index}: uv1=({:.6},{:.6}) uv2=({:.6},{:.6}) p1=({:.6},{:.6},{:.6}) w={:.6}",
280 station.uv1[0], station.uv1[1], station.uv2[0], station.uv2[1],
281 station.p1.x, station.p1.y, station.p1.z, station.weight,
282 );
283 }
284 }
285 let parameters = station_parameters(&stations);
286 let rows = match exact_closed_revolution_rows(&stations, &first_mate, &second_mate, chamfer) {
287 Some(rows) => rows?,
288 None => fit_closed_rows(&stations, ¶meters, chamfer)?,
289 };
290
291 let second_split = if second_seam {
295 let seam2 = second_face.surface.domain_u()?[0];
296 let period2 = {
297 let [d0, d1] = second_face.surface.domain_u()?;
298 d1 - d0
299 };
300 let u2_first = stations[0].uv2[0];
302 let u2_last = stations[stations.len() - 1].uv2[0];
303 let direction = (u2_last - u2_first).signum();
304 let mut k = ((u2_first - seam2) / period2).ceil();
305 if direction < 0.0 {
306 k = ((u2_first - seam2) / period2).floor();
307 }
308 let target = seam2 + k * period2;
309 let inside =
310 (target - u2_first) * direction > 1e-6 && (u2_last - target) * direction > 1e-6;
311 let aligned = (target - u2_first).abs() <= 1e-6
316 || (target - u2_last).abs() <= 1e-6
317 || ((u2_first - seam2) / period2).fract().abs() <= 1e-9;
318 if !inside && aligned {
319 None
320 } else if !inside {
321 return Err("blend: second seam crossing not bracketed by the march".into());
322 } else {
323 let [fit_low, fit_high] = rows.u_domain;
327 let mut p = fit_low
328 + (fit_high - fit_low) * {
329 let mut seed = 0.5;
331 for pair in 0..stations.len() - 1 {
332 let a = stations[pair].uv2[0];
333 let b = stations[pair + 1].uv2[0];
334 if (target - a) * (target - b) <= 0.0 {
335 let local = (target - a) / (b - a);
336 seed = (parameters[pair]
337 + (parameters[pair + 1] - parameters[pair]) * local)
338 .clamp(0.0, 1.0);
339 break;
340 }
341 }
342 seed
343 };
344 for _ in 0..NEWTON_ITERATIONS {
345 let value = rows.cs_pcurve.evaluate(p)?.x - target;
346 if value.abs() <= 1e-12 {
347 break;
348 }
349 let step = 1e-8;
350 let probed = rows.cs_pcurve.evaluate(p + step)?.x - target;
351 let derivative = (probed - value) / step;
352 if derivative.abs() <= 1e-14 {
353 return Err("blend: second seam crossing Newton stalled".into());
354 }
355 p -= value / derivative;
356 }
357 let crossing_v = rows.cs_pcurve.evaluate(p)?.y;
358 Some(SecondSeamSplit {
359 fit_parameter: p,
360 crossing_v,
361 period: period2 * direction,
362 })
363 }
364 } else {
365 None
366 };
367
368 build_surgery(
369 solid,
370 edge,
371 &first_mate,
372 &second_mate,
373 rows,
374 second_split,
375 name,
376 )
377}
378
379struct SecondSeamSplit {
382 fit_parameter: f64,
383 crossing_v: f64,
384 period: f64,
386}
387
388fn build_surgery(
391 solid: &BrepSolid,
392 edge: &EdgeRecord,
393 first: &BlendMate,
394 second: &BlendMate,
395 rows: FittedRows,
396 second_split: Option<SecondSeamSplit>,
397 name: Option<&str>,
398) -> Result<BrepSolid, String> {
399 let mut result = solid.clone();
400 let mut next_id = solid
401 .vertices
402 .iter()
403 .map(|vertex| vertex.id)
404 .chain(solid.edges.iter().map(|edge| edge.id))
405 .chain(
406 solid
407 .shells
408 .iter()
409 .flat_map(|shell| &shell.faces)
410 .flat_map(|face| {
411 face.loops
412 .iter()
413 .map(|loop_record| loop_record.id)
414 .chain(face.loops.iter().flat_map(|loop_record| {
415 loop_record.coedges.iter().map(|coedge| coedge.id)
416 }))
417 .chain(std::iter::once(face.id))
418 }),
419 )
420 .max()
421 .unwrap_or(0)
422 + 1;
423 let mut take_id = || {
424 let id = next_id;
425 next_id += 1;
426 id
427 };
428
429 let [u_start, u_end] = rows.u_domain;
430 let cr_start = rows.cr.evaluate(u_start)?;
431 let cs_start = rows.cs.evaluate(u_start)?;
432 let vertex1_id = take_id();
433 let vertex2_id = take_id();
434 result.vertices.push(VertexRecord {
435 id: vertex1_id,
436 point: cr_start,
437 });
438 result.vertices.push(VertexRecord {
439 id: vertex2_id,
440 point: cs_start,
441 });
442
443 let cr_edge_id = take_id();
444 result.edges.push(EdgeRecord {
445 id: cr_edge_id,
446 curve: rows.cr.clone(),
447 t0: u_start,
448 t1: u_end,
449 start_vertex_id: vertex1_id,
450 end_vertex_id: vertex1_id,
451 degenerate: false,
452 name: None,
453 });
454 let mut cs_pieces: Vec<(u64, [f64; 2], NurbsCurve)> = Vec::new();
458 let mut second_seam_vertex = vertex2_id;
459 let mut second_seam_point = cs_start;
460 let mut second_crossing_v = 0.0;
461 if let Some(split) = &second_split {
462 let p = split.fit_parameter;
463 second_crossing_v = split.crossing_v;
464 let (cs_a, cs_b) = rows.cs.split(p)?;
465 let (pc_a, pc_b) = rows.cs_pcurve.split(p)?;
466 let mut pc_b = pc_b;
468 for point in pc_b.control_points.iter_mut() {
469 point.x -= split.period * point.w;
470 }
471 let w2s = take_id();
472 second_seam_vertex = w2s;
473 second_seam_point = rows.cs.evaluate(p)?;
474 result.vertices.push(VertexRecord {
475 id: w2s,
476 point: second_seam_point,
477 });
478 let edge_a = take_id();
479 result.edges.push(EdgeRecord {
480 id: edge_a,
481 curve: cs_a,
482 t0: u_start,
483 t1: p,
484 start_vertex_id: vertex2_id,
485 end_vertex_id: w2s,
486 degenerate: false,
487 name: None,
488 });
489 cs_pieces.push((edge_a, [u_start, p], pc_a));
490 let edge_b = take_id();
491 result.edges.push(EdgeRecord {
492 id: edge_b,
493 curve: cs_b,
494 t0: p,
495 t1: u_end,
496 start_vertex_id: w2s,
497 end_vertex_id: vertex2_id,
498 degenerate: false,
499 name: None,
500 });
501 cs_pieces.push((edge_b, [p, u_end], pc_b));
502 } else {
503 let cs_edge_id = take_id();
504 result.edges.push(EdgeRecord {
505 id: cs_edge_id,
506 curve: rows.cs.clone(),
507 t0: u_start,
508 t1: u_end,
509 start_vertex_id: vertex2_id,
510 end_vertex_id: vertex2_id,
511 degenerate: false,
512 name: None,
513 });
514 cs_pieces.push((cs_edge_id, [u_start, u_end], rows.cs_pcurve.clone()));
515 }
516 let seam_curve = rows.surface.iso_curve_u(u_start)?;
517 let [seam_t0, seam_t1] = seam_curve.domain()?;
518 let blend_seam_id = take_id();
519 result.edges.push(EdgeRecord {
520 id: blend_seam_id,
521 curve: seam_curve,
522 t0: seam_t0,
523 t1: seam_t1,
524 start_vertex_id: vertex1_id,
525 end_vertex_id: vertex2_id,
526 degenerate: false,
527 name: None,
528 });
529
530 let old_vertex = edge.start_vertex_id;
531 let replace_in_face = |result: &mut BrepSolid,
532 mate: &BlendMate,
533 pieces: &[(u64, [f64; 2], NurbsCurve)],
534 uv_new: [f64; 2]|
535 -> Result<bool, String> {
536 let v_new = uv_new[1];
537 let seam_edge_ends: Vec<(u64, bool, bool)> = result
539 .edges
540 .iter()
541 .filter(|candidate| {
542 candidate.id != edge.id
543 && (candidate.start_vertex_id == old_vertex
544 || candidate.end_vertex_id == old_vertex)
545 })
546 .map(|candidate| {
547 (
548 candidate.id,
549 candidate.start_vertex_id == old_vertex,
550 candidate.end_vertex_id == old_vertex,
551 )
552 })
553 .collect();
554 let seam_edge_ids: Vec<u64> = seam_edge_ends.iter().map(|(id, ..)| *id).collect();
555 let face = result
556 .shells
557 .iter_mut()
558 .flat_map(|shell| &mut shell.faces)
559 .find(|face| face.id == mate.face.id)
560 .ok_or("blend: mate face lost during surgery")?;
561 let loop_record = &mut face.loops[mate.loop_index];
562 let position = loop_record
563 .coedges
564 .iter()
565 .position(|coedge| coedge.edge_id == edge.id)
566 .ok_or("blend: edge coedge lost during surgery")?;
567 let old_forward = loop_record.coedges[position].forward;
568 let old_seam_v = {
569 let pcurve = &loop_record.coedges[position].pcurve;
570 let [d0, _] = pcurve.domain()?;
571 pcurve.evaluate(d0)?.y
572 };
573 let mut replacements = Vec::new();
576 let ordered: Vec<&(u64, [f64; 2], NurbsCurve)> = if old_forward {
577 pieces.iter().collect()
578 } else {
579 pieces.iter().rev().collect()
580 };
581 for (index, (edge_id, _, pcurve)) in ordered.iter().enumerate() {
582 replacements.push(CoedgeRecord {
583 id: if index == 0 {
584 loop_record.coedges[position].id
585 } else {
586 0 },
588 edge_id: *edge_id,
589 forward: old_forward,
590 pcurve: if old_forward {
591 pcurve.clone()
592 } else {
593 pcurve.reversed()?
594 },
595 });
596 }
597 loop_record
598 .coedges
599 .splice(position..=position, replacements);
600 let mut repaired = false;
601 if loop_record.coedges.len() > 1 {
602 for coedge in &mut loop_record.coedges {
608 if pieces
609 .iter()
610 .any(|(edge_id, ..)| *edge_id == coedge.edge_id)
611 || !seam_edge_ids.contains(&coedge.edge_id)
612 {
613 continue;
614 }
615 let controls = &mut coedge.pcurve.control_points;
616 let last_index = controls.len() - 1;
617 let first_u = controls[0].x / controls[0].w;
618 let last_u = controls[last_index].x / controls[last_index].w;
619 let constant_u = (first_u - last_u).abs()
620 <= 1e-9 * (1.0 + first_u.abs().max(last_u.abs()));
621 if constant_u {
622 let first_v = controls[0].y / controls[0].w;
626 let last_v = controls[last_index].y / controls[last_index].w;
627 let target = if (first_v - old_seam_v).abs() < (last_v - old_seam_v).abs() {
628 0
629 } else {
630 last_index
631 };
632 let w = controls[target].w;
633 controls[target].y = v_new * w;
634 continue;
635 }
636 let Some((_, start_is_old, end_is_old)) = seam_edge_ends
645 .iter()
646 .find(|(id, ..)| *id == coedge.edge_id)
647 .copied()
648 else {
649 continue;
650 };
651 if start_is_old == end_is_old {
652 return Err(format!(
653 "blend: neighbour edge {} meets the blended edge at both ends; \
654 its trim end is ambiguous",
655 coedge.edge_id
656 ));
657 }
658 let target = if start_is_old == coedge.forward {
661 0
662 } else {
663 last_index
664 };
665 let w = controls[target].w;
666 controls[target].x = uv_new[0] * w;
667 repaired = true;
668 }
669 }
670 Ok(repaired)
671 };
672 let uv1_new = rows.cr_pcurve.evaluate(u_start)?;
673 let v1_new = uv1_new.y;
674 let cr_pieces = vec![(cr_edge_id, [u_start, u_end], rows.cr_pcurve.clone())];
675 let mut repaired = replace_in_face(&mut result, first, &cr_pieces, [uv1_new.x, v1_new])?;
676 let uv2_new = rows.cs_pcurve.evaluate(u_start)?;
677 let v2_new = if second_split.is_some() {
678 second_crossing_v
679 } else {
680 uv2_new.y
681 };
682 repaired |= replace_in_face(&mut result, second, &cs_pieces, [uv2_new.x, v2_new])?;
683
684 let collapse_winding_sphere_cap =
690 |result: &mut BrepSolid, face_id: u64, support_ids: &[u64]| -> Result<(), String> {
691 let face = result
692 .shells
693 .iter_mut()
694 .flat_map(|shell| &mut shell.faces)
695 .find(|face| face.id == face_id)
696 .ok_or("blend: sphere carrier lost during cap surgery")?;
697 if !matches!(
698 face.surface.analytic(),
699 Some(crate::AnalyticSurface::Sphere { .. })
700 ) || face.surface.closed_directions()? != (true, false)
701 {
702 return Ok(());
703 }
704 let [u0, u1] = face.surface.domain_u()?;
705 let [v0, v1] = face.surface.domain_v()?;
706 let period = u1 - u0;
707 let support_loop = face.loops.iter().position(|loop_record| {
708 !loop_record.coedges.is_empty()
709 && loop_record
710 .coedges
711 .iter()
712 .all(|coedge| support_ids.contains(&coedge.edge_id))
713 });
714 let Some(support_loop) = support_loop else {
715 return Ok(());
716 };
717 if face.loops[support_loop].coedges.len() != 1 {
718 return Ok(());
719 }
720 let support = &face.loops[support_loop].coedges[0];
721 let [s0, s1] = support.pcurve.domain()?;
722 let support_start = support.pcurve.evaluate(s0)?;
723 let support_end = support.pcurve.evaluate(s1)?;
724 let support_winding = support_end.x - support_start.x;
725 if (support_winding.abs() - period).abs() > 0.05 * period {
726 return Ok(());
727 }
728
729 let mut pole: Option<(usize, CoedgeRecord)> = None;
730 for (loop_index, loop_record) in face.loops.iter().enumerate() {
731 if loop_index == support_loop {
732 continue;
733 }
734 for coedge in &loop_record.coedges {
735 let [p0, p1] = coedge.pcurve.domain()?;
736 let start = coedge.pcurve.evaluate(p0)?;
737 let end = coedge.pcurve.evaluate(p1)?;
738 let winding = end.x - start.x;
739 let at_pole = (start.y - v0).abs() <= 1e-8 || (start.y - v1).abs() <= 1e-8;
740 if at_pole
741 && (end.y - start.y).abs() <= 1e-8
742 && (winding.abs() - period).abs() <= 0.05 * period
743 && winding * support_winding < 0.0
744 {
745 pole = Some((loop_index, coedge.clone()));
746 break;
747 }
748 }
749 if pole.is_some() {
750 break;
751 }
752 }
753 let Some((pole_loop, pole_coedge)) = pole else {
754 return Ok(());
755 };
756 let support_record = face.loops[support_loop].clone();
757 let pole_id = face.loops[pole_loop].id;
758 face.loops = vec![
759 LoopRecord {
760 id: pole_id,
761 coedges: vec![pole_coedge],
762 },
763 support_record,
764 ];
765 Ok(())
766 };
767 collapse_winding_sphere_cap(&mut result, first.face.id, &[cr_edge_id])?;
768 let cs_edge_ids: Vec<u64> = cs_pieces.iter().map(|(edge_id, ..)| *edge_id).collect();
769 collapse_winding_sphere_cap(&mut result, second.face.id, &cs_edge_ids)?;
770 for shell in &mut result.shells {
772 for face in &mut shell.faces {
773 for loop_record in &mut face.loops {
774 for coedge in &mut loop_record.coedges {
775 if coedge.id == 0 {
776 coedge.id = take_id();
777 }
778 }
779 }
780 }
781 }
782
783 let new_edge_ids: Vec<u64> = std::iter::once(cr_edge_id)
788 .chain(cs_pieces.iter().map(|(edge_id, ..)| *edge_id))
789 .chain(std::iter::once(blend_seam_id))
790 .collect();
791 let first_face_edges: Vec<u64> = first
792 .face
793 .loops
794 .iter()
795 .flat_map(|loop_record| loop_record.coedges.iter().map(|coedge| coedge.edge_id))
796 .collect();
797 for seam_edge in result.edges.iter_mut() {
798 if new_edge_ids.contains(&seam_edge.id) {
799 continue;
800 }
801 if seam_edge.start_vertex_id != old_vertex && seam_edge.end_vertex_id != old_vertex {
802 continue;
803 }
804 let (target_vertex, target_v, target_point) = if first_face_edges.contains(&seam_edge.id) {
805 (vertex1_id, v1_new, cr_start)
806 } else {
807 (second_seam_vertex, v2_new, second_seam_point)
808 };
809 let [c0, c1] = seam_edge.curve.domain()?;
810 let clamped = target_v.clamp(c0.min(c1), c0.max(c1));
811 let band = 1e-6 * (1.0 + target_point.length());
820 let split_parameter = if seam_edge.id == edge.id {
821 clamped
827 } else {
828 match seam_edge.curve.evaluate(clamped) {
829 Ok(point) if point.sub(target_point).length() <= band => clamped,
830 _ => {
831 repaired = true;
832 let projection =
833 crate::project_point_to_curve(&seam_edge.curve, target_point)?;
834 if projection.distance > band {
835 return Err(format!(
836 "blend: neighbour edge {} does not pass through the blend's support \
837 start (off by {:.9}); the trim has no parameter to move to",
838 seam_edge.id, projection.distance
839 ));
840 }
841 projection.u.clamp(c0.min(c1), c0.max(c1))
842 }
843 }
844 };
845 if seam_edge.start_vertex_id == old_vertex {
846 seam_edge.t0 = split_parameter;
847 seam_edge.start_vertex_id = target_vertex;
848 }
849 if seam_edge.end_vertex_id == old_vertex {
850 seam_edge.t1 = split_parameter;
851 seam_edge.end_vertex_id = target_vertex;
852 }
853 }
854
855 let mid_u = (u_start + u_end) * 0.5;
857 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
858 let station_uv = rows.cr_pcurve.evaluate(mid_u)?;
859 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
860 let out1 = if first.face.same_sense {
861 n1
862 } else {
863 n1.scale(-1.0)
864 };
865 let same_sense = blend_normal.dot(out1) >= 0.0;
866 let loop_id = take_id();
867 let mut coedges = vec![
868 CoedgeRecord {
869 id: take_id(),
870 edge_id: cr_edge_id,
871 forward: true,
872 pcurve: crate::sweep_topology::parameter_line(u_start, 0.0, u_end, 0.0)?,
873 },
874 CoedgeRecord {
875 id: take_id(),
876 edge_id: blend_seam_id,
877 forward: true,
878 pcurve: crate::sweep_topology::parameter_line(u_end, 0.0, u_end, 1.0)?,
879 },
880 ];
881 for (edge_id, window, _) in cs_pieces.iter().rev() {
882 coedges.push(CoedgeRecord {
883 id: take_id(),
884 edge_id: *edge_id,
885 forward: false,
886 pcurve: crate::sweep_topology::parameter_line(window[1], 1.0, window[0], 1.0)?,
887 });
888 }
889 coedges.push(CoedgeRecord {
890 id: take_id(),
891 edge_id: blend_seam_id,
892 forward: false,
893 pcurve: crate::sweep_topology::parameter_line(u_start, 1.0, u_start, 0.0)?,
894 });
895 if !same_sense {
905 coedges.reverse();
906 for coedge in &mut coedges {
907 coedge.forward = !coedge.forward;
908 coedge.pcurve = coedge.pcurve.reversed()?;
909 }
910 }
911 let blend_face = FaceRecord {
912 id: take_id(),
913 surface: rows.surface,
914 same_sense,
915 loops: vec![LoopRecord {
916 id: loop_id,
917 coedges,
918 }],
919 name: name.map(|value| value.to_string()),
920 };
921 let shell_index = result
922 .shells
923 .iter()
924 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
925 .ok_or("blend: mate shell lost during surgery")?;
926 result.shells[shell_index].faces.push(blend_face);
927
928 let used_edges: std::collections::HashSet<u64> = result
929 .shells
930 .iter()
931 .flat_map(|shell| &shell.faces)
932 .flat_map(|face| &face.loops)
933 .flat_map(|loop_record| &loop_record.coedges)
934 .map(|coedge| coedge.edge_id)
935 .collect();
936 result
937 .edges
938 .retain(|candidate| used_edges.contains(&candidate.id));
939 let used_vertices: std::collections::HashSet<u64> = result
940 .edges
941 .iter()
942 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
943 .collect();
944 result
945 .vertices
946 .retain(|candidate| used_vertices.contains(&candidate.id));
947 if repaired {
954 let problems = result.validate();
955 if !problems.is_empty() {
956 return Err(format!(
957 "blend: the repaired neighbour trim did not close ({} issue(s), first: {})",
958 problems.len(),
959 problems
960 .first()
961 .map(|issue| issue.message.as_str())
962 .unwrap_or("unknown")
963 ));
964 }
965 }
966 Ok(result)
967}