1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use super::*;
3
4pub fn blend_open_edge(
7 solid: &BrepSolid,
8 edge_id: u64,
9 radius: f64,
10 chamfer: bool,
11 name: Option<&str>,
12) -> Result<BrepSolid, KernelRefusal> {
13 let _operation = crate::blend::BlendOperation::enter();
16 if !(radius > 0.0) || !radius.is_finite() {
17 return Err(KernelRefusal::input(KernelStage::Collect, "radius", "blend: radius must be positive"));
18 }
19 blend_open_edge_impl(solid, edge_id, &|_| radius, Some(radius), chamfer, name)
20}
21
22pub(super) fn blend_open_edge_impl(
26 solid: &BrepSolid,
27 edge_id: u64,
28 radius_at: &dyn Fn(f64) -> f64,
29 constant: Option<f64>,
30 chamfer: bool,
31 name: Option<&str>,
32) -> Result<BrepSolid, KernelRefusal> {
33 let edge = solid
34 .edges
35 .iter()
36 .find(|edge| edge.id == edge_id)
37 .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
38 if edge.start_vertex_id == edge.end_vertex_id {
39 return Err(KernelRefusal::input(KernelStage::Collect, "open_edge", "blend: blend_open_edge requires an open edge"));
40 }
41 let (first_face, first_loop, first_coedge) = locate_mate(solid, edge_id, None)?;
42 let (second_face, second_loop, second_coedge) =
43 locate_mate(solid, edge_id, Some((first_face.id, first_loop)))?;
44 let mid_radius = radius_at(edge.t0 + (edge.t1 - edge.t0) * 0.5);
45 let (rho1, rho2) = signed_radii(
46 edge,
47 first_face,
48 first_coedge,
49 second_face,
50 second_coedge,
51 mid_radius,
52 )?;
53 let first_mate = BlendMate {
54 face: first_face,
55 coedge: first_coedge,
56 loop_index: first_loop,
57 rho: rho1,
58 };
59 let second_mate = BlendMate {
60 face: second_face,
61 coedge: second_coedge,
62 loop_index: second_loop,
63 rho: rho2,
64 };
65 let debug = std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok();
73 if debug {
74 let sp = edge.curve.evaluate(edge.t0);
75 let ep = edge.curve.evaluate(edge.t1);
76 eprintln!(
77 "OPEN edge {} v{}->v{} first_face {} second_face {} p0={:?} p1={:?}",
78 edge.id,
79 edge.start_vertex_id,
80 edge.end_vertex_id,
81 first_face.id,
82 second_face.id,
83 sp,
84 ep
85 );
86 }
87 let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
94 let wall_bar = crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
95 let model = crate::KernelTolerances::for_solid(solid, 1e-7).model;
96 let wall_radius = constant.filter(|radius| radius.is_finite() && *radius > 0.0);
99 let wall_check = !chamfer && wall_radius.is_some() && crate::blend::station_refinement_on();
100 let compute = |overshoot_fraction: f64, station_count: usize| -> Result<(FittedRows, Vec<EndSurgery>, bool, OpenRungReading), KernelRefusal> {
101 let (stations, vertex_indices, refinement, dense) = march_open_stations_core(
105 edge,
106 &first_mate,
107 &second_mate,
108 radius_at,
109 overshoot_fraction,
110 [edge.t0, edge.t1],
111 station_count,
112 [false, false],
113 [false, false],
114 crate::blend::station_refinement_on().then_some(rail_bar),
115 if wall_check { OPEN_WALL_CENTRES } else { 0 },
116 true,
117 )?;
118 let parameters = station_parameters(&stations);
119 let rows = fit_open_rows(
120 &stations,
121 ¶meters,
122 chamfer,
123 Some(vertex_indices),
124 extrusion_direction(edge, &first_mate, &second_mate),
125 )?;
126 crate::blend::fold::check_rows_loop(
129 &rows, &stations, ¶meters, Some(vertex_indices), radius_at(0.5 * (edge.t0 + edge.t1)), vec![first_mate.face.id, second_mate.face.id], crate::solid_scale(solid),
130 )?;
131 let mut ends = Vec::with_capacity(2);
132 let mut in_range = true;
133 for (vertex, at_start) in [(edge.start_vertex_id, true), (edge.end_vertex_id, false)] {
134 let (end, side_in_range) = resolve_free_end(
135 solid,
136 edge_id,
137 &first_mate,
138 &second_mate,
139 &rows,
140 vertex,
141 at_start,
142 )?;
143 if debug {
144 eprintln!(
145 " [os {overshoot_fraction:.2}] end v{vertex} at_start={at_start} end_face={} first_boundary={}(t={:.4}) second_boundary={}(t={:.4}) cr={:.4} cs={:.4} in_range={side_in_range}",
146 end.end_face_id,
147 end.first_edge_id,
148 end.first_edge_parameter,
149 end.second_edge_id,
150 end.second_edge_parameter,
151 end.cr_parameter,
152 end.cs_parameter,
153 );
154 }
155 in_range &= side_in_range;
156 ends.push(end);
157 }
158 let windows = [
163 shipped_window(¶meters, ends[0].cr_parameter, ends[1].cr_parameter),
164 shipped_window(¶meters, ends[0].cs_parameter, ends[1].cs_parameter),
165 ];
166 let (rails, rail_samples) =
167 rails_on_shipped_windows(&rows, ¶meters, &stations, [&first_mate.face.surface, &second_mate.face.surface], windows);
168 let walls = match (wall_check, windows) {
169 (false, _) => Vec::new(),
170 (true, [Some(first), Some(second)]) => open_wall_readings_why(
171 &rows,
172 ¶meters,
173 &stations,
174 [first[0].min(second[0]), first[1].max(second[1])],
175 &dense,
176 wall_radius.unwrap_or(f64::NAN),
177 ),
178 (true, _) => vec![Err("no shipped window to read".to_string())],
180 };
181 let first_unread = walls.iter().find_map(|wall| wall.as_ref().err().cloned());
182 let walls: Vec<Option<f64>> = if wall_check && windows.iter().any(|window| window.is_none()) {
183 Vec::new()
184 } else {
185 walls.into_iter().map(|wall| wall.ok()).collect()
186 };
187 let reading = OpenRungReading {
188 station_count,
189 stations: stations.len(),
190 rails,
191 rail_samples,
192 walls,
193 first_unread,
194 refine_exit: refinement.exit,
195 inserted: refinement.inserted,
196 };
197 Ok((rows, ends, in_range, reading))
198 };
199
200 let seeded_fraction: Option<f64> = (|| {
210 let span = edge.t1 - edge.t0;
211 let mut length = 0.0f64;
212 let mut previous = edge.curve.evaluate(edge.t0).ok()?;
213 for index in 1..=16 {
214 let t = edge.t0 + span * index as f64 / 16.0;
215 let point = edge.curve.evaluate(t).ok()?;
216 length += point.sub(previous).length();
217 previous = point;
218 }
219 if !(length > 0.0) || !length.is_finite() {
220 return None;
221 }
222 let mid_t = edge.t0 + span * 0.5;
223 let uv1 = edge_uv_on_face(first_mate.coedge, edge, mid_t).ok()?;
224 let uv2 = edge_uv_on_face(second_mate.coedge, edge, mid_t).ok()?;
225 let n1 = raw_normal(&first_mate.face.surface, uv1[0], uv1[1]).ok()?;
226 let n2 = raw_normal(&second_mate.face.surface, uv2[0], uv2[1]).ok()?;
227 let cos_alpha =
228 (rho1.signum() * rho2.signum() * n1.dot(n2)).clamp(-1.0, 1.0);
229 let tan_half = ((1.0 - cos_alpha).max(0.0) / (1.0 + cos_alpha).max(1e-9)).sqrt();
232 let end_radius = radius_at(edge.t0).abs().max(radius_at(edge.t1).abs());
233 let fraction = (1.5 * end_radius * tan_half / length).clamp(0.08, 0.45);
234 if !fraction.is_finite() || (fraction - 0.08).abs() < 1e-12 {
235 return None;
236 }
237 Some(fraction)
238 })();
239
240 let choose = |station_count: usize| -> Result<(FittedRows, Vec<EndSurgery>, OpenRungReading), KernelRefusal> {
247 let mut chosen: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
248 let mut last_error: Option<KernelRefusal> = None;
249 if let Some(fraction) = seeded_fraction {
253 match compute(fraction, station_count) {
254 Ok((rows, ends, true, reading)) => chosen = Some((rows, ends, reading)),
255 Ok(_) => {}
256 Err(error) if crate::blend::is_wall_fold(&error) => return Err(error),
257 Err(error) => last_error = Some(error),
258 }
259 }
260 if chosen.is_none() {
261 for &overshoot_fraction in &[0.08f64, 0.16, 0.28, 0.45] {
262 match compute(overshoot_fraction, station_count) {
263 Ok((rows, ends, in_range, reading)) => {
264 let fallback = chosen.is_none();
265 if in_range {
266 chosen = Some((rows, ends, reading));
267 break;
268 } else if fallback {
269 chosen = Some((rows, ends, reading));
270 }
271 }
272 Err(error) if crate::blend::is_wall_fold(&error) => return Err(error),
273 Err(error) => last_error = Some(error),
274 }
275 }
276 }
277 chosen.ok_or_else(|| {
278 last_error.unwrap_or_else(|| {
279 KernelRefusal::internal(
280 KernelStage::Refine,
281 "open_march",
282 "blend: open march failed at every overshoot",
283 )
284 })
285 })
286 };
287 let accepts = |reading: &OpenRungReading| {
300 reading.rails_read()
301 && reading.rails <= rail_bar
302 && (!wall_check || reading.worst_wall().is_some_and(|wall| wall <= wall_bar))
303 };
304 let meets_model = |reading: &OpenRungReading| reading.worst_wall().is_some_and(|wall| wall <= model);
305 let mut station_count = STATIONS;
306 let mut accepted: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
307 let mut unaccepted: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
310 let mut request_rungs = 0usize;
311 let mut unmet: Option<crate::BudgetReason> = None;
312 loop {
313 if accepted.is_some() {
316 request_rungs += 1;
317 }
318 let chosen = choose(station_count);
319 let (rows, ends, reading) = match chosen {
320 Ok(chosen) => chosen,
321 Err(_) if accepted.is_some() => {
323 unmet = Some(crate::BudgetReason::Incoherent);
324 break;
325 }
326 Err(error) => return Err(error),
327 };
328 #[cfg(not(test))]
329 let reject_request = false;
330 crate::blend::carve::carve_trace(format_args!(
331 "blend open edge {}: {} stations a rung ({} in all), rails {:.3e} (bar {rail_bar:.3e}), wall {:.3e} (bar {wall_bar:.3e}){}",
332 edge.id, reading.station_count, reading.stations, reading.rails,
333 reading.worst_wall().unwrap_or(f64::INFINITY),
334 if accepted.is_some() { ", a request rung" } else { "" }
335 ));
336 let accepted_now = accepts(&reading) && !reject_request;
337 if accepted.is_none() {
338 if accepted_now {
339 let met = !wall_check || meets_model(&reading);
340 accepted = Some((rows, ends, reading));
341 if met {
342 break;
343 }
344 } else if station_count < MAX_OPEN_STATIONS {
345 station_count *= 2;
346 continue;
347 } else {
348 crate::blend::record_blend_note(format!(
349 "blend: open edge {} at {} stations a rung ({} in all) — the top of the open march's \
350 ladder — still leaves its rails {:.3e} from their carriers (bar {rail_bar:.3e}) or its \
351 wall off the rolling ball (bar {wall_bar:.3e}); the wall ships as built, unaccepted",
352 edge.id, reading.station_count, reading.stations, reading.rails
353 ));
354 unaccepted = Some((rows, ends, reading));
355 break;
356 }
357 } else {
358 if !accepted_now {
359 unmet = Some(crate::BudgetReason::Rejected);
360 break;
361 }
362 let met = meets_model(&reading);
363 accepted = Some((rows, ends, reading));
364 if met {
365 unmet = None;
366 break;
367 }
368 }
369 if station_count < MAX_OPEN_STATIONS {
371 station_count *= 2;
372 } else {
373 unmet = Some(crate::BudgetReason::StationCeiling);
374 break;
375 }
376 }
377 let (shipped_accepted, (rows, ends, reading)) = match (accepted, unaccepted) {
378 (Some(rung), _) => (true, rung),
379 (None, Some(rung)) => (false, rung),
380 (None, None) => {
381 return Err(KernelRefusal::internal(KernelStage::Refine, "open_ladder", "blend: the open ladder ended without a rung"));
382 }
383 };
384 let report: Option<(crate::BudgetReason, f64, f64, String)> = if !shipped_accepted {
388 Some(acceptance_deficit(&reading, rail_bar, wall_bar, wall_check, reading.rails_read(), "no rung of the open ladder was accepted"))
389 } else if wall_check && !meets_model(&reading) {
390 let reason = unmet.unwrap_or(crate::BudgetReason::StationCeiling);
391 let declared = reading.worst_wall().unwrap_or(f64::INFINITY);
393 Some((reason, model, declared, String::new()))
394 } else {
395 None
396 };
397
398 let [start_end, finish_end] = match <[EndSurgery; 2]>::try_from(ends) {
399 Ok(pair) => pair,
400 Err(_) => return Err(KernelRefusal::internal(KernelStage::Sew, "open_surgery_ends", "blend: open surgery needs exactly two ends")),
401 };
402 let mut crossings = SpokeCrossings::default();
406 crossings.note(start_end.escalated || finish_end.escalated);
407 let mut result = solid.clone();
408 let mut take_id = fresh_id_source(solid);
409 let sewn = build_open_surgery(
410 solid,
411 &mut result,
412 &mut take_id,
413 edge,
414 &first_mate,
415 &second_mate,
416 rows,
417 [EndPlan::Free(start_end), EndPlan::Free(finish_end)],
418 name,
419 )?;
420 prune_orphan_vertices(&mut result);
421 crate::blend::fit_planar_charts_to_trims(solid, &mut result)?;
425 check_snap_closure(solid, &result, sewn.snap)?;
426 let built = crossings.gate(result)?;
427 let sewn_surface = built
430 .shells
431 .iter()
432 .flat_map(|shell| &shell.faces)
433 .find(|face| face.id == sewn.blend_face_id)
434 .map(|face| face.surface.clone())
435 .ok_or(KernelRefusal::internal(KernelStage::Sew, "open_blend_face", "blend: the sewn open blend face is not in the built body"))?;
436 if let Some((reason, bar, measured, deficit)) = report {
439 let rungs_used = if shipped_accepted { request_rungs } else { (reading.station_count / STATIONS).trailing_zeros() as usize };
445 let mut detail = format!(
446 "the shipped rung's local rail refinement ended {:?} with {} stations inserted",
447 reading.refine_exit, reading.inserted
448 );
449 if !deficit.is_empty() {
450 detail = format!("{deficit}; {detail}");
451 }
452 crate::blend::record_wall_model_report(crate::blend::WallModelReport {
453 name: name.map(str::to_string),
454 surface: sewn_surface,
455 request: bar,
456 residual: measured,
457 detail: Some(detail),
458 budget: crate::ApproximationBudget {
459 reason,
460 rounds_used: rungs_used,
461 rounds_limit: OPEN_REQUEST_RUNGS,
462 stations: reading.stations,
463 station_limit: REFINE_STATION_FACTOR * (MAX_OPEN_STATIONS + 1),
464 mechanism: crate::BudgetMechanism::StationRungs,
465 measured_component: crate::MeasuredComponent::Wall,
466 unread: 0,
467 },
468 });
469 }
470 Ok(built)
471}
472
473pub(in crate::blend) fn acceptance_deficit(
485 reading: &OpenRungReading,
486 rail_bar: f64,
487 wall_bar: f64,
488 wall_check: bool,
489 rails_read: bool,
490 what: &str,
491) -> (crate::BudgetReason, f64, f64, String) {
492 let rails = (rails_read && reading.rails.is_finite() && reading.rails >= 0.0).then_some(reading.rails);
493 let wall = if wall_check { reading.worst_wall() } else { None };
494 let unread = rails.is_none() || (wall_check && wall.is_none());
495 let mut worst = (0.0_f64, rail_bar);
496 for (value, bar) in [(rails, rail_bar), (wall, wall_bar)] {
497 if let Some(value) = value {
498 if value / bar > worst.0 / worst.1 {
499 worst = (value, bar);
500 }
501 }
502 }
503 let rail_text = match rails {
504 Some(value) => format!("rails {value:.3e} from their carriers (bar {rail_bar:.3e})"),
505 None => format!(
506 "rails UNREAD ({:?} over {} + {} samples) against bar {rail_bar:.3e}",
507 reading.rails, reading.rail_samples[0], reading.rail_samples[1]
508 ),
509 };
510 let wall_text = match (wall_check, wall) {
511 (false, _) => "the wall not judged (no constant-radius fillet provenance)".to_string(),
512 (true, Some(value)) => format!("wall {value:.3e} off the rolling ball (bar {wall_bar:.3e})"),
513 (true, None) => format!(
514 "wall UNREAD on {} of {} intervals against bar {wall_bar:.3e}{}",
515 reading.walls.iter().filter(|wall| !matches!(wall, Some(value) if value.is_finite())).count(),
516 reading.walls.len(),
517 reading.first_unread.as_deref().map(|why| format!(" (first: {why})")).unwrap_or_default()
518 ),
519 };
520 let reason = if unread { crate::BudgetReason::Unread } else { crate::BudgetReason::Unaccepted };
521 (reason, worst.1, worst.0, format!("{what}: {rail_text}, {wall_text}"))
522}
523
524const OPEN_WALL_SECTIONS: usize = 8;
527pub(in crate::blend) const OPEN_WALL_CENTRES: usize = 32;
528pub(in crate::blend) const OPEN_REQUEST_RUNGS: usize = (MAX_OPEN_STATIONS / STATIONS).trailing_zeros() as usize;
531
532#[derive(Clone, Debug)]
539pub(in crate::blend) struct OpenRungReading {
540 pub(in crate::blend) station_count: usize,
541 pub(in crate::blend) stations: usize,
542 pub(in crate::blend) rails: f64,
543 pub(in crate::blend) rail_samples: [usize; 2],
544 pub(in crate::blend) walls: Vec<Option<f64>>,
545 pub(in crate::blend) first_unread: Option<String>,
548 pub(in crate::blend) refine_exit: crate::blend::stations::StationRefineExit,
549 pub(in crate::blend) inserted: usize,
550}
551
552impl OpenRungReading {
553 pub(in crate::blend) fn rails_read(&self) -> bool {
557 self.rails.is_finite() && self.rails >= 0.0 && self.rail_samples.iter().all(|&samples| samples > 0)
558 }
559
560 pub(in crate::blend) fn worst_wall(&self) -> Option<f64> {
562 if self.walls.is_empty() {
563 return None;
564 }
565 self.walls.iter().try_fold(0.0_f64, |worst, wall| match wall {
566 Some(declared) if declared.is_finite() && *declared >= 0.0 => Some(worst.max(*declared)),
567 _ => None,
568 })
569 }
570}
571
572pub(in crate::blend) fn shipped_window(parameters: &[f64], a: f64, b: f64) -> Option<[f64; 2]> {
577 let (&first, &last) = (parameters.first()?, parameters.last()?);
578 let (low, high) = (a.min(b).max(first), a.max(b).min(last));
579 (low.is_finite() && high.is_finite() && low < high).then_some([low, high])
580}
581
582fn interval_at(parameters: &[f64], u: f64) -> Option<(usize, f64)> {
584 let last = parameters.len().checked_sub(2)?;
585 let interval = parameters.partition_point(|¶meter| parameter <= u).saturating_sub(1).min(last);
586 let (from, to) = (parameters[interval], parameters[interval + 1]);
587 (to > from).then(|| (interval, ((u - from) / (to - from)).clamp(0.0, 1.0)))
588}
589
590const AFFINE_READS_DEFAULT: bool = true;
597
598pub(in crate::blend) fn affine_reads_on() -> bool {
599 switch!(live "BREP_BLEND_AFFINE_READS", AFFINE_READS_DEFAULT)
600}
601
602
603fn affine_plane(surface: &NurbsSurface) -> Option<(Vec3, Vec3)> {
609 let (origin, _, _) = affine_plane_frame(surface)?;
610 let net = &surface.control_points;
612 let [p00, p01, p10] = [net[0][0].point().ok()?, net[0][1].point().ok()?, net[1][0].point().ok()?];
613 Some((origin, p10.sub(p00).cross(p01.sub(p00)).normalized().ok()?))
614}
615
616pub(in crate::blend) fn affine_plane_frame(surface: &NurbsSurface) -> Option<(Vec3, Vec3, Vec3)> {
619 if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || surface.degree_u != 1 || surface.degree_v != 1 {
620 return None;
621 }
622 let net = &surface.control_points;
623 if net.len() != 2 || net.iter().any(|row| row.len() != 2) {
624 return None;
625 }
626 let weight = net[0][0].w;
627 if !(weight > 0.0) || net.iter().flatten().any(|point| point.w != weight) {
628 return None;
629 }
630 let [p00, p01, p10, p11] = [net[0][0].point().ok()?, net[0][1].point().ok()?, net[1][0].point().ok()?, net[1][1].point().ok()?];
631 let scale = [p00, p01, p10, p11].iter().fold(0.0f64, |worst, point| worst.max(point.length()));
632 if p11.sub(p10).sub(p01.sub(p00)).length() > 1e-12 * (1.0 + scale) {
633 return None;
634 }
635 let [u0, u1] = surface.domain_u().ok()?;
636 let [v0, v1] = surface.domain_v().ok()?;
637 if !(u1 > u0 && v1 > v0) {
638 return None;
639 }
640 Some((p00, p10.sub(p00).scale(1.0 / (u1 - u0)), p01.sub(p00).scale(1.0 / (v1 - v0))))
641}
642
643fn affine_curve(curve: &NurbsCurve) -> bool {
646 curve.degree == 1
647 && curve.control_points.len() == 2
648 && curve.control_points[0].w > 0.0
649 && curve.control_points[0].w == curve.control_points[1].w
650}
651
652pub(in crate::blend) fn affine_rail_standoff(rail: &NurbsCurve, carrier: &NurbsSurface, window: [f64; 2]) -> Option<f64> {
658 if !affine_curve(rail) {
659 return None;
660 }
661 let (origin, normal) = affine_plane(carrier)?;
662 let mut worst = 0.0f64;
663 for u in window {
664 let point = rail.evaluate(u).ok()?;
665 let off = point.sub(origin).dot(normal).abs();
666 if !off.is_finite() {
667 return None;
668 }
669 worst = worst.max(off);
670 }
671 Some(worst)
672}
673
674const EXACT_REVOLUTION_READS_DEFAULT: bool = true;
678
679pub(in crate::blend) fn exact_revolution_reads_on() -> bool {
680 switch!(live "BREP_EXACT_REVOLUTION_READS", EXACT_REVOLUTION_READS_DEFAULT)
681}
682
683
684pub(in crate::blend) fn exact_revolution_read_bound(rail: &NurbsCurve, carrier: &NurbsSurface, pcurve: &NurbsCurve) -> Option<f64> {
704 if let Some((origin, s_u, s_v)) = affine_plane_frame(carrier) {
705 if pcurve.degree != rail.degree || pcurve.knots != rail.knots || pcurve.control_points.len() != rail.control_points.len() {
707 return None;
708 }
709 let ([u0, _], [v0, _]) = (carrier.domain_u().ok()?, carrier.domain_v().ok()?);
710 let image: Vec<Vec4> = pcurve
711 .control_points
712 .iter()
713 .map(|control| {
714 let (u, v) = (control.x / control.w, control.y / control.w);
715 let point = origin.add(s_u.scale(u - u0)).add(s_v.scale(v - v0));
716 Vec4 { x: point.x * control.w, y: point.y * control.w, z: point.z * control.w, w: control.w }
717 })
718 .collect();
719 return crate::blend::stations::net_bound(&rail.control_points, &image);
720 }
721 if !matches!(carrier.analytic(), Some(crate::AnalyticSurface::RuledRevolution { .. } | crate::AnalyticSurface::Revolution { .. } | crate::AnalyticSurface::Torus { .. } | crate::AnalyticSurface::Sphere { .. })) {
722 return None;
723 }
724 if !affine_curve(pcurve) {
725 return None;
726 }
727 let [q0, q1] = pcurve.domain().ok()?;
728 let (a, b) = (pcurve.evaluate(q0).ok()?, pcurve.evaluate(q1).ok()?);
729 let ([u0, u1], [v0, v1]) = (carrier.domain_u().ok()?, carrier.domain_v().ok()?);
730 let (circle, from, to) = if a.y.to_bits() == b.y.to_bits() && a.x != b.x && a.y >= v0 && a.y <= v1 {
731 (carrier.iso_curve_v(a.y).ok()?, a.x, b.x)
732 } else if a.x.to_bits() == b.x.to_bits() && a.y != b.y && a.x >= u0 && a.x <= u1 {
733 (carrier.iso_curve_u(a.x).ok()?, a.y, b.y)
734 } else {
735 return None;
736 };
737 let [c0, c1] = circle.domain().ok()?;
738 let reversed = to < from;
739 let circle = if reversed { circle.reversed().ok()? } else { circle };
740 let on_curve = |along: f64| if reversed { c0 + c1 - along } else { along };
741 let (lo, hi) = (on_curve(from), on_curve(to));
742 if !(hi > lo) {
743 return None;
744 }
745 let segment = super::support::extend_conic_ends(&circle, lo, hi).ok()?;
746 let [r0, r1] = segment.domain().ok()?;
747 let degree = segment.degree;
748 let mut knots = segment.knots.clone();
749 for knot in knots.iter_mut() {
750 *knot = ((*knot - r0) / (r1 - r0)).clamp(0.0, 1.0);
751 }
752 for knot in knots.iter_mut().take(degree + 1) {
753 *knot = 0.0;
754 }
755 let count = knots.len();
756 for knot in knots.iter_mut().skip(count - degree - 1) {
757 *knot = 1.0;
758 }
759 if rail.degree != degree || rail.knots != knots || rail.control_points.len() != segment.control_points.len() {
760 return None;
761 }
762 crate::blend::stations::net_bound(&rail.control_points, &segment.control_points)
763}
764
765pub(in crate::blend) fn rails_on_shipped_windows(
774 rows: &FittedRows,
775 parameters: &[f64],
776 stations: &[Station],
777 carriers: [&NurbsSurface; 2],
778 windows: [Option<[f64; 2]>; 2],
779) -> (f64, [usize; 2]) {
780 read_rail_windows([&rows.cr, &rows.cs], parameters, stations, carriers, windows,
781 rows.exact_extrusion && affine_reads_on(), certified_rail_bounds(rows, carriers), None)
782}
783
784fn certified_rail_bounds(rows: &FittedRows, carriers: [&NurbsSurface; 2]) -> [Option<f64>; 2] {
788 if rows.revolution_axis.is_none() || !exact_revolution_reads_on() {
789 return [None, None];
790 }
791 [
792 exact_revolution_read_bound(&rows.cr, carriers[0], &rows.cr_pcurve),
793 exact_revolution_read_bound(&rows.cs, carriers[1], &rows.cs_pcurve),
794 ]
795}
796
797fn read_rail_windows(
798 rails: [&NurbsCurve; 2], parameters: &[f64], stations: &[Station],
799 carriers: [&NurbsSurface; 2], windows: [Option<[f64; 2]>; 2], affine: bool,
800 certified: [Option<f64>; 2],
801 mut memo: Option<&mut RailFeet>,
802) -> (f64, [usize; 2]) {
803 let mut worst = 0.0_f64;
804 let mut samples = [0usize; 2];
805 for (side, (rail, carrier)) in [(rails[0], carriers[0]), (rails[1], carriers[1])].into_iter().enumerate() {
806 let Some([low, high]) = windows[side] else { continue };
807 if let Some(off) = affine.then(|| affine_rail_standoff(rail, carrier, [low, high])).flatten() {
810 crate::blend::record_count("count.read.affine_rails", 1);
811 samples[side] += 2;
812 if !worst.is_nan() && off > worst {
813 worst = off;
814 }
815 continue;
816 }
817 if let Some(bound) = certified[side] {
821 crate::blend::record_count("count.read.revolution_rails", 1);
822 samples[side] += 1;
823 if !worst.is_nan() && bound > worst {
824 worst = bound;
825 }
826 continue;
827 }
828 let mut at = vec![low, high];
829 for pair in parameters.windows(2) {
830 for fraction in [0.25, 0.5, 0.75] {
831 let u = pair[0] + (pair[1] - pair[0]) * fraction;
832 if u > low && u < high {
833 at.push(u);
834 }
835 }
836 }
837 for u in at {
838 let cached = memo.as_ref().and_then(|memo| memo.values[side].get(&u.to_bits()).copied());
839 let off = if let Some(off) = cached {
840 if let Some(memo) = memo.as_mut() { memo.hits += 1; }
841 crate::blend::record_count("count.read.rail_foot_reused", 1);
842 off
843 } else {
844 if let Some(memo) = memo.as_mut() {
845 memo.reads += 1;
846 crate::blend::record_count("count.read.rail_foot_fresh", 1);
847 }
848 let off = interval_at(parameters, u)
849 .and_then(|(interval, fraction)| {
850 let (left, right) = (&stations[interval], &stations[interval + 1]);
851 let (a, b) = if side == 0 { (left.uv1, right.uv1) } else { (left.uv2, right.uv2) };
852 let seed = [a[0] + fraction * (b[0] - a[0]), a[1] + fraction * (b[1] - a[1])];
853 let point = rail.evaluate(u).ok()?;
854 crate::blend::stations::extended_foot_distance(carrier, point, seed).ok()
855 })
856 .filter(|off| off.is_finite())
857 .unwrap_or(f64::NAN);
858 if off.is_finite() {
859 if let Some(memo) = memo.as_mut() { memo.values[side].insert(u.to_bits(), off); }
860 }
861 off
862 };
863 samples[side] += 1;
864 if !worst.is_nan() && (off.is_nan() || off > worst) {
865 worst = off;
866 }
867 }
868 }
869 (worst, samples)
870}
871
872pub(in crate::blend) struct RailWindowReader {
878 rails: [NurbsCurve; 2],
879 carriers: [NurbsSurface; 2],
880 parameters: Vec<f64>,
881 stations: Vec<Station>,
882 affine: bool,
883 certified: [Option<f64>; 2],
884 feet: RailFeet,
885}
886
887#[derive(Default)]
888struct RailFeet {
889 values: [rustc_hash::FxHashMap<u64, f64>; 2],
890 reads: u64,
891 hits: u64,
892}
893
894impl RailWindowReader {
895 pub(in crate::blend) fn new(rows: &FittedRows, parameters: &[f64], stations: &[Station], carriers: [&NurbsSurface; 2]) -> Self {
896 Self {
897 rails: [rows.cr.clone(), rows.cs.clone()],
898 carriers: [carriers[0].clone(), carriers[1].clone()],
899 parameters: parameters.to_vec(), stations: stations.to_vec(),
900 affine: rows.exact_extrusion && affine_reads_on(), certified: certified_rail_bounds(rows, carriers), feet: RailFeet::default(),
901 }
902 }
903
904 pub(in crate::blend) fn read(&mut self, windows: [Option<[f64; 2]>; 2]) -> (f64, [usize; 2]) {
905 read_rail_windows([&self.rails[0], &self.rails[1]], &self.parameters, &self.stations,
906 [&self.carriers[0], &self.carriers[1]], windows, self.affine, self.certified, Some(&mut self.feet))
907 }
908}
909
910pub(in crate::blend) fn network_rail_reuse_on() -> bool {
911 std::env::var("BREP_NETWORK_RAIL_REUSE").map_or(true, |value| value != "0" && value != "off")
914}
915
916pub(in crate::blend) fn junction_stencil(
926 left_nodes: &[Vec3],
927 right_nodes: &[Vec3],
928 station_ts: &[f64],
929 left: usize,
930) -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
931 let half = OPEN_WALL_CENTRES / 2;
932 if left_nodes.len() != OPEN_WALL_CENTRES + 1 || right_nodes.len() != OPEN_WALL_CENTRES + 1 || left + 2 >= station_ts.len() {
933 return Err(format!("interval {left}: junction needs two {}-node intervals and their three station parameters", OPEN_WALL_CENTRES + 1));
934 }
935 let (t0, t1, t2) = (station_ts[left], station_ts[left + 1], station_ts[left + 2]);
936 let (w_left, w_right) = (t1 - t0, t2 - t1);
937 if !(t0.is_finite() && t1.is_finite() && t2.is_finite() && w_left > 0.0 && w_right > 0.0 && w_left.is_finite() && w_right.is_finite()) {
938 return Err(format!("interval {left}: junction station parameters {t0}, {t1}, {t2} are not finite and strictly increasing"));
939 }
940 let nodes: Vec<Vec3> = left_nodes[half..].iter().chain(&right_nodes[1..=half]).copied().collect();
941 if w_left.to_bits() == w_right.to_bits() {
942 return Ok((nodes, None));
943 }
944 let abscissae: Vec<f64> = (half..=OPEN_WALL_CENTRES)
945 .map(|j| if j == OPEN_WALL_CENTRES { t1 } else { t0 + w_left * j as f64 / OPEN_WALL_CENTRES as f64 })
946 .chain((1..=half).map(|j| t1 + w_right * j as f64 / OPEN_WALL_CENTRES as f64))
947 .collect();
948 Ok((nodes, Some(abscissae)))
949}
950
951pub(in crate::blend) fn read_on_stencil(point: Vec3, nodes: &[Vec3], abscissae: Option<&[f64]>, step: usize, radius: f64) -> Result<(f64, f64), String> {
955 match abscissae {
956 None => crate::blend::chain::declared_wall_probe_why(point, nodes, step, radius),
957 Some(ts) => crate::blend::chain::declared_wall_probe_at_why(point, nodes, ts, step, radius),
958 }
959}
960
961pub(in crate::blend) fn exact_extrusion_wall_readings(
996 rows: &FittedRows,
997 parameters: &[f64],
998 window: [f64; 2],
999 radius: f64,
1000) -> Vec<Result<f64, String>> {
1001 let intervals = parameters.len().saturating_sub(1);
1002 let overlapped: Vec<usize> =
1003 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
1004 let line = match (rows.exact_extrusion, rows.center.as_ref()) {
1005 (true, Some(center)) => center.domain().and_then(|[a, b]| Ok((center.evaluate(a)?, center.evaluate(b)?))),
1006 (false, _) => Err("the rows are not an exact extrusion".to_string()),
1007 (true, None) => Err("the exact rows carry no centre line".to_string()),
1008 };
1009 let (anchor, direction) = match line.and_then(|(a, b)| b.sub(a).normalized().map(|d| (a, d))) {
1010 Ok(line) => line,
1011 Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
1012 };
1013 let Ok([v_low, v_high]) = rows.surface.domain_v() else {
1014 return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
1015 };
1016 overlapped
1017 .into_iter()
1018 .map(|interval| -> Result<f64, String> {
1019 let (from, to) = (parameters[interval], parameters[interval + 1]);
1020 let inside = |u: f64| u >= window[0] && u <= window[1];
1021 let mut probes: Vec<f64> =
1022 (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
1023 probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
1024 if probes.is_empty() {
1025 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1026 }
1027 let mut worst = 0.0_f64;
1028 for u in probes {
1029 for k in 1..OPEN_WALL_SECTIONS {
1030 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1031 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1032 let offset = point.sub(anchor);
1033 let standoff = offset.sub(direction.scale(offset.dot(direction))).length();
1034 let reading = (standoff - radius).abs();
1035 if !reading.is_finite() {
1036 return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
1037 }
1038 worst = worst.max(reading);
1039 }
1040 }
1041 Ok(worst)
1042 })
1043 .collect()
1044}
1045
1046pub(in crate::blend) fn exact_revolution_wall_readings(
1057 rows: &FittedRows,
1058 parameters: &[f64],
1059 window: [f64; 2],
1060 radius: f64,
1061) -> Vec<Result<f64, String>> {
1062 let intervals = parameters.len().saturating_sub(1);
1063 let overlapped: Vec<usize> =
1064 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
1065 let circle = match (rows.revolution_axis, rows.center.as_ref()) {
1066 (Some((origin, axis)), Some(center)) => center.domain().and_then(|[a, _]| center.evaluate(a)).map(|point| (origin, axis, point)),
1067 (None, _) => Err("the rows are not an exact revolution".to_string()),
1068 (Some(_), None) => Err("the exact rows carry no centre circle".to_string()),
1069 };
1070 let (origin, axis, centre_radius, centre_height) = match circle {
1071 Ok((origin, axis, point)) => {
1072 let offset = point.sub(origin);
1073 let height = offset.dot(axis);
1074 (origin, axis, offset.sub(axis.scale(height)).length(), height)
1075 }
1076 Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
1077 };
1078 let Ok([v_low, v_high]) = rows.surface.domain_v() else {
1079 return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
1080 };
1081 overlapped
1082 .into_iter()
1083 .map(|interval| -> Result<f64, String> {
1084 let (from, to) = (parameters[interval], parameters[interval + 1]);
1085 let inside = |u: f64| u >= window[0] && u <= window[1];
1086 let mut probes: Vec<f64> =
1087 (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
1088 probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
1089 if probes.is_empty() {
1090 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1091 }
1092 let mut worst = 0.0_f64;
1093 for u in probes {
1094 for k in 1..OPEN_WALL_SECTIONS {
1095 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1096 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1097 let offset = point.sub(origin);
1098 let height = offset.dot(axis);
1099 let radial = offset.sub(axis.scale(height)).length();
1100 let standoff = (radial - centre_radius).hypot(height - centre_height);
1101 let reading = (standoff - radius).abs();
1102 if !reading.is_finite() {
1103 return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
1104 }
1105 worst = worst.max(reading);
1106 }
1107 }
1108 Ok(worst)
1109 })
1110 .collect()
1111}
1112
1113pub(in crate::blend) fn open_wall_readings_why(
1114 rows: &FittedRows,
1115 parameters: &[f64],
1116 stations: &[Station],
1117 window: [f64; 2],
1118 dense: &OpenDenseCentres,
1119 radius: f64,
1120) -> Vec<Result<f64, String>> {
1121 let intervals = parameters.len().saturating_sub(1);
1122 let overlapped: Vec<usize> =
1123 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
1124 let Ok([v_low, v_high]) = rows.surface.domain_v() else {
1125 return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
1126 };
1127 let nodes_of = |interval: usize| -> Result<Vec<Vec3>, String> {
1129 let inner = dense.centres.get(interval).ok_or_else(|| format!("interval {interval}: no dense centre row"))?;
1130 if dense.subdivisions != OPEN_WALL_CENTRES || inner.len() + 1 != OPEN_WALL_CENTRES {
1131 return Err(format!("interval {interval}: dense row {} centres at {} subdivisions", inner.len(), dense.subdivisions));
1132 }
1133 let mut centres = Vec::with_capacity(OPEN_WALL_CENTRES + 1);
1134 centres.push(stations[interval].center);
1135 for (j, centre) in inner.iter().enumerate() {
1136 centres.push(centre.ok_or_else(|| format!("interval {interval}: exact centre {} of {OPEN_WALL_CENTRES} unsolved", j + 1))?);
1137 }
1138 centres.push(stations[interval + 1].center);
1139 Ok(centres)
1140 };
1141 let half = OPEN_WALL_CENTRES / 2;
1142 let junction = |left: usize| -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
1149 if dense.station_ts.len() != parameters.len() {
1150 return Err(format!("interval {left}: junction without the native station parameters"));
1151 }
1152 junction_stencil(&nodes_of(left)?, &nodes_of(left + 1)?, &dense.station_ts, left)
1153 };
1154 overlapped
1155 .into_iter()
1156 .map(|interval| -> Result<f64, String> {
1157 let centres = nodes_of(interval)?;
1158 let (from, to) = (parameters[interval], parameters[interval + 1]);
1159 let inside = |u: f64| u >= window[0] && u <= window[1];
1160 let mut probes: Vec<(f64, usize, Option<(Vec<Vec3>, Option<Vec<f64>>)>)> = (2..OPEN_WALL_CENTRES)
1163 .step_by(2)
1164 .map(|step| (from + (to - from) * step as f64 / OPEN_WALL_CENTRES as f64, step))
1165 .filter(|(u, _)| inside(*u))
1166 .map(|(u, step)| (u, step, None))
1167 .collect();
1168 for end in window {
1169 if end > from && end < to {
1170 let fraction = (end - from) / (to - from);
1171 let nearest = 2 * ((fraction * OPEN_WALL_CENTRES as f64 / 2.0).round() as usize);
1172 #[cfg(not(test))]
1173 let junction_off = false;
1174 if junction_off {
1175 probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1176 } else if nearest < 2 && interval > 0 {
1177 probes.push((end, half, Some(junction(interval - 1)?)));
1178 } else if nearest > OPEN_WALL_CENTRES - 2 && interval + 1 < intervals {
1179 probes.push((end, half, Some(junction(interval)?)));
1180 } else {
1181 probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1182 }
1183 }
1184 }
1185 if probes.is_empty() {
1186 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1187 }
1188 let mut declared = 0.0_f64;
1189 for (u, step, stencil) in probes {
1190 let (nodes, abscissae, what) = match &stencil {
1191 Some((nodes, None)) => (nodes, None, "junction"),
1192 Some((nodes, Some(ts))) => (nodes, Some(ts), "native-abscissa junction"),
1193 None => (¢res, None, "interval"),
1194 };
1195 for k in 1..OPEN_WALL_SECTIONS {
1196 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1197 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1198 let read = if stencil.is_none() {
1203 crate::blend::chain::declared_wall_probe_located_why(point, nodes, step, (u - from) / (to - from), radius)
1204 } else {
1205 read_on_stencil(point, nodes, abscissae.map(|ts| ts.as_slice()), step, radius)
1206 };
1207 let (reading, uncertainty) = read.map_err(|why| {
1208 format!(
1209 "interval {interval}: declared reader declined probe node {step} of the {what} stencil, section {k}/{OPEN_WALL_SECTIONS} \
1210 (u {u:.17}): {why}"
1211 )
1212 })?;
1213 declared = declared.max(reading + uncertainty);
1214 }
1215 }
1216 if declared.is_finite() { Ok(declared) } else { Err(format!("interval {interval}: non-finite reading {declared}")) }
1217 })
1218 .collect()
1219}
1220
1221
1222pub(in crate::blend) fn fresh_id_source(solid: &BrepSolid) -> impl FnMut() -> u64 {
1227 let mut next_id = solid
1228 .vertices
1229 .iter()
1230 .map(|vertex| vertex.id)
1231 .chain(solid.edges.iter().map(|edge| edge.id))
1232 .chain(
1233 solid
1234 .shells
1235 .iter()
1236 .flat_map(|shell| &shell.faces)
1237 .flat_map(|face| {
1238 face.loops
1239 .iter()
1240 .map(|loop_record| loop_record.id)
1241 .chain(face.loops.iter().flat_map(|loop_record| {
1242 loop_record.coedges.iter().map(|coedge| coedge.id)
1243 }))
1244 .chain(std::iter::once(face.id))
1245 }),
1246 )
1247 .max()
1248 .unwrap_or(0)
1249 + 1;
1250 move || {
1251 let id = next_id;
1252 next_id += 1;
1253 id
1254 }
1255}
1256
1257
1258pub(in crate::blend) fn march_trim_stands(read: &Result<(f64, f64, usize, f64), KernelRefusal>, floor: f64) -> bool {
1267 matches!(read, Ok((miss, standoff, _, _)) if *miss + *standoff <= floor)
1268}
1269
1270fn exact_planar_trim(rail: &NurbsCurve, surface: &NurbsSurface, d0: f64, d1: f64) -> Result<Option<NurbsCurve>, KernelRefusal> {
1280 if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || !(d1 > d0) {
1281 return Ok(None);
1282 }
1283 let feet_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.feet_ms");
1284 let mut control = Vec::with_capacity(2);
1285 for t in [d0, d1] {
1286 let point = rail.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
1287 let foot = crate::project_point_to_surface(surface, point).or_refuse(KernelStage::Refine, "project")?;
1288 control.push(crate::Vec4::from_point(crate::Vec3::new(foot.u, foot.v, 0.0), 1.0));
1289 }
1290 let line = NurbsCurve::new(1, vec![d0, d0, d1, d1], control).or_refuse(KernelStage::Refine, "new")?;
1291 feet_timing.stop();
1292 let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1293 let read = match affine_reads_on().then(|| affine_trim_read(rail, surface, &line, d0, d1)).flatten() {
1294 Some(read) => {
1295 crate::blend::record_count("count.read.affine_trims", 1);
1296 Ok(read)
1297 }
1298 None => crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &line, d0, d1),
1299 };
1300 read_timing.stop();
1301 Ok(march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE).then_some(line))
1302}
1303
1304fn affine_trim_read(rail: &NurbsCurve, surface: &NurbsSurface, pcurve: &NurbsCurve, d0: f64, d1: f64) -> Option<(f64, f64, usize, f64)> {
1312 if !affine_curve(rail) || !affine_curve(pcurve) {
1313 return None;
1314 }
1315 let (origin, normal) = affine_plane(surface)?;
1316 let (mut miss, mut standoff, mut worst) = (0.0f64, 0.0f64, d0);
1317 for t in [d0, d1] {
1318 let point = rail.evaluate(t).ok()?;
1319 let foot = point.sub(normal.scale(point.sub(origin).dot(normal)));
1320 let uv = pcurve.evaluate(t).ok()?;
1321 let image = crate::blend::track_fit::carrier_point(surface, uv.x, uv.y).ok()?;
1322 let (residual, off) = (image.sub(foot).length(), point.sub(foot).length());
1323 if !(residual.is_finite() && off.is_finite()) {
1324 return None;
1325 }
1326 if residual > miss {
1327 (miss, worst) = (residual, t);
1328 }
1329 standoff = standoff.max(off);
1330 }
1331 Some((miss, standoff, 2, worst))
1332}
1333
1334pub(in crate::blend) fn build_open_surgery(
1346 solid: &BrepSolid,
1347 result: &mut BrepSolid,
1348 take_id: &mut dyn FnMut() -> u64,
1349 edge: &EdgeRecord,
1350 first: &BlendMate,
1351 second: &BlendMate,
1352 rows: FittedRows,
1353 ends: [EndPlan; 2],
1354 name: Option<&str>,
1355) -> Result<SewnStripe, KernelRefusal> {
1356 build_open_surgery_with_tail(solid, result, take_id, edge, first, second, rows, ends, name, [None, None])
1357}
1358
1359#[allow(clippy::too_many_arguments)]
1369pub(in crate::blend) fn build_open_surgery_with_tail(
1370 solid: &BrepSolid,
1371 result: &mut BrepSolid,
1372 take_id: &mut dyn FnMut() -> u64,
1373 edge: &EdgeRecord,
1374 first: &BlendMate,
1375 second: &BlendMate,
1376 rows: FittedRows,
1377 ends: [EndPlan; 2],
1378 name: Option<&str>,
1379 tail: [Option<crate::blend::rollover::TailSplice<'_>>; 2],
1380) -> Result<SewnStripe, KernelRefusal> {
1381 let [start_end, finish_end] = ends;
1382
1383 let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, KernelRefusal> {
1390 let [low, high] = row.domain().or_refuse(KernelStage::Refine, "domain")?;
1391 let tail = if a == low { row.clone() } else { row.split(a).or_refuse(KernelStage::Refine, "split")?.1 };
1392 if b == high {
1393 return Ok(tail);
1394 }
1395 let (middle, _) = tail.split(b).or_refuse(KernelStage::Refine, "split")?;
1396 Ok(middle)
1397 };
1398 let kind = |plan: &EndPlan| match plan {
1399 EndPlan::Free(_) => "free",
1400 EndPlan::Corner(_) => "corner",
1401 EndPlan::Miter(_) => "miter",
1402 EndPlan::Cap(_) => "cap",
1403 };
1404 let describe_trim = |error: KernelRefusal| {
1405 error.with_message(|error| {
1406 format!(
1407 "{error} (edge {} rows trimmed for a {} start at cr {:.6}/cs {:.6} and a {} finish \
1408 at cr {:.6}/cs {:.6})",
1409 edge.id,
1410 kind(&start_end),
1411 start_end.cr_parameter(),
1412 start_end.cs_parameter(),
1413 kind(&finish_end),
1414 finish_end.cr_parameter(),
1415 finish_end.cs_parameter()
1416 )
1417 })
1418 };
1419 let band = consumed_band(solid);
1429 let exact_trim = crate::blend::exact_trim_on();
1430 let collapses = |row: &NurbsCurve, a: f64, b: f64| -> Result<bool, KernelRefusal> {
1431 let from = row.evaluate(a).or_refuse(KernelStage::Refine, "evaluate")?;
1432 Ok(row.evaluate(b).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band
1433 && row.evaluate(0.5 * (a + b)).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band)
1434 };
1435 let cr_collapsed = collapses(&rows.cr, start_end.cr_parameter(), finish_end.cr_parameter())?;
1436 let cs_collapsed = collapses(&rows.cs, start_end.cs_parameter(), finish_end.cs_parameter())?;
1437 if cr_collapsed || cs_collapsed {
1438 let unsupported = if start_end.free().is_some() && finish_end.free().is_some() {
1439 Some("neither end is a corner")
1440 } else if first.face.id == second.face.id {
1441 Some("both rails lie on one face")
1442 } else if matches!(start_end, EndPlan::Cap(_)) || matches!(finish_end, EndPlan::Cap(_)) {
1443 Some("a capped end has no corner to share the point with")
1444 } else {
1445 None
1446 };
1447 if let Some(reason) = unsupported {
1448 return Err(KernelRefusal::unsupported(KernelStage::Sew, super::RAIL_COLLAPSE_WHAT, format!(
1449 "{RAIL_COLLAPSE_UNSUPPORTED} edge {}'s rail shrinks to a point and {reason}",
1450 edge.id
1451 )));
1452 }
1453 }
1454 let trim_rail = |collapsed: bool,
1468 row: &NurbsCurve,
1469 pcurve: &NurbsCurve,
1470 surface: &NurbsSurface,
1471 a: f64,
1472 b: f64|
1473 -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1474 if collapsed {
1475 return Ok(None);
1476 }
1477 let rows_timing = crate::blend::BlendTimer::start("timing.surgery.trim.rows_ms");
1478 let rail = trim_row(row, a, b).map_err(describe_trim)?;
1479 let interpolated = trim_row(pcurve, a, b)?;
1480 rows_timing.stop();
1481 {
1482 let overrun_timing = crate::blend::BlendTimer::start("timing.surgery.trim.overrun_ms");
1483 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1484 let overrun = crate::blend::planar_chart_overrun(surface, &|t| rail.evaluate(t), d0, d1)?;
1485 overrun_timing.stop();
1486 if overrun.is_some_and(|excursion| excursion > band) {
1487 return Ok(Some((rail, interpolated)));
1488 }
1489 }
1490 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1491 if exact_trim && rows.exact_extrusion {
1495 if let Some(line) = exact_planar_trim(&rail, surface, d0, d1)? {
1496 crate::blend::record_count("count.surgery.exact_trim", 1);
1497 return Ok(Some((rail, line)));
1498 }
1499 }
1500 if rows.revolution_axis.is_some() {
1505 if exact_revolution_reads_on() {
1510 let cert_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.cert_ms");
1511 let bound = exact_revolution_read_bound(row, surface, pcurve);
1512 cert_timing.stop();
1513 if bound.is_some_and(|bound| 3.0 * bound <= crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
1514 crate::blend::record_count("count.surgery.exact_revolution_trim_certified", 1);
1515 return Ok(Some((rail, interpolated)));
1516 }
1517 }
1518 let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1519 let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &interpolated, d0, d1);
1520 read_timing.stop();
1521 if march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
1522 crate::blend::record_count("count.surgery.exact_revolution_trim", 1);
1523 return Ok(Some((rail, interpolated)));
1524 }
1525 crate::blend::record_count("count.surgery.exact_revolution_trim_declined", 1);
1526 }
1527 let mut march_feet = crate::blend::track_fit::FootMemo::default();
1536 let mut refit_feet = crate::blend::track_fit::FootMemo::default();
1537 let march_read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.march_ms");
1538 let march_read = crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &interpolated, d0, d1, &mut march_feet);
1539 march_read_timing.stop();
1540 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1541 let declined = |why: KernelRefusal| -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1542 let stands = march_trim_stands(&march_read, floor);
1545 if stands {
1546 Ok(Some((rail.clone(), interpolated.clone())))
1547 } else {
1548 Err(why)
1549 }
1550 };
1551 let refit_timing = crate::blend::BlendTimer::start("timing.surgery.trim.refit_ms");
1552 let refit = super::closed::refit_closed_support_pcurve_read(&rail, &interpolated, surface, &mut refit_feet);
1553 refit_timing.stop();
1554 let _reads_timing = crate::blend::BlendTimer::start("timing.surgery.trim.reads_ms");
1555 match refit {
1556 Ok((fitted, refit_read)) => {
1557 let (march_standoff, refit_standoff) = match (&march_read, Ok::<_, KernelRefusal>(refit_read)) {
1574 (_, Err(error)) => return declined(error),
1575 (Err(error), Ok(_)) => {
1576 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch_unread", format!(
1577 "blend: a support trim's refit cannot be shown to stand on its rail's branch: the march's own \
1578 trim is unreadable ({})",
1579 error.message
1580 )));
1581 }
1582 (Ok((_, march_standoff, _, _)), Ok((_, refit_standoff, _, _))) => (*march_standoff, refit_standoff),
1583 };
1584 let excess_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.excess_ms");
1585 let excess = crate::blend::track_fit::standoff_excess_memo(
1586 surface, &|t| rail.evaluate(t), &interpolated, &fitted, d0, d1, &mut march_feet, &mut refit_feet,
1587 );
1588 excess_timing.stop();
1589 let excess = match excess {
1590 Ok(excess) => excess,
1591 Err(error) => return declined(error),
1592 };
1593 if !(excess <= floor) {
1594 return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch", format!(
1595 "blend: a support trim's refit stands up to {excess:.3e} farther off its rail than the march's own trim \
1596 at the same rail parameter (largest standoffs {refit_standoff:.3e} and {march_standoff:.3e}): it took \
1597 another branch of the carrier"
1598 )));
1599 }
1600 let nearest_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.nearest_ms");
1608 let absolute = crate::blend::track_fit::standoff_over_nearest_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut refit_feet);
1609 nearest_timing.stop();
1610 let absolute = match absolute {
1611 Ok(absolute) => absolute,
1612 Err(error) => return declined(error),
1613 };
1614 if !(absolute <= floor) {
1615 return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_off_rail", format!(
1616 "blend: a support trim's refit stands up to {absolute:.3e} farther off its rail than the rail stands \
1617 off its carrier: its foot is not the rail's nearest point"
1618 )));
1619 }
1620 Ok(Some((rail, fitted)))
1621 }
1622 Err(error) => declined(error),
1623 }
1624 };
1625 let trim_timing = crate::blend::BlendTimer::start("timing.surgery.trim_rails_ms");
1626 let cr_rail = trim_rail(
1627 cr_collapsed,
1628 &rows.cr,
1629 &rows.cr_pcurve,
1630 &first.face.surface,
1631 start_end.cr_parameter(),
1632 finish_end.cr_parameter(),
1633 )?;
1634 let cs_rail = trim_rail(
1635 cs_collapsed,
1636 &rows.cs,
1637 &rows.cs_pcurve,
1638 &second.face.surface,
1639 start_end.cs_parameter(),
1640 finish_end.cs_parameter(),
1641 )?;
1642 trim_timing.stop();
1643 let cr_domain = match &cr_rail {
1645 Some((cr, _)) => cr.domain().or_refuse(KernelStage::Refine, "domain")?,
1646 None => [start_end.cr_parameter(), finish_end.cr_parameter()],
1647 };
1648 let cs_domain = match &cs_rail {
1649 Some((cs, _)) => cs.domain().or_refuse(KernelStage::Refine, "domain")?,
1650 None => [start_end.cs_parameter(), finish_end.cs_parameter()],
1651 };
1652 let cr_at = |parameter: f64| match &cr_rail {
1654 Some((cr, _)) => cr.evaluate(parameter),
1655 None => rows.cr.evaluate(parameter),
1656 };
1657 let cs_at = |parameter: f64| match &cs_rail {
1658 Some((cs, _)) => cs.evaluate(parameter),
1659 None => rows.cs.evaluate(parameter),
1660 };
1661 let old_start = edge.start_vertex_id;
1662 let old_finish = edge.end_vertex_id;
1663
1664 let classify =
1675 |boundary_id: u64, boundary_param: f64, corner: u64| -> Result<RimResolution, KernelRefusal> {
1676 let boundary = result
1677 .edges
1678 .iter()
1679 .find(|candidate| candidate.id == boundary_id)
1680 .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_boundary_edge", "blend: end boundary edge missing during surgery"))?;
1681 let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
1682 let far_vertex = if boundary.start_vertex_id == corner {
1683 Some((boundary.end_vertex_id, boundary.t1, boundary.t0))
1684 } else if boundary.end_vertex_id == corner {
1685 Some((boundary.start_vertex_id, boundary.t0, boundary.t1))
1686 } else {
1687 None
1688 };
1689 if let Some((far_id, far_t, near_t)) = far_vertex {
1690 let far_point = result
1701 .vertices
1702 .iter()
1703 .find(|candidate| candidate.id == far_id)
1704 .map(|candidate| candidate.point)
1705 .ok_or(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"))?;
1706 let crossing = boundary.curve.evaluate_extended(boundary_param).or_refuse(KernelStage::Refine, "evaluate_extended")?;
1707 let _ = span;
1708 let past_far = crossing.sub(far_point).length();
1709 if past_far <= band {
1710 if !result
1712 .vertices
1713 .iter()
1714 .any(|candidate| candidate.id == far_id)
1715 {
1716 return Err(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"));
1717 }
1718 return Ok(RimResolution::Consumed(far_id));
1719 }
1720 if (boundary_param - far_t) * (far_t - near_t) > 0.0 {
1733 return Err(KernelRefusal::unsupported(KernelStage::Sew, "blend_wider_than_face", format!(
1734 "{BLEND_WIDER_THAN_FACE} its rail crosses edge {boundary_id} {past_far:.3e} \
1735 past that edge's far vertex, and a rail within {band:.3e} of it is the \
1736 most this surgery snaps onto the edge"
1737 )));
1738 }
1739 }
1740 Ok(RimResolution::Fresh)
1741 };
1742 let resolve_side = |plan: &EndPlan,
1746 side_first: bool,
1747 corner: u64|
1748 -> Result<RimResolution, KernelRefusal> {
1749 if let Some(vertex) = plan.planned_rim(side_first) {
1750 return Ok(RimResolution::Corner(vertex));
1751 }
1752 match plan {
1753 EndPlan::Free(free) => classify(
1754 if side_first {
1755 free.first_edge_id
1756 } else {
1757 free.second_edge_id
1758 },
1759 if side_first {
1760 free.first_edge_parameter
1761 } else {
1762 free.second_edge_parameter
1763 },
1764 corner,
1765 ),
1766 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => {
1767 Err(KernelRefusal::internal(KernelStage::Sew, "rim_vertices", "blend: planned end without rim vertices"))
1768 }
1769 }
1770 };
1771 let crossings_timing = crate::blend::BlendTimer::start("timing.surgery.crossings_ms");
1772 let start_first = resolve_side(&start_end, true, old_start)?;
1773 let start_second = resolve_side(&start_end, false, old_start)?;
1774 let finish_first = resolve_side(&finish_end, true, old_finish)?;
1775 let finish_second = resolve_side(&finish_end, false, old_finish)?;
1776 crossings_timing.stop();
1777 let _topology_timing = crate::blend::BlendTimer::start("timing.surgery.topology_ms");
1778
1779 let first_use_forward = first.coedge.forward;
1782 let blend_cr_forward = !first_use_forward;
1783
1784 let (sew_first, sew_second) = match (start_end.free(), finish_end.free(), &cr_rail, &cs_rail) {
1799 (Some(start_free), Some(finish_free), Some((cr, _)), Some((cs, _))) => (
1800 detect_row_coincidence(
1801 solid,
1802 first,
1803 edge.id,
1804 start_free.first_edge_id,
1805 finish_free.first_edge_id,
1806 start_first.is_consumed(),
1807 start_first.existing().unwrap_or(0),
1808 finish_first.is_consumed(),
1809 finish_first.existing().unwrap_or(0),
1810 cr,
1811 blend_cr_forward,
1812 band,
1813 )?,
1814 detect_row_coincidence(
1815 solid,
1816 second,
1817 edge.id,
1818 start_free.second_edge_id,
1819 finish_free.second_edge_id,
1820 start_second.is_consumed(),
1821 start_second.existing().unwrap_or(0),
1822 finish_second.is_consumed(),
1823 finish_second.existing().unwrap_or(0),
1824 cs,
1825 !blend_cr_forward,
1826 band,
1827 )?,
1828 ),
1829 _ => (None, None),
1830 };
1831 let (sew_first, sew_second): (Option<RowSew>, Option<RowSew>) =
1835 if first.face.id == second.face.id {
1836 (None, None)
1837 } else {
1838 (sew_first, sew_second)
1839 };
1840
1841 let mut resolve = |rim: &RimResolution, point: Result<Vec3, String>| -> Result<u64, KernelRefusal> {
1844 match rim.existing() {
1845 Some(id) => Ok(id),
1846 None => {
1847 let id = take_id();
1848 result.vertices.push(VertexRecord { id, point: point.or_refuse(KernelStage::Refine, "point")? });
1849 Ok(id)
1850 }
1851 }
1852 };
1853 let w1a = resolve(&start_first, cr_at(cr_domain[0]))?;
1854 let w1b = resolve(&finish_first, cr_at(cr_domain[1]))?;
1855 let w2a = resolve(&start_second, cs_at(cs_domain[0]))?;
1856 let w2b = resolve(&finish_second, cs_at(cs_domain[1]))?;
1857 let mut snap = [&sew_first, &sew_second]
1860 .into_iter()
1861 .flatten()
1862 .fold(0.0f64, |worst, sew| worst.max(sew.deviation));
1863 for (rim, vertex, row_end) in [
1864 (&start_first, w1a, cr_at(cr_domain[0]).or_refuse(KernelStage::Refine, "cr_at")?),
1865 (&finish_first, w1b, cr_at(cr_domain[1]).or_refuse(KernelStage::Refine, "cr_at")?),
1866 (&start_second, w2a, cs_at(cs_domain[0]).or_refuse(KernelStage::Refine, "cs_at")?),
1867 (&finish_second, w2b, cs_at(cs_domain[1]).or_refuse(KernelStage::Refine, "cs_at")?),
1868 ] {
1869 if !rim.is_consumed() {
1870 continue;
1871 }
1872 let point = result
1873 .vertices
1874 .iter()
1875 .find(|candidate| candidate.id == vertex)
1876 .ok_or(KernelRefusal::internal(KernelStage::Sew, "rim_vertex", "blend: consumed rim vertex missing"))?
1877 .point;
1878 snap = snap.max(row_end.sub(point).length());
1879 }
1880
1881 let cr_edge_id = match (&sew_first, &cr_rail) {
1886 (Some(sew), _) => Some(sew.edge_id),
1887 (None, None) => None,
1888 (None, Some((cr, _))) => {
1889 let id = take_id();
1890 result.edges.push(EdgeRecord {
1891 id,
1892 curve: cr.clone(),
1893 t0: cr_domain[0],
1894 t1: cr_domain[1],
1895 start_vertex_id: w1a,
1896 end_vertex_id: w1b,
1897 degenerate: false,
1898 name: None,
1899 });
1900 Some(id)
1901 }
1902 };
1903 let cs_edge_id = match (&sew_second, &cs_rail) {
1904 (Some(sew), _) => Some(sew.edge_id),
1905 (None, None) => None,
1906 (None, Some((cs, _))) => {
1907 let id = take_id();
1908 result.edges.push(EdgeRecord {
1909 id,
1910 curve: cs.clone(),
1911 t0: cs_domain[0],
1912 t1: cs_domain[1],
1913 start_vertex_id: w2a,
1914 end_vertex_id: w2b,
1915 degenerate: false,
1916 name: None,
1917 });
1918 Some(id)
1919 }
1920 };
1921 let mut commit_end_edge = |plan: &EndPlan,
1926 first_rim: u64,
1927 second_rim: u64|
1928 -> Result<Option<u64>, KernelRefusal> {
1929 match plan {
1930 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
1931 EndPlan::Free(free) => {
1932 let id = take_id();
1933 let domain = free.transverse_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1934 result.edges.push(EdgeRecord {
1935 id,
1936 curve: free.transverse_curve.clone(),
1937 t0: domain[0],
1938 t1: domain[1],
1939 start_vertex_id: first_rim,
1940 end_vertex_id: second_rim,
1941 degenerate: false,
1942 name: None,
1943 });
1944 Ok(Some(id))
1945 }
1946 }
1947 };
1948 let transverse_a_id = commit_end_edge(&start_end, w1a, w2a)?;
1949 let transverse_b_id = commit_end_edge(&finish_end, w1b, w2b)?;
1950
1951 for (mate, rail, sewn) in [
1958 (first, cr_edge_id.zip(cr_rail.as_ref()), sew_first.is_some()),
1959 (second, cs_edge_id.zip(cs_rail.as_ref()), sew_second.is_some()),
1960 ] {
1961 if sewn {
1962 continue;
1963 }
1964 let face = result
1965 .shells
1966 .iter_mut()
1967 .flat_map(|shell| &mut shell.faces)
1968 .find(|face| face.id == mate.face.id)
1969 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1970 let loop_record = &mut face.loops[mate.loop_index];
1971 let position = loop_record
1972 .coedges
1973 .iter()
1974 .position(|coedge| coedge.edge_id == edge.id)
1975 .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
1976 let Some((new_edge_id, (_, pcurve_forward))) = rail else {
1977 loop_record.coedges.remove(position);
1978 continue;
1979 };
1980 let old_forward = loop_record.coedges[position].forward;
1981 loop_record.coedges[position] = CoedgeRecord {
1982 id: loop_record.coedges[position].id,
1983 edge_id: new_edge_id,
1984 forward: old_forward,
1985 pcurve: if old_forward {
1986 pcurve_forward.clone()
1987 } else {
1988 pcurve_forward.reversed().or_refuse(KernelStage::Refine, "reversed")?
1989 },
1990 };
1991 }
1992
1993 for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
1998 let EndPlan::Cap(cap) = plan else {
1999 continue;
2000 };
2001 for (mate, rail_edge_id, leg, rim) in [
2002 (first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
2003 (second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
2004 ] {
2005 let rail_edge_id = rail_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "end_rail", "blend: a capped end lost its rail"))?;
2007 let face = result
2008 .shells
2009 .iter_mut()
2010 .flat_map(|shell| &mut shell.faces)
2011 .find(|face| face.id == mate.face.id)
2012 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during cap splice"))?;
2013 let loop_record = &mut face.loops[mate.loop_index];
2014 let count = loop_record.coedges.len();
2015 let rail_at = loop_record
2016 .coedges
2017 .iter()
2018 .position(|coedge| coedge.edge_id == rail_edge_id)
2019 .ok_or(KernelRefusal::internal(KernelStage::Sew, "rail_coedge", "blend: rail coedge lost during cap splice"))?;
2020 let rail_forward = loop_record.coedges[rail_at].forward;
2026 let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
2027 let (insert_at, forward, pcurve) = if end_is_traversal_end {
2028 (rail_at + 1, true, leg.1.clone())
2030 } else {
2031 (rail_at, false, leg.1.reversed().or_refuse(KernelStage::Refine, "reversed")?)
2033 };
2034 let _ = (count, rim);
2035 loop_record.coedges.insert(
2036 insert_at,
2037 CoedgeRecord {
2038 id: take_id(),
2039 edge_id: leg.0,
2040 forward,
2041 pcurve,
2042 },
2043 );
2044 }
2045 }
2046
2047 let mut consumed_edges: Vec<u64> = Vec::new();
2053 let start_free = start_end.free();
2054 let finish_free = finish_end.free();
2055 for (mate, sewn, sides) in [
2056 (
2057 first,
2058 sew_first.is_some(),
2059 [
2060 (&start_first, start_free.map(|free| free.first_edge_id)),
2061 (&finish_first, finish_free.map(|free| free.first_edge_id)),
2062 ],
2063 ),
2064 (
2065 second,
2066 sew_second.is_some(),
2067 [
2068 (&start_second, start_free.map(|free| free.second_edge_id)),
2069 (&finish_second, finish_free.map(|free| free.second_edge_id)),
2070 ],
2071 ),
2072 ] {
2073 for (cross, boundary_id) in sides {
2074 if !cross.is_consumed() {
2075 continue;
2076 }
2077 let Some(boundary_id) = boundary_id else {
2078 continue;
2079 };
2080 if !sewn {
2081 let face = result
2082 .shells
2083 .iter_mut()
2084 .flat_map(|shell| &mut shell.faces)
2085 .find(|face| face.id == mate.face.id)
2086 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
2087 face.loops[mate.loop_index]
2088 .coedges
2089 .retain(|coedge| coedge.edge_id != boundary_id);
2090 }
2091 if !consumed_edges.contains(&boundary_id) {
2092 consumed_edges.push(boundary_id);
2093 }
2094 }
2095 }
2096 for sew in [&sew_first, &sew_second].into_iter().flatten() {
2098 for id in &sew.collapsed_edges {
2099 if !consumed_edges.contains(id) {
2100 consumed_edges.push(*id);
2101 }
2102 }
2103 }
2104
2105 let mut end_plans = Vec::with_capacity(2);
2114 for (end, transverse_id, corner, first_consumed, second_consumed) in [
2115 (
2116 start_end.free(),
2117 transverse_a_id,
2118 old_start,
2119 start_first.is_consumed(),
2120 start_second.is_consumed(),
2121 ),
2122 (
2123 finish_end.free(),
2124 transverse_b_id,
2125 old_finish,
2126 finish_first.is_consumed(),
2127 finish_second.is_consumed(),
2128 ),
2129 ] {
2130 let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
2131 continue;
2132 };
2133 let face = result
2134 .shells
2135 .iter()
2136 .flat_map(|shell| &shell.faces)
2137 .find(|face| face.id == end.end_face_id)
2138 .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_face", "blend: end face lost during surgery"))?;
2139 let Some(located) = locate_end_corner(
2142 &result,
2143 face,
2144 corner,
2145 end.first_edge_id,
2146 end.second_edge_id,
2147 ) else {
2148 return Err(KernelRefusal::internal(KernelStage::Sew, "end_face_corner", "blend: end-face corner (adjacent boundary coedges) not found"));
2149 };
2150 for pole in &located.pole_edge_ids {
2153 if !consumed_edges.contains(pole) {
2154 consumed_edges.push(*pole);
2155 }
2156 }
2157 end_plans.push((
2158 end.end_face_id,
2159 transverse_id,
2160 end.transverse_end_pcurve.clone(),
2161 located,
2162 first_consumed,
2163 second_consumed,
2164 ));
2165 }
2166
2167 for (rim_resolution, boundary, corner, rim) in [
2170 (
2171 &start_first,
2172 start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
2173 old_start,
2174 w1a,
2175 ),
2176 (
2177 &start_second,
2178 start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
2179 old_start,
2180 w2a,
2181 ),
2182 (
2183 &finish_first,
2184 finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
2185 old_finish,
2186 w1b,
2187 ),
2188 (
2189 &finish_second,
2190 finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
2191 old_finish,
2192 w2b,
2193 ),
2194 ] {
2195 if !rim_resolution.trims_boundary() {
2196 continue;
2197 }
2198 let Some((boundary_id, boundary_param)) = boundary else {
2199 continue;
2200 };
2201 trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
2202 }
2203
2204 for (end_face, transverse_id, transverse_end_pcurve, located, first_consumed, second_consumed) in
2207 end_plans
2208 {
2209 let forward = located.x_is_first;
2210 let pcurve = if forward {
2211 transverse_end_pcurve
2212 } else {
2213 transverse_end_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
2214 };
2215 let (x_consumed, y_consumed) = if located.x_is_first {
2216 (first_consumed, second_consumed)
2217 } else {
2218 (second_consumed, first_consumed)
2219 };
2220 let transverse_coedge = CoedgeRecord {
2221 id: take_id(),
2222 edge_id: transverse_id,
2223 forward,
2224 pcurve,
2225 };
2226 splice_end_corner(
2227 result,
2228 end_face,
2229 &located,
2230 transverse_coedge,
2231 x_consumed,
2232 y_consumed,
2233 )?;
2234 }
2235
2236 let (mid_u, station_uv) = match (&cr_rail, &cs_rail) {
2241 (Some((_, cr_pcurve)), _) => {
2242 let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
2243 (mid_u, cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2244 }
2245 (None, Some(_)) => {
2246 let mid_u = (cs_domain[0] + cs_domain[1]) * 0.5;
2247 (mid_u, rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2248 }
2249 (None, None) => (cr_domain[0], rows.cr_pcurve.evaluate(cr_domain[0]).or_refuse(KernelStage::Refine, "evaluate")?),
2250 };
2251 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
2252 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
2253 let out1 = if first.face.same_sense {
2254 n1
2255 } else {
2256 n1.scale(-1.0)
2257 };
2258 let same_sense = blend_normal.dot(out1) >= 0.0;
2259 let loop_id = take_id();
2260 let cr_forward = sew_first
2268 .as_ref()
2269 .map_or(blend_cr_forward, |sew| sew.forward);
2270 let cs_forward = sew_second
2271 .as_ref()
2272 .map_or(!blend_cr_forward, |sew| sew.forward);
2273 let mut end_slot = |plan: &EndPlan,
2283 free_edge_id: Option<u64>,
2284 natural_forward: bool|
2285 -> Result<Vec<CoedgeRecord>, KernelRefusal> {
2286 match plan {
2287 EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
2288 id: take_id(),
2289 edge_id: end.arc_edge_id,
2290 forward: true,
2291 pcurve: end.arc_blend_pcurve.clone(),
2292 }]),
2293 EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
2294 id: take_id(),
2295 edge_id: end.arc_edge_id,
2296 forward: true,
2297 pcurve: end.arc_blend_pcurve.clone(),
2298 }]),
2299 EndPlan::Miter(end) => end
2300 .edges
2301 .iter()
2302 .map(|(edge_id, forward, pcurve)| {
2303 Ok(CoedgeRecord {
2304 id: take_id(),
2305 edge_id: *edge_id,
2306 forward: *forward,
2307 pcurve: pcurve.clone(),
2308 })
2309 })
2310 .collect(),
2311 EndPlan::Free(free) => {
2312 let edge_id =
2313 free_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "transverse_edge", "blend: free end without its transverse edge"))?;
2314 Ok(vec![if natural_forward {
2315 CoedgeRecord {
2316 id: take_id(),
2317 edge_id,
2318 forward: true,
2319 pcurve: free.transverse_blend_pcurve.clone(),
2320 }
2321 } else {
2322 CoedgeRecord {
2323 id: take_id(),
2324 edge_id,
2325 forward: false,
2326 pcurve: free.transverse_blend_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?,
2327 }
2328 }])
2329 }
2330 }
2331 };
2332 let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
2333 let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
2334 let coedges = if blend_cr_forward {
2337 let mut coedges = Vec::new();
2338 if let Some(cr_edge_id) = cr_edge_id {
2339 coedges.push(CoedgeRecord {
2340 id: take_id(),
2341 edge_id: cr_edge_id,
2342 forward: cr_forward,
2343 pcurve: crate::sweep_topology::parameter_line(
2344 cr_domain[0],
2345 0.0,
2346 cr_domain[1],
2347 0.0,
2348 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2349 });
2350 }
2351 coedges.extend(finish_slot);
2352 if let Some(cs_edge_id) = cs_edge_id {
2353 coedges.push(CoedgeRecord {
2354 id: take_id(),
2355 edge_id: cs_edge_id,
2356 forward: cs_forward,
2357 pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
2358 });
2359 }
2360 coedges.extend(start_slot);
2361 coedges
2362 } else {
2363 let mut coedges = Vec::new();
2364 if let Some(cr_edge_id) = cr_edge_id {
2365 coedges.push(CoedgeRecord {
2366 id: take_id(),
2367 edge_id: cr_edge_id,
2368 forward: cr_forward,
2369 pcurve: crate::sweep_topology::parameter_line(
2370 cr_domain[1],
2371 0.0,
2372 cr_domain[0],
2373 0.0,
2374 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2375 });
2376 }
2377 coedges.extend(start_slot);
2378 if let Some(cs_edge_id) = cs_edge_id {
2379 coedges.push(CoedgeRecord {
2380 id: take_id(),
2381 edge_id: cs_edge_id,
2382 forward: cs_forward,
2383 pcurve: crate::sweep_topology::parameter_line(
2384 cs_domain[0],
2385 1.0,
2386 cs_domain[1],
2387 1.0,
2388 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2389 });
2390 }
2391 coedges.extend(finish_slot);
2392 coedges
2393 };
2394 let blend_face_id = take_id();
2395 let mut blend_face = FaceRecord {
2396 id: blend_face_id,
2397 surface: rows.surface,
2398 same_sense,
2399 loops: vec![LoopRecord {
2400 id: loop_id,
2401 coedges,
2402 }],
2403 name: name.map(|value| value.to_string()),
2404 };
2405 if rows.exact_extrusion {
2406 transpose_face(&mut blend_face)?;
2407 }
2408 let shell_index = result
2409 .shells
2410 .iter()
2411 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
2412 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
2413 for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
2418 if sew.is_some() {
2419 for shell in &mut result.shells {
2420 shell.faces.retain(|face| face.id != mate.face.id);
2421 }
2422 }
2423 }
2424 result.shells[shell_index].faces.push(blend_face);
2425
2426 result.edges.retain(|candidate| candidate.id != edge.id);
2427 result
2430 .edges
2431 .retain(|candidate| !consumed_edges.contains(&candidate.id));
2432 let mut identified = Vec::new();
2434 for (collapsed, from, to) in [(cr_collapsed, w1a, w1b), (cs_collapsed, w2a, w2b)] {
2435 if collapsed && from != to {
2436 identified.push((from, to));
2437 }
2438 }
2439 let mut cr_edge_id = cr_edge_id;
2443 let mut cs_edge_id = cs_edge_id;
2444 if tail.iter().any(Option::is_some) {
2445 let EndPlan::Cap(cap) = &finish_end else {
2446 return Err(KernelRefusal::unsupported(KernelStage::Sew, "stopped_end_rollover", "blend stopped-end rollover: the rolled stripe's finish end is not a cap, which is the only end this lane closes on the neighbours"));
2447 };
2448 let mut footprint = StoppedEndFootprint::default();
2449 {
2452 let sharp = result.vertices.iter().find(|vertex| vertex.id == cap.sharp_vertex).map(|vertex| vertex.point)
2453 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, "stopped_end_rollover", "blend stopped-end rollover: the sharp vertex vanished"))?;
2454 let d = edge.curve.derivatives(edge.t1, 1).or_refuse(KernelStage::Refine, "derivatives")?;
2455 footprint.plane = (sharp, d[1].normalized().or_refuse(KernelStage::Refine, "normalized")?);
2456 footprint.sharp_vertex = cap.sharp_vertex;
2457 }
2458 for (side, splice) in tail.into_iter().enumerate() {
2459 let Some(splice) = splice else { continue };
2460 let (mate, rail_edge_id, rim, sewn) = if side == 0 {
2461 (first, cr_edge_id, w1b, sew_first.is_some())
2462 } else {
2463 (second, cs_edge_id, w2b, sew_second.is_some())
2464 };
2465 let Some(rail_edge_id) = rail_edge_id.filter(|_| !sewn) else {
2466 return Err(KernelRefusal::unsupported(KernelStage::Sew, "stopped_end_rollover", "blend stopped-end rollover: a rolled side's rail collapsed or sewed onto an existing edge, which this lane does not split"));
2467 };
2468 let part_on_mate = roll_side_footprint(result, take_id, mate, &splice, rail_edge_id, rim, cap.sharp_vertex, &mut footprint)?;
2469 if side == 0 { cr_edge_id = Some(part_on_mate); } else { cs_edge_id = Some(part_on_mate); }
2470 }
2471 retrace_stopped_end_footprint(result, take_id, footprint)?;
2472 }
2473 Ok(SewnStripe {
2474 cr_edge_id,
2475 cs_edge_id,
2476 blend_face_id,
2477 consumed: [sew_first.is_some(), sew_second.is_some()],
2478 snap,
2479 identified,
2480 })
2481}
2482
2483#[derive(Default)]
2486struct StoppedEndFootprint {
2487 pieces: Vec<(u64, u64, u64, u64, NurbsCurve)>,
2490 faces: Vec<u64>,
2492 plane: (Vec3, Vec3),
2495 sharp_vertex: u64,
2496}
2497
2498#[allow(clippy::too_many_arguments)]
2506fn roll_side_footprint(
2507 result: &mut BrepSolid,
2508 take_id: &mut dyn FnMut() -> u64,
2509 mate: &BlendMate,
2510 splice: &crate::blend::rollover::TailSplice<'_>,
2511 rail_edge_id: u64,
2512 rim_vertex: u64,
2513 sharp_vertex: u64,
2514 footprint: &mut StoppedEndFootprint,
2515) -> Result<u64, KernelRefusal> {
2516 let what = "stopped_end_rollover";
2517 let not_built = |message: String| KernelRefusal::unsupported(KernelStage::Sew, what, format!("blend stopped-end rollover: {message}"));
2518 let _ = sharp_vertex;
2519 let rolled = splice.rolled;
2520 let crossing_vertex = take_id();
2521 result.vertices.push(VertexRecord { id: crossing_vertex, point: rolled.point });
2522 let rail_parts = crate::blend::rollover_surgery::split_edge_at_vertices(result, rail_edge_id, &[(crossing_vertex, rolled.point)], take_id)?;
2523 let [rail_near, rail_far] = [rail_parts[0], rail_parts[1]];
2524 let rail_far_edge = result.edges.iter().find(|edge| edge.id == rail_far).cloned()
2525 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the rail's far part vanished"))?;
2526 if rail_far_edge.end_vertex_id != rim_vertex {
2527 return Err(not_built(format!("the rail's far part ends at vertex {} rather than the finish rim {rim_vertex}", rail_far_edge.end_vertex_id)));
2528 }
2529 let holds_crossing = |result: &BrepSolid, id: u64| -> Result<bool, KernelRefusal> {
2532 let Some(edge) = result.edges.iter().find(|edge| edge.id == id) else { return Ok(false) };
2533 let foot = crate::project_point_to_curve(&edge.curve, rolled.point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
2534 Ok(foot.distance <= 1e-6 && foot.u > edge.t0.min(edge.t1) && foot.u < edge.t0.max(edge.t1))
2535 };
2536 let crossed = if result.edges.iter().any(|edge| edge.id == rolled.edge_id) {
2537 rolled.edge_id
2538 } else {
2539 let candidates: Vec<u64> = result.edges.iter().map(|edge| edge.id).collect();
2540 let mut found = None;
2541 for id in candidates {
2542 if holds_crossing(result, id)? {
2543 found = Some(id);
2544 break;
2545 }
2546 }
2547 found.ok_or_else(|| not_built(format!("no part of tangent edge {} holds the crossing any more", rolled.edge_id)))?
2548 };
2549 crate::blend::rollover_surgery::split_edge_at_vertices(result, crossed, &[(crossing_vertex, rolled.point)], take_id)?;
2550 let neighbour_surface = result
2552 .shells
2553 .iter()
2554 .flat_map(|shell| &shell.faces)
2555 .find(|face| face.id == rolled.neighbour_face_id)
2556 .map(|face| face.surface.clone())
2557 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the neighbour face vanished"))?;
2558 let far_pcurve = {
2559 const SAMPLES: usize = 24;
2560 let [t0, t1] = [rail_far_edge.t0, rail_far_edge.t1];
2561 let mut uvs = Vec::with_capacity(SAMPLES + 1);
2562 let mut ts = Vec::with_capacity(SAMPLES + 1);
2563 for k in 0..=SAMPLES {
2564 let t = t0 + (t1 - t0) * k as f64 / SAMPLES as f64;
2565 let point = rail_far_edge.curve.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
2566 let projection = crate::project_point_to_surface(&neighbour_surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
2567 uvs.push(Vec3::new(projection.u, projection.v, 0.0));
2568 ts.push(t);
2569 }
2570 crate::fit::interpolate_curve(&uvs, 3, &ts).or_refuse(KernelStage::Refine, "interpolate_curve")?
2572 };
2573 let leg_edge_id = splice.legs.first().map(|piece| piece.edge_id)
2575 .ok_or_else(|| not_built("a rolled side has no cap leg".to_string()))?;
2576 let mate_face = result
2577 .shells
2578 .iter_mut()
2579 .flat_map(|shell| shell.faces.iter_mut())
2580 .find(|face| face.id == mate.face.id)
2581 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the mate face vanished"))?;
2582 mate_face.loops[mate.loop_index].coedges.retain(|coedge| coedge.edge_id != rail_far && coedge.edge_id != leg_edge_id);
2583 footprint.pieces.push((rolled.neighbour_face_id, rail_far, crossing_vertex, rim_vertex, far_pcurve));
2584 for piece in &splice.legs {
2585 footprint.pieces.push((piece.face_id, piece.edge_id, piece.from, piece.to, piece.pcurve.clone()));
2586 }
2587 for face_id in [mate.face.id, rolled.neighbour_face_id].into_iter().chain(splice.legs.iter().map(|piece| piece.face_id)) {
2588 if !footprint.faces.contains(&face_id) {
2589 footprint.faces.push(face_id);
2590 }
2591 }
2592 Ok(rail_near)
2593}
2594
2595fn retrace_stopped_end_footprint(
2605 result: &mut BrepSolid,
2606 take_id: &mut dyn FnMut() -> u64,
2607 footprint: StoppedEndFootprint,
2608) -> Result<(), KernelRefusal> {
2609 let what = "stopped_end_rollover";
2610 let not_built = |message: String| KernelRefusal::unsupported(KernelStage::Sew, what, format!("blend stopped-end rollover: {message}"));
2611 let (sharp_point, tangent) = footprint.plane;
2612 let mut boundary_vertices: Vec<u64> = vec![footprint.sharp_vertex];
2613 for (_, _, from, to, _) in &footprint.pieces {
2614 for vertex in [*from, *to] {
2615 if !boundary_vertices.contains(&vertex) {
2616 boundary_vertices.push(vertex);
2617 }
2618 }
2619 }
2620 let piece_edges: Vec<u64> = footprint.pieces.iter().map(|piece| piece.1).collect();
2621 let mut interior: Vec<u64> = Vec::new();
2623 for face in result.shells.iter().flat_map(|shell| &shell.faces).filter(|face| footprint.faces.contains(&face.id)) {
2624 for coedge in face.loops.iter().flat_map(|lp| &lp.coedges) {
2625 let Some(edge) = result.edges.iter().find(|edge| edge.id == coedge.edge_id) else { continue };
2626 if piece_edges.contains(&edge.id) || interior.contains(&edge.id) {
2627 continue;
2628 }
2629 if !(boundary_vertices.contains(&edge.start_vertex_id) && boundary_vertices.contains(&edge.end_vertex_id)) {
2630 continue;
2631 }
2632 let mid = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1)).or_refuse(KernelStage::Refine, "evaluate")?;
2633 if mid.sub(sharp_point).dot(tangent) < 0.0 {
2634 interior.push(edge.id);
2635 }
2636 }
2637 }
2638 if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
2639 eprintln!("stopped-end rollover: footprint faces {:?}, interior edges {:?}, boundary vertices {:?}", footprint.faces, interior, boundary_vertices);
2640 }
2641 let edge_ends: std::collections::HashMap<u64, (u64, u64)> = result.edges.iter().map(|edge| (edge.id, (edge.start_vertex_id, edge.end_vertex_id))).collect();
2642 let mut new_faces: Vec<FaceRecord> = Vec::new();
2643 let mut taken: Vec<String> = result.shells.iter().flat_map(|shell| &shell.faces).filter_map(|face| face.name.clone()).collect();
2646 for face_id in footprint.faces.clone() {
2647 let face = result
2648 .shells
2649 .iter_mut()
2650 .flat_map(|shell| shell.faces.iter_mut())
2651 .find(|face| face.id == face_id)
2652 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: face {face_id} vanished")))?;
2653 let pieces: Vec<&(u64, u64, u64, u64, NurbsCurve)> = footprint.pieces.iter().filter(|piece| piece.0 == face_id).collect();
2654 let mut rebuilt_loops: Vec<LoopRecord> = Vec::new();
2655 for loop_record in &face.loops {
2656 let touched = loop_record.coedges.iter().any(|coedge| interior.contains(&coedge.edge_id))
2657 || loop_record.coedges.iter().any(|coedge| {
2658 edge_ends.get(&coedge.edge_id).is_some_and(|(a, b)| boundary_vertices.contains(a) || boundary_vertices.contains(b))
2659 });
2660 if !touched {
2661 rebuilt_loops.push(loop_record.clone());
2662 continue;
2663 }
2664 let n = loop_record.coedges.len();
2667 let mut remaining: Vec<(CoedgeRecord, u64, u64, usize)> = Vec::new();
2668 for (index, coedge) in loop_record.coedges.iter().enumerate() {
2669 if interior.contains(&coedge.edge_id) {
2670 continue;
2671 }
2672 let (start, end) = edge_ends.get(&coedge.edge_id).copied()
2673 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: coedge {} names a missing edge {}", coedge.id, coedge.edge_id)))?;
2674 let (from, to) = if coedge.forward { (start, end) } else { (end, start) };
2675 remaining.push((coedge.clone(), from, to, index));
2676 }
2677 let mut loose: Vec<&(u64, u64, u64, u64, NurbsCurve)> = pieces.clone();
2678 while !remaining.is_empty() {
2683 let start_at = remaining.iter().position(|(_, from, to, _)| from != to).unwrap_or(0);
2686 let (first, from0, to0, index0) = remaining.remove(start_at);
2687 let mut walk: Vec<CoedgeRecord> = vec![first];
2688 let mut at = to0;
2689 let mut previous = index0;
2690 let mut guard = 0usize;
2691 loop {
2692 guard += 1;
2693 if guard > 4096 {
2694 return Err(not_built(format!("face {face_id}: the re-trace does not close")));
2695 }
2696 let mut next_remaining: Vec<usize> = remaining.iter().enumerate().filter(|(_, (_, from, _, _))| *from == at).map(|(k, _)| k).collect();
2697 let successor_is_start = (previous + 1) % n == index0;
2698 if at == from0 && (successor_is_start || next_remaining.is_empty()) {
2699 if at == from0 && successor_is_start {
2702 break;
2703 }
2704 if loose.iter().all(|piece| piece.2 != at && piece.3 != at) {
2705 break;
2706 }
2707 }
2708 if next_remaining.len() > 1 {
2709 if let Some(k) = next_remaining.iter().copied().find(|k| remaining[*k].3 == (previous + 1) % n) {
2710 next_remaining = vec![k];
2711 }
2712 }
2713 let next_pieces: Vec<usize> = loose.iter().enumerate().filter(|(_, piece)| piece.2 == at || piece.3 == at).map(|(k, _)| k).collect();
2714 match (next_remaining.len(), next_pieces.len()) {
2715 (1, 0) => {
2716 let (coedge, _, to, index) = remaining.remove(next_remaining[0]);
2717 walk.push(coedge);
2718 at = to;
2719 previous = index;
2720 }
2721 (0, 1) => {
2722 let piece = loose.remove(next_pieces[0]);
2723 let (edge_id, from, to, pcurve) = (piece.1, piece.2, piece.3, &piece.4);
2724 let (start, _) = edge_ends.get(&edge_id).copied()
2725 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: footprint edge {edge_id} missing")))?;
2726 let along = from == at;
2727 let coedge_forward = if along { start == from } else { start == to };
2728 walk.push(CoedgeRecord {
2729 id: take_id(),
2730 edge_id,
2731 forward: coedge_forward,
2732 pcurve: if along { pcurve.clone() } else { pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")? },
2733 });
2734 at = if along { to } else { from };
2735 }
2736 (r, p) => {
2737 return Err(not_built(format!("face {face_id}: vertex {at} has {} ways on ({r} boundary, {p} new), not one", r + p)));
2738 }
2739 }
2740 }
2741 rebuilt_loops.push(LoopRecord { id: if rebuilt_loops.is_empty() { loop_record.id } else { take_id() }, coedges: walk });
2742 }
2743 if !loose.is_empty() {
2744 return Err(not_built(format!("face {face_id}: {} new boundary piece(s) joined no loop", loose.len())));
2745 }
2746 }
2747 let kept = rebuilt_loops.remove(0);
2750 face.loops = vec![kept];
2751 let surface = face.surface.clone();
2752 let same_sense = face.same_sense;
2753 let base_name = face.name.clone();
2754 for extra in rebuilt_loops {
2755 let name = base_name.as_ref().map(|base| {
2756 (1..).map(|k| format!("{base}_{k}")).find(|candidate| !taken.contains(candidate)).expect("a free name")
2757 });
2758 if let Some(name) = &name {
2759 taken.push(name.clone());
2760 }
2761 new_faces.push(FaceRecord { id: take_id(), surface: surface.clone(), same_sense, loops: vec![extra], name });
2762 }
2763 }
2764 if !new_faces.is_empty() {
2765 let shell_index = result.shells.iter().position(|shell| !shell.faces.is_empty())
2766 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: no shell"))?;
2767 result.shells[shell_index].faces.extend(new_faces);
2768 }
2769 result.edges.retain(|edge| !interior.contains(&edge.id));
2770 Ok(())
2771}
2772
2773pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), KernelRefusal> {
2779 let surface = &face.surface;
2780 let rows_u = surface.control_points.len();
2781 let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
2782 let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
2783 for row in &surface.control_points {
2784 for (j, point) in row.iter().enumerate() {
2785 transposed[j].push(*point);
2786 }
2787 }
2788 face.surface = NurbsSurface::new(
2789 surface.degree_v,
2790 surface.degree_u,
2791 surface.knots_v.clone(),
2792 surface.knots_u.clone(),
2793 transposed,
2794 ).or_refuse(KernelStage::Refine, "new")?;
2795 for loop_record in &mut face.loops {
2796 for coedge in &mut loop_record.coedges {
2797 for control in &mut coedge.pcurve.control_points {
2798 std::mem::swap(&mut control.x, &mut control.y);
2799 }
2800 }
2801 }
2802 face.same_sense = !face.same_sense;
2803 Ok(())
2804}
2805
2806pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
2811 let used: rustc_hash::FxHashSet<u64> = result
2812 .edges
2813 .iter()
2814 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
2815 .collect();
2816 result
2817 .vertices
2818 .retain(|candidate| used.contains(&candidate.id));
2819}
2820
2821fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
2822 crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")
2823}
2824
2825
2826