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 let _operation = crate::blend::BlendOperation::enter();
16 if !(radius > 0.0) || !radius.is_finite() {
17 return Err(KernelRefusal::input(KernelStage::Collect, "radius", "blend: radius must be positive"));
18 }
19 blend_closed_edge_impl(solid, edge_id, &|_| radius, Some(radius), chamfer, name)
20}
21
22pub fn blend_edge_variable(
26 solid: &BrepSolid,
27 edge_id: u64,
28 radii: &[(f64, f64)],
29 chamfer: bool,
30 name: Option<&str>,
31) -> Result<BrepSolid, KernelRefusal> {
32 let _operation = crate::blend::BlendOperation::enter();
35 if radii.is_empty() || radii.iter().any(|(_, radius)| !(*radius > 0.0)) {
36 return Err(KernelRefusal::input(KernelStage::Collect, "radius_stop", "blend: every radius stop must be positive"));
37 }
38 let mut stops = radii.to_vec();
39 stops.sort_by(|a, b| a.0.total_cmp(&b.0));
40 let edge = solid
41 .edges
42 .iter()
43 .find(|edge| edge.id == edge_id)
44 .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
45 let closed = edge.start_vertex_id == edge.end_vertex_id;
46 if closed && (stops[0].1 - stops[stops.len() - 1].1).abs() > 1e-12 {
47 return Err(KernelRefusal::input(KernelStage::Collect, "closed_edge_radii", "blend: closed edges need equal first/last radii"));
48 }
49 let first_radius = stops[0].1;
58 if stops
59 .iter()
60 .all(|(_, radius)| (radius - first_radius).abs() <= 1e-12 * (1.0 + first_radius.abs()))
61 {
62 return if chamfer {
63 crate::fillet::chamfer_edge(solid, edge_id, first_radius, name)
64 } else {
65 crate::fillet::fillet_edge(solid, edge_id, first_radius, name)
66 };
67 }
68 let (t0, t1) = (edge.t0, edge.t1);
69 let radius_at = move |t: f64| -> f64 {
70 let raw = (t - t0) / (t1 - t0);
71 let fraction = if closed {
79 raw.rem_euclid(1.0)
80 } else {
81 raw.clamp(0.0, 1.0)
82 };
83 if fraction <= stops[0].0 {
84 return stops[0].1;
85 }
86 for pair in stops.windows(2) {
87 if fraction <= pair[1].0 {
88 let width = (pair[1].0 - pair[0].0).max(1e-12);
89 let local = (fraction - pair[0].0) / width;
90 return pair[0].1 + (pair[1].1 - pair[0].1) * local;
91 }
92 }
93 stops[stops.len() - 1].1
94 };
95 if closed {
98 blend_closed_edge_impl(solid, edge_id, &radius_at, None, chamfer, name)
99 } else {
100 blend_open_edge_impl(solid, edge_id, &radius_at, None, chamfer, name)
101 }
102}
103
104fn blend_closed_edge_impl(
108 solid: &BrepSolid,
109 edge_id: u64,
110 radius_at: &dyn Fn(f64) -> f64,
111 constant: Option<f64>,
112 chamfer: bool,
113 name: Option<&str>,
114) -> Result<BrepSolid, KernelRefusal> {
115 if let Some(separated) = separate_spherical_domain_cut(solid, edge_id)? {
116 return blend_closed_edge_impl(&separated, edge_id, radius_at, constant, chamfer, name);
117 }
118 if let Some(simplified) = remove_retraced_cuts(solid, edge_id)? {
119 return blend_closed_edge_impl(&simplified, edge_id, radius_at, constant, chamfer, name);
120 }
121 let edge = solid
122 .edges
123 .iter()
124 .find(|edge| edge.id == edge_id)
125 .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
126 if edge.start_vertex_id != edge.end_vertex_id {
127 return Err(KernelRefusal::input(KernelStage::Collect, "closed_edge", "blend: general path currently requires a CLOSED edge"));
128 }
129 let (face_a, loop_a, coedge_a) = locate_mate(solid, edge_id, None)?;
130 let (face_b, loop_b, coedge_b) = locate_mate(solid, edge_id, Some((face_a.id, loop_a)))?;
131 if face_a.id == face_b.id && loop_a == loop_b {
132 return Err(KernelRefusal::unsupported(KernelStage::Classify, "seam_edge", "blend: edge is used twice by one loop (seam edge?)"));
133 }
134
135 let seam_a = face_a.loops[loop_a].coedges.len() > 1;
138 let seam_b = face_b.loops[loop_b].coedges.len() > 1;
139 let (first_face, first_loop, first_coedge, second_face, second_loop, second_coedge) =
140 if seam_a || !seam_b {
141 (face_a, loop_a, coedge_a, face_b, loop_b, coedge_b)
142 } else {
143 (face_b, loop_b, coedge_b, face_a, loop_a, coedge_a)
144 };
145 let anchored = first_face.loops[first_loop].coedges.len() > 1;
146 let second_seam = anchored && second_face.loops[second_loop].coedges.len() > 1;
147 let anchor_u = if anchored {
148 Some(first_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?[0])
150 } else {
151 None
152 };
153
154 let mid_radius = radius_at(edge.t0 + (edge.t1 - edge.t0) * 0.5);
156 let (rho1, rho2) = signed_radii(
157 edge,
158 first_face,
159 first_coedge,
160 second_face,
161 second_coedge,
162 mid_radius,
163 )?;
164
165 let first_mate = BlendMate {
166 face: first_face,
167 coedge: first_coedge,
168 loop_index: first_loop,
169 rho: rho1,
170 };
171 let second_mate = BlendMate {
172 face: second_face,
173 coedge: second_coedge,
174 loop_index: second_loop,
175 rho: rho2,
176 };
177 let mid_t = edge.t0 + (edge.t1 - edge.t0) * 0.5;
178 use crate::blend::chain::FoldPolicy;
193 let march_with = |fold: FoldPolicy, depth_budget: usize| -> Result<Vec<Station>, KernelRefusal> {
194 match march_stations(edge, &first_mate, &second_mate, radius_at, anchor_u, fold, depth_budget) {
195 Ok(stations) => Ok(stations),
196 Err(anchored_error) if anchor_u.is_some() => {
197 march_stations(edge, &first_mate, &second_mate, radius_at, None, fold, depth_budget).map_err(
198 |retry_error| {
199 if crate::blend::is_wall_fold(&retry_error)
200 || crate::blend::is_marched_fit_off_carriers(&retry_error) {
201 retry_error
202 } else {
203 anchored_error
204 }
205 },
206 )
207 }
208 Err(anchored_error) => Err(anchored_error),
209 }
210 };
211 let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
212 let wall_check = {
216 let radius = constant.unwrap_or(f64::NAN);
219 let constant = constant.is_some();
220 let model = crate::KernelTolerances::for_solid(solid, 1e-7).model;
223 let bar = crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
224 (!chamfer && constant && radius.is_finite() && radius != 0.0).then(|| crate::blend::stations::ClosedWallCheck {
225 radius: radius.abs(),
226 bar,
227 model: model.is_finite().then_some(model),
228 })
229 };
230 crate::blend::stations::CLOSED_MODEL_UNMET.with(|slot| slot.set(None));
231 crate::blend::stations::CLOSED_REQUEST_RAILS.with(|slot| slot.set(None));
232 let marched = match march_stations_with_rail_bar(edge, &first_mate, &second_mate, radius_at, anchor_u, FoldPolicy::Refuse, MAX_REFINEMENT_DEPTH, rail_bar, wall_check) {
233 Ok(stations) => Ok(stations),
234 Err(failure) => {
235 let anchored_error = failure.into_initial()?;
238 if anchor_u.is_some() {
239 if std::env::var("BREP_DEBUG_CLOSED_RAILS").is_ok() {
240 eprintln!("closed initial anchor fallback: edge {}: {anchored_error}", edge.id);
241 }
242 match march_stations_with_rail_bar(edge, &first_mate, &second_mate, radius_at, None, FoldPolicy::Refuse, MAX_REFINEMENT_DEPTH, rail_bar, wall_check) {
243 Ok(stations) => Ok(stations),
244 Err(failure) => {
245 let retry_error = failure.into_initial()?;
246 if crate::blend::is_wall_fold(&retry_error) { Err(retry_error) }
247 else { Err(anchored_error) }
248 }
249 }
250 } else { Err(anchored_error) }
251 }
252 };
253 let model_unmet = crate::blend::stations::CLOSED_MODEL_UNMET.with(|slot| slot.take());
266 let request_rails = crate::blend::stations::CLOSED_REQUEST_RAILS.with(|slot| slot.take());
267 let mut carve_fold = false;
268 let marched = match marched {
269 Err(error)
270 if crate::blend::is_wall_fold(&error)
271 && !crate::blend::fold::is_rail_fold(&error)
272 && !chamfer =>
273 {
274 crate::blend::carve::carve_trace(format_args!(
275 "blend closed-edge: the wall folds ({error}); re-marching under Carve"
276 ));
277 let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
286 let mut climbed: Option<Vec<Station>> = None;
287 let mut last_off = f64::NAN;
288 for depth_budget in MAX_REFINEMENT_DEPTH..=CARVE_MAX_REFINEMENT_DEPTH {
289 let stations = match march_with(FoldPolicy::Carve, depth_budget) {
290 Ok(stations) => stations,
291 Err(_) => break,
293 };
294 let parameters = station_parameters(&stations);
295 let off = match fit_closed_rows(&stations, ¶meters, chamfer) {
296 Ok(rows) => rails_off_closed_carriers(&rows, &stations, ¶meters, &first_mate, &second_mate)?,
297 Err(_) => break,
298 };
299 last_off = off;
300 crate::blend::carve::carve_trace(format_args!(
301 "blend closed-edge carve ladder: depth budget {depth_budget} ({} stations) leaves the rails {off:.3e} off their carriers (bar {rail_bar:.3e})",
302 stations.len()
303 ));
304 if off <= rail_bar {
305 climbed = Some(stations);
306 break;
307 }
308 }
309 match climbed {
310 Some(stations) => {
311 carve_fold = true;
312 Ok(stations)
313 }
314 None => Err(crate::blend::fold::wall_fold_refusal(
315 Vec::new(),
316 format!(
317 "{} {} fits this edge: the wall folds, and at depth budget {CARVE_MAX_REFINEMENT_DEPTH} — the top of the closed march's carve ladder — its fitted rails still stand {last_off:.3e} off their carriers between stations against a bar of {rail_bar:.3e}, so there is no wall accurate enough to carve ({error})",
318 crate::blend::WALL_FOLDS,
319 radius_at(mid_t).abs()
320 ),
321 )),
322 }
323 }
324 other => other,
325 };
326 let stations = match marched {
327 Ok(stations) => stations,
328 Err(march_error) if crate::blend::is_wall_fold(&march_error)
334 || crate::blend::is_marched_fit_off_carriers(&march_error) => return Err(march_error),
335 Err(march_error) if crate::blend::is_ball_off_carrier(&march_error) => {
336 return Err(march_error);
343 }
344 Err(march_error) => {
345 return blend_closed_edge_keep(
348 solid,
349 edge,
350 &first_mate,
351 &second_mate,
352 radius_at(mid_t),
353 rho1,
354 chamfer,
355 name,
356 )
357 .or_else(|_| {
358 blend_closed_edge_keep(
359 solid,
360 edge,
361 &second_mate,
362 &first_mate,
363 radius_at(mid_t),
364 rho2,
365 chamfer,
366 name,
367 )
368 })
369 .map_err(|keep_error| {
370 format!("{march_error}; edge-preserving blend also failed: {keep_error}")
371 }).or_refuse(KernelStage::Refine, "map_err");
372 }
373 };
374 let support_exits = |face: &FaceRecord, second_side: bool| -> bool {
378 let stride = (stations.len() / 16).max(1);
382 stations.iter().step_by(stride).any(|station| {
383 let mut uv = if second_side {
384 station.uv2
385 } else {
386 station.uv1
387 };
388 if let Ok((closed_u, closed_v)) = face.surface.closed_directions() {
389 if closed_u {
390 if let Ok([low, high]) = face.surface.domain_u() {
391 uv[0] = low + (uv[0] - low).rem_euclid(high - low);
392 }
393 }
394 if closed_v {
395 if let Ok([low, high]) = face.surface.domain_v() {
396 uv[1] = low + (uv[1] - low).rem_euclid(high - low);
397 }
398 }
399 }
400 crate::parameter_point_in_face(face, crate::Vec2 { x: uv[0], y: uv[1] }, 1e-6)
401 .map(|class| class == crate::PolygonClass::Outside)
402 .unwrap_or(true)
403 })
404 };
405 if support_exits(second_face, true) {
406 return blend_closed_edge_keep(
407 solid,
408 edge,
409 &first_mate,
410 &second_mate,
411 radius_at(mid_t),
412 rho1,
413 chamfer,
414 name,
415 );
416 }
417 if support_exits(first_face, false) {
418 return blend_closed_edge_keep(
419 solid,
420 edge,
421 &second_mate,
422 &first_mate,
423 radius_at(mid_t),
424 rho2,
425 chamfer,
426 name,
427 );
428 }
429 if std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok() {
430 let last = stations.len() - 1;
431 for index in [
432 0usize,
433 1.min(last),
434 2.min(last),
435 last.saturating_sub(1),
436 last,
437 ] {
438 let station = &stations[index];
439 eprintln!(
440 "station {index}: uv1=({:.6},{:.6}) uv2=({:.6},{:.6}) p1=({:.6},{:.6},{:.6}) w={:.6}",
441 station.uv1[0], station.uv1[1], station.uv2[0], station.uv2[1],
442 station.p1.x, station.p1.y, station.p1.z, station.weight,
443 );
444 }
445 }
446 let parameters = station_parameters(&stations);
447 let exact_rows = exact_closed_revolution_rows(&stations, &first_mate, &second_mate, chamfer);
448 let rows = match exact_rows {
449 Some(rows) => rows?,
450 None => {
451 let mut rows = fit_closed_rows(&stations, ¶meters, chamfer)?;
461 rows.cr_pcurve = refit_closed_support_pcurve(&rows.cr, &rows.cr_pcurve, &first_mate.face.surface)?;
462 rows.cs_pcurve = refit_closed_support_pcurve(&rows.cs, &rows.cs_pcurve, &second_mate.face.surface)?;
463 rows
464 }
465 };
466 let crease = if carve_fold {
467 Some(carve_closed_wall(solid, &rows, &stations, ¶meters, &first_mate, &second_mate, radius_at(mid_t).abs())?)
468 } else {
469 None
470 };
471
472 let second_split = if second_seam {
476 let seam2 = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?[0];
477 let period2 = {
478 let [d0, d1] = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
479 d1 - d0
480 };
481 let u2_first = stations[0].uv2[0];
483 let u2_last = stations[stations.len() - 1].uv2[0];
484 let direction = (u2_last - u2_first).signum();
485 let mut k = ((u2_first - seam2) / period2).ceil();
486 if direction < 0.0 {
487 k = ((u2_first - seam2) / period2).floor();
488 }
489 let target = seam2 + k * period2;
490 let inside =
491 (target - u2_first) * direction > 1e-6 && (u2_last - target) * direction > 1e-6;
492 let aligned = (target - u2_first).abs() <= 1e-6
497 || (target - u2_last).abs() <= 1e-6
498 || ((u2_first - seam2) / period2).fract().abs() <= 1e-9;
499 if !inside && aligned {
500 None
501 } else if !inside {
502 return Err(KernelRefusal::internal(KernelStage::Refine, "seam_crossing_bracket", "blend: second seam crossing not bracketed by the march"));
503 } else {
504 let [fit_low, fit_high] = rows.u_domain;
508 let mut p = fit_low
509 + (fit_high - fit_low) * {
510 let mut seed = 0.5;
512 for pair in 0..stations.len() - 1 {
513 let a = stations[pair].uv2[0];
514 let b = stations[pair + 1].uv2[0];
515 if (target - a) * (target - b) <= 0.0 {
516 let local = (target - a) / (b - a);
517 seed = (parameters[pair]
518 + (parameters[pair + 1] - parameters[pair]) * local)
519 .clamp(0.0, 1.0);
520 break;
521 }
522 }
523 seed
524 };
525 for _ in 0..NEWTON_ITERATIONS {
526 let value = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
527 if value.abs() <= 1e-12 {
528 break;
529 }
530 let step = 1e-8;
531 let probed = rows.cs_pcurve.evaluate(p + step).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
532 let derivative = (probed - value) / step;
533 if derivative.abs() <= 1e-14 {
534 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "seam_crossing_newton", "blend: second seam crossing Newton stalled"));
535 }
536 p -= value / derivative;
537 }
538 let crossing_v = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.y;
539 Some(SecondSeamSplit {
540 fit_parameter: p,
541 crossing_v,
542 period: period2 * direction,
543 })
544 }
545 } else {
546 None
547 };
548
549 let model_unmet = if carve_fold { None } else { model_unmet };
553 let wall_surface = model_unmet.map(|_| rows.surface.clone());
554 let built = build_surgery(
555 solid,
556 edge,
557 &first_mate,
558 &second_mate,
559 rows,
560 second_split,
561 name,
562 crease,
563 )?;
564 if let (Some((declared, reason, round, stations, ceiling)), Some(surface)) = (model_unmet, wall_surface) {
565 crate::blend::record_wall_model_report(crate::blend::WallModelReport {
566 name: name.map(str::to_string),
567 surface,
568 request: crate::KernelTolerances::for_solid(solid, 1e-7).model,
569 residual: declared,
570 detail: request_rails.map(|(rails, unread, wall)| {
573 format!("last request read: rails {rails:.3e} off their carriers at j/16 ({unread} interval(s) unread), wall {wall:.3e}")
574 }),
575 budget: crate::ApproximationBudget {
576 reason,
577 rounds_used: round,
578 rounds_limit: crate::blend::stations::REFINE_ROUNDS,
579 stations,
580 station_limit: ceiling,
581 mechanism: crate::BudgetMechanism::LocalRounds,
582 measured_component: match request_rails {
585 Some((rails, _, wall)) if rails > wall => crate::MeasuredComponent::Rails,
586 _ => crate::MeasuredComponent::Wall,
587 },
588 unread: request_rails.map_or(0, |(_, unread, _)| unread),
589 },
590 });
591 }
592 Ok(built)
593}
594
595fn separate_spherical_domain_cut(
601 solid: &BrepSolid,
602 edge_id: u64,
603) -> Result<Option<BrepSolid>, KernelRefusal> {
604 let Some(edge) = solid.edges.iter().find(|edge| edge.id == edge_id) else {
605 return Ok(None);
606 };
607 if edge.start_vertex_id != edge.end_vertex_id {
608 return Ok(None);
609 }
610 let mut result = solid.clone();
611 let mut take_id = crate::blend::edge::fresh_id_source(solid);
612 let mut separated = false;
613 for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
614 let Some(atlas) = crate::sphere_chart::SphereAtlas::of_surface(&face.surface) else {
615 continue;
616 };
617 let [u0, u1] = face
618 .surface
619 .domain_u()
620 .or_refuse(KernelStage::Collect, "domain_cut_u")?;
621 let [v0, v1] = face
622 .surface
623 .domain_v()
624 .or_refuse(KernelStage::Collect, "domain_cut_v")?;
625 if [u0, u1, v0, v1] != [0.0, 1.0, 0.0, 1.0] {
626 continue;
627 }
628 if face
629 .surface
630 .closed_directions()
631 .or_refuse(KernelStage::Collect, "domain_cut_closed")?
632 != (true, false)
633 {
634 continue;
635 }
636 let band = 1e-8 * atlas.radius;
637 let bottom = face
638 .surface
639 .evaluate(u0, v0)
640 .or_refuse(KernelStage::Collect, "domain_cut_bottom")?;
641 let top = face
642 .surface
643 .evaluate(u0, v1)
644 .or_refuse(KernelStage::Collect, "domain_cut_top")?;
645 if bottom.add(top).scale(0.5).sub(atlas.centre).length() > band
646 || (bottom.sub(top).length() - 2.0 * atlas.radius).abs() > band
647 {
648 continue;
649 }
650 let mut additions = Vec::new();
651 let mut cut_ids = std::collections::HashSet::new();
652 for rim in &mut face.loops {
653 if rim.coedges.len() < 5 {
654 continue;
655 }
656 let positions: Vec<_> = rim
657 .coedges
658 .iter()
659 .enumerate()
660 .filter_map(|(i, c)| (c.edge_id == edge_id).then_some(i))
661 .collect();
662 if positions.len() != 1 {
663 continue;
664 }
665 let position = positions[0];
666 let ring = &rim.coedges[position];
667 let [a, b] = ring
668 .pcurve
669 .domain()
670 .or_refuse(KernelStage::Collect, "domain_cut_ring")?;
671 if ring
672 .pcurve
673 .evaluate(a)
674 .or_refuse(KernelStage::Collect, "domain_cut_ring_start")?
675 .sub(
676 ring.pcurve
677 .evaluate(b)
678 .or_refuse(KernelStage::Collect, "domain_cut_ring_end")?,
679 )
680 .length()
681 > 1e-7
682 {
683 continue;
684 }
685 let rest: Vec<_> = rim
686 .coedges
687 .iter()
688 .filter(|c| c.edge_id != edge_id)
689 .collect();
690 let mut points = Vec::new();
691 let mut domain_cut = true;
692 for coedge in &rest {
693 let [a, b] = coedge
694 .pcurve
695 .domain()
696 .or_refuse(KernelStage::Collect, "domain_cut_trim")?;
697 let start = coedge
698 .pcurve
699 .evaluate(a)
700 .or_refuse(KernelStage::Collect, "domain_cut_start")?;
701 let end = coedge
702 .pcurve
703 .evaluate(b)
704 .or_refuse(KernelStage::Collect, "domain_cut_end")?;
705 points.push(start);
706 let degenerate = solid
707 .edges
708 .iter()
709 .any(|e| e.id == coedge.edge_id && e.degenerate);
710 let at = |x: f64, low: f64, high: f64| {
711 (x - low).abs() <= 1e-8 || (x - high).abs() <= 1e-8
712 };
713 let on_boundary = if degenerate {
714 (start.y - end.y).abs() <= 1e-8
715 && at(start.y, v0, v1)
716 && coedge
717 .pcurve
718 .control_points
719 .iter()
720 .all(|p| (p.y / p.w - start.y).abs() <= 1e-8)
721 } else {
722 at(start.x, u0, u1)
723 && (start.x - end.x).abs() <= 1e-8
724 && coedge
725 .pcurve
726 .control_points
727 .iter()
728 .all(|p| (p.x / p.w - start.x).abs() <= 1e-8)
729 && rest.iter().filter(|c| c.edge_id == coedge.edge_id).count() == 2
730 && rest
731 .iter()
732 .any(|c| c.edge_id == coedge.edge_id && c.forward != coedge.forward)
733 };
734 domain_cut &= on_boundary;
735 }
736 let twice_area: f64 = (0..points.len())
737 .map(|i| {
738 let a = points[i];
739 let b = points[(i + 1) % points.len()];
740 a.x * b.y - a.y * b.x
741 })
742 .sum();
743 let expected = (u1 - u0) * (v1 - v0) * if face.same_sense { 1.0 } else { -1.0 };
744 if !domain_cut || (0.5 * twice_area - expected).abs() > 1e-7 * expected.abs() {
745 continue;
746 }
747 let cuts: std::collections::HashSet<_> = rest
751 .iter()
752 .filter_map(|c| {
753 solid
754 .edges
755 .iter()
756 .find(|e| e.id == c.edge_id && !e.degenerate)
757 .map(|e| e.id)
758 })
759 .collect();
760 for cut in solid.edges.iter().filter(|e| cuts.contains(&e.id)) {
761 let users = solid
762 .shells
763 .iter()
764 .flat_map(|s| &s.faces)
765 .flat_map(|f| &f.loops)
766 .flat_map(|l| &l.coedges)
767 .filter(|c| c.edge_id == cut.id)
768 .count();
769 let exclusive_vertices = solid
770 .edges
771 .iter()
772 .filter(|e| {
773 e.start_vertex_id == cut.start_vertex_id
774 || e.end_vertex_id == cut.start_vertex_id
775 || e.start_vertex_id == cut.end_vertex_id
776 || e.end_vertex_id == cut.end_vertex_id
777 })
778 .all(|e| e.degenerate || cuts.contains(&e.id) || e.id == edge_id);
779 let mut same_parameter = cut.t0 >= v0 && cut.t1 <= v1 && cut.t0 < cut.t1;
780 for i in 0..=16 {
781 let t = cut.t0 + (cut.t1 - cut.t0) * i as f64 / 16.0;
782 let point = cut
783 .curve
784 .evaluate(t)
785 .or_refuse(KernelStage::Collect, "domain_cut_curve")?;
786 let carrier = face
787 .surface
788 .evaluate(u0, t)
789 .or_refuse(KernelStage::Collect, "domain_cut_carrier")?;
790 same_parameter &= point.sub(carrier).length() <= band;
791 }
792 domain_cut &= users == 2 && exclusive_vertices && same_parameter;
793 }
794 if !domain_cut {
795 continue;
796 }
797 cut_ids.extend(cuts);
798 additions.push(LoopRecord {
799 id: take_id(),
800 coedges: vec![rim.coedges.remove(position)],
801 });
802 separated = true;
803 }
804 if !additions.is_empty() {
805 let real = |id: u64| {
809 !cut_ids.contains(&id) && !solid.edges.iter().any(|e| e.id == id && e.degenerate)
810 };
811 let curves: Vec<_> = face
812 .loops
813 .iter()
814 .flat_map(|l| &l.coedges)
815 .chain(additions.iter().flat_map(|l| &l.coedges))
816 .filter(|c| real(c.edge_id) || c.edge_id == edge_id)
817 .map(|c| &c.pcurve)
818 .collect();
819 let mut candidates = Vec::new();
820 for shift in [0.25, 0.5, 0.75] {
821 let mut clearance = f64::INFINITY;
822 let mut fits = true;
823 for curve in &curves {
824 let first = curve.control_points[0].x / curve.control_points[0].w;
825 let offset = -(first - shift).floor();
826 for p in &curve.control_points {
827 let u = p.x / p.w - shift + offset;
828 fits &= u >= 0.0 && u <= 1.0;
829 clearance = clearance.min(u.min(1.0 - u));
830 }
831 }
832 if fits {
833 candidates.push((clearance, shift));
834 }
835 }
836 let Some((_, shift)) = candidates.into_iter().max_by(|a, b| a.0.total_cmp(&b.0)) else {
837 return Err(KernelRefusal::unsupported(
838 KernelStage::Collect,
839 "sphere_domain_cut",
840 "blend: no quarter-turn sphere chart keeps the real trims in-domain",
841 ));
842 };
843 let seam_direction = face
844 .surface
845 .evaluate(shift, 0.5)
846 .or_refuse(KernelStage::Collect, "sphere_cut_direction")?
847 .sub(atlas.centre);
848 let surface = crate::make_sphere_surface_framed(
849 atlas.centre,
850 atlas.radius,
851 atlas.basis[2],
852 Some(seam_direction),
853 )
854 .or_refuse(KernelStage::Collect, "sphere_cut_surface")?;
855 for i in 0..=16 {
857 for v in [0.0, 0.3, 0.5, 0.7, 1.0] {
858 let u = i as f64 / 16.0;
859 let old = face
860 .surface
861 .evaluate(u, v)
862 .or_refuse(KernelStage::Collect, "sphere_cut_old")?;
863 let new = surface
864 .evaluate((u - shift).rem_euclid(1.0), v)
865 .or_refuse(KernelStage::Collect, "sphere_cut_new")?;
866 if old.sub(new).length() > band {
867 return Err(KernelRefusal::unsupported(
868 KernelStage::Collect,
869 "sphere_cut_chart",
870 "blend: the sphere quarter turn is not the same carrier",
871 ));
872 }
873 }
874 }
875 let seam = surface
876 .iso_curve_u(0.0)
877 .or_refuse(KernelStage::Collect, "sphere_cut_seam")?;
878 for coedge in face
879 .loops
880 .iter_mut()
881 .flat_map(|l| &mut l.coedges)
882 .chain(additions.iter_mut().flat_map(|l| &mut l.coedges))
883 {
884 if solid
885 .edges
886 .iter()
887 .any(|e| e.id == coedge.edge_id && e.degenerate)
888 {
889 continue;
890 }
891 if !cut_ids.contains(&coedge.edge_id) {
892 let first =
893 coedge.pcurve.control_points[0].x / coedge.pcurve.control_points[0].w;
894 let offset = -(first - shift).floor() - shift;
895 for p in &mut coedge.pcurve.control_points {
896 p.x += offset * p.w;
897 }
898 }
899 }
900 for cut in result.edges.iter_mut().filter(|e| cut_ids.contains(&e.id)) {
901 for (vertex_id, t) in [(cut.start_vertex_id, cut.t0), (cut.end_vertex_id, cut.t1)] {
902 let point = seam
903 .evaluate(t)
904 .or_refuse(KernelStage::Collect, "sphere_cut_vertex")?;
905 let vertex = result
906 .vertices
907 .iter_mut()
908 .find(|v| v.id == vertex_id)
909 .ok_or(KernelRefusal::internal(
910 KernelStage::Collect,
911 "sphere_cut_vertex",
912 "blend: sphere seam vertex is missing",
913 ))?;
914 vertex.point = point;
915 }
916 cut.curve = seam.clone();
917 }
918 face.surface = surface;
919 }
920 face.loops.extend(additions);
921 }
922 if !separated {
923 return Ok(None);
924 }
925 let old_vertex = solid
926 .vertices
927 .iter()
928 .find(|v| v.id == edge.start_vertex_id)
929 .ok_or(KernelRefusal::internal(
930 KernelStage::Collect,
931 "domain_cut_vertex",
932 "blend: closed ring vertex is missing",
933 ))?;
934 let vertex_id = take_id();
935 result.vertices.push(VertexRecord {
936 id: vertex_id,
937 point: old_vertex.point,
938 });
939 let ring = result
940 .edges
941 .iter_mut()
942 .find(|e| e.id == edge_id)
943 .ok_or(KernelRefusal::internal(
944 KernelStage::Collect,
945 "domain_cut_ring",
946 "blend: closed ring edge is missing",
947 ))?;
948 ring.start_vertex_id = vertex_id;
949 ring.end_vertex_id = vertex_id;
950 Ok(Some(result))
951}
952
953fn remove_retraced_cuts(
959 solid: &BrepSolid,
960 edge_id: u64,
961) -> Result<Option<BrepSolid>, KernelRefusal> {
962 let mut result = solid.clone();
963 let mut removed = Vec::new();
964 for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
965 for rim in &mut face.loops {
966 if !rim.coedges.iter().any(|coedge| coedge.edge_id == edge_id) {
967 continue;
968 }
969 loop {
970 let n = rim.coedges.len();
971 let mut pair = None;
972 for i in 0..n {
973 let j = (i + 1) % n;
974 let (a, b) = (&rim.coedges[i], &rim.coedges[j]);
975 if a.edge_id == edge_id || a.edge_id != b.edge_id || a.forward == b.forward {
976 continue;
977 }
978 let users = solid
979 .shells
980 .iter()
981 .flat_map(|s| &s.faces)
982 .flat_map(|f| &f.loops)
983 .flat_map(|l| &l.coedges)
984 .filter(|c| c.edge_id == a.edge_id)
985 .count();
986 if users != 2 {
987 continue;
988 }
989 let [a0, a1] = a
990 .pcurve
991 .domain()
992 .or_refuse(KernelStage::Collect, "cut_domain")?;
993 let [b0, b1] = b
994 .pcurve
995 .domain()
996 .or_refuse(KernelStage::Collect, "cut_domain")?;
997 let agrees =
1000 a.pcurve.degree == b.pcurve.degree
1001 && a.pcurve.knots.len() == b.pcurve.knots.len()
1002 && a.pcurve.control_points.len() == b.pcurve.control_points.len()
1003 && a.pcurve.knots.iter().zip(b.pcurve.knots.iter().rev()).all(
1004 |(x, y)| {
1005 ((x - a0) / (a1 - a0) - (b1 - y) / (b1 - b0)).abs() <= 1e-9
1006 },
1007 )
1008 && a.pcurve
1009 .control_points
1010 .iter()
1011 .zip(b.pcurve.control_points.iter().rev())
1012 .all(|(p, q)| {
1013 p.point()
1014 .ok()
1015 .zip(q.point().ok())
1016 .is_some_and(|(p, q)| p.sub(q).length() <= 1e-9)
1017 && (p.w / a.pcurve.control_points[0].w
1018 - q.w / b.pcurve.control_points.last().unwrap().w)
1019 .abs()
1020 <= 1e-9
1021 });
1022 if agrees {
1023 pair = Some((i, j, a.edge_id));
1024 break;
1025 }
1026 }
1027 let Some((i, j, id)) = pair else {
1028 break;
1029 };
1030 rim.coedges.remove(i.max(j));
1031 rim.coedges.remove(i.min(j));
1032 removed.push(id);
1033 }
1034 }
1035 }
1036 if removed.is_empty() {
1037 return Ok(None);
1038 }
1039 result.edges.retain(|edge| !removed.contains(&edge.id));
1040 super::open::prune_orphan_vertices(&mut result);
1041 Ok(Some(result))
1042}
1043
1044struct SecondSeamSplit {
1047 fit_parameter: f64,
1048 crossing_v: f64,
1049 period: f64,
1051}
1052
1053const CARVE_MAX_REFINEMENT_DEPTH: usize = MAX_REFINEMENT_DEPTH + 4;
1065
1066fn rails_off_closed_carriers(
1070 rows: &FittedRows,
1071 stations: &[Station],
1072 parameters: &[f64],
1073 first: &BlendMate,
1074 second: &BlendMate,
1075) -> Result<f64, KernelRefusal> {
1076 Ok(closed_rail_interval_misses(rows, stations, parameters,
1077 [&first.face.surface, &second.face.surface])?.into_iter().fold(0.0_f64, f64::max))
1078}
1079
1080fn carve_closed_wall(
1081 solid: &BrepSolid,
1082 rows: &FittedRows,
1083 stations: &[Station],
1084 parameters: &[f64],
1085 first: &BlendMate,
1086 second: &BlendMate,
1087 radius: f64,
1088) -> Result<crate::blend::carve::CarvedCrease, KernelRefusal> {
1089 let policy = crate::KernelTolerances::for_solid(solid, 1e-7);
1090 let rail_bar = 0.5 * policy.intersection_fit;
1091 let off = rails_off_closed_carriers(rows, stations, parameters, first, second)?;
1092 crate::blend::carve::carve_trace(format_args!(
1093 "blend closed-edge carve: {} stations, rails {off:.3e} off their carriers between stations (bar {rail_bar:.3e})",
1094 parameters.len()
1095 ));
1096 if off > rail_bar {
1097 return Err(crate::blend::fold::wall_fold_refusal(
1098 Vec::new(),
1099 format!(
1100 "{} {radius} fits this edge: the wall folds, and the closed march's fitted rails stand {off:.3e} off their carriers between stations (bar {rail_bar:.3e}), so there is no wall accurate enough to carve",
1101 crate::blend::WALL_FOLDS
1102 ),
1103 ));
1104 }
1105 let Some(carved) = crate::blend::carve::trace_wall_crease(&rows.surface, radius)? else {
1106 return Err(KernelRefusal::internal(KernelStage::Refine, "carve_no_fold", "blend closed-edge carve: the wall re-marched under Carve carries no fold to carve, but the march refused one"));
1107 };
1108 let [u0, u1] = rows.u_domain;
1109 let margin = 1e-3 * (u1 - u0);
1110 let straddles = carved.ends.iter().any(|end| end[0] <= u0 + margin || end[0] >= u1 - margin);
1111 if straddles {
1112 return Err(crate::blend::fold::wall_fold_refusal(
1113 Vec::new(),
1114 format!(
1115 "{} {radius} fits this edge: the wall folds, and the fold band straddles the wall's own seam (lens ends at u = {:.6} and {:.6} of [{u0:.3}, {u1:.3}]), which the crease tracer reads as two half-lenses; carving across the seam is not built",
1116 crate::blend::WALL_FOLDS, carved.ends[0][0], carved.ends[1][0]
1117 ),
1118 ));
1119 }
1120 crate::blend::carve::fit_crease(&rows.surface, &carved, policy.pcurve_consistency)
1121}
1122
1123pub(super) fn refit_closed_support_pcurve(rail: &NurbsCurve, old: &NurbsCurve, surface: &NurbsSurface) -> Result<NurbsCurve, KernelRefusal> {
1143 const SHIPPED_READ_REPAIRS: usize = 3;
1144 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1145 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1146 let mut near: Vec<f64> = Vec::new();
1147 let (mut fitted, mut read) = fit_and_read_support(rail, old, surface, &near)?;
1148 for _ in 0..SHIPPED_READ_REPAIRS {
1149 if read.0 <= floor || !(d1 > d0) {
1150 break;
1151 }
1152 near.push((read.3 - d0) / (d1 - d0));
1153 match fit_and_read_support(rail, old, surface, &near) {
1154 Ok(repaired) => (fitted, read) = repaired,
1155 Err(_) => break,
1156 }
1157 }
1158 let (miss, standoff, samples, _) = read;
1159 if !(miss <= floor) {
1160 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "closed_support_pcurve_floor", format!(
1161 "{} a blend's support pcurve, as shipped, misses its rail's track by {miss:.3e} at {samples} samples, \
1162 against a floor of {floor:.1e}",
1163 crate::blend::PCURVE_OFF_FLOOR,
1164 )));
1165 }
1166 Ok(fitted)
1167}
1168
1169fn fit_and_read_support(
1173 rail: &NurbsCurve,
1174 old: &NurbsCurve,
1175 surface: &NurbsSurface,
1176 near: &[f64],
1177) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1178 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1179 let guide = crate::blend::chain::pcurve_guide(old, surface)?;
1182 let mut fitted = crate::blend::chain::project_piece_pcurve_within(rail, surface, false, false, Some(&guide), near)?;
1183 let [f0, f1] = fitted.domain().or_refuse(KernelStage::Refine, "domain")?;
1186 for knot in fitted.knots.iter_mut() {
1187 *knot = d0 + (*knot - f0) / (f1 - f0) * (d1 - d0);
1188 }
1189 let debug = std::env::var_os("BREP_DEBUG_TRACK_FIT").is_some();
1190 if debug {
1191 match crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &fitted, d0, d1) {
1194 Ok((miss, standoff, samples, worst)) => eprintln!(
1195 "TRACK_FIT closed-edge support: before the seam ends, miss {miss:.3e} at t {worst:.9} ({samples} samples), standoff {standoff:.3e}"),
1196 Err(error) => eprintln!("TRACK_FIT closed-edge support: before the seam ends, unreadable: {error}"),
1197 }
1198 }
1199 let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
1200 let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1201 let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1202 let old_start = old.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?;
1203 let old_end = old.evaluate(d1).or_refuse(KernelStage::Refine, "evaluate")?;
1204 let new_start = fitted.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?;
1205 let shift = |closed: bool, from: f64, to: f64, period: f64| if closed { ((to - from) / period).round() * period } else { 0.0 };
1207 let (shift_u, shift_v) = (shift(closed_u, new_start.x, old_start.x, u1 - u0), shift(closed_v, new_start.y, old_start.y, v1 - v0));
1208 for point in fitted.control_points.iter_mut() {
1209 point.x += shift_u * point.w;
1210 point.y += shift_v * point.w;
1211 }
1212 let on_seam = |closed: bool, value: f64, low: f64, high: f64| {
1213 let period = high - low;
1214 closed && ((value - low) / period - ((value - low) / period).round()).abs() * period <= 1e-12 * period.max(1.0)
1215 };
1216 let last = fitted.control_points.len() - 1;
1217 for (index, end) in [(0, old_start), (last, old_end)] {
1218 let point = &mut fitted.control_points[index];
1219 if on_seam(closed_u, end.x, u0, u1) {
1220 point.x = end.x * point.w;
1221 }
1222 if on_seam(closed_v, end.y, v0, v1) {
1223 point.y = end.y * point.w;
1224 }
1225 }
1226 let (miss, standoff, samples, worst) =
1231 crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &fitted, d0, d1)?;
1232 if debug {
1233 eprintln!("TRACK_FIT closed-edge support: shipped pcurve miss {miss:.3e} at t {worst:.9} of [{d0:.9}, {d1:.9}] ({samples} samples); rail standoff from its carrier {standoff:.3e}");
1234 }
1235 Ok((fitted, (miss, standoff, samples, worst)))
1236}
1237
1238
1239fn build_surgery(
1240 solid: &BrepSolid,
1241 edge: &EdgeRecord,
1242 first: &BlendMate,
1243 second: &BlendMate,
1244 rows: FittedRows,
1245 second_split: Option<SecondSeamSplit>,
1246 name: Option<&str>,
1247 crease: Option<crate::blend::carve::CarvedCrease>,
1248) -> Result<BrepSolid, KernelRefusal> {
1249 let mut result = solid.clone();
1250 let mut take_id = crate::blend::edge::fresh_id_source(solid);
1251
1252 let [u_start, u_end] = rows.u_domain;
1253 let cr_start = rows.cr.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1254 let cs_start = rows.cs.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1255 let vertex1_id = take_id();
1256 let vertex2_id = take_id();
1257 result.vertices.push(VertexRecord {
1258 id: vertex1_id,
1259 point: cr_start,
1260 });
1261 result.vertices.push(VertexRecord {
1262 id: vertex2_id,
1263 point: cs_start,
1264 });
1265
1266 let cr_edge_id = take_id();
1267 result.edges.push(EdgeRecord {
1268 id: cr_edge_id,
1269 curve: rows.cr.clone(),
1270 t0: u_start,
1271 t1: u_end,
1272 start_vertex_id: vertex1_id,
1273 end_vertex_id: vertex1_id,
1274 degenerate: false,
1275 name: None,
1276 });
1277 let mut cs_pieces: Vec<(u64, [f64; 2], NurbsCurve)> = Vec::new();
1281 let mut second_seam_vertex = vertex2_id;
1282 let mut second_seam_point = cs_start;
1283 let mut second_crossing_v = 0.0;
1284 if let Some(split) = &second_split {
1285 let p = split.fit_parameter;
1286 second_crossing_v = split.crossing_v;
1287 let (cs_a, cs_b) = rows.cs.split(p).or_refuse(KernelStage::Refine, "split")?;
1288 let (pc_a, pc_b) = rows.cs_pcurve.split(p).or_refuse(KernelStage::Refine, "split")?;
1289 let mut pc_b = pc_b;
1291 for point in pc_b.control_points.iter_mut() {
1292 point.x -= split.period * point.w;
1293 }
1294 let w2s = take_id();
1295 second_seam_vertex = w2s;
1296 second_seam_point = rows.cs.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?;
1297 result.vertices.push(VertexRecord {
1298 id: w2s,
1299 point: second_seam_point,
1300 });
1301 let edge_a = take_id();
1302 result.edges.push(EdgeRecord {
1303 id: edge_a,
1304 curve: cs_a,
1305 t0: u_start,
1306 t1: p,
1307 start_vertex_id: vertex2_id,
1308 end_vertex_id: w2s,
1309 degenerate: false,
1310 name: None,
1311 });
1312 cs_pieces.push((edge_a, [u_start, p], pc_a));
1313 let edge_b = take_id();
1314 result.edges.push(EdgeRecord {
1315 id: edge_b,
1316 curve: cs_b,
1317 t0: p,
1318 t1: u_end,
1319 start_vertex_id: w2s,
1320 end_vertex_id: vertex2_id,
1321 degenerate: false,
1322 name: None,
1323 });
1324 cs_pieces.push((edge_b, [p, u_end], pc_b));
1325 } else {
1326 let cs_edge_id = take_id();
1327 result.edges.push(EdgeRecord {
1328 id: cs_edge_id,
1329 curve: rows.cs.clone(),
1330 t0: u_start,
1331 t1: u_end,
1332 start_vertex_id: vertex2_id,
1333 end_vertex_id: vertex2_id,
1334 degenerate: false,
1335 name: None,
1336 });
1337 cs_pieces.push((cs_edge_id, [u_start, u_end], rows.cs_pcurve.clone()));
1338 }
1339 let seam_curve = rows.surface.iso_curve_u(u_start).or_refuse(KernelStage::Refine, "iso_curve_u")?;
1340 let [seam_t0, seam_t1] = seam_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1341 let blend_seam_id = take_id();
1342 result.edges.push(EdgeRecord {
1343 id: blend_seam_id,
1344 curve: seam_curve,
1345 t0: seam_t0,
1346 t1: seam_t1,
1347 start_vertex_id: vertex1_id,
1348 end_vertex_id: vertex2_id,
1349 degenerate: false,
1350 name: None,
1351 });
1352
1353 let old_vertex = edge.start_vertex_id;
1354 let replace_in_face = |result: &mut BrepSolid,
1355 mate: &BlendMate,
1356 pieces: &[(u64, [f64; 2], NurbsCurve)],
1357 uv_new: [f64; 2]|
1358 -> Result<bool, KernelRefusal> {
1359 let v_new = uv_new[1];
1360 let seam_edge_ends: Vec<(u64, bool, bool)> = result
1362 .edges
1363 .iter()
1364 .filter(|candidate| {
1365 candidate.id != edge.id
1366 && (candidate.start_vertex_id == old_vertex
1367 || candidate.end_vertex_id == old_vertex)
1368 })
1369 .map(|candidate| {
1370 (
1371 candidate.id,
1372 candidate.start_vertex_id == old_vertex,
1373 candidate.end_vertex_id == old_vertex,
1374 )
1375 })
1376 .collect();
1377 let seam_edge_ids: Vec<u64> = seam_edge_ends.iter().map(|(id, ..)| *id).collect();
1378 let pcurve_band = crate::KernelTolerances::for_solid(result, 1e-7).pcurve_consistency;
1379 let debug = std::env::var("BREP_DEBUG_NETWORK").is_ok();
1380 let face = result
1381 .shells
1382 .iter_mut()
1383 .flat_map(|shell| &mut shell.faces)
1384 .find(|face| face.id == mate.face.id)
1385 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1386 let face_id = face.id;
1387 let FaceRecord { surface, loops, .. } = &mut *face;
1388 let loop_record = &mut loops[mate.loop_index];
1389 let position = loop_record
1390 .coedges
1391 .iter()
1392 .position(|coedge| coedge.edge_id == edge.id)
1393 .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
1394 let old_forward = loop_record.coedges[position].forward;
1395 let old_seam_v = {
1396 let pcurve = &loop_record.coedges[position].pcurve;
1397 let [d0, _] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1398 pcurve.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?.y
1399 };
1400 let mut replacements = Vec::new();
1403 let ordered: Vec<&(u64, [f64; 2], NurbsCurve)> = if old_forward {
1404 pieces.iter().collect()
1405 } else {
1406 pieces.iter().rev().collect()
1407 };
1408 for (index, (edge_id, _, pcurve)) in ordered.iter().enumerate() {
1409 replacements.push(CoedgeRecord {
1410 id: if index == 0 {
1411 loop_record.coedges[position].id
1412 } else {
1413 0 },
1415 edge_id: *edge_id,
1416 forward: old_forward,
1417 pcurve: if old_forward {
1418 pcurve.clone()
1419 } else {
1420 pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
1421 },
1422 });
1423 }
1424 loop_record
1425 .coedges
1426 .splice(position..=position, replacements);
1427 let mut repaired = false;
1428 if loop_record.coedges.len() > 1 {
1429 for coedge in &mut loop_record.coedges {
1435 if pieces
1436 .iter()
1437 .any(|(edge_id, ..)| *edge_id == coedge.edge_id)
1438 || !seam_edge_ids.contains(&coedge.edge_id)
1439 {
1440 continue;
1441 }
1442 let controls = &coedge.pcurve.control_points;
1443 let last_index = controls.len() - 1;
1444 let first_u = controls[0].x / controls[0].w;
1445 let last_u = controls[last_index].x / controls[last_index].w;
1446 let constant_u = (first_u - last_u).abs()
1447 <= 1e-9 * (1.0 + first_u.abs().max(last_u.abs()));
1448 let (target, target_uv, fraction) = if constant_u {
1451 let first_v = controls[0].y / controls[0].w;
1455 let last_v = controls[last_index].y / controls[last_index].w;
1456 let target = if (first_v - old_seam_v).abs() < (last_v - old_seam_v).abs() {
1457 0
1458 } else {
1459 last_index
1460 };
1461 let span = last_v - first_v;
1462 let fraction = if span.abs() > 0.0 { (v_new - first_v) / span } else { 0.0 };
1463 (target, [first_u, v_new], fraction)
1464 } else {
1465 let Some((_, start_is_old, end_is_old)) = seam_edge_ends
1475 .iter()
1476 .find(|(id, ..)| *id == coedge.edge_id)
1477 .copied()
1478 else {
1479 continue;
1480 };
1481 if start_is_old == end_is_old {
1482 return Err(KernelRefusal::ill_posed(KernelStage::Sew, "neighbour_trim_end", format!(
1483 "blend: neighbour edge {} meets the blended edge at both ends; \
1484 its trim end is ambiguous",
1485 coedge.edge_id
1486 )));
1487 }
1488 let target = if start_is_old == coedge.forward { 0 } else { last_index };
1491 let v_end = controls[target].y / controls[target].w;
1492 let span = last_u - first_u;
1493 let fraction = if span.abs() > 0.0 { (uv_new[0] - first_u) / span } else { 0.0 };
1494 (target, [uv_new[0], v_end], fraction)
1495 };
1496 let [q0, q1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1508 let target_point = surface
1509 .evaluate_extended(target_uv[0], target_uv[1])
1510 .or_refuse(KernelStage::Refine, "evaluate_extended")?;
1511 let seed = (q0 + (q1 - q0) * fraction).clamp(q0.min(q1), q0.max(q1));
1517 let q_split = trim_parameter_at_point(
1518 surface, &coedge.pcurve, seed, target_point, pcurve_band,
1519 )?;
1520 let interior = q_split > q0 + 1e-9 * (q1 - q0) && q_split < q1 - 1e-9 * (q1 - q0);
1521 if debug {
1522 eprintln!(
1523 "closed-edge seam trim: face {face_id} edge {} coedge {} ({}): {} at q {q_split:.9} of [{q0:.6}, {q1:.6}], {} control points",
1524 coedge.edge_id, coedge.id, if constant_u { "meridian, constant u" } else { "along u" },
1525 if interior { "SPLIT" } else { "EXTEND" }, controls.len()
1526 );
1527 }
1528 if interior {
1529 let (low, high) = coedge.pcurve.split(q_split).or_refuse(KernelStage::Refine, "split")?;
1530 coedge.pcurve = if target == 0 { high } else { low };
1531 } else {
1532 if coedge.pcurve.degree != 1 || controls.len() != 2 {
1537 return Err(KernelRefusal::unsupported(KernelStage::Sew, "neighbour_seam_extend_curved", format!(
1538 "blend: neighbour seam edge {} would have to extend past its own end to meet the blend's support crossing, and its pcurve is not a single straight span (degree {}, {} control points)",
1539 coedge.edge_id, coedge.pcurve.degree, controls.len()
1540 )));
1541 }
1542 let w = coedge.pcurve.control_points[target].w;
1543 coedge.pcurve.control_points[target] = crate::Vec4::from_point(
1544 Vec3::new(target_uv[0], target_uv[1], 0.0), w,
1545 );
1546 }
1547 repaired = true;
1548 }
1549 }
1550 Ok(repaired)
1551 };
1552 let uv1_new = rows.cr_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1553 let v1_new = uv1_new.y;
1554 let cr_pieces = vec![(cr_edge_id, [u_start, u_end], rows.cr_pcurve.clone())];
1555 let mut repaired = replace_in_face(&mut result, first, &cr_pieces, [uv1_new.x, v1_new])?;
1556 let uv2_new = rows.cs_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1557 let v2_new = if second_split.is_some() {
1558 second_crossing_v
1559 } else {
1560 uv2_new.y
1561 };
1562 repaired |= replace_in_face(&mut result, second, &cs_pieces, [uv2_new.x, v2_new])?;
1563
1564 let collapse_winding_sphere_cap =
1570 |result: &mut BrepSolid, face_id: u64, support_ids: &[u64]| -> Result<(), KernelRefusal> {
1571 let face = result
1572 .shells
1573 .iter_mut()
1574 .flat_map(|shell| &mut shell.faces)
1575 .find(|face| face.id == face_id)
1576 .ok_or(KernelRefusal::internal(KernelStage::Sew, "sphere_carrier", "blend: sphere carrier lost during cap surgery"))?;
1577 if !matches!(
1578 face.surface.analytic(),
1579 Some(crate::AnalyticSurface::Sphere { .. })
1580 ) || face.surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")? != (true, false)
1581 {
1582 return Ok(());
1583 }
1584 let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1585 let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1586 let period = u1 - u0;
1587 let support_loop = face.loops.iter().position(|loop_record| {
1588 !loop_record.coedges.is_empty()
1589 && loop_record
1590 .coedges
1591 .iter()
1592 .all(|coedge| support_ids.contains(&coedge.edge_id))
1593 });
1594 let Some(support_loop) = support_loop else {
1595 return Ok(());
1596 };
1597 if face.loops[support_loop].coedges.len() != 1 {
1598 return Ok(());
1599 }
1600 let support = &face.loops[support_loop].coedges[0];
1601 let [s0, s1] = support.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1602 let support_start = support.pcurve.evaluate(s0).or_refuse(KernelStage::Refine, "evaluate")?;
1603 let support_end = support.pcurve.evaluate(s1).or_refuse(KernelStage::Refine, "evaluate")?;
1604 let support_winding = support_end.x - support_start.x;
1605 if (support_winding.abs() - period).abs() > 0.05 * period {
1606 return Ok(());
1607 }
1608
1609 let mut pole: Option<(usize, CoedgeRecord)> = None;
1610 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1611 if loop_index == support_loop {
1612 continue;
1613 }
1614 for coedge in &loop_record.coedges {
1615 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1616 let start = coedge.pcurve.evaluate(p0).or_refuse(KernelStage::Refine, "evaluate")?;
1617 let end = coedge.pcurve.evaluate(p1).or_refuse(KernelStage::Refine, "evaluate")?;
1618 let winding = end.x - start.x;
1619 let at_pole = (start.y - v0).abs() <= 1e-8 || (start.y - v1).abs() <= 1e-8;
1620 if at_pole
1621 && (end.y - start.y).abs() <= 1e-8
1622 && (winding.abs() - period).abs() <= 0.05 * period
1623 && winding * support_winding < 0.0
1624 {
1625 pole = Some((loop_index, coedge.clone()));
1626 break;
1627 }
1628 }
1629 if pole.is_some() {
1630 break;
1631 }
1632 }
1633 let Some((pole_loop, pole_coedge)) = pole else {
1634 return Ok(());
1635 };
1636 let support_record = face.loops[support_loop].clone();
1637 let pole_id = face.loops[pole_loop].id;
1638 face.loops = vec![
1639 LoopRecord {
1640 id: pole_id,
1641 coedges: vec![pole_coedge],
1642 },
1643 support_record,
1644 ];
1645 Ok(())
1646 };
1647 collapse_winding_sphere_cap(&mut result, first.face.id, &[cr_edge_id])?;
1648 let cs_edge_ids: Vec<u64> = cs_pieces.iter().map(|(edge_id, ..)| *edge_id).collect();
1649 collapse_winding_sphere_cap(&mut result, second.face.id, &cs_edge_ids)?;
1650 for shell in &mut result.shells {
1652 for face in &mut shell.faces {
1653 for loop_record in &mut face.loops {
1654 for coedge in &mut loop_record.coedges {
1655 if coedge.id == 0 {
1656 coedge.id = take_id();
1657 }
1658 }
1659 }
1660 }
1661 }
1662
1663 let new_edge_ids: Vec<u64> = std::iter::once(cr_edge_id)
1668 .chain(cs_pieces.iter().map(|(edge_id, ..)| *edge_id))
1669 .chain(std::iter::once(blend_seam_id))
1670 .collect();
1671 let first_face_edges: Vec<u64> = first
1672 .face
1673 .loops
1674 .iter()
1675 .flat_map(|loop_record| loop_record.coedges.iter().map(|coedge| coedge.edge_id))
1676 .collect();
1677 for seam_edge in result.edges.iter_mut() {
1678 if new_edge_ids.contains(&seam_edge.id) {
1679 continue;
1680 }
1681 if seam_edge.start_vertex_id != old_vertex && seam_edge.end_vertex_id != old_vertex {
1682 continue;
1683 }
1684 let (target_vertex, target_v, target_point) = if first_face_edges.contains(&seam_edge.id) {
1685 (vertex1_id, v1_new, cr_start)
1686 } else {
1687 (second_seam_vertex, v2_new, second_seam_point)
1688 };
1689 let [c0, c1] = seam_edge.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1690 let clamped = target_v.clamp(c0.min(c1), c0.max(c1));
1691 let band = 1e-6 * (1.0 + target_point.length());
1700 let split_parameter = if seam_edge.id == edge.id {
1701 clamped
1707 } else {
1708 match seam_edge.curve.evaluate(clamped) {
1709 Ok(point) if point.sub(target_point).length() <= band => clamped,
1710 _ => {
1711 repaired = true;
1712 let projection =
1713 crate::project_point_to_curve(&seam_edge.curve, target_point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
1714 if projection.distance > band {
1715 return Err(KernelRefusal::unsupported(KernelStage::Sew, "neighbour_off_support", format!(
1716 "blend: neighbour edge {} does not pass through the blend's support \
1717 start (off by {:.9}); the trim has no parameter to move to",
1718 seam_edge.id, projection.distance
1719 )));
1720 }
1721 projection.u.clamp(c0.min(c1), c0.max(c1))
1722 }
1723 }
1724 };
1725 if seam_edge.start_vertex_id == old_vertex {
1726 seam_edge.t0 = split_parameter;
1727 seam_edge.start_vertex_id = target_vertex;
1728 }
1729 if seam_edge.end_vertex_id == old_vertex {
1730 seam_edge.t1 = split_parameter;
1731 seam_edge.end_vertex_id = target_vertex;
1732 }
1733 }
1734
1735 let mid_u = (u_start + u_end) * 0.5;
1737 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
1738 let station_uv = rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?;
1739 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
1740 let out1 = if first.face.same_sense {
1741 n1
1742 } else {
1743 n1.scale(-1.0)
1744 };
1745 let same_sense = blend_normal.dot(out1) >= 0.0;
1746 let loop_id = take_id();
1747 let mut coedges = vec![
1748 CoedgeRecord {
1749 id: take_id(),
1750 edge_id: cr_edge_id,
1751 forward: true,
1752 pcurve: crate::sweep_topology::parameter_line(u_start, 0.0, u_end, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1753 },
1754 CoedgeRecord {
1755 id: take_id(),
1756 edge_id: blend_seam_id,
1757 forward: true,
1758 pcurve: crate::sweep_topology::parameter_line(u_end, 0.0, u_end, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1759 },
1760 ];
1761 for (edge_id, window, _) in cs_pieces.iter().rev() {
1762 coedges.push(CoedgeRecord {
1763 id: take_id(),
1764 edge_id: *edge_id,
1765 forward: false,
1766 pcurve: crate::sweep_topology::parameter_line(window[1], 1.0, window[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1767 });
1768 }
1769 coedges.push(CoedgeRecord {
1770 id: take_id(),
1771 edge_id: blend_seam_id,
1772 forward: false,
1773 pcurve: crate::sweep_topology::parameter_line(u_start, 1.0, u_start, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1774 });
1775 if !same_sense {
1785 coedges.reverse();
1786 for coedge in &mut coedges {
1787 coedge.forward = !coedge.forward;
1788 coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
1789 }
1790 }
1791 let mut loops = vec![LoopRecord {
1792 id: loop_id,
1793 coedges,
1794 }];
1795 if let Some(crease) = crease {
1804 let start_vertex = take_id();
1805 let end_vertex = take_id();
1806 let crease_edge = take_id();
1807 result.vertices.push(VertexRecord { id: start_vertex, point: crease.start });
1808 result.vertices.push(VertexRecord { id: end_vertex, point: crease.end });
1809 let [crease_t0, crease_t1] = crease.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1810 result.edges.push(EdgeRecord {
1811 id: crease_edge,
1812 curve: crease.curve,
1813 t0: crease_t0,
1814 t1: crease_t1,
1815 start_vertex_id: start_vertex,
1816 end_vertex_id: end_vertex,
1817 degenerate: false,
1818 name: name.map(|value| format!("{value}:CREASE")),
1819 });
1820 let outer = crate::blend::chain::signed_area(&loops[0].coedges)?;
1821 let forward_first = crate::blend::chain::signed_area(&[
1822 CoedgeRecord { id: 0, edge_id: crease_edge, forward: true, pcurve: crease.first.clone() },
1823 CoedgeRecord { id: 0, edge_id: crease_edge, forward: false, pcurve: crease.second.reversed().or_refuse(KernelStage::Refine, "reversed")? },
1824 ])?;
1825 let inner = if outer * forward_first < 0.0 {
1826 vec![
1827 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: true, pcurve: crease.first },
1828 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: false, pcurve: crease.second.reversed().or_refuse(KernelStage::Refine, "reversed")? },
1829 ]
1830 } else {
1831 vec![
1832 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: true, pcurve: crease.second },
1833 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: false, pcurve: crease.first.reversed().or_refuse(KernelStage::Refine, "reversed")? },
1834 ]
1835 };
1836 loops.push(LoopRecord { id: take_id(), coedges: inner });
1837 }
1838 let blend_face = FaceRecord {
1839 id: take_id(),
1840 surface: rows.surface,
1841 same_sense,
1842 loops,
1843 name: name.map(|value| value.to_string()),
1844 };
1845 let shell_index = result
1846 .shells
1847 .iter()
1848 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
1849 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
1850 result.shells[shell_index].faces.push(blend_face);
1851
1852 let used_edges: std::collections::HashSet<u64> = result
1853 .shells
1854 .iter()
1855 .flat_map(|shell| &shell.faces)
1856 .flat_map(|face| &face.loops)
1857 .flat_map(|loop_record| &loop_record.coedges)
1858 .map(|coedge| coedge.edge_id)
1859 .collect();
1860 result
1861 .edges
1862 .retain(|candidate| used_edges.contains(&candidate.id));
1863 let used_vertices: std::collections::HashSet<u64> = result
1864 .edges
1865 .iter()
1866 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
1867 .collect();
1868 result
1869 .vertices
1870 .retain(|candidate| used_vertices.contains(&candidate.id));
1871 if repaired {
1878 let problems = result.validate();
1879 if !problems.is_empty() {
1880 return Err(KernelRefusal::internal(KernelStage::Validate, "neighbour_trim_closure", format!(
1881 "blend: the repaired neighbour trim did not close ({} issue(s), first: {})",
1882 problems.len(),
1883 problems
1884 .first()
1885 .map(|issue| issue.message.as_str())
1886 .unwrap_or("unknown")
1887 )));
1888 }
1889 }
1890 Ok(result)
1891}