1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use super::*;
3
4pub(crate) const CONCAVE_RIM_OVERRUN_WHAT: &str = "concave_rim_overrun";
8
9fn concave_rim_overrun(
20 solid: &BrepSolid,
21 edge: &EdgeRecord,
22 mates: [(&FaceRecord, usize); 2],
23 radius: f64,
24) -> Result<Option<KernelRefusal>, KernelRefusal> {
25 const RIM_SAMPLES: usize = 64;
26 const LOOP_SAMPLES: usize = 256;
27 if edge.start_vertex_id != edge.end_vertex_id {
28 return Ok(None);
29 }
30 for (index, (face, rim_loop)) in mates.iter().copied().enumerate() {
31 if rim_loop == 0 {
32 continue;
33 }
34 let Some(crate::AnalyticSurface::Plane { origin, u_dir, v_dir, .. }) = face.surface.analytic() else { continue };
35 let wall = mates[1 - index].0;
36 let e1 = u_dir.normalized().or_refuse(KernelStage::Classify, "normalized")?;
37 let normal = u_dir.cross(*v_dir).normalized().or_refuse(KernelStage::Classify, "normalized")?;
38 let e2 = normal.cross(e1);
39 let flat = |p: Vec3| -> (f64, f64) { let d = p.sub(*origin); (d.dot(e1), d.dot(e2)) };
40 let polygon = |loop_index: usize| -> Result<Vec<(f64, f64)>, KernelRefusal> {
42 let mut points: Vec<(f64, f64)> = Vec::new();
43 for coedge in &face.loops[loop_index].coedges {
44 let Some(boundary) = solid.edges.iter().find(|candidate| candidate.id == coedge.edge_id) else { continue };
45 for k in 0..LOOP_SAMPLES {
46 let f = k as f64 / LOOP_SAMPLES as f64;
47 let t = if coedge.forward { boundary.t0 + (boundary.t1 - boundary.t0) * f } else { boundary.t1 - (boundary.t1 - boundary.t0) * f };
48 points.push(flat(boundary.curve.evaluate(t).or_refuse(KernelStage::Classify, "evaluate")?));
49 }
50 }
51 Ok(points)
52 };
53 let (outer, rim_polygon) = (polygon(0)?, polygon(rim_loop)?);
54 let within = |points: &[(f64, f64)], x: f64, y: f64| -> bool {
55 crate::polygon::parity_inside([x, y], points.len(), |k| [points[k].0, points[k].1])
56 };
57 let inside = |x: f64, y: f64| -> bool { within(&outer, x, y) && !within(&rim_polygon, x, y) };
60 {
64 let derivatives = edge.curve.derivatives(edge.t0, 1).or_refuse(KernelStage::Classify, "derivatives")?;
65 let (point, tangent) = (derivatives[0], derivatives[1]);
66 let side = normal.cross(tangent).normalized().or_refuse(KernelStage::Classify, "normalized")?;
67 let probe = radius * 0.05;
70 let (px, py) = flat(point.add(side.scale(probe)));
71 let toward = if inside(px, py) { side } else { side.scale(-1.0) };
72 let foot = crate::project_point_to_surface(&wall.surface, point).or_refuse(KernelStage::Classify, "project_point_to_surface")?;
73 let wall_normal = wall.surface.normal(foot.u, foot.v).or_refuse(KernelStage::Classify, "normal")?;
74 let outward = if wall.same_sense { wall_normal } else { wall_normal.scale(-1.0) };
75 if outward.dot(toward) <= 0.0 {
76 continue;
77 }
78 }
79 let mut overruns = false;
80 for k in 0..RIM_SAMPLES {
81 let t = edge.t0 + (edge.t1 - edge.t0) * (k as f64 + 0.5) / RIM_SAMPLES as f64;
82 let derivatives = edge.curve.derivatives(t, 1).or_refuse(KernelStage::Classify, "derivatives")?;
83 let (point, tangent) = (derivatives[0], derivatives[1]);
84 let Ok(side) = normal.cross(tangent).normalized() else { continue };
85 let probe = radius * 0.05;
90 let toward = if inside(flat(point.add(side.scale(probe))).0, flat(point.add(side.scale(probe))).1) { side } else { side.scale(-1.0) };
91 let contact = flat(point.add(toward.scale(radius)));
92 if !within(&outer, contact.0, contact.1) {
93 overruns = true;
94 break;
95 }
96 }
97 if overruns {
98 return Ok(Some(KernelRefusal::unsupported(KernelStage::Classify, CONCAVE_RIM_OVERRUN_WHAT, format!(
99 "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",
100 edge.id,
101 face.name.clone().unwrap_or_else(|| format!("{}", face.id)),
102 ))));
103 }
104 }
105 Ok(None)
106}
107
108pub fn blend_closed_edge(
109 solid: &BrepSolid,
110 edge_id: u64,
111 radius: f64,
112 chamfer: bool,
113 name: Option<&str>,
114) -> Result<BrepSolid, KernelRefusal> {
115 let _operation = crate::blend::BlendOperation::enter();
118 if !(radius > 0.0) || !radius.is_finite() {
119 return Err(KernelRefusal::input(KernelStage::Collect, "radius", "blend: radius must be positive"));
120 }
121 blend_closed_edge_impl(solid, edge_id, &|_| radius, Some(radius), chamfer, name)
122}
123
124pub fn blend_closed_edge_knot_aware(
134 solid: &BrepSolid,
135 edge_id: u64,
136 radius: f64,
137 chamfer: bool,
138 name: Option<&str>,
139) -> Result<BrepSolid, KernelRefusal> {
140 let _operation = crate::blend::BlendOperation::enter();
141 if !(radius > 0.0) || !radius.is_finite() {
142 return Err(KernelRefusal::input(KernelStage::Collect, "radius", "blend: radius must be positive"));
143 }
144 KNOT_RETRY.with(|flag| flag.set(true));
145 let built = blend_closed_edge_impl(solid, edge_id, &|_| radius, Some(radius), chamfer, name);
146 KNOT_RETRY.with(|flag| flag.set(false));
147 built
148}
149
150pub fn is_pcurve_floor_refusal(refusal: &KernelRefusal) -> bool {
154 matches!(&refusal.class, crate::RefusalClass::NonConvergence { what }
155 if what == crate::blend::chain::CHAIN_PCURVE_FLOOR_WHAT || what == CLOSED_SUPPORT_PCURVE_FLOOR_WHAT)
156}
157
158pub(crate) const CLOSED_SUPPORT_PCURVE_FLOOR_WHAT: &str = "closed_support_pcurve_floor";
160
161pub fn blend_edge_variable(
165 solid: &BrepSolid,
166 edge_id: u64,
167 radii: &[(f64, f64)],
168 chamfer: bool,
169 name: Option<&str>,
170) -> Result<BrepSolid, KernelRefusal> {
171 let _operation = crate::blend::BlendOperation::enter();
174 if radii.is_empty() || radii.iter().any(|(_, radius)| !(*radius > 0.0)) {
175 return Err(KernelRefusal::input(KernelStage::Collect, "radius_stop", "blend: every radius stop must be positive"));
176 }
177 let mut stops = radii.to_vec();
178 stops.sort_by(|a, b| a.0.total_cmp(&b.0));
179 let edge = solid
180 .edges
181 .iter()
182 .find(|edge| edge.id == edge_id)
183 .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
184 let closed = edge.start_vertex_id == edge.end_vertex_id;
185 if closed && (stops[0].1 - stops[stops.len() - 1].1).abs() > 1e-12 {
186 return Err(KernelRefusal::input(KernelStage::Collect, "closed_edge_radii", "blend: closed edges need equal first/last radii"));
187 }
188 let first_radius = stops[0].1;
197 if stops
198 .iter()
199 .all(|(_, radius)| (radius - first_radius).abs() <= 1e-12 * (1.0 + first_radius.abs()))
200 {
201 return if chamfer {
202 crate::fillet::chamfer_edge(solid, edge_id, first_radius, name)
203 } else {
204 crate::fillet::fillet_edge(solid, edge_id, first_radius, name)
205 };
206 }
207 let (t0, t1) = (edge.t0, edge.t1);
208 let radius_at = move |t: f64| -> f64 {
209 let raw = (t - t0) / (t1 - t0);
210 let fraction = if closed {
218 raw.rem_euclid(1.0)
219 } else {
220 raw.clamp(0.0, 1.0)
221 };
222 if fraction <= stops[0].0 {
223 return stops[0].1;
224 }
225 for pair in stops.windows(2) {
226 if fraction <= pair[1].0 {
227 let width = (pair[1].0 - pair[0].0).max(1e-12);
228 let local = (fraction - pair[0].0) / width;
229 return pair[0].1 + (pair[1].1 - pair[0].1) * local;
230 }
231 }
232 stops[stops.len() - 1].1
233 };
234 if closed {
237 blend_closed_edge_impl(solid, edge_id, &radius_at, None, chamfer, name)
238 } else {
239 blend_open_edge_impl(solid, edge_id, &radius_at, None, chamfer, name)
240 }
241}
242
243fn blend_closed_edge_impl(
247 solid: &BrepSolid,
248 edge_id: u64,
249 radius_at: &dyn Fn(f64) -> f64,
250 constant: Option<f64>,
251 chamfer: bool,
252 name: Option<&str>,
253) -> Result<BrepSolid, KernelRefusal> {
254 let _closed_timing = crate::blend::BlendTimer::start("timing.closed_rim_ms");
255 let prepare_timing = crate::blend::BlendTimer::start("timing.closed.prepare_ms");
256 if let Some(separated) = separate_spherical_domain_cut(solid, edge_id)? {
257 return blend_closed_edge_impl(&separated, edge_id, radius_at, constant, chamfer, name);
258 }
259 if let Some(simplified) = remove_retraced_cuts(solid, edge_id)? {
260 return blend_closed_edge_impl(&simplified, edge_id, radius_at, constant, chamfer, name);
261 }
262 let edge = solid
263 .edges
264 .iter()
265 .find(|edge| edge.id == edge_id)
266 .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
267 if edge.start_vertex_id != edge.end_vertex_id {
268 return Err(KernelRefusal::input(KernelStage::Collect, "closed_edge", "blend: general path currently requires a CLOSED edge"));
269 }
270 let (face_a, loop_a, coedge_a) = locate_mate(solid, edge_id, None)?;
271 let (face_b, loop_b, coedge_b) = locate_mate(solid, edge_id, Some((face_a.id, loop_a)))?;
272 if face_a.id == face_b.id && loop_a == loop_b {
273 return Err(KernelRefusal::unsupported(KernelStage::Classify, "seam_edge", "blend: edge is used twice by one loop (seam edge?)"));
274 }
275
276 let seam_a = face_a.loops[loop_a].coedges.len() > 1;
279 let seam_b = face_b.loops[loop_b].coedges.len() > 1;
280 let (first_face, first_loop, first_coedge, second_face, second_loop, second_coedge) =
281 if seam_a || !seam_b {
282 (face_a, loop_a, coedge_a, face_b, loop_b, coedge_b)
283 } else {
284 (face_b, loop_b, coedge_b, face_a, loop_a, coedge_a)
285 };
286 let anchored = first_face.loops[first_loop].coedges.len() > 1;
287 let second_seam = anchored && second_face.loops[second_loop].coedges.len() > 1;
288 let anchor_u = if anchored {
289 Some(first_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?[0])
291 } else {
292 None
293 };
294
295 let mid_radius = radius_at(edge.t0 + (edge.t1 - edge.t0) * 0.5);
297 let (rho1, rho2) = signed_radii(
298 edge,
299 first_face,
300 first_coedge,
301 second_face,
302 second_coedge,
303 mid_radius,
304 )?;
305
306 let first_mate = BlendMate {
307 face: first_face,
308 coedge: first_coedge,
309 loop_index: first_loop,
310 rho: rho1,
311 };
312 let second_mate = BlendMate {
313 face: second_face,
314 coedge: second_coedge,
315 loop_index: second_loop,
316 rho: rho2,
317 };
318 let mid_t = edge.t0 + (edge.t1 - edge.t0) * 0.5;
319 use crate::blend::chain::FoldPolicy;
334 let march_with = |fold: FoldPolicy, depth_budget: usize| -> Result<Vec<Station>, KernelRefusal> {
335 match march_stations(edge, &first_mate, &second_mate, radius_at, anchor_u, fold, depth_budget) {
336 Ok(stations) => Ok(stations),
337 Err(anchored_error) if anchor_u.is_some() => {
338 march_stations(edge, &first_mate, &second_mate, radius_at, None, fold, depth_budget).map_err(
339 |retry_error| {
340 if crate::blend::is_wall_fold(&retry_error)
341 || crate::blend::is_marched_fit_off_carriers(&retry_error) {
342 retry_error
343 } else {
344 anchored_error
345 }
346 },
347 )
348 }
349 Err(anchored_error) => Err(anchored_error),
350 }
351 };
352 let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
353 let wall_check = {
357 let radius = constant.unwrap_or(f64::NAN);
360 let constant = constant.is_some();
361 let model = crate::KernelTolerances::for_solid(solid, 1e-7).model;
364 let bar = crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
365 (!chamfer && constant && radius.is_finite() && radius != 0.0).then(|| crate::blend::stations::ClosedWallCheck {
366 radius: radius.abs(),
367 bar,
368 model: model.is_finite().then_some(model),
369 })
370 };
371 crate::blend::stations::CLOSED_MODEL_UNMET.with(|slot| slot.set(None));
372 crate::blend::stations::CLOSED_REQUEST_RAILS.with(|slot| slot.set(None));
373 prepare_timing.stop();
374 let exact_revolution = crate::blend::exact_rim_lane_on()
375 && crate::blend::stations::closed_revolution_axis(&first_mate, &second_mate).is_some();
376 let depth_budget = if exact_revolution { 0 } else { MAX_REFINEMENT_DEPTH };
377 if exact_revolution {
378 crate::blend::record_count("count.closed.exact_lane", 1);
379 }
380 let march_timing = crate::blend::BlendTimer::start("timing.closed.march_ms");
381 let attempt_timing = crate::blend::BlendTimer::start("timing.closed.march.anchored_fallback_ms");
382 let setback = crate::blend::setback::active();
387 let analytic = if setback.is_some() {
388 None
389 } else {
390 crate::blend::stations::analytic_rim::construct(edge, &first_mate, &second_mate, constant, chamfer, anchor_u)
391 };
392 let setback_stations = match &setback {
393 Some(law) => Some(crate::blend::setback::march_closed(solid, edge, &first_mate, &second_mate, law, anchor_u, rail_bar)?),
394 None => None,
395 };
396 let (analytic_stations, analytic_rows) = match analytic {
397 Some((stations, rows)) => (Some(stations), Some(rows)),
398 None => (None, None),
399 };
400 let analytic_stations = setback_stations.or(analytic_stations);
401 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)) {
402 Ok(stations) => {
403 std::mem::forget(attempt_timing);
404 Ok(stations)
405 }
406 Err(failure) => {
407 let anchored_error = failure.into_initial()?;
410 if anchor_u.is_some() {
411 attempt_timing.stop();
412 crate::blend::record_count("count.closed.march.anchor_fallback", 1);
413 if std::env::var("BREP_DEBUG_CLOSED_RAILS").is_ok() {
414 eprintln!("closed initial anchor fallback: edge {}: {anchored_error}", edge.id);
415 }
416 match march_stations_with_rail_bar(edge, &first_mate, &second_mate, radius_at, None, FoldPolicy::Refuse, depth_budget, rail_bar, wall_check) {
417 Ok(stations) => Ok(stations),
418 Err(failure) => {
419 let retry_error = failure.into_initial()?;
420 if crate::blend::is_wall_fold(&retry_error) { Err(retry_error) }
421 else { Err(anchored_error) }
422 }
423 }
424 } else { Err(anchored_error) }
425 }
426 };
427 let model_unmet = crate::blend::stations::CLOSED_MODEL_UNMET.with(|slot| slot.take());
440 let request_rails = crate::blend::stations::CLOSED_REQUEST_RAILS.with(|slot| slot.take());
441 let mut carve_fold = false;
442 let climb_carve_ladder = |error: &KernelRefusal| -> Result<Vec<Station>, KernelRefusal> {
449 let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
450 let mut climbed: Option<Vec<Station>> = None;
451 let mut last_off = f64::NAN;
452 for depth_budget in MAX_REFINEMENT_DEPTH..=CARVE_MAX_REFINEMENT_DEPTH {
453 let stations = match march_with(FoldPolicy::Carve, depth_budget) {
454 Ok(stations) => stations,
455 Err(_) => break,
457 };
458 let parameters = crate::blend::stations::closed_station_parameters(&stations, [&first_mate.face.surface, &second_mate.face.surface]);
459 let off = match fit_closed_rows(&stations, ¶meters, chamfer) {
460 Ok(rows) => rails_off_closed_carriers(&rows, &stations, ¶meters, &first_mate, &second_mate)?,
461 Err(_) => break,
462 };
463 last_off = off;
464 crate::blend::carve::carve_trace(format_args!(
465 "blend closed-edge carve ladder: depth budget {depth_budget} ({} stations) leaves the rails {off:.3e} off their carriers (bar {rail_bar:.3e})",
466 stations.len()
467 ));
468 if off <= rail_bar {
469 climbed = Some(stations);
470 break;
471 }
472 }
473 climbed.ok_or_else(|| {
474 crate::blend::fold::wall_fold_refusal(
475 Vec::new(),
476 format!(
477 "{} {} 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})",
478 crate::blend::WALL_FOLDS,
479 radius_at(mid_t).abs()
480 ),
481 )
482 })
483 };
484 let marched = match marched {
485 Err(error)
486 if crate::blend::is_wall_fold(&error)
487 && !crate::blend::fold::is_rail_fold(&error)
488 && !chamfer =>
489 {
490 crate::blend::carve::carve_trace(format_args!(
491 "blend closed-edge: the wall folds ({error}); re-marching under Carve"
492 ));
493 match climb_carve_ladder(&error) {
494 Ok(stations) => {
495 carve_fold = true;
496 Ok(stations)
497 }
498 Err(refusal) => Err(refusal),
499 }
500 }
501 other => other,
502 };
503 let mut stations = match marched {
504 Ok(stations) => stations,
505 Err(march_error) if crate::blend::is_wall_fold(&march_error)
511 || crate::blend::is_marched_fit_off_carriers(&march_error) => return Err(march_error),
512 Err(march_error) if crate::blend::is_ball_off_carrier(&march_error) => {
513 return Err(march_error);
520 }
521 Err(march_error) => {
522 if let Some(refusal) = concave_rim_overrun(solid, edge, [(first_face, first_loop), (second_face, second_loop)], radius_at(mid_t))? {
525 return Err(refusal);
526 }
527 return blend_closed_edge_keep(
530 solid,
531 edge,
532 &first_mate,
533 &second_mate,
534 radius_at(mid_t),
535 rho1,
536 chamfer,
537 name,
538 )
539 .or_else(|_| {
540 blend_closed_edge_keep(
541 solid,
542 edge,
543 &second_mate,
544 &first_mate,
545 radius_at(mid_t),
546 rho2,
547 chamfer,
548 name,
549 )
550 })
551 .map_err(|keep_error| {
552 format!("{march_error}; edge-preserving blend also failed: {keep_error}")
553 }).or_refuse(KernelStage::Refine, "map_err");
554 }
555 };
556 let support_exits = |face: &FaceRecord, second_side: bool| -> bool {
560 let stride = (stations.len() / 16).max(1);
564 stations.iter().step_by(stride).any(|station| {
565 support_exits_at(face, if second_side { station.uv2 } else { station.uv1 })
566 })
567 };
568 let exits_timing = crate::blend::BlendTimer::start("timing.closed.march.exits_ms");
576 let revolution = crate::blend::stations::closed_revolution_axis(&first_mate, &second_mate);
580 let exit_of = |face: &FaceRecord, second_side: bool| -> bool {
581 let contacts: Vec<Vec3> = stations.iter().map(|station| if second_side { station.p2 } else { station.p1 }).collect();
582 let probe = |station: usize| -> bool {
583 support_exits_at(face, if second_side { stations[station].uv2 } else { stations[station].uv1 })
584 };
585 match revolution.and_then(|axis| support_exit_between_parallels(solid, face, axis, &contacts, &probe)) {
586 Some(exit) => {
587 crate::blend::record_count("count.closed.exits_by_parallels", 1);
588 exit
589 }
590 None => support_exits(face, second_side),
591 }
592 };
593 let exits = [exit_of(first_face, false), exit_of(second_face, true)];
594 exits_timing.stop();
595 if setback.is_some() && (exits[0] || exits[1]) {
598 return Err(crate::blend::setback::unsupported(
599 "setback_rail_leaves_face",
600 format!(
601 "asymmetric chamfer: the rail on face {} runs off that face",
602 if exits[0] { first_face.id } else { second_face.id }
603 ),
604 ));
605 }
606 if exits[0] || exits[1] {
607 let scale = crate::blend::stations::march_model_scale(&edge.curve, edge.t0, edge.t1, radius_at(mid_t))?;
608 match crate::blend::rollover::plan_closed(solid, edge, &stations, [&first_mate, &second_mate], radius_at, scale) {
609 Ok(Some(plan)) => {
610 if crate::blend::rollover_on() {
613 let pieces = crate::blend::rollover::march_pieces(
614 solid, edge, &plan, &stations, [&first_mate, &second_mate], radius_at, scale, rail_bar, chamfer, anchor_u,
615 )?;
616 let built = crate::blend::rollover::build(solid, edge, [&first_mate, &second_mate], &plan, pieces, anchor_u, name)?;
617 march_timing.stop();
618 return Ok(built);
619 }
620 return Err(crate::blend::rollover::refusal(&plan, radius_at(mid_t).abs()));
621 }
622 Ok(None) => {}
623 Err(error) => {
624 if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
625 eprintln!("closed-rim rollover scan did not read the exit: {error}");
626 }
627 }
628 }
629 }
630 if setback.is_none() {
631 if let Some(refusal) = concave_rim_overrun(solid, edge, [(first_face, first_loop), (second_face, second_loop)], radius_at(mid_t))? {
632 return Err(refusal);
633 }
634 }
635 if exits[1] {
636 return blend_closed_edge_keep(
637 solid,
638 edge,
639 &first_mate,
640 &second_mate,
641 radius_at(mid_t),
642 rho1,
643 chamfer,
644 name,
645 );
646 }
647 if exits[0] {
648 return blend_closed_edge_keep(
649 solid,
650 edge,
651 &second_mate,
652 &first_mate,
653 radius_at(mid_t),
654 rho2,
655 chamfer,
656 name,
657 );
658 }
659 if std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok() {
660 let last = stations.len() - 1;
661 for index in [
662 0usize,
663 1.min(last),
664 2.min(last),
665 last.saturating_sub(1),
666 last,
667 ] {
668 let station = &stations[index];
669 eprintln!(
670 "station {index}: uv1=({:.6},{:.6}) uv2=({:.6},{:.6}) p1=({:.6},{:.6},{:.6}) w={:.6}",
671 station.uv1[0], station.uv1[1], station.uv2[0], station.uv2[1],
672 station.p1.x, station.p1.y, station.p1.z, station.weight,
673 );
674 }
675 }
676 march_timing.stop();
677 crate::blend::record_count("count.closed.stations", stations.len() as u64);
678 let rows_timing = crate::blend::BlendTimer::start("timing.closed.rows_ms");
679 let mut parameters = crate::blend::stations::closed_station_parameters(&stations, [&first_mate.face.surface, &second_mate.face.surface]);
680 let exact_rows = analytic_rows.map(Ok).or_else(|| exact_closed_revolution_rows(&stations, &first_mate, &second_mate, chamfer));
681 crate::blend::record_count(
682 if exact_rows.is_some() { "count.rows.exact_revolution" } else { "count.rows.fitted_closed" },
683 1,
684 );
685 let rows = match exact_rows {
686 Some(rows) => rows?,
687 None => {
688 let fit_timing = crate::blend::BlendTimer::start("timing.closed.rows.fit_ms");
698 let mut rows = fit_closed_rows(&stations, ¶meters, chamfer)?;
699 fit_timing.stop();
700 if !carve_fold && !chamfer {
708 if let Err(error) = crate::blend::fold::check_rows_loop(
709 &rows, &stations, ¶meters, None, radius_at(mid_t).abs(), vec![first_face.id, second_face.id], crate::solid_scale(solid),
710 ) {
711 crate::blend::carve::carve_trace(format_args!(
712 "blend closed-edge: the fitted rows loop ({error}); re-marching under Carve"
713 ));
714 stations = climb_carve_ladder(&error)?;
715 parameters = crate::blend::stations::closed_station_parameters(&stations, [&first_mate.face.surface, &second_mate.face.surface]);
716 rows = fit_closed_rows(&stations, ¶meters, chamfer)?;
717 carve_fold = true;
718 }
719 }
720 let read_first = crate::blend::closed_trim_read_first_on();
724 let refit_timing = crate::blend::BlendTimer::start("timing.closed.rows.refit_first_ms");
725 rows.cr_pcurve = if read_first {
726 closed_support_trim(&rows.cr, &rows.cr_pcurve, &first_mate.face.surface)?
727 } else {
728 refit_closed_support_pcurve(&rows.cr, &rows.cr_pcurve, &first_mate.face.surface)?
729 };
730 refit_timing.stop();
731 let refit_timing = crate::blend::BlendTimer::start("timing.closed.rows.refit_second_ms");
732 rows.cs_pcurve = if read_first {
733 closed_support_trim(&rows.cs, &rows.cs_pcurve, &second_mate.face.surface)?
734 } else {
735 refit_closed_support_pcurve(&rows.cs, &rows.cs_pcurve, &second_mate.face.surface)?
736 };
737 refit_timing.stop();
738 rows
739 }
740 };
741 rows_timing.stop();
742 let carve_timing = crate::blend::BlendTimer::start("timing.closed.carve_ms");
743 let crease = if carve_fold {
744 Some(carve_closed_wall(solid, &rows, &stations, ¶meters, &first_mate, &second_mate, radius_at(mid_t).abs())?)
745 } else {
746 None
747 };
748 carve_timing.stop();
749 let surgery_timing = crate::blend::BlendTimer::start("timing.closed.surgery_ms");
750
751 let second_split = if second_seam {
755 let seam2 = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?[0];
756 let period2 = {
757 let [d0, d1] = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
758 d1 - d0
759 };
760 let u2_first = stations[0].uv2[0];
762 let u2_last = stations[stations.len() - 1].uv2[0];
763 let direction = (u2_last - u2_first).signum();
764 let mut k = ((u2_first - seam2) / period2).ceil();
765 if direction < 0.0 {
766 k = ((u2_first - seam2) / period2).floor();
767 }
768 let target = seam2 + k * period2;
769 let inside =
770 (target - u2_first) * direction > 1e-6 && (u2_last - target) * direction > 1e-6;
771 let aligned = (target - u2_first).abs() <= 1e-6
776 || (target - u2_last).abs() <= 1e-6
777 || ((u2_first - seam2) / period2).fract().abs() <= 1e-9;
778 if !inside && aligned {
779 None
780 } else if !inside {
781 return Err(KernelRefusal::internal(KernelStage::Refine, "seam_crossing_bracket", "blend: second seam crossing not bracketed by the march"));
782 } else {
783 let [fit_low, fit_high] = rows.u_domain;
787 let mut p = fit_low
788 + (fit_high - fit_low) * {
789 let mut seed = 0.5;
791 for pair in 0..stations.len() - 1 {
792 let a = stations[pair].uv2[0];
793 let b = stations[pair + 1].uv2[0];
794 if (target - a) * (target - b) <= 0.0 {
795 let local = (target - a) / (b - a);
796 seed = (parameters[pair]
797 + (parameters[pair + 1] - parameters[pair]) * local)
798 .clamp(0.0, 1.0);
799 break;
800 }
801 }
802 seed
803 };
804 for _ in 0..NEWTON_ITERATIONS {
805 let value = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
806 if value.abs() <= 1e-12 {
807 break;
808 }
809 let step = 1e-8;
810 let probed = rows.cs_pcurve.evaluate(p + step).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
811 let derivative = (probed - value) / step;
812 if derivative.abs() <= 1e-14 {
813 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "seam_crossing_newton", "blend: second seam crossing Newton stalled"));
814 }
815 p -= value / derivative;
816 }
817 let crossing_v = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.y;
818 Some(SecondSeamSplit {
819 fit_parameter: p,
820 crossing_v,
821 period: period2 * direction,
822 })
823 }
824 } else {
825 None
826 };
827
828 let model_unmet = if carve_fold { None } else { model_unmet };
832 let wall_surface = model_unmet.map(|_| rows.surface.clone());
833 let built = build_surgery(
834 solid,
835 edge,
836 &first_mate,
837 &second_mate,
838 rows,
839 second_split,
840 name,
841 crease,
842 )?;
843 surgery_timing.stop();
844 if let (Some((declared, reason, round, stations, ceiling)), Some(surface)) = (model_unmet, wall_surface) {
845 crate::blend::record_wall_model_report(crate::blend::WallModelReport {
846 name: name.map(str::to_string),
847 surface,
848 request: crate::KernelTolerances::for_solid(solid, 1e-7).model,
849 residual: declared,
850 detail: request_rails.map(|(rails, unread, wall)| {
853 format!("last request read: rails {rails:.3e} off their carriers at j/16 ({unread} interval(s) unread), wall {wall:.3e}")
854 }),
855 budget: crate::ApproximationBudget {
856 reason,
857 rounds_used: round,
858 rounds_limit: crate::blend::stations::REFINE_ROUNDS,
859 stations,
860 station_limit: ceiling,
861 mechanism: crate::BudgetMechanism::LocalRounds,
862 measured_component: match request_rails {
865 Some((rails, _, wall)) if rails > wall => crate::MeasuredComponent::Rails,
866 _ => crate::MeasuredComponent::Wall,
867 },
868 unread: request_rails.map_or(0, |(_, unread, _)| unread),
869 },
870 });
871 }
872 Ok(built)
873}
874
875fn separate_spherical_domain_cut(
881 solid: &BrepSolid,
882 edge_id: u64,
883) -> Result<Option<BrepSolid>, KernelRefusal> {
884 let Some(edge) = solid.edges.iter().find(|edge| edge.id == edge_id) else {
885 return Ok(None);
886 };
887 if edge.start_vertex_id != edge.end_vertex_id {
888 return Ok(None);
889 }
890 let mut result = solid.clone();
891 let mut take_id = crate::blend::edge::fresh_id_source(solid);
892 let mut separated = false;
893 for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
894 let Some(atlas) = crate::sphere_chart::SphereAtlas::of_surface(&face.surface) else {
895 continue;
896 };
897 let [u0, u1] = face
898 .surface
899 .domain_u()
900 .or_refuse(KernelStage::Collect, "domain_cut_u")?;
901 let [v0, v1] = face
902 .surface
903 .domain_v()
904 .or_refuse(KernelStage::Collect, "domain_cut_v")?;
905 if [u0, u1, v0, v1] != [0.0, 1.0, 0.0, 1.0] {
906 continue;
907 }
908 if face
909 .surface
910 .closed_directions()
911 .or_refuse(KernelStage::Collect, "domain_cut_closed")?
912 != (true, false)
913 {
914 continue;
915 }
916 let band = 1e-8 * atlas.radius;
917 let bottom = face
918 .surface
919 .evaluate(u0, v0)
920 .or_refuse(KernelStage::Collect, "domain_cut_bottom")?;
921 let top = face
922 .surface
923 .evaluate(u0, v1)
924 .or_refuse(KernelStage::Collect, "domain_cut_top")?;
925 if bottom.add(top).scale(0.5).sub(atlas.centre).length() > band
926 || (bottom.sub(top).length() - 2.0 * atlas.radius).abs() > band
927 {
928 continue;
929 }
930 let mut additions = Vec::new();
931 let mut cut_ids = std::collections::HashSet::new();
932 for rim in &mut face.loops {
933 if rim.coedges.len() < 5 {
934 continue;
935 }
936 let positions: Vec<_> = rim
937 .coedges
938 .iter()
939 .enumerate()
940 .filter_map(|(i, c)| (c.edge_id == edge_id).then_some(i))
941 .collect();
942 if positions.len() != 1 {
943 continue;
944 }
945 let position = positions[0];
946 let ring = &rim.coedges[position];
947 let [a, b] = ring
948 .pcurve
949 .domain()
950 .or_refuse(KernelStage::Collect, "domain_cut_ring")?;
951 if ring
952 .pcurve
953 .evaluate(a)
954 .or_refuse(KernelStage::Collect, "domain_cut_ring_start")?
955 .sub(
956 ring.pcurve
957 .evaluate(b)
958 .or_refuse(KernelStage::Collect, "domain_cut_ring_end")?,
959 )
960 .length()
961 > 1e-7
962 {
963 continue;
964 }
965 let rest: Vec<_> = rim
966 .coedges
967 .iter()
968 .filter(|c| c.edge_id != edge_id)
969 .collect();
970 let mut points = Vec::new();
971 let mut domain_cut = true;
972 for coedge in &rest {
973 let [a, b] = coedge
974 .pcurve
975 .domain()
976 .or_refuse(KernelStage::Collect, "domain_cut_trim")?;
977 let start = coedge
978 .pcurve
979 .evaluate(a)
980 .or_refuse(KernelStage::Collect, "domain_cut_start")?;
981 let end = coedge
982 .pcurve
983 .evaluate(b)
984 .or_refuse(KernelStage::Collect, "domain_cut_end")?;
985 points.push(start);
986 let degenerate = solid
987 .edges
988 .iter()
989 .any(|e| e.id == coedge.edge_id && e.degenerate);
990 let at = |x: f64, low: f64, high: f64| {
991 (x - low).abs() <= 1e-8 || (x - high).abs() <= 1e-8
992 };
993 let on_boundary = if degenerate {
994 (start.y - end.y).abs() <= 1e-8
995 && at(start.y, v0, v1)
996 && coedge
997 .pcurve
998 .control_points
999 .iter()
1000 .all(|p| (p.y / p.w - start.y).abs() <= 1e-8)
1001 } else {
1002 at(start.x, u0, u1)
1003 && (start.x - end.x).abs() <= 1e-8
1004 && coedge
1005 .pcurve
1006 .control_points
1007 .iter()
1008 .all(|p| (p.x / p.w - start.x).abs() <= 1e-8)
1009 && rest.iter().filter(|c| c.edge_id == coedge.edge_id).count() == 2
1010 && rest
1011 .iter()
1012 .any(|c| c.edge_id == coedge.edge_id && c.forward != coedge.forward)
1013 };
1014 domain_cut &= on_boundary;
1015 }
1016 let twice_area: f64 = (0..points.len())
1017 .map(|i| {
1018 let a = points[i];
1019 let b = points[(i + 1) % points.len()];
1020 a.x * b.y - a.y * b.x
1021 })
1022 .sum();
1023 let expected = (u1 - u0) * (v1 - v0) * if face.same_sense { 1.0 } else { -1.0 };
1024 if !domain_cut || (0.5 * twice_area - expected).abs() > 1e-7 * expected.abs() {
1025 continue;
1026 }
1027 let cuts: std::collections::HashSet<_> = rest
1031 .iter()
1032 .filter_map(|c| {
1033 solid
1034 .edges
1035 .iter()
1036 .find(|e| e.id == c.edge_id && !e.degenerate)
1037 .map(|e| e.id)
1038 })
1039 .collect();
1040 for cut in solid.edges.iter().filter(|e| cuts.contains(&e.id)) {
1041 let users = solid
1042 .shells
1043 .iter()
1044 .flat_map(|s| &s.faces)
1045 .flat_map(|f| &f.loops)
1046 .flat_map(|l| &l.coedges)
1047 .filter(|c| c.edge_id == cut.id)
1048 .count();
1049 let exclusive_vertices = solid
1050 .edges
1051 .iter()
1052 .filter(|e| {
1053 e.start_vertex_id == cut.start_vertex_id
1054 || e.end_vertex_id == cut.start_vertex_id
1055 || e.start_vertex_id == cut.end_vertex_id
1056 || e.end_vertex_id == cut.end_vertex_id
1057 })
1058 .all(|e| e.degenerate || cuts.contains(&e.id) || e.id == edge_id);
1059 let mut same_parameter = cut.t0 >= v0 && cut.t1 <= v1 && cut.t0 < cut.t1;
1060 for i in 0..=16 {
1061 let t = cut.t0 + (cut.t1 - cut.t0) * i as f64 / 16.0;
1062 let point = cut
1063 .curve
1064 .evaluate(t)
1065 .or_refuse(KernelStage::Collect, "domain_cut_curve")?;
1066 let carrier = face
1067 .surface
1068 .evaluate(u0, t)
1069 .or_refuse(KernelStage::Collect, "domain_cut_carrier")?;
1070 same_parameter &= point.sub(carrier).length() <= band;
1071 }
1072 domain_cut &= users == 2 && exclusive_vertices && same_parameter;
1073 }
1074 if !domain_cut {
1075 continue;
1076 }
1077 cut_ids.extend(cuts);
1078 additions.push(LoopRecord {
1079 id: take_id(),
1080 coedges: vec![rim.coedges.remove(position)],
1081 });
1082 separated = true;
1083 }
1084 if !additions.is_empty() {
1085 let real = |id: u64| {
1089 !cut_ids.contains(&id) && !solid.edges.iter().any(|e| e.id == id && e.degenerate)
1090 };
1091 let curves: Vec<_> = face
1092 .loops
1093 .iter()
1094 .flat_map(|l| &l.coedges)
1095 .chain(additions.iter().flat_map(|l| &l.coedges))
1096 .filter(|c| real(c.edge_id) || c.edge_id == edge_id)
1097 .map(|c| &c.pcurve)
1098 .collect();
1099 let mut candidates = Vec::new();
1100 for shift in [0.25, 0.5, 0.75] {
1101 let mut clearance = f64::INFINITY;
1102 let mut fits = true;
1103 for curve in &curves {
1104 let first = curve.control_points[0].x / curve.control_points[0].w;
1105 let offset = -(first - shift).floor();
1106 for p in &curve.control_points {
1107 let u = p.x / p.w - shift + offset;
1108 fits &= u >= 0.0 && u <= 1.0;
1109 clearance = clearance.min(u.min(1.0 - u));
1110 }
1111 }
1112 if fits {
1113 candidates.push((clearance, shift));
1114 }
1115 }
1116 let Some((_, shift)) = candidates.into_iter().max_by(|a, b| a.0.total_cmp(&b.0)) else {
1117 return Err(KernelRefusal::unsupported(
1118 KernelStage::Collect,
1119 "sphere_domain_cut",
1120 "blend: no quarter-turn sphere chart keeps the real trims in-domain",
1121 ));
1122 };
1123 let seam_direction = face
1124 .surface
1125 .evaluate(shift, 0.5)
1126 .or_refuse(KernelStage::Collect, "sphere_cut_direction")?
1127 .sub(atlas.centre);
1128 let surface = crate::make_sphere_surface_framed(
1129 atlas.centre,
1130 atlas.radius,
1131 atlas.basis[2],
1132 Some(seam_direction),
1133 )
1134 .or_refuse(KernelStage::Collect, "sphere_cut_surface")?;
1135 for i in 0..=16 {
1137 for v in [0.0, 0.3, 0.5, 0.7, 1.0] {
1138 let u = i as f64 / 16.0;
1139 let old = face
1140 .surface
1141 .evaluate(u, v)
1142 .or_refuse(KernelStage::Collect, "sphere_cut_old")?;
1143 let new = surface
1144 .evaluate((u - shift).rem_euclid(1.0), v)
1145 .or_refuse(KernelStage::Collect, "sphere_cut_new")?;
1146 if old.sub(new).length() > band {
1147 return Err(KernelRefusal::unsupported(
1148 KernelStage::Collect,
1149 "sphere_cut_chart",
1150 "blend: the sphere quarter turn is not the same carrier",
1151 ));
1152 }
1153 }
1154 }
1155 let seam = surface
1156 .iso_curve_u(0.0)
1157 .or_refuse(KernelStage::Collect, "sphere_cut_seam")?;
1158 for coedge in face
1159 .loops
1160 .iter_mut()
1161 .flat_map(|l| &mut l.coedges)
1162 .chain(additions.iter_mut().flat_map(|l| &mut l.coedges))
1163 {
1164 if solid
1165 .edges
1166 .iter()
1167 .any(|e| e.id == coedge.edge_id && e.degenerate)
1168 {
1169 continue;
1170 }
1171 if !cut_ids.contains(&coedge.edge_id) {
1172 let first =
1173 coedge.pcurve.control_points[0].x / coedge.pcurve.control_points[0].w;
1174 let offset = -(first - shift).floor() - shift;
1175 for p in &mut coedge.pcurve.control_points {
1176 p.x += offset * p.w;
1177 }
1178 }
1179 }
1180 for cut in result.edges.iter_mut().filter(|e| cut_ids.contains(&e.id)) {
1181 for (vertex_id, t) in [(cut.start_vertex_id, cut.t0), (cut.end_vertex_id, cut.t1)] {
1182 let point = seam
1183 .evaluate(t)
1184 .or_refuse(KernelStage::Collect, "sphere_cut_vertex")?;
1185 let vertex = result
1186 .vertices
1187 .iter_mut()
1188 .find(|v| v.id == vertex_id)
1189 .ok_or(KernelRefusal::internal(
1190 KernelStage::Collect,
1191 "sphere_cut_vertex",
1192 "blend: sphere seam vertex is missing",
1193 ))?;
1194 vertex.point = point;
1195 }
1196 cut.curve = seam.clone();
1197 }
1198 face.surface = surface;
1199 }
1200 face.loops.extend(additions);
1201 }
1202 if !separated {
1203 return Ok(None);
1204 }
1205 let old_vertex = solid
1206 .vertices
1207 .iter()
1208 .find(|v| v.id == edge.start_vertex_id)
1209 .ok_or(KernelRefusal::internal(
1210 KernelStage::Collect,
1211 "domain_cut_vertex",
1212 "blend: closed ring vertex is missing",
1213 ))?;
1214 let vertex_id = take_id();
1215 result.vertices.push(VertexRecord {
1216 id: vertex_id,
1217 point: old_vertex.point,
1218 });
1219 let ring = result
1220 .edges
1221 .iter_mut()
1222 .find(|e| e.id == edge_id)
1223 .ok_or(KernelRefusal::internal(
1224 KernelStage::Collect,
1225 "domain_cut_ring",
1226 "blend: closed ring edge is missing",
1227 ))?;
1228 ring.start_vertex_id = vertex_id;
1229 ring.end_vertex_id = vertex_id;
1230 Ok(Some(result))
1231}
1232
1233fn remove_retraced_cuts(
1239 solid: &BrepSolid,
1240 edge_id: u64,
1241) -> Result<Option<BrepSolid>, KernelRefusal> {
1242 let mut result = solid.clone();
1243 let mut removed = Vec::new();
1244 for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
1245 for rim in &mut face.loops {
1246 if !rim.coedges.iter().any(|coedge| coedge.edge_id == edge_id) {
1247 continue;
1248 }
1249 loop {
1250 let n = rim.coedges.len();
1251 let mut pair = None;
1252 for i in 0..n {
1253 let j = (i + 1) % n;
1254 let (a, b) = (&rim.coedges[i], &rim.coedges[j]);
1255 if a.edge_id == edge_id || a.edge_id != b.edge_id || a.forward == b.forward {
1256 continue;
1257 }
1258 let users = solid
1259 .shells
1260 .iter()
1261 .flat_map(|s| &s.faces)
1262 .flat_map(|f| &f.loops)
1263 .flat_map(|l| &l.coedges)
1264 .filter(|c| c.edge_id == a.edge_id)
1265 .count();
1266 if users != 2 {
1267 continue;
1268 }
1269 let [a0, a1] = a
1270 .pcurve
1271 .domain()
1272 .or_refuse(KernelStage::Collect, "cut_domain")?;
1273 let [b0, b1] = b
1274 .pcurve
1275 .domain()
1276 .or_refuse(KernelStage::Collect, "cut_domain")?;
1277 let agrees =
1280 a.pcurve.degree == b.pcurve.degree
1281 && a.pcurve.knots.len() == b.pcurve.knots.len()
1282 && a.pcurve.control_points.len() == b.pcurve.control_points.len()
1283 && a.pcurve.knots.iter().zip(b.pcurve.knots.iter().rev()).all(
1284 |(x, y)| {
1285 ((x - a0) / (a1 - a0) - (b1 - y) / (b1 - b0)).abs() <= 1e-9
1286 },
1287 )
1288 && a.pcurve
1289 .control_points
1290 .iter()
1291 .zip(b.pcurve.control_points.iter().rev())
1292 .all(|(p, q)| {
1293 p.point()
1294 .ok()
1295 .zip(q.point().ok())
1296 .is_some_and(|(p, q)| p.sub(q).length() <= 1e-9)
1297 && (p.w / a.pcurve.control_points[0].w
1298 - q.w / b.pcurve.control_points.last().unwrap().w)
1299 .abs()
1300 <= 1e-9
1301 });
1302 if agrees {
1303 pair = Some((i, j, a.edge_id));
1304 break;
1305 }
1306 }
1307 let Some((i, j, id)) = pair else {
1308 break;
1309 };
1310 rim.coedges.remove(i.max(j));
1311 rim.coedges.remove(i.min(j));
1312 removed.push(id);
1313 }
1314 }
1315 }
1316 if removed.is_empty() {
1317 return Ok(None);
1318 }
1319 result.edges.retain(|edge| !removed.contains(&edge.id));
1320 super::open::prune_orphan_vertices(&mut result);
1321 Ok(Some(result))
1322}
1323
1324struct SecondSeamSplit {
1327 fit_parameter: f64,
1328 crossing_v: f64,
1329 period: f64,
1331}
1332
1333const CARVE_MAX_REFINEMENT_DEPTH: usize = MAX_REFINEMENT_DEPTH + 4;
1345
1346fn rails_off_closed_carriers(
1350 rows: &FittedRows,
1351 stations: &[Station],
1352 parameters: &[f64],
1353 first: &BlendMate,
1354 second: &BlendMate,
1355) -> Result<f64, KernelRefusal> {
1356 Ok(closed_rail_interval_misses(rows, stations, parameters,
1357 [&first.face.surface, &second.face.surface])?.into_iter().fold(0.0_f64, f64::max))
1358}
1359
1360fn carve_closed_wall(
1361 solid: &BrepSolid,
1362 rows: &FittedRows,
1363 stations: &[Station],
1364 parameters: &[f64],
1365 first: &BlendMate,
1366 second: &BlendMate,
1367 radius: f64,
1368) -> Result<crate::blend::carve::CarvedCrease, KernelRefusal> {
1369 let policy = crate::KernelTolerances::for_solid(solid, 1e-7);
1370 let rail_bar = 0.5 * policy.intersection_fit;
1371 let off = rails_off_closed_carriers(rows, stations, parameters, first, second)?;
1372 crate::blend::carve::carve_trace(format_args!(
1373 "blend closed-edge carve: {} stations, rails {off:.3e} off their carriers between stations (bar {rail_bar:.3e})",
1374 parameters.len()
1375 ));
1376 if off > rail_bar {
1377 return Err(crate::blend::fold::wall_fold_refusal(
1378 Vec::new(),
1379 format!(
1380 "{} {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",
1381 crate::blend::WALL_FOLDS
1382 ),
1383 ));
1384 }
1385 let Some(carved) = crate::blend::carve::trace_wall_crease(&rows.surface, radius)? else {
1386 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"));
1387 };
1388 let [u0, u1] = rows.u_domain;
1389 let margin = 1e-3 * (u1 - u0);
1390 let straddles = carved.ends.iter().any(|end| end[0] <= u0 + margin || end[0] >= u1 - margin);
1391 if straddles {
1392 return Err(crate::blend::fold::wall_fold_refusal(
1393 Vec::new(),
1394 format!(
1395 "{} {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",
1396 crate::blend::WALL_FOLDS, carved.ends[0][0], carved.ends[1][0]
1397 ),
1398 ));
1399 }
1400 crate::blend::carve::fit_crease(&rows.surface, &carved, policy.pcurve_consistency)
1401}
1402
1403fn closed_support_trim(rail: &NurbsCurve, old: &NurbsCurve, surface: &NurbsSurface) -> Result<NurbsCurve, KernelRefusal> {
1421 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1422 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1423 let within_floor = |candidate: &NurbsCurve, what: &str| -> bool {
1424 let _read_timing = crate::blend::BlendTimer::start("timing.closed.rows.trim_read_ms");
1425 let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), candidate, d0, d1);
1426 if std::env::var_os("BREP_DEBUG_TRACK_FIT").is_some() {
1427 match &read {
1428 Ok((miss, standoff, samples, worst)) => eprintln!(
1429 "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}",
1430 candidate.control_points.len()),
1431 Err(error) => eprintln!("TRACK_FIT closed-edge support, {what} candidate: unreadable: {error}"),
1432 }
1433 }
1434 read.is_ok_and(|(miss, _, _, _)| miss <= floor)
1435 };
1436 if let Some(affine) = plane_affine_pcurve(rail, surface) {
1437 if within_floor(&affine, "affine plane image") {
1438 crate::blend::record_count("count.closed.rows.trim.affine", 1);
1439 return Ok(affine);
1440 }
1441 }
1442 crate::blend::record_count("count.closed.rows.trim.refit", 1);
1443 refit_closed_support_pcurve(rail, old, surface)
1444}
1445
1446fn plane_affine_pcurve(rail: &NurbsCurve, surface: &NurbsSurface) -> Option<NurbsCurve> {
1452 if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) {
1453 return None;
1454 }
1455 let [u0, u1] = surface.domain_u().ok()?;
1456 let [v0, v1] = surface.domain_v().ok()?;
1457 let origin = surface.evaluate(u0, v0).ok()?;
1458 let du = surface.evaluate(u1, v0).ok()?.sub(origin).scale(1.0 / (u1 - u0));
1459 let dv = surface.evaluate(u0, v1).ok()?.sub(origin).scale(1.0 / (v1 - v0));
1460 let (a, b, c) = (du.dot(du), du.dot(dv), dv.dot(dv));
1461 let det = a * c - b * b;
1462 let chart_scale = origin.length().max(du.length()).max(dv.length()).max(1.0);
1463 if !(det > 1e-24 * chart_scale.powi(4)) {
1464 return None;
1465 }
1466 let mut pcurve = rail.clone();
1467 for control in &mut pcurve.control_points {
1468 let weight = control.w;
1469 let offset = Vec3::new(control.x / weight, control.y / weight, control.z / weight).sub(origin);
1470 let (r, s) = (offset.dot(du), offset.dot(dv));
1471 control.x = (u0 + (c * r - b * s) / det) * weight;
1472 control.y = (v0 + (a * s - b * r) / det) * weight;
1473 control.z = 0.0;
1474 }
1475 Some(pcurve)
1476}
1477
1478pub(in crate::blend) fn refit_closed_support_pcurve(rail: &NurbsCurve, old: &NurbsCurve, surface: &NurbsSurface) -> Result<NurbsCurve, KernelRefusal> {
1503 let knot_retry = KNOT_RETRY.with(|flag| flag.get());
1504 refit_closed_support_pcurve_read_with(rail, old, surface, &mut crate::blend::track_fit::FootMemo::transient(), knot_retry).map(|(fitted, _)| fitted)
1505}
1506
1507thread_local! {
1508 static KNOT_RETRY: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1512}
1513
1514pub(in crate::blend) fn refit_closed_support_pcurve_read(
1522 rail: &NurbsCurve,
1523 old: &NurbsCurve,
1524 surface: &NurbsSurface,
1525 memo: &mut crate::blend::track_fit::FootMemo,
1526) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1527 let knot_retry = KNOT_RETRY.with(|flag| flag.get());
1530 refit_closed_support_pcurve_read_with(rail, old, surface, memo, knot_retry)
1531}
1532
1533pub fn with_pcurve_knot_retry<T>(build: impl FnOnce() -> T) -> T {
1537 let previous = KNOT_RETRY.with(|flag| flag.replace(true));
1538 let built = build();
1539 KNOT_RETRY.with(|flag| flag.set(previous));
1540 built
1541}
1542
1543fn refit_closed_support_pcurve_read_with(
1546 rail: &NurbsCurve,
1547 old: &NurbsCurve,
1548 surface: &NurbsSurface,
1549 memo: &mut crate::blend::track_fit::FootMemo,
1550 knot_retry: bool,
1551) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1552 const SHIPPED_READ_REPAIRS: usize = 3;
1553 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1554 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1555 let mut near: Vec<f64> = Vec::new();
1556 let (mut fitted, mut read) = fit_and_read_support(rail, old, surface, &near, memo, knot_retry)?;
1560 for _ in 0..SHIPPED_READ_REPAIRS {
1561 if read.0 <= floor || !(d1 > d0) {
1562 break;
1563 }
1564 near.push((read.3 - d0) / (d1 - d0));
1565 crate::blend::record_count("count.surgery.refit_repairs", 1);
1566 match fit_and_read_support(rail, old, surface, &near, memo, false) {
1569 Ok(repaired) => (fitted, read) = repaired,
1570 Err(_) => break,
1571 }
1572 }
1573 let (miss, standoff, samples, _) = read;
1574 if !(miss <= floor) {
1575 return Err(KernelRefusal::non_convergence(KernelStage::Refine, CLOSED_SUPPORT_PCURVE_FLOOR_WHAT, format!(
1576 "{} a blend's support pcurve, as shipped, misses its rail's track by {miss:.3e} at {samples} samples, \
1577 against a floor of {floor:.1e}",
1578 crate::blend::PCURVE_OFF_FLOOR,
1579 )));
1580 }
1581 Ok((fitted, read))
1582}
1583
1584fn fit_and_read_support(
1588 rail: &NurbsCurve,
1589 old: &NurbsCurve,
1590 surface: &NurbsSurface,
1591 near: &[f64],
1592 memo: &mut crate::blend::track_fit::FootMemo,
1593 knot_retry: bool,
1594) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1595 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1596 let guide = crate::blend::chain::pcurve_guide(old, surface)?;
1599 let track_timing = crate::blend::BlendTimer::start("timing.support_refit.track_ms");
1600 let mut fitted = if knot_retry {
1601 crate::blend::chain::project_piece_pcurve_within_knot_retry(rail, surface, false, false, Some(&guide), near)?
1602 } else {
1603 crate::blend::chain::project_piece_pcurve_within(rail, surface, false, false, Some(&guide), near)?
1604 };
1605 track_timing.stop();
1606 let [f0, f1] = fitted.domain().or_refuse(KernelStage::Refine, "domain")?;
1609 for knot in fitted.knots.iter_mut() {
1610 *knot = d0 + (*knot - f0) / (f1 - f0) * (d1 - d0);
1611 }
1612 let debug = std::env::var_os("BREP_DEBUG_TRACK_FIT").is_some();
1613 if debug {
1614 match crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &fitted, d0, d1) {
1617 Ok((miss, standoff, samples, worst)) => eprintln!(
1618 "TRACK_FIT closed-edge support: before the seam ends, miss {miss:.3e} at t {worst:.9} ({samples} samples), standoff {standoff:.3e}"),
1619 Err(error) => eprintln!("TRACK_FIT closed-edge support: before the seam ends, unreadable: {error}"),
1620 }
1621 }
1622 let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
1623 let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1624 let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1625 let old_start = old.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?;
1626 let old_end = old.evaluate(d1).or_refuse(KernelStage::Refine, "evaluate")?;
1627 let new_start = fitted.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?;
1628 let shift = |closed: bool, from: f64, to: f64, period: f64| if closed { ((to - from) / period).round() * period } else { 0.0 };
1630 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));
1631 for point in fitted.control_points.iter_mut() {
1632 point.x += shift_u * point.w;
1633 point.y += shift_v * point.w;
1634 }
1635 let on_seam = |closed: bool, value: f64, low: f64, high: f64| {
1636 let period = high - low;
1637 closed && ((value - low) / period - ((value - low) / period).round()).abs() * period <= 1e-12 * period.max(1.0)
1638 };
1639 let last = fitted.control_points.len() - 1;
1640 for (index, end) in [(0, old_start), (last, old_end)] {
1641 let point = &mut fitted.control_points[index];
1642 if on_seam(closed_u, end.x, u0, u1) {
1643 point.x = end.x * point.w;
1644 }
1645 if on_seam(closed_v, end.y, v0, v1) {
1646 point.y = end.y * point.w;
1647 }
1648 }
1649 let mut own = memo.empty_like();
1659 let (miss, standoff, samples, worst) =
1660 crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut own)?;
1661 *memo = own;
1662 if debug {
1663 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}");
1664 }
1665 Ok((fitted, (miss, standoff, samples, worst)))
1666}
1667
1668
1669fn support_exits_at(face: &FaceRecord, mut uv: [f64; 2]) -> bool {
1673 if let Ok((closed_u, closed_v)) = face.surface.closed_directions() {
1674 if closed_u {
1675 if let Ok([low, high]) = face.surface.domain_u() {
1676 uv[0] = low + (uv[0] - low).rem_euclid(high - low);
1677 }
1678 }
1679 if closed_v {
1680 if let Ok([low, high]) = face.surface.domain_v() {
1681 uv[1] = low + (uv[1] - low).rem_euclid(high - low);
1682 }
1683 }
1684 }
1685 crate::parameter_point_in_face(face, crate::Vec2 { x: uv[0], y: uv[1] }, 1e-6)
1686 .map(|class| class == crate::PolygonClass::Outside)
1687 .unwrap_or(true)
1688}
1689
1690fn support_exit_between_parallels(
1708 solid: &BrepSolid,
1709 face: &FaceRecord,
1710 (origin, axis): (Vec3, Vec3),
1711 contacts: &[Vec3],
1712 probe: &dyn Fn(usize) -> bool,
1713) -> Option<bool> {
1714 let radial = match face.surface.analytic()? {
1715 crate::AnalyticSurface::Plane { .. } => true,
1716 crate::AnalyticSurface::RuledRevolution { .. } => false,
1717 _ => return None,
1718 };
1719 if contacts.is_empty() {
1720 return None;
1721 }
1722 let axis = axis.normalized().ok()?;
1723 let coordinates = |point: Vec3| -> (f64, f64) {
1724 let offset = point.sub(origin);
1725 let height = offset.dot(axis);
1726 (offset.sub(axis.scale(height)).length(), height)
1727 };
1728 let meridian = |point: Vec3| -> f64 {
1729 let (radius, height) = coordinates(point);
1730 if radial { radius } else { height }
1731 };
1732 let scale = contacts.iter().map(|point| point.sub(origin).length()).fold(1.0, f64::max);
1733 let parallel_band = 1e-9 * scale;
1734 let margin = 1e-5 * scale;
1735 let mut boundaries: Vec<f64> = Vec::new();
1738 for loop_record in &face.loops {
1739 for coedge in &loop_record.coedges {
1740 if loop_record.coedges.iter().filter(|other| other.edge_id == coedge.edge_id).count() == 2 {
1741 continue;
1742 }
1743 let boundary = solid.edges.iter().find(|candidate| candidate.id == coedge.edge_id)?;
1744 if boundary.degenerate {
1745 continue;
1746 }
1747 let mut read: Option<(f64, f64)> = None;
1748 for step in 0..=8 {
1749 let point = boundary.curve.evaluate(boundary.t0 + (boundary.t1 - boundary.t0) * step as f64 / 8.0).ok()?;
1750 let here = coordinates(point);
1751 match read {
1752 None => read = Some(here),
1753 Some(first) => {
1754 if (here.0 - first.0).abs() > parallel_band || (here.1 - first.1).abs() > parallel_band {
1755 return None;
1756 }
1757 }
1758 }
1759 }
1760 let (radius, height) = read?;
1761 boundaries.push(if radial { radius } else { height });
1762 }
1763 }
1764 let readings: Vec<f64> = contacts.iter().map(|point| meridian(*point)).collect();
1766 let low = readings.iter().copied().fold(f64::INFINITY, f64::min);
1767 let high = readings.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1768 if !(low.is_finite() && high.is_finite()) {
1769 return None;
1770 }
1771 if boundaries.iter().any(|boundary| *boundary >= low - margin && *boundary <= high + margin) {
1772 return None;
1773 }
1774 Some(probe(0))
1775}
1776
1777fn build_surgery(
1778 solid: &BrepSolid,
1779 edge: &EdgeRecord,
1780 first: &BlendMate,
1781 second: &BlendMate,
1782 rows: FittedRows,
1783 second_split: Option<SecondSeamSplit>,
1784 name: Option<&str>,
1785 crease: Option<crate::blend::carve::CarvedCrease>,
1786) -> Result<BrepSolid, KernelRefusal> {
1787 let mut result = solid.clone();
1788 let mut take_id = crate::blend::edge::fresh_id_source(solid);
1789
1790 let [u_start, u_end] = rows.u_domain;
1791 let cr_start = rows.cr.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1792 let cs_start = rows.cs.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1793 let vertex1_id = take_id();
1794 let vertex2_id = take_id();
1795 result.vertices.push(VertexRecord {
1796 id: vertex1_id,
1797 point: cr_start,
1798 });
1799 result.vertices.push(VertexRecord {
1800 id: vertex2_id,
1801 point: cs_start,
1802 });
1803
1804 let cr_edge_id = take_id();
1805 result.edges.push(EdgeRecord {
1806 id: cr_edge_id,
1807 curve: rows.cr.clone(),
1808 t0: u_start,
1809 t1: u_end,
1810 start_vertex_id: vertex1_id,
1811 end_vertex_id: vertex1_id,
1812 degenerate: false,
1813 name: None,
1814 });
1815 let mut cs_pieces: Vec<(u64, [f64; 2], NurbsCurve)> = Vec::new();
1819 let mut second_seam_vertex = vertex2_id;
1820 let mut second_seam_point = cs_start;
1821 let mut second_crossing_v = 0.0;
1822 if let Some(split) = &second_split {
1823 let p = split.fit_parameter;
1824 second_crossing_v = split.crossing_v;
1825 let (cs_a, cs_b) = rows.cs.split(p).or_refuse(KernelStage::Refine, "split")?;
1826 let (pc_a, pc_b) = rows.cs_pcurve.split(p).or_refuse(KernelStage::Refine, "split")?;
1827 let mut pc_b = pc_b;
1829 for point in pc_b.control_points.iter_mut() {
1830 point.x -= split.period * point.w;
1831 }
1832 let w2s = take_id();
1833 second_seam_vertex = w2s;
1834 second_seam_point = rows.cs.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?;
1835 result.vertices.push(VertexRecord {
1836 id: w2s,
1837 point: second_seam_point,
1838 });
1839 let edge_a = take_id();
1840 result.edges.push(EdgeRecord {
1841 id: edge_a,
1842 curve: cs_a,
1843 t0: u_start,
1844 t1: p,
1845 start_vertex_id: vertex2_id,
1846 end_vertex_id: w2s,
1847 degenerate: false,
1848 name: None,
1849 });
1850 cs_pieces.push((edge_a, [u_start, p], pc_a));
1851 let edge_b = take_id();
1852 result.edges.push(EdgeRecord {
1853 id: edge_b,
1854 curve: cs_b,
1855 t0: p,
1856 t1: u_end,
1857 start_vertex_id: w2s,
1858 end_vertex_id: vertex2_id,
1859 degenerate: false,
1860 name: None,
1861 });
1862 cs_pieces.push((edge_b, [p, u_end], pc_b));
1863 } else {
1864 let cs_edge_id = take_id();
1865 result.edges.push(EdgeRecord {
1866 id: cs_edge_id,
1867 curve: rows.cs.clone(),
1868 t0: u_start,
1869 t1: u_end,
1870 start_vertex_id: vertex2_id,
1871 end_vertex_id: vertex2_id,
1872 degenerate: false,
1873 name: None,
1874 });
1875 cs_pieces.push((cs_edge_id, [u_start, u_end], rows.cs_pcurve.clone()));
1876 }
1877 let seam_curve = rows.surface.iso_curve_u(u_start).or_refuse(KernelStage::Refine, "iso_curve_u")?;
1878 let [seam_t0, seam_t1] = seam_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1879 let blend_seam_id = take_id();
1880 result.edges.push(EdgeRecord {
1881 id: blend_seam_id,
1882 curve: seam_curve,
1883 t0: seam_t0,
1884 t1: seam_t1,
1885 start_vertex_id: vertex1_id,
1886 end_vertex_id: vertex2_id,
1887 degenerate: false,
1888 name: None,
1889 });
1890
1891 let replace_in_face = |result: &mut BrepSolid, mate: &BlendMate, pieces: &[(u64, [f64; 2], NurbsCurve)], uv_new: [f64; 2]| -> Result<bool, KernelRefusal> {
1892 replace_rim_in_face(result, edge, mate, pieces, uv_new)
1893 };
1894 let uv1_new = rows.cr_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1895 let v1_new = uv1_new.y;
1896 let cr_pieces = vec![(cr_edge_id, [u_start, u_end], rows.cr_pcurve.clone())];
1897 let mut repaired = replace_in_face(&mut result, first, &cr_pieces, [uv1_new.x, v1_new])?;
1898 let uv2_new = rows.cs_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1899 let v2_new = if second_split.is_some() {
1900 second_crossing_v
1901 } else {
1902 uv2_new.y
1903 };
1904 repaired |= replace_in_face(&mut result, second, &cs_pieces, [uv2_new.x, v2_new])?;
1905
1906 let collapse_winding_sphere_cap =
1912 |result: &mut BrepSolid, face_id: u64, support_ids: &[u64]| -> Result<(), KernelRefusal> {
1913 let face = result
1914 .shells
1915 .iter_mut()
1916 .flat_map(|shell| &mut shell.faces)
1917 .find(|face| face.id == face_id)
1918 .ok_or(KernelRefusal::internal(KernelStage::Sew, "sphere_carrier", "blend: sphere carrier lost during cap surgery"))?;
1919 if !matches!(
1920 face.surface.analytic(),
1921 Some(crate::AnalyticSurface::Sphere { .. })
1922 ) || face.surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")? != (true, false)
1923 {
1924 return Ok(());
1925 }
1926 let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1927 let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1928 let period = u1 - u0;
1929 let support_loop = face.loops.iter().position(|loop_record| {
1930 !loop_record.coedges.is_empty()
1931 && loop_record
1932 .coedges
1933 .iter()
1934 .all(|coedge| support_ids.contains(&coedge.edge_id))
1935 });
1936 let Some(support_loop) = support_loop else {
1937 return Ok(());
1938 };
1939 if face.loops[support_loop].coedges.len() != 1 {
1940 return Ok(());
1941 }
1942 let support = &face.loops[support_loop].coedges[0];
1943 let [s0, s1] = support.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1944 let support_start = support.pcurve.evaluate(s0).or_refuse(KernelStage::Refine, "evaluate")?;
1945 let support_end = support.pcurve.evaluate(s1).or_refuse(KernelStage::Refine, "evaluate")?;
1946 let support_winding = support_end.x - support_start.x;
1947 if (support_winding.abs() - period).abs() > 0.05 * period {
1948 return Ok(());
1949 }
1950
1951 let mut pole: Option<(usize, CoedgeRecord)> = None;
1952 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1953 if loop_index == support_loop {
1954 continue;
1955 }
1956 for coedge in &loop_record.coedges {
1957 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1958 let start = coedge.pcurve.evaluate(p0).or_refuse(KernelStage::Refine, "evaluate")?;
1959 let end = coedge.pcurve.evaluate(p1).or_refuse(KernelStage::Refine, "evaluate")?;
1960 let winding = end.x - start.x;
1961 let at_pole = (start.y - v0).abs() <= 1e-8 || (start.y - v1).abs() <= 1e-8;
1962 if at_pole
1963 && (end.y - start.y).abs() <= 1e-8
1964 && (winding.abs() - period).abs() <= 0.05 * period
1965 && winding * support_winding < 0.0
1966 {
1967 pole = Some((loop_index, coedge.clone()));
1968 break;
1969 }
1970 }
1971 if pole.is_some() {
1972 break;
1973 }
1974 }
1975 let Some((pole_loop, pole_coedge)) = pole else {
1976 return Ok(());
1977 };
1978 let support_record = face.loops[support_loop].clone();
1979 let pole_id = face.loops[pole_loop].id;
1980 face.loops = vec![
1981 LoopRecord {
1982 id: pole_id,
1983 coedges: vec![pole_coedge],
1984 },
1985 support_record,
1986 ];
1987 Ok(())
1988 };
1989 collapse_winding_sphere_cap(&mut result, first.face.id, &[cr_edge_id])?;
1990 let cs_edge_ids: Vec<u64> = cs_pieces.iter().map(|(edge_id, ..)| *edge_id).collect();
1991 collapse_winding_sphere_cap(&mut result, second.face.id, &cs_edge_ids)?;
1992 for shell in &mut result.shells {
1994 for face in &mut shell.faces {
1995 for loop_record in &mut face.loops {
1996 for coedge in &mut loop_record.coedges {
1997 if coedge.id == 0 {
1998 coedge.id = take_id();
1999 }
2000 }
2001 }
2002 }
2003 }
2004
2005 let new_edge_ids: Vec<u64> = std::iter::once(cr_edge_id)
2010 .chain(cs_pieces.iter().map(|(edge_id, ..)| *edge_id))
2011 .chain(std::iter::once(blend_seam_id))
2012 .collect();
2013 let first_face_edges: Vec<u64> = first
2014 .face
2015 .loops
2016 .iter()
2017 .flat_map(|loop_record| loop_record.coedges.iter().map(|coedge| coedge.edge_id))
2018 .collect();
2019 repaired |= trim_seam_edges(
2020 &mut result,
2021 edge,
2022 &new_edge_ids,
2023 &first_face_edges,
2024 (vertex1_id, v1_new, cr_start),
2025 (second_seam_vertex, v2_new, second_seam_point),
2026 )?;
2027
2028 let mid_u = (u_start + u_end) * 0.5;
2030 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
2031 let station_uv = rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?;
2032 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
2033 let out1 = if first.face.same_sense {
2034 n1
2035 } else {
2036 n1.scale(-1.0)
2037 };
2038 let same_sense = if crate::blend::setback::active().is_some() {
2044 let uv2 = rows.cs_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?;
2045 let n2 = raw_normal(&second.face.surface, uv2.x, uv2.y)?;
2046 let out2 = if second.face.same_sense { n2 } else { n2.scale(-1.0) };
2047 let v_far = rows.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?[1];
2048 let far_normal = raw_normal(&rows.surface, mid_u, v_far)?;
2049 blend_normal.dot(out1) + far_normal.dot(out2) >= 0.0
2050 } else {
2051 blend_normal.dot(out1) >= 0.0
2052 };
2053 let loop_id = take_id();
2054 let mut coedges = vec![
2055 CoedgeRecord {
2056 id: take_id(),
2057 edge_id: cr_edge_id,
2058 forward: true,
2059 pcurve: crate::sweep_topology::parameter_line(u_start, 0.0, u_end, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
2060 },
2061 CoedgeRecord {
2062 id: take_id(),
2063 edge_id: blend_seam_id,
2064 forward: true,
2065 pcurve: crate::sweep_topology::parameter_line(u_end, 0.0, u_end, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
2066 },
2067 ];
2068 for (edge_id, window, _) in cs_pieces.iter().rev() {
2069 coedges.push(CoedgeRecord {
2070 id: take_id(),
2071 edge_id: *edge_id,
2072 forward: false,
2073 pcurve: crate::sweep_topology::parameter_line(window[1], 1.0, window[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
2074 });
2075 }
2076 coedges.push(CoedgeRecord {
2077 id: take_id(),
2078 edge_id: blend_seam_id,
2079 forward: false,
2080 pcurve: crate::sweep_topology::parameter_line(u_start, 1.0, u_start, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
2081 });
2082 if !same_sense {
2092 coedges.reverse();
2093 for coedge in &mut coedges {
2094 coedge.forward = !coedge.forward;
2095 coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
2096 }
2097 }
2098 let mut loops = vec![LoopRecord {
2099 id: loop_id,
2100 coedges,
2101 }];
2102 if let Some(crease) = crease {
2111 let start_vertex = take_id();
2112 let end_vertex = take_id();
2113 let crease_edge = take_id();
2114 result.vertices.push(VertexRecord { id: start_vertex, point: crease.start });
2115 result.vertices.push(VertexRecord { id: end_vertex, point: crease.end });
2116 let [crease_t0, crease_t1] = crease.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
2117 result.edges.push(EdgeRecord {
2118 id: crease_edge,
2119 curve: crease.curve,
2120 t0: crease_t0,
2121 t1: crease_t1,
2122 start_vertex_id: start_vertex,
2123 end_vertex_id: end_vertex,
2124 degenerate: false,
2125 name: name.map(|value| format!("{value}:CREASE")),
2126 });
2127 let outer = crate::blend::chain::signed_area(&loops[0].coedges)?;
2128 let forward_first = crate::blend::chain::signed_area(&[
2129 CoedgeRecord { id: 0, edge_id: crease_edge, forward: true, pcurve: crease.first.clone() },
2130 CoedgeRecord { id: 0, edge_id: crease_edge, forward: false, pcurve: crease.second.reversed().or_refuse(KernelStage::Refine, "reversed")? },
2131 ])?;
2132 let inner = if outer * forward_first < 0.0 {
2133 vec![
2134 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: true, pcurve: crease.first },
2135 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: false, pcurve: crease.second.reversed().or_refuse(KernelStage::Refine, "reversed")? },
2136 ]
2137 } else {
2138 vec![
2139 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: true, pcurve: crease.second },
2140 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: false, pcurve: crease.first.reversed().or_refuse(KernelStage::Refine, "reversed")? },
2141 ]
2142 };
2143 loops.push(LoopRecord { id: take_id(), coedges: inner });
2144 }
2145 let blend_face = FaceRecord {
2146 id: take_id(),
2147 surface: rows.surface,
2148 same_sense,
2149 loops,
2150 name: name.map(|value| value.to_string()),
2151 };
2152 let shell_index = result
2153 .shells
2154 .iter()
2155 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
2156 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
2157 result.shells[shell_index].faces.push(blend_face);
2158
2159 let used_edges: std::collections::HashSet<u64> = result
2160 .shells
2161 .iter()
2162 .flat_map(|shell| &shell.faces)
2163 .flat_map(|face| &face.loops)
2164 .flat_map(|loop_record| &loop_record.coedges)
2165 .map(|coedge| coedge.edge_id)
2166 .collect();
2167 result
2168 .edges
2169 .retain(|candidate| used_edges.contains(&candidate.id));
2170 let used_vertices: std::collections::HashSet<u64> = result
2171 .edges
2172 .iter()
2173 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
2174 .collect();
2175 result
2176 .vertices
2177 .retain(|candidate| used_vertices.contains(&candidate.id));
2178 if repaired {
2185 let problems = result.validate_memoised();
2186 if !problems.is_empty() {
2187 return Err(KernelRefusal::internal(KernelStage::Validate, "neighbour_trim_closure", format!(
2188 "blend: the repaired neighbour trim did not close ({} issue(s), first: {})",
2189 problems.len(),
2190 problems
2191 .first()
2192 .map(|issue| issue.message.as_str())
2193 .unwrap_or("unknown")
2194 )));
2195 }
2196 }
2197 Ok(result)
2198}
2199
2200pub(in crate::blend) fn replace_rim_in_face(
2206 result: &mut BrepSolid,
2207 edge: &EdgeRecord,
2208 mate: &BlendMate,
2209 pieces: &[(u64, [f64; 2], NurbsCurve)],
2210 uv_new: [f64; 2],
2211) -> Result<bool, KernelRefusal> {
2212 let old_vertex = edge.start_vertex_id;
2213 let v_new = uv_new[1];
2214 let seam_edge_ends: Vec<(u64, bool, bool)> = result
2216 .edges
2217 .iter()
2218 .filter(|candidate| {
2219 candidate.id != edge.id
2220 && (candidate.start_vertex_id == old_vertex
2221 || candidate.end_vertex_id == old_vertex)
2222 })
2223 .map(|candidate| {
2224 (
2225 candidate.id,
2226 candidate.start_vertex_id == old_vertex,
2227 candidate.end_vertex_id == old_vertex,
2228 )
2229 })
2230 .collect();
2231 let seam_edge_ids: Vec<u64> = seam_edge_ends.iter().map(|(id, ..)| *id).collect();
2232 let pcurve_band = crate::KernelTolerances::for_solid(result, 1e-7).pcurve_consistency;
2233 let debug = std::env::var("BREP_DEBUG_NETWORK").is_ok();
2234 let face = result
2235 .shells
2236 .iter_mut()
2237 .flat_map(|shell| &mut shell.faces)
2238 .find(|face| face.id == mate.face.id)
2239 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
2240 let face_id = face.id;
2241 let FaceRecord { surface, loops, .. } = &mut *face;
2242 let loop_record = &mut loops[mate.loop_index];
2243 let position = loop_record
2244 .coedges
2245 .iter()
2246 .position(|coedge| coedge.edge_id == edge.id)
2247 .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
2248 let old_forward = loop_record.coedges[position].forward;
2249 let old_seam_v = {
2250 let pcurve = &loop_record.coedges[position].pcurve;
2251 let [d0, _] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2252 pcurve.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?.y
2253 };
2254 let mut replacements = Vec::new();
2257 let ordered: Vec<&(u64, [f64; 2], NurbsCurve)> = if old_forward {
2258 pieces.iter().collect()
2259 } else {
2260 pieces.iter().rev().collect()
2261 };
2262 for (index, (edge_id, _, pcurve)) in ordered.iter().enumerate() {
2263 replacements.push(CoedgeRecord {
2264 id: if index == 0 {
2265 loop_record.coedges[position].id
2266 } else {
2267 0 },
2269 edge_id: *edge_id,
2270 forward: old_forward,
2271 pcurve: if old_forward {
2272 pcurve.clone()
2273 } else {
2274 pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
2275 },
2276 });
2277 }
2278 loop_record
2279 .coedges
2280 .splice(position..=position, replacements);
2281 let mut repaired = false;
2282 if loop_record.coedges.len() > 1 {
2283 for coedge in &mut loop_record.coedges {
2289 if pieces
2290 .iter()
2291 .any(|(edge_id, ..)| *edge_id == coedge.edge_id)
2292 || !seam_edge_ids.contains(&coedge.edge_id)
2293 {
2294 continue;
2295 }
2296 let controls = &coedge.pcurve.control_points;
2297 let last_index = controls.len() - 1;
2298 let first_u = controls[0].x / controls[0].w;
2299 let last_u = controls[last_index].x / controls[last_index].w;
2300 let constant_u = (first_u - last_u).abs()
2301 <= 1e-9 * (1.0 + first_u.abs().max(last_u.abs()));
2302 let (target, target_uv, fraction) = if constant_u {
2305 let first_v = controls[0].y / controls[0].w;
2309 let last_v = controls[last_index].y / controls[last_index].w;
2310 let target = if (first_v - old_seam_v).abs() < (last_v - old_seam_v).abs() {
2311 0
2312 } else {
2313 last_index
2314 };
2315 let span = last_v - first_v;
2316 let fraction = if span.abs() > 0.0 { (v_new - first_v) / span } else { 0.0 };
2317 (target, [first_u, v_new], fraction)
2318 } else {
2319 let Some((_, start_is_old, end_is_old)) = seam_edge_ends
2329 .iter()
2330 .find(|(id, ..)| *id == coedge.edge_id)
2331 .copied()
2332 else {
2333 continue;
2334 };
2335 if start_is_old == end_is_old {
2336 return Err(KernelRefusal::ill_posed(KernelStage::Sew, "neighbour_trim_end", format!(
2337 "blend: neighbour edge {} meets the blended edge at both ends; \
2338 its trim end is ambiguous",
2339 coedge.edge_id
2340 )));
2341 }
2342 let target = if start_is_old == coedge.forward { 0 } else { last_index };
2345 let v_end = controls[target].y / controls[target].w;
2346 let span = last_u - first_u;
2347 let fraction = if span.abs() > 0.0 { (uv_new[0] - first_u) / span } else { 0.0 };
2348 (target, [uv_new[0], v_end], fraction)
2349 };
2350 let [q0, q1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2362 let target_point = surface
2363 .evaluate_extended(target_uv[0], target_uv[1])
2364 .or_refuse(KernelStage::Refine, "evaluate_extended")?;
2365 let seed = (q0 + (q1 - q0) * fraction).clamp(q0.min(q1), q0.max(q1));
2371 let q_split = trim_parameter_at_point(
2372 surface, &coedge.pcurve, seed, target_point, pcurve_band,
2373 )?;
2374 let interior = q_split > q0 + 1e-9 * (q1 - q0) && q_split < q1 - 1e-9 * (q1 - q0);
2375 if debug {
2376 eprintln!(
2377 "closed-edge seam trim: face {face_id} edge {} coedge {} ({}): {} at q {q_split:.9} of [{q0:.6}, {q1:.6}], {} control points",
2378 coedge.edge_id, coedge.id, if constant_u { "meridian, constant u" } else { "along u" },
2379 if interior { "SPLIT" } else { "EXTEND" }, controls.len()
2380 );
2381 }
2382 if interior {
2383 let (low, high) = coedge.pcurve.split(q_split).or_refuse(KernelStage::Refine, "split")?;
2384 coedge.pcurve = if target == 0 { high } else { low };
2385 } else {
2386 if coedge.pcurve.degree != 1 || controls.len() != 2 {
2391 return Err(KernelRefusal::unsupported(KernelStage::Sew, "neighbour_seam_extend_curved", format!(
2392 "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)",
2393 coedge.edge_id, coedge.pcurve.degree, controls.len()
2394 )));
2395 }
2396 let w = coedge.pcurve.control_points[target].w;
2397 coedge.pcurve.control_points[target] = crate::Vec4::from_point(
2398 Vec3::new(target_uv[0], target_uv[1], 0.0), w,
2399 );
2400 }
2401 repaired = true;
2402 }
2403 }
2404 Ok(repaired)
2405}
2406
2407pub(in crate::blend) fn trim_seam_edges(
2413 result: &mut BrepSolid,
2414 edge: &EdgeRecord,
2415 new_edge_ids: &[u64],
2416 first_face_edges: &[u64],
2417 first_target: (u64, f64, Vec3),
2418 second_target: (u64, f64, Vec3),
2419) -> Result<bool, KernelRefusal> {
2420 let old_vertex = edge.start_vertex_id;
2421 let mut repaired = false;
2422 let (vertex1_id, v1_new, cr_start) = first_target;
2423 let (second_seam_vertex, v2_new, second_seam_point) = second_target;
2424 for seam_edge in result.edges.iter_mut() {
2425 if new_edge_ids.contains(&seam_edge.id) {
2426 continue;
2427 }
2428 if seam_edge.start_vertex_id != old_vertex && seam_edge.end_vertex_id != old_vertex {
2429 continue;
2430 }
2431 let (target_vertex, target_v, target_point) = if first_face_edges.contains(&seam_edge.id) {
2432 (vertex1_id, v1_new, cr_start)
2433 } else {
2434 (second_seam_vertex, v2_new, second_seam_point)
2435 };
2436 let [c0, c1] = seam_edge.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
2437 let clamped = target_v.clamp(c0.min(c1), c0.max(c1));
2438 let band = 1e-6 * (1.0 + target_point.length());
2447 let split_parameter = if seam_edge.id == edge.id {
2448 clamped
2454 } else {
2455 match seam_edge.curve.evaluate(clamped) {
2456 Ok(point) if point.sub(target_point).length() <= band => clamped,
2457 _ => {
2458 repaired = true;
2459 let projection =
2460 crate::project_point_to_curve(&seam_edge.curve, target_point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
2461 if projection.distance > band {
2462 return Err(KernelRefusal::unsupported(KernelStage::Sew, "neighbour_off_support", format!(
2463 "blend: neighbour edge {} does not pass through the blend's support \
2464 start (off by {:.9}); the trim has no parameter to move to",
2465 seam_edge.id, projection.distance
2466 )));
2467 }
2468 projection.u.clamp(c0.min(c1), c0.max(c1))
2469 }
2470 }
2471 };
2472 if seam_edge.start_vertex_id == old_vertex {
2473 seam_edge.t0 = split_parameter;
2474 seam_edge.start_vertex_id = target_vertex;
2475 }
2476 if seam_edge.end_vertex_id == old_vertex {
2477 seam_edge.t1 = split_parameter;
2478 seam_edge.end_vertex_id = target_vertex;
2479 }
2480 }
2481 Ok(repaired)
2482}