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 let mut odd = false;
56 for k in 0..points.len() {
57 let (ax, ay) = points[k];
58 let (bx, by) = points[(k + 1) % points.len()];
59 if (ay > y) != (by > y) && x < ax + (y - ay) * (bx - ax) / (by - ay) {
60 odd = !odd;
61 }
62 }
63 odd
64 };
65 let inside = |x: f64, y: f64| -> bool { within(&outer, x, y) && !within(&rim_polygon, x, y) };
68 {
72 let derivatives = edge.curve.derivatives(edge.t0, 1).or_refuse(KernelStage::Classify, "derivatives")?;
73 let (point, tangent) = (derivatives[0], derivatives[1]);
74 let side = normal.cross(tangent).normalized().or_refuse(KernelStage::Classify, "normalized")?;
75 let probe = radius * 0.05;
78 let (px, py) = flat(point.add(side.scale(probe)));
79 let toward = if inside(px, py) { side } else { side.scale(-1.0) };
80 let foot = crate::project_point_to_surface(&wall.surface, point).or_refuse(KernelStage::Classify, "project_point_to_surface")?;
81 let wall_normal = wall.surface.normal(foot.u, foot.v).or_refuse(KernelStage::Classify, "normal")?;
82 let outward = if wall.same_sense { wall_normal } else { wall_normal.scale(-1.0) };
83 if outward.dot(toward) <= 0.0 {
84 continue;
85 }
86 }
87 let mut overruns = false;
88 for k in 0..RIM_SAMPLES {
89 let t = edge.t0 + (edge.t1 - edge.t0) * (k as f64 + 0.5) / RIM_SAMPLES as f64;
90 let derivatives = edge.curve.derivatives(t, 1).or_refuse(KernelStage::Classify, "derivatives")?;
91 let (point, tangent) = (derivatives[0], derivatives[1]);
92 let Ok(side) = normal.cross(tangent).normalized() else { continue };
93 let probe = radius * 0.05;
98 let toward = if inside(flat(point.add(side.scale(probe))).0, flat(point.add(side.scale(probe))).1) { side } else { side.scale(-1.0) };
99 let contact = flat(point.add(toward.scale(radius)));
100 if !within(&outer, contact.0, contact.1) {
101 overruns = true;
102 break;
103 }
104 }
105 if overruns {
106 return Ok(Some(KernelRefusal::unsupported(KernelStage::Classify, CONCAVE_RIM_OVERRUN_WHAT, format!(
107 "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",
108 edge.id,
109 face.name.clone().unwrap_or_else(|| format!("{}", face.id)),
110 ))));
111 }
112 }
113 Ok(None)
114}
115
116pub fn blend_closed_edge(
117 solid: &BrepSolid,
118 edge_id: u64,
119 radius: f64,
120 chamfer: bool,
121 name: Option<&str>,
122) -> Result<BrepSolid, KernelRefusal> {
123 let _operation = crate::blend::BlendOperation::enter();
126 if !(radius > 0.0) || !radius.is_finite() {
127 return Err(KernelRefusal::input(KernelStage::Collect, "radius", "blend: radius must be positive"));
128 }
129 blend_closed_edge_impl(solid, edge_id, &|_| radius, Some(radius), chamfer, name)
130}
131
132pub fn blend_closed_edge_knot_aware(
142 solid: &BrepSolid,
143 edge_id: u64,
144 radius: f64,
145 chamfer: bool,
146 name: Option<&str>,
147) -> Result<BrepSolid, KernelRefusal> {
148 let _operation = crate::blend::BlendOperation::enter();
149 if !(radius > 0.0) || !radius.is_finite() {
150 return Err(KernelRefusal::input(KernelStage::Collect, "radius", "blend: radius must be positive"));
151 }
152 KNOT_RETRY.with(|flag| flag.set(true));
153 let built = blend_closed_edge_impl(solid, edge_id, &|_| radius, Some(radius), chamfer, name);
154 KNOT_RETRY.with(|flag| flag.set(false));
155 built
156}
157
158pub fn is_pcurve_floor_refusal(refusal: &KernelRefusal) -> bool {
162 matches!(&refusal.class, crate::RefusalClass::NonConvergence { what }
163 if what == crate::blend::chain::CHAIN_PCURVE_FLOOR_WHAT || what == CLOSED_SUPPORT_PCURVE_FLOOR_WHAT)
164}
165
166pub(crate) const CLOSED_SUPPORT_PCURVE_FLOOR_WHAT: &str = "closed_support_pcurve_floor";
168
169pub fn blend_edge_variable(
173 solid: &BrepSolid,
174 edge_id: u64,
175 radii: &[(f64, f64)],
176 chamfer: bool,
177 name: Option<&str>,
178) -> Result<BrepSolid, KernelRefusal> {
179 let _operation = crate::blend::BlendOperation::enter();
182 if radii.is_empty() || radii.iter().any(|(_, radius)| !(*radius > 0.0)) {
183 return Err(KernelRefusal::input(KernelStage::Collect, "radius_stop", "blend: every radius stop must be positive"));
184 }
185 let mut stops = radii.to_vec();
186 stops.sort_by(|a, b| a.0.total_cmp(&b.0));
187 let edge = solid
188 .edges
189 .iter()
190 .find(|edge| edge.id == edge_id)
191 .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
192 let closed = edge.start_vertex_id == edge.end_vertex_id;
193 if closed && (stops[0].1 - stops[stops.len() - 1].1).abs() > 1e-12 {
194 return Err(KernelRefusal::input(KernelStage::Collect, "closed_edge_radii", "blend: closed edges need equal first/last radii"));
195 }
196 let first_radius = stops[0].1;
205 if stops
206 .iter()
207 .all(|(_, radius)| (radius - first_radius).abs() <= 1e-12 * (1.0 + first_radius.abs()))
208 {
209 return if chamfer {
210 crate::fillet::chamfer_edge(solid, edge_id, first_radius, name)
211 } else {
212 crate::fillet::fillet_edge(solid, edge_id, first_radius, name)
213 };
214 }
215 let (t0, t1) = (edge.t0, edge.t1);
216 let radius_at = move |t: f64| -> f64 {
217 let raw = (t - t0) / (t1 - t0);
218 let fraction = if closed {
226 raw.rem_euclid(1.0)
227 } else {
228 raw.clamp(0.0, 1.0)
229 };
230 if fraction <= stops[0].0 {
231 return stops[0].1;
232 }
233 for pair in stops.windows(2) {
234 if fraction <= pair[1].0 {
235 let width = (pair[1].0 - pair[0].0).max(1e-12);
236 let local = (fraction - pair[0].0) / width;
237 return pair[0].1 + (pair[1].1 - pair[0].1) * local;
238 }
239 }
240 stops[stops.len() - 1].1
241 };
242 if closed {
245 blend_closed_edge_impl(solid, edge_id, &radius_at, None, chamfer, name)
246 } else {
247 blend_open_edge_impl(solid, edge_id, &radius_at, None, chamfer, name)
248 }
249}
250
251fn blend_closed_edge_impl(
255 solid: &BrepSolid,
256 edge_id: u64,
257 radius_at: &dyn Fn(f64) -> f64,
258 constant: Option<f64>,
259 chamfer: bool,
260 name: Option<&str>,
261) -> Result<BrepSolid, KernelRefusal> {
262 let _closed_timing = crate::blend::BlendTimer::start("timing.closed_rim_ms");
263 let prepare_timing = crate::blend::BlendTimer::start("timing.closed.prepare_ms");
264 if let Some(separated) = separate_spherical_domain_cut(solid, edge_id)? {
265 return blend_closed_edge_impl(&separated, edge_id, radius_at, constant, chamfer, name);
266 }
267 if let Some(simplified) = remove_retraced_cuts(solid, edge_id)? {
268 return blend_closed_edge_impl(&simplified, edge_id, radius_at, constant, chamfer, name);
269 }
270 let edge = solid
271 .edges
272 .iter()
273 .find(|edge| edge.id == edge_id)
274 .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
275 if edge.start_vertex_id != edge.end_vertex_id {
276 return Err(KernelRefusal::input(KernelStage::Collect, "closed_edge", "blend: general path currently requires a CLOSED edge"));
277 }
278 let (face_a, loop_a, coedge_a) = locate_mate(solid, edge_id, None)?;
279 let (face_b, loop_b, coedge_b) = locate_mate(solid, edge_id, Some((face_a.id, loop_a)))?;
280 if face_a.id == face_b.id && loop_a == loop_b {
281 return Err(KernelRefusal::unsupported(KernelStage::Classify, "seam_edge", "blend: edge is used twice by one loop (seam edge?)"));
282 }
283
284 let seam_a = face_a.loops[loop_a].coedges.len() > 1;
287 let seam_b = face_b.loops[loop_b].coedges.len() > 1;
288 let (first_face, first_loop, first_coedge, second_face, second_loop, second_coedge) =
289 if seam_a || !seam_b {
290 (face_a, loop_a, coedge_a, face_b, loop_b, coedge_b)
291 } else {
292 (face_b, loop_b, coedge_b, face_a, loop_a, coedge_a)
293 };
294 let anchored = first_face.loops[first_loop].coedges.len() > 1;
295 let second_seam = anchored && second_face.loops[second_loop].coedges.len() > 1;
296 let anchor_u = if anchored {
297 Some(first_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?[0])
299 } else {
300 None
301 };
302
303 let mid_radius = radius_at(edge.t0 + (edge.t1 - edge.t0) * 0.5);
305 let (rho1, rho2) = signed_radii(
306 edge,
307 first_face,
308 first_coedge,
309 second_face,
310 second_coedge,
311 mid_radius,
312 )?;
313
314 let first_mate = BlendMate {
315 face: first_face,
316 coedge: first_coedge,
317 loop_index: first_loop,
318 rho: rho1,
319 };
320 let second_mate = BlendMate {
321 face: second_face,
322 coedge: second_coedge,
323 loop_index: second_loop,
324 rho: rho2,
325 };
326 let mid_t = edge.t0 + (edge.t1 - edge.t0) * 0.5;
327 use crate::blend::chain::FoldPolicy;
342 let march_with = |fold: FoldPolicy, depth_budget: usize| -> Result<Vec<Station>, KernelRefusal> {
343 match march_stations(edge, &first_mate, &second_mate, radius_at, anchor_u, fold, depth_budget) {
344 Ok(stations) => Ok(stations),
345 Err(anchored_error) if anchor_u.is_some() => {
346 march_stations(edge, &first_mate, &second_mate, radius_at, None, fold, depth_budget).map_err(
347 |retry_error| {
348 if crate::blend::is_wall_fold(&retry_error)
349 || crate::blend::is_marched_fit_off_carriers(&retry_error) {
350 retry_error
351 } else {
352 anchored_error
353 }
354 },
355 )
356 }
357 Err(anchored_error) => Err(anchored_error),
358 }
359 };
360 let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
361 let wall_check = {
365 let radius = constant.unwrap_or(f64::NAN);
368 let constant = constant.is_some();
369 let model = crate::KernelTolerances::for_solid(solid, 1e-7).model;
372 let bar = crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
373 (!chamfer && constant && radius.is_finite() && radius != 0.0).then(|| crate::blend::stations::ClosedWallCheck {
374 radius: radius.abs(),
375 bar,
376 model: model.is_finite().then_some(model),
377 })
378 };
379 crate::blend::stations::CLOSED_MODEL_UNMET.with(|slot| slot.set(None));
380 crate::blend::stations::CLOSED_REQUEST_RAILS.with(|slot| slot.set(None));
381 prepare_timing.stop();
382 let exact_revolution = crate::blend::exact_rim_lane_on()
383 && crate::blend::stations::closed_revolution_axis(&first_mate, &second_mate).is_some();
384 let depth_budget = if exact_revolution { 0 } else { MAX_REFINEMENT_DEPTH };
385 if exact_revolution {
386 crate::blend::record_count("count.closed.exact_lane", 1);
387 }
388 let march_timing = crate::blend::BlendTimer::start("timing.closed.march_ms");
389 let attempt_timing = crate::blend::BlendTimer::start("timing.closed.march.anchored_fallback_ms");
390 let analytic = crate::blend::stations::analytic_rim::construct(
391 edge, &first_mate, &second_mate, constant, chamfer, anchor_u,
392 );
393 let (analytic_stations, analytic_rows) = match analytic {
394 Some((stations, rows)) => (Some(stations), Some(rows)),
395 None => (None, None),
396 };
397 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)) {
398 Ok(stations) => {
399 std::mem::forget(attempt_timing);
400 Ok(stations)
401 }
402 Err(failure) => {
403 let anchored_error = failure.into_initial()?;
406 if anchor_u.is_some() {
407 attempt_timing.stop();
408 crate::blend::record_count("count.closed.march.anchor_fallback", 1);
409 if std::env::var("BREP_DEBUG_CLOSED_RAILS").is_ok() {
410 eprintln!("closed initial anchor fallback: edge {}: {anchored_error}", edge.id);
411 }
412 match march_stations_with_rail_bar(edge, &first_mate, &second_mate, radius_at, None, FoldPolicy::Refuse, depth_budget, rail_bar, wall_check) {
413 Ok(stations) => Ok(stations),
414 Err(failure) => {
415 let retry_error = failure.into_initial()?;
416 if crate::blend::is_wall_fold(&retry_error) { Err(retry_error) }
417 else { Err(anchored_error) }
418 }
419 }
420 } else { Err(anchored_error) }
421 }
422 };
423 let model_unmet = crate::blend::stations::CLOSED_MODEL_UNMET.with(|slot| slot.take());
436 let request_rails = crate::blend::stations::CLOSED_REQUEST_RAILS.with(|slot| slot.take());
437 let mut carve_fold = false;
438 let climb_carve_ladder = |error: &KernelRefusal| -> Result<Vec<Station>, KernelRefusal> {
445 let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
446 let mut climbed: Option<Vec<Station>> = None;
447 let mut last_off = f64::NAN;
448 for depth_budget in MAX_REFINEMENT_DEPTH..=CARVE_MAX_REFINEMENT_DEPTH {
449 let stations = match march_with(FoldPolicy::Carve, depth_budget) {
450 Ok(stations) => stations,
451 Err(_) => break,
453 };
454 let parameters = station_parameters(&stations);
455 let off = match fit_closed_rows(&stations, ¶meters, chamfer) {
456 Ok(rows) => rails_off_closed_carriers(&rows, &stations, ¶meters, &first_mate, &second_mate)?,
457 Err(_) => break,
458 };
459 last_off = off;
460 crate::blend::carve::carve_trace(format_args!(
461 "blend closed-edge carve ladder: depth budget {depth_budget} ({} stations) leaves the rails {off:.3e} off their carriers (bar {rail_bar:.3e})",
462 stations.len()
463 ));
464 if off <= rail_bar {
465 climbed = Some(stations);
466 break;
467 }
468 }
469 climbed.ok_or_else(|| {
470 crate::blend::fold::wall_fold_refusal(
471 Vec::new(),
472 format!(
473 "{} {} 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})",
474 crate::blend::WALL_FOLDS,
475 radius_at(mid_t).abs()
476 ),
477 )
478 })
479 };
480 let marched = match marched {
481 Err(error)
482 if crate::blend::is_wall_fold(&error)
483 && !crate::blend::fold::is_rail_fold(&error)
484 && !chamfer =>
485 {
486 crate::blend::carve::carve_trace(format_args!(
487 "blend closed-edge: the wall folds ({error}); re-marching under Carve"
488 ));
489 match climb_carve_ladder(&error) {
490 Ok(stations) => {
491 carve_fold = true;
492 Ok(stations)
493 }
494 Err(refusal) => Err(refusal),
495 }
496 }
497 other => other,
498 };
499 let mut stations = match marched {
500 Ok(stations) => stations,
501 Err(march_error) if crate::blend::is_wall_fold(&march_error)
507 || crate::blend::is_marched_fit_off_carriers(&march_error) => return Err(march_error),
508 Err(march_error) if crate::blend::is_ball_off_carrier(&march_error) => {
509 return Err(march_error);
516 }
517 Err(march_error) => {
518 if let Some(refusal) = concave_rim_overrun(solid, edge, [(first_face, first_loop), (second_face, second_loop)], radius_at(mid_t))? {
521 return Err(refusal);
522 }
523 return blend_closed_edge_keep(
526 solid,
527 edge,
528 &first_mate,
529 &second_mate,
530 radius_at(mid_t),
531 rho1,
532 chamfer,
533 name,
534 )
535 .or_else(|_| {
536 blend_closed_edge_keep(
537 solid,
538 edge,
539 &second_mate,
540 &first_mate,
541 radius_at(mid_t),
542 rho2,
543 chamfer,
544 name,
545 )
546 })
547 .map_err(|keep_error| {
548 format!("{march_error}; edge-preserving blend also failed: {keep_error}")
549 }).or_refuse(KernelStage::Refine, "map_err");
550 }
551 };
552 let support_exits = |face: &FaceRecord, second_side: bool| -> bool {
556 let stride = (stations.len() / 16).max(1);
560 stations.iter().step_by(stride).any(|station| {
561 let mut uv = if second_side {
562 station.uv2
563 } else {
564 station.uv1
565 };
566 if let Ok((closed_u, closed_v)) = face.surface.closed_directions() {
567 if closed_u {
568 if let Ok([low, high]) = face.surface.domain_u() {
569 uv[0] = low + (uv[0] - low).rem_euclid(high - low);
570 }
571 }
572 if closed_v {
573 if let Ok([low, high]) = face.surface.domain_v() {
574 uv[1] = low + (uv[1] - low).rem_euclid(high - low);
575 }
576 }
577 }
578 crate::parameter_point_in_face(face, crate::Vec2 { x: uv[0], y: uv[1] }, 1e-6)
579 .map(|class| class == crate::PolygonClass::Outside)
580 .unwrap_or(true)
581 })
582 };
583 let exits_timing = crate::blend::BlendTimer::start("timing.closed.march.exits_ms");
591 let exits = [support_exits(first_face, false), support_exits(second_face, true)];
592 exits_timing.stop();
593 if exits[0] || exits[1] {
594 let scale = crate::blend::stations::march_model_scale(&edge.curve, edge.t0, edge.t1, radius_at(mid_t))?;
595 match crate::blend::rollover::plan_closed(solid, edge, &stations, [&first_mate, &second_mate], radius_at, scale) {
596 Ok(Some(plan)) => {
597 if crate::blend::rollover_on() {
600 let pieces = crate::blend::rollover::march_pieces(
601 solid, edge, &plan, &stations, [&first_mate, &second_mate], radius_at, scale, rail_bar, chamfer, anchor_u,
602 )?;
603 let built = crate::blend::rollover::build(solid, edge, [&first_mate, &second_mate], &plan, pieces, anchor_u, name)?;
604 march_timing.stop();
605 return Ok(built);
606 }
607 return Err(crate::blend::rollover::refusal(&plan, radius_at(mid_t).abs()));
608 }
609 Ok(None) => {}
610 Err(error) => {
611 if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
612 eprintln!("closed-rim rollover scan did not read the exit: {error}");
613 }
614 }
615 }
616 }
617 if let Some(refusal) = concave_rim_overrun(solid, edge, [(first_face, first_loop), (second_face, second_loop)], radius_at(mid_t))? {
618 return Err(refusal);
619 }
620 if exits[1] {
621 return blend_closed_edge_keep(
622 solid,
623 edge,
624 &first_mate,
625 &second_mate,
626 radius_at(mid_t),
627 rho1,
628 chamfer,
629 name,
630 );
631 }
632 if exits[0] {
633 return blend_closed_edge_keep(
634 solid,
635 edge,
636 &second_mate,
637 &first_mate,
638 radius_at(mid_t),
639 rho2,
640 chamfer,
641 name,
642 );
643 }
644 if std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok() {
645 let last = stations.len() - 1;
646 for index in [
647 0usize,
648 1.min(last),
649 2.min(last),
650 last.saturating_sub(1),
651 last,
652 ] {
653 let station = &stations[index];
654 eprintln!(
655 "station {index}: uv1=({:.6},{:.6}) uv2=({:.6},{:.6}) p1=({:.6},{:.6},{:.6}) w={:.6}",
656 station.uv1[0], station.uv1[1], station.uv2[0], station.uv2[1],
657 station.p1.x, station.p1.y, station.p1.z, station.weight,
658 );
659 }
660 }
661 march_timing.stop();
662 crate::blend::record_count("count.closed.stations", stations.len() as u64);
663 let rows_timing = crate::blend::BlendTimer::start("timing.closed.rows_ms");
664 let mut parameters = station_parameters(&stations);
665 let exact_rows = analytic_rows.map(Ok).or_else(|| exact_closed_revolution_rows(&stations, &first_mate, &second_mate, chamfer));
666 crate::blend::record_count(
667 if exact_rows.is_some() { "count.rows.exact_revolution" } else { "count.rows.fitted_closed" },
668 1,
669 );
670 let rows = match exact_rows {
671 Some(rows) => rows?,
672 None => {
673 let fit_timing = crate::blend::BlendTimer::start("timing.closed.rows.fit_ms");
683 let mut rows = fit_closed_rows(&stations, ¶meters, chamfer)?;
684 fit_timing.stop();
685 if !carve_fold && !chamfer {
693 if let Err(error) = crate::blend::fold::check_rows_loop(
694 &rows, &stations, ¶meters, None, radius_at(mid_t).abs(), vec![first_face.id, second_face.id], crate::solid_scale(solid),
695 ) {
696 crate::blend::carve::carve_trace(format_args!(
697 "blend closed-edge: the fitted rows loop ({error}); re-marching under Carve"
698 ));
699 stations = climb_carve_ladder(&error)?;
700 parameters = station_parameters(&stations);
701 rows = fit_closed_rows(&stations, ¶meters, chamfer)?;
702 carve_fold = true;
703 }
704 }
705 let read_first = crate::blend::closed_trim_read_first_on();
709 let refit_timing = crate::blend::BlendTimer::start("timing.closed.rows.refit_first_ms");
710 rows.cr_pcurve = if read_first {
711 closed_support_trim(&rows.cr, &rows.cr_pcurve, &first_mate.face.surface)?
712 } else {
713 refit_closed_support_pcurve(&rows.cr, &rows.cr_pcurve, &first_mate.face.surface)?
714 };
715 refit_timing.stop();
716 let refit_timing = crate::blend::BlendTimer::start("timing.closed.rows.refit_second_ms");
717 rows.cs_pcurve = if read_first {
718 closed_support_trim(&rows.cs, &rows.cs_pcurve, &second_mate.face.surface)?
719 } else {
720 refit_closed_support_pcurve(&rows.cs, &rows.cs_pcurve, &second_mate.face.surface)?
721 };
722 refit_timing.stop();
723 rows
724 }
725 };
726 rows_timing.stop();
727 let carve_timing = crate::blend::BlendTimer::start("timing.closed.carve_ms");
728 let crease = if carve_fold {
729 Some(carve_closed_wall(solid, &rows, &stations, ¶meters, &first_mate, &second_mate, radius_at(mid_t).abs())?)
730 } else {
731 None
732 };
733 carve_timing.stop();
734 let surgery_timing = crate::blend::BlendTimer::start("timing.closed.surgery_ms");
735
736 let second_split = if second_seam {
740 let seam2 = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?[0];
741 let period2 = {
742 let [d0, d1] = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
743 d1 - d0
744 };
745 let u2_first = stations[0].uv2[0];
747 let u2_last = stations[stations.len() - 1].uv2[0];
748 let direction = (u2_last - u2_first).signum();
749 let mut k = ((u2_first - seam2) / period2).ceil();
750 if direction < 0.0 {
751 k = ((u2_first - seam2) / period2).floor();
752 }
753 let target = seam2 + k * period2;
754 let inside =
755 (target - u2_first) * direction > 1e-6 && (u2_last - target) * direction > 1e-6;
756 let aligned = (target - u2_first).abs() <= 1e-6
761 || (target - u2_last).abs() <= 1e-6
762 || ((u2_first - seam2) / period2).fract().abs() <= 1e-9;
763 if !inside && aligned {
764 None
765 } else if !inside {
766 return Err(KernelRefusal::internal(KernelStage::Refine, "seam_crossing_bracket", "blend: second seam crossing not bracketed by the march"));
767 } else {
768 let [fit_low, fit_high] = rows.u_domain;
772 let mut p = fit_low
773 + (fit_high - fit_low) * {
774 let mut seed = 0.5;
776 for pair in 0..stations.len() - 1 {
777 let a = stations[pair].uv2[0];
778 let b = stations[pair + 1].uv2[0];
779 if (target - a) * (target - b) <= 0.0 {
780 let local = (target - a) / (b - a);
781 seed = (parameters[pair]
782 + (parameters[pair + 1] - parameters[pair]) * local)
783 .clamp(0.0, 1.0);
784 break;
785 }
786 }
787 seed
788 };
789 for _ in 0..NEWTON_ITERATIONS {
790 let value = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
791 if value.abs() <= 1e-12 {
792 break;
793 }
794 let step = 1e-8;
795 let probed = rows.cs_pcurve.evaluate(p + step).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
796 let derivative = (probed - value) / step;
797 if derivative.abs() <= 1e-14 {
798 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "seam_crossing_newton", "blend: second seam crossing Newton stalled"));
799 }
800 p -= value / derivative;
801 }
802 let crossing_v = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.y;
803 Some(SecondSeamSplit {
804 fit_parameter: p,
805 crossing_v,
806 period: period2 * direction,
807 })
808 }
809 } else {
810 None
811 };
812
813 let model_unmet = if carve_fold { None } else { model_unmet };
817 let wall_surface = model_unmet.map(|_| rows.surface.clone());
818 let built = build_surgery(
819 solid,
820 edge,
821 &first_mate,
822 &second_mate,
823 rows,
824 second_split,
825 name,
826 crease,
827 )?;
828 surgery_timing.stop();
829 if let (Some((declared, reason, round, stations, ceiling)), Some(surface)) = (model_unmet, wall_surface) {
830 crate::blend::record_wall_model_report(crate::blend::WallModelReport {
831 name: name.map(str::to_string),
832 surface,
833 request: crate::KernelTolerances::for_solid(solid, 1e-7).model,
834 residual: declared,
835 detail: request_rails.map(|(rails, unread, wall)| {
838 format!("last request read: rails {rails:.3e} off their carriers at j/16 ({unread} interval(s) unread), wall {wall:.3e}")
839 }),
840 budget: crate::ApproximationBudget {
841 reason,
842 rounds_used: round,
843 rounds_limit: crate::blend::stations::REFINE_ROUNDS,
844 stations,
845 station_limit: ceiling,
846 mechanism: crate::BudgetMechanism::LocalRounds,
847 measured_component: match request_rails {
850 Some((rails, _, wall)) if rails > wall => crate::MeasuredComponent::Rails,
851 _ => crate::MeasuredComponent::Wall,
852 },
853 unread: request_rails.map_or(0, |(_, unread, _)| unread),
854 },
855 });
856 }
857 Ok(built)
858}
859
860fn separate_spherical_domain_cut(
866 solid: &BrepSolid,
867 edge_id: u64,
868) -> Result<Option<BrepSolid>, KernelRefusal> {
869 let Some(edge) = solid.edges.iter().find(|edge| edge.id == edge_id) else {
870 return Ok(None);
871 };
872 if edge.start_vertex_id != edge.end_vertex_id {
873 return Ok(None);
874 }
875 let mut result = solid.clone();
876 let mut take_id = crate::blend::edge::fresh_id_source(solid);
877 let mut separated = false;
878 for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
879 let Some(atlas) = crate::sphere_chart::SphereAtlas::of_surface(&face.surface) else {
880 continue;
881 };
882 let [u0, u1] = face
883 .surface
884 .domain_u()
885 .or_refuse(KernelStage::Collect, "domain_cut_u")?;
886 let [v0, v1] = face
887 .surface
888 .domain_v()
889 .or_refuse(KernelStage::Collect, "domain_cut_v")?;
890 if [u0, u1, v0, v1] != [0.0, 1.0, 0.0, 1.0] {
891 continue;
892 }
893 if face
894 .surface
895 .closed_directions()
896 .or_refuse(KernelStage::Collect, "domain_cut_closed")?
897 != (true, false)
898 {
899 continue;
900 }
901 let band = 1e-8 * atlas.radius;
902 let bottom = face
903 .surface
904 .evaluate(u0, v0)
905 .or_refuse(KernelStage::Collect, "domain_cut_bottom")?;
906 let top = face
907 .surface
908 .evaluate(u0, v1)
909 .or_refuse(KernelStage::Collect, "domain_cut_top")?;
910 if bottom.add(top).scale(0.5).sub(atlas.centre).length() > band
911 || (bottom.sub(top).length() - 2.0 * atlas.radius).abs() > band
912 {
913 continue;
914 }
915 let mut additions = Vec::new();
916 let mut cut_ids = std::collections::HashSet::new();
917 for rim in &mut face.loops {
918 if rim.coedges.len() < 5 {
919 continue;
920 }
921 let positions: Vec<_> = rim
922 .coedges
923 .iter()
924 .enumerate()
925 .filter_map(|(i, c)| (c.edge_id == edge_id).then_some(i))
926 .collect();
927 if positions.len() != 1 {
928 continue;
929 }
930 let position = positions[0];
931 let ring = &rim.coedges[position];
932 let [a, b] = ring
933 .pcurve
934 .domain()
935 .or_refuse(KernelStage::Collect, "domain_cut_ring")?;
936 if ring
937 .pcurve
938 .evaluate(a)
939 .or_refuse(KernelStage::Collect, "domain_cut_ring_start")?
940 .sub(
941 ring.pcurve
942 .evaluate(b)
943 .or_refuse(KernelStage::Collect, "domain_cut_ring_end")?,
944 )
945 .length()
946 > 1e-7
947 {
948 continue;
949 }
950 let rest: Vec<_> = rim
951 .coedges
952 .iter()
953 .filter(|c| c.edge_id != edge_id)
954 .collect();
955 let mut points = Vec::new();
956 let mut domain_cut = true;
957 for coedge in &rest {
958 let [a, b] = coedge
959 .pcurve
960 .domain()
961 .or_refuse(KernelStage::Collect, "domain_cut_trim")?;
962 let start = coedge
963 .pcurve
964 .evaluate(a)
965 .or_refuse(KernelStage::Collect, "domain_cut_start")?;
966 let end = coedge
967 .pcurve
968 .evaluate(b)
969 .or_refuse(KernelStage::Collect, "domain_cut_end")?;
970 points.push(start);
971 let degenerate = solid
972 .edges
973 .iter()
974 .any(|e| e.id == coedge.edge_id && e.degenerate);
975 let at = |x: f64, low: f64, high: f64| {
976 (x - low).abs() <= 1e-8 || (x - high).abs() <= 1e-8
977 };
978 let on_boundary = if degenerate {
979 (start.y - end.y).abs() <= 1e-8
980 && at(start.y, v0, v1)
981 && coedge
982 .pcurve
983 .control_points
984 .iter()
985 .all(|p| (p.y / p.w - start.y).abs() <= 1e-8)
986 } else {
987 at(start.x, u0, u1)
988 && (start.x - end.x).abs() <= 1e-8
989 && coedge
990 .pcurve
991 .control_points
992 .iter()
993 .all(|p| (p.x / p.w - start.x).abs() <= 1e-8)
994 && rest.iter().filter(|c| c.edge_id == coedge.edge_id).count() == 2
995 && rest
996 .iter()
997 .any(|c| c.edge_id == coedge.edge_id && c.forward != coedge.forward)
998 };
999 domain_cut &= on_boundary;
1000 }
1001 let twice_area: f64 = (0..points.len())
1002 .map(|i| {
1003 let a = points[i];
1004 let b = points[(i + 1) % points.len()];
1005 a.x * b.y - a.y * b.x
1006 })
1007 .sum();
1008 let expected = (u1 - u0) * (v1 - v0) * if face.same_sense { 1.0 } else { -1.0 };
1009 if !domain_cut || (0.5 * twice_area - expected).abs() > 1e-7 * expected.abs() {
1010 continue;
1011 }
1012 let cuts: std::collections::HashSet<_> = rest
1016 .iter()
1017 .filter_map(|c| {
1018 solid
1019 .edges
1020 .iter()
1021 .find(|e| e.id == c.edge_id && !e.degenerate)
1022 .map(|e| e.id)
1023 })
1024 .collect();
1025 for cut in solid.edges.iter().filter(|e| cuts.contains(&e.id)) {
1026 let users = solid
1027 .shells
1028 .iter()
1029 .flat_map(|s| &s.faces)
1030 .flat_map(|f| &f.loops)
1031 .flat_map(|l| &l.coedges)
1032 .filter(|c| c.edge_id == cut.id)
1033 .count();
1034 let exclusive_vertices = solid
1035 .edges
1036 .iter()
1037 .filter(|e| {
1038 e.start_vertex_id == cut.start_vertex_id
1039 || e.end_vertex_id == cut.start_vertex_id
1040 || e.start_vertex_id == cut.end_vertex_id
1041 || e.end_vertex_id == cut.end_vertex_id
1042 })
1043 .all(|e| e.degenerate || cuts.contains(&e.id) || e.id == edge_id);
1044 let mut same_parameter = cut.t0 >= v0 && cut.t1 <= v1 && cut.t0 < cut.t1;
1045 for i in 0..=16 {
1046 let t = cut.t0 + (cut.t1 - cut.t0) * i as f64 / 16.0;
1047 let point = cut
1048 .curve
1049 .evaluate(t)
1050 .or_refuse(KernelStage::Collect, "domain_cut_curve")?;
1051 let carrier = face
1052 .surface
1053 .evaluate(u0, t)
1054 .or_refuse(KernelStage::Collect, "domain_cut_carrier")?;
1055 same_parameter &= point.sub(carrier).length() <= band;
1056 }
1057 domain_cut &= users == 2 && exclusive_vertices && same_parameter;
1058 }
1059 if !domain_cut {
1060 continue;
1061 }
1062 cut_ids.extend(cuts);
1063 additions.push(LoopRecord {
1064 id: take_id(),
1065 coedges: vec![rim.coedges.remove(position)],
1066 });
1067 separated = true;
1068 }
1069 if !additions.is_empty() {
1070 let real = |id: u64| {
1074 !cut_ids.contains(&id) && !solid.edges.iter().any(|e| e.id == id && e.degenerate)
1075 };
1076 let curves: Vec<_> = face
1077 .loops
1078 .iter()
1079 .flat_map(|l| &l.coedges)
1080 .chain(additions.iter().flat_map(|l| &l.coedges))
1081 .filter(|c| real(c.edge_id) || c.edge_id == edge_id)
1082 .map(|c| &c.pcurve)
1083 .collect();
1084 let mut candidates = Vec::new();
1085 for shift in [0.25, 0.5, 0.75] {
1086 let mut clearance = f64::INFINITY;
1087 let mut fits = true;
1088 for curve in &curves {
1089 let first = curve.control_points[0].x / curve.control_points[0].w;
1090 let offset = -(first - shift).floor();
1091 for p in &curve.control_points {
1092 let u = p.x / p.w - shift + offset;
1093 fits &= u >= 0.0 && u <= 1.0;
1094 clearance = clearance.min(u.min(1.0 - u));
1095 }
1096 }
1097 if fits {
1098 candidates.push((clearance, shift));
1099 }
1100 }
1101 let Some((_, shift)) = candidates.into_iter().max_by(|a, b| a.0.total_cmp(&b.0)) else {
1102 return Err(KernelRefusal::unsupported(
1103 KernelStage::Collect,
1104 "sphere_domain_cut",
1105 "blend: no quarter-turn sphere chart keeps the real trims in-domain",
1106 ));
1107 };
1108 let seam_direction = face
1109 .surface
1110 .evaluate(shift, 0.5)
1111 .or_refuse(KernelStage::Collect, "sphere_cut_direction")?
1112 .sub(atlas.centre);
1113 let surface = crate::make_sphere_surface_framed(
1114 atlas.centre,
1115 atlas.radius,
1116 atlas.basis[2],
1117 Some(seam_direction),
1118 )
1119 .or_refuse(KernelStage::Collect, "sphere_cut_surface")?;
1120 for i in 0..=16 {
1122 for v in [0.0, 0.3, 0.5, 0.7, 1.0] {
1123 let u = i as f64 / 16.0;
1124 let old = face
1125 .surface
1126 .evaluate(u, v)
1127 .or_refuse(KernelStage::Collect, "sphere_cut_old")?;
1128 let new = surface
1129 .evaluate((u - shift).rem_euclid(1.0), v)
1130 .or_refuse(KernelStage::Collect, "sphere_cut_new")?;
1131 if old.sub(new).length() > band {
1132 return Err(KernelRefusal::unsupported(
1133 KernelStage::Collect,
1134 "sphere_cut_chart",
1135 "blend: the sphere quarter turn is not the same carrier",
1136 ));
1137 }
1138 }
1139 }
1140 let seam = surface
1141 .iso_curve_u(0.0)
1142 .or_refuse(KernelStage::Collect, "sphere_cut_seam")?;
1143 for coedge in face
1144 .loops
1145 .iter_mut()
1146 .flat_map(|l| &mut l.coedges)
1147 .chain(additions.iter_mut().flat_map(|l| &mut l.coedges))
1148 {
1149 if solid
1150 .edges
1151 .iter()
1152 .any(|e| e.id == coedge.edge_id && e.degenerate)
1153 {
1154 continue;
1155 }
1156 if !cut_ids.contains(&coedge.edge_id) {
1157 let first =
1158 coedge.pcurve.control_points[0].x / coedge.pcurve.control_points[0].w;
1159 let offset = -(first - shift).floor() - shift;
1160 for p in &mut coedge.pcurve.control_points {
1161 p.x += offset * p.w;
1162 }
1163 }
1164 }
1165 for cut in result.edges.iter_mut().filter(|e| cut_ids.contains(&e.id)) {
1166 for (vertex_id, t) in [(cut.start_vertex_id, cut.t0), (cut.end_vertex_id, cut.t1)] {
1167 let point = seam
1168 .evaluate(t)
1169 .or_refuse(KernelStage::Collect, "sphere_cut_vertex")?;
1170 let vertex = result
1171 .vertices
1172 .iter_mut()
1173 .find(|v| v.id == vertex_id)
1174 .ok_or(KernelRefusal::internal(
1175 KernelStage::Collect,
1176 "sphere_cut_vertex",
1177 "blend: sphere seam vertex is missing",
1178 ))?;
1179 vertex.point = point;
1180 }
1181 cut.curve = seam.clone();
1182 }
1183 face.surface = surface;
1184 }
1185 face.loops.extend(additions);
1186 }
1187 if !separated {
1188 return Ok(None);
1189 }
1190 let old_vertex = solid
1191 .vertices
1192 .iter()
1193 .find(|v| v.id == edge.start_vertex_id)
1194 .ok_or(KernelRefusal::internal(
1195 KernelStage::Collect,
1196 "domain_cut_vertex",
1197 "blend: closed ring vertex is missing",
1198 ))?;
1199 let vertex_id = take_id();
1200 result.vertices.push(VertexRecord {
1201 id: vertex_id,
1202 point: old_vertex.point,
1203 });
1204 let ring = result
1205 .edges
1206 .iter_mut()
1207 .find(|e| e.id == edge_id)
1208 .ok_or(KernelRefusal::internal(
1209 KernelStage::Collect,
1210 "domain_cut_ring",
1211 "blend: closed ring edge is missing",
1212 ))?;
1213 ring.start_vertex_id = vertex_id;
1214 ring.end_vertex_id = vertex_id;
1215 Ok(Some(result))
1216}
1217
1218fn remove_retraced_cuts(
1224 solid: &BrepSolid,
1225 edge_id: u64,
1226) -> Result<Option<BrepSolid>, KernelRefusal> {
1227 let mut result = solid.clone();
1228 let mut removed = Vec::new();
1229 for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
1230 for rim in &mut face.loops {
1231 if !rim.coedges.iter().any(|coedge| coedge.edge_id == edge_id) {
1232 continue;
1233 }
1234 loop {
1235 let n = rim.coedges.len();
1236 let mut pair = None;
1237 for i in 0..n {
1238 let j = (i + 1) % n;
1239 let (a, b) = (&rim.coedges[i], &rim.coedges[j]);
1240 if a.edge_id == edge_id || a.edge_id != b.edge_id || a.forward == b.forward {
1241 continue;
1242 }
1243 let users = solid
1244 .shells
1245 .iter()
1246 .flat_map(|s| &s.faces)
1247 .flat_map(|f| &f.loops)
1248 .flat_map(|l| &l.coedges)
1249 .filter(|c| c.edge_id == a.edge_id)
1250 .count();
1251 if users != 2 {
1252 continue;
1253 }
1254 let [a0, a1] = a
1255 .pcurve
1256 .domain()
1257 .or_refuse(KernelStage::Collect, "cut_domain")?;
1258 let [b0, b1] = b
1259 .pcurve
1260 .domain()
1261 .or_refuse(KernelStage::Collect, "cut_domain")?;
1262 let agrees =
1265 a.pcurve.degree == b.pcurve.degree
1266 && a.pcurve.knots.len() == b.pcurve.knots.len()
1267 && a.pcurve.control_points.len() == b.pcurve.control_points.len()
1268 && a.pcurve.knots.iter().zip(b.pcurve.knots.iter().rev()).all(
1269 |(x, y)| {
1270 ((x - a0) / (a1 - a0) - (b1 - y) / (b1 - b0)).abs() <= 1e-9
1271 },
1272 )
1273 && a.pcurve
1274 .control_points
1275 .iter()
1276 .zip(b.pcurve.control_points.iter().rev())
1277 .all(|(p, q)| {
1278 p.point()
1279 .ok()
1280 .zip(q.point().ok())
1281 .is_some_and(|(p, q)| p.sub(q).length() <= 1e-9)
1282 && (p.w / a.pcurve.control_points[0].w
1283 - q.w / b.pcurve.control_points.last().unwrap().w)
1284 .abs()
1285 <= 1e-9
1286 });
1287 if agrees {
1288 pair = Some((i, j, a.edge_id));
1289 break;
1290 }
1291 }
1292 let Some((i, j, id)) = pair else {
1293 break;
1294 };
1295 rim.coedges.remove(i.max(j));
1296 rim.coedges.remove(i.min(j));
1297 removed.push(id);
1298 }
1299 }
1300 }
1301 if removed.is_empty() {
1302 return Ok(None);
1303 }
1304 result.edges.retain(|edge| !removed.contains(&edge.id));
1305 super::open::prune_orphan_vertices(&mut result);
1306 Ok(Some(result))
1307}
1308
1309struct SecondSeamSplit {
1312 fit_parameter: f64,
1313 crossing_v: f64,
1314 period: f64,
1316}
1317
1318const CARVE_MAX_REFINEMENT_DEPTH: usize = MAX_REFINEMENT_DEPTH + 4;
1330
1331fn rails_off_closed_carriers(
1335 rows: &FittedRows,
1336 stations: &[Station],
1337 parameters: &[f64],
1338 first: &BlendMate,
1339 second: &BlendMate,
1340) -> Result<f64, KernelRefusal> {
1341 Ok(closed_rail_interval_misses(rows, stations, parameters,
1342 [&first.face.surface, &second.face.surface])?.into_iter().fold(0.0_f64, f64::max))
1343}
1344
1345fn carve_closed_wall(
1346 solid: &BrepSolid,
1347 rows: &FittedRows,
1348 stations: &[Station],
1349 parameters: &[f64],
1350 first: &BlendMate,
1351 second: &BlendMate,
1352 radius: f64,
1353) -> Result<crate::blend::carve::CarvedCrease, KernelRefusal> {
1354 let policy = crate::KernelTolerances::for_solid(solid, 1e-7);
1355 let rail_bar = 0.5 * policy.intersection_fit;
1356 let off = rails_off_closed_carriers(rows, stations, parameters, first, second)?;
1357 crate::blend::carve::carve_trace(format_args!(
1358 "blend closed-edge carve: {} stations, rails {off:.3e} off their carriers between stations (bar {rail_bar:.3e})",
1359 parameters.len()
1360 ));
1361 if off > rail_bar {
1362 return Err(crate::blend::fold::wall_fold_refusal(
1363 Vec::new(),
1364 format!(
1365 "{} {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",
1366 crate::blend::WALL_FOLDS
1367 ),
1368 ));
1369 }
1370 let Some(carved) = crate::blend::carve::trace_wall_crease(&rows.surface, radius)? else {
1371 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"));
1372 };
1373 let [u0, u1] = rows.u_domain;
1374 let margin = 1e-3 * (u1 - u0);
1375 let straddles = carved.ends.iter().any(|end| end[0] <= u0 + margin || end[0] >= u1 - margin);
1376 if straddles {
1377 return Err(crate::blend::fold::wall_fold_refusal(
1378 Vec::new(),
1379 format!(
1380 "{} {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",
1381 crate::blend::WALL_FOLDS, carved.ends[0][0], carved.ends[1][0]
1382 ),
1383 ));
1384 }
1385 crate::blend::carve::fit_crease(&rows.surface, &carved, policy.pcurve_consistency)
1386}
1387
1388fn closed_support_trim(rail: &NurbsCurve, old: &NurbsCurve, surface: &NurbsSurface) -> Result<NurbsCurve, KernelRefusal> {
1406 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1407 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1408 let within_floor = |candidate: &NurbsCurve, what: &str| -> bool {
1409 let _read_timing = crate::blend::BlendTimer::start("timing.closed.rows.trim_read_ms");
1410 let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), candidate, d0, d1);
1411 if std::env::var_os("BREP_DEBUG_TRACK_FIT").is_some() {
1412 match &read {
1413 Ok((miss, standoff, samples, worst)) => eprintln!(
1414 "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}",
1415 candidate.control_points.len()),
1416 Err(error) => eprintln!("TRACK_FIT closed-edge support, {what} candidate: unreadable: {error}"),
1417 }
1418 }
1419 read.is_ok_and(|(miss, _, _, _)| miss <= floor)
1420 };
1421 if let Some(affine) = plane_affine_pcurve(rail, surface) {
1422 if within_floor(&affine, "affine plane image") {
1423 crate::blend::record_count("count.closed.rows.trim.affine", 1);
1424 return Ok(affine);
1425 }
1426 }
1427 crate::blend::record_count("count.closed.rows.trim.refit", 1);
1428 refit_closed_support_pcurve(rail, old, surface)
1429}
1430
1431fn plane_affine_pcurve(rail: &NurbsCurve, surface: &NurbsSurface) -> Option<NurbsCurve> {
1437 if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) {
1438 return None;
1439 }
1440 let [u0, u1] = surface.domain_u().ok()?;
1441 let [v0, v1] = surface.domain_v().ok()?;
1442 let origin = surface.evaluate(u0, v0).ok()?;
1443 let du = surface.evaluate(u1, v0).ok()?.sub(origin).scale(1.0 / (u1 - u0));
1444 let dv = surface.evaluate(u0, v1).ok()?.sub(origin).scale(1.0 / (v1 - v0));
1445 let (a, b, c) = (du.dot(du), du.dot(dv), dv.dot(dv));
1446 let det = a * c - b * b;
1447 let chart_scale = origin.length().max(du.length()).max(dv.length()).max(1.0);
1448 if !(det > 1e-24 * chart_scale.powi(4)) {
1449 return None;
1450 }
1451 let mut pcurve = rail.clone();
1452 for control in &mut pcurve.control_points {
1453 let weight = control.w;
1454 let offset = Vec3::new(control.x / weight, control.y / weight, control.z / weight).sub(origin);
1455 let (r, s) = (offset.dot(du), offset.dot(dv));
1456 control.x = (u0 + (c * r - b * s) / det) * weight;
1457 control.y = (v0 + (a * s - b * r) / det) * weight;
1458 control.z = 0.0;
1459 }
1460 Some(pcurve)
1461}
1462
1463pub(in crate::blend) fn refit_closed_support_pcurve(rail: &NurbsCurve, old: &NurbsCurve, surface: &NurbsSurface) -> Result<NurbsCurve, KernelRefusal> {
1488 let knot_retry = KNOT_RETRY.with(|flag| flag.get());
1489 refit_closed_support_pcurve_read_with(rail, old, surface, &mut crate::blend::track_fit::FootMemo::default(), knot_retry).map(|(fitted, _)| fitted)
1490}
1491
1492thread_local! {
1493 static KNOT_RETRY: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1497}
1498
1499pub(in crate::blend) fn refit_closed_support_pcurve_read(
1507 rail: &NurbsCurve,
1508 old: &NurbsCurve,
1509 surface: &NurbsSurface,
1510 memo: &mut crate::blend::track_fit::FootMemo,
1511) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1512 let knot_retry = KNOT_RETRY.with(|flag| flag.get());
1515 refit_closed_support_pcurve_read_with(rail, old, surface, memo, knot_retry)
1516}
1517
1518pub fn with_pcurve_knot_retry<T>(build: impl FnOnce() -> T) -> T {
1522 let previous = KNOT_RETRY.with(|flag| flag.replace(true));
1523 let built = build();
1524 KNOT_RETRY.with(|flag| flag.set(previous));
1525 built
1526}
1527
1528fn refit_closed_support_pcurve_read_with(
1531 rail: &NurbsCurve,
1532 old: &NurbsCurve,
1533 surface: &NurbsSurface,
1534 memo: &mut crate::blend::track_fit::FootMemo,
1535 knot_retry: bool,
1536) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1537 const SHIPPED_READ_REPAIRS: usize = 3;
1538 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1539 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1540 let mut near: Vec<f64> = Vec::new();
1541 let (mut fitted, mut read) = fit_and_read_support(rail, old, surface, &near, memo, knot_retry)?;
1545 for _ in 0..SHIPPED_READ_REPAIRS {
1546 if read.0 <= floor || !(d1 > d0) {
1547 break;
1548 }
1549 near.push((read.3 - d0) / (d1 - d0));
1550 crate::blend::record_count("count.surgery.refit_repairs", 1);
1551 match fit_and_read_support(rail, old, surface, &near, memo, false) {
1554 Ok(repaired) => (fitted, read) = repaired,
1555 Err(_) => break,
1556 }
1557 }
1558 let (miss, standoff, samples, _) = read;
1559 if !(miss <= floor) {
1560 return Err(KernelRefusal::non_convergence(KernelStage::Refine, CLOSED_SUPPORT_PCURVE_FLOOR_WHAT, format!(
1561 "{} a blend's support pcurve, as shipped, misses its rail's track by {miss:.3e} at {samples} samples, \
1562 against a floor of {floor:.1e}",
1563 crate::blend::PCURVE_OFF_FLOOR,
1564 )));
1565 }
1566 Ok((fitted, read))
1567}
1568
1569fn fit_and_read_support(
1573 rail: &NurbsCurve,
1574 old: &NurbsCurve,
1575 surface: &NurbsSurface,
1576 near: &[f64],
1577 memo: &mut crate::blend::track_fit::FootMemo,
1578 knot_retry: bool,
1579) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1580 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1581 let guide = crate::blend::chain::pcurve_guide(old, surface)?;
1584 let track_timing = crate::blend::BlendTimer::start("timing.support_refit.track_ms");
1585 let mut fitted = if knot_retry {
1586 crate::blend::chain::project_piece_pcurve_within_knot_retry(rail, surface, false, false, Some(&guide), near)?
1587 } else {
1588 crate::blend::chain::project_piece_pcurve_within(rail, surface, false, false, Some(&guide), near)?
1589 };
1590 track_timing.stop();
1591 let [f0, f1] = fitted.domain().or_refuse(KernelStage::Refine, "domain")?;
1594 for knot in fitted.knots.iter_mut() {
1595 *knot = d0 + (*knot - f0) / (f1 - f0) * (d1 - d0);
1596 }
1597 let debug = std::env::var_os("BREP_DEBUG_TRACK_FIT").is_some();
1598 if debug {
1599 match crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &fitted, d0, d1) {
1602 Ok((miss, standoff, samples, worst)) => eprintln!(
1603 "TRACK_FIT closed-edge support: before the seam ends, miss {miss:.3e} at t {worst:.9} ({samples} samples), standoff {standoff:.3e}"),
1604 Err(error) => eprintln!("TRACK_FIT closed-edge support: before the seam ends, unreadable: {error}"),
1605 }
1606 }
1607 let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
1608 let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1609 let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1610 let old_start = old.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?;
1611 let old_end = old.evaluate(d1).or_refuse(KernelStage::Refine, "evaluate")?;
1612 let new_start = fitted.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?;
1613 let shift = |closed: bool, from: f64, to: f64, period: f64| if closed { ((to - from) / period).round() * period } else { 0.0 };
1615 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));
1616 for point in fitted.control_points.iter_mut() {
1617 point.x += shift_u * point.w;
1618 point.y += shift_v * point.w;
1619 }
1620 let on_seam = |closed: bool, value: f64, low: f64, high: f64| {
1621 let period = high - low;
1622 closed && ((value - low) / period - ((value - low) / period).round()).abs() * period <= 1e-12 * period.max(1.0)
1623 };
1624 let last = fitted.control_points.len() - 1;
1625 for (index, end) in [(0, old_start), (last, old_end)] {
1626 let point = &mut fitted.control_points[index];
1627 if on_seam(closed_u, end.x, u0, u1) {
1628 point.x = end.x * point.w;
1629 }
1630 if on_seam(closed_v, end.y, v0, v1) {
1631 point.y = end.y * point.w;
1632 }
1633 }
1634 let mut own = crate::blend::track_fit::FootMemo::default();
1644 let (miss, standoff, samples, worst) =
1645 crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut own)?;
1646 *memo = own;
1647 if debug {
1648 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}");
1649 }
1650 Ok((fitted, (miss, standoff, samples, worst)))
1651}
1652
1653
1654fn build_surgery(
1655 solid: &BrepSolid,
1656 edge: &EdgeRecord,
1657 first: &BlendMate,
1658 second: &BlendMate,
1659 rows: FittedRows,
1660 second_split: Option<SecondSeamSplit>,
1661 name: Option<&str>,
1662 crease: Option<crate::blend::carve::CarvedCrease>,
1663) -> Result<BrepSolid, KernelRefusal> {
1664 let mut result = solid.clone();
1665 let mut take_id = crate::blend::edge::fresh_id_source(solid);
1666
1667 let [u_start, u_end] = rows.u_domain;
1668 let cr_start = rows.cr.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1669 let cs_start = rows.cs.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1670 let vertex1_id = take_id();
1671 let vertex2_id = take_id();
1672 result.vertices.push(VertexRecord {
1673 id: vertex1_id,
1674 point: cr_start,
1675 });
1676 result.vertices.push(VertexRecord {
1677 id: vertex2_id,
1678 point: cs_start,
1679 });
1680
1681 let cr_edge_id = take_id();
1682 result.edges.push(EdgeRecord {
1683 id: cr_edge_id,
1684 curve: rows.cr.clone(),
1685 t0: u_start,
1686 t1: u_end,
1687 start_vertex_id: vertex1_id,
1688 end_vertex_id: vertex1_id,
1689 degenerate: false,
1690 name: None,
1691 });
1692 let mut cs_pieces: Vec<(u64, [f64; 2], NurbsCurve)> = Vec::new();
1696 let mut second_seam_vertex = vertex2_id;
1697 let mut second_seam_point = cs_start;
1698 let mut second_crossing_v = 0.0;
1699 if let Some(split) = &second_split {
1700 let p = split.fit_parameter;
1701 second_crossing_v = split.crossing_v;
1702 let (cs_a, cs_b) = rows.cs.split(p).or_refuse(KernelStage::Refine, "split")?;
1703 let (pc_a, pc_b) = rows.cs_pcurve.split(p).or_refuse(KernelStage::Refine, "split")?;
1704 let mut pc_b = pc_b;
1706 for point in pc_b.control_points.iter_mut() {
1707 point.x -= split.period * point.w;
1708 }
1709 let w2s = take_id();
1710 second_seam_vertex = w2s;
1711 second_seam_point = rows.cs.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?;
1712 result.vertices.push(VertexRecord {
1713 id: w2s,
1714 point: second_seam_point,
1715 });
1716 let edge_a = take_id();
1717 result.edges.push(EdgeRecord {
1718 id: edge_a,
1719 curve: cs_a,
1720 t0: u_start,
1721 t1: p,
1722 start_vertex_id: vertex2_id,
1723 end_vertex_id: w2s,
1724 degenerate: false,
1725 name: None,
1726 });
1727 cs_pieces.push((edge_a, [u_start, p], pc_a));
1728 let edge_b = take_id();
1729 result.edges.push(EdgeRecord {
1730 id: edge_b,
1731 curve: cs_b,
1732 t0: p,
1733 t1: u_end,
1734 start_vertex_id: w2s,
1735 end_vertex_id: vertex2_id,
1736 degenerate: false,
1737 name: None,
1738 });
1739 cs_pieces.push((edge_b, [p, u_end], pc_b));
1740 } else {
1741 let cs_edge_id = take_id();
1742 result.edges.push(EdgeRecord {
1743 id: cs_edge_id,
1744 curve: rows.cs.clone(),
1745 t0: u_start,
1746 t1: u_end,
1747 start_vertex_id: vertex2_id,
1748 end_vertex_id: vertex2_id,
1749 degenerate: false,
1750 name: None,
1751 });
1752 cs_pieces.push((cs_edge_id, [u_start, u_end], rows.cs_pcurve.clone()));
1753 }
1754 let seam_curve = rows.surface.iso_curve_u(u_start).or_refuse(KernelStage::Refine, "iso_curve_u")?;
1755 let [seam_t0, seam_t1] = seam_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1756 let blend_seam_id = take_id();
1757 result.edges.push(EdgeRecord {
1758 id: blend_seam_id,
1759 curve: seam_curve,
1760 t0: seam_t0,
1761 t1: seam_t1,
1762 start_vertex_id: vertex1_id,
1763 end_vertex_id: vertex2_id,
1764 degenerate: false,
1765 name: None,
1766 });
1767
1768 let replace_in_face = |result: &mut BrepSolid, mate: &BlendMate, pieces: &[(u64, [f64; 2], NurbsCurve)], uv_new: [f64; 2]| -> Result<bool, KernelRefusal> {
1769 replace_rim_in_face(result, edge, mate, pieces, uv_new)
1770 };
1771 let uv1_new = rows.cr_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1772 let v1_new = uv1_new.y;
1773 let cr_pieces = vec![(cr_edge_id, [u_start, u_end], rows.cr_pcurve.clone())];
1774 let mut repaired = replace_in_face(&mut result, first, &cr_pieces, [uv1_new.x, v1_new])?;
1775 let uv2_new = rows.cs_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1776 let v2_new = if second_split.is_some() {
1777 second_crossing_v
1778 } else {
1779 uv2_new.y
1780 };
1781 repaired |= replace_in_face(&mut result, second, &cs_pieces, [uv2_new.x, v2_new])?;
1782
1783 let collapse_winding_sphere_cap =
1789 |result: &mut BrepSolid, face_id: u64, support_ids: &[u64]| -> Result<(), KernelRefusal> {
1790 let face = result
1791 .shells
1792 .iter_mut()
1793 .flat_map(|shell| &mut shell.faces)
1794 .find(|face| face.id == face_id)
1795 .ok_or(KernelRefusal::internal(KernelStage::Sew, "sphere_carrier", "blend: sphere carrier lost during cap surgery"))?;
1796 if !matches!(
1797 face.surface.analytic(),
1798 Some(crate::AnalyticSurface::Sphere { .. })
1799 ) || face.surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")? != (true, false)
1800 {
1801 return Ok(());
1802 }
1803 let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1804 let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1805 let period = u1 - u0;
1806 let support_loop = face.loops.iter().position(|loop_record| {
1807 !loop_record.coedges.is_empty()
1808 && loop_record
1809 .coedges
1810 .iter()
1811 .all(|coedge| support_ids.contains(&coedge.edge_id))
1812 });
1813 let Some(support_loop) = support_loop else {
1814 return Ok(());
1815 };
1816 if face.loops[support_loop].coedges.len() != 1 {
1817 return Ok(());
1818 }
1819 let support = &face.loops[support_loop].coedges[0];
1820 let [s0, s1] = support.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1821 let support_start = support.pcurve.evaluate(s0).or_refuse(KernelStage::Refine, "evaluate")?;
1822 let support_end = support.pcurve.evaluate(s1).or_refuse(KernelStage::Refine, "evaluate")?;
1823 let support_winding = support_end.x - support_start.x;
1824 if (support_winding.abs() - period).abs() > 0.05 * period {
1825 return Ok(());
1826 }
1827
1828 let mut pole: Option<(usize, CoedgeRecord)> = None;
1829 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1830 if loop_index == support_loop {
1831 continue;
1832 }
1833 for coedge in &loop_record.coedges {
1834 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1835 let start = coedge.pcurve.evaluate(p0).or_refuse(KernelStage::Refine, "evaluate")?;
1836 let end = coedge.pcurve.evaluate(p1).or_refuse(KernelStage::Refine, "evaluate")?;
1837 let winding = end.x - start.x;
1838 let at_pole = (start.y - v0).abs() <= 1e-8 || (start.y - v1).abs() <= 1e-8;
1839 if at_pole
1840 && (end.y - start.y).abs() <= 1e-8
1841 && (winding.abs() - period).abs() <= 0.05 * period
1842 && winding * support_winding < 0.0
1843 {
1844 pole = Some((loop_index, coedge.clone()));
1845 break;
1846 }
1847 }
1848 if pole.is_some() {
1849 break;
1850 }
1851 }
1852 let Some((pole_loop, pole_coedge)) = pole else {
1853 return Ok(());
1854 };
1855 let support_record = face.loops[support_loop].clone();
1856 let pole_id = face.loops[pole_loop].id;
1857 face.loops = vec![
1858 LoopRecord {
1859 id: pole_id,
1860 coedges: vec![pole_coedge],
1861 },
1862 support_record,
1863 ];
1864 Ok(())
1865 };
1866 collapse_winding_sphere_cap(&mut result, first.face.id, &[cr_edge_id])?;
1867 let cs_edge_ids: Vec<u64> = cs_pieces.iter().map(|(edge_id, ..)| *edge_id).collect();
1868 collapse_winding_sphere_cap(&mut result, second.face.id, &cs_edge_ids)?;
1869 for shell in &mut result.shells {
1871 for face in &mut shell.faces {
1872 for loop_record in &mut face.loops {
1873 for coedge in &mut loop_record.coedges {
1874 if coedge.id == 0 {
1875 coedge.id = take_id();
1876 }
1877 }
1878 }
1879 }
1880 }
1881
1882 let new_edge_ids: Vec<u64> = std::iter::once(cr_edge_id)
1887 .chain(cs_pieces.iter().map(|(edge_id, ..)| *edge_id))
1888 .chain(std::iter::once(blend_seam_id))
1889 .collect();
1890 let first_face_edges: Vec<u64> = first
1891 .face
1892 .loops
1893 .iter()
1894 .flat_map(|loop_record| loop_record.coedges.iter().map(|coedge| coedge.edge_id))
1895 .collect();
1896 repaired |= trim_seam_edges(
1897 &mut result,
1898 edge,
1899 &new_edge_ids,
1900 &first_face_edges,
1901 (vertex1_id, v1_new, cr_start),
1902 (second_seam_vertex, v2_new, second_seam_point),
1903 )?;
1904
1905 let mid_u = (u_start + u_end) * 0.5;
1907 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
1908 let station_uv = rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?;
1909 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
1910 let out1 = if first.face.same_sense {
1911 n1
1912 } else {
1913 n1.scale(-1.0)
1914 };
1915 let same_sense = blend_normal.dot(out1) >= 0.0;
1916 let loop_id = take_id();
1917 let mut coedges = vec![
1918 CoedgeRecord {
1919 id: take_id(),
1920 edge_id: cr_edge_id,
1921 forward: true,
1922 pcurve: crate::sweep_topology::parameter_line(u_start, 0.0, u_end, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1923 },
1924 CoedgeRecord {
1925 id: take_id(),
1926 edge_id: blend_seam_id,
1927 forward: true,
1928 pcurve: crate::sweep_topology::parameter_line(u_end, 0.0, u_end, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1929 },
1930 ];
1931 for (edge_id, window, _) in cs_pieces.iter().rev() {
1932 coedges.push(CoedgeRecord {
1933 id: take_id(),
1934 edge_id: *edge_id,
1935 forward: false,
1936 pcurve: crate::sweep_topology::parameter_line(window[1], 1.0, window[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1937 });
1938 }
1939 coedges.push(CoedgeRecord {
1940 id: take_id(),
1941 edge_id: blend_seam_id,
1942 forward: false,
1943 pcurve: crate::sweep_topology::parameter_line(u_start, 1.0, u_start, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1944 });
1945 if !same_sense {
1955 coedges.reverse();
1956 for coedge in &mut coedges {
1957 coedge.forward = !coedge.forward;
1958 coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
1959 }
1960 }
1961 let mut loops = vec![LoopRecord {
1962 id: loop_id,
1963 coedges,
1964 }];
1965 if let Some(crease) = crease {
1974 let start_vertex = take_id();
1975 let end_vertex = take_id();
1976 let crease_edge = take_id();
1977 result.vertices.push(VertexRecord { id: start_vertex, point: crease.start });
1978 result.vertices.push(VertexRecord { id: end_vertex, point: crease.end });
1979 let [crease_t0, crease_t1] = crease.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1980 result.edges.push(EdgeRecord {
1981 id: crease_edge,
1982 curve: crease.curve,
1983 t0: crease_t0,
1984 t1: crease_t1,
1985 start_vertex_id: start_vertex,
1986 end_vertex_id: end_vertex,
1987 degenerate: false,
1988 name: name.map(|value| format!("{value}:CREASE")),
1989 });
1990 let outer = crate::blend::chain::signed_area(&loops[0].coedges)?;
1991 let forward_first = crate::blend::chain::signed_area(&[
1992 CoedgeRecord { id: 0, edge_id: crease_edge, forward: true, pcurve: crease.first.clone() },
1993 CoedgeRecord { id: 0, edge_id: crease_edge, forward: false, pcurve: crease.second.reversed().or_refuse(KernelStage::Refine, "reversed")? },
1994 ])?;
1995 let inner = if outer * forward_first < 0.0 {
1996 vec![
1997 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: true, pcurve: crease.first },
1998 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: false, pcurve: crease.second.reversed().or_refuse(KernelStage::Refine, "reversed")? },
1999 ]
2000 } else {
2001 vec![
2002 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: true, pcurve: crease.second },
2003 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: false, pcurve: crease.first.reversed().or_refuse(KernelStage::Refine, "reversed")? },
2004 ]
2005 };
2006 loops.push(LoopRecord { id: take_id(), coedges: inner });
2007 }
2008 let blend_face = FaceRecord {
2009 id: take_id(),
2010 surface: rows.surface,
2011 same_sense,
2012 loops,
2013 name: name.map(|value| value.to_string()),
2014 };
2015 let shell_index = result
2016 .shells
2017 .iter()
2018 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
2019 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
2020 result.shells[shell_index].faces.push(blend_face);
2021
2022 let used_edges: std::collections::HashSet<u64> = result
2023 .shells
2024 .iter()
2025 .flat_map(|shell| &shell.faces)
2026 .flat_map(|face| &face.loops)
2027 .flat_map(|loop_record| &loop_record.coedges)
2028 .map(|coedge| coedge.edge_id)
2029 .collect();
2030 result
2031 .edges
2032 .retain(|candidate| used_edges.contains(&candidate.id));
2033 let used_vertices: std::collections::HashSet<u64> = result
2034 .edges
2035 .iter()
2036 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
2037 .collect();
2038 result
2039 .vertices
2040 .retain(|candidate| used_vertices.contains(&candidate.id));
2041 if repaired {
2048 let problems = result.validate_memoised();
2049 if !problems.is_empty() {
2050 return Err(KernelRefusal::internal(KernelStage::Validate, "neighbour_trim_closure", format!(
2051 "blend: the repaired neighbour trim did not close ({} issue(s), first: {})",
2052 problems.len(),
2053 problems
2054 .first()
2055 .map(|issue| issue.message.as_str())
2056 .unwrap_or("unknown")
2057 )));
2058 }
2059 }
2060 Ok(result)
2061}
2062
2063pub(in crate::blend) fn replace_rim_in_face(
2069 result: &mut BrepSolid,
2070 edge: &EdgeRecord,
2071 mate: &BlendMate,
2072 pieces: &[(u64, [f64; 2], NurbsCurve)],
2073 uv_new: [f64; 2],
2074) -> Result<bool, KernelRefusal> {
2075 let old_vertex = edge.start_vertex_id;
2076 let v_new = uv_new[1];
2077 let seam_edge_ends: Vec<(u64, bool, bool)> = result
2079 .edges
2080 .iter()
2081 .filter(|candidate| {
2082 candidate.id != edge.id
2083 && (candidate.start_vertex_id == old_vertex
2084 || candidate.end_vertex_id == old_vertex)
2085 })
2086 .map(|candidate| {
2087 (
2088 candidate.id,
2089 candidate.start_vertex_id == old_vertex,
2090 candidate.end_vertex_id == old_vertex,
2091 )
2092 })
2093 .collect();
2094 let seam_edge_ids: Vec<u64> = seam_edge_ends.iter().map(|(id, ..)| *id).collect();
2095 let pcurve_band = crate::KernelTolerances::for_solid(result, 1e-7).pcurve_consistency;
2096 let debug = std::env::var("BREP_DEBUG_NETWORK").is_ok();
2097 let face = result
2098 .shells
2099 .iter_mut()
2100 .flat_map(|shell| &mut shell.faces)
2101 .find(|face| face.id == mate.face.id)
2102 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
2103 let face_id = face.id;
2104 let FaceRecord { surface, loops, .. } = &mut *face;
2105 let loop_record = &mut loops[mate.loop_index];
2106 let position = loop_record
2107 .coedges
2108 .iter()
2109 .position(|coedge| coedge.edge_id == edge.id)
2110 .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
2111 let old_forward = loop_record.coedges[position].forward;
2112 let old_seam_v = {
2113 let pcurve = &loop_record.coedges[position].pcurve;
2114 let [d0, _] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2115 pcurve.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?.y
2116 };
2117 let mut replacements = Vec::new();
2120 let ordered: Vec<&(u64, [f64; 2], NurbsCurve)> = if old_forward {
2121 pieces.iter().collect()
2122 } else {
2123 pieces.iter().rev().collect()
2124 };
2125 for (index, (edge_id, _, pcurve)) in ordered.iter().enumerate() {
2126 replacements.push(CoedgeRecord {
2127 id: if index == 0 {
2128 loop_record.coedges[position].id
2129 } else {
2130 0 },
2132 edge_id: *edge_id,
2133 forward: old_forward,
2134 pcurve: if old_forward {
2135 pcurve.clone()
2136 } else {
2137 pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
2138 },
2139 });
2140 }
2141 loop_record
2142 .coedges
2143 .splice(position..=position, replacements);
2144 let mut repaired = false;
2145 if loop_record.coedges.len() > 1 {
2146 for coedge in &mut loop_record.coedges {
2152 if pieces
2153 .iter()
2154 .any(|(edge_id, ..)| *edge_id == coedge.edge_id)
2155 || !seam_edge_ids.contains(&coedge.edge_id)
2156 {
2157 continue;
2158 }
2159 let controls = &coedge.pcurve.control_points;
2160 let last_index = controls.len() - 1;
2161 let first_u = controls[0].x / controls[0].w;
2162 let last_u = controls[last_index].x / controls[last_index].w;
2163 let constant_u = (first_u - last_u).abs()
2164 <= 1e-9 * (1.0 + first_u.abs().max(last_u.abs()));
2165 let (target, target_uv, fraction) = if constant_u {
2168 let first_v = controls[0].y / controls[0].w;
2172 let last_v = controls[last_index].y / controls[last_index].w;
2173 let target = if (first_v - old_seam_v).abs() < (last_v - old_seam_v).abs() {
2174 0
2175 } else {
2176 last_index
2177 };
2178 let span = last_v - first_v;
2179 let fraction = if span.abs() > 0.0 { (v_new - first_v) / span } else { 0.0 };
2180 (target, [first_u, v_new], fraction)
2181 } else {
2182 let Some((_, start_is_old, end_is_old)) = seam_edge_ends
2192 .iter()
2193 .find(|(id, ..)| *id == coedge.edge_id)
2194 .copied()
2195 else {
2196 continue;
2197 };
2198 if start_is_old == end_is_old {
2199 return Err(KernelRefusal::ill_posed(KernelStage::Sew, "neighbour_trim_end", format!(
2200 "blend: neighbour edge {} meets the blended edge at both ends; \
2201 its trim end is ambiguous",
2202 coedge.edge_id
2203 )));
2204 }
2205 let target = if start_is_old == coedge.forward { 0 } else { last_index };
2208 let v_end = controls[target].y / controls[target].w;
2209 let span = last_u - first_u;
2210 let fraction = if span.abs() > 0.0 { (uv_new[0] - first_u) / span } else { 0.0 };
2211 (target, [uv_new[0], v_end], fraction)
2212 };
2213 let [q0, q1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2225 let target_point = surface
2226 .evaluate_extended(target_uv[0], target_uv[1])
2227 .or_refuse(KernelStage::Refine, "evaluate_extended")?;
2228 let seed = (q0 + (q1 - q0) * fraction).clamp(q0.min(q1), q0.max(q1));
2234 let q_split = trim_parameter_at_point(
2235 surface, &coedge.pcurve, seed, target_point, pcurve_band,
2236 )?;
2237 let interior = q_split > q0 + 1e-9 * (q1 - q0) && q_split < q1 - 1e-9 * (q1 - q0);
2238 if debug {
2239 eprintln!(
2240 "closed-edge seam trim: face {face_id} edge {} coedge {} ({}): {} at q {q_split:.9} of [{q0:.6}, {q1:.6}], {} control points",
2241 coedge.edge_id, coedge.id, if constant_u { "meridian, constant u" } else { "along u" },
2242 if interior { "SPLIT" } else { "EXTEND" }, controls.len()
2243 );
2244 }
2245 if interior {
2246 let (low, high) = coedge.pcurve.split(q_split).or_refuse(KernelStage::Refine, "split")?;
2247 coedge.pcurve = if target == 0 { high } else { low };
2248 } else {
2249 if coedge.pcurve.degree != 1 || controls.len() != 2 {
2254 return Err(KernelRefusal::unsupported(KernelStage::Sew, "neighbour_seam_extend_curved", format!(
2255 "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)",
2256 coedge.edge_id, coedge.pcurve.degree, controls.len()
2257 )));
2258 }
2259 let w = coedge.pcurve.control_points[target].w;
2260 coedge.pcurve.control_points[target] = crate::Vec4::from_point(
2261 Vec3::new(target_uv[0], target_uv[1], 0.0), w,
2262 );
2263 }
2264 repaired = true;
2265 }
2266 }
2267 Ok(repaired)
2268}
2269
2270pub(in crate::blend) fn trim_seam_edges(
2276 result: &mut BrepSolid,
2277 edge: &EdgeRecord,
2278 new_edge_ids: &[u64],
2279 first_face_edges: &[u64],
2280 first_target: (u64, f64, Vec3),
2281 second_target: (u64, f64, Vec3),
2282) -> Result<bool, KernelRefusal> {
2283 let old_vertex = edge.start_vertex_id;
2284 let mut repaired = false;
2285 let (vertex1_id, v1_new, cr_start) = first_target;
2286 let (second_seam_vertex, v2_new, second_seam_point) = second_target;
2287 for seam_edge in result.edges.iter_mut() {
2288 if new_edge_ids.contains(&seam_edge.id) {
2289 continue;
2290 }
2291 if seam_edge.start_vertex_id != old_vertex && seam_edge.end_vertex_id != old_vertex {
2292 continue;
2293 }
2294 let (target_vertex, target_v, target_point) = if first_face_edges.contains(&seam_edge.id) {
2295 (vertex1_id, v1_new, cr_start)
2296 } else {
2297 (second_seam_vertex, v2_new, second_seam_point)
2298 };
2299 let [c0, c1] = seam_edge.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
2300 let clamped = target_v.clamp(c0.min(c1), c0.max(c1));
2301 let band = 1e-6 * (1.0 + target_point.length());
2310 let split_parameter = if seam_edge.id == edge.id {
2311 clamped
2317 } else {
2318 match seam_edge.curve.evaluate(clamped) {
2319 Ok(point) if point.sub(target_point).length() <= band => clamped,
2320 _ => {
2321 repaired = true;
2322 let projection =
2323 crate::project_point_to_curve(&seam_edge.curve, target_point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
2324 if projection.distance > band {
2325 return Err(KernelRefusal::unsupported(KernelStage::Sew, "neighbour_off_support", format!(
2326 "blend: neighbour edge {} does not pass through the blend's support \
2327 start (off by {:.9}); the trim has no parameter to move to",
2328 seam_edge.id, projection.distance
2329 )));
2330 }
2331 projection.u.clamp(c0.min(c1), c0.max(c1))
2332 }
2333 }
2334 };
2335 if seam_edge.start_vertex_id == old_vertex {
2336 seam_edge.t0 = split_parameter;
2337 seam_edge.start_vertex_id = target_vertex;
2338 }
2339 if seam_edge.end_vertex_id == old_vertex {
2340 seam_edge.t1 = split_parameter;
2341 seam_edge.end_vertex_id = target_vertex;
2342 }
2343 }
2344 Ok(repaired)
2345}