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 law = crate::blend::setback::active();
105 let (stations, vertex_indices, refinement, dense) = if let Some(law) = law {
106 let parameters = |count: usize| {
109 open_station_parameters(edge, overshoot_fraction, [edge.t0, edge.t1], count, [false, false])
110 };
111 let (stations, vertex_indices) = crate::blend::setback::march_open(
112 solid, edge, &first_mate, &second_mate, &law, station_count, ¶meters, rail_bar,
113 )?;
114 let refinement = StationRefinement { exit: StationRefineExit::Converged, ..Default::default() };
115 (stations, vertex_indices, refinement, OpenDenseCentres::default())
116 } else { march_open_stations_core(
117 edge,
118 &first_mate,
119 &second_mate,
120 radius_at,
121 overshoot_fraction,
122 [edge.t0, edge.t1],
123 station_count,
124 [false, false],
125 [false, false],
126 crate::blend::station_refinement_on().then_some(rail_bar),
127 if wall_check { OPEN_WALL_CENTRES } else { 0 },
128 true,
129 )? };
130 let parameters = station_parameters(&stations);
131 let rows = fit_open_rows(
132 &stations,
133 ¶meters,
134 chamfer,
135 Some(vertex_indices),
136 extrusion_direction(edge, &first_mate, &second_mate),
137 )?;
138 crate::blend::fold::check_rows_loop(
141 &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),
142 )?;
143 let mut ends = Vec::with_capacity(2);
144 let mut in_range = true;
145 for (vertex, at_start) in [(edge.start_vertex_id, true), (edge.end_vertex_id, false)] {
146 let (end, side_in_range) = resolve_free_end(
147 solid,
148 edge_id,
149 &first_mate,
150 &second_mate,
151 &rows,
152 vertex,
153 at_start,
154 )?;
155 if debug {
156 eprintln!(
157 " [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}",
158 end.end_face_id,
159 end.first_edge_id,
160 end.first_edge_parameter,
161 end.second_edge_id,
162 end.second_edge_parameter,
163 end.cr_parameter,
164 end.cs_parameter,
165 );
166 }
167 in_range &= side_in_range;
168 ends.push(end);
169 }
170 let windows = [
175 shipped_window(¶meters, ends[0].cr_parameter, ends[1].cr_parameter),
176 shipped_window(¶meters, ends[0].cs_parameter, ends[1].cs_parameter),
177 ];
178 let (rails, rail_samples) =
179 rails_on_shipped_windows(&rows, ¶meters, &stations, [&first_mate.face.surface, &second_mate.face.surface], windows);
180 let walls = match (wall_check, windows) {
181 (false, _) => Vec::new(),
182 (true, [Some(first), Some(second)]) => open_wall_readings_why(
183 &rows,
184 ¶meters,
185 &stations,
186 [first[0].min(second[0]), first[1].max(second[1])],
187 &dense,
188 wall_radius.unwrap_or(f64::NAN),
189 ),
190 (true, _) => vec![Err("no shipped window to read".to_string())],
192 };
193 let first_unread = walls.iter().find_map(|wall| wall.as_ref().err().cloned());
194 let walls: Vec<Option<f64>> = if wall_check && windows.iter().any(|window| window.is_none()) {
195 Vec::new()
196 } else {
197 walls.into_iter().map(|wall| wall.ok()).collect()
198 };
199 let reading = OpenRungReading {
200 station_count,
201 stations: stations.len(),
202 rails,
203 rail_samples,
204 walls,
205 first_unread,
206 refine_exit: refinement.exit,
207 inserted: refinement.inserted,
208 };
209 Ok((rows, ends, in_range, reading))
210 };
211
212 let seeded_fraction: Option<f64> = (|| {
222 let span = edge.t1 - edge.t0;
223 let mut length = 0.0f64;
224 let mut previous = edge.curve.evaluate(edge.t0).ok()?;
225 for index in 1..=16 {
226 let t = edge.t0 + span * index as f64 / 16.0;
227 let point = edge.curve.evaluate(t).ok()?;
228 length += point.sub(previous).length();
229 previous = point;
230 }
231 if !(length > 0.0) || !length.is_finite() {
232 return None;
233 }
234 let mid_t = edge.t0 + span * 0.5;
235 let uv1 = edge_uv_on_face(first_mate.coedge, edge, mid_t).ok()?;
236 let uv2 = edge_uv_on_face(second_mate.coedge, edge, mid_t).ok()?;
237 let n1 = raw_normal(&first_mate.face.surface, uv1[0], uv1[1]).ok()?;
238 let n2 = raw_normal(&second_mate.face.surface, uv2[0], uv2[1]).ok()?;
239 let cos_alpha =
240 (rho1.signum() * rho2.signum() * n1.dot(n2)).clamp(-1.0, 1.0);
241 let tan_half = ((1.0 - cos_alpha).max(0.0) / (1.0 + cos_alpha).max(1e-9)).sqrt();
244 let end_radius = radius_at(edge.t0).abs().max(radius_at(edge.t1).abs());
245 let fraction = (1.5 * end_radius * tan_half / length).clamp(0.08, 0.45);
246 if !fraction.is_finite() || (fraction - 0.08).abs() < 1e-12 {
247 return None;
248 }
249 Some(fraction)
250 })();
251
252 let choose = |station_count: usize| -> Result<(FittedRows, Vec<EndSurgery>, OpenRungReading), KernelRefusal> {
259 let mut chosen: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
260 let mut last_error: Option<KernelRefusal> = None;
261 if let Some(fraction) = seeded_fraction {
265 match compute(fraction, station_count) {
266 Ok((rows, ends, true, reading)) => chosen = Some((rows, ends, reading)),
267 Ok(_) => {}
268 Err(error) if crate::blend::is_wall_fold(&error) => return Err(error),
269 Err(error) => last_error = Some(error),
270 }
271 }
272 if chosen.is_none() {
273 for &overshoot_fraction in &[0.08f64, 0.16, 0.28, 0.45] {
274 match compute(overshoot_fraction, station_count) {
275 Ok((rows, ends, in_range, reading)) => {
276 let fallback = chosen.is_none();
277 if in_range {
278 chosen = Some((rows, ends, reading));
279 break;
280 } else if fallback {
281 chosen = Some((rows, ends, reading));
282 }
283 }
284 Err(error) if crate::blend::is_wall_fold(&error) => return Err(error),
285 Err(error) => last_error = Some(error),
286 }
287 }
288 }
289 chosen.ok_or_else(|| {
290 last_error.unwrap_or_else(|| {
291 KernelRefusal::internal(
292 KernelStage::Refine,
293 "open_march",
294 "blend: open march failed at every overshoot",
295 )
296 })
297 })
298 };
299 let accepts = |reading: &OpenRungReading| {
312 reading.rails_read()
313 && reading.rails <= rail_bar
314 && (!wall_check || reading.worst_wall().is_some_and(|wall| wall <= wall_bar))
315 };
316 let meets_model = |reading: &OpenRungReading| reading.worst_wall().is_some_and(|wall| wall <= model);
317 let mut station_count = STATIONS;
318 let mut accepted: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
319 let mut unaccepted: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
322 let mut request_rungs = 0usize;
323 let mut unmet: Option<crate::BudgetReason> = None;
324 loop {
325 if accepted.is_some() {
328 request_rungs += 1;
329 }
330 let chosen = choose(station_count);
331 let (rows, ends, reading) = match chosen {
332 Ok(chosen) => chosen,
333 Err(_) if accepted.is_some() => {
335 unmet = Some(crate::BudgetReason::Incoherent);
336 break;
337 }
338 Err(error) => return Err(error),
339 };
340 #[cfg(not(test))]
341 let reject_request = false;
342 crate::blend::carve::carve_trace(format_args!(
343 "blend open edge {}: {} stations a rung ({} in all), rails {:.3e} (bar {rail_bar:.3e}), wall {:.3e} (bar {wall_bar:.3e}){}",
344 edge.id, reading.station_count, reading.stations, reading.rails,
345 reading.worst_wall().unwrap_or(f64::INFINITY),
346 if accepted.is_some() { ", a request rung" } else { "" }
347 ));
348 let accepted_now = accepts(&reading) && !reject_request;
349 if accepted.is_none() {
350 if accepted_now {
351 let met = !wall_check || meets_model(&reading);
352 accepted = Some((rows, ends, reading));
353 if met {
354 break;
355 }
356 } else if station_count < MAX_OPEN_STATIONS {
357 station_count *= 2;
358 continue;
359 } else {
360 crate::blend::record_blend_note(format!(
361 "blend: open edge {} at {} stations a rung ({} in all) — the top of the open march's \
362 ladder — still leaves its rails {:.3e} from their carriers (bar {rail_bar:.3e}) or its \
363 wall off the rolling ball (bar {wall_bar:.3e}); the wall ships as built, unaccepted",
364 edge.id, reading.station_count, reading.stations, reading.rails
365 ));
366 unaccepted = Some((rows, ends, reading));
367 break;
368 }
369 } else {
370 if !accepted_now {
371 unmet = Some(crate::BudgetReason::Rejected);
372 break;
373 }
374 let met = meets_model(&reading);
375 accepted = Some((rows, ends, reading));
376 if met {
377 unmet = None;
378 break;
379 }
380 }
381 if station_count < MAX_OPEN_STATIONS {
383 station_count *= 2;
384 } else {
385 unmet = Some(crate::BudgetReason::StationCeiling);
386 break;
387 }
388 }
389 let (shipped_accepted, (rows, ends, reading)) = match (accepted, unaccepted) {
390 (Some(rung), _) => (true, rung),
391 (None, Some(rung)) => (false, rung),
392 (None, None) => {
393 return Err(KernelRefusal::internal(KernelStage::Refine, "open_ladder", "blend: the open ladder ended without a rung"));
394 }
395 };
396 let report: Option<(crate::BudgetReason, f64, f64, String)> = if !shipped_accepted {
400 Some(acceptance_deficit(&reading, rail_bar, wall_bar, wall_check, reading.rails_read(), "no rung of the open ladder was accepted"))
401 } else if wall_check && !meets_model(&reading) {
402 let reason = unmet.unwrap_or(crate::BudgetReason::StationCeiling);
403 let declared = reading.worst_wall().unwrap_or(f64::INFINITY);
405 Some((reason, model, declared, String::new()))
406 } else {
407 None
408 };
409
410 let [start_end, finish_end] = match <[EndSurgery; 2]>::try_from(ends) {
411 Ok(pair) => pair,
412 Err(_) => return Err(KernelRefusal::internal(KernelStage::Sew, "open_surgery_ends", "blend: open surgery needs exactly two ends")),
413 };
414 let mut crossings = SpokeCrossings::default();
418 crossings.note(start_end.escalated || finish_end.escalated);
419 let mut result = solid.clone();
420 let mut take_id = fresh_id_source(solid);
421 let sewn = build_open_surgery(
422 solid,
423 &mut result,
424 &mut take_id,
425 edge,
426 &first_mate,
427 &second_mate,
428 rows,
429 [EndPlan::Free(start_end), EndPlan::Free(finish_end)],
430 name,
431 )?;
432 prune_orphan_vertices(&mut result);
433 crate::blend::fit_planar_charts_to_trims(solid, &mut result)?;
437 check_snap_closure(solid, &result, sewn.snap)?;
438 let built = crossings.gate(result)?;
439 let sewn_surface = built
442 .shells
443 .iter()
444 .flat_map(|shell| &shell.faces)
445 .find(|face| face.id == sewn.blend_face_id)
446 .map(|face| face.surface.clone())
447 .ok_or(KernelRefusal::internal(KernelStage::Sew, "open_blend_face", "blend: the sewn open blend face is not in the built body"))?;
448 if let Some((reason, bar, measured, deficit)) = report {
451 let rungs_used = if shipped_accepted { request_rungs } else { (reading.station_count / STATIONS).trailing_zeros() as usize };
457 let mut detail = format!(
458 "the shipped rung's local rail refinement ended {:?} with {} stations inserted",
459 reading.refine_exit, reading.inserted
460 );
461 if !deficit.is_empty() {
462 detail = format!("{deficit}; {detail}");
463 }
464 crate::blend::record_wall_model_report(crate::blend::WallModelReport {
465 name: name.map(str::to_string),
466 surface: sewn_surface,
467 request: bar,
468 residual: measured,
469 detail: Some(detail),
470 budget: crate::ApproximationBudget {
471 reason,
472 rounds_used: rungs_used,
473 rounds_limit: OPEN_REQUEST_RUNGS,
474 stations: reading.stations,
475 station_limit: REFINE_STATION_FACTOR * (MAX_OPEN_STATIONS + 1),
476 mechanism: crate::BudgetMechanism::StationRungs,
477 measured_component: crate::MeasuredComponent::Wall,
478 unread: 0,
479 },
480 });
481 }
482 Ok(built)
483}
484
485pub(in crate::blend) fn acceptance_deficit(
497 reading: &OpenRungReading,
498 rail_bar: f64,
499 wall_bar: f64,
500 wall_check: bool,
501 rails_read: bool,
502 what: &str,
503) -> (crate::BudgetReason, f64, f64, String) {
504 let rails = (rails_read && reading.rails.is_finite() && reading.rails >= 0.0).then_some(reading.rails);
505 let wall = if wall_check { reading.worst_wall() } else { None };
506 let unread = rails.is_none() || (wall_check && wall.is_none());
507 let mut worst = (0.0_f64, rail_bar);
508 for (value, bar) in [(rails, rail_bar), (wall, wall_bar)] {
509 if let Some(value) = value {
510 if value / bar > worst.0 / worst.1 {
511 worst = (value, bar);
512 }
513 }
514 }
515 let rail_text = match rails {
516 Some(value) => format!("rails {value:.3e} from their carriers (bar {rail_bar:.3e})"),
517 None => format!(
518 "rails UNREAD ({:?} over {} + {} samples) against bar {rail_bar:.3e}",
519 reading.rails, reading.rail_samples[0], reading.rail_samples[1]
520 ),
521 };
522 let wall_text = match (wall_check, wall) {
523 (false, _) => "the wall not judged (no constant-radius fillet provenance)".to_string(),
524 (true, Some(value)) => format!("wall {value:.3e} off the rolling ball (bar {wall_bar:.3e})"),
525 (true, None) => format!(
526 "wall UNREAD on {} of {} intervals against bar {wall_bar:.3e}{}",
527 reading.walls.iter().filter(|wall| !matches!(wall, Some(value) if value.is_finite())).count(),
528 reading.walls.len(),
529 reading.first_unread.as_deref().map(|why| format!(" (first: {why})")).unwrap_or_default()
530 ),
531 };
532 let reason = if unread { crate::BudgetReason::Unread } else { crate::BudgetReason::Unaccepted };
533 (reason, worst.1, worst.0, format!("{what}: {rail_text}, {wall_text}"))
534}
535
536const OPEN_WALL_SECTIONS: usize = 8;
539pub(in crate::blend) const OPEN_WALL_CENTRES: usize = 32;
540pub(in crate::blend) const OPEN_REQUEST_RUNGS: usize = (MAX_OPEN_STATIONS / STATIONS).trailing_zeros() as usize;
543
544#[derive(Clone, Debug)]
551pub(in crate::blend) struct OpenRungReading {
552 pub(in crate::blend) station_count: usize,
553 pub(in crate::blend) stations: usize,
554 pub(in crate::blend) rails: f64,
555 pub(in crate::blend) rail_samples: [usize; 2],
556 pub(in crate::blend) walls: Vec<Option<f64>>,
557 pub(in crate::blend) first_unread: Option<String>,
560 pub(in crate::blend) refine_exit: crate::blend::stations::StationRefineExit,
561 pub(in crate::blend) inserted: usize,
562}
563
564impl OpenRungReading {
565 pub(in crate::blend) fn rails_read(&self) -> bool {
569 self.rails.is_finite() && self.rails >= 0.0 && self.rail_samples.iter().all(|&samples| samples > 0)
570 }
571
572 pub(in crate::blend) fn worst_wall(&self) -> Option<f64> {
574 if self.walls.is_empty() {
575 return None;
576 }
577 self.walls.iter().try_fold(0.0_f64, |worst, wall| match wall {
578 Some(declared) if declared.is_finite() && *declared >= 0.0 => Some(worst.max(*declared)),
579 _ => None,
580 })
581 }
582}
583
584pub(in crate::blend) fn shipped_window(parameters: &[f64], a: f64, b: f64) -> Option<[f64; 2]> {
589 let (&first, &last) = (parameters.first()?, parameters.last()?);
590 let (low, high) = (a.min(b).max(first), a.max(b).min(last));
591 (low.is_finite() && high.is_finite() && low < high).then_some([low, high])
592}
593
594fn interval_at(parameters: &[f64], u: f64) -> Option<(usize, f64)> {
596 let last = parameters.len().checked_sub(2)?;
597 let interval = parameters.partition_point(|¶meter| parameter <= u).saturating_sub(1).min(last);
598 let (from, to) = (parameters[interval], parameters[interval + 1]);
599 (to > from).then(|| (interval, ((u - from) / (to - from)).clamp(0.0, 1.0)))
600}
601
602const AFFINE_READS_DEFAULT: bool = true;
609
610pub(in crate::blend) fn affine_reads_on() -> bool {
611 switch!(live "BREP_BLEND_AFFINE_READS", AFFINE_READS_DEFAULT)
612}
613
614
615fn affine_plane(surface: &NurbsSurface) -> Option<(Vec3, Vec3)> {
621 let (origin, _, _) = affine_plane_frame(surface)?;
622 let net = &surface.control_points;
624 let [p00, p01, p10] = [net[0][0].point().ok()?, net[0][1].point().ok()?, net[1][0].point().ok()?];
625 Some((origin, p10.sub(p00).cross(p01.sub(p00)).normalized().ok()?))
626}
627
628pub(in crate::blend) fn affine_plane_frame(surface: &NurbsSurface) -> Option<(Vec3, Vec3, Vec3)> {
631 if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || surface.degree_u != 1 || surface.degree_v != 1 {
632 return None;
633 }
634 let net = &surface.control_points;
635 if net.len() != 2 || net.iter().any(|row| row.len() != 2) {
636 return None;
637 }
638 let weight = net[0][0].w;
639 if !(weight > 0.0) || net.iter().flatten().any(|point| point.w != weight) {
640 return None;
641 }
642 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()?];
643 let scale = [p00, p01, p10, p11].iter().fold(0.0f64, |worst, point| worst.max(point.length()));
644 if p11.sub(p10).sub(p01.sub(p00)).length() > 1e-12 * (1.0 + scale) {
645 return None;
646 }
647 let [u0, u1] = surface.domain_u().ok()?;
648 let [v0, v1] = surface.domain_v().ok()?;
649 if !(u1 > u0 && v1 > v0) {
650 return None;
651 }
652 Some((p00, p10.sub(p00).scale(1.0 / (u1 - u0)), p01.sub(p00).scale(1.0 / (v1 - v0))))
653}
654
655fn affine_curve(curve: &NurbsCurve) -> bool {
658 curve.degree == 1
659 && curve.control_points.len() == 2
660 && curve.control_points[0].w > 0.0
661 && curve.control_points[0].w == curve.control_points[1].w
662}
663
664pub(in crate::blend) fn affine_rail_standoff(rail: &NurbsCurve, carrier: &NurbsSurface, window: [f64; 2]) -> Option<f64> {
670 if !affine_curve(rail) {
671 return None;
672 }
673 let (origin, normal) = affine_plane(carrier)?;
674 let mut worst = 0.0f64;
675 for u in window {
676 let point = rail.evaluate(u).ok()?;
677 let off = point.sub(origin).dot(normal).abs();
678 if !off.is_finite() {
679 return None;
680 }
681 worst = worst.max(off);
682 }
683 Some(worst)
684}
685
686const EXACT_REVOLUTION_READS_DEFAULT: bool = true;
690
691pub(in crate::blend) fn exact_revolution_reads_on() -> bool {
692 switch!(live "BREP_EXACT_REVOLUTION_READS", EXACT_REVOLUTION_READS_DEFAULT)
693}
694
695
696pub(in crate::blend) fn exact_revolution_read_bound(rail: &NurbsCurve, carrier: &NurbsSurface, pcurve: &NurbsCurve) -> Option<f64> {
716 if let Some((origin, s_u, s_v)) = affine_plane_frame(carrier) {
717 if pcurve.degree != rail.degree || pcurve.knots != rail.knots || pcurve.control_points.len() != rail.control_points.len() {
719 return None;
720 }
721 let ([u0, _], [v0, _]) = (carrier.domain_u().ok()?, carrier.domain_v().ok()?);
722 let image: Vec<Vec4> = pcurve
723 .control_points
724 .iter()
725 .map(|control| {
726 let (u, v) = (control.x / control.w, control.y / control.w);
727 let point = origin.add(s_u.scale(u - u0)).add(s_v.scale(v - v0));
728 Vec4 { x: point.x * control.w, y: point.y * control.w, z: point.z * control.w, w: control.w }
729 })
730 .collect();
731 return crate::blend::stations::net_bound(&rail.control_points, &image);
732 }
733 if !matches!(carrier.analytic(), Some(crate::AnalyticSurface::RuledRevolution { .. } | crate::AnalyticSurface::Revolution { .. } | crate::AnalyticSurface::Torus { .. } | crate::AnalyticSurface::Sphere { .. })) {
734 return None;
735 }
736 if !affine_curve(pcurve) {
737 return None;
738 }
739 let [q0, q1] = pcurve.domain().ok()?;
740 let (a, b) = (pcurve.evaluate(q0).ok()?, pcurve.evaluate(q1).ok()?);
741 let ([u0, u1], [v0, v1]) = (carrier.domain_u().ok()?, carrier.domain_v().ok()?);
742 let (circle, from, to) = if a.y.to_bits() == b.y.to_bits() && a.x != b.x && a.y >= v0 && a.y <= v1 {
743 (carrier.iso_curve_v(a.y).ok()?, a.x, b.x)
744 } else if a.x.to_bits() == b.x.to_bits() && a.y != b.y && a.x >= u0 && a.x <= u1 {
745 (carrier.iso_curve_u(a.x).ok()?, a.y, b.y)
746 } else {
747 return None;
748 };
749 let [c0, c1] = circle.domain().ok()?;
750 let reversed = to < from;
751 let circle = if reversed { circle.reversed().ok()? } else { circle };
752 let on_curve = |along: f64| if reversed { c0 + c1 - along } else { along };
753 let (lo, hi) = (on_curve(from), on_curve(to));
754 if !(hi > lo) {
755 return None;
756 }
757 let segment = super::support::extend_conic_ends(&circle, lo, hi).ok()?;
758 let [r0, r1] = segment.domain().ok()?;
759 let degree = segment.degree;
760 let mut knots = segment.knots.clone();
761 for knot in knots.iter_mut() {
762 *knot = ((*knot - r0) / (r1 - r0)).clamp(0.0, 1.0);
763 }
764 for knot in knots.iter_mut().take(degree + 1) {
765 *knot = 0.0;
766 }
767 let count = knots.len();
768 for knot in knots.iter_mut().skip(count - degree - 1) {
769 *knot = 1.0;
770 }
771 if rail.degree != degree || rail.knots != knots || rail.control_points.len() != segment.control_points.len() {
772 return None;
773 }
774 crate::blend::stations::net_bound(&rail.control_points, &segment.control_points)
775}
776
777pub(in crate::blend) fn rails_on_shipped_windows(
786 rows: &FittedRows,
787 parameters: &[f64],
788 stations: &[Station],
789 carriers: [&NurbsSurface; 2],
790 windows: [Option<[f64; 2]>; 2],
791) -> (f64, [usize; 2]) {
792 read_rail_windows([&rows.cr, &rows.cs], parameters, stations, carriers, windows,
793 rows.exact_extrusion && affine_reads_on(), certified_rail_bounds(rows, carriers), None)
794}
795
796fn certified_rail_bounds(rows: &FittedRows, carriers: [&NurbsSurface; 2]) -> [Option<f64>; 2] {
800 if rows.revolution_axis.is_none() || !exact_revolution_reads_on() {
801 return [None, None];
802 }
803 [
804 exact_revolution_read_bound(&rows.cr, carriers[0], &rows.cr_pcurve),
805 exact_revolution_read_bound(&rows.cs, carriers[1], &rows.cs_pcurve),
806 ]
807}
808
809fn read_rail_windows(
810 rails: [&NurbsCurve; 2], parameters: &[f64], stations: &[Station],
811 carriers: [&NurbsSurface; 2], windows: [Option<[f64; 2]>; 2], affine: bool,
812 certified: [Option<f64>; 2],
813 mut memo: Option<&mut RailFeet>,
814) -> (f64, [usize; 2]) {
815 let mut worst = 0.0_f64;
816 let mut samples = [0usize; 2];
817 for (side, (rail, carrier)) in [(rails[0], carriers[0]), (rails[1], carriers[1])].into_iter().enumerate() {
818 let Some([low, high]) = windows[side] else { continue };
819 if let Some(off) = affine.then(|| affine_rail_standoff(rail, carrier, [low, high])).flatten() {
822 crate::blend::record_count("count.read.affine_rails", 1);
823 samples[side] += 2;
824 if !worst.is_nan() && off > worst {
825 worst = off;
826 }
827 continue;
828 }
829 if let Some(bound) = certified[side] {
833 crate::blend::record_count("count.read.revolution_rails", 1);
834 samples[side] += 1;
835 if !worst.is_nan() && bound > worst {
836 worst = bound;
837 }
838 continue;
839 }
840 let mut at = vec![low, high];
841 for pair in parameters.windows(2) {
842 for fraction in [0.25, 0.5, 0.75] {
843 let u = pair[0] + (pair[1] - pair[0]) * fraction;
844 if u > low && u < high {
845 at.push(u);
846 }
847 }
848 }
849 for u in at {
850 let cached = memo.as_ref().and_then(|memo| memo.values[side].get(&u.to_bits()).copied());
851 let off = if let Some(off) = cached {
852 if let Some(memo) = memo.as_mut() { memo.hits += 1; }
853 crate::blend::record_count("count.read.rail_foot_reused", 1);
854 off
855 } else {
856 if let Some(memo) = memo.as_mut() {
857 memo.reads += 1;
858 crate::blend::record_count("count.read.rail_foot_fresh", 1);
859 }
860 let off = interval_at(parameters, u)
861 .and_then(|(interval, fraction)| {
862 let (left, right) = (&stations[interval], &stations[interval + 1]);
863 let (a, b) = if side == 0 { (left.uv1, right.uv1) } else { (left.uv2, right.uv2) };
864 let seed = [a[0] + fraction * (b[0] - a[0]), a[1] + fraction * (b[1] - a[1])];
865 let point = rail.evaluate(u).ok()?;
866 crate::blend::stations::extended_foot_distance(carrier, point, seed).ok()
867 })
868 .filter(|off| off.is_finite())
869 .unwrap_or(f64::NAN);
870 if off.is_finite() {
871 if let Some(memo) = memo.as_mut() { memo.values[side].insert(u.to_bits(), off); }
872 }
873 off
874 };
875 samples[side] += 1;
876 if !worst.is_nan() && (off.is_nan() || off > worst) {
877 worst = off;
878 }
879 }
880 }
881 (worst, samples)
882}
883
884pub(in crate::blend) struct RailWindowReader {
890 rails: [NurbsCurve; 2],
891 carriers: [NurbsSurface; 2],
892 parameters: Vec<f64>,
893 stations: Vec<Station>,
894 affine: bool,
895 certified: [Option<f64>; 2],
896 feet: RailFeet,
897}
898
899#[derive(Default)]
900struct RailFeet {
901 values: [rustc_hash::FxHashMap<u64, f64>; 2],
902 reads: u64,
903 hits: u64,
904}
905
906impl RailWindowReader {
907 pub(in crate::blend) fn new(rows: &FittedRows, parameters: &[f64], stations: &[Station], carriers: [&NurbsSurface; 2]) -> Self {
908 Self {
909 rails: [rows.cr.clone(), rows.cs.clone()],
910 carriers: [carriers[0].clone(), carriers[1].clone()],
911 parameters: parameters.to_vec(), stations: stations.to_vec(),
912 affine: rows.exact_extrusion && affine_reads_on(), certified: certified_rail_bounds(rows, carriers), feet: RailFeet::default(),
913 }
914 }
915
916 pub(in crate::blend) fn read(&mut self, windows: [Option<[f64; 2]>; 2]) -> (f64, [usize; 2]) {
917 read_rail_windows([&self.rails[0], &self.rails[1]], &self.parameters, &self.stations,
918 [&self.carriers[0], &self.carriers[1]], windows, self.affine, self.certified, Some(&mut self.feet))
919 }
920}
921
922pub(in crate::blend) fn network_rail_reuse_on() -> bool {
923 std::env::var("BREP_NETWORK_RAIL_REUSE").map_or(true, |value| value != "0" && value != "off")
926}
927
928pub(in crate::blend) fn junction_stencil(
938 left_nodes: &[Vec3],
939 right_nodes: &[Vec3],
940 station_ts: &[f64],
941 left: usize,
942) -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
943 let half = OPEN_WALL_CENTRES / 2;
944 if left_nodes.len() != OPEN_WALL_CENTRES + 1 || right_nodes.len() != OPEN_WALL_CENTRES + 1 || left + 2 >= station_ts.len() {
945 return Err(format!("interval {left}: junction needs two {}-node intervals and their three station parameters", OPEN_WALL_CENTRES + 1));
946 }
947 let (t0, t1, t2) = (station_ts[left], station_ts[left + 1], station_ts[left + 2]);
948 let (w_left, w_right) = (t1 - t0, t2 - t1);
949 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()) {
950 return Err(format!("interval {left}: junction station parameters {t0}, {t1}, {t2} are not finite and strictly increasing"));
951 }
952 let nodes: Vec<Vec3> = left_nodes[half..].iter().chain(&right_nodes[1..=half]).copied().collect();
953 if w_left.to_bits() == w_right.to_bits() {
954 return Ok((nodes, None));
955 }
956 let abscissae: Vec<f64> = (half..=OPEN_WALL_CENTRES)
957 .map(|j| if j == OPEN_WALL_CENTRES { t1 } else { t0 + w_left * j as f64 / OPEN_WALL_CENTRES as f64 })
958 .chain((1..=half).map(|j| t1 + w_right * j as f64 / OPEN_WALL_CENTRES as f64))
959 .collect();
960 Ok((nodes, Some(abscissae)))
961}
962
963pub(in crate::blend) fn read_on_stencil(point: Vec3, nodes: &[Vec3], abscissae: Option<&[f64]>, step: usize, radius: f64) -> Result<(f64, f64), String> {
967 match abscissae {
968 None => crate::blend::chain::declared_wall_probe_why(point, nodes, step, radius),
969 Some(ts) => crate::blend::chain::declared_wall_probe_at_why(point, nodes, ts, step, radius),
970 }
971}
972
973pub(in crate::blend) fn exact_extrusion_wall_readings(
1008 rows: &FittedRows,
1009 parameters: &[f64],
1010 window: [f64; 2],
1011 radius: f64,
1012) -> Vec<Result<f64, String>> {
1013 let intervals = parameters.len().saturating_sub(1);
1014 let overlapped: Vec<usize> =
1015 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
1016 let line = match (rows.exact_extrusion, rows.center.as_ref()) {
1017 (true, Some(center)) => center.domain().and_then(|[a, b]| Ok((center.evaluate(a)?, center.evaluate(b)?))),
1018 (false, _) => Err("the rows are not an exact extrusion".to_string()),
1019 (true, None) => Err("the exact rows carry no centre line".to_string()),
1020 };
1021 let (anchor, direction) = match line.and_then(|(a, b)| b.sub(a).normalized().map(|d| (a, d))) {
1022 Ok(line) => line,
1023 Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
1024 };
1025 let Ok([v_low, v_high]) = rows.surface.domain_v() else {
1026 return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
1027 };
1028 overlapped
1029 .into_iter()
1030 .map(|interval| -> Result<f64, String> {
1031 let (from, to) = (parameters[interval], parameters[interval + 1]);
1032 let inside = |u: f64| u >= window[0] && u <= window[1];
1033 let mut probes: Vec<f64> =
1034 (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
1035 probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
1036 if probes.is_empty() {
1037 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1038 }
1039 let mut worst = 0.0_f64;
1040 for u in probes {
1041 for k in 1..OPEN_WALL_SECTIONS {
1042 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1043 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1044 let offset = point.sub(anchor);
1045 let standoff = offset.sub(direction.scale(offset.dot(direction))).length();
1046 let reading = (standoff - radius).abs();
1047 if !reading.is_finite() {
1048 return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
1049 }
1050 worst = worst.max(reading);
1051 }
1052 }
1053 Ok(worst)
1054 })
1055 .collect()
1056}
1057
1058pub(in crate::blend) fn exact_revolution_wall_readings(
1069 rows: &FittedRows,
1070 parameters: &[f64],
1071 window: [f64; 2],
1072 radius: f64,
1073) -> Vec<Result<f64, String>> {
1074 let intervals = parameters.len().saturating_sub(1);
1075 let overlapped: Vec<usize> =
1076 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
1077 let circle = match (rows.revolution_axis, rows.center.as_ref()) {
1078 (Some((origin, axis)), Some(center)) => center.domain().and_then(|[a, _]| center.evaluate(a)).map(|point| (origin, axis, point)),
1079 (None, _) => Err("the rows are not an exact revolution".to_string()),
1080 (Some(_), None) => Err("the exact rows carry no centre circle".to_string()),
1081 };
1082 let (origin, axis, centre_radius, centre_height) = match circle {
1083 Ok((origin, axis, point)) => {
1084 let offset = point.sub(origin);
1085 let height = offset.dot(axis);
1086 (origin, axis, offset.sub(axis.scale(height)).length(), height)
1087 }
1088 Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
1089 };
1090 let Ok([v_low, v_high]) = rows.surface.domain_v() else {
1091 return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
1092 };
1093 overlapped
1094 .into_iter()
1095 .map(|interval| -> Result<f64, String> {
1096 let (from, to) = (parameters[interval], parameters[interval + 1]);
1097 let inside = |u: f64| u >= window[0] && u <= window[1];
1098 let mut probes: Vec<f64> =
1099 (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
1100 probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
1101 if probes.is_empty() {
1102 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1103 }
1104 let mut worst = 0.0_f64;
1105 for u in probes {
1106 for k in 1..OPEN_WALL_SECTIONS {
1107 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1108 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1109 let offset = point.sub(origin);
1110 let height = offset.dot(axis);
1111 let radial = offset.sub(axis.scale(height)).length();
1112 let standoff = (radial - centre_radius).hypot(height - centre_height);
1113 let reading = (standoff - radius).abs();
1114 if !reading.is_finite() {
1115 return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
1116 }
1117 worst = worst.max(reading);
1118 }
1119 }
1120 Ok(worst)
1121 })
1122 .collect()
1123}
1124
1125pub(in crate::blend) fn open_wall_readings_why(
1126 rows: &FittedRows,
1127 parameters: &[f64],
1128 stations: &[Station],
1129 window: [f64; 2],
1130 dense: &OpenDenseCentres,
1131 radius: f64,
1132) -> Vec<Result<f64, String>> {
1133 let intervals = parameters.len().saturating_sub(1);
1134 let overlapped: Vec<usize> =
1135 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
1136 let Ok([v_low, v_high]) = rows.surface.domain_v() else {
1137 return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
1138 };
1139 let nodes_of = |interval: usize| -> Result<Vec<Vec3>, String> {
1141 let inner = dense.centres.get(interval).ok_or_else(|| format!("interval {interval}: no dense centre row"))?;
1142 if dense.subdivisions != OPEN_WALL_CENTRES || inner.len() + 1 != OPEN_WALL_CENTRES {
1143 return Err(format!("interval {interval}: dense row {} centres at {} subdivisions", inner.len(), dense.subdivisions));
1144 }
1145 let mut centres = Vec::with_capacity(OPEN_WALL_CENTRES + 1);
1146 centres.push(stations[interval].center);
1147 for (j, centre) in inner.iter().enumerate() {
1148 centres.push(centre.ok_or_else(|| format!("interval {interval}: exact centre {} of {OPEN_WALL_CENTRES} unsolved", j + 1))?);
1149 }
1150 centres.push(stations[interval + 1].center);
1151 Ok(centres)
1152 };
1153 let half = OPEN_WALL_CENTRES / 2;
1154 let junction = |left: usize| -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
1161 if dense.station_ts.len() != parameters.len() {
1162 return Err(format!("interval {left}: junction without the native station parameters"));
1163 }
1164 junction_stencil(&nodes_of(left)?, &nodes_of(left + 1)?, &dense.station_ts, left)
1165 };
1166 overlapped
1167 .into_iter()
1168 .map(|interval| -> Result<f64, String> {
1169 let centres = nodes_of(interval)?;
1170 let (from, to) = (parameters[interval], parameters[interval + 1]);
1171 let inside = |u: f64| u >= window[0] && u <= window[1];
1172 let mut probes: Vec<(f64, usize, Option<(Vec<Vec3>, Option<Vec<f64>>)>)> = (2..OPEN_WALL_CENTRES)
1175 .step_by(2)
1176 .map(|step| (from + (to - from) * step as f64 / OPEN_WALL_CENTRES as f64, step))
1177 .filter(|(u, _)| inside(*u))
1178 .map(|(u, step)| (u, step, None))
1179 .collect();
1180 for end in window {
1181 if end > from && end < to {
1182 let fraction = (end - from) / (to - from);
1183 let nearest = 2 * ((fraction * OPEN_WALL_CENTRES as f64 / 2.0).round() as usize);
1184 #[cfg(not(test))]
1185 let junction_off = false;
1186 if junction_off {
1187 probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1188 } else if nearest < 2 && interval > 0 {
1189 probes.push((end, half, Some(junction(interval - 1)?)));
1190 } else if nearest > OPEN_WALL_CENTRES - 2 && interval + 1 < intervals {
1191 probes.push((end, half, Some(junction(interval)?)));
1192 } else {
1193 probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1194 }
1195 }
1196 }
1197 if probes.is_empty() {
1198 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1199 }
1200 let mut declared = 0.0_f64;
1201 for (u, step, stencil) in probes {
1202 let (nodes, abscissae, what) = match &stencil {
1203 Some((nodes, None)) => (nodes, None, "junction"),
1204 Some((nodes, Some(ts))) => (nodes, Some(ts), "native-abscissa junction"),
1205 None => (¢res, None, "interval"),
1206 };
1207 for k in 1..OPEN_WALL_SECTIONS {
1208 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1209 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1210 let read = if stencil.is_none() {
1215 crate::blend::chain::declared_wall_probe_located_why(point, nodes, step, (u - from) / (to - from), radius)
1216 } else {
1217 read_on_stencil(point, nodes, abscissae.map(|ts| ts.as_slice()), step, radius)
1218 };
1219 let (reading, uncertainty) = read.map_err(|why| {
1220 format!(
1221 "interval {interval}: declared reader declined probe node {step} of the {what} stencil, section {k}/{OPEN_WALL_SECTIONS} \
1222 (u {u:.17}): {why}"
1223 )
1224 })?;
1225 declared = declared.max(reading + uncertainty);
1226 }
1227 }
1228 if declared.is_finite() { Ok(declared) } else { Err(format!("interval {interval}: non-finite reading {declared}")) }
1229 })
1230 .collect()
1231}
1232
1233
1234pub(in crate::blend) fn fresh_id_source(solid: &BrepSolid) -> impl FnMut() -> u64 {
1239 let mut next_id = solid
1240 .vertices
1241 .iter()
1242 .map(|vertex| vertex.id)
1243 .chain(solid.edges.iter().map(|edge| edge.id))
1244 .chain(
1245 solid
1246 .shells
1247 .iter()
1248 .flat_map(|shell| &shell.faces)
1249 .flat_map(|face| {
1250 face.loops
1251 .iter()
1252 .map(|loop_record| loop_record.id)
1253 .chain(face.loops.iter().flat_map(|loop_record| {
1254 loop_record.coedges.iter().map(|coedge| coedge.id)
1255 }))
1256 .chain(std::iter::once(face.id))
1257 }),
1258 )
1259 .max()
1260 .unwrap_or(0)
1261 + 1;
1262 move || {
1263 let id = next_id;
1264 next_id += 1;
1265 id
1266 }
1267}
1268
1269
1270pub(in crate::blend) fn march_trim_stands(read: &Result<(f64, f64, usize, f64), KernelRefusal>, floor: f64) -> bool {
1279 matches!(read, Ok((miss, standoff, _, _)) if *miss + *standoff <= floor)
1280}
1281
1282fn exact_planar_trim(rail: &NurbsCurve, surface: &NurbsSurface, d0: f64, d1: f64) -> Result<Option<NurbsCurve>, KernelRefusal> {
1292 if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || !(d1 > d0) {
1293 return Ok(None);
1294 }
1295 let feet_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.feet_ms");
1296 let mut control = Vec::with_capacity(2);
1297 for t in [d0, d1] {
1298 let point = rail.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
1299 let foot = crate::project_point_to_surface(surface, point).or_refuse(KernelStage::Refine, "project")?;
1300 control.push(crate::Vec4::from_point(crate::Vec3::new(foot.u, foot.v, 0.0), 1.0));
1301 }
1302 let line = NurbsCurve::new(1, vec![d0, d0, d1, d1], control).or_refuse(KernelStage::Refine, "new")?;
1303 feet_timing.stop();
1304 let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1305 let read = match affine_reads_on().then(|| affine_trim_read(rail, surface, &line, d0, d1)).flatten() {
1306 Some(read) => {
1307 crate::blend::record_count("count.read.affine_trims", 1);
1308 Ok(read)
1309 }
1310 None => crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &line, d0, d1),
1311 };
1312 read_timing.stop();
1313 Ok(march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE).then_some(line))
1314}
1315
1316fn affine_trim_read(rail: &NurbsCurve, surface: &NurbsSurface, pcurve: &NurbsCurve, d0: f64, d1: f64) -> Option<(f64, f64, usize, f64)> {
1324 if !affine_curve(rail) || !affine_curve(pcurve) {
1325 return None;
1326 }
1327 let (origin, normal) = affine_plane(surface)?;
1328 let (mut miss, mut standoff, mut worst) = (0.0f64, 0.0f64, d0);
1329 for t in [d0, d1] {
1330 let point = rail.evaluate(t).ok()?;
1331 let foot = point.sub(normal.scale(point.sub(origin).dot(normal)));
1332 let uv = pcurve.evaluate(t).ok()?;
1333 let image = crate::blend::track_fit::carrier_point(surface, uv.x, uv.y).ok()?;
1334 let (residual, off) = (image.sub(foot).length(), point.sub(foot).length());
1335 if !(residual.is_finite() && off.is_finite()) {
1336 return None;
1337 }
1338 if residual > miss {
1339 (miss, worst) = (residual, t);
1340 }
1341 standoff = standoff.max(off);
1342 }
1343 Some((miss, standoff, 2, worst))
1344}
1345
1346pub(in crate::blend) fn build_open_surgery(
1358 solid: &BrepSolid,
1359 result: &mut BrepSolid,
1360 take_id: &mut dyn FnMut() -> u64,
1361 edge: &EdgeRecord,
1362 first: &BlendMate,
1363 second: &BlendMate,
1364 rows: FittedRows,
1365 ends: [EndPlan; 2],
1366 name: Option<&str>,
1367) -> Result<SewnStripe, KernelRefusal> {
1368 build_open_surgery_with_tail(solid, result, take_id, edge, first, second, rows, ends, name, [None, None])
1369}
1370
1371#[allow(clippy::too_many_arguments)]
1381pub(in crate::blend) fn build_open_surgery_with_tail(
1382 solid: &BrepSolid,
1383 result: &mut BrepSolid,
1384 take_id: &mut dyn FnMut() -> u64,
1385 edge: &EdgeRecord,
1386 first: &BlendMate,
1387 second: &BlendMate,
1388 rows: FittedRows,
1389 ends: [EndPlan; 2],
1390 name: Option<&str>,
1391 tail: [Option<crate::blend::rollover::TailSplice<'_>>; 2],
1392) -> Result<SewnStripe, KernelRefusal> {
1393 let [start_end, finish_end] = ends;
1394
1395 let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, KernelRefusal> {
1402 let [low, high] = row.domain().or_refuse(KernelStage::Refine, "domain")?;
1403 let tail = if a == low { row.clone() } else { row.split(a).or_refuse(KernelStage::Refine, "split")?.1 };
1404 if b == high {
1405 return Ok(tail);
1406 }
1407 let (middle, _) = tail.split(b).or_refuse(KernelStage::Refine, "split")?;
1408 Ok(middle)
1409 };
1410 let kind = |plan: &EndPlan| match plan {
1411 EndPlan::Free(_) => "free",
1412 EndPlan::Corner(_) => "corner",
1413 EndPlan::Miter(_) => "miter",
1414 EndPlan::Cap(_) => "cap",
1415 };
1416 let describe_trim = |error: KernelRefusal| {
1417 error.with_message(|error| {
1418 format!(
1419 "{error} (edge {} rows trimmed for a {} start at cr {:.6}/cs {:.6} and a {} finish \
1420 at cr {:.6}/cs {:.6})",
1421 edge.id,
1422 kind(&start_end),
1423 start_end.cr_parameter(),
1424 start_end.cs_parameter(),
1425 kind(&finish_end),
1426 finish_end.cr_parameter(),
1427 finish_end.cs_parameter()
1428 )
1429 })
1430 };
1431 let band = consumed_band(solid);
1441 let exact_trim = crate::blend::exact_trim_on();
1442 let collapses = |row: &NurbsCurve, a: f64, b: f64| -> Result<bool, KernelRefusal> {
1443 let from = row.evaluate(a).or_refuse(KernelStage::Refine, "evaluate")?;
1444 Ok(row.evaluate(b).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band
1445 && row.evaluate(0.5 * (a + b)).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band)
1446 };
1447 let cr_collapsed = collapses(&rows.cr, start_end.cr_parameter(), finish_end.cr_parameter())?;
1448 let cs_collapsed = collapses(&rows.cs, start_end.cs_parameter(), finish_end.cs_parameter())?;
1449 if cr_collapsed || cs_collapsed {
1450 let unsupported = if start_end.free().is_some() && finish_end.free().is_some() {
1451 Some("neither end is a corner")
1452 } else if first.face.id == second.face.id {
1453 Some("both rails lie on one face")
1454 } else if matches!(start_end, EndPlan::Cap(_)) || matches!(finish_end, EndPlan::Cap(_)) {
1455 Some("a capped end has no corner to share the point with")
1456 } else {
1457 None
1458 };
1459 if let Some(reason) = unsupported {
1460 return Err(KernelRefusal::unsupported(KernelStage::Sew, super::RAIL_COLLAPSE_WHAT, format!(
1461 "{RAIL_COLLAPSE_UNSUPPORTED} edge {}'s rail shrinks to a point and {reason}",
1462 edge.id
1463 )));
1464 }
1465 }
1466 let trim_rail = |collapsed: bool,
1480 row: &NurbsCurve,
1481 pcurve: &NurbsCurve,
1482 surface: &NurbsSurface,
1483 a: f64,
1484 b: f64|
1485 -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1486 if collapsed {
1487 return Ok(None);
1488 }
1489 let rows_timing = crate::blend::BlendTimer::start("timing.surgery.trim.rows_ms");
1490 let rail = trim_row(row, a, b).map_err(describe_trim)?;
1491 let interpolated = trim_row(pcurve, a, b)?;
1492 rows_timing.stop();
1493 {
1494 let overrun_timing = crate::blend::BlendTimer::start("timing.surgery.trim.overrun_ms");
1495 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1496 let overrun = crate::blend::planar_chart_overrun(surface, &|t| rail.evaluate(t), d0, d1)?;
1497 overrun_timing.stop();
1498 if overrun.is_some_and(|excursion| excursion > band) {
1499 return Ok(Some((rail, interpolated)));
1500 }
1501 }
1502 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1503 if exact_trim && rows.exact_extrusion {
1507 if let Some(line) = exact_planar_trim(&rail, surface, d0, d1)? {
1508 crate::blend::record_count("count.surgery.exact_trim", 1);
1509 return Ok(Some((rail, line)));
1510 }
1511 }
1512 if rows.revolution_axis.is_some() {
1517 if exact_revolution_reads_on() {
1522 let cert_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.cert_ms");
1523 let bound = exact_revolution_read_bound(row, surface, pcurve);
1524 cert_timing.stop();
1525 if bound.is_some_and(|bound| 3.0 * bound <= crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
1526 crate::blend::record_count("count.surgery.exact_revolution_trim_certified", 1);
1527 return Ok(Some((rail, interpolated)));
1528 }
1529 }
1530 let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1531 let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &interpolated, d0, d1);
1532 read_timing.stop();
1533 if march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
1534 crate::blend::record_count("count.surgery.exact_revolution_trim", 1);
1535 return Ok(Some((rail, interpolated)));
1536 }
1537 crate::blend::record_count("count.surgery.exact_revolution_trim_declined", 1);
1538 }
1539 let mut march_feet = crate::blend::track_fit::FootMemo::default();
1548 let mut refit_feet = crate::blend::track_fit::FootMemo::default();
1549 let march_read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.march_ms");
1550 let march_read = crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &interpolated, d0, d1, &mut march_feet);
1551 march_read_timing.stop();
1552 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1553 let declined = |why: KernelRefusal| -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1554 let stands = march_trim_stands(&march_read, floor);
1557 if stands {
1558 Ok(Some((rail.clone(), interpolated.clone())))
1559 } else {
1560 Err(why)
1561 }
1562 };
1563 let refit_timing = crate::blend::BlendTimer::start("timing.surgery.trim.refit_ms");
1564 let refit = super::closed::refit_closed_support_pcurve_read(&rail, &interpolated, surface, &mut refit_feet);
1565 refit_timing.stop();
1566 let _reads_timing = crate::blend::BlendTimer::start("timing.surgery.trim.reads_ms");
1567 match refit {
1568 Ok((fitted, refit_read)) => {
1569 let (march_standoff, refit_standoff) = match (&march_read, Ok::<_, KernelRefusal>(refit_read)) {
1586 (_, Err(error)) => return declined(error),
1587 (Err(error), Ok(_)) => {
1588 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch_unread", format!(
1589 "blend: a support trim's refit cannot be shown to stand on its rail's branch: the march's own \
1590 trim is unreadable ({})",
1591 error.message
1592 )));
1593 }
1594 (Ok((_, march_standoff, _, _)), Ok((_, refit_standoff, _, _))) => (*march_standoff, refit_standoff),
1595 };
1596 let excess_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.excess_ms");
1597 let excess = crate::blend::track_fit::standoff_excess_memo(
1598 surface, &|t| rail.evaluate(t), &interpolated, &fitted, d0, d1, &mut march_feet, &mut refit_feet,
1599 );
1600 excess_timing.stop();
1601 let excess = match excess {
1602 Ok(excess) => excess,
1603 Err(error) => return declined(error),
1604 };
1605 if !(excess <= floor) {
1606 return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch", format!(
1607 "blend: a support trim's refit stands up to {excess:.3e} farther off its rail than the march's own trim \
1608 at the same rail parameter (largest standoffs {refit_standoff:.3e} and {march_standoff:.3e}): it took \
1609 another branch of the carrier"
1610 )));
1611 }
1612 let nearest_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.nearest_ms");
1620 let absolute = crate::blend::track_fit::standoff_over_nearest_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut refit_feet);
1621 nearest_timing.stop();
1622 let absolute = match absolute {
1623 Ok(absolute) => absolute,
1624 Err(error) => return declined(error),
1625 };
1626 if !(absolute <= floor) {
1627 return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_off_rail", format!(
1628 "blend: a support trim's refit stands up to {absolute:.3e} farther off its rail than the rail stands \
1629 off its carrier: its foot is not the rail's nearest point"
1630 )));
1631 }
1632 Ok(Some((rail, fitted)))
1633 }
1634 Err(error) => declined(error),
1635 }
1636 };
1637 let trim_timing = crate::blend::BlendTimer::start("timing.surgery.trim_rails_ms");
1638 let cr_rail = trim_rail(
1639 cr_collapsed,
1640 &rows.cr,
1641 &rows.cr_pcurve,
1642 &first.face.surface,
1643 start_end.cr_parameter(),
1644 finish_end.cr_parameter(),
1645 )?;
1646 let cs_rail = trim_rail(
1647 cs_collapsed,
1648 &rows.cs,
1649 &rows.cs_pcurve,
1650 &second.face.surface,
1651 start_end.cs_parameter(),
1652 finish_end.cs_parameter(),
1653 )?;
1654 trim_timing.stop();
1655 let cr_domain = match &cr_rail {
1657 Some((cr, _)) => cr.domain().or_refuse(KernelStage::Refine, "domain")?,
1658 None => [start_end.cr_parameter(), finish_end.cr_parameter()],
1659 };
1660 let cs_domain = match &cs_rail {
1661 Some((cs, _)) => cs.domain().or_refuse(KernelStage::Refine, "domain")?,
1662 None => [start_end.cs_parameter(), finish_end.cs_parameter()],
1663 };
1664 let cr_at = |parameter: f64| match &cr_rail {
1666 Some((cr, _)) => cr.evaluate(parameter),
1667 None => rows.cr.evaluate(parameter),
1668 };
1669 let cs_at = |parameter: f64| match &cs_rail {
1670 Some((cs, _)) => cs.evaluate(parameter),
1671 None => rows.cs.evaluate(parameter),
1672 };
1673 let old_start = edge.start_vertex_id;
1674 let old_finish = edge.end_vertex_id;
1675
1676 let classify =
1687 |boundary_id: u64, boundary_param: f64, corner: u64| -> Result<RimResolution, KernelRefusal> {
1688 let boundary = result
1689 .edges
1690 .iter()
1691 .find(|candidate| candidate.id == boundary_id)
1692 .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_boundary_edge", "blend: end boundary edge missing during surgery"))?;
1693 let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
1694 let far_vertex = if boundary.start_vertex_id == corner {
1695 Some((boundary.end_vertex_id, boundary.t1, boundary.t0))
1696 } else if boundary.end_vertex_id == corner {
1697 Some((boundary.start_vertex_id, boundary.t0, boundary.t1))
1698 } else {
1699 None
1700 };
1701 if let Some((far_id, far_t, near_t)) = far_vertex {
1702 let far_point = result
1713 .vertices
1714 .iter()
1715 .find(|candidate| candidate.id == far_id)
1716 .map(|candidate| candidate.point)
1717 .ok_or(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"))?;
1718 let crossing = boundary.curve.evaluate_extended(boundary_param).or_refuse(KernelStage::Refine, "evaluate_extended")?;
1719 let _ = span;
1720 let past_far = crossing.sub(far_point).length();
1721 if past_far <= band {
1722 if !result
1724 .vertices
1725 .iter()
1726 .any(|candidate| candidate.id == far_id)
1727 {
1728 return Err(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"));
1729 }
1730 return Ok(RimResolution::Consumed(far_id));
1731 }
1732 if (boundary_param - far_t) * (far_t - near_t) > 0.0 {
1745 return Err(KernelRefusal::unsupported(KernelStage::Sew, "blend_wider_than_face", format!(
1746 "{BLEND_WIDER_THAN_FACE} its rail crosses edge {boundary_id} {past_far:.3e} \
1747 past that edge's far vertex, and a rail within {band:.3e} of it is the \
1748 most this surgery snaps onto the edge"
1749 )));
1750 }
1751 }
1752 Ok(RimResolution::Fresh)
1753 };
1754 let resolve_side = |plan: &EndPlan,
1758 side_first: bool,
1759 corner: u64|
1760 -> Result<RimResolution, KernelRefusal> {
1761 if let Some(vertex) = plan.planned_rim(side_first) {
1762 return Ok(RimResolution::Corner(vertex));
1763 }
1764 match plan {
1765 EndPlan::Free(free) => classify(
1766 if side_first {
1767 free.first_edge_id
1768 } else {
1769 free.second_edge_id
1770 },
1771 if side_first {
1772 free.first_edge_parameter
1773 } else {
1774 free.second_edge_parameter
1775 },
1776 corner,
1777 ),
1778 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => {
1779 Err(KernelRefusal::internal(KernelStage::Sew, "rim_vertices", "blend: planned end without rim vertices"))
1780 }
1781 }
1782 };
1783 let crossings_timing = crate::blend::BlendTimer::start("timing.surgery.crossings_ms");
1784 let start_first = resolve_side(&start_end, true, old_start)?;
1785 let start_second = resolve_side(&start_end, false, old_start)?;
1786 let finish_first = resolve_side(&finish_end, true, old_finish)?;
1787 let finish_second = resolve_side(&finish_end, false, old_finish)?;
1788 crossings_timing.stop();
1789 let _topology_timing = crate::blend::BlendTimer::start("timing.surgery.topology_ms");
1790
1791 let first_use_forward = first.coedge.forward;
1794 let blend_cr_forward = !first_use_forward;
1795
1796 let (sew_first, sew_second) = match (start_end.free(), finish_end.free(), &cr_rail, &cs_rail) {
1811 (Some(start_free), Some(finish_free), Some((cr, _)), Some((cs, _))) => (
1812 detect_row_coincidence(
1813 solid,
1814 first,
1815 edge.id,
1816 start_free.first_edge_id,
1817 finish_free.first_edge_id,
1818 start_first.is_consumed(),
1819 start_first.existing().unwrap_or(0),
1820 finish_first.is_consumed(),
1821 finish_first.existing().unwrap_or(0),
1822 cr,
1823 blend_cr_forward,
1824 band,
1825 )?,
1826 detect_row_coincidence(
1827 solid,
1828 second,
1829 edge.id,
1830 start_free.second_edge_id,
1831 finish_free.second_edge_id,
1832 start_second.is_consumed(),
1833 start_second.existing().unwrap_or(0),
1834 finish_second.is_consumed(),
1835 finish_second.existing().unwrap_or(0),
1836 cs,
1837 !blend_cr_forward,
1838 band,
1839 )?,
1840 ),
1841 _ => (None, None),
1842 };
1843 let (sew_first, sew_second): (Option<RowSew>, Option<RowSew>) =
1847 if first.face.id == second.face.id {
1848 (None, None)
1849 } else {
1850 (sew_first, sew_second)
1851 };
1852
1853 let mut resolve = |rim: &RimResolution, point: Result<Vec3, String>| -> Result<u64, KernelRefusal> {
1856 match rim.existing() {
1857 Some(id) => Ok(id),
1858 None => {
1859 let id = take_id();
1860 result.vertices.push(VertexRecord { id, point: point.or_refuse(KernelStage::Refine, "point")? });
1861 Ok(id)
1862 }
1863 }
1864 };
1865 let w1a = resolve(&start_first, cr_at(cr_domain[0]))?;
1866 let w1b = resolve(&finish_first, cr_at(cr_domain[1]))?;
1867 let w2a = resolve(&start_second, cs_at(cs_domain[0]))?;
1868 let w2b = resolve(&finish_second, cs_at(cs_domain[1]))?;
1869 let mut snap = [&sew_first, &sew_second]
1872 .into_iter()
1873 .flatten()
1874 .fold(0.0f64, |worst, sew| worst.max(sew.deviation));
1875 for (rim, vertex, row_end) in [
1876 (&start_first, w1a, cr_at(cr_domain[0]).or_refuse(KernelStage::Refine, "cr_at")?),
1877 (&finish_first, w1b, cr_at(cr_domain[1]).or_refuse(KernelStage::Refine, "cr_at")?),
1878 (&start_second, w2a, cs_at(cs_domain[0]).or_refuse(KernelStage::Refine, "cs_at")?),
1879 (&finish_second, w2b, cs_at(cs_domain[1]).or_refuse(KernelStage::Refine, "cs_at")?),
1880 ] {
1881 if !rim.is_consumed() {
1882 continue;
1883 }
1884 let point = result
1885 .vertices
1886 .iter()
1887 .find(|candidate| candidate.id == vertex)
1888 .ok_or(KernelRefusal::internal(KernelStage::Sew, "rim_vertex", "blend: consumed rim vertex missing"))?
1889 .point;
1890 snap = snap.max(row_end.sub(point).length());
1891 }
1892
1893 let cr_edge_id = match (&sew_first, &cr_rail) {
1898 (Some(sew), _) => Some(sew.edge_id),
1899 (None, None) => None,
1900 (None, Some((cr, _))) => {
1901 let id = take_id();
1902 result.edges.push(EdgeRecord {
1903 id,
1904 curve: cr.clone(),
1905 t0: cr_domain[0],
1906 t1: cr_domain[1],
1907 start_vertex_id: w1a,
1908 end_vertex_id: w1b,
1909 degenerate: false,
1910 name: None,
1911 });
1912 Some(id)
1913 }
1914 };
1915 let cs_edge_id = match (&sew_second, &cs_rail) {
1916 (Some(sew), _) => Some(sew.edge_id),
1917 (None, None) => None,
1918 (None, Some((cs, _))) => {
1919 let id = take_id();
1920 result.edges.push(EdgeRecord {
1921 id,
1922 curve: cs.clone(),
1923 t0: cs_domain[0],
1924 t1: cs_domain[1],
1925 start_vertex_id: w2a,
1926 end_vertex_id: w2b,
1927 degenerate: false,
1928 name: None,
1929 });
1930 Some(id)
1931 }
1932 };
1933 let mut commit_split = |plan: &EndPlan, second_rim: u64, corner: u64| -> Result<Option<super::split_floor::SplitCommit>, KernelRefusal> {
1938 match plan.free().and_then(|free| free.split.as_ref()) {
1939 Some(split) => super::split_floor::commit_split_end(result, &mut *take_id, split, second_rim, corner).map(Some),
1940 None => Ok(None),
1941 }
1942 };
1943 let start_split = commit_split(&start_end, w2a, old_start)?;
1944 let finish_split = commit_split(&finish_end, w2b, old_finish)?;
1945 let mut commit_end_edge = |plan: &EndPlan,
1946 first_rim: u64,
1947 second_rim: u64|
1948 -> Result<Option<u64>, KernelRefusal> {
1949 match plan {
1950 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
1951 EndPlan::Free(free) => {
1952 let id = take_id();
1953 let domain = free.transverse_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1954 result.edges.push(EdgeRecord {
1955 id,
1956 curve: free.transverse_curve.clone(),
1957 t0: domain[0],
1958 t1: domain[1],
1959 start_vertex_id: first_rim,
1960 end_vertex_id: second_rim,
1961 degenerate: false,
1962 name: None,
1963 });
1964 Ok(Some(id))
1965 }
1966 }
1967 };
1968 let transverse_a_id = commit_end_edge(&start_end, w1a, start_split.as_ref().map_or(w2a, |split| split.q1_vertex))?;
1969 let transverse_b_id = commit_end_edge(&finish_end, w1b, finish_split.as_ref().map_or(w2b, |split| split.q1_vertex))?;
1970
1971 for (mate, rail, sewn) in [
1978 (first, cr_edge_id.zip(cr_rail.as_ref()), sew_first.is_some()),
1979 (second, cs_edge_id.zip(cs_rail.as_ref()), sew_second.is_some()),
1980 ] {
1981 if sewn {
1982 continue;
1983 }
1984 let face = result
1985 .shells
1986 .iter_mut()
1987 .flat_map(|shell| &mut shell.faces)
1988 .find(|face| face.id == mate.face.id)
1989 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1990 let loop_record = &mut face.loops[mate.loop_index];
1991 let position = loop_record
1992 .coedges
1993 .iter()
1994 .position(|coedge| coedge.edge_id == edge.id)
1995 .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
1996 let Some((new_edge_id, (_, pcurve_forward))) = rail else {
1997 loop_record.coedges.remove(position);
1998 continue;
1999 };
2000 let old_forward = loop_record.coedges[position].forward;
2001 loop_record.coedges[position] = CoedgeRecord {
2002 id: loop_record.coedges[position].id,
2003 edge_id: new_edge_id,
2004 forward: old_forward,
2005 pcurve: if old_forward {
2006 pcurve_forward.clone()
2007 } else {
2008 pcurve_forward.reversed().or_refuse(KernelStage::Refine, "reversed")?
2009 },
2010 };
2011 }
2012
2013 for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
2018 let EndPlan::Cap(cap) = plan else {
2019 continue;
2020 };
2021 for (mate, rail_edge_id, leg, rim) in [
2022 (first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
2023 (second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
2024 ] {
2025 let rail_edge_id = rail_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "end_rail", "blend: a capped end lost its rail"))?;
2027 let face = result
2028 .shells
2029 .iter_mut()
2030 .flat_map(|shell| &mut shell.faces)
2031 .find(|face| face.id == mate.face.id)
2032 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during cap splice"))?;
2033 let loop_record = &mut face.loops[mate.loop_index];
2034 let count = loop_record.coedges.len();
2035 let rail_at = loop_record
2036 .coedges
2037 .iter()
2038 .position(|coedge| coedge.edge_id == rail_edge_id)
2039 .ok_or(KernelRefusal::internal(KernelStage::Sew, "rail_coedge", "blend: rail coedge lost during cap splice"))?;
2040 let rail_forward = loop_record.coedges[rail_at].forward;
2046 let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
2047 let (insert_at, forward, pcurve) = if end_is_traversal_end {
2048 (rail_at + 1, true, leg.1.clone())
2050 } else {
2051 (rail_at, false, leg.1.reversed().or_refuse(KernelStage::Refine, "reversed")?)
2053 };
2054 let _ = (count, rim);
2055 loop_record.coedges.insert(
2056 insert_at,
2057 CoedgeRecord {
2058 id: take_id(),
2059 edge_id: leg.0,
2060 forward,
2061 pcurve,
2062 },
2063 );
2064 }
2065 }
2066
2067 let mut consumed_edges: Vec<u64> = Vec::new();
2073 let start_free = start_end.free();
2074 let finish_free = finish_end.free();
2075 for (mate, sewn, sides) in [
2076 (
2077 first,
2078 sew_first.is_some(),
2079 [
2080 (&start_first, start_free.map(|free| free.first_edge_id)),
2081 (&finish_first, finish_free.map(|free| free.first_edge_id)),
2082 ],
2083 ),
2084 (
2085 second,
2086 sew_second.is_some(),
2087 [
2088 (&start_second, start_free.map(|free| free.second_edge_id)),
2089 (&finish_second, finish_free.map(|free| free.second_edge_id)),
2090 ],
2091 ),
2092 ] {
2093 for (cross, boundary_id) in sides {
2094 if !cross.is_consumed() {
2095 continue;
2096 }
2097 let Some(boundary_id) = boundary_id else {
2098 continue;
2099 };
2100 if !sewn {
2101 let face = result
2102 .shells
2103 .iter_mut()
2104 .flat_map(|shell| &mut shell.faces)
2105 .find(|face| face.id == mate.face.id)
2106 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
2107 face.loops[mate.loop_index]
2108 .coedges
2109 .retain(|coedge| coedge.edge_id != boundary_id);
2110 }
2111 if !consumed_edges.contains(&boundary_id) {
2112 consumed_edges.push(boundary_id);
2113 }
2114 }
2115 }
2116 for sew in [&sew_first, &sew_second].into_iter().flatten() {
2118 for id in &sew.collapsed_edges {
2119 if !consumed_edges.contains(id) {
2120 consumed_edges.push(*id);
2121 }
2122 }
2123 }
2124
2125 let mut end_plans = Vec::with_capacity(2);
2134 for (end, transverse_id, corner, first_consumed, second_consumed) in [
2135 (
2136 start_end.free(),
2137 transverse_a_id,
2138 old_start,
2139 start_first.is_consumed(),
2140 start_second.is_consumed(),
2141 ),
2142 (
2143 finish_end.free(),
2144 transverse_b_id,
2145 old_finish,
2146 finish_first.is_consumed(),
2147 finish_second.is_consumed(),
2148 ),
2149 ] {
2150 let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
2151 continue;
2152 };
2153 if end.split.is_some() {
2154 continue;
2155 }
2156 let face = result
2157 .shells
2158 .iter()
2159 .flat_map(|shell| &shell.faces)
2160 .find(|face| face.id == end.end_face_id)
2161 .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_face", "blend: end face lost during surgery"))?;
2162 let Some(located) = locate_end_corner(
2165 &result,
2166 face,
2167 corner,
2168 end.first_edge_id,
2169 end.second_edge_id,
2170 ) else {
2171 return Err(KernelRefusal::internal(KernelStage::Sew, "end_face_corner", "blend: end-face corner (adjacent boundary coedges) not found"));
2172 };
2173 for pole in &located.pole_edge_ids {
2176 if !consumed_edges.contains(pole) {
2177 consumed_edges.push(*pole);
2178 }
2179 }
2180 end_plans.push((
2181 end.end_face_id,
2182 transverse_id,
2183 end.transverse_end_pcurve.clone(),
2184 located,
2185 first_consumed,
2186 second_consumed,
2187 ));
2188 }
2189
2190 let split_corners = |end: Option<&EndSurgery>, corner: u64, result: &BrepSolid| -> Result<Option<[super::support::EndCorner; 2]>, KernelRefusal> {
2191 match end.and_then(|end| end.split.as_ref().map(|split| (end, split))) {
2192 Some((end, split)) => super::split_floor::locate_split_corners(result, end, split, corner).map(Some),
2193 None => Ok(None),
2194 }
2195 };
2196 let start_split_corners = split_corners(start_free, old_start, result)?;
2197 let finish_split_corners = split_corners(finish_free, old_finish, result)?;
2198 if (start_split_corners.is_some() && (start_first.is_consumed() || start_second.is_consumed()))
2199 || (finish_split_corners.is_some() && (finish_first.is_consumed() || finish_second.is_consumed()))
2200 {
2201 return Err(KernelRefusal::unsupported(KernelStage::Sew, "split_floor_consumed", "blend: a two-face end whose rail consumes its boundary"));
2202 }
2203
2204 for (rim_resolution, boundary, corner, rim) in [
2207 (
2208 &start_first,
2209 start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
2210 old_start,
2211 w1a,
2212 ),
2213 (
2214 &start_second,
2215 start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
2216 old_start,
2217 w2a,
2218 ),
2219 (
2220 &finish_first,
2221 finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
2222 old_finish,
2223 w1b,
2224 ),
2225 (
2226 &finish_second,
2227 finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
2228 old_finish,
2229 w2b,
2230 ),
2231 ] {
2232 if !rim_resolution.trims_boundary() {
2233 continue;
2234 }
2235 let Some((boundary_id, boundary_param)) = boundary else {
2236 continue;
2237 };
2238 trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
2239 }
2240
2241 for (end_face, transverse_id, transverse_end_pcurve, located, first_consumed, second_consumed) in
2244 end_plans
2245 {
2246 let forward = located.x_is_first;
2247 let pcurve = if forward {
2248 transverse_end_pcurve
2249 } else {
2250 transverse_end_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
2251 };
2252 let (x_consumed, y_consumed) = if located.x_is_first {
2253 (first_consumed, second_consumed)
2254 } else {
2255 (second_consumed, first_consumed)
2256 };
2257 let transverse_coedge = CoedgeRecord {
2258 id: take_id(),
2259 edge_id: transverse_id,
2260 forward,
2261 pcurve,
2262 };
2263 splice_end_corner(
2264 result,
2265 end_face,
2266 &located,
2267 transverse_coedge,
2268 x_consumed,
2269 y_consumed,
2270 )?;
2271 }
2272
2273 let (mid_u, station_uv) = match (&cr_rail, &cs_rail) {
2278 (Some((_, cr_pcurve)), _) => {
2279 let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
2280 (mid_u, cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2281 }
2282 (None, Some(_)) => {
2283 let mid_u = (cs_domain[0] + cs_domain[1]) * 0.5;
2284 (mid_u, rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2285 }
2286 (None, None) => (cr_domain[0], rows.cr_pcurve.evaluate(cr_domain[0]).or_refuse(KernelStage::Refine, "evaluate")?),
2287 };
2288 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
2289 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
2290 let out1 = if first.face.same_sense {
2291 n1
2292 } else {
2293 n1.scale(-1.0)
2294 };
2295 let same_sense = blend_normal.dot(out1) >= 0.0;
2296 let loop_id = take_id();
2297 let cr_forward = sew_first
2305 .as_ref()
2306 .map_or(blend_cr_forward, |sew| sew.forward);
2307 let cs_forward = sew_second
2308 .as_ref()
2309 .map_or(!blend_cr_forward, |sew| sew.forward);
2310 let mut end_slot = |plan: &EndPlan,
2320 free_edge_id: Option<u64>,
2321 natural_forward: bool|
2322 -> Result<Vec<CoedgeRecord>, KernelRefusal> {
2323 match plan {
2324 EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
2325 id: take_id(),
2326 edge_id: end.arc_edge_id,
2327 forward: true,
2328 pcurve: end.arc_blend_pcurve.clone(),
2329 }]),
2330 EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
2331 id: take_id(),
2332 edge_id: end.arc_edge_id,
2333 forward: true,
2334 pcurve: end.arc_blend_pcurve.clone(),
2335 }]),
2336 EndPlan::Miter(end) => end
2337 .edges
2338 .iter()
2339 .map(|(edge_id, forward, pcurve)| {
2340 Ok(CoedgeRecord {
2341 id: take_id(),
2342 edge_id: *edge_id,
2343 forward: *forward,
2344 pcurve: pcurve.clone(),
2345 })
2346 })
2347 .collect(),
2348 EndPlan::Free(free) => {
2349 let edge_id =
2350 free_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "transverse_edge", "blend: free end without its transverse edge"))?;
2351 Ok(vec![if natural_forward {
2352 CoedgeRecord {
2353 id: take_id(),
2354 edge_id,
2355 forward: true,
2356 pcurve: free.transverse_blend_pcurve.clone(),
2357 }
2358 } else {
2359 CoedgeRecord {
2360 id: take_id(),
2361 edge_id,
2362 forward: false,
2363 pcurve: free.transverse_blend_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?,
2364 }
2365 }])
2366 }
2367 }
2368 };
2369 let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
2370 let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
2371 let start_slot = match (start_free, &start_split, transverse_a_id) {
2372 (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)?,
2373 _ => start_slot,
2374 };
2375 let finish_slot = match (finish_free, &finish_split, transverse_b_id) {
2376 (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)?,
2377 _ => finish_slot,
2378 };
2379 for (end, commit, id, corners, corner, forward) in [
2380 (start_free, &start_split, transverse_a_id, &start_split_corners, old_start, !blend_cr_forward),
2381 (finish_free, &finish_split, transverse_b_id, &finish_split_corners, old_finish, blend_cr_forward),
2382 ] {
2383 if let (Some(end), Some(commit), Some(id), Some(corners)) = (end, commit, id, corners) {
2384 super::split_floor::finish_split_end(result, &mut *take_id, end, end.split.as_ref().expect("planned"), commit, id, corners, corner, forward, name)?;
2385 }
2386 }
2387 let coedges = if blend_cr_forward {
2390 let mut coedges = Vec::new();
2391 if let Some(cr_edge_id) = cr_edge_id {
2392 coedges.push(CoedgeRecord {
2393 id: take_id(),
2394 edge_id: cr_edge_id,
2395 forward: cr_forward,
2396 pcurve: crate::sweep_topology::parameter_line(
2397 cr_domain[0],
2398 0.0,
2399 cr_domain[1],
2400 0.0,
2401 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2402 });
2403 }
2404 coedges.extend(finish_slot);
2405 if let Some(cs_edge_id) = cs_edge_id {
2406 coedges.push(CoedgeRecord {
2407 id: take_id(),
2408 edge_id: cs_edge_id,
2409 forward: cs_forward,
2410 pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
2411 });
2412 }
2413 coedges.extend(start_slot);
2414 coedges
2415 } else {
2416 let mut coedges = Vec::new();
2417 if let Some(cr_edge_id) = cr_edge_id {
2418 coedges.push(CoedgeRecord {
2419 id: take_id(),
2420 edge_id: cr_edge_id,
2421 forward: cr_forward,
2422 pcurve: crate::sweep_topology::parameter_line(
2423 cr_domain[1],
2424 0.0,
2425 cr_domain[0],
2426 0.0,
2427 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2428 });
2429 }
2430 coedges.extend(start_slot);
2431 if let Some(cs_edge_id) = cs_edge_id {
2432 coedges.push(CoedgeRecord {
2433 id: take_id(),
2434 edge_id: cs_edge_id,
2435 forward: cs_forward,
2436 pcurve: crate::sweep_topology::parameter_line(
2437 cs_domain[0],
2438 1.0,
2439 cs_domain[1],
2440 1.0,
2441 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2442 });
2443 }
2444 coedges.extend(finish_slot);
2445 coedges
2446 };
2447 let blend_face_id = take_id();
2448 let mut blend_face = FaceRecord {
2449 id: blend_face_id,
2450 surface: rows.surface,
2451 same_sense,
2452 loops: vec![LoopRecord {
2453 id: loop_id,
2454 coedges,
2455 }],
2456 name: name.map(|value| value.to_string()),
2457 };
2458 if rows.exact_extrusion {
2459 transpose_face(&mut blend_face)?;
2460 }
2461 let shell_index = result
2462 .shells
2463 .iter()
2464 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
2465 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
2466 for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
2471 if sew.is_some() {
2472 for shell in &mut result.shells {
2473 shell.faces.retain(|face| face.id != mate.face.id);
2474 }
2475 }
2476 }
2477 result.shells[shell_index].faces.push(blend_face);
2478
2479 result.edges.retain(|candidate| candidate.id != edge.id);
2480 result
2483 .edges
2484 .retain(|candidate| !consumed_edges.contains(&candidate.id));
2485 let mut identified = Vec::new();
2487 for (collapsed, from, to) in [(cr_collapsed, w1a, w1b), (cs_collapsed, w2a, w2b)] {
2488 if collapsed && from != to {
2489 identified.push((from, to));
2490 }
2491 }
2492 let mut cr_edge_id = cr_edge_id;
2496 let mut cs_edge_id = cs_edge_id;
2497 if tail.iter().any(Option::is_some) {
2498 let EndPlan::Cap(cap) = &finish_end else {
2499 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"));
2500 };
2501 let mut footprint = StoppedEndFootprint::default();
2502 {
2505 let sharp = result.vertices.iter().find(|vertex| vertex.id == cap.sharp_vertex).map(|vertex| vertex.point)
2506 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, "stopped_end_rollover", "blend stopped-end rollover: the sharp vertex vanished"))?;
2507 let d = edge.curve.derivatives(edge.t1, 1).or_refuse(KernelStage::Refine, "derivatives")?;
2508 footprint.plane = (sharp, d[1].normalized().or_refuse(KernelStage::Refine, "normalized")?);
2509 footprint.sharp_vertex = cap.sharp_vertex;
2510 }
2511 for (side, splice) in tail.into_iter().enumerate() {
2512 let Some(splice) = splice else { continue };
2513 let (mate, rail_edge_id, rim, sewn) = if side == 0 {
2514 (first, cr_edge_id, w1b, sew_first.is_some())
2515 } else {
2516 (second, cs_edge_id, w2b, sew_second.is_some())
2517 };
2518 let Some(rail_edge_id) = rail_edge_id.filter(|_| !sewn) else {
2519 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"));
2520 };
2521 let part_on_mate = roll_side_footprint(result, take_id, mate, &splice, rail_edge_id, rim, cap.sharp_vertex, &mut footprint)?;
2522 if side == 0 { cr_edge_id = Some(part_on_mate); } else { cs_edge_id = Some(part_on_mate); }
2523 }
2524 retrace_stopped_end_footprint(result, take_id, footprint)?;
2525 }
2526 Ok(SewnStripe {
2527 cr_edge_id,
2528 cs_edge_id,
2529 blend_face_id,
2530 consumed: [sew_first.is_some(), sew_second.is_some()],
2531 snap,
2532 identified,
2533 })
2534}
2535
2536#[derive(Default)]
2539struct StoppedEndFootprint {
2540 pieces: Vec<(u64, u64, u64, u64, NurbsCurve)>,
2543 faces: Vec<u64>,
2545 plane: (Vec3, Vec3),
2548 sharp_vertex: u64,
2549}
2550
2551#[allow(clippy::too_many_arguments)]
2559fn roll_side_footprint(
2560 result: &mut BrepSolid,
2561 take_id: &mut dyn FnMut() -> u64,
2562 mate: &BlendMate,
2563 splice: &crate::blend::rollover::TailSplice<'_>,
2564 rail_edge_id: u64,
2565 rim_vertex: u64,
2566 sharp_vertex: u64,
2567 footprint: &mut StoppedEndFootprint,
2568) -> Result<u64, KernelRefusal> {
2569 let what = "stopped_end_rollover";
2570 let not_built = |message: String| KernelRefusal::unsupported(KernelStage::Sew, what, format!("blend stopped-end rollover: {message}"));
2571 let _ = sharp_vertex;
2572 let rolled = splice.rolled;
2573 let crossing_vertex = take_id();
2574 result.vertices.push(VertexRecord { id: crossing_vertex, point: rolled.point });
2575 let rail_parts = crate::blend::rollover_surgery::split_edge_at_vertices(result, rail_edge_id, &[(crossing_vertex, rolled.point)], take_id)?;
2576 let [rail_near, rail_far] = [rail_parts[0], rail_parts[1]];
2577 let rail_far_edge = result.edges.iter().find(|edge| edge.id == rail_far).cloned()
2578 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the rail's far part vanished"))?;
2579 if rail_far_edge.end_vertex_id != rim_vertex {
2580 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)));
2581 }
2582 let holds_crossing = |result: &BrepSolid, id: u64| -> Result<bool, KernelRefusal> {
2585 let Some(edge) = result.edges.iter().find(|edge| edge.id == id) else { return Ok(false) };
2586 let foot = crate::project_point_to_curve(&edge.curve, rolled.point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
2587 Ok(foot.distance <= 1e-6 && foot.u > edge.t0.min(edge.t1) && foot.u < edge.t0.max(edge.t1))
2588 };
2589 let crossed = if result.edges.iter().any(|edge| edge.id == rolled.edge_id) {
2590 rolled.edge_id
2591 } else {
2592 let candidates: Vec<u64> = result.edges.iter().map(|edge| edge.id).collect();
2593 let mut found = None;
2594 for id in candidates {
2595 if holds_crossing(result, id)? {
2596 found = Some(id);
2597 break;
2598 }
2599 }
2600 found.ok_or_else(|| not_built(format!("no part of tangent edge {} holds the crossing any more", rolled.edge_id)))?
2601 };
2602 crate::blend::rollover_surgery::split_edge_at_vertices(result, crossed, &[(crossing_vertex, rolled.point)], take_id)?;
2603 let neighbour_surface = result
2605 .shells
2606 .iter()
2607 .flat_map(|shell| &shell.faces)
2608 .find(|face| face.id == rolled.neighbour_face_id)
2609 .map(|face| face.surface.clone())
2610 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the neighbour face vanished"))?;
2611 let far_pcurve = {
2612 const SAMPLES: usize = 24;
2613 let [t0, t1] = [rail_far_edge.t0, rail_far_edge.t1];
2614 let mut uvs = Vec::with_capacity(SAMPLES + 1);
2615 let mut ts = Vec::with_capacity(SAMPLES + 1);
2616 for k in 0..=SAMPLES {
2617 let t = t0 + (t1 - t0) * k as f64 / SAMPLES as f64;
2618 let point = rail_far_edge.curve.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
2619 let projection = crate::project_point_to_surface(&neighbour_surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
2620 uvs.push(Vec3::new(projection.u, projection.v, 0.0));
2621 ts.push(t);
2622 }
2623 crate::fit::interpolate_curve(&uvs, 3, &ts).or_refuse(KernelStage::Refine, "interpolate_curve")?
2625 };
2626 let leg_edge_id = splice.legs.first().map(|piece| piece.edge_id)
2628 .ok_or_else(|| not_built("a rolled side has no cap leg".to_string()))?;
2629 let mate_face = result
2630 .shells
2631 .iter_mut()
2632 .flat_map(|shell| shell.faces.iter_mut())
2633 .find(|face| face.id == mate.face.id)
2634 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the mate face vanished"))?;
2635 mate_face.loops[mate.loop_index].coedges.retain(|coedge| coedge.edge_id != rail_far && coedge.edge_id != leg_edge_id);
2636 footprint.pieces.push((rolled.neighbour_face_id, rail_far, crossing_vertex, rim_vertex, far_pcurve));
2637 for piece in &splice.legs {
2638 footprint.pieces.push((piece.face_id, piece.edge_id, piece.from, piece.to, piece.pcurve.clone()));
2639 }
2640 for face_id in [mate.face.id, rolled.neighbour_face_id].into_iter().chain(splice.legs.iter().map(|piece| piece.face_id)) {
2641 if !footprint.faces.contains(&face_id) {
2642 footprint.faces.push(face_id);
2643 }
2644 }
2645 Ok(rail_near)
2646}
2647
2648fn retrace_stopped_end_footprint(
2658 result: &mut BrepSolid,
2659 take_id: &mut dyn FnMut() -> u64,
2660 footprint: StoppedEndFootprint,
2661) -> Result<(), KernelRefusal> {
2662 let what = "stopped_end_rollover";
2663 let not_built = |message: String| KernelRefusal::unsupported(KernelStage::Sew, what, format!("blend stopped-end rollover: {message}"));
2664 let (sharp_point, tangent) = footprint.plane;
2665 let mut boundary_vertices: Vec<u64> = vec![footprint.sharp_vertex];
2666 for (_, _, from, to, _) in &footprint.pieces {
2667 for vertex in [*from, *to] {
2668 if !boundary_vertices.contains(&vertex) {
2669 boundary_vertices.push(vertex);
2670 }
2671 }
2672 }
2673 let piece_edges: Vec<u64> = footprint.pieces.iter().map(|piece| piece.1).collect();
2674 let mut interior: Vec<u64> = Vec::new();
2676 for face in result.shells.iter().flat_map(|shell| &shell.faces).filter(|face| footprint.faces.contains(&face.id)) {
2677 for coedge in face.loops.iter().flat_map(|lp| &lp.coedges) {
2678 let Some(edge) = result.edges.iter().find(|edge| edge.id == coedge.edge_id) else { continue };
2679 if piece_edges.contains(&edge.id) || interior.contains(&edge.id) {
2680 continue;
2681 }
2682 if !(boundary_vertices.contains(&edge.start_vertex_id) && boundary_vertices.contains(&edge.end_vertex_id)) {
2683 continue;
2684 }
2685 let mid = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1)).or_refuse(KernelStage::Refine, "evaluate")?;
2686 if mid.sub(sharp_point).dot(tangent) < 0.0 {
2687 interior.push(edge.id);
2688 }
2689 }
2690 }
2691 if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
2692 eprintln!("stopped-end rollover: footprint faces {:?}, interior edges {:?}, boundary vertices {:?}", footprint.faces, interior, boundary_vertices);
2693 }
2694 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();
2695 let mut new_faces: Vec<FaceRecord> = Vec::new();
2696 let mut taken: Vec<String> = result.shells.iter().flat_map(|shell| &shell.faces).filter_map(|face| face.name.clone()).collect();
2699 for face_id in footprint.faces.clone() {
2700 let face = result
2701 .shells
2702 .iter_mut()
2703 .flat_map(|shell| shell.faces.iter_mut())
2704 .find(|face| face.id == face_id)
2705 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: face {face_id} vanished")))?;
2706 let pieces: Vec<&(u64, u64, u64, u64, NurbsCurve)> = footprint.pieces.iter().filter(|piece| piece.0 == face_id).collect();
2707 let mut rebuilt_loops: Vec<LoopRecord> = Vec::new();
2708 for loop_record in &face.loops {
2709 let touched = loop_record.coedges.iter().any(|coedge| interior.contains(&coedge.edge_id))
2710 || loop_record.coedges.iter().any(|coedge| {
2711 edge_ends.get(&coedge.edge_id).is_some_and(|(a, b)| boundary_vertices.contains(a) || boundary_vertices.contains(b))
2712 });
2713 if !touched {
2714 rebuilt_loops.push(loop_record.clone());
2715 continue;
2716 }
2717 let n = loop_record.coedges.len();
2720 let mut remaining: Vec<(CoedgeRecord, u64, u64, usize)> = Vec::new();
2721 for (index, coedge) in loop_record.coedges.iter().enumerate() {
2722 if interior.contains(&coedge.edge_id) {
2723 continue;
2724 }
2725 let (start, end) = edge_ends.get(&coedge.edge_id).copied()
2726 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: coedge {} names a missing edge {}", coedge.id, coedge.edge_id)))?;
2727 let (from, to) = if coedge.forward { (start, end) } else { (end, start) };
2728 remaining.push((coedge.clone(), from, to, index));
2729 }
2730 let mut loose: Vec<&(u64, u64, u64, u64, NurbsCurve)> = pieces.clone();
2731 while !remaining.is_empty() {
2736 let start_at = remaining.iter().position(|(_, from, to, _)| from != to).unwrap_or(0);
2739 let (first, from0, to0, index0) = remaining.remove(start_at);
2740 let mut walk: Vec<CoedgeRecord> = vec![first];
2741 let mut at = to0;
2742 let mut previous = index0;
2743 let mut guard = 0usize;
2744 loop {
2745 guard += 1;
2746 if guard > 4096 {
2747 return Err(not_built(format!("face {face_id}: the re-trace does not close")));
2748 }
2749 let mut next_remaining: Vec<usize> = remaining.iter().enumerate().filter(|(_, (_, from, _, _))| *from == at).map(|(k, _)| k).collect();
2750 let successor_is_start = (previous + 1) % n == index0;
2751 if at == from0 && (successor_is_start || next_remaining.is_empty()) {
2752 if at == from0 && successor_is_start {
2755 break;
2756 }
2757 if loose.iter().all(|piece| piece.2 != at && piece.3 != at) {
2758 break;
2759 }
2760 }
2761 if next_remaining.len() > 1 {
2762 if let Some(k) = next_remaining.iter().copied().find(|k| remaining[*k].3 == (previous + 1) % n) {
2763 next_remaining = vec![k];
2764 }
2765 }
2766 let next_pieces: Vec<usize> = loose.iter().enumerate().filter(|(_, piece)| piece.2 == at || piece.3 == at).map(|(k, _)| k).collect();
2767 match (next_remaining.len(), next_pieces.len()) {
2768 (1, 0) => {
2769 let (coedge, _, to, index) = remaining.remove(next_remaining[0]);
2770 walk.push(coedge);
2771 at = to;
2772 previous = index;
2773 }
2774 (0, 1) => {
2775 let piece = loose.remove(next_pieces[0]);
2776 let (edge_id, from, to, pcurve) = (piece.1, piece.2, piece.3, &piece.4);
2777 let (start, _) = edge_ends.get(&edge_id).copied()
2778 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: footprint edge {edge_id} missing")))?;
2779 let along = from == at;
2780 let coedge_forward = if along { start == from } else { start == to };
2781 walk.push(CoedgeRecord {
2782 id: take_id(),
2783 edge_id,
2784 forward: coedge_forward,
2785 pcurve: if along { pcurve.clone() } else { pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")? },
2786 });
2787 at = if along { to } else { from };
2788 }
2789 (r, p) => {
2790 return Err(not_built(format!("face {face_id}: vertex {at} has {} ways on ({r} boundary, {p} new), not one", r + p)));
2791 }
2792 }
2793 }
2794 rebuilt_loops.push(LoopRecord { id: if rebuilt_loops.is_empty() { loop_record.id } else { take_id() }, coedges: walk });
2795 }
2796 if !loose.is_empty() {
2797 return Err(not_built(format!("face {face_id}: {} new boundary piece(s) joined no loop", loose.len())));
2798 }
2799 }
2800 let kept = rebuilt_loops.remove(0);
2803 face.loops = vec![kept];
2804 let surface = face.surface.clone();
2805 let same_sense = face.same_sense;
2806 let base_name = face.name.clone();
2807 for extra in rebuilt_loops {
2808 let name = base_name.as_ref().map(|base| {
2809 (1..).map(|k| format!("{base}_{k}")).find(|candidate| !taken.contains(candidate)).expect("a free name")
2810 });
2811 if let Some(name) = &name {
2812 taken.push(name.clone());
2813 }
2814 new_faces.push(FaceRecord { id: take_id(), surface: surface.clone(), same_sense, loops: vec![extra], name });
2815 }
2816 }
2817 if !new_faces.is_empty() {
2818 let shell_index = result.shells.iter().position(|shell| !shell.faces.is_empty())
2819 .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: no shell"))?;
2820 result.shells[shell_index].faces.extend(new_faces);
2821 }
2822 result.edges.retain(|edge| !interior.contains(&edge.id));
2823 Ok(())
2824}
2825
2826pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), KernelRefusal> {
2832 let surface = &face.surface;
2833 let rows_u = surface.control_points.len();
2834 let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
2835 let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
2836 for row in &surface.control_points {
2837 for (j, point) in row.iter().enumerate() {
2838 transposed[j].push(*point);
2839 }
2840 }
2841 face.surface = NurbsSurface::new(
2842 surface.degree_v,
2843 surface.degree_u,
2844 surface.knots_v.clone(),
2845 surface.knots_u.clone(),
2846 transposed,
2847 ).or_refuse(KernelStage::Refine, "new")?;
2848 for loop_record in &mut face.loops {
2849 for coedge in &mut loop_record.coedges {
2850 for control in &mut coedge.pcurve.control_points {
2851 std::mem::swap(&mut control.x, &mut control.y);
2852 }
2853 }
2854 }
2855 face.same_sense = !face.same_sense;
2856 Ok(())
2857}
2858
2859pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
2864 let used: rustc_hash::FxHashSet<u64> = result
2865 .edges
2866 .iter()
2867 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
2868 .collect();
2869 result
2870 .vertices
2871 .retain(|candidate| used.contains(&candidate.id));
2872}
2873
2874fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
2875 crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")
2876}
2877
2878
2879