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 let _closed_timing = crate::blend::BlendTimer::start("timing.closed_rim_ms");
116 let prepare_timing = crate::blend::BlendTimer::start("timing.closed.prepare_ms");
117 if let Some(separated) = separate_spherical_domain_cut(solid, edge_id)? {
118 return blend_closed_edge_impl(&separated, edge_id, radius_at, constant, chamfer, name);
119 }
120 if let Some(simplified) = remove_retraced_cuts(solid, edge_id)? {
121 return blend_closed_edge_impl(&simplified, edge_id, radius_at, constant, chamfer, name);
122 }
123 let edge = solid
124 .edges
125 .iter()
126 .find(|edge| edge.id == edge_id)
127 .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
128 if edge.start_vertex_id != edge.end_vertex_id {
129 return Err(KernelRefusal::input(KernelStage::Collect, "closed_edge", "blend: general path currently requires a CLOSED edge"));
130 }
131 let (face_a, loop_a, coedge_a) = locate_mate(solid, edge_id, None)?;
132 let (face_b, loop_b, coedge_b) = locate_mate(solid, edge_id, Some((face_a.id, loop_a)))?;
133 if face_a.id == face_b.id && loop_a == loop_b {
134 return Err(KernelRefusal::unsupported(KernelStage::Classify, "seam_edge", "blend: edge is used twice by one loop (seam edge?)"));
135 }
136
137 let seam_a = face_a.loops[loop_a].coedges.len() > 1;
140 let seam_b = face_b.loops[loop_b].coedges.len() > 1;
141 let (first_face, first_loop, first_coedge, second_face, second_loop, second_coedge) =
142 if seam_a || !seam_b {
143 (face_a, loop_a, coedge_a, face_b, loop_b, coedge_b)
144 } else {
145 (face_b, loop_b, coedge_b, face_a, loop_a, coedge_a)
146 };
147 let anchored = first_face.loops[first_loop].coedges.len() > 1;
148 let second_seam = anchored && second_face.loops[second_loop].coedges.len() > 1;
149 let anchor_u = if anchored {
150 Some(first_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?[0])
152 } else {
153 None
154 };
155
156 let mid_radius = radius_at(edge.t0 + (edge.t1 - edge.t0) * 0.5);
158 let (rho1, rho2) = signed_radii(
159 edge,
160 first_face,
161 first_coedge,
162 second_face,
163 second_coedge,
164 mid_radius,
165 )?;
166
167 let first_mate = BlendMate {
168 face: first_face,
169 coedge: first_coedge,
170 loop_index: first_loop,
171 rho: rho1,
172 };
173 let second_mate = BlendMate {
174 face: second_face,
175 coedge: second_coedge,
176 loop_index: second_loop,
177 rho: rho2,
178 };
179 let mid_t = edge.t0 + (edge.t1 - edge.t0) * 0.5;
180 use crate::blend::chain::FoldPolicy;
195 let march_with = |fold: FoldPolicy, depth_budget: usize| -> Result<Vec<Station>, KernelRefusal> {
196 match march_stations(edge, &first_mate, &second_mate, radius_at, anchor_u, fold, depth_budget) {
197 Ok(stations) => Ok(stations),
198 Err(anchored_error) if anchor_u.is_some() => {
199 march_stations(edge, &first_mate, &second_mate, radius_at, None, fold, depth_budget).map_err(
200 |retry_error| {
201 if crate::blend::is_wall_fold(&retry_error)
202 || crate::blend::is_marched_fit_off_carriers(&retry_error) {
203 retry_error
204 } else {
205 anchored_error
206 }
207 },
208 )
209 }
210 Err(anchored_error) => Err(anchored_error),
211 }
212 };
213 let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
214 let wall_check = {
218 let radius = constant.unwrap_or(f64::NAN);
221 let constant = constant.is_some();
222 let model = crate::KernelTolerances::for_solid(solid, 1e-7).model;
225 let bar = crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
226 (!chamfer && constant && radius.is_finite() && radius != 0.0).then(|| crate::blend::stations::ClosedWallCheck {
227 radius: radius.abs(),
228 bar,
229 model: model.is_finite().then_some(model),
230 })
231 };
232 crate::blend::stations::CLOSED_MODEL_UNMET.with(|slot| slot.set(None));
233 crate::blend::stations::CLOSED_REQUEST_RAILS.with(|slot| slot.set(None));
234 prepare_timing.stop();
235 let exact_revolution = crate::blend::exact_rim_lane_on()
236 && crate::blend::stations::closed_revolution_axis(&first_mate, &second_mate).is_some();
237 let depth_budget = if exact_revolution { 0 } else { MAX_REFINEMENT_DEPTH };
238 if exact_revolution {
239 crate::blend::record_count("count.closed.exact_lane", 1);
240 }
241 let march_timing = crate::blend::BlendTimer::start("timing.closed.march_ms");
242 let marched = match march_stations_with_rail_bar(edge, &first_mate, &second_mate, radius_at, anchor_u, FoldPolicy::Refuse, depth_budget, rail_bar, wall_check) {
243 Ok(stations) => Ok(stations),
244 Err(failure) => {
245 let anchored_error = failure.into_initial()?;
248 if anchor_u.is_some() {
249 if std::env::var("BREP_DEBUG_CLOSED_RAILS").is_ok() {
250 eprintln!("closed initial anchor fallback: edge {}: {anchored_error}", edge.id);
251 }
252 match march_stations_with_rail_bar(edge, &first_mate, &second_mate, radius_at, None, FoldPolicy::Refuse, depth_budget, rail_bar, wall_check) {
253 Ok(stations) => Ok(stations),
254 Err(failure) => {
255 let retry_error = failure.into_initial()?;
256 if crate::blend::is_wall_fold(&retry_error) { Err(retry_error) }
257 else { Err(anchored_error) }
258 }
259 }
260 } else { Err(anchored_error) }
261 }
262 };
263 let model_unmet = crate::blend::stations::CLOSED_MODEL_UNMET.with(|slot| slot.take());
276 let request_rails = crate::blend::stations::CLOSED_REQUEST_RAILS.with(|slot| slot.take());
277 let mut carve_fold = false;
278 let marched = match marched {
279 Err(error)
280 if crate::blend::is_wall_fold(&error)
281 && !crate::blend::fold::is_rail_fold(&error)
282 && !chamfer =>
283 {
284 crate::blend::carve::carve_trace(format_args!(
285 "blend closed-edge: the wall folds ({error}); re-marching under Carve"
286 ));
287 let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
296 let mut climbed: Option<Vec<Station>> = None;
297 let mut last_off = f64::NAN;
298 for depth_budget in MAX_REFINEMENT_DEPTH..=CARVE_MAX_REFINEMENT_DEPTH {
299 let stations = match march_with(FoldPolicy::Carve, depth_budget) {
300 Ok(stations) => stations,
301 Err(_) => break,
303 };
304 let parameters = station_parameters(&stations);
305 let off = match fit_closed_rows(&stations, ¶meters, chamfer) {
306 Ok(rows) => rails_off_closed_carriers(&rows, &stations, ¶meters, &first_mate, &second_mate)?,
307 Err(_) => break,
308 };
309 last_off = off;
310 crate::blend::carve::carve_trace(format_args!(
311 "blend closed-edge carve ladder: depth budget {depth_budget} ({} stations) leaves the rails {off:.3e} off their carriers (bar {rail_bar:.3e})",
312 stations.len()
313 ));
314 if off <= rail_bar {
315 climbed = Some(stations);
316 break;
317 }
318 }
319 match climbed {
320 Some(stations) => {
321 carve_fold = true;
322 Ok(stations)
323 }
324 None => Err(crate::blend::fold::wall_fold_refusal(
325 Vec::new(),
326 format!(
327 "{} {} 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})",
328 crate::blend::WALL_FOLDS,
329 radius_at(mid_t).abs()
330 ),
331 )),
332 }
333 }
334 other => other,
335 };
336 let stations = match marched {
337 Ok(stations) => stations,
338 Err(march_error) if crate::blend::is_wall_fold(&march_error)
344 || crate::blend::is_marched_fit_off_carriers(&march_error) => return Err(march_error),
345 Err(march_error) if crate::blend::is_ball_off_carrier(&march_error) => {
346 return Err(march_error);
353 }
354 Err(march_error) => {
355 return blend_closed_edge_keep(
358 solid,
359 edge,
360 &first_mate,
361 &second_mate,
362 radius_at(mid_t),
363 rho1,
364 chamfer,
365 name,
366 )
367 .or_else(|_| {
368 blend_closed_edge_keep(
369 solid,
370 edge,
371 &second_mate,
372 &first_mate,
373 radius_at(mid_t),
374 rho2,
375 chamfer,
376 name,
377 )
378 })
379 .map_err(|keep_error| {
380 format!("{march_error}; edge-preserving blend also failed: {keep_error}")
381 }).or_refuse(KernelStage::Refine, "map_err");
382 }
383 };
384 let support_exits = |face: &FaceRecord, second_side: bool| -> bool {
388 let stride = (stations.len() / 16).max(1);
392 stations.iter().step_by(stride).any(|station| {
393 let mut uv = if second_side {
394 station.uv2
395 } else {
396 station.uv1
397 };
398 if let Ok((closed_u, closed_v)) = face.surface.closed_directions() {
399 if closed_u {
400 if let Ok([low, high]) = face.surface.domain_u() {
401 uv[0] = low + (uv[0] - low).rem_euclid(high - low);
402 }
403 }
404 if closed_v {
405 if let Ok([low, high]) = face.surface.domain_v() {
406 uv[1] = low + (uv[1] - low).rem_euclid(high - low);
407 }
408 }
409 }
410 crate::parameter_point_in_face(face, crate::Vec2 { x: uv[0], y: uv[1] }, 1e-6)
411 .map(|class| class == crate::PolygonClass::Outside)
412 .unwrap_or(true)
413 })
414 };
415 let exits = [support_exits(first_face, false), support_exits(second_face, true)];
423 if exits[0] || exits[1] {
424 let scale = crate::blend::stations::march_model_scale(&edge.curve, edge.t0, edge.t1, radius_at(mid_t))?;
425 match crate::blend::rollover::plan_closed(solid, edge, &stations, [&first_mate, &second_mate], radius_at, scale) {
426 Ok(Some(plan)) => {
427 if crate::blend::rollover_on() {
430 let pieces = crate::blend::rollover::march_pieces(
431 solid, edge, &plan, &stations, [&first_mate, &second_mate], radius_at, scale, rail_bar, chamfer, anchor_u,
432 )?;
433 let built = crate::blend::rollover::build(solid, edge, [&first_mate, &second_mate], &plan, pieces, anchor_u, name)?;
434 march_timing.stop();
435 return Ok(built);
436 }
437 return Err(crate::blend::rollover::refusal(&plan, radius_at(mid_t).abs()));
438 }
439 Ok(None) => {}
440 Err(error) => {
441 if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
442 eprintln!("closed-rim rollover scan did not read the exit: {error}");
443 }
444 }
445 }
446 }
447 if exits[1] {
448 return blend_closed_edge_keep(
449 solid,
450 edge,
451 &first_mate,
452 &second_mate,
453 radius_at(mid_t),
454 rho1,
455 chamfer,
456 name,
457 );
458 }
459 if exits[0] {
460 return blend_closed_edge_keep(
461 solid,
462 edge,
463 &second_mate,
464 &first_mate,
465 radius_at(mid_t),
466 rho2,
467 chamfer,
468 name,
469 );
470 }
471 if std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok() {
472 let last = stations.len() - 1;
473 for index in [
474 0usize,
475 1.min(last),
476 2.min(last),
477 last.saturating_sub(1),
478 last,
479 ] {
480 let station = &stations[index];
481 eprintln!(
482 "station {index}: uv1=({:.6},{:.6}) uv2=({:.6},{:.6}) p1=({:.6},{:.6},{:.6}) w={:.6}",
483 station.uv1[0], station.uv1[1], station.uv2[0], station.uv2[1],
484 station.p1.x, station.p1.y, station.p1.z, station.weight,
485 );
486 }
487 }
488 march_timing.stop();
489 crate::blend::record_count("count.closed.stations", stations.len() as u64);
490 let rows_timing = crate::blend::BlendTimer::start("timing.closed.rows_ms");
491 let parameters = station_parameters(&stations);
492 let exact_rows = exact_closed_revolution_rows(&stations, &first_mate, &second_mate, chamfer);
493 crate::blend::record_count(
494 if exact_rows.is_some() { "count.rows.exact_revolution" } else { "count.rows.fitted_closed" },
495 1,
496 );
497 let rows = match exact_rows {
498 Some(rows) => rows?,
499 None => {
500 let mut rows = fit_closed_rows(&stations, ¶meters, chamfer)?;
510 rows.cr_pcurve = refit_closed_support_pcurve(&rows.cr, &rows.cr_pcurve, &first_mate.face.surface)?;
511 rows.cs_pcurve = refit_closed_support_pcurve(&rows.cs, &rows.cs_pcurve, &second_mate.face.surface)?;
512 rows
513 }
514 };
515 rows_timing.stop();
516 let carve_timing = crate::blend::BlendTimer::start("timing.closed.carve_ms");
517 let crease = if carve_fold {
518 Some(carve_closed_wall(solid, &rows, &stations, ¶meters, &first_mate, &second_mate, radius_at(mid_t).abs())?)
519 } else {
520 None
521 };
522 carve_timing.stop();
523 let surgery_timing = crate::blend::BlendTimer::start("timing.closed.surgery_ms");
524
525 let second_split = if second_seam {
529 let seam2 = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?[0];
530 let period2 = {
531 let [d0, d1] = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
532 d1 - d0
533 };
534 let u2_first = stations[0].uv2[0];
536 let u2_last = stations[stations.len() - 1].uv2[0];
537 let direction = (u2_last - u2_first).signum();
538 let mut k = ((u2_first - seam2) / period2).ceil();
539 if direction < 0.0 {
540 k = ((u2_first - seam2) / period2).floor();
541 }
542 let target = seam2 + k * period2;
543 let inside =
544 (target - u2_first) * direction > 1e-6 && (u2_last - target) * direction > 1e-6;
545 let aligned = (target - u2_first).abs() <= 1e-6
550 || (target - u2_last).abs() <= 1e-6
551 || ((u2_first - seam2) / period2).fract().abs() <= 1e-9;
552 if !inside && aligned {
553 None
554 } else if !inside {
555 return Err(KernelRefusal::internal(KernelStage::Refine, "seam_crossing_bracket", "blend: second seam crossing not bracketed by the march"));
556 } else {
557 let [fit_low, fit_high] = rows.u_domain;
561 let mut p = fit_low
562 + (fit_high - fit_low) * {
563 let mut seed = 0.5;
565 for pair in 0..stations.len() - 1 {
566 let a = stations[pair].uv2[0];
567 let b = stations[pair + 1].uv2[0];
568 if (target - a) * (target - b) <= 0.0 {
569 let local = (target - a) / (b - a);
570 seed = (parameters[pair]
571 + (parameters[pair + 1] - parameters[pair]) * local)
572 .clamp(0.0, 1.0);
573 break;
574 }
575 }
576 seed
577 };
578 for _ in 0..NEWTON_ITERATIONS {
579 let value = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
580 if value.abs() <= 1e-12 {
581 break;
582 }
583 let step = 1e-8;
584 let probed = rows.cs_pcurve.evaluate(p + step).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
585 let derivative = (probed - value) / step;
586 if derivative.abs() <= 1e-14 {
587 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "seam_crossing_newton", "blend: second seam crossing Newton stalled"));
588 }
589 p -= value / derivative;
590 }
591 let crossing_v = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.y;
592 Some(SecondSeamSplit {
593 fit_parameter: p,
594 crossing_v,
595 period: period2 * direction,
596 })
597 }
598 } else {
599 None
600 };
601
602 let model_unmet = if carve_fold { None } else { model_unmet };
606 let wall_surface = model_unmet.map(|_| rows.surface.clone());
607 let built = build_surgery(
608 solid,
609 edge,
610 &first_mate,
611 &second_mate,
612 rows,
613 second_split,
614 name,
615 crease,
616 )?;
617 surgery_timing.stop();
618 if let (Some((declared, reason, round, stations, ceiling)), Some(surface)) = (model_unmet, wall_surface) {
619 crate::blend::record_wall_model_report(crate::blend::WallModelReport {
620 name: name.map(str::to_string),
621 surface,
622 request: crate::KernelTolerances::for_solid(solid, 1e-7).model,
623 residual: declared,
624 detail: request_rails.map(|(rails, unread, wall)| {
627 format!("last request read: rails {rails:.3e} off their carriers at j/16 ({unread} interval(s) unread), wall {wall:.3e}")
628 }),
629 budget: crate::ApproximationBudget {
630 reason,
631 rounds_used: round,
632 rounds_limit: crate::blend::stations::REFINE_ROUNDS,
633 stations,
634 station_limit: ceiling,
635 mechanism: crate::BudgetMechanism::LocalRounds,
636 measured_component: match request_rails {
639 Some((rails, _, wall)) if rails > wall => crate::MeasuredComponent::Rails,
640 _ => crate::MeasuredComponent::Wall,
641 },
642 unread: request_rails.map_or(0, |(_, unread, _)| unread),
643 },
644 });
645 }
646 Ok(built)
647}
648
649fn separate_spherical_domain_cut(
655 solid: &BrepSolid,
656 edge_id: u64,
657) -> Result<Option<BrepSolid>, KernelRefusal> {
658 let Some(edge) = solid.edges.iter().find(|edge| edge.id == edge_id) else {
659 return Ok(None);
660 };
661 if edge.start_vertex_id != edge.end_vertex_id {
662 return Ok(None);
663 }
664 let mut result = solid.clone();
665 let mut take_id = crate::blend::edge::fresh_id_source(solid);
666 let mut separated = false;
667 for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
668 let Some(atlas) = crate::sphere_chart::SphereAtlas::of_surface(&face.surface) else {
669 continue;
670 };
671 let [u0, u1] = face
672 .surface
673 .domain_u()
674 .or_refuse(KernelStage::Collect, "domain_cut_u")?;
675 let [v0, v1] = face
676 .surface
677 .domain_v()
678 .or_refuse(KernelStage::Collect, "domain_cut_v")?;
679 if [u0, u1, v0, v1] != [0.0, 1.0, 0.0, 1.0] {
680 continue;
681 }
682 if face
683 .surface
684 .closed_directions()
685 .or_refuse(KernelStage::Collect, "domain_cut_closed")?
686 != (true, false)
687 {
688 continue;
689 }
690 let band = 1e-8 * atlas.radius;
691 let bottom = face
692 .surface
693 .evaluate(u0, v0)
694 .or_refuse(KernelStage::Collect, "domain_cut_bottom")?;
695 let top = face
696 .surface
697 .evaluate(u0, v1)
698 .or_refuse(KernelStage::Collect, "domain_cut_top")?;
699 if bottom.add(top).scale(0.5).sub(atlas.centre).length() > band
700 || (bottom.sub(top).length() - 2.0 * atlas.radius).abs() > band
701 {
702 continue;
703 }
704 let mut additions = Vec::new();
705 let mut cut_ids = std::collections::HashSet::new();
706 for rim in &mut face.loops {
707 if rim.coedges.len() < 5 {
708 continue;
709 }
710 let positions: Vec<_> = rim
711 .coedges
712 .iter()
713 .enumerate()
714 .filter_map(|(i, c)| (c.edge_id == edge_id).then_some(i))
715 .collect();
716 if positions.len() != 1 {
717 continue;
718 }
719 let position = positions[0];
720 let ring = &rim.coedges[position];
721 let [a, b] = ring
722 .pcurve
723 .domain()
724 .or_refuse(KernelStage::Collect, "domain_cut_ring")?;
725 if ring
726 .pcurve
727 .evaluate(a)
728 .or_refuse(KernelStage::Collect, "domain_cut_ring_start")?
729 .sub(
730 ring.pcurve
731 .evaluate(b)
732 .or_refuse(KernelStage::Collect, "domain_cut_ring_end")?,
733 )
734 .length()
735 > 1e-7
736 {
737 continue;
738 }
739 let rest: Vec<_> = rim
740 .coedges
741 .iter()
742 .filter(|c| c.edge_id != edge_id)
743 .collect();
744 let mut points = Vec::new();
745 let mut domain_cut = true;
746 for coedge in &rest {
747 let [a, b] = coedge
748 .pcurve
749 .domain()
750 .or_refuse(KernelStage::Collect, "domain_cut_trim")?;
751 let start = coedge
752 .pcurve
753 .evaluate(a)
754 .or_refuse(KernelStage::Collect, "domain_cut_start")?;
755 let end = coedge
756 .pcurve
757 .evaluate(b)
758 .or_refuse(KernelStage::Collect, "domain_cut_end")?;
759 points.push(start);
760 let degenerate = solid
761 .edges
762 .iter()
763 .any(|e| e.id == coedge.edge_id && e.degenerate);
764 let at = |x: f64, low: f64, high: f64| {
765 (x - low).abs() <= 1e-8 || (x - high).abs() <= 1e-8
766 };
767 let on_boundary = if degenerate {
768 (start.y - end.y).abs() <= 1e-8
769 && at(start.y, v0, v1)
770 && coedge
771 .pcurve
772 .control_points
773 .iter()
774 .all(|p| (p.y / p.w - start.y).abs() <= 1e-8)
775 } else {
776 at(start.x, u0, u1)
777 && (start.x - end.x).abs() <= 1e-8
778 && coedge
779 .pcurve
780 .control_points
781 .iter()
782 .all(|p| (p.x / p.w - start.x).abs() <= 1e-8)
783 && rest.iter().filter(|c| c.edge_id == coedge.edge_id).count() == 2
784 && rest
785 .iter()
786 .any(|c| c.edge_id == coedge.edge_id && c.forward != coedge.forward)
787 };
788 domain_cut &= on_boundary;
789 }
790 let twice_area: f64 = (0..points.len())
791 .map(|i| {
792 let a = points[i];
793 let b = points[(i + 1) % points.len()];
794 a.x * b.y - a.y * b.x
795 })
796 .sum();
797 let expected = (u1 - u0) * (v1 - v0) * if face.same_sense { 1.0 } else { -1.0 };
798 if !domain_cut || (0.5 * twice_area - expected).abs() > 1e-7 * expected.abs() {
799 continue;
800 }
801 let cuts: std::collections::HashSet<_> = rest
805 .iter()
806 .filter_map(|c| {
807 solid
808 .edges
809 .iter()
810 .find(|e| e.id == c.edge_id && !e.degenerate)
811 .map(|e| e.id)
812 })
813 .collect();
814 for cut in solid.edges.iter().filter(|e| cuts.contains(&e.id)) {
815 let users = solid
816 .shells
817 .iter()
818 .flat_map(|s| &s.faces)
819 .flat_map(|f| &f.loops)
820 .flat_map(|l| &l.coedges)
821 .filter(|c| c.edge_id == cut.id)
822 .count();
823 let exclusive_vertices = solid
824 .edges
825 .iter()
826 .filter(|e| {
827 e.start_vertex_id == cut.start_vertex_id
828 || e.end_vertex_id == cut.start_vertex_id
829 || e.start_vertex_id == cut.end_vertex_id
830 || e.end_vertex_id == cut.end_vertex_id
831 })
832 .all(|e| e.degenerate || cuts.contains(&e.id) || e.id == edge_id);
833 let mut same_parameter = cut.t0 >= v0 && cut.t1 <= v1 && cut.t0 < cut.t1;
834 for i in 0..=16 {
835 let t = cut.t0 + (cut.t1 - cut.t0) * i as f64 / 16.0;
836 let point = cut
837 .curve
838 .evaluate(t)
839 .or_refuse(KernelStage::Collect, "domain_cut_curve")?;
840 let carrier = face
841 .surface
842 .evaluate(u0, t)
843 .or_refuse(KernelStage::Collect, "domain_cut_carrier")?;
844 same_parameter &= point.sub(carrier).length() <= band;
845 }
846 domain_cut &= users == 2 && exclusive_vertices && same_parameter;
847 }
848 if !domain_cut {
849 continue;
850 }
851 cut_ids.extend(cuts);
852 additions.push(LoopRecord {
853 id: take_id(),
854 coedges: vec![rim.coedges.remove(position)],
855 });
856 separated = true;
857 }
858 if !additions.is_empty() {
859 let real = |id: u64| {
863 !cut_ids.contains(&id) && !solid.edges.iter().any(|e| e.id == id && e.degenerate)
864 };
865 let curves: Vec<_> = face
866 .loops
867 .iter()
868 .flat_map(|l| &l.coedges)
869 .chain(additions.iter().flat_map(|l| &l.coedges))
870 .filter(|c| real(c.edge_id) || c.edge_id == edge_id)
871 .map(|c| &c.pcurve)
872 .collect();
873 let mut candidates = Vec::new();
874 for shift in [0.25, 0.5, 0.75] {
875 let mut clearance = f64::INFINITY;
876 let mut fits = true;
877 for curve in &curves {
878 let first = curve.control_points[0].x / curve.control_points[0].w;
879 let offset = -(first - shift).floor();
880 for p in &curve.control_points {
881 let u = p.x / p.w - shift + offset;
882 fits &= u >= 0.0 && u <= 1.0;
883 clearance = clearance.min(u.min(1.0 - u));
884 }
885 }
886 if fits {
887 candidates.push((clearance, shift));
888 }
889 }
890 let Some((_, shift)) = candidates.into_iter().max_by(|a, b| a.0.total_cmp(&b.0)) else {
891 return Err(KernelRefusal::unsupported(
892 KernelStage::Collect,
893 "sphere_domain_cut",
894 "blend: no quarter-turn sphere chart keeps the real trims in-domain",
895 ));
896 };
897 let seam_direction = face
898 .surface
899 .evaluate(shift, 0.5)
900 .or_refuse(KernelStage::Collect, "sphere_cut_direction")?
901 .sub(atlas.centre);
902 let surface = crate::make_sphere_surface_framed(
903 atlas.centre,
904 atlas.radius,
905 atlas.basis[2],
906 Some(seam_direction),
907 )
908 .or_refuse(KernelStage::Collect, "sphere_cut_surface")?;
909 for i in 0..=16 {
911 for v in [0.0, 0.3, 0.5, 0.7, 1.0] {
912 let u = i as f64 / 16.0;
913 let old = face
914 .surface
915 .evaluate(u, v)
916 .or_refuse(KernelStage::Collect, "sphere_cut_old")?;
917 let new = surface
918 .evaluate((u - shift).rem_euclid(1.0), v)
919 .or_refuse(KernelStage::Collect, "sphere_cut_new")?;
920 if old.sub(new).length() > band {
921 return Err(KernelRefusal::unsupported(
922 KernelStage::Collect,
923 "sphere_cut_chart",
924 "blend: the sphere quarter turn is not the same carrier",
925 ));
926 }
927 }
928 }
929 let seam = surface
930 .iso_curve_u(0.0)
931 .or_refuse(KernelStage::Collect, "sphere_cut_seam")?;
932 for coedge in face
933 .loops
934 .iter_mut()
935 .flat_map(|l| &mut l.coedges)
936 .chain(additions.iter_mut().flat_map(|l| &mut l.coedges))
937 {
938 if solid
939 .edges
940 .iter()
941 .any(|e| e.id == coedge.edge_id && e.degenerate)
942 {
943 continue;
944 }
945 if !cut_ids.contains(&coedge.edge_id) {
946 let first =
947 coedge.pcurve.control_points[0].x / coedge.pcurve.control_points[0].w;
948 let offset = -(first - shift).floor() - shift;
949 for p in &mut coedge.pcurve.control_points {
950 p.x += offset * p.w;
951 }
952 }
953 }
954 for cut in result.edges.iter_mut().filter(|e| cut_ids.contains(&e.id)) {
955 for (vertex_id, t) in [(cut.start_vertex_id, cut.t0), (cut.end_vertex_id, cut.t1)] {
956 let point = seam
957 .evaluate(t)
958 .or_refuse(KernelStage::Collect, "sphere_cut_vertex")?;
959 let vertex = result
960 .vertices
961 .iter_mut()
962 .find(|v| v.id == vertex_id)
963 .ok_or(KernelRefusal::internal(
964 KernelStage::Collect,
965 "sphere_cut_vertex",
966 "blend: sphere seam vertex is missing",
967 ))?;
968 vertex.point = point;
969 }
970 cut.curve = seam.clone();
971 }
972 face.surface = surface;
973 }
974 face.loops.extend(additions);
975 }
976 if !separated {
977 return Ok(None);
978 }
979 let old_vertex = solid
980 .vertices
981 .iter()
982 .find(|v| v.id == edge.start_vertex_id)
983 .ok_or(KernelRefusal::internal(
984 KernelStage::Collect,
985 "domain_cut_vertex",
986 "blend: closed ring vertex is missing",
987 ))?;
988 let vertex_id = take_id();
989 result.vertices.push(VertexRecord {
990 id: vertex_id,
991 point: old_vertex.point,
992 });
993 let ring = result
994 .edges
995 .iter_mut()
996 .find(|e| e.id == edge_id)
997 .ok_or(KernelRefusal::internal(
998 KernelStage::Collect,
999 "domain_cut_ring",
1000 "blend: closed ring edge is missing",
1001 ))?;
1002 ring.start_vertex_id = vertex_id;
1003 ring.end_vertex_id = vertex_id;
1004 Ok(Some(result))
1005}
1006
1007fn remove_retraced_cuts(
1013 solid: &BrepSolid,
1014 edge_id: u64,
1015) -> Result<Option<BrepSolid>, KernelRefusal> {
1016 let mut result = solid.clone();
1017 let mut removed = Vec::new();
1018 for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
1019 for rim in &mut face.loops {
1020 if !rim.coedges.iter().any(|coedge| coedge.edge_id == edge_id) {
1021 continue;
1022 }
1023 loop {
1024 let n = rim.coedges.len();
1025 let mut pair = None;
1026 for i in 0..n {
1027 let j = (i + 1) % n;
1028 let (a, b) = (&rim.coedges[i], &rim.coedges[j]);
1029 if a.edge_id == edge_id || a.edge_id != b.edge_id || a.forward == b.forward {
1030 continue;
1031 }
1032 let users = solid
1033 .shells
1034 .iter()
1035 .flat_map(|s| &s.faces)
1036 .flat_map(|f| &f.loops)
1037 .flat_map(|l| &l.coedges)
1038 .filter(|c| c.edge_id == a.edge_id)
1039 .count();
1040 if users != 2 {
1041 continue;
1042 }
1043 let [a0, a1] = a
1044 .pcurve
1045 .domain()
1046 .or_refuse(KernelStage::Collect, "cut_domain")?;
1047 let [b0, b1] = b
1048 .pcurve
1049 .domain()
1050 .or_refuse(KernelStage::Collect, "cut_domain")?;
1051 let agrees =
1054 a.pcurve.degree == b.pcurve.degree
1055 && a.pcurve.knots.len() == b.pcurve.knots.len()
1056 && a.pcurve.control_points.len() == b.pcurve.control_points.len()
1057 && a.pcurve.knots.iter().zip(b.pcurve.knots.iter().rev()).all(
1058 |(x, y)| {
1059 ((x - a0) / (a1 - a0) - (b1 - y) / (b1 - b0)).abs() <= 1e-9
1060 },
1061 )
1062 && a.pcurve
1063 .control_points
1064 .iter()
1065 .zip(b.pcurve.control_points.iter().rev())
1066 .all(|(p, q)| {
1067 p.point()
1068 .ok()
1069 .zip(q.point().ok())
1070 .is_some_and(|(p, q)| p.sub(q).length() <= 1e-9)
1071 && (p.w / a.pcurve.control_points[0].w
1072 - q.w / b.pcurve.control_points.last().unwrap().w)
1073 .abs()
1074 <= 1e-9
1075 });
1076 if agrees {
1077 pair = Some((i, j, a.edge_id));
1078 break;
1079 }
1080 }
1081 let Some((i, j, id)) = pair else {
1082 break;
1083 };
1084 rim.coedges.remove(i.max(j));
1085 rim.coedges.remove(i.min(j));
1086 removed.push(id);
1087 }
1088 }
1089 }
1090 if removed.is_empty() {
1091 return Ok(None);
1092 }
1093 result.edges.retain(|edge| !removed.contains(&edge.id));
1094 super::open::prune_orphan_vertices(&mut result);
1095 Ok(Some(result))
1096}
1097
1098struct SecondSeamSplit {
1101 fit_parameter: f64,
1102 crossing_v: f64,
1103 period: f64,
1105}
1106
1107const CARVE_MAX_REFINEMENT_DEPTH: usize = MAX_REFINEMENT_DEPTH + 4;
1119
1120fn rails_off_closed_carriers(
1124 rows: &FittedRows,
1125 stations: &[Station],
1126 parameters: &[f64],
1127 first: &BlendMate,
1128 second: &BlendMate,
1129) -> Result<f64, KernelRefusal> {
1130 Ok(closed_rail_interval_misses(rows, stations, parameters,
1131 [&first.face.surface, &second.face.surface])?.into_iter().fold(0.0_f64, f64::max))
1132}
1133
1134fn carve_closed_wall(
1135 solid: &BrepSolid,
1136 rows: &FittedRows,
1137 stations: &[Station],
1138 parameters: &[f64],
1139 first: &BlendMate,
1140 second: &BlendMate,
1141 radius: f64,
1142) -> Result<crate::blend::carve::CarvedCrease, KernelRefusal> {
1143 let policy = crate::KernelTolerances::for_solid(solid, 1e-7);
1144 let rail_bar = 0.5 * policy.intersection_fit;
1145 let off = rails_off_closed_carriers(rows, stations, parameters, first, second)?;
1146 crate::blend::carve::carve_trace(format_args!(
1147 "blend closed-edge carve: {} stations, rails {off:.3e} off their carriers between stations (bar {rail_bar:.3e})",
1148 parameters.len()
1149 ));
1150 if off > rail_bar {
1151 return Err(crate::blend::fold::wall_fold_refusal(
1152 Vec::new(),
1153 format!(
1154 "{} {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",
1155 crate::blend::WALL_FOLDS
1156 ),
1157 ));
1158 }
1159 let Some(carved) = crate::blend::carve::trace_wall_crease(&rows.surface, radius)? else {
1160 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"));
1161 };
1162 let [u0, u1] = rows.u_domain;
1163 let margin = 1e-3 * (u1 - u0);
1164 let straddles = carved.ends.iter().any(|end| end[0] <= u0 + margin || end[0] >= u1 - margin);
1165 if straddles {
1166 return Err(crate::blend::fold::wall_fold_refusal(
1167 Vec::new(),
1168 format!(
1169 "{} {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",
1170 crate::blend::WALL_FOLDS, carved.ends[0][0], carved.ends[1][0]
1171 ),
1172 ));
1173 }
1174 crate::blend::carve::fit_crease(&rows.surface, &carved, policy.pcurve_consistency)
1175}
1176
1177pub(in crate::blend) fn refit_closed_support_pcurve(rail: &NurbsCurve, old: &NurbsCurve, surface: &NurbsSurface) -> Result<NurbsCurve, KernelRefusal> {
1197 refit_closed_support_pcurve_read(rail, old, surface, &mut crate::blend::track_fit::FootMemo::default()).map(|(fitted, _)| fitted)
1198}
1199
1200pub(in crate::blend) fn refit_closed_support_pcurve_read(
1208 rail: &NurbsCurve,
1209 old: &NurbsCurve,
1210 surface: &NurbsSurface,
1211 memo: &mut crate::blend::track_fit::FootMemo,
1212) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1213 const SHIPPED_READ_REPAIRS: usize = 3;
1214 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1215 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1216 let mut near: Vec<f64> = Vec::new();
1217 let (mut fitted, mut read) = fit_and_read_support(rail, old, surface, &near, memo)?;
1218 for _ in 0..SHIPPED_READ_REPAIRS {
1219 if read.0 <= floor || !(d1 > d0) {
1220 break;
1221 }
1222 near.push((read.3 - d0) / (d1 - d0));
1223 crate::blend::record_count("count.surgery.refit_repairs", 1);
1224 match fit_and_read_support(rail, old, surface, &near, memo) {
1227 Ok(repaired) => (fitted, read) = repaired,
1228 Err(_) => break,
1229 }
1230 }
1231 let (miss, standoff, samples, _) = read;
1232 if !(miss <= floor) {
1233 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "closed_support_pcurve_floor", format!(
1234 "{} a blend's support pcurve, as shipped, misses its rail's track by {miss:.3e} at {samples} samples, \
1235 against a floor of {floor:.1e}",
1236 crate::blend::PCURVE_OFF_FLOOR,
1237 )));
1238 }
1239 Ok((fitted, read))
1240}
1241
1242fn fit_and_read_support(
1246 rail: &NurbsCurve,
1247 old: &NurbsCurve,
1248 surface: &NurbsSurface,
1249 near: &[f64],
1250 memo: &mut crate::blend::track_fit::FootMemo,
1251) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1252 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1253 let guide = crate::blend::chain::pcurve_guide(old, surface)?;
1256 let mut fitted = crate::blend::chain::project_piece_pcurve_within(rail, surface, false, false, Some(&guide), near)?;
1257 let [f0, f1] = fitted.domain().or_refuse(KernelStage::Refine, "domain")?;
1260 for knot in fitted.knots.iter_mut() {
1261 *knot = d0 + (*knot - f0) / (f1 - f0) * (d1 - d0);
1262 }
1263 let debug = std::env::var_os("BREP_DEBUG_TRACK_FIT").is_some();
1264 if debug {
1265 match crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &fitted, d0, d1) {
1268 Ok((miss, standoff, samples, worst)) => eprintln!(
1269 "TRACK_FIT closed-edge support: before the seam ends, miss {miss:.3e} at t {worst:.9} ({samples} samples), standoff {standoff:.3e}"),
1270 Err(error) => eprintln!("TRACK_FIT closed-edge support: before the seam ends, unreadable: {error}"),
1271 }
1272 }
1273 let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
1274 let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1275 let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1276 let old_start = old.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?;
1277 let old_end = old.evaluate(d1).or_refuse(KernelStage::Refine, "evaluate")?;
1278 let new_start = fitted.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?;
1279 let shift = |closed: bool, from: f64, to: f64, period: f64| if closed { ((to - from) / period).round() * period } else { 0.0 };
1281 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));
1282 for point in fitted.control_points.iter_mut() {
1283 point.x += shift_u * point.w;
1284 point.y += shift_v * point.w;
1285 }
1286 let on_seam = |closed: bool, value: f64, low: f64, high: f64| {
1287 let period = high - low;
1288 closed && ((value - low) / period - ((value - low) / period).round()).abs() * period <= 1e-12 * period.max(1.0)
1289 };
1290 let last = fitted.control_points.len() - 1;
1291 for (index, end) in [(0, old_start), (last, old_end)] {
1292 let point = &mut fitted.control_points[index];
1293 if on_seam(closed_u, end.x, u0, u1) {
1294 point.x = end.x * point.w;
1295 }
1296 if on_seam(closed_v, end.y, v0, v1) {
1297 point.y = end.y * point.w;
1298 }
1299 }
1300 let mut own = crate::blend::track_fit::FootMemo::default();
1310 let (miss, standoff, samples, worst) =
1311 crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut own)?;
1312 *memo = own;
1313 if debug {
1314 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}");
1315 }
1316 Ok((fitted, (miss, standoff, samples, worst)))
1317}
1318
1319
1320fn build_surgery(
1321 solid: &BrepSolid,
1322 edge: &EdgeRecord,
1323 first: &BlendMate,
1324 second: &BlendMate,
1325 rows: FittedRows,
1326 second_split: Option<SecondSeamSplit>,
1327 name: Option<&str>,
1328 crease: Option<crate::blend::carve::CarvedCrease>,
1329) -> Result<BrepSolid, KernelRefusal> {
1330 let mut result = solid.clone();
1331 let mut take_id = crate::blend::edge::fresh_id_source(solid);
1332
1333 let [u_start, u_end] = rows.u_domain;
1334 let cr_start = rows.cr.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1335 let cs_start = rows.cs.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1336 let vertex1_id = take_id();
1337 let vertex2_id = take_id();
1338 result.vertices.push(VertexRecord {
1339 id: vertex1_id,
1340 point: cr_start,
1341 });
1342 result.vertices.push(VertexRecord {
1343 id: vertex2_id,
1344 point: cs_start,
1345 });
1346
1347 let cr_edge_id = take_id();
1348 result.edges.push(EdgeRecord {
1349 id: cr_edge_id,
1350 curve: rows.cr.clone(),
1351 t0: u_start,
1352 t1: u_end,
1353 start_vertex_id: vertex1_id,
1354 end_vertex_id: vertex1_id,
1355 degenerate: false,
1356 name: None,
1357 });
1358 let mut cs_pieces: Vec<(u64, [f64; 2], NurbsCurve)> = Vec::new();
1362 let mut second_seam_vertex = vertex2_id;
1363 let mut second_seam_point = cs_start;
1364 let mut second_crossing_v = 0.0;
1365 if let Some(split) = &second_split {
1366 let p = split.fit_parameter;
1367 second_crossing_v = split.crossing_v;
1368 let (cs_a, cs_b) = rows.cs.split(p).or_refuse(KernelStage::Refine, "split")?;
1369 let (pc_a, pc_b) = rows.cs_pcurve.split(p).or_refuse(KernelStage::Refine, "split")?;
1370 let mut pc_b = pc_b;
1372 for point in pc_b.control_points.iter_mut() {
1373 point.x -= split.period * point.w;
1374 }
1375 let w2s = take_id();
1376 second_seam_vertex = w2s;
1377 second_seam_point = rows.cs.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?;
1378 result.vertices.push(VertexRecord {
1379 id: w2s,
1380 point: second_seam_point,
1381 });
1382 let edge_a = take_id();
1383 result.edges.push(EdgeRecord {
1384 id: edge_a,
1385 curve: cs_a,
1386 t0: u_start,
1387 t1: p,
1388 start_vertex_id: vertex2_id,
1389 end_vertex_id: w2s,
1390 degenerate: false,
1391 name: None,
1392 });
1393 cs_pieces.push((edge_a, [u_start, p], pc_a));
1394 let edge_b = take_id();
1395 result.edges.push(EdgeRecord {
1396 id: edge_b,
1397 curve: cs_b,
1398 t0: p,
1399 t1: u_end,
1400 start_vertex_id: w2s,
1401 end_vertex_id: vertex2_id,
1402 degenerate: false,
1403 name: None,
1404 });
1405 cs_pieces.push((edge_b, [p, u_end], pc_b));
1406 } else {
1407 let cs_edge_id = take_id();
1408 result.edges.push(EdgeRecord {
1409 id: cs_edge_id,
1410 curve: rows.cs.clone(),
1411 t0: u_start,
1412 t1: u_end,
1413 start_vertex_id: vertex2_id,
1414 end_vertex_id: vertex2_id,
1415 degenerate: false,
1416 name: None,
1417 });
1418 cs_pieces.push((cs_edge_id, [u_start, u_end], rows.cs_pcurve.clone()));
1419 }
1420 let seam_curve = rows.surface.iso_curve_u(u_start).or_refuse(KernelStage::Refine, "iso_curve_u")?;
1421 let [seam_t0, seam_t1] = seam_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1422 let blend_seam_id = take_id();
1423 result.edges.push(EdgeRecord {
1424 id: blend_seam_id,
1425 curve: seam_curve,
1426 t0: seam_t0,
1427 t1: seam_t1,
1428 start_vertex_id: vertex1_id,
1429 end_vertex_id: vertex2_id,
1430 degenerate: false,
1431 name: None,
1432 });
1433
1434 let replace_in_face = |result: &mut BrepSolid, mate: &BlendMate, pieces: &[(u64, [f64; 2], NurbsCurve)], uv_new: [f64; 2]| -> Result<bool, KernelRefusal> {
1435 replace_rim_in_face(result, edge, mate, pieces, uv_new)
1436 };
1437 let uv1_new = rows.cr_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1438 let v1_new = uv1_new.y;
1439 let cr_pieces = vec![(cr_edge_id, [u_start, u_end], rows.cr_pcurve.clone())];
1440 let mut repaired = replace_in_face(&mut result, first, &cr_pieces, [uv1_new.x, v1_new])?;
1441 let uv2_new = rows.cs_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1442 let v2_new = if second_split.is_some() {
1443 second_crossing_v
1444 } else {
1445 uv2_new.y
1446 };
1447 repaired |= replace_in_face(&mut result, second, &cs_pieces, [uv2_new.x, v2_new])?;
1448
1449 let collapse_winding_sphere_cap =
1455 |result: &mut BrepSolid, face_id: u64, support_ids: &[u64]| -> Result<(), KernelRefusal> {
1456 let face = result
1457 .shells
1458 .iter_mut()
1459 .flat_map(|shell| &mut shell.faces)
1460 .find(|face| face.id == face_id)
1461 .ok_or(KernelRefusal::internal(KernelStage::Sew, "sphere_carrier", "blend: sphere carrier lost during cap surgery"))?;
1462 if !matches!(
1463 face.surface.analytic(),
1464 Some(crate::AnalyticSurface::Sphere { .. })
1465 ) || face.surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")? != (true, false)
1466 {
1467 return Ok(());
1468 }
1469 let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1470 let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1471 let period = u1 - u0;
1472 let support_loop = face.loops.iter().position(|loop_record| {
1473 !loop_record.coedges.is_empty()
1474 && loop_record
1475 .coedges
1476 .iter()
1477 .all(|coedge| support_ids.contains(&coedge.edge_id))
1478 });
1479 let Some(support_loop) = support_loop else {
1480 return Ok(());
1481 };
1482 if face.loops[support_loop].coedges.len() != 1 {
1483 return Ok(());
1484 }
1485 let support = &face.loops[support_loop].coedges[0];
1486 let [s0, s1] = support.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1487 let support_start = support.pcurve.evaluate(s0).or_refuse(KernelStage::Refine, "evaluate")?;
1488 let support_end = support.pcurve.evaluate(s1).or_refuse(KernelStage::Refine, "evaluate")?;
1489 let support_winding = support_end.x - support_start.x;
1490 if (support_winding.abs() - period).abs() > 0.05 * period {
1491 return Ok(());
1492 }
1493
1494 let mut pole: Option<(usize, CoedgeRecord)> = None;
1495 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1496 if loop_index == support_loop {
1497 continue;
1498 }
1499 for coedge in &loop_record.coedges {
1500 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1501 let start = coedge.pcurve.evaluate(p0).or_refuse(KernelStage::Refine, "evaluate")?;
1502 let end = coedge.pcurve.evaluate(p1).or_refuse(KernelStage::Refine, "evaluate")?;
1503 let winding = end.x - start.x;
1504 let at_pole = (start.y - v0).abs() <= 1e-8 || (start.y - v1).abs() <= 1e-8;
1505 if at_pole
1506 && (end.y - start.y).abs() <= 1e-8
1507 && (winding.abs() - period).abs() <= 0.05 * period
1508 && winding * support_winding < 0.0
1509 {
1510 pole = Some((loop_index, coedge.clone()));
1511 break;
1512 }
1513 }
1514 if pole.is_some() {
1515 break;
1516 }
1517 }
1518 let Some((pole_loop, pole_coedge)) = pole else {
1519 return Ok(());
1520 };
1521 let support_record = face.loops[support_loop].clone();
1522 let pole_id = face.loops[pole_loop].id;
1523 face.loops = vec![
1524 LoopRecord {
1525 id: pole_id,
1526 coedges: vec![pole_coedge],
1527 },
1528 support_record,
1529 ];
1530 Ok(())
1531 };
1532 collapse_winding_sphere_cap(&mut result, first.face.id, &[cr_edge_id])?;
1533 let cs_edge_ids: Vec<u64> = cs_pieces.iter().map(|(edge_id, ..)| *edge_id).collect();
1534 collapse_winding_sphere_cap(&mut result, second.face.id, &cs_edge_ids)?;
1535 for shell in &mut result.shells {
1537 for face in &mut shell.faces {
1538 for loop_record in &mut face.loops {
1539 for coedge in &mut loop_record.coedges {
1540 if coedge.id == 0 {
1541 coedge.id = take_id();
1542 }
1543 }
1544 }
1545 }
1546 }
1547
1548 let new_edge_ids: Vec<u64> = std::iter::once(cr_edge_id)
1553 .chain(cs_pieces.iter().map(|(edge_id, ..)| *edge_id))
1554 .chain(std::iter::once(blend_seam_id))
1555 .collect();
1556 let first_face_edges: Vec<u64> = first
1557 .face
1558 .loops
1559 .iter()
1560 .flat_map(|loop_record| loop_record.coedges.iter().map(|coedge| coedge.edge_id))
1561 .collect();
1562 repaired |= trim_seam_edges(
1563 &mut result,
1564 edge,
1565 &new_edge_ids,
1566 &first_face_edges,
1567 (vertex1_id, v1_new, cr_start),
1568 (second_seam_vertex, v2_new, second_seam_point),
1569 )?;
1570
1571 let mid_u = (u_start + u_end) * 0.5;
1573 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
1574 let station_uv = rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?;
1575 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
1576 let out1 = if first.face.same_sense {
1577 n1
1578 } else {
1579 n1.scale(-1.0)
1580 };
1581 let same_sense = blend_normal.dot(out1) >= 0.0;
1582 let loop_id = take_id();
1583 let mut coedges = vec![
1584 CoedgeRecord {
1585 id: take_id(),
1586 edge_id: cr_edge_id,
1587 forward: true,
1588 pcurve: crate::sweep_topology::parameter_line(u_start, 0.0, u_end, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1589 },
1590 CoedgeRecord {
1591 id: take_id(),
1592 edge_id: blend_seam_id,
1593 forward: true,
1594 pcurve: crate::sweep_topology::parameter_line(u_end, 0.0, u_end, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1595 },
1596 ];
1597 for (edge_id, window, _) in cs_pieces.iter().rev() {
1598 coedges.push(CoedgeRecord {
1599 id: take_id(),
1600 edge_id: *edge_id,
1601 forward: false,
1602 pcurve: crate::sweep_topology::parameter_line(window[1], 1.0, window[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1603 });
1604 }
1605 coedges.push(CoedgeRecord {
1606 id: take_id(),
1607 edge_id: blend_seam_id,
1608 forward: false,
1609 pcurve: crate::sweep_topology::parameter_line(u_start, 1.0, u_start, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1610 });
1611 if !same_sense {
1621 coedges.reverse();
1622 for coedge in &mut coedges {
1623 coedge.forward = !coedge.forward;
1624 coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
1625 }
1626 }
1627 let mut loops = vec![LoopRecord {
1628 id: loop_id,
1629 coedges,
1630 }];
1631 if let Some(crease) = crease {
1640 let start_vertex = take_id();
1641 let end_vertex = take_id();
1642 let crease_edge = take_id();
1643 result.vertices.push(VertexRecord { id: start_vertex, point: crease.start });
1644 result.vertices.push(VertexRecord { id: end_vertex, point: crease.end });
1645 let [crease_t0, crease_t1] = crease.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1646 result.edges.push(EdgeRecord {
1647 id: crease_edge,
1648 curve: crease.curve,
1649 t0: crease_t0,
1650 t1: crease_t1,
1651 start_vertex_id: start_vertex,
1652 end_vertex_id: end_vertex,
1653 degenerate: false,
1654 name: name.map(|value| format!("{value}:CREASE")),
1655 });
1656 let outer = crate::blend::chain::signed_area(&loops[0].coedges)?;
1657 let forward_first = crate::blend::chain::signed_area(&[
1658 CoedgeRecord { id: 0, edge_id: crease_edge, forward: true, pcurve: crease.first.clone() },
1659 CoedgeRecord { id: 0, edge_id: crease_edge, forward: false, pcurve: crease.second.reversed().or_refuse(KernelStage::Refine, "reversed")? },
1660 ])?;
1661 let inner = if outer * forward_first < 0.0 {
1662 vec![
1663 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: true, pcurve: crease.first },
1664 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: false, pcurve: crease.second.reversed().or_refuse(KernelStage::Refine, "reversed")? },
1665 ]
1666 } else {
1667 vec![
1668 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: true, pcurve: crease.second },
1669 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: false, pcurve: crease.first.reversed().or_refuse(KernelStage::Refine, "reversed")? },
1670 ]
1671 };
1672 loops.push(LoopRecord { id: take_id(), coedges: inner });
1673 }
1674 let blend_face = FaceRecord {
1675 id: take_id(),
1676 surface: rows.surface,
1677 same_sense,
1678 loops,
1679 name: name.map(|value| value.to_string()),
1680 };
1681 let shell_index = result
1682 .shells
1683 .iter()
1684 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
1685 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
1686 result.shells[shell_index].faces.push(blend_face);
1687
1688 let used_edges: std::collections::HashSet<u64> = result
1689 .shells
1690 .iter()
1691 .flat_map(|shell| &shell.faces)
1692 .flat_map(|face| &face.loops)
1693 .flat_map(|loop_record| &loop_record.coedges)
1694 .map(|coedge| coedge.edge_id)
1695 .collect();
1696 result
1697 .edges
1698 .retain(|candidate| used_edges.contains(&candidate.id));
1699 let used_vertices: std::collections::HashSet<u64> = result
1700 .edges
1701 .iter()
1702 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
1703 .collect();
1704 result
1705 .vertices
1706 .retain(|candidate| used_vertices.contains(&candidate.id));
1707 if repaired {
1714 let problems = result.validate();
1715 if !problems.is_empty() {
1716 return Err(KernelRefusal::internal(KernelStage::Validate, "neighbour_trim_closure", format!(
1717 "blend: the repaired neighbour trim did not close ({} issue(s), first: {})",
1718 problems.len(),
1719 problems
1720 .first()
1721 .map(|issue| issue.message.as_str())
1722 .unwrap_or("unknown")
1723 )));
1724 }
1725 }
1726 Ok(result)
1727}
1728
1729pub(in crate::blend) fn replace_rim_in_face(
1735 result: &mut BrepSolid,
1736 edge: &EdgeRecord,
1737 mate: &BlendMate,
1738 pieces: &[(u64, [f64; 2], NurbsCurve)],
1739 uv_new: [f64; 2],
1740) -> Result<bool, KernelRefusal> {
1741 let old_vertex = edge.start_vertex_id;
1742 let v_new = uv_new[1];
1743 let seam_edge_ends: Vec<(u64, bool, bool)> = result
1745 .edges
1746 .iter()
1747 .filter(|candidate| {
1748 candidate.id != edge.id
1749 && (candidate.start_vertex_id == old_vertex
1750 || candidate.end_vertex_id == old_vertex)
1751 })
1752 .map(|candidate| {
1753 (
1754 candidate.id,
1755 candidate.start_vertex_id == old_vertex,
1756 candidate.end_vertex_id == old_vertex,
1757 )
1758 })
1759 .collect();
1760 let seam_edge_ids: Vec<u64> = seam_edge_ends.iter().map(|(id, ..)| *id).collect();
1761 let pcurve_band = crate::KernelTolerances::for_solid(result, 1e-7).pcurve_consistency;
1762 let debug = std::env::var("BREP_DEBUG_NETWORK").is_ok();
1763 let face = result
1764 .shells
1765 .iter_mut()
1766 .flat_map(|shell| &mut shell.faces)
1767 .find(|face| face.id == mate.face.id)
1768 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1769 let face_id = face.id;
1770 let FaceRecord { surface, loops, .. } = &mut *face;
1771 let loop_record = &mut loops[mate.loop_index];
1772 let position = loop_record
1773 .coedges
1774 .iter()
1775 .position(|coedge| coedge.edge_id == edge.id)
1776 .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
1777 let old_forward = loop_record.coedges[position].forward;
1778 let old_seam_v = {
1779 let pcurve = &loop_record.coedges[position].pcurve;
1780 let [d0, _] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1781 pcurve.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?.y
1782 };
1783 let mut replacements = Vec::new();
1786 let ordered: Vec<&(u64, [f64; 2], NurbsCurve)> = if old_forward {
1787 pieces.iter().collect()
1788 } else {
1789 pieces.iter().rev().collect()
1790 };
1791 for (index, (edge_id, _, pcurve)) in ordered.iter().enumerate() {
1792 replacements.push(CoedgeRecord {
1793 id: if index == 0 {
1794 loop_record.coedges[position].id
1795 } else {
1796 0 },
1798 edge_id: *edge_id,
1799 forward: old_forward,
1800 pcurve: if old_forward {
1801 pcurve.clone()
1802 } else {
1803 pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
1804 },
1805 });
1806 }
1807 loop_record
1808 .coedges
1809 .splice(position..=position, replacements);
1810 let mut repaired = false;
1811 if loop_record.coedges.len() > 1 {
1812 for coedge in &mut loop_record.coedges {
1818 if pieces
1819 .iter()
1820 .any(|(edge_id, ..)| *edge_id == coedge.edge_id)
1821 || !seam_edge_ids.contains(&coedge.edge_id)
1822 {
1823 continue;
1824 }
1825 let controls = &coedge.pcurve.control_points;
1826 let last_index = controls.len() - 1;
1827 let first_u = controls[0].x / controls[0].w;
1828 let last_u = controls[last_index].x / controls[last_index].w;
1829 let constant_u = (first_u - last_u).abs()
1830 <= 1e-9 * (1.0 + first_u.abs().max(last_u.abs()));
1831 let (target, target_uv, fraction) = if constant_u {
1834 let first_v = controls[0].y / controls[0].w;
1838 let last_v = controls[last_index].y / controls[last_index].w;
1839 let target = if (first_v - old_seam_v).abs() < (last_v - old_seam_v).abs() {
1840 0
1841 } else {
1842 last_index
1843 };
1844 let span = last_v - first_v;
1845 let fraction = if span.abs() > 0.0 { (v_new - first_v) / span } else { 0.0 };
1846 (target, [first_u, v_new], fraction)
1847 } else {
1848 let Some((_, start_is_old, end_is_old)) = seam_edge_ends
1858 .iter()
1859 .find(|(id, ..)| *id == coedge.edge_id)
1860 .copied()
1861 else {
1862 continue;
1863 };
1864 if start_is_old == end_is_old {
1865 return Err(KernelRefusal::ill_posed(KernelStage::Sew, "neighbour_trim_end", format!(
1866 "blend: neighbour edge {} meets the blended edge at both ends; \
1867 its trim end is ambiguous",
1868 coedge.edge_id
1869 )));
1870 }
1871 let target = if start_is_old == coedge.forward { 0 } else { last_index };
1874 let v_end = controls[target].y / controls[target].w;
1875 let span = last_u - first_u;
1876 let fraction = if span.abs() > 0.0 { (uv_new[0] - first_u) / span } else { 0.0 };
1877 (target, [uv_new[0], v_end], fraction)
1878 };
1879 let [q0, q1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1891 let target_point = surface
1892 .evaluate_extended(target_uv[0], target_uv[1])
1893 .or_refuse(KernelStage::Refine, "evaluate_extended")?;
1894 let seed = (q0 + (q1 - q0) * fraction).clamp(q0.min(q1), q0.max(q1));
1900 let q_split = trim_parameter_at_point(
1901 surface, &coedge.pcurve, seed, target_point, pcurve_band,
1902 )?;
1903 let interior = q_split > q0 + 1e-9 * (q1 - q0) && q_split < q1 - 1e-9 * (q1 - q0);
1904 if debug {
1905 eprintln!(
1906 "closed-edge seam trim: face {face_id} edge {} coedge {} ({}): {} at q {q_split:.9} of [{q0:.6}, {q1:.6}], {} control points",
1907 coedge.edge_id, coedge.id, if constant_u { "meridian, constant u" } else { "along u" },
1908 if interior { "SPLIT" } else { "EXTEND" }, controls.len()
1909 );
1910 }
1911 if interior {
1912 let (low, high) = coedge.pcurve.split(q_split).or_refuse(KernelStage::Refine, "split")?;
1913 coedge.pcurve = if target == 0 { high } else { low };
1914 } else {
1915 if coedge.pcurve.degree != 1 || controls.len() != 2 {
1920 return Err(KernelRefusal::unsupported(KernelStage::Sew, "neighbour_seam_extend_curved", format!(
1921 "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)",
1922 coedge.edge_id, coedge.pcurve.degree, controls.len()
1923 )));
1924 }
1925 let w = coedge.pcurve.control_points[target].w;
1926 coedge.pcurve.control_points[target] = crate::Vec4::from_point(
1927 Vec3::new(target_uv[0], target_uv[1], 0.0), w,
1928 );
1929 }
1930 repaired = true;
1931 }
1932 }
1933 Ok(repaired)
1934}
1935
1936pub(in crate::blend) fn trim_seam_edges(
1942 result: &mut BrepSolid,
1943 edge: &EdgeRecord,
1944 new_edge_ids: &[u64],
1945 first_face_edges: &[u64],
1946 first_target: (u64, f64, Vec3),
1947 second_target: (u64, f64, Vec3),
1948) -> Result<bool, KernelRefusal> {
1949 let old_vertex = edge.start_vertex_id;
1950 let mut repaired = false;
1951 let (vertex1_id, v1_new, cr_start) = first_target;
1952 let (second_seam_vertex, v2_new, second_seam_point) = second_target;
1953 for seam_edge in result.edges.iter_mut() {
1954 if new_edge_ids.contains(&seam_edge.id) {
1955 continue;
1956 }
1957 if seam_edge.start_vertex_id != old_vertex && seam_edge.end_vertex_id != old_vertex {
1958 continue;
1959 }
1960 let (target_vertex, target_v, target_point) = if first_face_edges.contains(&seam_edge.id) {
1961 (vertex1_id, v1_new, cr_start)
1962 } else {
1963 (second_seam_vertex, v2_new, second_seam_point)
1964 };
1965 let [c0, c1] = seam_edge.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1966 let clamped = target_v.clamp(c0.min(c1), c0.max(c1));
1967 let band = 1e-6 * (1.0 + target_point.length());
1976 let split_parameter = if seam_edge.id == edge.id {
1977 clamped
1983 } else {
1984 match seam_edge.curve.evaluate(clamped) {
1985 Ok(point) if point.sub(target_point).length() <= band => clamped,
1986 _ => {
1987 repaired = true;
1988 let projection =
1989 crate::project_point_to_curve(&seam_edge.curve, target_point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
1990 if projection.distance > band {
1991 return Err(KernelRefusal::unsupported(KernelStage::Sew, "neighbour_off_support", format!(
1992 "blend: neighbour edge {} does not pass through the blend's support \
1993 start (off by {:.9}); the trim has no parameter to move to",
1994 seam_edge.id, projection.distance
1995 )));
1996 }
1997 projection.u.clamp(c0.min(c1), c0.max(c1))
1998 }
1999 }
2000 };
2001 if seam_edge.start_vertex_id == old_vertex {
2002 seam_edge.t0 = split_parameter;
2003 seam_edge.start_vertex_id = target_vertex;
2004 }
2005 if seam_edge.end_vertex_id == old_vertex {
2006 seam_edge.t1 = split_parameter;
2007 seam_edge.end_vertex_id = target_vertex;
2008 }
2009 }
2010 Ok(repaired)
2011}