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 marched = match march_stations_with_rail_bar(edge, &first_mate, &second_mate, radius_at, anchor_u, FoldPolicy::Refuse, depth_budget, rail_bar, wall_check) {
391 Ok(stations) => {
392 std::mem::forget(attempt_timing);
393 Ok(stations)
394 }
395 Err(failure) => {
396 let anchored_error = failure.into_initial()?;
399 if anchor_u.is_some() {
400 attempt_timing.stop();
401 crate::blend::record_count("count.closed.march.anchor_fallback", 1);
402 if std::env::var("BREP_DEBUG_CLOSED_RAILS").is_ok() {
403 eprintln!("closed initial anchor fallback: edge {}: {anchored_error}", edge.id);
404 }
405 match march_stations_with_rail_bar(edge, &first_mate, &second_mate, radius_at, None, FoldPolicy::Refuse, depth_budget, rail_bar, wall_check) {
406 Ok(stations) => Ok(stations),
407 Err(failure) => {
408 let retry_error = failure.into_initial()?;
409 if crate::blend::is_wall_fold(&retry_error) { Err(retry_error) }
410 else { Err(anchored_error) }
411 }
412 }
413 } else { Err(anchored_error) }
414 }
415 };
416 let model_unmet = crate::blend::stations::CLOSED_MODEL_UNMET.with(|slot| slot.take());
429 let request_rails = crate::blend::stations::CLOSED_REQUEST_RAILS.with(|slot| slot.take());
430 let mut carve_fold = false;
431 let climb_carve_ladder = |error: &KernelRefusal| -> Result<Vec<Station>, KernelRefusal> {
438 let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
439 let mut climbed: Option<Vec<Station>> = None;
440 let mut last_off = f64::NAN;
441 for depth_budget in MAX_REFINEMENT_DEPTH..=CARVE_MAX_REFINEMENT_DEPTH {
442 let stations = match march_with(FoldPolicy::Carve, depth_budget) {
443 Ok(stations) => stations,
444 Err(_) => break,
446 };
447 let parameters = station_parameters(&stations);
448 let off = match fit_closed_rows(&stations, ¶meters, chamfer) {
449 Ok(rows) => rails_off_closed_carriers(&rows, &stations, ¶meters, &first_mate, &second_mate)?,
450 Err(_) => break,
451 };
452 last_off = off;
453 crate::blend::carve::carve_trace(format_args!(
454 "blend closed-edge carve ladder: depth budget {depth_budget} ({} stations) leaves the rails {off:.3e} off their carriers (bar {rail_bar:.3e})",
455 stations.len()
456 ));
457 if off <= rail_bar {
458 climbed = Some(stations);
459 break;
460 }
461 }
462 climbed.ok_or_else(|| {
463 crate::blend::fold::wall_fold_refusal(
464 Vec::new(),
465 format!(
466 "{} {} 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})",
467 crate::blend::WALL_FOLDS,
468 radius_at(mid_t).abs()
469 ),
470 )
471 })
472 };
473 let marched = match marched {
474 Err(error)
475 if crate::blend::is_wall_fold(&error)
476 && !crate::blend::fold::is_rail_fold(&error)
477 && !chamfer =>
478 {
479 crate::blend::carve::carve_trace(format_args!(
480 "blend closed-edge: the wall folds ({error}); re-marching under Carve"
481 ));
482 match climb_carve_ladder(&error) {
483 Ok(stations) => {
484 carve_fold = true;
485 Ok(stations)
486 }
487 Err(refusal) => Err(refusal),
488 }
489 }
490 other => other,
491 };
492 let mut stations = match marched {
493 Ok(stations) => stations,
494 Err(march_error) if crate::blend::is_wall_fold(&march_error)
500 || crate::blend::is_marched_fit_off_carriers(&march_error) => return Err(march_error),
501 Err(march_error) if crate::blend::is_ball_off_carrier(&march_error) => {
502 return Err(march_error);
509 }
510 Err(march_error) => {
511 if let Some(refusal) = concave_rim_overrun(solid, edge, [(first_face, first_loop), (second_face, second_loop)], radius_at(mid_t))? {
514 return Err(refusal);
515 }
516 return blend_closed_edge_keep(
519 solid,
520 edge,
521 &first_mate,
522 &second_mate,
523 radius_at(mid_t),
524 rho1,
525 chamfer,
526 name,
527 )
528 .or_else(|_| {
529 blend_closed_edge_keep(
530 solid,
531 edge,
532 &second_mate,
533 &first_mate,
534 radius_at(mid_t),
535 rho2,
536 chamfer,
537 name,
538 )
539 })
540 .map_err(|keep_error| {
541 format!("{march_error}; edge-preserving blend also failed: {keep_error}")
542 }).or_refuse(KernelStage::Refine, "map_err");
543 }
544 };
545 let support_exits = |face: &FaceRecord, second_side: bool| -> bool {
549 let stride = (stations.len() / 16).max(1);
553 stations.iter().step_by(stride).any(|station| {
554 let mut uv = if second_side {
555 station.uv2
556 } else {
557 station.uv1
558 };
559 if let Ok((closed_u, closed_v)) = face.surface.closed_directions() {
560 if closed_u {
561 if let Ok([low, high]) = face.surface.domain_u() {
562 uv[0] = low + (uv[0] - low).rem_euclid(high - low);
563 }
564 }
565 if closed_v {
566 if let Ok([low, high]) = face.surface.domain_v() {
567 uv[1] = low + (uv[1] - low).rem_euclid(high - low);
568 }
569 }
570 }
571 crate::parameter_point_in_face(face, crate::Vec2 { x: uv[0], y: uv[1] }, 1e-6)
572 .map(|class| class == crate::PolygonClass::Outside)
573 .unwrap_or(true)
574 })
575 };
576 let exits_timing = crate::blend::BlendTimer::start("timing.closed.march.exits_ms");
584 let exits = [support_exits(first_face, false), support_exits(second_face, true)];
585 exits_timing.stop();
586 if exits[0] || exits[1] {
587 let scale = crate::blend::stations::march_model_scale(&edge.curve, edge.t0, edge.t1, radius_at(mid_t))?;
588 match crate::blend::rollover::plan_closed(solid, edge, &stations, [&first_mate, &second_mate], radius_at, scale) {
589 Ok(Some(plan)) => {
590 if crate::blend::rollover_on() {
593 let pieces = crate::blend::rollover::march_pieces(
594 solid, edge, &plan, &stations, [&first_mate, &second_mate], radius_at, scale, rail_bar, chamfer, anchor_u,
595 )?;
596 let built = crate::blend::rollover::build(solid, edge, [&first_mate, &second_mate], &plan, pieces, anchor_u, name)?;
597 march_timing.stop();
598 return Ok(built);
599 }
600 return Err(crate::blend::rollover::refusal(&plan, radius_at(mid_t).abs()));
601 }
602 Ok(None) => {}
603 Err(error) => {
604 if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
605 eprintln!("closed-rim rollover scan did not read the exit: {error}");
606 }
607 }
608 }
609 }
610 if let Some(refusal) = concave_rim_overrun(solid, edge, [(first_face, first_loop), (second_face, second_loop)], radius_at(mid_t))? {
611 return Err(refusal);
612 }
613 if exits[1] {
614 return blend_closed_edge_keep(
615 solid,
616 edge,
617 &first_mate,
618 &second_mate,
619 radius_at(mid_t),
620 rho1,
621 chamfer,
622 name,
623 );
624 }
625 if exits[0] {
626 return blend_closed_edge_keep(
627 solid,
628 edge,
629 &second_mate,
630 &first_mate,
631 radius_at(mid_t),
632 rho2,
633 chamfer,
634 name,
635 );
636 }
637 if std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok() {
638 let last = stations.len() - 1;
639 for index in [
640 0usize,
641 1.min(last),
642 2.min(last),
643 last.saturating_sub(1),
644 last,
645 ] {
646 let station = &stations[index];
647 eprintln!(
648 "station {index}: uv1=({:.6},{:.6}) uv2=({:.6},{:.6}) p1=({:.6},{:.6},{:.6}) w={:.6}",
649 station.uv1[0], station.uv1[1], station.uv2[0], station.uv2[1],
650 station.p1.x, station.p1.y, station.p1.z, station.weight,
651 );
652 }
653 }
654 march_timing.stop();
655 crate::blend::record_count("count.closed.stations", stations.len() as u64);
656 let rows_timing = crate::blend::BlendTimer::start("timing.closed.rows_ms");
657 let mut parameters = station_parameters(&stations);
658 let exact_rows = exact_closed_revolution_rows(&stations, &first_mate, &second_mate, chamfer);
659 crate::blend::record_count(
660 if exact_rows.is_some() { "count.rows.exact_revolution" } else { "count.rows.fitted_closed" },
661 1,
662 );
663 let rows = match exact_rows {
664 Some(rows) => rows?,
665 None => {
666 let fit_timing = crate::blend::BlendTimer::start("timing.closed.rows.fit_ms");
676 let mut rows = fit_closed_rows(&stations, ¶meters, chamfer)?;
677 fit_timing.stop();
678 if !carve_fold && !chamfer {
686 if let Err(error) = crate::blend::fold::check_rows_loop(
687 &rows, &stations, ¶meters, None, radius_at(mid_t).abs(), vec![first_face.id, second_face.id], crate::solid_scale(solid),
688 ) {
689 crate::blend::carve::carve_trace(format_args!(
690 "blend closed-edge: the fitted rows loop ({error}); re-marching under Carve"
691 ));
692 stations = climb_carve_ladder(&error)?;
693 parameters = station_parameters(&stations);
694 rows = fit_closed_rows(&stations, ¶meters, chamfer)?;
695 carve_fold = true;
696 }
697 }
698 let read_first = crate::blend::closed_trim_read_first_on();
702 let refit_timing = crate::blend::BlendTimer::start("timing.closed.rows.refit_first_ms");
703 rows.cr_pcurve = if read_first {
704 closed_support_trim(&rows.cr, &rows.cr_pcurve, &first_mate.face.surface)?
705 } else {
706 refit_closed_support_pcurve(&rows.cr, &rows.cr_pcurve, &first_mate.face.surface)?
707 };
708 refit_timing.stop();
709 let refit_timing = crate::blend::BlendTimer::start("timing.closed.rows.refit_second_ms");
710 rows.cs_pcurve = if read_first {
711 closed_support_trim(&rows.cs, &rows.cs_pcurve, &second_mate.face.surface)?
712 } else {
713 refit_closed_support_pcurve(&rows.cs, &rows.cs_pcurve, &second_mate.face.surface)?
714 };
715 refit_timing.stop();
716 rows
717 }
718 };
719 rows_timing.stop();
720 let carve_timing = crate::blend::BlendTimer::start("timing.closed.carve_ms");
721 let crease = if carve_fold {
722 Some(carve_closed_wall(solid, &rows, &stations, ¶meters, &first_mate, &second_mate, radius_at(mid_t).abs())?)
723 } else {
724 None
725 };
726 carve_timing.stop();
727 let surgery_timing = crate::blend::BlendTimer::start("timing.closed.surgery_ms");
728
729 let second_split = if second_seam {
733 let seam2 = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?[0];
734 let period2 = {
735 let [d0, d1] = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
736 d1 - d0
737 };
738 let u2_first = stations[0].uv2[0];
740 let u2_last = stations[stations.len() - 1].uv2[0];
741 let direction = (u2_last - u2_first).signum();
742 let mut k = ((u2_first - seam2) / period2).ceil();
743 if direction < 0.0 {
744 k = ((u2_first - seam2) / period2).floor();
745 }
746 let target = seam2 + k * period2;
747 let inside =
748 (target - u2_first) * direction > 1e-6 && (u2_last - target) * direction > 1e-6;
749 let aligned = (target - u2_first).abs() <= 1e-6
754 || (target - u2_last).abs() <= 1e-6
755 || ((u2_first - seam2) / period2).fract().abs() <= 1e-9;
756 if !inside && aligned {
757 None
758 } else if !inside {
759 return Err(KernelRefusal::internal(KernelStage::Refine, "seam_crossing_bracket", "blend: second seam crossing not bracketed by the march"));
760 } else {
761 let [fit_low, fit_high] = rows.u_domain;
765 let mut p = fit_low
766 + (fit_high - fit_low) * {
767 let mut seed = 0.5;
769 for pair in 0..stations.len() - 1 {
770 let a = stations[pair].uv2[0];
771 let b = stations[pair + 1].uv2[0];
772 if (target - a) * (target - b) <= 0.0 {
773 let local = (target - a) / (b - a);
774 seed = (parameters[pair]
775 + (parameters[pair + 1] - parameters[pair]) * local)
776 .clamp(0.0, 1.0);
777 break;
778 }
779 }
780 seed
781 };
782 for _ in 0..NEWTON_ITERATIONS {
783 let value = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
784 if value.abs() <= 1e-12 {
785 break;
786 }
787 let step = 1e-8;
788 let probed = rows.cs_pcurve.evaluate(p + step).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
789 let derivative = (probed - value) / step;
790 if derivative.abs() <= 1e-14 {
791 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "seam_crossing_newton", "blend: second seam crossing Newton stalled"));
792 }
793 p -= value / derivative;
794 }
795 let crossing_v = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.y;
796 Some(SecondSeamSplit {
797 fit_parameter: p,
798 crossing_v,
799 period: period2 * direction,
800 })
801 }
802 } else {
803 None
804 };
805
806 let model_unmet = if carve_fold { None } else { model_unmet };
810 let wall_surface = model_unmet.map(|_| rows.surface.clone());
811 let built = build_surgery(
812 solid,
813 edge,
814 &first_mate,
815 &second_mate,
816 rows,
817 second_split,
818 name,
819 crease,
820 )?;
821 surgery_timing.stop();
822 if let (Some((declared, reason, round, stations, ceiling)), Some(surface)) = (model_unmet, wall_surface) {
823 crate::blend::record_wall_model_report(crate::blend::WallModelReport {
824 name: name.map(str::to_string),
825 surface,
826 request: crate::KernelTolerances::for_solid(solid, 1e-7).model,
827 residual: declared,
828 detail: request_rails.map(|(rails, unread, wall)| {
831 format!("last request read: rails {rails:.3e} off their carriers at j/16 ({unread} interval(s) unread), wall {wall:.3e}")
832 }),
833 budget: crate::ApproximationBudget {
834 reason,
835 rounds_used: round,
836 rounds_limit: crate::blend::stations::REFINE_ROUNDS,
837 stations,
838 station_limit: ceiling,
839 mechanism: crate::BudgetMechanism::LocalRounds,
840 measured_component: match request_rails {
843 Some((rails, _, wall)) if rails > wall => crate::MeasuredComponent::Rails,
844 _ => crate::MeasuredComponent::Wall,
845 },
846 unread: request_rails.map_or(0, |(_, unread, _)| unread),
847 },
848 });
849 }
850 Ok(built)
851}
852
853fn separate_spherical_domain_cut(
859 solid: &BrepSolid,
860 edge_id: u64,
861) -> Result<Option<BrepSolid>, KernelRefusal> {
862 let Some(edge) = solid.edges.iter().find(|edge| edge.id == edge_id) else {
863 return Ok(None);
864 };
865 if edge.start_vertex_id != edge.end_vertex_id {
866 return Ok(None);
867 }
868 let mut result = solid.clone();
869 let mut take_id = crate::blend::edge::fresh_id_source(solid);
870 let mut separated = false;
871 for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
872 let Some(atlas) = crate::sphere_chart::SphereAtlas::of_surface(&face.surface) else {
873 continue;
874 };
875 let [u0, u1] = face
876 .surface
877 .domain_u()
878 .or_refuse(KernelStage::Collect, "domain_cut_u")?;
879 let [v0, v1] = face
880 .surface
881 .domain_v()
882 .or_refuse(KernelStage::Collect, "domain_cut_v")?;
883 if [u0, u1, v0, v1] != [0.0, 1.0, 0.0, 1.0] {
884 continue;
885 }
886 if face
887 .surface
888 .closed_directions()
889 .or_refuse(KernelStage::Collect, "domain_cut_closed")?
890 != (true, false)
891 {
892 continue;
893 }
894 let band = 1e-8 * atlas.radius;
895 let bottom = face
896 .surface
897 .evaluate(u0, v0)
898 .or_refuse(KernelStage::Collect, "domain_cut_bottom")?;
899 let top = face
900 .surface
901 .evaluate(u0, v1)
902 .or_refuse(KernelStage::Collect, "domain_cut_top")?;
903 if bottom.add(top).scale(0.5).sub(atlas.centre).length() > band
904 || (bottom.sub(top).length() - 2.0 * atlas.radius).abs() > band
905 {
906 continue;
907 }
908 let mut additions = Vec::new();
909 let mut cut_ids = std::collections::HashSet::new();
910 for rim in &mut face.loops {
911 if rim.coedges.len() < 5 {
912 continue;
913 }
914 let positions: Vec<_> = rim
915 .coedges
916 .iter()
917 .enumerate()
918 .filter_map(|(i, c)| (c.edge_id == edge_id).then_some(i))
919 .collect();
920 if positions.len() != 1 {
921 continue;
922 }
923 let position = positions[0];
924 let ring = &rim.coedges[position];
925 let [a, b] = ring
926 .pcurve
927 .domain()
928 .or_refuse(KernelStage::Collect, "domain_cut_ring")?;
929 if ring
930 .pcurve
931 .evaluate(a)
932 .or_refuse(KernelStage::Collect, "domain_cut_ring_start")?
933 .sub(
934 ring.pcurve
935 .evaluate(b)
936 .or_refuse(KernelStage::Collect, "domain_cut_ring_end")?,
937 )
938 .length()
939 > 1e-7
940 {
941 continue;
942 }
943 let rest: Vec<_> = rim
944 .coedges
945 .iter()
946 .filter(|c| c.edge_id != edge_id)
947 .collect();
948 let mut points = Vec::new();
949 let mut domain_cut = true;
950 for coedge in &rest {
951 let [a, b] = coedge
952 .pcurve
953 .domain()
954 .or_refuse(KernelStage::Collect, "domain_cut_trim")?;
955 let start = coedge
956 .pcurve
957 .evaluate(a)
958 .or_refuse(KernelStage::Collect, "domain_cut_start")?;
959 let end = coedge
960 .pcurve
961 .evaluate(b)
962 .or_refuse(KernelStage::Collect, "domain_cut_end")?;
963 points.push(start);
964 let degenerate = solid
965 .edges
966 .iter()
967 .any(|e| e.id == coedge.edge_id && e.degenerate);
968 let at = |x: f64, low: f64, high: f64| {
969 (x - low).abs() <= 1e-8 || (x - high).abs() <= 1e-8
970 };
971 let on_boundary = if degenerate {
972 (start.y - end.y).abs() <= 1e-8
973 && at(start.y, v0, v1)
974 && coedge
975 .pcurve
976 .control_points
977 .iter()
978 .all(|p| (p.y / p.w - start.y).abs() <= 1e-8)
979 } else {
980 at(start.x, u0, u1)
981 && (start.x - end.x).abs() <= 1e-8
982 && coedge
983 .pcurve
984 .control_points
985 .iter()
986 .all(|p| (p.x / p.w - start.x).abs() <= 1e-8)
987 && rest.iter().filter(|c| c.edge_id == coedge.edge_id).count() == 2
988 && rest
989 .iter()
990 .any(|c| c.edge_id == coedge.edge_id && c.forward != coedge.forward)
991 };
992 domain_cut &= on_boundary;
993 }
994 let twice_area: f64 = (0..points.len())
995 .map(|i| {
996 let a = points[i];
997 let b = points[(i + 1) % points.len()];
998 a.x * b.y - a.y * b.x
999 })
1000 .sum();
1001 let expected = (u1 - u0) * (v1 - v0) * if face.same_sense { 1.0 } else { -1.0 };
1002 if !domain_cut || (0.5 * twice_area - expected).abs() > 1e-7 * expected.abs() {
1003 continue;
1004 }
1005 let cuts: std::collections::HashSet<_> = rest
1009 .iter()
1010 .filter_map(|c| {
1011 solid
1012 .edges
1013 .iter()
1014 .find(|e| e.id == c.edge_id && !e.degenerate)
1015 .map(|e| e.id)
1016 })
1017 .collect();
1018 for cut in solid.edges.iter().filter(|e| cuts.contains(&e.id)) {
1019 let users = solid
1020 .shells
1021 .iter()
1022 .flat_map(|s| &s.faces)
1023 .flat_map(|f| &f.loops)
1024 .flat_map(|l| &l.coedges)
1025 .filter(|c| c.edge_id == cut.id)
1026 .count();
1027 let exclusive_vertices = solid
1028 .edges
1029 .iter()
1030 .filter(|e| {
1031 e.start_vertex_id == cut.start_vertex_id
1032 || e.end_vertex_id == cut.start_vertex_id
1033 || e.start_vertex_id == cut.end_vertex_id
1034 || e.end_vertex_id == cut.end_vertex_id
1035 })
1036 .all(|e| e.degenerate || cuts.contains(&e.id) || e.id == edge_id);
1037 let mut same_parameter = cut.t0 >= v0 && cut.t1 <= v1 && cut.t0 < cut.t1;
1038 for i in 0..=16 {
1039 let t = cut.t0 + (cut.t1 - cut.t0) * i as f64 / 16.0;
1040 let point = cut
1041 .curve
1042 .evaluate(t)
1043 .or_refuse(KernelStage::Collect, "domain_cut_curve")?;
1044 let carrier = face
1045 .surface
1046 .evaluate(u0, t)
1047 .or_refuse(KernelStage::Collect, "domain_cut_carrier")?;
1048 same_parameter &= point.sub(carrier).length() <= band;
1049 }
1050 domain_cut &= users == 2 && exclusive_vertices && same_parameter;
1051 }
1052 if !domain_cut {
1053 continue;
1054 }
1055 cut_ids.extend(cuts);
1056 additions.push(LoopRecord {
1057 id: take_id(),
1058 coedges: vec![rim.coedges.remove(position)],
1059 });
1060 separated = true;
1061 }
1062 if !additions.is_empty() {
1063 let real = |id: u64| {
1067 !cut_ids.contains(&id) && !solid.edges.iter().any(|e| e.id == id && e.degenerate)
1068 };
1069 let curves: Vec<_> = face
1070 .loops
1071 .iter()
1072 .flat_map(|l| &l.coedges)
1073 .chain(additions.iter().flat_map(|l| &l.coedges))
1074 .filter(|c| real(c.edge_id) || c.edge_id == edge_id)
1075 .map(|c| &c.pcurve)
1076 .collect();
1077 let mut candidates = Vec::new();
1078 for shift in [0.25, 0.5, 0.75] {
1079 let mut clearance = f64::INFINITY;
1080 let mut fits = true;
1081 for curve in &curves {
1082 let first = curve.control_points[0].x / curve.control_points[0].w;
1083 let offset = -(first - shift).floor();
1084 for p in &curve.control_points {
1085 let u = p.x / p.w - shift + offset;
1086 fits &= u >= 0.0 && u <= 1.0;
1087 clearance = clearance.min(u.min(1.0 - u));
1088 }
1089 }
1090 if fits {
1091 candidates.push((clearance, shift));
1092 }
1093 }
1094 let Some((_, shift)) = candidates.into_iter().max_by(|a, b| a.0.total_cmp(&b.0)) else {
1095 return Err(KernelRefusal::unsupported(
1096 KernelStage::Collect,
1097 "sphere_domain_cut",
1098 "blend: no quarter-turn sphere chart keeps the real trims in-domain",
1099 ));
1100 };
1101 let seam_direction = face
1102 .surface
1103 .evaluate(shift, 0.5)
1104 .or_refuse(KernelStage::Collect, "sphere_cut_direction")?
1105 .sub(atlas.centre);
1106 let surface = crate::make_sphere_surface_framed(
1107 atlas.centre,
1108 atlas.radius,
1109 atlas.basis[2],
1110 Some(seam_direction),
1111 )
1112 .or_refuse(KernelStage::Collect, "sphere_cut_surface")?;
1113 for i in 0..=16 {
1115 for v in [0.0, 0.3, 0.5, 0.7, 1.0] {
1116 let u = i as f64 / 16.0;
1117 let old = face
1118 .surface
1119 .evaluate(u, v)
1120 .or_refuse(KernelStage::Collect, "sphere_cut_old")?;
1121 let new = surface
1122 .evaluate((u - shift).rem_euclid(1.0), v)
1123 .or_refuse(KernelStage::Collect, "sphere_cut_new")?;
1124 if old.sub(new).length() > band {
1125 return Err(KernelRefusal::unsupported(
1126 KernelStage::Collect,
1127 "sphere_cut_chart",
1128 "blend: the sphere quarter turn is not the same carrier",
1129 ));
1130 }
1131 }
1132 }
1133 let seam = surface
1134 .iso_curve_u(0.0)
1135 .or_refuse(KernelStage::Collect, "sphere_cut_seam")?;
1136 for coedge in face
1137 .loops
1138 .iter_mut()
1139 .flat_map(|l| &mut l.coedges)
1140 .chain(additions.iter_mut().flat_map(|l| &mut l.coedges))
1141 {
1142 if solid
1143 .edges
1144 .iter()
1145 .any(|e| e.id == coedge.edge_id && e.degenerate)
1146 {
1147 continue;
1148 }
1149 if !cut_ids.contains(&coedge.edge_id) {
1150 let first =
1151 coedge.pcurve.control_points[0].x / coedge.pcurve.control_points[0].w;
1152 let offset = -(first - shift).floor() - shift;
1153 for p in &mut coedge.pcurve.control_points {
1154 p.x += offset * p.w;
1155 }
1156 }
1157 }
1158 for cut in result.edges.iter_mut().filter(|e| cut_ids.contains(&e.id)) {
1159 for (vertex_id, t) in [(cut.start_vertex_id, cut.t0), (cut.end_vertex_id, cut.t1)] {
1160 let point = seam
1161 .evaluate(t)
1162 .or_refuse(KernelStage::Collect, "sphere_cut_vertex")?;
1163 let vertex = result
1164 .vertices
1165 .iter_mut()
1166 .find(|v| v.id == vertex_id)
1167 .ok_or(KernelRefusal::internal(
1168 KernelStage::Collect,
1169 "sphere_cut_vertex",
1170 "blend: sphere seam vertex is missing",
1171 ))?;
1172 vertex.point = point;
1173 }
1174 cut.curve = seam.clone();
1175 }
1176 face.surface = surface;
1177 }
1178 face.loops.extend(additions);
1179 }
1180 if !separated {
1181 return Ok(None);
1182 }
1183 let old_vertex = solid
1184 .vertices
1185 .iter()
1186 .find(|v| v.id == edge.start_vertex_id)
1187 .ok_or(KernelRefusal::internal(
1188 KernelStage::Collect,
1189 "domain_cut_vertex",
1190 "blend: closed ring vertex is missing",
1191 ))?;
1192 let vertex_id = take_id();
1193 result.vertices.push(VertexRecord {
1194 id: vertex_id,
1195 point: old_vertex.point,
1196 });
1197 let ring = result
1198 .edges
1199 .iter_mut()
1200 .find(|e| e.id == edge_id)
1201 .ok_or(KernelRefusal::internal(
1202 KernelStage::Collect,
1203 "domain_cut_ring",
1204 "blend: closed ring edge is missing",
1205 ))?;
1206 ring.start_vertex_id = vertex_id;
1207 ring.end_vertex_id = vertex_id;
1208 Ok(Some(result))
1209}
1210
1211fn remove_retraced_cuts(
1217 solid: &BrepSolid,
1218 edge_id: u64,
1219) -> Result<Option<BrepSolid>, KernelRefusal> {
1220 let mut result = solid.clone();
1221 let mut removed = Vec::new();
1222 for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
1223 for rim in &mut face.loops {
1224 if !rim.coedges.iter().any(|coedge| coedge.edge_id == edge_id) {
1225 continue;
1226 }
1227 loop {
1228 let n = rim.coedges.len();
1229 let mut pair = None;
1230 for i in 0..n {
1231 let j = (i + 1) % n;
1232 let (a, b) = (&rim.coedges[i], &rim.coedges[j]);
1233 if a.edge_id == edge_id || a.edge_id != b.edge_id || a.forward == b.forward {
1234 continue;
1235 }
1236 let users = solid
1237 .shells
1238 .iter()
1239 .flat_map(|s| &s.faces)
1240 .flat_map(|f| &f.loops)
1241 .flat_map(|l| &l.coedges)
1242 .filter(|c| c.edge_id == a.edge_id)
1243 .count();
1244 if users != 2 {
1245 continue;
1246 }
1247 let [a0, a1] = a
1248 .pcurve
1249 .domain()
1250 .or_refuse(KernelStage::Collect, "cut_domain")?;
1251 let [b0, b1] = b
1252 .pcurve
1253 .domain()
1254 .or_refuse(KernelStage::Collect, "cut_domain")?;
1255 let agrees =
1258 a.pcurve.degree == b.pcurve.degree
1259 && a.pcurve.knots.len() == b.pcurve.knots.len()
1260 && a.pcurve.control_points.len() == b.pcurve.control_points.len()
1261 && a.pcurve.knots.iter().zip(b.pcurve.knots.iter().rev()).all(
1262 |(x, y)| {
1263 ((x - a0) / (a1 - a0) - (b1 - y) / (b1 - b0)).abs() <= 1e-9
1264 },
1265 )
1266 && a.pcurve
1267 .control_points
1268 .iter()
1269 .zip(b.pcurve.control_points.iter().rev())
1270 .all(|(p, q)| {
1271 p.point()
1272 .ok()
1273 .zip(q.point().ok())
1274 .is_some_and(|(p, q)| p.sub(q).length() <= 1e-9)
1275 && (p.w / a.pcurve.control_points[0].w
1276 - q.w / b.pcurve.control_points.last().unwrap().w)
1277 .abs()
1278 <= 1e-9
1279 });
1280 if agrees {
1281 pair = Some((i, j, a.edge_id));
1282 break;
1283 }
1284 }
1285 let Some((i, j, id)) = pair else {
1286 break;
1287 };
1288 rim.coedges.remove(i.max(j));
1289 rim.coedges.remove(i.min(j));
1290 removed.push(id);
1291 }
1292 }
1293 }
1294 if removed.is_empty() {
1295 return Ok(None);
1296 }
1297 result.edges.retain(|edge| !removed.contains(&edge.id));
1298 super::open::prune_orphan_vertices(&mut result);
1299 Ok(Some(result))
1300}
1301
1302struct SecondSeamSplit {
1305 fit_parameter: f64,
1306 crossing_v: f64,
1307 period: f64,
1309}
1310
1311const CARVE_MAX_REFINEMENT_DEPTH: usize = MAX_REFINEMENT_DEPTH + 4;
1323
1324fn rails_off_closed_carriers(
1328 rows: &FittedRows,
1329 stations: &[Station],
1330 parameters: &[f64],
1331 first: &BlendMate,
1332 second: &BlendMate,
1333) -> Result<f64, KernelRefusal> {
1334 Ok(closed_rail_interval_misses(rows, stations, parameters,
1335 [&first.face.surface, &second.face.surface])?.into_iter().fold(0.0_f64, f64::max))
1336}
1337
1338fn carve_closed_wall(
1339 solid: &BrepSolid,
1340 rows: &FittedRows,
1341 stations: &[Station],
1342 parameters: &[f64],
1343 first: &BlendMate,
1344 second: &BlendMate,
1345 radius: f64,
1346) -> Result<crate::blend::carve::CarvedCrease, KernelRefusal> {
1347 let policy = crate::KernelTolerances::for_solid(solid, 1e-7);
1348 let rail_bar = 0.5 * policy.intersection_fit;
1349 let off = rails_off_closed_carriers(rows, stations, parameters, first, second)?;
1350 crate::blend::carve::carve_trace(format_args!(
1351 "blend closed-edge carve: {} stations, rails {off:.3e} off their carriers between stations (bar {rail_bar:.3e})",
1352 parameters.len()
1353 ));
1354 if off > rail_bar {
1355 return Err(crate::blend::fold::wall_fold_refusal(
1356 Vec::new(),
1357 format!(
1358 "{} {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",
1359 crate::blend::WALL_FOLDS
1360 ),
1361 ));
1362 }
1363 let Some(carved) = crate::blend::carve::trace_wall_crease(&rows.surface, radius)? else {
1364 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"));
1365 };
1366 let [u0, u1] = rows.u_domain;
1367 let margin = 1e-3 * (u1 - u0);
1368 let straddles = carved.ends.iter().any(|end| end[0] <= u0 + margin || end[0] >= u1 - margin);
1369 if straddles {
1370 return Err(crate::blend::fold::wall_fold_refusal(
1371 Vec::new(),
1372 format!(
1373 "{} {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",
1374 crate::blend::WALL_FOLDS, carved.ends[0][0], carved.ends[1][0]
1375 ),
1376 ));
1377 }
1378 crate::blend::carve::fit_crease(&rows.surface, &carved, policy.pcurve_consistency)
1379}
1380
1381fn closed_support_trim(rail: &NurbsCurve, old: &NurbsCurve, surface: &NurbsSurface) -> Result<NurbsCurve, KernelRefusal> {
1399 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1400 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1401 let within_floor = |candidate: &NurbsCurve, what: &str| -> bool {
1402 let _read_timing = crate::blend::BlendTimer::start("timing.closed.rows.trim_read_ms");
1403 let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), candidate, d0, d1);
1404 if std::env::var_os("BREP_DEBUG_TRACK_FIT").is_some() {
1405 match &read {
1406 Ok((miss, standoff, samples, worst)) => eprintln!(
1407 "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}",
1408 candidate.control_points.len()),
1409 Err(error) => eprintln!("TRACK_FIT closed-edge support, {what} candidate: unreadable: {error}"),
1410 }
1411 }
1412 read.is_ok_and(|(miss, _, _, _)| miss <= floor)
1413 };
1414 if let Some(affine) = plane_affine_pcurve(rail, surface) {
1415 if within_floor(&affine, "affine plane image") {
1416 crate::blend::record_count("count.closed.rows.trim.affine", 1);
1417 return Ok(affine);
1418 }
1419 }
1420 crate::blend::record_count("count.closed.rows.trim.refit", 1);
1421 refit_closed_support_pcurve(rail, old, surface)
1422}
1423
1424fn plane_affine_pcurve(rail: &NurbsCurve, surface: &NurbsSurface) -> Option<NurbsCurve> {
1430 if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) {
1431 return None;
1432 }
1433 let [u0, u1] = surface.domain_u().ok()?;
1434 let [v0, v1] = surface.domain_v().ok()?;
1435 let origin = surface.evaluate(u0, v0).ok()?;
1436 let du = surface.evaluate(u1, v0).ok()?.sub(origin).scale(1.0 / (u1 - u0));
1437 let dv = surface.evaluate(u0, v1).ok()?.sub(origin).scale(1.0 / (v1 - v0));
1438 let (a, b, c) = (du.dot(du), du.dot(dv), dv.dot(dv));
1439 let det = a * c - b * b;
1440 let chart_scale = origin.length().max(du.length()).max(dv.length()).max(1.0);
1441 if !(det > 1e-24 * chart_scale.powi(4)) {
1442 return None;
1443 }
1444 let mut pcurve = rail.clone();
1445 for control in &mut pcurve.control_points {
1446 let weight = control.w;
1447 let offset = Vec3::new(control.x / weight, control.y / weight, control.z / weight).sub(origin);
1448 let (r, s) = (offset.dot(du), offset.dot(dv));
1449 control.x = (u0 + (c * r - b * s) / det) * weight;
1450 control.y = (v0 + (a * s - b * r) / det) * weight;
1451 control.z = 0.0;
1452 }
1453 Some(pcurve)
1454}
1455
1456pub(in crate::blend) fn refit_closed_support_pcurve(rail: &NurbsCurve, old: &NurbsCurve, surface: &NurbsSurface) -> Result<NurbsCurve, KernelRefusal> {
1481 let knot_retry = KNOT_RETRY.with(|flag| flag.get());
1482 refit_closed_support_pcurve_read_with(rail, old, surface, &mut crate::blend::track_fit::FootMemo::default(), knot_retry).map(|(fitted, _)| fitted)
1483}
1484
1485thread_local! {
1486 static KNOT_RETRY: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1490}
1491
1492pub(in crate::blend) fn refit_closed_support_pcurve_read(
1500 rail: &NurbsCurve,
1501 old: &NurbsCurve,
1502 surface: &NurbsSurface,
1503 memo: &mut crate::blend::track_fit::FootMemo,
1504) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1505 let knot_retry = KNOT_RETRY.with(|flag| flag.get());
1508 refit_closed_support_pcurve_read_with(rail, old, surface, memo, knot_retry)
1509}
1510
1511pub fn with_pcurve_knot_retry<T>(build: impl FnOnce() -> T) -> T {
1515 let previous = KNOT_RETRY.with(|flag| flag.replace(true));
1516 let built = build();
1517 KNOT_RETRY.with(|flag| flag.set(previous));
1518 built
1519}
1520
1521fn refit_closed_support_pcurve_read_with(
1524 rail: &NurbsCurve,
1525 old: &NurbsCurve,
1526 surface: &NurbsSurface,
1527 memo: &mut crate::blend::track_fit::FootMemo,
1528 knot_retry: bool,
1529) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1530 const SHIPPED_READ_REPAIRS: usize = 3;
1531 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1532 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1533 let mut near: Vec<f64> = Vec::new();
1534 let (mut fitted, mut read) = fit_and_read_support(rail, old, surface, &near, memo, knot_retry)?;
1538 for _ in 0..SHIPPED_READ_REPAIRS {
1539 if read.0 <= floor || !(d1 > d0) {
1540 break;
1541 }
1542 near.push((read.3 - d0) / (d1 - d0));
1543 crate::blend::record_count("count.surgery.refit_repairs", 1);
1544 match fit_and_read_support(rail, old, surface, &near, memo, false) {
1547 Ok(repaired) => (fitted, read) = repaired,
1548 Err(_) => break,
1549 }
1550 }
1551 let (miss, standoff, samples, _) = read;
1552 if !(miss <= floor) {
1553 return Err(KernelRefusal::non_convergence(KernelStage::Refine, CLOSED_SUPPORT_PCURVE_FLOOR_WHAT, format!(
1554 "{} a blend's support pcurve, as shipped, misses its rail's track by {miss:.3e} at {samples} samples, \
1555 against a floor of {floor:.1e}",
1556 crate::blend::PCURVE_OFF_FLOOR,
1557 )));
1558 }
1559 Ok((fitted, read))
1560}
1561
1562fn fit_and_read_support(
1566 rail: &NurbsCurve,
1567 old: &NurbsCurve,
1568 surface: &NurbsSurface,
1569 near: &[f64],
1570 memo: &mut crate::blend::track_fit::FootMemo,
1571 knot_retry: bool,
1572) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1573 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1574 let guide = crate::blend::chain::pcurve_guide(old, surface)?;
1577 let track_timing = crate::blend::BlendTimer::start("timing.support_refit.track_ms");
1578 let mut fitted = if knot_retry {
1579 crate::blend::chain::project_piece_pcurve_within_knot_retry(rail, surface, false, false, Some(&guide), near)?
1580 } else {
1581 crate::blend::chain::project_piece_pcurve_within(rail, surface, false, false, Some(&guide), near)?
1582 };
1583 track_timing.stop();
1584 let [f0, f1] = fitted.domain().or_refuse(KernelStage::Refine, "domain")?;
1587 for knot in fitted.knots.iter_mut() {
1588 *knot = d0 + (*knot - f0) / (f1 - f0) * (d1 - d0);
1589 }
1590 let debug = std::env::var_os("BREP_DEBUG_TRACK_FIT").is_some();
1591 if debug {
1592 match crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &fitted, d0, d1) {
1595 Ok((miss, standoff, samples, worst)) => eprintln!(
1596 "TRACK_FIT closed-edge support: before the seam ends, miss {miss:.3e} at t {worst:.9} ({samples} samples), standoff {standoff:.3e}"),
1597 Err(error) => eprintln!("TRACK_FIT closed-edge support: before the seam ends, unreadable: {error}"),
1598 }
1599 }
1600 let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
1601 let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1602 let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1603 let old_start = old.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?;
1604 let old_end = old.evaluate(d1).or_refuse(KernelStage::Refine, "evaluate")?;
1605 let new_start = fitted.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?;
1606 let shift = |closed: bool, from: f64, to: f64, period: f64| if closed { ((to - from) / period).round() * period } else { 0.0 };
1608 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));
1609 for point in fitted.control_points.iter_mut() {
1610 point.x += shift_u * point.w;
1611 point.y += shift_v * point.w;
1612 }
1613 let on_seam = |closed: bool, value: f64, low: f64, high: f64| {
1614 let period = high - low;
1615 closed && ((value - low) / period - ((value - low) / period).round()).abs() * period <= 1e-12 * period.max(1.0)
1616 };
1617 let last = fitted.control_points.len() - 1;
1618 for (index, end) in [(0, old_start), (last, old_end)] {
1619 let point = &mut fitted.control_points[index];
1620 if on_seam(closed_u, end.x, u0, u1) {
1621 point.x = end.x * point.w;
1622 }
1623 if on_seam(closed_v, end.y, v0, v1) {
1624 point.y = end.y * point.w;
1625 }
1626 }
1627 let mut own = crate::blend::track_fit::FootMemo::default();
1637 let (miss, standoff, samples, worst) =
1638 crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut own)?;
1639 *memo = own;
1640 if debug {
1641 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}");
1642 }
1643 Ok((fitted, (miss, standoff, samples, worst)))
1644}
1645
1646
1647fn build_surgery(
1648 solid: &BrepSolid,
1649 edge: &EdgeRecord,
1650 first: &BlendMate,
1651 second: &BlendMate,
1652 rows: FittedRows,
1653 second_split: Option<SecondSeamSplit>,
1654 name: Option<&str>,
1655 crease: Option<crate::blend::carve::CarvedCrease>,
1656) -> Result<BrepSolid, KernelRefusal> {
1657 let mut result = solid.clone();
1658 let mut take_id = crate::blend::edge::fresh_id_source(solid);
1659
1660 let [u_start, u_end] = rows.u_domain;
1661 let cr_start = rows.cr.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1662 let cs_start = rows.cs.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1663 let vertex1_id = take_id();
1664 let vertex2_id = take_id();
1665 result.vertices.push(VertexRecord {
1666 id: vertex1_id,
1667 point: cr_start,
1668 });
1669 result.vertices.push(VertexRecord {
1670 id: vertex2_id,
1671 point: cs_start,
1672 });
1673
1674 let cr_edge_id = take_id();
1675 result.edges.push(EdgeRecord {
1676 id: cr_edge_id,
1677 curve: rows.cr.clone(),
1678 t0: u_start,
1679 t1: u_end,
1680 start_vertex_id: vertex1_id,
1681 end_vertex_id: vertex1_id,
1682 degenerate: false,
1683 name: None,
1684 });
1685 let mut cs_pieces: Vec<(u64, [f64; 2], NurbsCurve)> = Vec::new();
1689 let mut second_seam_vertex = vertex2_id;
1690 let mut second_seam_point = cs_start;
1691 let mut second_crossing_v = 0.0;
1692 if let Some(split) = &second_split {
1693 let p = split.fit_parameter;
1694 second_crossing_v = split.crossing_v;
1695 let (cs_a, cs_b) = rows.cs.split(p).or_refuse(KernelStage::Refine, "split")?;
1696 let (pc_a, pc_b) = rows.cs_pcurve.split(p).or_refuse(KernelStage::Refine, "split")?;
1697 let mut pc_b = pc_b;
1699 for point in pc_b.control_points.iter_mut() {
1700 point.x -= split.period * point.w;
1701 }
1702 let w2s = take_id();
1703 second_seam_vertex = w2s;
1704 second_seam_point = rows.cs.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?;
1705 result.vertices.push(VertexRecord {
1706 id: w2s,
1707 point: second_seam_point,
1708 });
1709 let edge_a = take_id();
1710 result.edges.push(EdgeRecord {
1711 id: edge_a,
1712 curve: cs_a,
1713 t0: u_start,
1714 t1: p,
1715 start_vertex_id: vertex2_id,
1716 end_vertex_id: w2s,
1717 degenerate: false,
1718 name: None,
1719 });
1720 cs_pieces.push((edge_a, [u_start, p], pc_a));
1721 let edge_b = take_id();
1722 result.edges.push(EdgeRecord {
1723 id: edge_b,
1724 curve: cs_b,
1725 t0: p,
1726 t1: u_end,
1727 start_vertex_id: w2s,
1728 end_vertex_id: vertex2_id,
1729 degenerate: false,
1730 name: None,
1731 });
1732 cs_pieces.push((edge_b, [p, u_end], pc_b));
1733 } else {
1734 let cs_edge_id = take_id();
1735 result.edges.push(EdgeRecord {
1736 id: cs_edge_id,
1737 curve: rows.cs.clone(),
1738 t0: u_start,
1739 t1: u_end,
1740 start_vertex_id: vertex2_id,
1741 end_vertex_id: vertex2_id,
1742 degenerate: false,
1743 name: None,
1744 });
1745 cs_pieces.push((cs_edge_id, [u_start, u_end], rows.cs_pcurve.clone()));
1746 }
1747 let seam_curve = rows.surface.iso_curve_u(u_start).or_refuse(KernelStage::Refine, "iso_curve_u")?;
1748 let [seam_t0, seam_t1] = seam_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1749 let blend_seam_id = take_id();
1750 result.edges.push(EdgeRecord {
1751 id: blend_seam_id,
1752 curve: seam_curve,
1753 t0: seam_t0,
1754 t1: seam_t1,
1755 start_vertex_id: vertex1_id,
1756 end_vertex_id: vertex2_id,
1757 degenerate: false,
1758 name: None,
1759 });
1760
1761 let replace_in_face = |result: &mut BrepSolid, mate: &BlendMate, pieces: &[(u64, [f64; 2], NurbsCurve)], uv_new: [f64; 2]| -> Result<bool, KernelRefusal> {
1762 replace_rim_in_face(result, edge, mate, pieces, uv_new)
1763 };
1764 let uv1_new = rows.cr_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1765 let v1_new = uv1_new.y;
1766 let cr_pieces = vec![(cr_edge_id, [u_start, u_end], rows.cr_pcurve.clone())];
1767 let mut repaired = replace_in_face(&mut result, first, &cr_pieces, [uv1_new.x, v1_new])?;
1768 let uv2_new = rows.cs_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1769 let v2_new = if second_split.is_some() {
1770 second_crossing_v
1771 } else {
1772 uv2_new.y
1773 };
1774 repaired |= replace_in_face(&mut result, second, &cs_pieces, [uv2_new.x, v2_new])?;
1775
1776 let collapse_winding_sphere_cap =
1782 |result: &mut BrepSolid, face_id: u64, support_ids: &[u64]| -> Result<(), KernelRefusal> {
1783 let face = result
1784 .shells
1785 .iter_mut()
1786 .flat_map(|shell| &mut shell.faces)
1787 .find(|face| face.id == face_id)
1788 .ok_or(KernelRefusal::internal(KernelStage::Sew, "sphere_carrier", "blend: sphere carrier lost during cap surgery"))?;
1789 if !matches!(
1790 face.surface.analytic(),
1791 Some(crate::AnalyticSurface::Sphere { .. })
1792 ) || face.surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")? != (true, false)
1793 {
1794 return Ok(());
1795 }
1796 let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1797 let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1798 let period = u1 - u0;
1799 let support_loop = face.loops.iter().position(|loop_record| {
1800 !loop_record.coedges.is_empty()
1801 && loop_record
1802 .coedges
1803 .iter()
1804 .all(|coedge| support_ids.contains(&coedge.edge_id))
1805 });
1806 let Some(support_loop) = support_loop else {
1807 return Ok(());
1808 };
1809 if face.loops[support_loop].coedges.len() != 1 {
1810 return Ok(());
1811 }
1812 let support = &face.loops[support_loop].coedges[0];
1813 let [s0, s1] = support.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1814 let support_start = support.pcurve.evaluate(s0).or_refuse(KernelStage::Refine, "evaluate")?;
1815 let support_end = support.pcurve.evaluate(s1).or_refuse(KernelStage::Refine, "evaluate")?;
1816 let support_winding = support_end.x - support_start.x;
1817 if (support_winding.abs() - period).abs() > 0.05 * period {
1818 return Ok(());
1819 }
1820
1821 let mut pole: Option<(usize, CoedgeRecord)> = None;
1822 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1823 if loop_index == support_loop {
1824 continue;
1825 }
1826 for coedge in &loop_record.coedges {
1827 let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1828 let start = coedge.pcurve.evaluate(p0).or_refuse(KernelStage::Refine, "evaluate")?;
1829 let end = coedge.pcurve.evaluate(p1).or_refuse(KernelStage::Refine, "evaluate")?;
1830 let winding = end.x - start.x;
1831 let at_pole = (start.y - v0).abs() <= 1e-8 || (start.y - v1).abs() <= 1e-8;
1832 if at_pole
1833 && (end.y - start.y).abs() <= 1e-8
1834 && (winding.abs() - period).abs() <= 0.05 * period
1835 && winding * support_winding < 0.0
1836 {
1837 pole = Some((loop_index, coedge.clone()));
1838 break;
1839 }
1840 }
1841 if pole.is_some() {
1842 break;
1843 }
1844 }
1845 let Some((pole_loop, pole_coedge)) = pole else {
1846 return Ok(());
1847 };
1848 let support_record = face.loops[support_loop].clone();
1849 let pole_id = face.loops[pole_loop].id;
1850 face.loops = vec![
1851 LoopRecord {
1852 id: pole_id,
1853 coedges: vec![pole_coedge],
1854 },
1855 support_record,
1856 ];
1857 Ok(())
1858 };
1859 collapse_winding_sphere_cap(&mut result, first.face.id, &[cr_edge_id])?;
1860 let cs_edge_ids: Vec<u64> = cs_pieces.iter().map(|(edge_id, ..)| *edge_id).collect();
1861 collapse_winding_sphere_cap(&mut result, second.face.id, &cs_edge_ids)?;
1862 for shell in &mut result.shells {
1864 for face in &mut shell.faces {
1865 for loop_record in &mut face.loops {
1866 for coedge in &mut loop_record.coedges {
1867 if coedge.id == 0 {
1868 coedge.id = take_id();
1869 }
1870 }
1871 }
1872 }
1873 }
1874
1875 let new_edge_ids: Vec<u64> = std::iter::once(cr_edge_id)
1880 .chain(cs_pieces.iter().map(|(edge_id, ..)| *edge_id))
1881 .chain(std::iter::once(blend_seam_id))
1882 .collect();
1883 let first_face_edges: Vec<u64> = first
1884 .face
1885 .loops
1886 .iter()
1887 .flat_map(|loop_record| loop_record.coedges.iter().map(|coedge| coedge.edge_id))
1888 .collect();
1889 repaired |= trim_seam_edges(
1890 &mut result,
1891 edge,
1892 &new_edge_ids,
1893 &first_face_edges,
1894 (vertex1_id, v1_new, cr_start),
1895 (second_seam_vertex, v2_new, second_seam_point),
1896 )?;
1897
1898 let mid_u = (u_start + u_end) * 0.5;
1900 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
1901 let station_uv = rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?;
1902 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
1903 let out1 = if first.face.same_sense {
1904 n1
1905 } else {
1906 n1.scale(-1.0)
1907 };
1908 let same_sense = blend_normal.dot(out1) >= 0.0;
1909 let loop_id = take_id();
1910 let mut coedges = vec![
1911 CoedgeRecord {
1912 id: take_id(),
1913 edge_id: cr_edge_id,
1914 forward: true,
1915 pcurve: crate::sweep_topology::parameter_line(u_start, 0.0, u_end, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1916 },
1917 CoedgeRecord {
1918 id: take_id(),
1919 edge_id: blend_seam_id,
1920 forward: true,
1921 pcurve: crate::sweep_topology::parameter_line(u_end, 0.0, u_end, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1922 },
1923 ];
1924 for (edge_id, window, _) in cs_pieces.iter().rev() {
1925 coedges.push(CoedgeRecord {
1926 id: take_id(),
1927 edge_id: *edge_id,
1928 forward: false,
1929 pcurve: crate::sweep_topology::parameter_line(window[1], 1.0, window[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1930 });
1931 }
1932 coedges.push(CoedgeRecord {
1933 id: take_id(),
1934 edge_id: blend_seam_id,
1935 forward: false,
1936 pcurve: crate::sweep_topology::parameter_line(u_start, 1.0, u_start, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
1937 });
1938 if !same_sense {
1948 coedges.reverse();
1949 for coedge in &mut coedges {
1950 coedge.forward = !coedge.forward;
1951 coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
1952 }
1953 }
1954 let mut loops = vec![LoopRecord {
1955 id: loop_id,
1956 coedges,
1957 }];
1958 if let Some(crease) = crease {
1967 let start_vertex = take_id();
1968 let end_vertex = take_id();
1969 let crease_edge = take_id();
1970 result.vertices.push(VertexRecord { id: start_vertex, point: crease.start });
1971 result.vertices.push(VertexRecord { id: end_vertex, point: crease.end });
1972 let [crease_t0, crease_t1] = crease.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1973 result.edges.push(EdgeRecord {
1974 id: crease_edge,
1975 curve: crease.curve,
1976 t0: crease_t0,
1977 t1: crease_t1,
1978 start_vertex_id: start_vertex,
1979 end_vertex_id: end_vertex,
1980 degenerate: false,
1981 name: name.map(|value| format!("{value}:CREASE")),
1982 });
1983 let outer = crate::blend::chain::signed_area(&loops[0].coedges)?;
1984 let forward_first = crate::blend::chain::signed_area(&[
1985 CoedgeRecord { id: 0, edge_id: crease_edge, forward: true, pcurve: crease.first.clone() },
1986 CoedgeRecord { id: 0, edge_id: crease_edge, forward: false, pcurve: crease.second.reversed().or_refuse(KernelStage::Refine, "reversed")? },
1987 ])?;
1988 let inner = if outer * forward_first < 0.0 {
1989 vec![
1990 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: true, pcurve: crease.first },
1991 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: false, pcurve: crease.second.reversed().or_refuse(KernelStage::Refine, "reversed")? },
1992 ]
1993 } else {
1994 vec![
1995 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: true, pcurve: crease.second },
1996 CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: false, pcurve: crease.first.reversed().or_refuse(KernelStage::Refine, "reversed")? },
1997 ]
1998 };
1999 loops.push(LoopRecord { id: take_id(), coedges: inner });
2000 }
2001 let blend_face = FaceRecord {
2002 id: take_id(),
2003 surface: rows.surface,
2004 same_sense,
2005 loops,
2006 name: name.map(|value| value.to_string()),
2007 };
2008 let shell_index = result
2009 .shells
2010 .iter()
2011 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
2012 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
2013 result.shells[shell_index].faces.push(blend_face);
2014
2015 let used_edges: std::collections::HashSet<u64> = result
2016 .shells
2017 .iter()
2018 .flat_map(|shell| &shell.faces)
2019 .flat_map(|face| &face.loops)
2020 .flat_map(|loop_record| &loop_record.coedges)
2021 .map(|coedge| coedge.edge_id)
2022 .collect();
2023 result
2024 .edges
2025 .retain(|candidate| used_edges.contains(&candidate.id));
2026 let used_vertices: std::collections::HashSet<u64> = result
2027 .edges
2028 .iter()
2029 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
2030 .collect();
2031 result
2032 .vertices
2033 .retain(|candidate| used_vertices.contains(&candidate.id));
2034 if repaired {
2041 let problems = result.validate_memoised();
2042 if !problems.is_empty() {
2043 return Err(KernelRefusal::internal(KernelStage::Validate, "neighbour_trim_closure", format!(
2044 "blend: the repaired neighbour trim did not close ({} issue(s), first: {})",
2045 problems.len(),
2046 problems
2047 .first()
2048 .map(|issue| issue.message.as_str())
2049 .unwrap_or("unknown")
2050 )));
2051 }
2052 }
2053 Ok(result)
2054}
2055
2056pub(in crate::blend) fn replace_rim_in_face(
2062 result: &mut BrepSolid,
2063 edge: &EdgeRecord,
2064 mate: &BlendMate,
2065 pieces: &[(u64, [f64; 2], NurbsCurve)],
2066 uv_new: [f64; 2],
2067) -> Result<bool, KernelRefusal> {
2068 let old_vertex = edge.start_vertex_id;
2069 let v_new = uv_new[1];
2070 let seam_edge_ends: Vec<(u64, bool, bool)> = result
2072 .edges
2073 .iter()
2074 .filter(|candidate| {
2075 candidate.id != edge.id
2076 && (candidate.start_vertex_id == old_vertex
2077 || candidate.end_vertex_id == old_vertex)
2078 })
2079 .map(|candidate| {
2080 (
2081 candidate.id,
2082 candidate.start_vertex_id == old_vertex,
2083 candidate.end_vertex_id == old_vertex,
2084 )
2085 })
2086 .collect();
2087 let seam_edge_ids: Vec<u64> = seam_edge_ends.iter().map(|(id, ..)| *id).collect();
2088 let pcurve_band = crate::KernelTolerances::for_solid(result, 1e-7).pcurve_consistency;
2089 let debug = std::env::var("BREP_DEBUG_NETWORK").is_ok();
2090 let face = result
2091 .shells
2092 .iter_mut()
2093 .flat_map(|shell| &mut shell.faces)
2094 .find(|face| face.id == mate.face.id)
2095 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
2096 let face_id = face.id;
2097 let FaceRecord { surface, loops, .. } = &mut *face;
2098 let loop_record = &mut loops[mate.loop_index];
2099 let position = loop_record
2100 .coedges
2101 .iter()
2102 .position(|coedge| coedge.edge_id == edge.id)
2103 .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
2104 let old_forward = loop_record.coedges[position].forward;
2105 let old_seam_v = {
2106 let pcurve = &loop_record.coedges[position].pcurve;
2107 let [d0, _] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2108 pcurve.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?.y
2109 };
2110 let mut replacements = Vec::new();
2113 let ordered: Vec<&(u64, [f64; 2], NurbsCurve)> = if old_forward {
2114 pieces.iter().collect()
2115 } else {
2116 pieces.iter().rev().collect()
2117 };
2118 for (index, (edge_id, _, pcurve)) in ordered.iter().enumerate() {
2119 replacements.push(CoedgeRecord {
2120 id: if index == 0 {
2121 loop_record.coedges[position].id
2122 } else {
2123 0 },
2125 edge_id: *edge_id,
2126 forward: old_forward,
2127 pcurve: if old_forward {
2128 pcurve.clone()
2129 } else {
2130 pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
2131 },
2132 });
2133 }
2134 loop_record
2135 .coedges
2136 .splice(position..=position, replacements);
2137 let mut repaired = false;
2138 if loop_record.coedges.len() > 1 {
2139 for coedge in &mut loop_record.coedges {
2145 if pieces
2146 .iter()
2147 .any(|(edge_id, ..)| *edge_id == coedge.edge_id)
2148 || !seam_edge_ids.contains(&coedge.edge_id)
2149 {
2150 continue;
2151 }
2152 let controls = &coedge.pcurve.control_points;
2153 let last_index = controls.len() - 1;
2154 let first_u = controls[0].x / controls[0].w;
2155 let last_u = controls[last_index].x / controls[last_index].w;
2156 let constant_u = (first_u - last_u).abs()
2157 <= 1e-9 * (1.0 + first_u.abs().max(last_u.abs()));
2158 let (target, target_uv, fraction) = if constant_u {
2161 let first_v = controls[0].y / controls[0].w;
2165 let last_v = controls[last_index].y / controls[last_index].w;
2166 let target = if (first_v - old_seam_v).abs() < (last_v - old_seam_v).abs() {
2167 0
2168 } else {
2169 last_index
2170 };
2171 let span = last_v - first_v;
2172 let fraction = if span.abs() > 0.0 { (v_new - first_v) / span } else { 0.0 };
2173 (target, [first_u, v_new], fraction)
2174 } else {
2175 let Some((_, start_is_old, end_is_old)) = seam_edge_ends
2185 .iter()
2186 .find(|(id, ..)| *id == coedge.edge_id)
2187 .copied()
2188 else {
2189 continue;
2190 };
2191 if start_is_old == end_is_old {
2192 return Err(KernelRefusal::ill_posed(KernelStage::Sew, "neighbour_trim_end", format!(
2193 "blend: neighbour edge {} meets the blended edge at both ends; \
2194 its trim end is ambiguous",
2195 coedge.edge_id
2196 )));
2197 }
2198 let target = if start_is_old == coedge.forward { 0 } else { last_index };
2201 let v_end = controls[target].y / controls[target].w;
2202 let span = last_u - first_u;
2203 let fraction = if span.abs() > 0.0 { (uv_new[0] - first_u) / span } else { 0.0 };
2204 (target, [uv_new[0], v_end], fraction)
2205 };
2206 let [q0, q1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2218 let target_point = surface
2219 .evaluate_extended(target_uv[0], target_uv[1])
2220 .or_refuse(KernelStage::Refine, "evaluate_extended")?;
2221 let seed = (q0 + (q1 - q0) * fraction).clamp(q0.min(q1), q0.max(q1));
2227 let q_split = trim_parameter_at_point(
2228 surface, &coedge.pcurve, seed, target_point, pcurve_band,
2229 )?;
2230 let interior = q_split > q0 + 1e-9 * (q1 - q0) && q_split < q1 - 1e-9 * (q1 - q0);
2231 if debug {
2232 eprintln!(
2233 "closed-edge seam trim: face {face_id} edge {} coedge {} ({}): {} at q {q_split:.9} of [{q0:.6}, {q1:.6}], {} control points",
2234 coedge.edge_id, coedge.id, if constant_u { "meridian, constant u" } else { "along u" },
2235 if interior { "SPLIT" } else { "EXTEND" }, controls.len()
2236 );
2237 }
2238 if interior {
2239 let (low, high) = coedge.pcurve.split(q_split).or_refuse(KernelStage::Refine, "split")?;
2240 coedge.pcurve = if target == 0 { high } else { low };
2241 } else {
2242 if coedge.pcurve.degree != 1 || controls.len() != 2 {
2247 return Err(KernelRefusal::unsupported(KernelStage::Sew, "neighbour_seam_extend_curved", format!(
2248 "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)",
2249 coedge.edge_id, coedge.pcurve.degree, controls.len()
2250 )));
2251 }
2252 let w = coedge.pcurve.control_points[target].w;
2253 coedge.pcurve.control_points[target] = crate::Vec4::from_point(
2254 Vec3::new(target_uv[0], target_uv[1], 0.0), w,
2255 );
2256 }
2257 repaired = true;
2258 }
2259 }
2260 Ok(repaired)
2261}
2262
2263pub(in crate::blend) fn trim_seam_edges(
2269 result: &mut BrepSolid,
2270 edge: &EdgeRecord,
2271 new_edge_ids: &[u64],
2272 first_face_edges: &[u64],
2273 first_target: (u64, f64, Vec3),
2274 second_target: (u64, f64, Vec3),
2275) -> Result<bool, KernelRefusal> {
2276 let old_vertex = edge.start_vertex_id;
2277 let mut repaired = false;
2278 let (vertex1_id, v1_new, cr_start) = first_target;
2279 let (second_seam_vertex, v2_new, second_seam_point) = second_target;
2280 for seam_edge in result.edges.iter_mut() {
2281 if new_edge_ids.contains(&seam_edge.id) {
2282 continue;
2283 }
2284 if seam_edge.start_vertex_id != old_vertex && seam_edge.end_vertex_id != old_vertex {
2285 continue;
2286 }
2287 let (target_vertex, target_v, target_point) = if first_face_edges.contains(&seam_edge.id) {
2288 (vertex1_id, v1_new, cr_start)
2289 } else {
2290 (second_seam_vertex, v2_new, second_seam_point)
2291 };
2292 let [c0, c1] = seam_edge.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
2293 let clamped = target_v.clamp(c0.min(c1), c0.max(c1));
2294 let band = 1e-6 * (1.0 + target_point.length());
2303 let split_parameter = if seam_edge.id == edge.id {
2304 clamped
2310 } else {
2311 match seam_edge.curve.evaluate(clamped) {
2312 Ok(point) if point.sub(target_point).length() <= band => clamped,
2313 _ => {
2314 repaired = true;
2315 let projection =
2316 crate::project_point_to_curve(&seam_edge.curve, target_point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
2317 if projection.distance > band {
2318 return Err(KernelRefusal::unsupported(KernelStage::Sew, "neighbour_off_support", format!(
2319 "blend: neighbour edge {} does not pass through the blend's support \
2320 start (off by {:.9}); the trim has no parameter to move to",
2321 seam_edge.id, projection.distance
2322 )));
2323 }
2324 projection.u.clamp(c0.min(c1), c0.max(c1))
2325 }
2326 }
2327 };
2328 if seam_edge.start_vertex_id == old_vertex {
2329 seam_edge.t0 = split_parameter;
2330 seam_edge.start_vertex_id = target_vertex;
2331 }
2332 if seam_edge.end_vertex_id == old_vertex {
2333 seam_edge.t1 = split_parameter;
2334 seam_edge.end_vertex_id = target_vertex;
2335 }
2336 }
2337 Ok(repaired)
2338}