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 stations = match march_stations(edge, &first_mate, &second_mate, radius_at, anchor_u) {
160 Ok(stations) => stations,
161 Err(march_error) => {
162 return blend_closed_edge_keep(
165 solid,
166 edge,
167 &first_mate,
168 &second_mate,
169 radius_at(mid_t),
170 rho1,
171 chamfer,
172 name,
173 )
174 .or_else(|_| {
175 blend_closed_edge_keep(
176 solid,
177 edge,
178 &second_mate,
179 &first_mate,
180 radius_at(mid_t),
181 rho2,
182 chamfer,
183 name,
184 )
185 })
186 .map_err(|keep_error| {
187 format!("{march_error}; edge-preserving blend also failed: {keep_error}")
188 });
189 }
190 };
191 let support_exits = |face: &FaceRecord, second_side: bool| -> bool {
195 stations.iter().step_by(4).any(|station| {
196 let uv = if second_side {
197 station.uv2
198 } else {
199 station.uv1
200 };
201 crate::parameter_point_in_face(face, crate::Vec2 { x: uv[0], y: uv[1] }, 1e-6)
202 .map(|class| class == crate::PolygonClass::Outside)
203 .unwrap_or(true)
204 })
205 };
206 if support_exits(second_face, true) {
207 return blend_closed_edge_keep(
208 solid,
209 edge,
210 &first_mate,
211 &second_mate,
212 radius_at(mid_t),
213 rho1,
214 chamfer,
215 name,
216 );
217 }
218 if support_exits(first_face, false) {
219 return blend_closed_edge_keep(
220 solid,
221 edge,
222 &second_mate,
223 &first_mate,
224 radius_at(mid_t),
225 rho2,
226 chamfer,
227 name,
228 );
229 }
230 if std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok() {
231 for index in [0usize, 1, 2, STATIONS - 1, STATIONS] {
232 let station = &stations[index];
233 eprintln!(
234 "station {index}: uv1=({:.6},{:.6}) uv2=({:.6},{:.6}) p1=({:.6},{:.6},{:.6}) w={:.6}",
235 station.uv1[0], station.uv1[1], station.uv2[0], station.uv2[1],
236 station.p1.x, station.p1.y, station.p1.z, station.weight,
237 );
238 }
239 }
240 let parameters = station_parameters(&stations);
241 let rows = fit_closed_rows(&stations, ¶meters, chamfer)?;
242
243 let second_split = if second_seam {
247 let seam2 = second_face.surface.domain_u()?[0];
248 let period2 = {
249 let [d0, d1] = second_face.surface.domain_u()?;
250 d1 - d0
251 };
252 let u2_first = stations[0].uv2[0];
254 let u2_last = stations[stations.len() - 1].uv2[0];
255 let direction = (u2_last - u2_first).signum();
256 let mut k = ((u2_first - seam2) / period2).ceil();
257 if direction < 0.0 {
258 k = ((u2_first - seam2) / period2).floor();
259 }
260 let target = seam2 + k * period2;
261 let inside =
262 (target - u2_first) * direction > 1e-6 && (u2_last - target) * direction > 1e-6;
263 let aligned = (target - u2_first).abs() <= 1e-6
268 || (target - u2_last).abs() <= 1e-6
269 || ((u2_first - seam2) / period2).fract().abs() <= 1e-9;
270 if !inside && aligned {
271 None
272 } else if !inside {
273 return Err("blend: second seam crossing not bracketed by the march".into());
274 } else {
275 let [fit_low, fit_high] = rows.u_domain;
279 let mut p = fit_low
280 + (fit_high - fit_low) * {
281 let mut seed = 0.5;
283 for pair in 0..stations.len() - 1 {
284 let a = stations[pair].uv2[0];
285 let b = stations[pair + 1].uv2[0];
286 if (target - a) * (target - b) <= 0.0 {
287 let local = (target - a) / (b - a);
288 seed = (parameters[pair]
289 + (parameters[pair + 1] - parameters[pair]) * local)
290 .clamp(0.0, 1.0);
291 break;
292 }
293 }
294 seed
295 };
296 for _ in 0..NEWTON_ITERATIONS {
297 let value = rows.cs_pcurve.evaluate(p)?.x - target;
298 if value.abs() <= 1e-12 {
299 break;
300 }
301 let step = 1e-8;
302 let probed = rows.cs_pcurve.evaluate(p + step)?.x - target;
303 let derivative = (probed - value) / step;
304 if derivative.abs() <= 1e-14 {
305 return Err("blend: second seam crossing Newton stalled".into());
306 }
307 p -= value / derivative;
308 }
309 let crossing_v = rows.cs_pcurve.evaluate(p)?.y;
310 Some(SecondSeamSplit {
311 fit_parameter: p,
312 crossing_v,
313 period: period2 * direction,
314 })
315 }
316 } else {
317 None
318 };
319
320 build_surgery(
321 solid,
322 edge,
323 &first_mate,
324 &second_mate,
325 rows,
326 second_split,
327 name,
328 )
329}
330
331struct SecondSeamSplit {
334 fit_parameter: f64,
335 crossing_v: f64,
336 period: f64,
338}
339
340fn build_surgery(
343 solid: &BrepSolid,
344 edge: &EdgeRecord,
345 first: &BlendMate,
346 second: &BlendMate,
347 rows: FittedRows,
348 second_split: Option<SecondSeamSplit>,
349 name: Option<&str>,
350) -> Result<BrepSolid, String> {
351 let mut result = solid.clone();
352 let mut next_id = solid
353 .vertices
354 .iter()
355 .map(|vertex| vertex.id)
356 .chain(solid.edges.iter().map(|edge| edge.id))
357 .chain(
358 solid
359 .shells
360 .iter()
361 .flat_map(|shell| &shell.faces)
362 .flat_map(|face| {
363 face.loops
364 .iter()
365 .map(|loop_record| loop_record.id)
366 .chain(face.loops.iter().flat_map(|loop_record| {
367 loop_record.coedges.iter().map(|coedge| coedge.id)
368 }))
369 .chain(std::iter::once(face.id))
370 }),
371 )
372 .max()
373 .unwrap_or(0)
374 + 1;
375 let mut take_id = || {
376 let id = next_id;
377 next_id += 1;
378 id
379 };
380
381 let [u_start, u_end] = rows.u_domain;
382 let cr_start = rows.cr.evaluate(u_start)?;
383 let cs_start = rows.cs.evaluate(u_start)?;
384 let vertex1_id = take_id();
385 let vertex2_id = take_id();
386 result.vertices.push(VertexRecord {
387 id: vertex1_id,
388 point: cr_start,
389 });
390 result.vertices.push(VertexRecord {
391 id: vertex2_id,
392 point: cs_start,
393 });
394
395 let cr_edge_id = take_id();
396 result.edges.push(EdgeRecord {
397 id: cr_edge_id,
398 curve: rows.cr.clone(),
399 t0: u_start,
400 t1: u_end,
401 start_vertex_id: vertex1_id,
402 end_vertex_id: vertex1_id,
403 degenerate: false,
404 name: None,
405 });
406 let mut cs_pieces: Vec<(u64, [f64; 2], NurbsCurve)> = Vec::new();
410 let mut second_seam_vertex = vertex2_id;
411 let mut second_crossing_v = 0.0;
412 if let Some(split) = &second_split {
413 let p = split.fit_parameter;
414 second_crossing_v = split.crossing_v;
415 let (cs_a, cs_b) = rows.cs.split(p)?;
416 let (pc_a, pc_b) = rows.cs_pcurve.split(p)?;
417 let mut pc_b = pc_b;
419 for point in pc_b.control_points.iter_mut() {
420 point.x -= split.period * point.w;
421 }
422 let w2s = take_id();
423 second_seam_vertex = w2s;
424 result.vertices.push(VertexRecord {
425 id: w2s,
426 point: rows.cs.evaluate(p)?,
427 });
428 let edge_a = take_id();
429 result.edges.push(EdgeRecord {
430 id: edge_a,
431 curve: cs_a,
432 t0: u_start,
433 t1: p,
434 start_vertex_id: vertex2_id,
435 end_vertex_id: w2s,
436 degenerate: false,
437 name: None,
438 });
439 cs_pieces.push((edge_a, [u_start, p], pc_a));
440 let edge_b = take_id();
441 result.edges.push(EdgeRecord {
442 id: edge_b,
443 curve: cs_b,
444 t0: p,
445 t1: u_end,
446 start_vertex_id: w2s,
447 end_vertex_id: vertex2_id,
448 degenerate: false,
449 name: None,
450 });
451 cs_pieces.push((edge_b, [p, u_end], pc_b));
452 } else {
453 let cs_edge_id = take_id();
454 result.edges.push(EdgeRecord {
455 id: cs_edge_id,
456 curve: rows.cs.clone(),
457 t0: u_start,
458 t1: u_end,
459 start_vertex_id: vertex2_id,
460 end_vertex_id: vertex2_id,
461 degenerate: false,
462 name: None,
463 });
464 cs_pieces.push((cs_edge_id, [u_start, u_end], rows.cs_pcurve.clone()));
465 }
466 let seam_curve = rows.surface.iso_curve_u(u_start)?;
467 let [seam_t0, seam_t1] = seam_curve.domain()?;
468 let blend_seam_id = take_id();
469 result.edges.push(EdgeRecord {
470 id: blend_seam_id,
471 curve: seam_curve,
472 t0: seam_t0,
473 t1: seam_t1,
474 start_vertex_id: vertex1_id,
475 end_vertex_id: vertex2_id,
476 degenerate: false,
477 name: None,
478 });
479
480 let old_vertex = edge.start_vertex_id;
481 let mut replace_in_face = |result: &mut BrepSolid,
482 mate: &BlendMate,
483 pieces: &[(u64, [f64; 2], NurbsCurve)],
484 v_new: f64|
485 -> Result<(), String> {
486 let seam_edge_ids: Vec<u64> = result
487 .edges
488 .iter()
489 .filter(|candidate| {
490 candidate.id != edge.id
491 && (candidate.start_vertex_id == old_vertex
492 || candidate.end_vertex_id == old_vertex)
493 })
494 .map(|candidate| candidate.id)
495 .collect();
496 let face = result
497 .shells
498 .iter_mut()
499 .flat_map(|shell| &mut shell.faces)
500 .find(|face| face.id == mate.face.id)
501 .ok_or("blend: mate face lost during surgery")?;
502 let loop_record = &mut face.loops[mate.loop_index];
503 let position = loop_record
504 .coedges
505 .iter()
506 .position(|coedge| coedge.edge_id == edge.id)
507 .ok_or("blend: edge coedge lost during surgery")?;
508 let old_forward = loop_record.coedges[position].forward;
509 let old_seam_v = {
510 let pcurve = &loop_record.coedges[position].pcurve;
511 let [d0, _] = pcurve.domain()?;
512 pcurve.evaluate(d0)?.y
513 };
514 let mut replacements = Vec::new();
517 let ordered: Vec<&(u64, [f64; 2], NurbsCurve)> = if old_forward {
518 pieces.iter().collect()
519 } else {
520 pieces.iter().rev().collect()
521 };
522 for (index, (edge_id, _, pcurve)) in ordered.iter().enumerate() {
523 replacements.push(CoedgeRecord {
524 id: if index == 0 {
525 loop_record.coedges[position].id
526 } else {
527 0 },
529 edge_id: *edge_id,
530 forward: old_forward,
531 pcurve: if old_forward {
532 pcurve.clone()
533 } else {
534 pcurve.reversed()?
535 },
536 });
537 }
538 loop_record
539 .coedges
540 .splice(position..=position, replacements);
541 if loop_record.coedges.len() > 1 {
542 for coedge in &mut loop_record.coedges {
548 if pieces
549 .iter()
550 .any(|(edge_id, ..)| *edge_id == coedge.edge_id)
551 || !seam_edge_ids.contains(&coedge.edge_id)
552 {
553 continue;
554 }
555 let controls = &mut coedge.pcurve.control_points;
556 let last_index = controls.len() - 1;
557 let first_v = controls[0].y / controls[0].w;
560 let last_v = controls[last_index].y / controls[last_index].w;
561 let target = if (first_v - old_seam_v).abs() < (last_v - old_seam_v).abs() {
562 0
563 } else {
564 last_index
565 };
566 let w = controls[target].w;
567 controls[target].y = v_new * w;
568 }
569 }
570 Ok(())
571 };
572 let v1_new = rows.cr_pcurve.evaluate(u_start)?.y;
573 let cr_pieces = vec![(cr_edge_id, [u_start, u_end], rows.cr_pcurve.clone())];
574 replace_in_face(&mut result, first, &cr_pieces, v1_new)?;
575 let v2_new = if second_split.is_some() {
576 second_crossing_v
577 } else {
578 rows.cs_pcurve.evaluate(u_start)?.y
579 };
580 replace_in_face(&mut result, second, &cs_pieces, v2_new)?;
581 for shell in &mut result.shells {
583 for face in &mut shell.faces {
584 for loop_record in &mut face.loops {
585 for coedge in &mut loop_record.coedges {
586 if coedge.id == 0 {
587 coedge.id = take_id();
588 }
589 }
590 }
591 }
592 }
593
594 let new_edge_ids: Vec<u64> = std::iter::once(cr_edge_id)
599 .chain(cs_pieces.iter().map(|(edge_id, ..)| *edge_id))
600 .chain(std::iter::once(blend_seam_id))
601 .collect();
602 let first_face_edges: Vec<u64> = first
603 .face
604 .loops
605 .iter()
606 .flat_map(|loop_record| loop_record.coedges.iter().map(|coedge| coedge.edge_id))
607 .collect();
608 for seam_edge in result.edges.iter_mut() {
609 if new_edge_ids.contains(&seam_edge.id) {
610 continue;
611 }
612 if seam_edge.start_vertex_id != old_vertex && seam_edge.end_vertex_id != old_vertex {
613 continue;
614 }
615 let (target_vertex, target_v) = if first_face_edges.contains(&seam_edge.id) {
616 (vertex1_id, v1_new)
617 } else {
618 (second_seam_vertex, v2_new)
619 };
620 let [c0, c1] = seam_edge.curve.domain()?;
621 let split_parameter = target_v.clamp(c0.min(c1), c0.max(c1));
622 if seam_edge.start_vertex_id == old_vertex {
623 seam_edge.t0 = split_parameter;
624 seam_edge.start_vertex_id = target_vertex;
625 }
626 if seam_edge.end_vertex_id == old_vertex {
627 seam_edge.t1 = split_parameter;
628 seam_edge.end_vertex_id = target_vertex;
629 }
630 }
631
632 let mid_u = (u_start + u_end) * 0.5;
634 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
635 let station_uv = rows.cr_pcurve.evaluate(mid_u)?;
636 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
637 let out1 = if first.face.same_sense {
638 n1
639 } else {
640 n1.scale(-1.0)
641 };
642 let same_sense = blend_normal.dot(out1) >= 0.0;
643 let loop_id = take_id();
644 let mut coedges = vec![
645 CoedgeRecord {
646 id: take_id(),
647 edge_id: cr_edge_id,
648 forward: true,
649 pcurve: crate::sweep_topology::parameter_line(u_start, 0.0, u_end, 0.0)?,
650 },
651 CoedgeRecord {
652 id: take_id(),
653 edge_id: blend_seam_id,
654 forward: true,
655 pcurve: crate::sweep_topology::parameter_line(u_end, 0.0, u_end, 1.0)?,
656 },
657 ];
658 for (edge_id, window, _) in cs_pieces.iter().rev() {
659 coedges.push(CoedgeRecord {
660 id: take_id(),
661 edge_id: *edge_id,
662 forward: false,
663 pcurve: crate::sweep_topology::parameter_line(window[1], 1.0, window[0], 1.0)?,
664 });
665 }
666 coedges.push(CoedgeRecord {
667 id: take_id(),
668 edge_id: blend_seam_id,
669 forward: false,
670 pcurve: crate::sweep_topology::parameter_line(u_start, 1.0, u_start, 0.0)?,
671 });
672 if !same_sense {
682 coedges.reverse();
683 for coedge in &mut coedges {
684 coedge.forward = !coedge.forward;
685 coedge.pcurve = coedge.pcurve.reversed()?;
686 }
687 }
688 let blend_face = FaceRecord {
689 id: take_id(),
690 surface: rows.surface,
691 same_sense,
692 loops: vec![LoopRecord {
693 id: loop_id,
694 coedges,
695 }],
696 name: name.map(|value| value.to_string()),
697 };
698 let shell_index = result
699 .shells
700 .iter()
701 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
702 .ok_or("blend: mate shell lost during surgery")?;
703 result.shells[shell_index].faces.push(blend_face);
704
705 result.edges.retain(|candidate| candidate.id != edge.id);
706 let old_vertex_still_used = result.edges.iter().any(|candidate| {
707 candidate.start_vertex_id == old_vertex || candidate.end_vertex_id == old_vertex
708 });
709 if !old_vertex_still_used {
710 result
711 .vertices
712 .retain(|candidate| candidate.id != old_vertex);
713 }
714 Ok(result)
715}