1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use super::*;
3
4pub(crate) const CONCAVE_RIM_OVERRUN_WHAT: &str = "concave_rim_overrun";
8
9fn concave_rim_true_contacts_on() -> bool {
25 switch!("BREP_CONCAVE_RIM_TRUE_CONTACTS", true)
26}
27
28fn concave_rim_overrun(
29 solid: &BrepSolid,
30 edge: &EdgeRecord,
31 mates: [(&FaceRecord, usize); 2],
32 radius: f64,
33 contacts: Option<[Vec<Vec3>; 2]>,
34) -> Result<Option<KernelRefusal>, KernelRefusal> {
35 const RIM_SAMPLES: usize = 64;
36 const LOOP_SAMPLES: usize = 256;
37 if edge.start_vertex_id != edge.end_vertex_id {
38 return Ok(None);
39 }
40 for (index, (face, rim_loop)) in mates.iter().copied().enumerate() {
41 if rim_loop == 0 {
42 continue;
43 }
44 let Some(crate::AnalyticSurface::Plane { origin, u_dir, v_dir, .. }) = face.surface.analytic() else { continue };
45 let wall = mates[1 - index].0;
46 let e1 = u_dir.normalized().or_refuse(KernelStage::Classify, "normalized")?;
47 let normal = u_dir.cross(*v_dir).normalized().or_refuse(KernelStage::Classify, "normalized")?;
48 let e2 = normal.cross(e1);
49 let flat = |p: Vec3| -> (f64, f64) { let d = p.sub(*origin); (d.dot(e1), d.dot(e2)) };
50 let polygon = |loop_index: usize| -> Result<Vec<(f64, f64)>, KernelRefusal> {
52 let mut points: Vec<(f64, f64)> = Vec::new();
53 for coedge in &face.loops[loop_index].coedges {
54 let Some(boundary) = solid.edges.iter().find(|candidate| candidate.id == coedge.edge_id) else { continue };
55 for k in 0..LOOP_SAMPLES {
56 let f = k as f64 / LOOP_SAMPLES as f64;
57 let t = if coedge.forward { boundary.t0 + (boundary.t1 - boundary.t0) * f } else { boundary.t1 - (boundary.t1 - boundary.t0) * f };
58 points.push(flat(boundary.curve.evaluate(t).or_refuse(KernelStage::Classify, "evaluate")?));
59 }
60 }
61 Ok(points)
62 };
63 let (outer, rim_polygon) = (polygon(0)?, polygon(rim_loop)?);
64 let within = |points: &[(f64, f64)], x: f64, y: f64| -> bool {
65 crate::polygon::parity_inside([x, y], points.len(), |k| [points[k].0, points[k].1])
66 };
67 let inside = |x: f64, y: f64| -> bool { within(&outer, x, y) && !within(&rim_polygon, x, y) };
70 {
74 let derivatives = edge.curve.derivatives(edge.t0, 1).or_refuse(KernelStage::Classify, "derivatives")?;
75 let (point, tangent) = (derivatives[0], derivatives[1]);
76 let side = normal.cross(tangent).normalized().or_refuse(KernelStage::Classify, "normalized")?;
77 let probe = radius * 0.05;
80 let (px, py) = flat(point.add(side.scale(probe)));
81 let toward = if inside(px, py) { side } else { side.scale(-1.0) };
82 let foot = crate::project_point_to_surface(&wall.surface, point).or_refuse(KernelStage::Classify, "project_point_to_surface")?;
83 let wall_normal = wall.surface.normal(foot.u, foot.v).or_refuse(KernelStage::Classify, "normal")?;
84 let outward = if wall.same_sense { wall_normal } else { wall_normal.scale(-1.0) };
85 if outward.dot(toward) <= 0.0 {
86 continue;
87 }
88 }
89 let mut overruns = false;
90 if let (true, Some(contacts)) = (concave_rim_true_contacts_on(), contacts.as_ref()) {
91 let on_loop = |x: f64, y: f64| -> bool {
96 let band = 1e-7 * (1.0 + x.abs() + y.abs());
97 (0..outer.len()).any(|k| {
98 let (a, b) = (outer[k], outer[(k + 1) % outer.len()]);
99 let (dx, dy) = (b.0 - a.0, b.1 - a.1);
100 let length2 = dx * dx + dy * dy;
101 let t = if length2 > 0.0 { (((x - a.0) * dx + (y - a.1) * dy) / length2).clamp(0.0, 1.0) } else { 0.0 };
102 (x - a.0 - t * dx).hypot(y - a.1 - t * dy) <= band
103 })
104 };
105 for contact in &contacts[index] {
106 let (x, y) = flat(*contact);
107 if !within(&outer, x, y) && !on_loop(x, y) {
108 overruns = true;
109 break;
110 }
111 }
112 } else {
113 for k in 0..RIM_SAMPLES {
114 let t = edge.t0 + (edge.t1 - edge.t0) * (k as f64 + 0.5) / RIM_SAMPLES as f64;
115 let derivatives = edge.curve.derivatives(t, 1).or_refuse(KernelStage::Classify, "derivatives")?;
116 let (point, tangent) = (derivatives[0], derivatives[1]);
117 let Ok(side) = normal.cross(tangent).normalized() else { continue };
118 let probe = radius * 0.05;
123 let toward = if inside(flat(point.add(side.scale(probe))).0, flat(point.add(side.scale(probe))).1) { side } else { side.scale(-1.0) };
124 let contact = flat(point.add(toward.scale(radius)));
125 if !within(&outer, contact.0, contact.1) {
126 overruns = true;
127 break;
128 }
129 }
130 }
131 if overruns {
132 return Ok(Some(KernelRefusal::unsupported(KernelStage::Classify, CONCAVE_RIM_OVERRUN_WHAT, format!(
133 "blend: the concave rim fill overruns the face's outer boundary — rolling on rim edge {}, the ball's contact on the plane {} it stands on (radius {radius} from the rim) reaches past that plane's outer loop, so the fill must be clipped by the plane's neighbouring faces, which this lane does not build",
134 edge.id,
135 face.name.clone().unwrap_or_else(|| format!("{}", face.id)),
136 ))));
137 }
138 }
139 Ok(None)
140}
141
142pub fn blend_closed_edge(
143 solid: &BrepSolid,
144 edge_id: u64,
145 radius: f64,
146 chamfer: bool,
147 name: Option<&str>,
148) -> Result<BrepSolid, KernelRefusal> {
149 let _operation = crate::blend::BlendOperation::enter();
152 if !(radius > 0.0) || !radius.is_finite() {
153 return Err(KernelRefusal::input(KernelStage::Collect, "radius", "blend: radius must be positive"));
154 }
155 blend_closed_edge_impl(solid, edge_id, &|_| radius, Some(radius), chamfer, name)
156}
157
158pub fn blend_closed_edge_knot_aware(
168 solid: &BrepSolid,
169 edge_id: u64,
170 radius: f64,
171 chamfer: bool,
172 name: Option<&str>,
173) -> Result<BrepSolid, KernelRefusal> {
174 let _operation = crate::blend::BlendOperation::enter();
175 if !(radius > 0.0) || !radius.is_finite() {
176 return Err(KernelRefusal::input(KernelStage::Collect, "radius", "blend: radius must be positive"));
177 }
178 KNOT_RETRY.with(|flag| flag.set(true));
179 let built = blend_closed_edge_impl(solid, edge_id, &|_| radius, Some(radius), chamfer, name);
180 KNOT_RETRY.with(|flag| flag.set(false));
181 built
182}
183
184pub fn is_pcurve_floor_refusal(refusal: &KernelRefusal) -> bool {
188 matches!(&refusal.class, crate::RefusalClass::NonConvergence { what }
189 if what == crate::blend::chain::CHAIN_PCURVE_FLOOR_WHAT || what == CLOSED_SUPPORT_PCURVE_FLOOR_WHAT)
190}
191
192pub(crate) const CLOSED_SUPPORT_PCURVE_FLOOR_WHAT: &str = "closed_support_pcurve_floor";
194
195pub fn blend_edge_variable(
199 solid: &BrepSolid,
200 edge_id: u64,
201 radii: &[(f64, f64)],
202 chamfer: bool,
203 name: Option<&str>,
204) -> Result<BrepSolid, KernelRefusal> {
205 let _operation = crate::blend::BlendOperation::enter();
208 if radii.is_empty() || radii.iter().any(|(_, radius)| !(*radius > 0.0)) {
209 return Err(KernelRefusal::input(KernelStage::Collect, "radius_stop", "blend: every radius stop must be positive"));
210 }
211 let mut stops = radii.to_vec();
212 stops.sort_by(|a, b| a.0.total_cmp(&b.0));
213 let edge = solid
214 .edges
215 .iter()
216 .find(|edge| edge.id == edge_id)
217 .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
218 let closed = edge.start_vertex_id == edge.end_vertex_id;
219 if closed && (stops[0].1 - stops[stops.len() - 1].1).abs() > 1e-12 {
220 return Err(KernelRefusal::input(KernelStage::Collect, "closed_edge_radii", "blend: closed edges need equal first/last radii"));
221 }
222 let first_radius = stops[0].1;
231 if stops
232 .iter()
233 .all(|(_, radius)| (radius - first_radius).abs() <= 1e-12 * (1.0 + first_radius.abs()))
234 {
235 return if chamfer {
236 crate::fillet::chamfer_edge(solid, edge_id, first_radius, name)
237 } else {
238 crate::fillet::fillet_edge(solid, edge_id, first_radius, name)
239 };
240 }
241 let (t0, t1) = (edge.t0, edge.t1);
242 let radius_at = move |t: f64| -> f64 {
243 let raw = (t - t0) / (t1 - t0);
244 let fraction = if closed {
252 raw.rem_euclid(1.0)
253 } else {
254 raw.clamp(0.0, 1.0)
255 };
256 if fraction <= stops[0].0 {
257 return stops[0].1;
258 }
259 for pair in stops.windows(2) {
260 if fraction <= pair[1].0 {
261 let width = (pair[1].0 - pair[0].0).max(1e-12);
262 let local = (fraction - pair[0].0) / width;
263 return pair[0].1 + (pair[1].1 - pair[0].1) * local;
264 }
265 }
266 stops[stops.len() - 1].1
267 };
268 if closed {
271 blend_closed_edge_impl(solid, edge_id, &radius_at, None, chamfer, name)
272 } else {
273 blend_open_edge_impl(solid, edge_id, &radius_at, None, chamfer, name)
274 }
275}
276
277fn blend_closed_edge_impl(
281 solid: &BrepSolid,
282 edge_id: u64,
283 radius_at: &dyn Fn(f64) -> f64,
284 constant: Option<f64>,
285 chamfer: bool,
286 name: Option<&str>,
287) -> Result<BrepSolid, KernelRefusal> {
288 let _closed_timing = crate::blend::BlendTimer::start("timing.closed_rim_ms");
289 let prepare_timing = crate::blend::BlendTimer::start("timing.closed.prepare_ms");
290 if let Some(separated) = separate_spherical_domain_cut(solid, edge_id)? {
291 return blend_closed_edge_impl(&separated, edge_id, radius_at, constant, chamfer, name);
292 }
293 if let Some(simplified) = remove_retraced_cuts(solid, edge_id)? {
294 return blend_closed_edge_impl(&simplified, edge_id, radius_at, constant, chamfer, name);
295 }
296 let edge = solid
297 .edges
298 .iter()
299 .find(|edge| edge.id == edge_id)
300 .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
301 if edge.start_vertex_id != edge.end_vertex_id {
302 return Err(KernelRefusal::input(KernelStage::Collect, "closed_edge", "blend: general path currently requires a CLOSED edge"));
303 }
304 let (face_a, loop_a, coedge_a) = locate_mate(solid, edge_id, None)?;
305 let (face_b, loop_b, coedge_b) = locate_mate(solid, edge_id, Some((face_a.id, loop_a)))?;
306 if face_a.id == face_b.id && loop_a == loop_b {
307 return Err(KernelRefusal::unsupported(KernelStage::Classify, "seam_edge", "blend: edge is used twice by one loop (seam edge?)"));
308 }
309
310 let seam_a = face_a.loops[loop_a].coedges.len() > 1;
313 let seam_b = face_b.loops[loop_b].coedges.len() > 1;
314 let (first_face, first_loop, first_coedge, second_face, second_loop, second_coedge) =
315 if seam_a || !seam_b {
316 (face_a, loop_a, coedge_a, face_b, loop_b, coedge_b)
317 } else {
318 (face_b, loop_b, coedge_b, face_a, loop_a, coedge_a)
319 };
320 let anchored = first_face.loops[first_loop].coedges.len() > 1;
321 let second_seam = anchored && second_face.loops[second_loop].coedges.len() > 1;
322 let anchor_u = if anchored {
323 Some(first_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?[0])
325 } else {
326 None
327 };
328
329 let mid_radius = radius_at(edge.t0 + (edge.t1 - edge.t0) * 0.5);
331 let (rho1, rho2) = signed_radii(
332 edge,
333 first_face,
334 first_coedge,
335 second_face,
336 second_coedge,
337 mid_radius,
338 )?;
339
340 let first_mate = BlendMate {
341 face: first_face,
342 coedge: first_coedge,
343 loop_index: first_loop,
344 rho: rho1,
345 };
346 let second_mate = BlendMate {
347 face: second_face,
348 coedge: second_coedge,
349 loop_index: second_loop,
350 rho: rho2,
351 };
352 let mid_t = edge.t0 + (edge.t1 - edge.t0) * 0.5;
353 use crate::blend::chain::FoldPolicy;
368 let march_with = |fold: FoldPolicy, depth_budget: usize| -> Result<Vec<Station>, KernelRefusal> {
369 match march_stations(edge, &first_mate, &second_mate, radius_at, anchor_u, fold, depth_budget) {
370 Ok(stations) => Ok(stations),
371 Err(anchored_error) if anchor_u.is_some() => {
372 march_stations(edge, &first_mate, &second_mate, radius_at, None, fold, depth_budget).map_err(
373 |retry_error| {
374 if crate::blend::is_wall_fold(&retry_error)
375 || crate::blend::is_marched_fit_off_carriers(&retry_error) {
376 retry_error
377 } else {
378 anchored_error
379 }
380 },
381 )
382 }
383 Err(anchored_error) => Err(anchored_error),
384 }
385 };
386 let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
387 let wall_check = {
391 let radius = constant.unwrap_or(f64::NAN);
394 let constant = constant.is_some();
395 let model = crate::KernelTolerances::for_solid(solid, 1e-7).model;
398 let bar = crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
399 (!chamfer && constant && radius.is_finite() && radius != 0.0).then(|| crate::blend::stations::ClosedWallCheck {
400 radius: radius.abs(),
401 bar,
402 model: model.is_finite().then_some(model),
403 })
404 };
405 crate::blend::stations::CLOSED_MODEL_UNMET.with(|slot| slot.set(None));
406 crate::blend::stations::CLOSED_REQUEST_RAILS.with(|slot| slot.set(None));
407 prepare_timing.stop();
408 let exact_revolution = crate::blend::exact_rim_lane_on()
409 && crate::blend::stations::closed_revolution_axis(&first_mate, &second_mate).is_some();
410 let depth_budget = if exact_revolution { 0 } else { MAX_REFINEMENT_DEPTH };
411 if exact_revolution {
412 crate::blend::record_count("count.closed.exact_lane", 1);
413 }
414 let march_timing = crate::blend::BlendTimer::start("timing.closed.march_ms");
415 let attempt_timing = crate::blend::BlendTimer::start("timing.closed.march.anchored_fallback_ms");
416 let setback = crate::blend::setback::active();
421 let analytic = if setback.is_some() {
422 None
423 } else {
424 crate::blend::stations::analytic_rim::construct(edge, &first_mate, &second_mate, constant, chamfer, anchor_u)
425 };
426 let setback_stations = match &setback {
427 Some(law) => Some(crate::blend::setback::march_closed(solid, edge, &first_mate, &second_mate, law, anchor_u, rail_bar)?),
428 None => None,
429 };
430 let (analytic_stations, analytic_rows) = match analytic {
431 Some((stations, rows)) => (Some(stations), Some(rows)),
432 None => (None, None),
433 };
434 let analytic_stations = setback_stations.or(analytic_stations);
435 let marched = match analytic_stations.map(Ok).unwrap_or_else(|| march_stations_with_rail_bar(edge, &first_mate, &second_mate, radius_at, anchor_u, FoldPolicy::Refuse, depth_budget, rail_bar, wall_check)) {
436 Ok(stations) => {
437 std::mem::forget(attempt_timing);
438 Ok(stations)
439 }
440 Err(failure) => {
441 let anchored_error = failure.into_initial()?;
444 if anchor_u.is_some() {
445 attempt_timing.stop();
446 crate::blend::record_count("count.closed.march.anchor_fallback", 1);
447 if std::env::var("BREP_DEBUG_CLOSED_RAILS").is_ok() {
448 eprintln!("closed initial anchor fallback: edge {}: {anchored_error}", edge.id);
449 }
450 match march_stations_with_rail_bar(edge, &first_mate, &second_mate, radius_at, None, FoldPolicy::Refuse, depth_budget, rail_bar, wall_check) {
451 Ok(stations) => Ok(stations),
452 Err(failure) => {
453 let retry_error = failure.into_initial()?;
454 if crate::blend::is_wall_fold(&retry_error) { Err(retry_error) }
455 else { Err(anchored_error) }
456 }
457 }
458 } else { Err(anchored_error) }
459 }
460 };
461 let model_unmet = crate::blend::stations::CLOSED_MODEL_UNMET.with(|slot| slot.take());
474 let request_rails = crate::blend::stations::CLOSED_REQUEST_RAILS.with(|slot| slot.take());
475 let mut carve_fold = false;
476 let climb_carve_ladder = |error: &KernelRefusal| -> Result<Vec<Station>, KernelRefusal> {
483 let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
484 let mut climbed: Option<Vec<Station>> = None;
485 let mut last_off = f64::NAN;
486 for depth_budget in MAX_REFINEMENT_DEPTH..=CARVE_MAX_REFINEMENT_DEPTH {
487 let stations = match march_with(FoldPolicy::Carve, depth_budget) {
488 Ok(stations) => stations,
489 Err(_) => break,
491 };
492 let parameters = crate::blend::stations::closed_station_parameters(&stations, [&first_mate.face.surface, &second_mate.face.surface]);
493 let off = match fit_closed_rows(&stations, ¶meters, chamfer) {
494 Ok(rows) => rails_off_closed_carriers(&rows, &stations, ¶meters, &first_mate, &second_mate)?,
495 Err(_) => break,
496 };
497 last_off = off;
498 crate::blend::carve::carve_trace(format_args!(
499 "blend closed-edge carve ladder: depth budget {depth_budget} ({} stations) leaves the rails {off:.3e} off their carriers (bar {rail_bar:.3e})",
500 stations.len()
501 ));
502 if off <= rail_bar {
503 climbed = Some(stations);
504 break;
505 }
506 }
507 climbed.ok_or_else(|| {
508 crate::blend::fold::wall_fold_refusal(
509 Vec::new(),
510 format!(
511 "{} {} 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})",
512 crate::blend::WALL_FOLDS,
513 radius_at(mid_t).abs()
514 ),
515 )
516 })
517 };
518 let marched = match marched {
519 Err(error)
520 if crate::blend::is_wall_fold(&error)
521 && !crate::blend::fold::is_rail_fold(&error)
522 && !chamfer =>
523 {
524 crate::blend::carve::carve_trace(format_args!(
525 "blend closed-edge: the wall folds ({error}); re-marching under Carve"
526 ));
527 match climb_carve_ladder(&error) {
528 Ok(stations) => {
529 carve_fold = true;
530 Ok(stations)
531 }
532 Err(refusal) => Err(refusal),
533 }
534 }
535 other => other,
536 };
537 let mut stations = match marched {
538 Ok(stations) => stations,
539 Err(march_error) if crate::blend::is_wall_fold(&march_error)
545 || crate::blend::is_marched_fit_off_carriers(&march_error) => return Err(march_error),
546 Err(march_error) if crate::blend::is_ball_off_carrier(&march_error) => {
547 return Err(march_error);
554 }
555 Err(march_error) => {
556 if let Some(refusal) = concave_rim_overrun(solid, edge, [(first_face, first_loop), (second_face, second_loop)], radius_at(mid_t), None)? {
559 return Err(refusal);
560 }
561 return blend_closed_edge_keep(
564 solid,
565 edge,
566 &first_mate,
567 &second_mate,
568 radius_at(mid_t),
569 rho1,
570 chamfer,
571 name,
572 )
573 .or_else(|_| {
574 blend_closed_edge_keep(
575 solid,
576 edge,
577 &second_mate,
578 &first_mate,
579 radius_at(mid_t),
580 rho2,
581 chamfer,
582 name,
583 )
584 })
585 .map_err(|keep_error| {
586 format!("{march_error}; edge-preserving blend also failed: {keep_error}")
587 }).or_refuse(KernelStage::Refine, "map_err");
588 }
589 };
590 let support_exits = |face: &FaceRecord, second_side: bool| -> bool {
594 let stride = (stations.len() / 16).max(1);
598 stations.iter().step_by(stride).any(|station| {
599 support_exits_at(face, if second_side { station.uv2 } else { station.uv1 })
600 })
601 };
602 let exits_timing = crate::blend::BlendTimer::start("timing.closed.march.exits_ms");
610 let revolution = crate::blend::stations::closed_revolution_axis(&first_mate, &second_mate);
614 let exit_of = |face: &FaceRecord, second_side: bool| -> bool {
615 let contacts: Vec<Vec3> = stations.iter().map(|station| if second_side { station.p2 } else { station.p1 }).collect();
616 let probe = |station: usize| -> bool {
617 support_exits_at(face, if second_side { stations[station].uv2 } else { stations[station].uv1 })
618 };
619 match revolution.and_then(|axis| support_exit_between_parallels(solid, face, axis, &contacts, &probe)) {
620 Some(exit) => {
621 crate::blend::record_count("count.closed.exits_by_parallels", 1);
622 exit
623 }
624 None => support_exits(face, second_side),
625 }
626 };
627 let exits = [exit_of(first_face, false), exit_of(second_face, true)];
628 exits_timing.stop();
629 if setback.is_some() && (exits[0] || exits[1]) {
632 return Err(crate::blend::setback::unsupported(
633 "setback_rail_leaves_face",
634 format!(
635 "asymmetric chamfer: the rail on face {} runs off that face",
636 if exits[0] { first_face.id } else { second_face.id }
637 ),
638 ));
639 }
640 if exits[0] || exits[1] {
641 let scale = crate::blend::stations::march_model_scale(&edge.curve, edge.t0, edge.t1, radius_at(mid_t))?;
642 match crate::blend::rollover::plan_closed(solid, edge, &stations, [&first_mate, &second_mate], radius_at, scale) {
643 Ok(Some(plan)) => {
644 if crate::blend::rollover_on() {
647 let pieces = crate::blend::rollover::march_pieces(
648 solid, edge, &plan, &stations, [&first_mate, &second_mate], radius_at, scale, rail_bar, chamfer, anchor_u,
649 )?;
650 let built = crate::blend::rollover::build(solid, edge, [&first_mate, &second_mate], &plan, pieces, anchor_u, name)?;
651 march_timing.stop();
652 return Ok(built);
653 }
654 return Err(crate::blend::rollover::refusal(&plan, radius_at(mid_t).abs()));
655 }
656 Ok(None) => {}
657 Err(error) => {
658 if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
659 eprintln!("closed-rim rollover scan did not read the exit: {error}");
660 }
661 }
662 }
663 }
664 if setback.is_none() {
665 let contacts = [
666 stations.iter().map(|station| station.p1).collect::<Vec<_>>(),
667 stations.iter().map(|station| station.p2).collect::<Vec<_>>(),
668 ];
669 if let Some(refusal) = concave_rim_overrun(solid, edge, [(first_face, first_loop), (second_face, second_loop)], radius_at(mid_t), Some(contacts))? {
670 return Err(refusal);
671 }
672 }
673 if exits[1] {
674 return blend_closed_edge_keep(
675 solid,
676 edge,
677 &first_mate,
678 &second_mate,
679 radius_at(mid_t),
680 rho1,
681 chamfer,
682 name,
683 );
684 }
685 if exits[0] {
686 return blend_closed_edge_keep(
687 solid,
688 edge,
689 &second_mate,
690 &first_mate,
691 radius_at(mid_t),
692 rho2,
693 chamfer,
694 name,
695 );
696 }
697 if std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok() {
698 let last = stations.len() - 1;
699 for index in [
700 0usize,
701 1.min(last),
702 2.min(last),
703 last.saturating_sub(1),
704 last,
705 ] {
706 let station = &stations[index];
707 eprintln!(
708 "station {index}: uv1=({:.6},{:.6}) uv2=({:.6},{:.6}) p1=({:.6},{:.6},{:.6}) w={:.6}",
709 station.uv1[0], station.uv1[1], station.uv2[0], station.uv2[1],
710 station.p1.x, station.p1.y, station.p1.z, station.weight,
711 );
712 }
713 }
714 march_timing.stop();
715 crate::blend::record_count("count.closed.stations", stations.len() as u64);
716 let rows_timing = crate::blend::BlendTimer::start("timing.closed.rows_ms");
717 let mut parameters = crate::blend::stations::closed_station_parameters(&stations, [&first_mate.face.surface, &second_mate.face.surface]);
718 let exact_rows = analytic_rows.map(Ok).or_else(|| exact_closed_revolution_rows(&stations, &first_mate, &second_mate, chamfer));
719 crate::blend::record_count(
720 if exact_rows.is_some() { "count.rows.exact_revolution" } else { "count.rows.fitted_closed" },
721 1,
722 );
723 let rows = match exact_rows {
724 Some(rows) => rows?,
725 None => {
726 let fit_timing = crate::blend::BlendTimer::start("timing.closed.rows.fit_ms");
736 let mut rows = fit_closed_rows(&stations, ¶meters, chamfer)?;
737 fit_timing.stop();
738 if !carve_fold && !chamfer {
746 if let Err(error) = crate::blend::fold::check_rows_loop(
747 &rows, &stations, ¶meters, None, radius_at(mid_t).abs(), vec![first_face.id, second_face.id], crate::solid_scale(solid),
748 ) {
749 crate::blend::carve::carve_trace(format_args!(
750 "blend closed-edge: the fitted rows loop ({error}); re-marching under Carve"
751 ));
752 stations = climb_carve_ladder(&error)?;
753 parameters = crate::blend::stations::closed_station_parameters(&stations, [&first_mate.face.surface, &second_mate.face.surface]);
754 rows = fit_closed_rows(&stations, ¶meters, chamfer)?;
755 carve_fold = true;
756 }
757 }
758 let read_first = crate::blend::closed_trim_read_first_on();
762 let refit_timing = crate::blend::BlendTimer::start("timing.closed.rows.refit_first_ms");
763 rows.cr_pcurve = if read_first {
764 closed_support_trim(&rows.cr, &rows.cr_pcurve, &first_mate.face.surface)?
765 } else {
766 refit_closed_support_pcurve(&rows.cr, &rows.cr_pcurve, &first_mate.face.surface)?
767 };
768 refit_timing.stop();
769 let refit_timing = crate::blend::BlendTimer::start("timing.closed.rows.refit_second_ms");
770 rows.cs_pcurve = if read_first {
771 closed_support_trim(&rows.cs, &rows.cs_pcurve, &second_mate.face.surface)?
772 } else {
773 refit_closed_support_pcurve(&rows.cs, &rows.cs_pcurve, &second_mate.face.surface)?
774 };
775 refit_timing.stop();
776 rows
777 }
778 };
779 rows_timing.stop();
780 let carve_timing = crate::blend::BlendTimer::start("timing.closed.carve_ms");
781 let crease = if carve_fold {
782 Some(carve_closed_wall(solid, &rows, &stations, ¶meters, &first_mate, &second_mate, radius_at(mid_t).abs())?)
783 } else {
784 None
785 };
786 carve_timing.stop();
787 let surgery_timing = crate::blend::BlendTimer::start("timing.closed.surgery_ms");
788
789 let second_split = if second_seam {
793 let seam2 = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?[0];
794 let period2 = {
795 let [d0, d1] = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
796 d1 - d0
797 };
798 let u2_first = stations[0].uv2[0];
800 let u2_last = stations[stations.len() - 1].uv2[0];
801 let direction = (u2_last - u2_first).signum();
802 let mut k = ((u2_first - seam2) / period2).ceil();
803 if direction < 0.0 {
804 k = ((u2_first - seam2) / period2).floor();
805 }
806 let target = seam2 + k * period2;
807 let inside =
808 (target - u2_first) * direction > 1e-6 && (u2_last - target) * direction > 1e-6;
809 let aligned = (target - u2_first).abs() <= 1e-6
814 || (target - u2_last).abs() <= 1e-6
815 || ((u2_first - seam2) / period2).fract().abs() <= 1e-9;
816 if !inside && aligned {
817 None
818 } else if !inside {
819 return Err(KernelRefusal::internal(KernelStage::Refine, "seam_crossing_bracket", "blend: second seam crossing not bracketed by the march"));
820 } else {
821 let [fit_low, fit_high] = rows.u_domain;
825 let mut p = fit_low
826 + (fit_high - fit_low) * {
827 let mut seed = 0.5;
829 for pair in 0..stations.len() - 1 {
830 let a = stations[pair].uv2[0];
831 let b = stations[pair + 1].uv2[0];
832 if (target - a) * (target - b) <= 0.0 {
833 let local = (target - a) / (b - a);
834 seed = (parameters[pair]
835 + (parameters[pair + 1] - parameters[pair]) * local)
836 .clamp(0.0, 1.0);
837 break;
838 }
839 }
840 seed
841 };
842 for _ in 0..NEWTON_ITERATIONS {
843 let value = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
844 if value.abs() <= 1e-12 {
845 break;
846 }
847 let step = 1e-8;
848 let probed = rows.cs_pcurve.evaluate(p + step).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
849 let derivative = (probed - value) / step;
850 if derivative.abs() <= 1e-14 {
851 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "seam_crossing_newton", "blend: second seam crossing Newton stalled"));
852 }
853 p -= value / derivative;
854 }
855 let crossing_v = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.y;
856 Some(SecondSeamSplit {
857 fit_parameter: p,
858 crossing_v,
859 period: period2 * direction,
860 })
861 }
862 } else {
863 None
864 };
865
866 let model_unmet = if carve_fold { None } else { model_unmet };
870 let wall_surface = model_unmet.map(|_| rows.surface.clone());
871 let built = build_surgery(
872 solid,
873 edge,
874 &first_mate,
875 &second_mate,
876 rows,
877 second_split,
878 name,
879 crease,
880 )?;
881 surgery_timing.stop();
882 if let (Some((declared, reason, round, stations, ceiling)), Some(surface)) = (model_unmet, wall_surface) {
883 crate::blend::record_wall_model_report(crate::blend::WallModelReport {
884 name: name.map(str::to_string),
885 surface,
886 request: crate::KernelTolerances::for_solid(solid, 1e-7).model,
887 residual: declared,
888 detail: request_rails.map(|(rails, unread, wall)| {
891 format!("last request read: rails {rails:.3e} off their carriers at j/16 ({unread} interval(s) unread), wall {wall:.3e}")
892 }),
893 budget: crate::ApproximationBudget {
894 reason,
895 rounds_used: round,
896 rounds_limit: crate::blend::stations::REFINE_ROUNDS,
897 stations,
898 station_limit: ceiling,
899 mechanism: crate::BudgetMechanism::LocalRounds,
900 measured_component: match request_rails {
903 Some((rails, _, wall)) if rails > wall => crate::MeasuredComponent::Rails,
904 _ => crate::MeasuredComponent::Wall,
905 },
906 unread: request_rails.map_or(0, |(_, unread, _)| unread),
907 },
908 });
909 }
910 Ok(built)
911}
912
913fn separate_spherical_domain_cut(
919 solid: &BrepSolid,
920 edge_id: u64,
921) -> Result<Option<BrepSolid>, KernelRefusal> {
922 let Some(edge) = solid.edges.iter().find(|edge| edge.id == edge_id) else {
923 return Ok(None);
924 };
925 if edge.start_vertex_id != edge.end_vertex_id {
926 return Ok(None);
927 }
928 let mut result = solid.clone();
929 let mut take_id = crate::blend::edge::fresh_id_source(solid);
930 let mut separated = false;
931 for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
932 let Some(atlas) = crate::sphere_chart::SphereAtlas::of_surface(&face.surface) else {
933 continue;
934 };
935 let [u0, u1] = face
936 .surface
937 .domain_u()
938 .or_refuse(KernelStage::Collect, "domain_cut_u")?;
939 let [v0, v1] = face
940 .surface
941 .domain_v()
942 .or_refuse(KernelStage::Collect, "domain_cut_v")?;
943 if [u0, u1, v0, v1] != [0.0, 1.0, 0.0, 1.0] {
944 continue;
945 }
946 if face
947 .surface
948 .closed_directions()
949 .or_refuse(KernelStage::Collect, "domain_cut_closed")?
950 != (true, false)
951 {
952 continue;
953 }
954 let band = 1e-8 * atlas.radius;
955 let bottom = face
956 .surface
957 .evaluate(u0, v0)
958 .or_refuse(KernelStage::Collect, "domain_cut_bottom")?;
959 let top = face
960 .surface
961 .evaluate(u0, v1)
962 .or_refuse(KernelStage::Collect, "domain_cut_top")?;
963 if bottom.add(top).scale(0.5).sub(atlas.centre).length() > band
964 || (bottom.sub(top).length() - 2.0 * atlas.radius).abs() > band
965 {
966 continue;
967 }
968 let mut additions = Vec::new();
969 let mut cut_ids = std::collections::HashSet::new();
970 for rim in &mut face.loops {
971 if rim.coedges.len() < 5 {
972 continue;
973 }
974 let positions: Vec<_> = rim
975 .coedges
976 .iter()
977 .enumerate()
978 .filter_map(|(i, c)| (c.edge_id == edge_id).then_some(i))
979 .collect();
980 if positions.len() != 1 {
981 continue;
982 }
983 let position = positions[0];
984 let ring = &rim.coedges[position];
985 let [a, b] = ring
986 .pcurve
987 .domain()
988 .or_refuse(KernelStage::Collect, "domain_cut_ring")?;
989 if ring
990 .pcurve
991 .evaluate(a)
992 .or_refuse(KernelStage::Collect, "domain_cut_ring_start")?
993 .sub(
994 ring.pcurve
995 .evaluate(b)
996 .or_refuse(KernelStage::Collect, "domain_cut_ring_end")?,
997 )
998 .length()
999 > 1e-7
1000 {
1001 continue;
1002 }
1003 let rest: Vec<_> = rim
1004 .coedges
1005 .iter()
1006 .filter(|c| c.edge_id != edge_id)
1007 .collect();
1008 let mut points = Vec::new();
1009 let mut domain_cut = true;
1010 for coedge in &rest {
1011 let [a, b] = coedge
1012 .pcurve
1013 .domain()
1014 .or_refuse(KernelStage::Collect, "domain_cut_trim")?;
1015 let start = coedge
1016 .pcurve
1017 .evaluate(a)
1018 .or_refuse(KernelStage::Collect, "domain_cut_start")?;
1019 let end = coedge
1020 .pcurve
1021 .evaluate(b)
1022 .or_refuse(KernelStage::Collect, "domain_cut_end")?;
1023 points.push(start);
1024 let degenerate = solid
1025 .edges
1026 .iter()
1027 .any(|e| e.id == coedge.edge_id && e.degenerate);
1028 let at = |x: f64, low: f64, high: f64| {
1029 (x - low).abs() <= 1e-8 || (x - high).abs() <= 1e-8
1030 };
1031 let on_boundary = if degenerate {
1032 (start.y - end.y).abs() <= 1e-8
1033 && at(start.y, v0, v1)
1034 && coedge
1035 .pcurve
1036 .control_points
1037 .iter()
1038 .all(|p| (p.y / p.w - start.y).abs() <= 1e-8)
1039 } else {
1040 at(start.x, u0, u1)
1041 && (start.x - end.x).abs() <= 1e-8
1042 && coedge
1043 .pcurve
1044 .control_points
1045 .iter()
1046 .all(|p| (p.x / p.w - start.x).abs() <= 1e-8)
1047 && rest.iter().filter(|c| c.edge_id == coedge.edge_id).count() == 2
1048 && rest
1049 .iter()
1050 .any(|c| c.edge_id == coedge.edge_id && c.forward != coedge.forward)
1051 };
1052 domain_cut &= on_boundary;
1053 }
1054 let twice_area: f64 = (0..points.len())
1055 .map(|i| {
1056 let a = points[i];
1057 let b = points[(i + 1) % points.len()];
1058 a.x * b.y - a.y * b.x
1059 })
1060 .sum();
1061 let expected = (u1 - u0) * (v1 - v0) * if face.same_sense { 1.0 } else { -1.0 };
1062 if !domain_cut || (0.5 * twice_area - expected).abs() > 1e-7 * expected.abs() {
1063 continue;
1064 }
1065 let cuts: std::collections::HashSet<_> = rest
1069 .iter()
1070 .filter_map(|c| {
1071 solid
1072 .edges
1073 .iter()
1074 .find(|e| e.id == c.edge_id && !e.degenerate)
1075 .map(|e| e.id)
1076 })
1077 .collect();
1078 for cut in solid.edges.iter().filter(|e| cuts.contains(&e.id)) {
1079 let users = solid
1080 .shells
1081 .iter()
1082 .flat_map(|s| &s.faces)
1083 .flat_map(|f| &f.loops)
1084 .flat_map(|l| &l.coedges)
1085 .filter(|c| c.edge_id == cut.id)
1086 .count();
1087 let exclusive_vertices = solid
1088 .edges
1089 .iter()
1090 .filter(|e| {
1091 e.start_vertex_id == cut.start_vertex_id
1092 || e.end_vertex_id == cut.start_vertex_id
1093 || e.start_vertex_id == cut.end_vertex_id
1094 || e.end_vertex_id == cut.end_vertex_id
1095 })
1096 .all(|e| e.degenerate || cuts.contains(&e.id) || e.id == edge_id);
1097 let mut same_parameter = cut.t0 >= v0 && cut.t1 <= v1 && cut.t0 < cut.t1;
1098 for i in 0..=16 {
1099 let t = cut.t0 + (cut.t1 - cut.t0) * i as f64 / 16.0;
1100 let point = cut
1101 .curve
1102 .evaluate(t)
1103 .or_refuse(KernelStage::Collect, "domain_cut_curve")?;
1104 let carrier = face
1105 .surface
1106 .evaluate(u0, t)
1107 .or_refuse(KernelStage::Collect, "domain_cut_carrier")?;
1108 same_parameter &= point.sub(carrier).length() <= band;
1109 }
1110 domain_cut &= users == 2 && exclusive_vertices && same_parameter;
1111 }
1112 if !domain_cut {
1113 continue;
1114 }
1115 cut_ids.extend(cuts);
1116 additions.push(LoopRecord {
1117 id: take_id(),
1118 coedges: vec![rim.coedges.remove(position)],
1119 });
1120 separated = true;
1121 }
1122 if !additions.is_empty() {
1123 let real = |id: u64| {
1127 !cut_ids.contains(&id) && !solid.edges.iter().any(|e| e.id == id && e.degenerate)
1128 };
1129 let curves: Vec<_> = face
1130 .loops
1131 .iter()
1132 .flat_map(|l| &l.coedges)
1133 .chain(additions.iter().flat_map(|l| &l.coedges))
1134 .filter(|c| real(c.edge_id) || c.edge_id == edge_id)
1135 .map(|c| &c.pcurve)
1136 .collect();
1137 let mut candidates = Vec::new();
1138 for shift in [0.25, 0.5, 0.75] {
1139 let mut clearance = f64::INFINITY;
1140 let mut fits = true;
1141 for curve in &curves {
1142 let first = curve.control_points[0].x / curve.control_points[0].w;
1143 let offset = -(first - shift).floor();
1144 for p in &curve.control_points {
1145 let u = p.x / p.w - shift + offset;
1146 fits &= u >= 0.0 && u <= 1.0;
1147 clearance = clearance.min(u.min(1.0 - u));
1148 }
1149 }
1150 if fits {
1151 candidates.push((clearance, shift));
1152 }
1153 }
1154 let Some((_, shift)) = candidates.into_iter().max_by(|a, b| a.0.total_cmp(&b.0)) else {
1155 return Err(KernelRefusal::unsupported(
1156 KernelStage::Collect,
1157 "sphere_domain_cut",
1158 "blend: no quarter-turn sphere chart keeps the real trims in-domain",
1159 ));
1160 };
1161 let seam_direction = face
1162 .surface
1163 .evaluate(shift, 0.5)
1164 .or_refuse(KernelStage::Collect, "sphere_cut_direction")?
1165 .sub(atlas.centre);
1166 let surface = crate::make_sphere_surface_framed(
1167 atlas.centre,
1168 atlas.radius,
1169 atlas.basis[2],
1170 Some(seam_direction),
1171 )
1172 .or_refuse(KernelStage::Collect, "sphere_cut_surface")?;
1173 for i in 0..=16 {
1175 for v in [0.0, 0.3, 0.5, 0.7, 1.0] {
1176 let u = i as f64 / 16.0;
1177 let old = face
1178 .surface
1179 .evaluate(u, v)
1180 .or_refuse(KernelStage::Collect, "sphere_cut_old")?;
1181 let new = surface
1182 .evaluate((u - shift).rem_euclid(1.0), v)
1183 .or_refuse(KernelStage::Collect, "sphere_cut_new")?;
1184 if old.sub(new).length() > band {
1185 return Err(KernelRefusal::unsupported(
1186 KernelStage::Collect,
1187 "sphere_cut_chart",
1188 "blend: the sphere quarter turn is not the same carrier",
1189 ));
1190 }
1191 }
1192 }
1193 let seam = surface
1194 .iso_curve_u(0.0)
1195 .or_refuse(KernelStage::Collect, "sphere_cut_seam")?;
1196 for coedge in face
1197 .loops
1198 .iter_mut()
1199 .flat_map(|l| &mut l.coedges)
1200 .chain(additions.iter_mut().flat_map(|l| &mut l.coedges))
1201 {
1202 if solid
1203 .edges
1204 .iter()
1205 .any(|e| e.id == coedge.edge_id && e.degenerate)
1206 {
1207 continue;
1208 }
1209 if !cut_ids.contains(&coedge.edge_id) {
1210 let first =
1211 coedge.pcurve.control_points[0].x / coedge.pcurve.control_points[0].w;
1212 let offset = -(first - shift).floor() - shift;
1213 for p in &mut coedge.pcurve.control_points {
1214 p.x += offset * p.w;
1215 }
1216 }
1217 }
1218 for cut in result.edges.iter_mut().filter(|e| cut_ids.contains(&e.id)) {
1219 for (vertex_id, t) in [(cut.start_vertex_id, cut.t0), (cut.end_vertex_id, cut.t1)] {
1220 let point = seam
1221 .evaluate(t)
1222 .or_refuse(KernelStage::Collect, "sphere_cut_vertex")?;
1223 let vertex = result
1224 .vertices
1225 .iter_mut()
1226 .find(|v| v.id == vertex_id)
1227 .ok_or(KernelRefusal::internal(
1228 KernelStage::Collect,
1229 "sphere_cut_vertex",
1230 "blend: sphere seam vertex is missing",
1231 ))?;
1232 vertex.point = point;
1233 }
1234 cut.curve = seam.clone();
1235 }
1236 face.surface = surface;
1237 }
1238 face.loops.extend(additions);
1239 }
1240 if !separated {
1241 return Ok(None);
1242 }
1243 let old_vertex = solid
1244 .vertices
1245 .iter()
1246 .find(|v| v.id == edge.start_vertex_id)
1247 .ok_or(KernelRefusal::internal(
1248 KernelStage::Collect,
1249 "domain_cut_vertex",
1250 "blend: closed ring vertex is missing",
1251 ))?;
1252 let vertex_id = take_id();
1253 result.vertices.push(VertexRecord {
1254 id: vertex_id,
1255 point: old_vertex.point,
1256 });
1257 let ring = result
1258 .edges
1259 .iter_mut()
1260 .find(|e| e.id == edge_id)
1261 .ok_or(KernelRefusal::internal(
1262 KernelStage::Collect,
1263 "domain_cut_ring",
1264 "blend: closed ring edge is missing",
1265 ))?;
1266 ring.start_vertex_id = vertex_id;
1267 ring.end_vertex_id = vertex_id;
1268 Ok(Some(result))
1269}
1270
1271fn remove_retraced_cuts(
1277 solid: &BrepSolid,
1278 edge_id: u64,
1279) -> Result<Option<BrepSolid>, KernelRefusal> {
1280 let mut result = solid.clone();
1281 let mut removed = Vec::new();
1282 for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
1283 for rim in &mut face.loops {
1284 if !rim.coedges.iter().any(|coedge| coedge.edge_id == edge_id) {
1285 continue;
1286 }
1287 loop {
1288 let n = rim.coedges.len();
1289 let mut pair = None;
1290 for i in 0..n {
1291 let j = (i + 1) % n;
1292 let (a, b) = (&rim.coedges[i], &rim.coedges[j]);
1293 if a.edge_id == edge_id || a.edge_id != b.edge_id || a.forward == b.forward {
1294 continue;
1295 }
1296 let users = solid
1297 .shells
1298 .iter()
1299 .flat_map(|s| &s.faces)
1300 .flat_map(|f| &f.loops)
1301 .flat_map(|l| &l.coedges)
1302 .filter(|c| c.edge_id == a.edge_id)
1303 .count();
1304 if users != 2 {
1305 continue;
1306 }
1307 let [a0, a1] = a
1308 .pcurve
1309 .domain()
1310 .or_refuse(KernelStage::Collect, "cut_domain")?;
1311 let [b0, b1] = b
1312 .pcurve
1313 .domain()
1314 .or_refuse(KernelStage::Collect, "cut_domain")?;
1315 let agrees =
1318 a.pcurve.degree == b.pcurve.degree
1319 && a.pcurve.knots.len() == b.pcurve.knots.len()
1320 && a.pcurve.control_points.len() == b.pcurve.control_points.len()
1321 && a.pcurve.knots.iter().zip(b.pcurve.knots.iter().rev()).all(
1322 |(x, y)| {
1323 ((x - a0) / (a1 - a0) - (b1 - y) / (b1 - b0)).abs() <= 1e-9
1324 },
1325 )
1326 && a.pcurve
1327 .control_points
1328 .iter()
1329 .zip(b.pcurve.control_points.iter().rev())
1330 .all(|(p, q)| {
1331 p.point()
1332 .ok()
1333 .zip(q.point().ok())
1334 .is_some_and(|(p, q)| p.sub(q).length() <= 1e-9)
1335 && (p.w / a.pcurve.control_points[0].w
1336 - q.w / b.pcurve.control_points.last().unwrap().w)
1337 .abs()
1338 <= 1e-9
1339 });
1340 if agrees {
1341 pair = Some((i, j, a.edge_id));
1342 break;
1343 }
1344 }
1345 let Some((i, j, id)) = pair else {
1346 break;
1347 };
1348 rim.coedges.remove(i.max(j));
1349 rim.coedges.remove(i.min(j));
1350 removed.push(id);
1351 }
1352 }
1353 }
1354 if removed.is_empty() {
1355 return Ok(None);
1356 }
1357 result.edges.retain(|edge| !removed.contains(&edge.id));
1358 super::open::prune_orphan_vertices(&mut result);
1359 Ok(Some(result))
1360}
1361
1362struct SecondSeamSplit {
1365 fit_parameter: f64,
1366 crossing_v: f64,
1367 period: f64,
1369}
1370
1371const CARVE_MAX_REFINEMENT_DEPTH: usize = MAX_REFINEMENT_DEPTH + 4;
1383
1384fn rails_off_closed_carriers(
1388 rows: &FittedRows,
1389 stations: &[Station],
1390 parameters: &[f64],
1391 first: &BlendMate,
1392 second: &BlendMate,
1393) -> Result<f64, KernelRefusal> {
1394 Ok(closed_rail_interval_misses(rows, stations, parameters,
1395 [&first.face.surface, &second.face.surface])?.into_iter().fold(0.0_f64, f64::max))
1396}
1397
1398fn carve_closed_wall(
1399 solid: &BrepSolid,
1400 rows: &FittedRows,
1401 stations: &[Station],
1402 parameters: &[f64],
1403 first: &BlendMate,
1404 second: &BlendMate,
1405 radius: f64,
1406) -> Result<crate::blend::carve::CarvedCrease, KernelRefusal> {
1407 let policy = crate::KernelTolerances::for_solid(solid, 1e-7);
1408 let rail_bar = 0.5 * policy.intersection_fit;
1409 let off = rails_off_closed_carriers(rows, stations, parameters, first, second)?;
1410 crate::blend::carve::carve_trace(format_args!(
1411 "blend closed-edge carve: {} stations, rails {off:.3e} off their carriers between stations (bar {rail_bar:.3e})",
1412 parameters.len()
1413 ));
1414 if off > rail_bar {
1415 return Err(crate::blend::fold::wall_fold_refusal(
1416 Vec::new(),
1417 format!(
1418 "{} {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",
1419 crate::blend::WALL_FOLDS
1420 ),
1421 ));
1422 }
1423 let Some(carved) = crate::blend::carve::trace_wall_crease(&rows.surface, radius)? else {
1424 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"));
1425 };
1426 let [u0, u1] = rows.u_domain;
1427 let margin = 1e-3 * (u1 - u0);
1428 let straddles = carved.ends.iter().any(|end| end[0] <= u0 + margin || end[0] >= u1 - margin);
1429 if straddles {
1430 return Err(crate::blend::fold::wall_fold_refusal(
1431 Vec::new(),
1432 format!(
1433 "{} {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",
1434 crate::blend::WALL_FOLDS, carved.ends[0][0], carved.ends[1][0]
1435 ),
1436 ));
1437 }
1438 crate::blend::carve::fit_crease(&rows.surface, &carved, policy.pcurve_consistency)
1439}
1440
1441fn closed_support_trim(rail: &NurbsCurve, old: &NurbsCurve, surface: &NurbsSurface) -> Result<NurbsCurve, KernelRefusal> {
1459 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1460 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1461 let within_floor = |candidate: &NurbsCurve, what: &str| -> bool {
1462 let _read_timing = crate::blend::BlendTimer::start("timing.closed.rows.trim_read_ms");
1463 let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), candidate, d0, d1);
1464 if std::env::var_os("BREP_DEBUG_TRACK_FIT").is_some() {
1465 match &read {
1466 Ok((miss, standoff, samples, worst)) => eprintln!(
1467 "TRACK_FIT closed-edge support, {what} candidate ({} control points): miss {miss:.3e} at t {worst:.9} ({samples} samples), rail standoff {standoff:.3e}, floor {floor:e}",
1468 candidate.control_points.len()),
1469 Err(error) => eprintln!("TRACK_FIT closed-edge support, {what} candidate: unreadable: {error}"),
1470 }
1471 }
1472 read.is_ok_and(|(miss, _, _, _)| miss <= floor)
1473 };
1474 if let Some(affine) = plane_affine_pcurve(rail, surface) {
1475 if within_floor(&affine, "affine plane image") {
1476 crate::blend::record_count("count.closed.rows.trim.affine", 1);
1477 return Ok(affine);
1478 }
1479 }
1480 crate::blend::record_count("count.closed.rows.trim.refit", 1);
1481 refit_closed_support_pcurve(rail, old, surface)
1482}
1483
1484fn plane_affine_pcurve(rail: &NurbsCurve, surface: &NurbsSurface) -> Option<NurbsCurve> {
1490 if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) {
1491 return None;
1492 }
1493 let [u0, u1] = surface.domain_u().ok()?;
1494 let [v0, v1] = surface.domain_v().ok()?;
1495 let origin = surface.evaluate(u0, v0).ok()?;
1496 let du = surface.evaluate(u1, v0).ok()?.sub(origin).scale(1.0 / (u1 - u0));
1497 let dv = surface.evaluate(u0, v1).ok()?.sub(origin).scale(1.0 / (v1 - v0));
1498 let (a, b, c) = (du.dot(du), du.dot(dv), dv.dot(dv));
1499 let det = a * c - b * b;
1500 let chart_scale = origin.length().max(du.length()).max(dv.length()).max(1.0);
1501 if !(det > 1e-24 * chart_scale.powi(4)) {
1502 return None;
1503 }
1504 let mut pcurve = rail.clone();
1505 for control in &mut pcurve.control_points {
1506 let weight = control.w;
1507 let offset = Vec3::new(control.x / weight, control.y / weight, control.z / weight).sub(origin);
1508 let (r, s) = (offset.dot(du), offset.dot(dv));
1509 control.x = (u0 + (c * r - b * s) / det) * weight;
1510 control.y = (v0 + (a * s - b * r) / det) * weight;
1511 control.z = 0.0;
1512 }
1513 Some(pcurve)
1514}
1515
1516pub(in crate::blend) fn refit_closed_support_pcurve(rail: &NurbsCurve, old: &NurbsCurve, surface: &NurbsSurface) -> Result<NurbsCurve, KernelRefusal> {
1541 let knot_retry = KNOT_RETRY.with(|flag| flag.get());
1542 refit_closed_support_pcurve_read_with(rail, old, surface, &mut crate::blend::track_fit::FootMemo::transient(), knot_retry).map(|(fitted, _)| fitted)
1543}
1544
1545thread_local! {
1546 static KNOT_RETRY: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1550}
1551
1552pub(in crate::blend) fn refit_closed_support_pcurve_read(
1560 rail: &NurbsCurve,
1561 old: &NurbsCurve,
1562 surface: &NurbsSurface,
1563 memo: &mut crate::blend::track_fit::FootMemo,
1564) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1565 let knot_retry = KNOT_RETRY.with(|flag| flag.get());
1568 refit_closed_support_pcurve_read_with(rail, old, surface, memo, knot_retry)
1569}
1570
1571pub fn with_pcurve_knot_retry<T>(build: impl FnOnce() -> T) -> T {
1575 let previous = KNOT_RETRY.with(|flag| flag.replace(true));
1576 let built = build();
1577 KNOT_RETRY.with(|flag| flag.set(previous));
1578 built
1579}
1580
1581fn refit_closed_support_pcurve_read_with(
1584 rail: &NurbsCurve,
1585 old: &NurbsCurve,
1586 surface: &NurbsSurface,
1587 memo: &mut crate::blend::track_fit::FootMemo,
1588 knot_retry: bool,
1589) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1590 const SHIPPED_READ_REPAIRS: usize = 3;
1591 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1592 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1593 let mut near: Vec<f64> = Vec::new();
1594 let (mut fitted, mut read) = fit_and_read_support(rail, old, surface, &near, memo, knot_retry)?;
1598 for _ in 0..SHIPPED_READ_REPAIRS {
1599 if read.0 <= floor || !(d1 > d0) {
1600 break;
1601 }
1602 near.push((read.3 - d0) / (d1 - d0));
1603 crate::blend::record_count("count.surgery.refit_repairs", 1);
1604 match fit_and_read_support(rail, old, surface, &near, memo, false) {
1607 Ok(repaired) => (fitted, read) = repaired,
1608 Err(_) => break,
1609 }
1610 }
1611 let (miss, standoff, samples, _) = read;
1612 if !(miss <= floor) {
1613 return Err(KernelRefusal::non_convergence(KernelStage::Refine, CLOSED_SUPPORT_PCURVE_FLOOR_WHAT, format!(
1614 "{} a blend's support pcurve, as shipped, misses its rail's track by {miss:.3e} at {samples} samples, \
1615 against a floor of {floor:.1e}",
1616 crate::blend::PCURVE_OFF_FLOOR,
1617 )));
1618 }
1619 Ok((fitted, read))
1620}
1621
1622fn fit_and_read_support(
1626 rail: &NurbsCurve,
1627 old: &NurbsCurve,
1628 surface: &NurbsSurface,
1629 near: &[f64],
1630 memo: &mut crate::blend::track_fit::FootMemo,
1631 knot_retry: bool,
1632) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1633 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1634 let guide = crate::blend::chain::pcurve_guide(old, surface)?;
1637 let track_timing = crate::blend::BlendTimer::start("timing.support_refit.track_ms");
1638 let mut fitted = if knot_retry {
1639 crate::blend::chain::project_piece_pcurve_within_knot_retry(rail, surface, false, false, Some(&guide), near)?
1640 } else {
1641 crate::blend::chain::project_piece_pcurve_within(rail, surface, false, false, Some(&guide), near)?
1642 };
1643 track_timing.stop();
1644 let [f0, f1] = fitted.domain().or_refuse(KernelStage::Refine, "domain")?;
1647 for knot in fitted.knots.iter_mut() {
1648 *knot = d0 + (*knot - f0) / (f1 - f0) * (d1 - d0);
1649 }
1650 let debug = std::env::var_os("BREP_DEBUG_TRACK_FIT").is_some();
1651 if debug {
1652 match crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &fitted, d0, d1) {
1655 Ok((miss, standoff, samples, worst)) => eprintln!(
1656 "TRACK_FIT closed-edge support: before the seam ends, miss {miss:.3e} at t {worst:.9} ({samples} samples), standoff {standoff:.3e}"),
1657 Err(error) => eprintln!("TRACK_FIT closed-edge support: before the seam ends, unreadable: {error}"),
1658 }
1659 }
1660 let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
1661 let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1662 let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1663 let old_start = old.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?;
1664 let old_end = old.evaluate(d1).or_refuse(KernelStage::Refine, "evaluate")?;
1665 let new_start = fitted.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?;
1666 let shift = |closed: bool, from: f64, to: f64, period: f64| if closed { ((to - from) / period).round() * period } else { 0.0 };
1668 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));
1669 for point in fitted.control_points.iter_mut() {
1670 point.x += shift_u * point.w;
1671 point.y += shift_v * point.w;
1672 }
1673 let on_seam = |closed: bool, value: f64, low: f64, high: f64| {
1674 let period = high - low;
1675 closed && ((value - low) / period - ((value - low) / period).round()).abs() * period <= 1e-12 * period.max(1.0)
1676 };
1677 let last = fitted.control_points.len() - 1;
1678 for (index, end) in [(0, old_start), (last, old_end)] {
1679 let point = &mut fitted.control_points[index];
1680 if on_seam(closed_u, end.x, u0, u1) {
1681 point.x = end.x * point.w;
1682 }
1683 if on_seam(closed_v, end.y, v0, v1) {
1684 point.y = end.y * point.w;
1685 }
1686 }
1687 let mut own = memo.empty_like();
1697 let (miss, standoff, samples, worst) =
1698 crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut own)?;
1699 *memo = own;
1700 if debug {
1701 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}");
1702 }
1703 Ok((fitted, (miss, standoff, samples, worst)))
1704}
1705
1706
1707fn support_exits_at(face: &FaceRecord, mut uv: [f64; 2]) -> bool {
1711 if let Ok((closed_u, closed_v)) = face.surface.closed_directions() {
1712 if closed_u {
1713 if let Ok([low, high]) = face.surface.domain_u() {
1714 uv[0] = low + (uv[0] - low).rem_euclid(high - low);
1715 }
1716 }
1717 if closed_v {
1718 if let Ok([low, high]) = face.surface.domain_v() {
1719 uv[1] = low + (uv[1] - low).rem_euclid(high - low);
1720 }
1721 }
1722 }
1723 crate::parameter_point_in_face(face, crate::Vec2 { x: uv[0], y: uv[1] }, 1e-6)
1724 .map(|class| class == crate::PolygonClass::Outside)
1725 .unwrap_or(true)
1726}
1727
1728fn support_exit_between_parallels(
1746 solid: &BrepSolid,
1747 face: &FaceRecord,
1748 (origin, axis): (Vec3, Vec3),
1749 contacts: &[Vec3],
1750 probe: &dyn Fn(usize) -> bool,
1751) -> Option<bool> {
1752 let radial = match face.surface.analytic()? {
1753 crate::AnalyticSurface::Plane { .. } => true,
1754 crate::AnalyticSurface::RuledRevolution { .. } => false,
1755 _ => return None,
1756 };
1757 if contacts.is_empty() {
1758 return None;
1759 }
1760 let axis = axis.normalized().ok()?;
1761 let coordinates = |point: Vec3| -> (f64, f64) {
1762 let offset = point.sub(origin);
1763 let height = offset.dot(axis);
1764 (offset.sub(axis.scale(height)).length(), height)
1765 };
1766 let meridian = |point: Vec3| -> f64 {
1767 let (radius, height) = coordinates(point);
1768 if radial { radius } else { height }
1769 };
1770 let scale = contacts.iter().map(|point| point.sub(origin).length()).fold(1.0, f64::max);
1771 let parallel_band = 1e-9 * scale;
1772 let margin = 1e-5 * scale;
1773 let mut boundaries: Vec<f64> = Vec::new();
1776 for loop_record in &face.loops {
1777 for coedge in &loop_record.coedges {
1778 if loop_record.coedges.iter().filter(|other| other.edge_id == coedge.edge_id).count() == 2 {
1779 continue;
1780 }
1781 let boundary = solid.edges.iter().find(|candidate| candidate.id == coedge.edge_id)?;
1782 if boundary.degenerate {
1783 continue;
1784 }
1785 let mut read: Option<(f64, f64)> = None;
1786 for step in 0..=8 {
1787 let point = boundary.curve.evaluate(boundary.t0 + (boundary.t1 - boundary.t0) * step as f64 / 8.0).ok()?;
1788 let here = coordinates(point);
1789 match read {
1790 None => read = Some(here),
1791 Some(first) => {
1792 if (here.0 - first.0).abs() > parallel_band || (here.1 - first.1).abs() > parallel_band {
1793 return None;
1794 }
1795 }
1796 }
1797 }
1798 let (radius, height) = read?;
1799 boundaries.push(if radial { radius } else { height });
1800 }
1801 }
1802 let readings: Vec<f64> = contacts.iter().map(|point| meridian(*point)).collect();
1804 let low = readings.iter().copied().fold(f64::INFINITY, f64::min);
1805 let high = readings.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1806 if !(low.is_finite() && high.is_finite()) {
1807 return None;
1808 }
1809 if boundaries.iter().any(|boundary| *boundary >= low - margin && *boundary <= high + margin) {
1810 return None;
1811 }
1812 Some(probe(0))
1813}
1814
1815fn build_surgery(
1816 solid: &BrepSolid,
1817 edge: &EdgeRecord,
1818 first: &BlendMate,
1819 second: &BlendMate,
1820 rows: FittedRows,
1821 second_split: Option<SecondSeamSplit>,
1822 name: Option<&str>,
1823 crease: Option<crate::blend::carve::CarvedCrease>,
1824) -> Result<BrepSolid, KernelRefusal> {
1825 let mut result = solid.clone();
1826 let mut take_id = crate::blend::edge::fresh_id_source(solid);
1827
1828 let [u_start, u_end] = rows.u_domain;
1829 let cr_start = rows.cr.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1830 let cs_start = rows.cs.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1831 let vertex1_id = take_id();
1832 let vertex2_id = take_id();
1833 result.vertices.push(VertexRecord {
1834 id: vertex1_id,
1835 point: cr_start,
1836 });
1837 result.vertices.push(VertexRecord {
1838 id: vertex2_id,
1839 point: cs_start,
1840 });
1841
1842 let cr_edge_id = take_id();
1843 result.edges.push(EdgeRecord {
1844 id: cr_edge_id,
1845 curve: rows.cr.clone(),
1846 t0: u_start,
1847 t1: u_end,
1848 start_vertex_id: vertex1_id,
1849 end_vertex_id: vertex1_id,
1850 degenerate: false,
1851 name: None,
1852 });
1853 let mut cs_pieces: Vec<(u64, [f64; 2], NurbsCurve)> = Vec::new();
1857 let mut second_seam_vertex = vertex2_id;
1858 let mut second_seam_point = cs_start;
1859 let mut second_crossing_v = 0.0;
1860 if let Some(split) = &second_split {
1861 let p = split.fit_parameter;
1862 second_crossing_v = split.crossing_v;
1863 let (cs_a, cs_b) = rows.cs.split(p).or_refuse(KernelStage::Refine, "split")?;
1864 let (pc_a, pc_b) = rows.cs_pcurve.split(p).or_refuse(KernelStage::Refine, "split")?;
1865 let mut pc_b = pc_b;
1867 for point in pc_b.control_points.iter_mut() {
1868 point.x -= split.period * point.w;
1869 }
1870 let w2s = take_id();
1871 second_seam_vertex = w2s;
1872 second_seam_point = rows.cs.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?;
1873 result.vertices.push(VertexRecord {
1874 id: w2s,
1875 point: second_seam_point,
1876 });
1877 let edge_a = take_id();
1878 result.edges.push(EdgeRecord {
1879 id: edge_a,
1880 curve: cs_a,
1881 t0: u_start,
1882 t1: p,
1883 start_vertex_id: vertex2_id,
1884 end_vertex_id: w2s,
1885 degenerate: false,
1886 name: None,
1887 });
1888 cs_pieces.push((edge_a, [u_start, p], pc_a));
1889 let edge_b = take_id();
1890 result.edges.push(EdgeRecord {
1891 id: edge_b,
1892 curve: cs_b,
1893 t0: p,
1894 t1: u_end,
1895 start_vertex_id: w2s,
1896 end_vertex_id: vertex2_id,
1897 degenerate: false,
1898 name: None,
1899 });
1900 cs_pieces.push((edge_b, [p, u_end], pc_b));
1901 } else {
1902 let cs_edge_id = take_id();
1903 result.edges.push(EdgeRecord {
1904 id: cs_edge_id,
1905 curve: rows.cs.clone(),
1906 t0: u_start,
1907 t1: u_end,
1908 start_vertex_id: vertex2_id,
1909 end_vertex_id: vertex2_id,
1910 degenerate: false,
1911 name: None,
1912 });
1913 cs_pieces.push((cs_edge_id, [u_start, u_end], rows.cs_pcurve.clone()));
1914 }
1915 let seam_curve = rows.surface.iso_curve_u(u_start).or_refuse(KernelStage::Refine, "iso_curve_u")?;
1916 let [seam_t0, seam_t1] = seam_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1917 let blend_seam_id = take_id();
1918 result.edges.push(EdgeRecord {
1919 id: blend_seam_id,
1920 curve: seam_curve,
1921 t0: seam_t0,
1922 t1: seam_t1,
1923 start_vertex_id: vertex1_id,
1924 end_vertex_id: vertex2_id,
1925 degenerate: false,
1926 name: None,
1927 });
1928
1929 let replace_in_face = |result: &mut BrepSolid, mate: &BlendMate, pieces: &[(u64, [f64; 2], NurbsCurve)], uv_new: [f64; 2]| -> Result<bool, KernelRefusal> {
1930 replace_rim_in_face(result, edge, mate, pieces, uv_new)
1931 };
1932 let uv1_new = rows.cr_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1933 let v1_new = uv1_new.y;
1934 let cr_pieces = vec![(cr_edge_id, [u_start, u_end], rows.cr_pcurve.clone())];
1935 SEAM_CONSUMED.with(|flag| flag.set(None));
1936 let mut repaired = replace_in_face(&mut result, first, &cr_pieces, [uv1_new.x, v1_new])?;
1937 let uv2_new = rows.cs_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1938 let v2_new = if second_split.is_some() {
1939 second_crossing_v
1940 } else {
1941 uv2_new.y
1942 };
1943 repaired |= replace_in_face(&mut result, second, &cs_pieces, [uv2_new.x, v2_new])?;
1944
1945 let collapse_winding_sphere_cap =
1951 |result: &mut BrepSolid, face_id: u64, support_ids: &[u64]| -> Result<(), KernelRefusal> {
1952 let face = result
1953 .shells
1954 .iter_mut()
1955 .flat_map(|shell| &mut shell.faces)
1956 .find(|face| face.id == face_id)
1957 .ok_or(KernelRefusal::internal(KernelStage::Sew, "sphere_carrier", "blend: sphere carrier lost during cap surgery"))?;
1958 if !matches!(
1959 face.surface.analytic(),
1960 Some(crate::AnalyticSurface::Sphere { .. })
1961 ) || face.surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")? != (true, false)
1962 {
1963 return Ok(());
1964 }
1965 let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1966 let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1967 let period = u1 - u0;
1968 let support_loop = face.loops.iter().position(|loop_record| {
1969 !loop_record.coedges.is_empty()
1970 && loop_record
1971 .coedges
1972 .iter()
1973 .all(|coedge| support_ids.contains(&coedge.edge_id))
1974 });
1975 let Some(support_loop) = support_loop else {
1976 return Ok(());
1977 };
1978 if face.loops[support_loop].coedges.len() != 1 {
1979 return Ok(());
1980 }
1981 let support = &face.loops[support_loop].coedges[0];
1982 let [s0, s1] = support.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1983 let support_start = support.pcurve.evaluate(s0).or_refuse(KernelStage::Refine, "evaluate")?;
1984 let support_end = support.pcurve.evaluate(s1).or_refuse(KernelStage::Refine, "evaluate")?;
1985 let support_winding = support_end.x - support_start.x;
1986 if (support_winding.abs() - period).abs() > 0.05 * period {
1987 return Ok(());
1988 }
1989
1990 let mut pole: Option<(usize, CoedgeRecord)> = None;
1991 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1992 if loop_index == support_loop {
1993 continue;
1994 }
1995 for coedge in &loop_record.coedges {
1996 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1997 let start = coedge.pcurve.evaluate(p0).or_refuse(KernelStage::Refine, "evaluate")?;
1998 let end = coedge.pcurve.evaluate(p1).or_refuse(KernelStage::Refine, "evaluate")?;
1999 let winding = end.x - start.x;
2000 let at_pole = (start.y - v0).abs() <= 1e-8 || (start.y - v1).abs() <= 1e-8;
2001 if at_pole
2002 && (end.y - start.y).abs() <= 1e-8
2003 && (winding.abs() - period).abs() <= 0.05 * period
2004 && winding * support_winding < 0.0
2005 {
2006 pole = Some((loop_index, coedge.clone()));
2007 break;
2008 }
2009 }
2010 if pole.is_some() {
2011 break;
2012 }
2013 }
2014 let Some((pole_loop, pole_coedge)) = pole else {
2015 return Ok(());
2016 };
2017 let support_record = face.loops[support_loop].clone();
2018 let pole_id = face.loops[pole_loop].id;
2019 face.loops = vec![
2020 LoopRecord {
2021 id: pole_id,
2022 coedges: vec![pole_coedge],
2023 },
2024 support_record,
2025 ];
2026 Ok(())
2027 };
2028 collapse_winding_sphere_cap(&mut result, first.face.id, &[cr_edge_id])?;
2029 let cs_edge_ids: Vec<u64> = cs_pieces.iter().map(|(edge_id, ..)| *edge_id).collect();
2030 collapse_winding_sphere_cap(&mut result, second.face.id, &cs_edge_ids)?;
2031 for shell in &mut result.shells {
2033 for face in &mut shell.faces {
2034 for loop_record in &mut face.loops {
2035 for coedge in &mut loop_record.coedges {
2036 if coedge.id == 0 {
2037 coedge.id = take_id();
2038 }
2039 }
2040 }
2041 }
2042 }
2043
2044 let new_edge_ids: Vec<u64> = std::iter::once(cr_edge_id)
2049 .chain(cs_pieces.iter().map(|(edge_id, ..)| *edge_id))
2050 .chain(std::iter::once(blend_seam_id))
2051 .collect();
2052 let first_face_edges: Vec<u64> = first
2053 .face
2054 .loops
2055 .iter()
2056 .flat_map(|loop_record| loop_record.coedges.iter().map(|coedge| coedge.edge_id))
2057 .collect();
2058 repaired |= trim_seam_edges(
2059 &mut result,
2060 edge,
2061 &new_edge_ids,
2062 &first_face_edges,
2063 (vertex1_id, v1_new, cr_start),
2064 (second_seam_vertex, v2_new, second_seam_point),
2065 )?;
2066
2067 let mid_u = (u_start + u_end) * 0.5;
2069 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
2070 let station_uv = rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?;
2071 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
2072 let out1 = if first.face.same_sense {
2073 n1
2074 } else {
2075 n1.scale(-1.0)
2076 };
2077 let same_sense = if crate::blend::setback::active().is_some() {
2083 let uv2 = rows.cs_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?;
2084 let n2 = raw_normal(&second.face.surface, uv2.x, uv2.y)?;
2085 let out2 = if second.face.same_sense { n2 } else { n2.scale(-1.0) };
2086 let v_far = rows.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?[1];
2087 let far_normal = raw_normal(&rows.surface, mid_u, v_far)?;
2088 blend_normal.dot(out1) + far_normal.dot(out2) >= 0.0
2089 } else {
2090 blend_normal.dot(out1) >= 0.0
2091 };
2092 let loop_id = take_id();
2093 let mut coedges = vec![
2094 CoedgeRecord {
2095 id: take_id(),
2096 edge_id: cr_edge_id,
2097 forward: true,
2098 pcurve: crate::sweep_topology::parameter_line(u_start, 0.0, u_end, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
2099 },
2100 CoedgeRecord {
2101 id: take_id(),
2102 edge_id: blend_seam_id,
2103 forward: true,
2104 pcurve: crate::sweep_topology::parameter_line(u_end, 0.0, u_end, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
2105 },
2106 ];
2107 for (edge_id, window, _) in cs_pieces.iter().rev() {
2108 coedges.push(CoedgeRecord {
2109 id: take_id(),
2110 edge_id: *edge_id,
2111 forward: false,
2112 pcurve: crate::sweep_topology::parameter_line(window[1], 1.0, window[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
2113 });
2114 }
2115 coedges.push(CoedgeRecord {
2116 id: take_id(),
2117 edge_id: blend_seam_id,
2118 forward: false,
2119 pcurve: crate::sweep_topology::parameter_line(u_start, 1.0, u_start, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
2120 });
2121 if !same_sense {
2131 coedges.reverse();
2132 for coedge in &mut coedges {
2133 coedge.forward = !coedge.forward;
2134 coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
2135 }
2136 }
2137 let mut loops = vec![LoopRecord {
2138 id: loop_id,
2139 coedges,
2140 }];
2141 if let Some(crease) = crease {
2150 let start_vertex = take_id();
2151 let end_vertex = take_id();
2152 let crease_edge = take_id();
2153 result.vertices.push(VertexRecord { id: start_vertex, point: crease.start });
2154 result.vertices.push(VertexRecord { id: end_vertex, point: crease.end });
2155 let [crease_t0, crease_t1] = crease.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
2156 result.edges.push(EdgeRecord {
2157 id: crease_edge,
2158 curve: crease.curve,
2159 t0: crease_t0,
2160 t1: crease_t1,
2161 start_vertex_id: start_vertex,
2162 end_vertex_id: end_vertex,
2163 degenerate: false,
2164 name: name.map(|value| format!("{value}:CREASE")),
2165 });
2166 let outer = crate::blend::chain::signed_area(&loops[0].coedges)?;
2167 let forward_first = crate::blend::chain::signed_area(&[
2168 CoedgeRecord { id: 0, edge_id: crease_edge, forward: true, pcurve: crease.first.clone() },
2169 CoedgeRecord { id: 0, edge_id: crease_edge, forward: false, pcurve: crease.second.reversed().or_refuse(KernelStage::Refine, "reversed")? },
2170 ])?;
2171 let inner = if outer * forward_first < 0.0 {
2172 vec![
2173 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: true, pcurve: crease.first },
2174 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: false, pcurve: crease.second.reversed().or_refuse(KernelStage::Refine, "reversed")? },
2175 ]
2176 } else {
2177 vec![
2178 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: true, pcurve: crease.second },
2179 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: false, pcurve: crease.first.reversed().or_refuse(KernelStage::Refine, "reversed")? },
2180 ]
2181 };
2182 loops.push(LoopRecord { id: take_id(), coedges: inner });
2183 }
2184 let blend_face = FaceRecord {
2185 id: take_id(),
2186 surface: rows.surface,
2187 same_sense,
2188 loops,
2189 name: name.map(|value| value.to_string()),
2190 };
2191 let shell_index = result
2192 .shells
2193 .iter()
2194 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
2195 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
2196 result.shells[shell_index].faces.push(blend_face);
2197
2198 let used_edges: std::collections::HashSet<u64> = result
2199 .shells
2200 .iter()
2201 .flat_map(|shell| &shell.faces)
2202 .flat_map(|face| &face.loops)
2203 .flat_map(|loop_record| &loop_record.coedges)
2204 .map(|coedge| coedge.edge_id)
2205 .collect();
2206 result
2207 .edges
2208 .retain(|candidate| used_edges.contains(&candidate.id));
2209 let used_vertices: std::collections::HashSet<u64> = result
2210 .edges
2211 .iter()
2212 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
2213 .collect();
2214 result
2215 .vertices
2216 .retain(|candidate| used_vertices.contains(&candidate.id));
2217 let consumed = SEAM_CONSUMED.with(|flag| flag.take());
2224 if repaired {
2225 let problems = result.validate_memoised();
2226 if !problems.is_empty() {
2227 if let (Some(face_id), true) = (consumed, crate::blend::setback::setback_consumed_face_refusal_on()) {
2230 return Err(crate::blend::setback::unsupported(
2231 "setback_consumes_face",
2232 format!(
2233 "asymmetric chamfer: the setback on face {face_id} reaches that face's far edge, which \
2234 would consume the whole face — refusing"
2235 ),
2236 ));
2237 }
2238 return Err(KernelRefusal::internal(KernelStage::Validate, "neighbour_trim_closure", format!(
2239 "blend: the repaired neighbour trim did not close ({} issue(s), first: {})",
2240 problems.len(),
2241 problems
2242 .first()
2243 .map(|issue| issue.message.as_str())
2244 .unwrap_or("unknown")
2245 )));
2246 }
2247 }
2248 Ok(result)
2249}
2250
2251thread_local! {
2252 static SEAM_CONSUMED: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
2255}
2256
2257pub(in crate::blend) fn replace_rim_in_face(
2263 result: &mut BrepSolid,
2264 edge: &EdgeRecord,
2265 mate: &BlendMate,
2266 pieces: &[(u64, [f64; 2], NurbsCurve)],
2267 uv_new: [f64; 2],
2268) -> Result<bool, KernelRefusal> {
2269 let old_vertex = edge.start_vertex_id;
2270 let v_new = uv_new[1];
2271 let seam_edge_ends: Vec<(u64, bool, bool)> = result
2273 .edges
2274 .iter()
2275 .filter(|candidate| {
2276 candidate.id != edge.id
2277 && (candidate.start_vertex_id == old_vertex
2278 || candidate.end_vertex_id == old_vertex)
2279 })
2280 .map(|candidate| {
2281 (
2282 candidate.id,
2283 candidate.start_vertex_id == old_vertex,
2284 candidate.end_vertex_id == old_vertex,
2285 )
2286 })
2287 .collect();
2288 let seam_edge_ids: Vec<u64> = seam_edge_ends.iter().map(|(id, ..)| *id).collect();
2289 let pcurve_band = crate::KernelTolerances::for_solid(result, 1e-7).pcurve_consistency;
2290 let debug = std::env::var("BREP_DEBUG_NETWORK").is_ok();
2291 let face = result
2292 .shells
2293 .iter_mut()
2294 .flat_map(|shell| &mut shell.faces)
2295 .find(|face| face.id == mate.face.id)
2296 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
2297 let face_id = face.id;
2298 let FaceRecord { surface, loops, .. } = &mut *face;
2299 let loop_record = &mut loops[mate.loop_index];
2300 let position = loop_record
2301 .coedges
2302 .iter()
2303 .position(|coedge| coedge.edge_id == edge.id)
2304 .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
2305 let old_forward = loop_record.coedges[position].forward;
2306 let old_seam_v = {
2307 let pcurve = &loop_record.coedges[position].pcurve;
2308 let [d0, _] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2309 pcurve.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?.y
2310 };
2311 let mut replacements = Vec::new();
2314 let ordered: Vec<&(u64, [f64; 2], NurbsCurve)> = if old_forward {
2315 pieces.iter().collect()
2316 } else {
2317 pieces.iter().rev().collect()
2318 };
2319 for (index, (edge_id, _, pcurve)) in ordered.iter().enumerate() {
2320 replacements.push(CoedgeRecord {
2321 id: if index == 0 {
2322 loop_record.coedges[position].id
2323 } else {
2324 0 },
2326 edge_id: *edge_id,
2327 forward: old_forward,
2328 pcurve: if old_forward {
2329 pcurve.clone()
2330 } else {
2331 pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
2332 },
2333 });
2334 }
2335 loop_record
2336 .coedges
2337 .splice(position..=position, replacements);
2338 let mut repaired = false;
2339 if loop_record.coedges.len() > 1 {
2340 for coedge in &mut loop_record.coedges {
2346 if pieces
2347 .iter()
2348 .any(|(edge_id, ..)| *edge_id == coedge.edge_id)
2349 || !seam_edge_ids.contains(&coedge.edge_id)
2350 {
2351 continue;
2352 }
2353 let controls = &coedge.pcurve.control_points;
2354 let last_index = controls.len() - 1;
2355 let first_u = controls[0].x / controls[0].w;
2356 let last_u = controls[last_index].x / controls[last_index].w;
2357 let constant_u = (first_u - last_u).abs()
2358 <= 1e-9 * (1.0 + first_u.abs().max(last_u.abs()));
2359 let (target, target_uv, fraction) = if constant_u {
2362 let first_v = controls[0].y / controls[0].w;
2366 let last_v = controls[last_index].y / controls[last_index].w;
2367 let target = if (first_v - old_seam_v).abs() < (last_v - old_seam_v).abs() {
2368 0
2369 } else {
2370 last_index
2371 };
2372 let span = last_v - first_v;
2373 let fraction = if span.abs() > 0.0 { (v_new - first_v) / span } else { 0.0 };
2374 (target, [first_u, v_new], fraction)
2375 } else {
2376 let Some((_, start_is_old, end_is_old)) = seam_edge_ends
2386 .iter()
2387 .find(|(id, ..)| *id == coedge.edge_id)
2388 .copied()
2389 else {
2390 continue;
2391 };
2392 if start_is_old == end_is_old {
2393 return Err(KernelRefusal::ill_posed(KernelStage::Sew, "neighbour_trim_end", format!(
2394 "blend: neighbour edge {} meets the blended edge at both ends; \
2395 its trim end is ambiguous",
2396 coedge.edge_id
2397 )));
2398 }
2399 let target = if start_is_old == coedge.forward { 0 } else { last_index };
2402 let v_end = controls[target].y / controls[target].w;
2403 let span = last_u - first_u;
2404 let fraction = if span.abs() > 0.0 { (uv_new[0] - first_u) / span } else { 0.0 };
2405 (target, [uv_new[0], v_end], fraction)
2406 };
2407 let [q0, q1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2419 let target_point = surface
2420 .evaluate_extended(target_uv[0], target_uv[1])
2421 .or_refuse(KernelStage::Refine, "evaluate_extended")?;
2422 let seed = (q0 + (q1 - q0) * fraction).clamp(q0.min(q1), q0.max(q1));
2428 let q_split = trim_parameter_at_point(
2429 surface, &coedge.pcurve, seed, target_point, pcurve_band,
2430 )?;
2431 let interior = q_split > q0 + 1e-9 * (q1 - q0) && q_split < q1 - 1e-9 * (q1 - q0);
2432 if debug {
2433 eprintln!(
2434 "closed-edge seam trim: face {face_id} edge {} coedge {} ({}): {} at q {q_split:.9} of [{q0:.6}, {q1:.6}], {} control points",
2435 coedge.edge_id, coedge.id, if constant_u { "meridian, constant u" } else { "along u" },
2436 if interior { "SPLIT" } else { "EXTEND" }, controls.len()
2437 );
2438 }
2439 if interior {
2440 let (low, high) = coedge.pcurve.split(q_split).or_refuse(KernelStage::Refine, "split")?;
2441 coedge.pcurve = if target == 0 { high } else { low };
2442 } else {
2443 if coedge.pcurve.degree != 1 || controls.len() != 2 {
2448 return Err(KernelRefusal::unsupported(KernelStage::Sew, "neighbour_seam_extend_curved", format!(
2449 "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)",
2450 coedge.edge_id, coedge.pcurve.degree, controls.len()
2451 )));
2452 }
2453 let w = coedge.pcurve.control_points[target].w;
2454 coedge.pcurve.control_points[target] = crate::Vec4::from_point(
2455 Vec3::new(target_uv[0], target_uv[1], 0.0), w,
2456 );
2457 }
2458 if crate::blend::setback::active().is_some() {
2464 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2465 let (a, b) = (
2466 coedge.pcurve.evaluate(p0).or_refuse(KernelStage::Refine, "evaluate")?,
2467 coedge.pcurve.evaluate(p1).or_refuse(KernelStage::Refine, "evaluate")?,
2468 );
2469 if a.sub(b).length() <= 1e-12 * (1.0 + a.length()) {
2470 SEAM_CONSUMED.with(|flag| flag.set(Some(face_id)));
2471 }
2472 }
2473 repaired = true;
2474 }
2475 }
2476 Ok(repaired)
2477}
2478
2479pub(in crate::blend) fn trim_seam_edges(
2485 result: &mut BrepSolid,
2486 edge: &EdgeRecord,
2487 new_edge_ids: &[u64],
2488 first_face_edges: &[u64],
2489 first_target: (u64, f64, Vec3),
2490 second_target: (u64, f64, Vec3),
2491) -> Result<bool, KernelRefusal> {
2492 let old_vertex = edge.start_vertex_id;
2493 let mut repaired = false;
2494 let (vertex1_id, v1_new, cr_start) = first_target;
2495 let (second_seam_vertex, v2_new, second_seam_point) = second_target;
2496 for seam_edge in result.edges.iter_mut() {
2497 if new_edge_ids.contains(&seam_edge.id) {
2498 continue;
2499 }
2500 if seam_edge.start_vertex_id != old_vertex && seam_edge.end_vertex_id != old_vertex {
2501 continue;
2502 }
2503 let (target_vertex, target_v, target_point) = if first_face_edges.contains(&seam_edge.id) {
2504 (vertex1_id, v1_new, cr_start)
2505 } else {
2506 (second_seam_vertex, v2_new, second_seam_point)
2507 };
2508 let [c0, c1] = seam_edge.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
2509 let clamped = target_v.clamp(c0.min(c1), c0.max(c1));
2510 let band = 1e-6 * (1.0 + target_point.length());
2519 let split_parameter = if seam_edge.id == edge.id {
2520 clamped
2526 } else {
2527 match seam_edge.curve.evaluate(clamped) {
2528 Ok(point) if point.sub(target_point).length() <= band => clamped,
2529 _ => {
2530 repaired = true;
2531 let projection =
2532 crate::project_point_to_curve(&seam_edge.curve, target_point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
2533 if projection.distance > band {
2534 return Err(KernelRefusal::unsupported(KernelStage::Sew, "neighbour_off_support", format!(
2535 "blend: neighbour edge {} does not pass through the blend's support \
2536 start (off by {:.9}); the trim has no parameter to move to",
2537 seam_edge.id, projection.distance
2538 )));
2539 }
2540 projection.u.clamp(c0.min(c1), c0.max(c1))
2541 }
2542 }
2543 };
2544 if seam_edge.start_vertex_id == old_vertex {
2545 seam_edge.t0 = split_parameter;
2546 seam_edge.start_vertex_id = target_vertex;
2547 }
2548 if seam_edge.end_vertex_id == old_vertex {
2549 seam_edge.t1 = split_parameter;
2550 seam_edge.end_vertex_id = target_vertex;
2551 }
2552 }
2553 Ok(repaired)
2554}