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_split = |plan: &EndPlan, second_rim: u64, corner: u64| -> Result<Option<super::split_floor::SplitCommit>, KernelRefusal> {
1926 match plan.free().and_then(|free| free.split.as_ref()) {
1927 Some(split) => super::split_floor::commit_split_end(result, &mut *take_id, split, second_rim, corner).map(Some),
1928 None => Ok(None),
1929 }
1930 };
1931 let start_split = commit_split(&start_end, w2a, old_start)?;
1932 let finish_split = commit_split(&finish_end, w2b, old_finish)?;
1933 let mut commit_end_edge = |plan: &EndPlan,
1934 first_rim: u64,
1935 second_rim: u64|
1936 -> Result<Option<u64>, KernelRefusal> {
1937 match plan {
1938 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
1939 EndPlan::Free(free) => {
1940 let id = take_id();
1941 let domain = free.transverse_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1942 result.edges.push(EdgeRecord {
1943 id,
1944 curve: free.transverse_curve.clone(),
1945 t0: domain[0],
1946 t1: domain[1],
1947 start_vertex_id: first_rim,
1948 end_vertex_id: second_rim,
1949 degenerate: false,
1950 name: None,
1951 });
1952 Ok(Some(id))
1953 }
1954 }
1955 };
1956 let transverse_a_id = commit_end_edge(&start_end, w1a, start_split.as_ref().map_or(w2a, |split| split.q1_vertex))?;
1957 let transverse_b_id = commit_end_edge(&finish_end, w1b, finish_split.as_ref().map_or(w2b, |split| split.q1_vertex))?;
1958
1959 for (mate, rail, sewn) in [
1966 (first, cr_edge_id.zip(cr_rail.as_ref()), sew_first.is_some()),
1967 (second, cs_edge_id.zip(cs_rail.as_ref()), sew_second.is_some()),
1968 ] {
1969 if sewn {
1970 continue;
1971 }
1972 let face = result
1973 .shells
1974 .iter_mut()
1975 .flat_map(|shell| &mut shell.faces)
1976 .find(|face| face.id == mate.face.id)
1977 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1978 let loop_record = &mut face.loops[mate.loop_index];
1979 let position = loop_record
1980 .coedges
1981 .iter()
1982 .position(|coedge| coedge.edge_id == edge.id)
1983 .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
1984 let Some((new_edge_id, (_, pcurve_forward))) = rail else {
1985 loop_record.coedges.remove(position);
1986 continue;
1987 };
1988 let old_forward = loop_record.coedges[position].forward;
1989 loop_record.coedges[position] = CoedgeRecord {
1990 id: loop_record.coedges[position].id,
1991 edge_id: new_edge_id,
1992 forward: old_forward,
1993 pcurve: if old_forward {
1994 pcurve_forward.clone()
1995 } else {
1996 pcurve_forward.reversed().or_refuse(KernelStage::Refine, "reversed")?
1997 },
1998 };
1999 }
2000
2001 for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
2006 let EndPlan::Cap(cap) = plan else {
2007 continue;
2008 };
2009 for (mate, rail_edge_id, leg, rim) in [
2010 (first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
2011 (second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
2012 ] {
2013 let rail_edge_id = rail_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "end_rail", "blend: a capped end lost its rail"))?;
2015 let face = result
2016 .shells
2017 .iter_mut()
2018 .flat_map(|shell| &mut shell.faces)
2019 .find(|face| face.id == mate.face.id)
2020 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during cap splice"))?;
2021 let loop_record = &mut face.loops[mate.loop_index];
2022 let count = loop_record.coedges.len();
2023 let rail_at = loop_record
2024 .coedges
2025 .iter()
2026 .position(|coedge| coedge.edge_id == rail_edge_id)
2027 .ok_or(KernelRefusal::internal(KernelStage::Sew, "rail_coedge", "blend: rail coedge lost during cap splice"))?;
2028 let rail_forward = loop_record.coedges[rail_at].forward;
2034 let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
2035 let (insert_at, forward, pcurve) = if end_is_traversal_end {
2036 (rail_at + 1, true, leg.1.clone())
2038 } else {
2039 (rail_at, false, leg.1.reversed().or_refuse(KernelStage::Refine, "reversed")?)
2041 };
2042 let _ = (count, rim);
2043 loop_record.coedges.insert(
2044 insert_at,
2045 CoedgeRecord {
2046 id: take_id(),
2047 edge_id: leg.0,
2048 forward,
2049 pcurve,
2050 },
2051 );
2052 }
2053 }
2054
2055 let mut consumed_edges: Vec<u64> = Vec::new();
2061 let start_free = start_end.free();
2062 let finish_free = finish_end.free();
2063 for (mate, sewn, sides) in [
2064 (
2065 first,
2066 sew_first.is_some(),
2067 [
2068 (&start_first, start_free.map(|free| free.first_edge_id)),
2069 (&finish_first, finish_free.map(|free| free.first_edge_id)),
2070 ],
2071 ),
2072 (
2073 second,
2074 sew_second.is_some(),
2075 [
2076 (&start_second, start_free.map(|free| free.second_edge_id)),
2077 (&finish_second, finish_free.map(|free| free.second_edge_id)),
2078 ],
2079 ),
2080 ] {
2081 for (cross, boundary_id) in sides {
2082 if !cross.is_consumed() {
2083 continue;
2084 }
2085 let Some(boundary_id) = boundary_id else {
2086 continue;
2087 };
2088 if !sewn {
2089 let face = result
2090 .shells
2091 .iter_mut()
2092 .flat_map(|shell| &mut shell.faces)
2093 .find(|face| face.id == mate.face.id)
2094 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
2095 face.loops[mate.loop_index]
2096 .coedges
2097 .retain(|coedge| coedge.edge_id != boundary_id);
2098 }
2099 if !consumed_edges.contains(&boundary_id) {
2100 consumed_edges.push(boundary_id);
2101 }
2102 }
2103 }
2104 for sew in [&sew_first, &sew_second].into_iter().flatten() {
2106 for id in &sew.collapsed_edges {
2107 if !consumed_edges.contains(id) {
2108 consumed_edges.push(*id);
2109 }
2110 }
2111 }
2112
2113 let mut end_plans = Vec::with_capacity(2);
2122 for (end, transverse_id, corner, first_consumed, second_consumed) in [
2123 (
2124 start_end.free(),
2125 transverse_a_id,
2126 old_start,
2127 start_first.is_consumed(),
2128 start_second.is_consumed(),
2129 ),
2130 (
2131 finish_end.free(),
2132 transverse_b_id,
2133 old_finish,
2134 finish_first.is_consumed(),
2135 finish_second.is_consumed(),
2136 ),
2137 ] {
2138 let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
2139 continue;
2140 };
2141 if end.split.is_some() {
2142 continue;
2143 }
2144 let face = result
2145 .shells
2146 .iter()
2147 .flat_map(|shell| &shell.faces)
2148 .find(|face| face.id == end.end_face_id)
2149 .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_face", "blend: end face lost during surgery"))?;
2150 let Some(located) = locate_end_corner(
2153 &result,
2154 face,
2155 corner,
2156 end.first_edge_id,
2157 end.second_edge_id,
2158 ) else {
2159 return Err(KernelRefusal::internal(KernelStage::Sew, "end_face_corner", "blend: end-face corner (adjacent boundary coedges) not found"));
2160 };
2161 for pole in &located.pole_edge_ids {
2164 if !consumed_edges.contains(pole) {
2165 consumed_edges.push(*pole);
2166 }
2167 }
2168 end_plans.push((
2169 end.end_face_id,
2170 transverse_id,
2171 end.transverse_end_pcurve.clone(),
2172 located,
2173 first_consumed,
2174 second_consumed,
2175 ));
2176 }
2177
2178 let split_corners = |end: Option<&EndSurgery>, corner: u64, result: &BrepSolid| -> Result<Option<[super::support::EndCorner; 2]>, KernelRefusal> {
2179 match end.and_then(|end| end.split.as_ref().map(|split| (end, split))) {
2180 Some((end, split)) => super::split_floor::locate_split_corners(result, end, split, corner).map(Some),
2181 None => Ok(None),
2182 }
2183 };
2184 let start_split_corners = split_corners(start_free, old_start, result)?;
2185 let finish_split_corners = split_corners(finish_free, old_finish, result)?;
2186 if (start_split_corners.is_some() && (start_first.is_consumed() || start_second.is_consumed()))
2187 || (finish_split_corners.is_some() && (finish_first.is_consumed() || finish_second.is_consumed()))
2188 {
2189 return Err(KernelRefusal::unsupported(KernelStage::Sew, "split_floor_consumed", "blend: a two-face end whose rail consumes its boundary"));
2190 }
2191
2192 for (rim_resolution, boundary, corner, rim) in [
2195 (
2196 &start_first,
2197 start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
2198 old_start,
2199 w1a,
2200 ),
2201 (
2202 &start_second,
2203 start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
2204 old_start,
2205 w2a,
2206 ),
2207 (
2208 &finish_first,
2209 finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
2210 old_finish,
2211 w1b,
2212 ),
2213 (
2214 &finish_second,
2215 finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
2216 old_finish,
2217 w2b,
2218 ),
2219 ] {
2220 if !rim_resolution.trims_boundary() {
2221 continue;
2222 }
2223 let Some((boundary_id, boundary_param)) = boundary else {
2224 continue;
2225 };
2226 trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
2227 }
2228
2229 for (end_face, transverse_id, transverse_end_pcurve, located, first_consumed, second_consumed) in
2232 end_plans
2233 {
2234 let forward = located.x_is_first;
2235 let pcurve = if forward {
2236 transverse_end_pcurve
2237 } else {
2238 transverse_end_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
2239 };
2240 let (x_consumed, y_consumed) = if located.x_is_first {
2241 (first_consumed, second_consumed)
2242 } else {
2243 (second_consumed, first_consumed)
2244 };
2245 let transverse_coedge = CoedgeRecord {
2246 id: take_id(),
2247 edge_id: transverse_id,
2248 forward,
2249 pcurve,
2250 };
2251 splice_end_corner(
2252 result,
2253 end_face,
2254 &located,
2255 transverse_coedge,
2256 x_consumed,
2257 y_consumed,
2258 )?;
2259 }
2260
2261 let (mid_u, station_uv) = match (&cr_rail, &cs_rail) {
2266 (Some((_, cr_pcurve)), _) => {
2267 let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
2268 (mid_u, cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2269 }
2270 (None, Some(_)) => {
2271 let mid_u = (cs_domain[0] + cs_domain[1]) * 0.5;
2272 (mid_u, rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2273 }
2274 (None, None) => (cr_domain[0], rows.cr_pcurve.evaluate(cr_domain[0]).or_refuse(KernelStage::Refine, "evaluate")?),
2275 };
2276 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
2277 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
2278 let out1 = if first.face.same_sense {
2279 n1
2280 } else {
2281 n1.scale(-1.0)
2282 };
2283 let same_sense = blend_normal.dot(out1) >= 0.0;
2284 let loop_id = take_id();
2285 let cr_forward = sew_first
2293 .as_ref()
2294 .map_or(blend_cr_forward, |sew| sew.forward);
2295 let cs_forward = sew_second
2296 .as_ref()
2297 .map_or(!blend_cr_forward, |sew| sew.forward);
2298 let mut end_slot = |plan: &EndPlan,
2308 free_edge_id: Option<u64>,
2309 natural_forward: bool|
2310 -> Result<Vec<CoedgeRecord>, KernelRefusal> {
2311 match plan {
2312 EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
2313 id: take_id(),
2314 edge_id: end.arc_edge_id,
2315 forward: true,
2316 pcurve: end.arc_blend_pcurve.clone(),
2317 }]),
2318 EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
2319 id: take_id(),
2320 edge_id: end.arc_edge_id,
2321 forward: true,
2322 pcurve: end.arc_blend_pcurve.clone(),
2323 }]),
2324 EndPlan::Miter(end) => end
2325 .edges
2326 .iter()
2327 .map(|(edge_id, forward, pcurve)| {
2328 Ok(CoedgeRecord {
2329 id: take_id(),
2330 edge_id: *edge_id,
2331 forward: *forward,
2332 pcurve: pcurve.clone(),
2333 })
2334 })
2335 .collect(),
2336 EndPlan::Free(free) => {
2337 let edge_id =
2338 free_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "transverse_edge", "blend: free end without its transverse edge"))?;
2339 Ok(vec![if natural_forward {
2340 CoedgeRecord {
2341 id: take_id(),
2342 edge_id,
2343 forward: true,
2344 pcurve: free.transverse_blend_pcurve.clone(),
2345 }
2346 } else {
2347 CoedgeRecord {
2348 id: take_id(),
2349 edge_id,
2350 forward: false,
2351 pcurve: free.transverse_blend_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?,
2352 }
2353 }])
2354 }
2355 }
2356 };
2357 let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
2358 let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
2359 let start_slot = match (start_free, &start_split, transverse_a_id) {
2360 (Some(end), Some(commit), Some(id)) => super::split_floor::split_slot(end, end.split.as_ref().expect("planned"), commit, id, !blend_cr_forward, &mut *take_id)?,
2361 _ => start_slot,
2362 };
2363 let finish_slot = match (finish_free, &finish_split, transverse_b_id) {
2364 (Some(end), Some(commit), Some(id)) => super::split_floor::split_slot(end, end.split.as_ref().expect("planned"), commit, id, blend_cr_forward, &mut *take_id)?,
2365 _ => finish_slot,
2366 };
2367 for (end, commit, id, corners, corner, forward) in [
2368 (start_free, &start_split, transverse_a_id, &start_split_corners, old_start, !blend_cr_forward),
2369 (finish_free, &finish_split, transverse_b_id, &finish_split_corners, old_finish, blend_cr_forward),
2370 ] {
2371 if let (Some(end), Some(commit), Some(id), Some(corners)) = (end, commit, id, corners) {
2372 super::split_floor::finish_split_end(result, &mut *take_id, end, end.split.as_ref().expect("planned"), commit, id, corners, corner, forward, name)?;
2373 }
2374 }
2375 let coedges = if blend_cr_forward {
2378 let mut coedges = Vec::new();
2379 if let Some(cr_edge_id) = cr_edge_id {
2380 coedges.push(CoedgeRecord {
2381 id: take_id(),
2382 edge_id: cr_edge_id,
2383 forward: cr_forward,
2384 pcurve: crate::sweep_topology::parameter_line(
2385 cr_domain[0],
2386 0.0,
2387 cr_domain[1],
2388 0.0,
2389 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2390 });
2391 }
2392 coedges.extend(finish_slot);
2393 if let Some(cs_edge_id) = cs_edge_id {
2394 coedges.push(CoedgeRecord {
2395 id: take_id(),
2396 edge_id: cs_edge_id,
2397 forward: cs_forward,
2398 pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
2399 });
2400 }
2401 coedges.extend(start_slot);
2402 coedges
2403 } else {
2404 let mut coedges = Vec::new();
2405 if let Some(cr_edge_id) = cr_edge_id {
2406 coedges.push(CoedgeRecord {
2407 id: take_id(),
2408 edge_id: cr_edge_id,
2409 forward: cr_forward,
2410 pcurve: crate::sweep_topology::parameter_line(
2411 cr_domain[1],
2412 0.0,
2413 cr_domain[0],
2414 0.0,
2415 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2416 });
2417 }
2418 coedges.extend(start_slot);
2419 if let Some(cs_edge_id) = cs_edge_id {
2420 coedges.push(CoedgeRecord {
2421 id: take_id(),
2422 edge_id: cs_edge_id,
2423 forward: cs_forward,
2424 pcurve: crate::sweep_topology::parameter_line(
2425 cs_domain[0],
2426 1.0,
2427 cs_domain[1],
2428 1.0,
2429 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2430 });
2431 }
2432 coedges.extend(finish_slot);
2433 coedges
2434 };
2435 let blend_face_id = take_id();
2436 let mut blend_face = FaceRecord {
2437 id: blend_face_id,
2438 surface: rows.surface,
2439 same_sense,
2440 loops: vec![LoopRecord {
2441 id: loop_id,
2442 coedges,
2443 }],
2444 name: name.map(|value| value.to_string()),
2445 };
2446 if rows.exact_extrusion {
2447 transpose_face(&mut blend_face)?;
2448 }
2449 let shell_index = result
2450 .shells
2451 .iter()
2452 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
2453 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
2454 for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
2459 if sew.is_some() {
2460 for shell in &mut result.shells {
2461 shell.faces.retain(|face| face.id != mate.face.id);
2462 }
2463 }
2464 }
2465 result.shells[shell_index].faces.push(blend_face);
2466
2467 result.edges.retain(|candidate| candidate.id != edge.id);
2468 result
2471 .edges
2472 .retain(|candidate| !consumed_edges.contains(&candidate.id));
2473 let mut identified = Vec::new();
2475 for (collapsed, from, to) in [(cr_collapsed, w1a, w1b), (cs_collapsed, w2a, w2b)] {
2476 if collapsed && from != to {
2477 identified.push((from, to));
2478 }
2479 }
2480 let mut cr_edge_id = cr_edge_id;
2484 let mut cs_edge_id = cs_edge_id;
2485 if tail.iter().any(Option::is_some) {
2486 let EndPlan::Cap(cap) = &finish_end else {
2487 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"));
2488 };
2489 let mut footprint = StoppedEndFootprint::default();
2490 {
2493 let sharp = result.vertices.iter().find(|vertex| vertex.id == cap.sharp_vertex).map(|vertex| vertex.point)
2494 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, "stopped_end_rollover", "blend stopped-end rollover: the sharp vertex vanished"))?;
2495 let d = edge.curve.derivatives(edge.t1, 1).or_refuse(KernelStage::Refine, "derivatives")?;
2496 footprint.plane = (sharp, d[1].normalized().or_refuse(KernelStage::Refine, "normalized")?);
2497 footprint.sharp_vertex = cap.sharp_vertex;
2498 }
2499 for (side, splice) in tail.into_iter().enumerate() {
2500 let Some(splice) = splice else { continue };
2501 let (mate, rail_edge_id, rim, sewn) = if side == 0 {
2502 (first, cr_edge_id, w1b, sew_first.is_some())
2503 } else {
2504 (second, cs_edge_id, w2b, sew_second.is_some())
2505 };
2506 let Some(rail_edge_id) = rail_edge_id.filter(|_| !sewn) else {
2507 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"));
2508 };
2509 let part_on_mate = roll_side_footprint(result, take_id, mate, &splice, rail_edge_id, rim, cap.sharp_vertex, &mut footprint)?;
2510 if side == 0 { cr_edge_id = Some(part_on_mate); } else { cs_edge_id = Some(part_on_mate); }
2511 }
2512 retrace_stopped_end_footprint(result, take_id, footprint)?;
2513 }
2514 Ok(SewnStripe {
2515 cr_edge_id,
2516 cs_edge_id,
2517 blend_face_id,
2518 consumed: [sew_first.is_some(), sew_second.is_some()],
2519 snap,
2520 identified,
2521 })
2522}
2523
2524#[derive(Default)]
2527struct StoppedEndFootprint {
2528 pieces: Vec<(u64, u64, u64, u64, NurbsCurve)>,
2531 faces: Vec<u64>,
2533 plane: (Vec3, Vec3),
2536 sharp_vertex: u64,
2537}
2538
2539#[allow(clippy::too_many_arguments)]
2547fn roll_side_footprint(
2548 result: &mut BrepSolid,
2549 take_id: &mut dyn FnMut() -> u64,
2550 mate: &BlendMate,
2551 splice: &crate::blend::rollover::TailSplice<'_>,
2552 rail_edge_id: u64,
2553 rim_vertex: u64,
2554 sharp_vertex: u64,
2555 footprint: &mut StoppedEndFootprint,
2556) -> Result<u64, KernelRefusal> {
2557 let what = "stopped_end_rollover";
2558 let not_built = |message: String| KernelRefusal::unsupported(KernelStage::Sew, what, format!("blend stopped-end rollover: {message}"));
2559 let _ = sharp_vertex;
2560 let rolled = splice.rolled;
2561 let crossing_vertex = take_id();
2562 result.vertices.push(VertexRecord { id: crossing_vertex, point: rolled.point });
2563 let rail_parts = crate::blend::rollover_surgery::split_edge_at_vertices(result, rail_edge_id, &[(crossing_vertex, rolled.point)], take_id)?;
2564 let [rail_near, rail_far] = [rail_parts[0], rail_parts[1]];
2565 let rail_far_edge = result.edges.iter().find(|edge| edge.id == rail_far).cloned()
2566 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the rail's far part vanished"))?;
2567 if rail_far_edge.end_vertex_id != rim_vertex {
2568 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)));
2569 }
2570 let holds_crossing = |result: &BrepSolid, id: u64| -> Result<bool, KernelRefusal> {
2573 let Some(edge) = result.edges.iter().find(|edge| edge.id == id) else { return Ok(false) };
2574 let foot = crate::project_point_to_curve(&edge.curve, rolled.point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
2575 Ok(foot.distance <= 1e-6 && foot.u > edge.t0.min(edge.t1) && foot.u < edge.t0.max(edge.t1))
2576 };
2577 let crossed = if result.edges.iter().any(|edge| edge.id == rolled.edge_id) {
2578 rolled.edge_id
2579 } else {
2580 let candidates: Vec<u64> = result.edges.iter().map(|edge| edge.id).collect();
2581 let mut found = None;
2582 for id in candidates {
2583 if holds_crossing(result, id)? {
2584 found = Some(id);
2585 break;
2586 }
2587 }
2588 found.ok_or_else(|| not_built(format!("no part of tangent edge {} holds the crossing any more", rolled.edge_id)))?
2589 };
2590 crate::blend::rollover_surgery::split_edge_at_vertices(result, crossed, &[(crossing_vertex, rolled.point)], take_id)?;
2591 let neighbour_surface = result
2593 .shells
2594 .iter()
2595 .flat_map(|shell| &shell.faces)
2596 .find(|face| face.id == rolled.neighbour_face_id)
2597 .map(|face| face.surface.clone())
2598 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the neighbour face vanished"))?;
2599 let far_pcurve = {
2600 const SAMPLES: usize = 24;
2601 let [t0, t1] = [rail_far_edge.t0, rail_far_edge.t1];
2602 let mut uvs = Vec::with_capacity(SAMPLES + 1);
2603 let mut ts = Vec::with_capacity(SAMPLES + 1);
2604 for k in 0..=SAMPLES {
2605 let t = t0 + (t1 - t0) * k as f64 / SAMPLES as f64;
2606 let point = rail_far_edge.curve.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
2607 let projection = crate::project_point_to_surface(&neighbour_surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
2608 uvs.push(Vec3::new(projection.u, projection.v, 0.0));
2609 ts.push(t);
2610 }
2611 crate::fit::interpolate_curve(&uvs, 3, &ts).or_refuse(KernelStage::Refine, "interpolate_curve")?
2613 };
2614 let leg_edge_id = splice.legs.first().map(|piece| piece.edge_id)
2616 .ok_or_else(|| not_built("a rolled side has no cap leg".to_string()))?;
2617 let mate_face = result
2618 .shells
2619 .iter_mut()
2620 .flat_map(|shell| shell.faces.iter_mut())
2621 .find(|face| face.id == mate.face.id)
2622 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the mate face vanished"))?;
2623 mate_face.loops[mate.loop_index].coedges.retain(|coedge| coedge.edge_id != rail_far && coedge.edge_id != leg_edge_id);
2624 footprint.pieces.push((rolled.neighbour_face_id, rail_far, crossing_vertex, rim_vertex, far_pcurve));
2625 for piece in &splice.legs {
2626 footprint.pieces.push((piece.face_id, piece.edge_id, piece.from, piece.to, piece.pcurve.clone()));
2627 }
2628 for face_id in [mate.face.id, rolled.neighbour_face_id].into_iter().chain(splice.legs.iter().map(|piece| piece.face_id)) {
2629 if !footprint.faces.contains(&face_id) {
2630 footprint.faces.push(face_id);
2631 }
2632 }
2633 Ok(rail_near)
2634}
2635
2636fn retrace_stopped_end_footprint(
2646 result: &mut BrepSolid,
2647 take_id: &mut dyn FnMut() -> u64,
2648 footprint: StoppedEndFootprint,
2649) -> Result<(), KernelRefusal> {
2650 let what = "stopped_end_rollover";
2651 let not_built = |message: String| KernelRefusal::unsupported(KernelStage::Sew, what, format!("blend stopped-end rollover: {message}"));
2652 let (sharp_point, tangent) = footprint.plane;
2653 let mut boundary_vertices: Vec<u64> = vec![footprint.sharp_vertex];
2654 for (_, _, from, to, _) in &footprint.pieces {
2655 for vertex in [*from, *to] {
2656 if !boundary_vertices.contains(&vertex) {
2657 boundary_vertices.push(vertex);
2658 }
2659 }
2660 }
2661 let piece_edges: Vec<u64> = footprint.pieces.iter().map(|piece| piece.1).collect();
2662 let mut interior: Vec<u64> = Vec::new();
2664 for face in result.shells.iter().flat_map(|shell| &shell.faces).filter(|face| footprint.faces.contains(&face.id)) {
2665 for coedge in face.loops.iter().flat_map(|lp| &lp.coedges) {
2666 let Some(edge) = result.edges.iter().find(|edge| edge.id == coedge.edge_id) else { continue };
2667 if piece_edges.contains(&edge.id) || interior.contains(&edge.id) {
2668 continue;
2669 }
2670 if !(boundary_vertices.contains(&edge.start_vertex_id) && boundary_vertices.contains(&edge.end_vertex_id)) {
2671 continue;
2672 }
2673 let mid = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1)).or_refuse(KernelStage::Refine, "evaluate")?;
2674 if mid.sub(sharp_point).dot(tangent) < 0.0 {
2675 interior.push(edge.id);
2676 }
2677 }
2678 }
2679 if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
2680 eprintln!("stopped-end rollover: footprint faces {:?}, interior edges {:?}, boundary vertices {:?}", footprint.faces, interior, boundary_vertices);
2681 }
2682 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();
2683 let mut new_faces: Vec<FaceRecord> = Vec::new();
2684 let mut taken: Vec<String> = result.shells.iter().flat_map(|shell| &shell.faces).filter_map(|face| face.name.clone()).collect();
2687 for face_id in footprint.faces.clone() {
2688 let face = result
2689 .shells
2690 .iter_mut()
2691 .flat_map(|shell| shell.faces.iter_mut())
2692 .find(|face| face.id == face_id)
2693 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: face {face_id} vanished")))?;
2694 let pieces: Vec<&(u64, u64, u64, u64, NurbsCurve)> = footprint.pieces.iter().filter(|piece| piece.0 == face_id).collect();
2695 let mut rebuilt_loops: Vec<LoopRecord> = Vec::new();
2696 for loop_record in &face.loops {
2697 let touched = loop_record.coedges.iter().any(|coedge| interior.contains(&coedge.edge_id))
2698 || loop_record.coedges.iter().any(|coedge| {
2699 edge_ends.get(&coedge.edge_id).is_some_and(|(a, b)| boundary_vertices.contains(a) || boundary_vertices.contains(b))
2700 });
2701 if !touched {
2702 rebuilt_loops.push(loop_record.clone());
2703 continue;
2704 }
2705 let n = loop_record.coedges.len();
2708 let mut remaining: Vec<(CoedgeRecord, u64, u64, usize)> = Vec::new();
2709 for (index, coedge) in loop_record.coedges.iter().enumerate() {
2710 if interior.contains(&coedge.edge_id) {
2711 continue;
2712 }
2713 let (start, end) = edge_ends.get(&coedge.edge_id).copied()
2714 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: coedge {} names a missing edge {}", coedge.id, coedge.edge_id)))?;
2715 let (from, to) = if coedge.forward { (start, end) } else { (end, start) };
2716 remaining.push((coedge.clone(), from, to, index));
2717 }
2718 let mut loose: Vec<&(u64, u64, u64, u64, NurbsCurve)> = pieces.clone();
2719 while !remaining.is_empty() {
2724 let start_at = remaining.iter().position(|(_, from, to, _)| from != to).unwrap_or(0);
2727 let (first, from0, to0, index0) = remaining.remove(start_at);
2728 let mut walk: Vec<CoedgeRecord> = vec![first];
2729 let mut at = to0;
2730 let mut previous = index0;
2731 let mut guard = 0usize;
2732 loop {
2733 guard += 1;
2734 if guard > 4096 {
2735 return Err(not_built(format!("face {face_id}: the re-trace does not close")));
2736 }
2737 let mut next_remaining: Vec<usize> = remaining.iter().enumerate().filter(|(_, (_, from, _, _))| *from == at).map(|(k, _)| k).collect();
2738 let successor_is_start = (previous + 1) % n == index0;
2739 if at == from0 && (successor_is_start || next_remaining.is_empty()) {
2740 if at == from0 && successor_is_start {
2743 break;
2744 }
2745 if loose.iter().all(|piece| piece.2 != at && piece.3 != at) {
2746 break;
2747 }
2748 }
2749 if next_remaining.len() > 1 {
2750 if let Some(k) = next_remaining.iter().copied().find(|k| remaining[*k].3 == (previous + 1) % n) {
2751 next_remaining = vec![k];
2752 }
2753 }
2754 let next_pieces: Vec<usize> = loose.iter().enumerate().filter(|(_, piece)| piece.2 == at || piece.3 == at).map(|(k, _)| k).collect();
2755 match (next_remaining.len(), next_pieces.len()) {
2756 (1, 0) => {
2757 let (coedge, _, to, index) = remaining.remove(next_remaining[0]);
2758 walk.push(coedge);
2759 at = to;
2760 previous = index;
2761 }
2762 (0, 1) => {
2763 let piece = loose.remove(next_pieces[0]);
2764 let (edge_id, from, to, pcurve) = (piece.1, piece.2, piece.3, &piece.4);
2765 let (start, _) = edge_ends.get(&edge_id).copied()
2766 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: footprint edge {edge_id} missing")))?;
2767 let along = from == at;
2768 let coedge_forward = if along { start == from } else { start == to };
2769 walk.push(CoedgeRecord {
2770 id: take_id(),
2771 edge_id,
2772 forward: coedge_forward,
2773 pcurve: if along { pcurve.clone() } else { pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")? },
2774 });
2775 at = if along { to } else { from };
2776 }
2777 (r, p) => {
2778 return Err(not_built(format!("face {face_id}: vertex {at} has {} ways on ({r} boundary, {p} new), not one", r + p)));
2779 }
2780 }
2781 }
2782 rebuilt_loops.push(LoopRecord { id: if rebuilt_loops.is_empty() { loop_record.id } else { take_id() }, coedges: walk });
2783 }
2784 if !loose.is_empty() {
2785 return Err(not_built(format!("face {face_id}: {} new boundary piece(s) joined no loop", loose.len())));
2786 }
2787 }
2788 let kept = rebuilt_loops.remove(0);
2791 face.loops = vec![kept];
2792 let surface = face.surface.clone();
2793 let same_sense = face.same_sense;
2794 let base_name = face.name.clone();
2795 for extra in rebuilt_loops {
2796 let name = base_name.as_ref().map(|base| {
2797 (1..).map(|k| format!("{base}_{k}")).find(|candidate| !taken.contains(candidate)).expect("a free name")
2798 });
2799 if let Some(name) = &name {
2800 taken.push(name.clone());
2801 }
2802 new_faces.push(FaceRecord { id: take_id(), surface: surface.clone(), same_sense, loops: vec![extra], name });
2803 }
2804 }
2805 if !new_faces.is_empty() {
2806 let shell_index = result.shells.iter().position(|shell| !shell.faces.is_empty())
2807 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: no shell"))?;
2808 result.shells[shell_index].faces.extend(new_faces);
2809 }
2810 result.edges.retain(|edge| !interior.contains(&edge.id));
2811 Ok(())
2812}
2813
2814pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), KernelRefusal> {
2820 let surface = &face.surface;
2821 let rows_u = surface.control_points.len();
2822 let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
2823 let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
2824 for row in &surface.control_points {
2825 for (j, point) in row.iter().enumerate() {
2826 transposed[j].push(*point);
2827 }
2828 }
2829 face.surface = NurbsSurface::new(
2830 surface.degree_v,
2831 surface.degree_u,
2832 surface.knots_v.clone(),
2833 surface.knots_u.clone(),
2834 transposed,
2835 ).or_refuse(KernelStage::Refine, "new")?;
2836 for loop_record in &mut face.loops {
2837 for coedge in &mut loop_record.coedges {
2838 for control in &mut coedge.pcurve.control_points {
2839 std::mem::swap(&mut control.x, &mut control.y);
2840 }
2841 }
2842 }
2843 face.same_sense = !face.same_sense;
2844 Ok(())
2845}
2846
2847pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
2852 let used: rustc_hash::FxHashSet<u64> = result
2853 .edges
2854 .iter()
2855 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
2856 .collect();
2857 result
2858 .vertices
2859 .retain(|candidate| used.contains(&candidate.id));
2860}
2861
2862fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
2863 crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")
2864}
2865
2866
2867