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
598thread_local! {
599 static AFFINE_READS_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
600}
601
602pub(in crate::blend) fn affine_reads_on() -> bool {
603 if let Some(forced) = AFFINE_READS_FOR_TEST.with(|forced| forced.get()) {
604 return forced;
605 }
606 match std::env::var("BREP_BLEND_AFFINE_READS").as_deref() {
607 Ok("0") | Ok("off") => false,
608 Ok("1") | Ok("on") => true,
609 _ => AFFINE_READS_DEFAULT,
610 }
611}
612
613
614fn affine_plane(surface: &NurbsSurface) -> Option<(Vec3, Vec3)> {
620 let (origin, _, _) = affine_plane_frame(surface)?;
621 let net = &surface.control_points;
623 let [p00, p01, p10] = [net[0][0].point().ok()?, net[0][1].point().ok()?, net[1][0].point().ok()?];
624 Some((origin, p10.sub(p00).cross(p01.sub(p00)).normalized().ok()?))
625}
626
627pub(in crate::blend) fn affine_plane_frame(surface: &NurbsSurface) -> Option<(Vec3, Vec3, Vec3)> {
630 if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || surface.degree_u != 1 || surface.degree_v != 1 {
631 return None;
632 }
633 let net = &surface.control_points;
634 if net.len() != 2 || net.iter().any(|row| row.len() != 2) {
635 return None;
636 }
637 let weight = net[0][0].w;
638 if !(weight > 0.0) || net.iter().flatten().any(|point| point.w != weight) {
639 return None;
640 }
641 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()?];
642 let scale = [p00, p01, p10, p11].iter().fold(0.0f64, |worst, point| worst.max(point.length()));
643 if p11.sub(p10).sub(p01.sub(p00)).length() > 1e-12 * (1.0 + scale) {
644 return None;
645 }
646 let [u0, u1] = surface.domain_u().ok()?;
647 let [v0, v1] = surface.domain_v().ok()?;
648 if !(u1 > u0 && v1 > v0) {
649 return None;
650 }
651 Some((p00, p10.sub(p00).scale(1.0 / (u1 - u0)), p01.sub(p00).scale(1.0 / (v1 - v0))))
652}
653
654fn affine_curve(curve: &NurbsCurve) -> bool {
657 curve.degree == 1
658 && curve.control_points.len() == 2
659 && curve.control_points[0].w > 0.0
660 && curve.control_points[0].w == curve.control_points[1].w
661}
662
663pub(in crate::blend) fn affine_rail_standoff(rail: &NurbsCurve, carrier: &NurbsSurface, window: [f64; 2]) -> Option<f64> {
669 if !affine_curve(rail) {
670 return None;
671 }
672 let (origin, normal) = affine_plane(carrier)?;
673 let mut worst = 0.0f64;
674 for u in window {
675 let point = rail.evaluate(u).ok()?;
676 let off = point.sub(origin).dot(normal).abs();
677 if !off.is_finite() {
678 return None;
679 }
680 worst = worst.max(off);
681 }
682 Some(worst)
683}
684
685pub(in crate::blend) fn rails_on_shipped_windows(
694 rows: &FittedRows,
695 parameters: &[f64],
696 stations: &[Station],
697 carriers: [&NurbsSurface; 2],
698 windows: [Option<[f64; 2]>; 2],
699) -> (f64, [usize; 2]) {
700 read_rail_windows([&rows.cr, &rows.cs], parameters, stations, carriers, windows,
701 rows.exact_extrusion && affine_reads_on(), None)
702}
703
704fn read_rail_windows(
705 rails: [&NurbsCurve; 2], parameters: &[f64], stations: &[Station],
706 carriers: [&NurbsSurface; 2], windows: [Option<[f64; 2]>; 2], affine: bool,
707 mut memo: Option<&mut RailFeet>,
708) -> (f64, [usize; 2]) {
709 let mut worst = 0.0_f64;
710 let mut samples = [0usize; 2];
711 for (side, (rail, carrier)) in [(rails[0], carriers[0]), (rails[1], carriers[1])].into_iter().enumerate() {
712 let Some([low, high]) = windows[side] else { continue };
713 if let Some(off) = affine.then(|| affine_rail_standoff(rail, carrier, [low, high])).flatten() {
716 crate::blend::record_count("count.read.affine_rails", 1);
717 samples[side] += 2;
718 if !worst.is_nan() && off > worst {
719 worst = off;
720 }
721 continue;
722 }
723 let mut at = vec![low, high];
724 for pair in parameters.windows(2) {
725 for fraction in [0.25, 0.5, 0.75] {
726 let u = pair[0] + (pair[1] - pair[0]) * fraction;
727 if u > low && u < high {
728 at.push(u);
729 }
730 }
731 }
732 for u in at {
733 let cached = memo.as_ref().and_then(|memo| memo.values[side].get(&u.to_bits()).copied());
734 let off = if let Some(off) = cached {
735 if let Some(memo) = memo.as_mut() { memo.hits += 1; }
736 crate::blend::record_count("count.read.rail_foot_reused", 1);
737 off
738 } else {
739 if let Some(memo) = memo.as_mut() {
740 memo.reads += 1;
741 crate::blend::record_count("count.read.rail_foot_fresh", 1);
742 }
743 let off = interval_at(parameters, u)
744 .and_then(|(interval, fraction)| {
745 let (left, right) = (&stations[interval], &stations[interval + 1]);
746 let (a, b) = if side == 0 { (left.uv1, right.uv1) } else { (left.uv2, right.uv2) };
747 let seed = [a[0] + fraction * (b[0] - a[0]), a[1] + fraction * (b[1] - a[1])];
748 let point = rail.evaluate(u).ok()?;
749 crate::blend::stations::extended_foot_distance(carrier, point, seed).ok()
750 })
751 .filter(|off| off.is_finite())
752 .unwrap_or(f64::NAN);
753 if off.is_finite() {
754 if let Some(memo) = memo.as_mut() { memo.values[side].insert(u.to_bits(), off); }
755 }
756 off
757 };
758 samples[side] += 1;
759 if !worst.is_nan() && (off.is_nan() || off > worst) {
760 worst = off;
761 }
762 }
763 }
764 (worst, samples)
765}
766
767pub(in crate::blend) struct RailWindowReader {
773 rails: [NurbsCurve; 2],
774 carriers: [NurbsSurface; 2],
775 parameters: Vec<f64>,
776 stations: Vec<Station>,
777 affine: bool,
778 feet: RailFeet,
779}
780
781#[derive(Default)]
782struct RailFeet {
783 values: [rustc_hash::FxHashMap<u64, f64>; 2],
784 reads: u64,
785 hits: u64,
786}
787
788impl RailWindowReader {
789 pub(in crate::blend) fn new(rows: &FittedRows, parameters: &[f64], stations: &[Station], carriers: [&NurbsSurface; 2]) -> Self {
790 Self {
791 rails: [rows.cr.clone(), rows.cs.clone()],
792 carriers: [carriers[0].clone(), carriers[1].clone()],
793 parameters: parameters.to_vec(), stations: stations.to_vec(),
794 affine: rows.exact_extrusion && affine_reads_on(), feet: RailFeet::default(),
795 }
796 }
797
798 pub(in crate::blend) fn read(&mut self, windows: [Option<[f64; 2]>; 2]) -> (f64, [usize; 2]) {
799 read_rail_windows([&self.rails[0], &self.rails[1]], &self.parameters, &self.stations,
800 [&self.carriers[0], &self.carriers[1]], windows, self.affine, Some(&mut self.feet))
801 }
802}
803
804pub(in crate::blend) fn network_rail_reuse_on() -> bool {
805 std::env::var("BREP_NETWORK_RAIL_REUSE").map_or(true, |value| value != "0" && value != "off")
808}
809
810pub(in crate::blend) fn junction_stencil(
820 left_nodes: &[Vec3],
821 right_nodes: &[Vec3],
822 station_ts: &[f64],
823 left: usize,
824) -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
825 let half = OPEN_WALL_CENTRES / 2;
826 if left_nodes.len() != OPEN_WALL_CENTRES + 1 || right_nodes.len() != OPEN_WALL_CENTRES + 1 || left + 2 >= station_ts.len() {
827 return Err(format!("interval {left}: junction needs two {}-node intervals and their three station parameters", OPEN_WALL_CENTRES + 1));
828 }
829 let (t0, t1, t2) = (station_ts[left], station_ts[left + 1], station_ts[left + 2]);
830 let (w_left, w_right) = (t1 - t0, t2 - t1);
831 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()) {
832 return Err(format!("interval {left}: junction station parameters {t0}, {t1}, {t2} are not finite and strictly increasing"));
833 }
834 let nodes: Vec<Vec3> = left_nodes[half..].iter().chain(&right_nodes[1..=half]).copied().collect();
835 if w_left.to_bits() == w_right.to_bits() {
836 return Ok((nodes, None));
837 }
838 let abscissae: Vec<f64> = (half..=OPEN_WALL_CENTRES)
839 .map(|j| if j == OPEN_WALL_CENTRES { t1 } else { t0 + w_left * j as f64 / OPEN_WALL_CENTRES as f64 })
840 .chain((1..=half).map(|j| t1 + w_right * j as f64 / OPEN_WALL_CENTRES as f64))
841 .collect();
842 Ok((nodes, Some(abscissae)))
843}
844
845pub(in crate::blend) fn read_on_stencil(point: Vec3, nodes: &[Vec3], abscissae: Option<&[f64]>, step: usize, radius: f64) -> Result<(f64, f64), String> {
849 match abscissae {
850 None => crate::blend::chain::declared_wall_probe_why(point, nodes, step, radius),
851 Some(ts) => crate::blend::chain::declared_wall_probe_at_why(point, nodes, ts, step, radius),
852 }
853}
854
855pub(in crate::blend) fn exact_extrusion_wall_readings(
890 rows: &FittedRows,
891 parameters: &[f64],
892 window: [f64; 2],
893 radius: f64,
894) -> Vec<Result<f64, String>> {
895 let intervals = parameters.len().saturating_sub(1);
896 let overlapped: Vec<usize> =
897 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
898 let line = match (rows.exact_extrusion, rows.center.as_ref()) {
899 (true, Some(center)) => center.domain().and_then(|[a, b]| Ok((center.evaluate(a)?, center.evaluate(b)?))),
900 (false, _) => Err("the rows are not an exact extrusion".to_string()),
901 (true, None) => Err("the exact rows carry no centre line".to_string()),
902 };
903 let (anchor, direction) = match line.and_then(|(a, b)| b.sub(a).normalized().map(|d| (a, d))) {
904 Ok(line) => line,
905 Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
906 };
907 let Ok([v_low, v_high]) = rows.surface.domain_v() else {
908 return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
909 };
910 overlapped
911 .into_iter()
912 .map(|interval| -> Result<f64, String> {
913 let (from, to) = (parameters[interval], parameters[interval + 1]);
914 let inside = |u: f64| u >= window[0] && u <= window[1];
915 let mut probes: Vec<f64> =
916 (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
917 probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
918 if probes.is_empty() {
919 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
920 }
921 let mut worst = 0.0_f64;
922 for u in probes {
923 for k in 1..OPEN_WALL_SECTIONS {
924 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
925 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
926 let offset = point.sub(anchor);
927 let standoff = offset.sub(direction.scale(offset.dot(direction))).length();
928 let reading = (standoff - radius).abs();
929 if !reading.is_finite() {
930 return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
931 }
932 worst = worst.max(reading);
933 }
934 }
935 Ok(worst)
936 })
937 .collect()
938}
939
940pub(in crate::blend) fn exact_revolution_wall_readings(
951 rows: &FittedRows,
952 parameters: &[f64],
953 window: [f64; 2],
954 radius: f64,
955) -> Vec<Result<f64, String>> {
956 let intervals = parameters.len().saturating_sub(1);
957 let overlapped: Vec<usize> =
958 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
959 let circle = match (rows.revolution_axis, rows.center.as_ref()) {
960 (Some((origin, axis)), Some(center)) => center.domain().and_then(|[a, _]| center.evaluate(a)).map(|point| (origin, axis, point)),
961 (None, _) => Err("the rows are not an exact revolution".to_string()),
962 (Some(_), None) => Err("the exact rows carry no centre circle".to_string()),
963 };
964 let (origin, axis, centre_radius, centre_height) = match circle {
965 Ok((origin, axis, point)) => {
966 let offset = point.sub(origin);
967 let height = offset.dot(axis);
968 (origin, axis, offset.sub(axis.scale(height)).length(), height)
969 }
970 Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
971 };
972 let Ok([v_low, v_high]) = rows.surface.domain_v() else {
973 return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
974 };
975 overlapped
976 .into_iter()
977 .map(|interval| -> Result<f64, String> {
978 let (from, to) = (parameters[interval], parameters[interval + 1]);
979 let inside = |u: f64| u >= window[0] && u <= window[1];
980 let mut probes: Vec<f64> =
981 (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
982 probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
983 if probes.is_empty() {
984 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
985 }
986 let mut worst = 0.0_f64;
987 for u in probes {
988 for k in 1..OPEN_WALL_SECTIONS {
989 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
990 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
991 let offset = point.sub(origin);
992 let height = offset.dot(axis);
993 let radial = offset.sub(axis.scale(height)).length();
994 let standoff = (radial - centre_radius).hypot(height - centre_height);
995 let reading = (standoff - radius).abs();
996 if !reading.is_finite() {
997 return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
998 }
999 worst = worst.max(reading);
1000 }
1001 }
1002 Ok(worst)
1003 })
1004 .collect()
1005}
1006
1007pub(in crate::blend) fn open_wall_readings_why(
1008 rows: &FittedRows,
1009 parameters: &[f64],
1010 stations: &[Station],
1011 window: [f64; 2],
1012 dense: &OpenDenseCentres,
1013 radius: f64,
1014) -> Vec<Result<f64, String>> {
1015 let intervals = parameters.len().saturating_sub(1);
1016 let overlapped: Vec<usize> =
1017 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
1018 let Ok([v_low, v_high]) = rows.surface.domain_v() else {
1019 return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
1020 };
1021 let nodes_of = |interval: usize| -> Result<Vec<Vec3>, String> {
1023 let inner = dense.centres.get(interval).ok_or_else(|| format!("interval {interval}: no dense centre row"))?;
1024 if dense.subdivisions != OPEN_WALL_CENTRES || inner.len() + 1 != OPEN_WALL_CENTRES {
1025 return Err(format!("interval {interval}: dense row {} centres at {} subdivisions", inner.len(), dense.subdivisions));
1026 }
1027 let mut centres = Vec::with_capacity(OPEN_WALL_CENTRES + 1);
1028 centres.push(stations[interval].center);
1029 for (j, centre) in inner.iter().enumerate() {
1030 centres.push(centre.ok_or_else(|| format!("interval {interval}: exact centre {} of {OPEN_WALL_CENTRES} unsolved", j + 1))?);
1031 }
1032 centres.push(stations[interval + 1].center);
1033 Ok(centres)
1034 };
1035 let half = OPEN_WALL_CENTRES / 2;
1036 let junction = |left: usize| -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
1043 if dense.station_ts.len() != parameters.len() {
1044 return Err(format!("interval {left}: junction without the native station parameters"));
1045 }
1046 junction_stencil(&nodes_of(left)?, &nodes_of(left + 1)?, &dense.station_ts, left)
1047 };
1048 overlapped
1049 .into_iter()
1050 .map(|interval| -> Result<f64, String> {
1051 let centres = nodes_of(interval)?;
1052 let (from, to) = (parameters[interval], parameters[interval + 1]);
1053 let inside = |u: f64| u >= window[0] && u <= window[1];
1054 let mut probes: Vec<(f64, usize, Option<(Vec<Vec3>, Option<Vec<f64>>)>)> = (2..OPEN_WALL_CENTRES)
1057 .step_by(2)
1058 .map(|step| (from + (to - from) * step as f64 / OPEN_WALL_CENTRES as f64, step))
1059 .filter(|(u, _)| inside(*u))
1060 .map(|(u, step)| (u, step, None))
1061 .collect();
1062 for end in window {
1063 if end > from && end < to {
1064 let fraction = (end - from) / (to - from);
1065 let nearest = 2 * ((fraction * OPEN_WALL_CENTRES as f64 / 2.0).round() as usize);
1066 #[cfg(not(test))]
1067 let junction_off = false;
1068 if junction_off {
1069 probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1070 } else if nearest < 2 && interval > 0 {
1071 probes.push((end, half, Some(junction(interval - 1)?)));
1072 } else if nearest > OPEN_WALL_CENTRES - 2 && interval + 1 < intervals {
1073 probes.push((end, half, Some(junction(interval)?)));
1074 } else {
1075 probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1076 }
1077 }
1078 }
1079 if probes.is_empty() {
1080 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1081 }
1082 let mut declared = 0.0_f64;
1083 for (u, step, stencil) in probes {
1084 let (nodes, abscissae, what) = match &stencil {
1085 Some((nodes, None)) => (nodes, None, "junction"),
1086 Some((nodes, Some(ts))) => (nodes, Some(ts), "native-abscissa junction"),
1087 None => (¢res, None, "interval"),
1088 };
1089 for k in 1..OPEN_WALL_SECTIONS {
1090 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1091 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1092 let read = if stencil.is_none() {
1097 crate::blend::chain::declared_wall_probe_located_why(point, nodes, step, (u - from) / (to - from), radius)
1098 } else {
1099 read_on_stencil(point, nodes, abscissae.map(|ts| ts.as_slice()), step, radius)
1100 };
1101 let (reading, uncertainty) = read.map_err(|why| {
1102 format!(
1103 "interval {interval}: declared reader declined probe node {step} of the {what} stencil, section {k}/{OPEN_WALL_SECTIONS} \
1104 (u {u:.17}): {why}"
1105 )
1106 })?;
1107 declared = declared.max(reading + uncertainty);
1108 }
1109 }
1110 if declared.is_finite() { Ok(declared) } else { Err(format!("interval {interval}: non-finite reading {declared}")) }
1111 })
1112 .collect()
1113}
1114
1115
1116pub(in crate::blend) fn fresh_id_source(solid: &BrepSolid) -> impl FnMut() -> u64 {
1121 let mut next_id = solid
1122 .vertices
1123 .iter()
1124 .map(|vertex| vertex.id)
1125 .chain(solid.edges.iter().map(|edge| edge.id))
1126 .chain(
1127 solid
1128 .shells
1129 .iter()
1130 .flat_map(|shell| &shell.faces)
1131 .flat_map(|face| {
1132 face.loops
1133 .iter()
1134 .map(|loop_record| loop_record.id)
1135 .chain(face.loops.iter().flat_map(|loop_record| {
1136 loop_record.coedges.iter().map(|coedge| coedge.id)
1137 }))
1138 .chain(std::iter::once(face.id))
1139 }),
1140 )
1141 .max()
1142 .unwrap_or(0)
1143 + 1;
1144 move || {
1145 let id = next_id;
1146 next_id += 1;
1147 id
1148 }
1149}
1150
1151
1152pub(in crate::blend) fn march_trim_stands(read: &Result<(f64, f64, usize, f64), KernelRefusal>, floor: f64) -> bool {
1161 matches!(read, Ok((miss, standoff, _, _)) if *miss + *standoff <= floor)
1162}
1163
1164fn exact_planar_trim(rail: &NurbsCurve, surface: &NurbsSurface, d0: f64, d1: f64) -> Result<Option<NurbsCurve>, KernelRefusal> {
1174 if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || !(d1 > d0) {
1175 return Ok(None);
1176 }
1177 let feet_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.feet_ms");
1178 let mut control = Vec::with_capacity(2);
1179 for t in [d0, d1] {
1180 let point = rail.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
1181 let foot = crate::project_point_to_surface(surface, point).or_refuse(KernelStage::Refine, "project")?;
1182 control.push(crate::Vec4::from_point(crate::Vec3::new(foot.u, foot.v, 0.0), 1.0));
1183 }
1184 let line = NurbsCurve::new(1, vec![d0, d0, d1, d1], control).or_refuse(KernelStage::Refine, "new")?;
1185 feet_timing.stop();
1186 let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1187 let read = match affine_reads_on().then(|| affine_trim_read(rail, surface, &line, d0, d1)).flatten() {
1188 Some(read) => {
1189 crate::blend::record_count("count.read.affine_trims", 1);
1190 Ok(read)
1191 }
1192 None => crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &line, d0, d1),
1193 };
1194 read_timing.stop();
1195 Ok(march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE).then_some(line))
1196}
1197
1198fn affine_trim_read(rail: &NurbsCurve, surface: &NurbsSurface, pcurve: &NurbsCurve, d0: f64, d1: f64) -> Option<(f64, f64, usize, f64)> {
1206 if !affine_curve(rail) || !affine_curve(pcurve) {
1207 return None;
1208 }
1209 let (origin, normal) = affine_plane(surface)?;
1210 let (mut miss, mut standoff, mut worst) = (0.0f64, 0.0f64, d0);
1211 for t in [d0, d1] {
1212 let point = rail.evaluate(t).ok()?;
1213 let foot = point.sub(normal.scale(point.sub(origin).dot(normal)));
1214 let uv = pcurve.evaluate(t).ok()?;
1215 let image = crate::blend::track_fit::carrier_point(surface, uv.x, uv.y).ok()?;
1216 let (residual, off) = (image.sub(foot).length(), point.sub(foot).length());
1217 if !(residual.is_finite() && off.is_finite()) {
1218 return None;
1219 }
1220 if residual > miss {
1221 (miss, worst) = (residual, t);
1222 }
1223 standoff = standoff.max(off);
1224 }
1225 Some((miss, standoff, 2, worst))
1226}
1227
1228pub(in crate::blend) fn build_open_surgery(
1240 solid: &BrepSolid,
1241 result: &mut BrepSolid,
1242 take_id: &mut dyn FnMut() -> u64,
1243 edge: &EdgeRecord,
1244 first: &BlendMate,
1245 second: &BlendMate,
1246 rows: FittedRows,
1247 ends: [EndPlan; 2],
1248 name: Option<&str>,
1249) -> Result<SewnStripe, KernelRefusal> {
1250 let [start_end, finish_end] = ends;
1251
1252 let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, KernelRefusal> {
1259 let [low, high] = row.domain().or_refuse(KernelStage::Refine, "domain")?;
1260 let tail = if a == low { row.clone() } else { row.split(a).or_refuse(KernelStage::Refine, "split")?.1 };
1261 if b == high {
1262 return Ok(tail);
1263 }
1264 let (middle, _) = tail.split(b).or_refuse(KernelStage::Refine, "split")?;
1265 Ok(middle)
1266 };
1267 let kind = |plan: &EndPlan| match plan {
1268 EndPlan::Free(_) => "free",
1269 EndPlan::Corner(_) => "corner",
1270 EndPlan::Miter(_) => "miter",
1271 EndPlan::Cap(_) => "cap",
1272 };
1273 let describe_trim = |error: KernelRefusal| {
1274 error.with_message(|error| {
1275 format!(
1276 "{error} (edge {} rows trimmed for a {} start at cr {:.6}/cs {:.6} and a {} finish \
1277 at cr {:.6}/cs {:.6})",
1278 edge.id,
1279 kind(&start_end),
1280 start_end.cr_parameter(),
1281 start_end.cs_parameter(),
1282 kind(&finish_end),
1283 finish_end.cr_parameter(),
1284 finish_end.cs_parameter()
1285 )
1286 })
1287 };
1288 let band = consumed_band(solid);
1298 let exact_trim = crate::blend::exact_trim_on();
1299 let collapses = |row: &NurbsCurve, a: f64, b: f64| -> Result<bool, KernelRefusal> {
1300 let from = row.evaluate(a).or_refuse(KernelStage::Refine, "evaluate")?;
1301 Ok(row.evaluate(b).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band
1302 && row.evaluate(0.5 * (a + b)).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band)
1303 };
1304 let cr_collapsed = collapses(&rows.cr, start_end.cr_parameter(), finish_end.cr_parameter())?;
1305 let cs_collapsed = collapses(&rows.cs, start_end.cs_parameter(), finish_end.cs_parameter())?;
1306 if cr_collapsed || cs_collapsed {
1307 let unsupported = if start_end.free().is_some() && finish_end.free().is_some() {
1308 Some("neither end is a corner")
1309 } else if first.face.id == second.face.id {
1310 Some("both rails lie on one face")
1311 } else if matches!(start_end, EndPlan::Cap(_)) || matches!(finish_end, EndPlan::Cap(_)) {
1312 Some("a capped end has no corner to share the point with")
1313 } else {
1314 None
1315 };
1316 if let Some(reason) = unsupported {
1317 return Err(KernelRefusal::unsupported(KernelStage::Sew, super::RAIL_COLLAPSE_WHAT, format!(
1318 "{RAIL_COLLAPSE_UNSUPPORTED} edge {}'s rail shrinks to a point and {reason}",
1319 edge.id
1320 )));
1321 }
1322 }
1323 let trim_rail = |collapsed: bool,
1337 row: &NurbsCurve,
1338 pcurve: &NurbsCurve,
1339 surface: &NurbsSurface,
1340 a: f64,
1341 b: f64|
1342 -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1343 if collapsed {
1344 return Ok(None);
1345 }
1346 let rows_timing = crate::blend::BlendTimer::start("timing.surgery.trim.rows_ms");
1347 let rail = trim_row(row, a, b).map_err(describe_trim)?;
1348 let interpolated = trim_row(pcurve, a, b)?;
1349 rows_timing.stop();
1350 {
1351 let overrun_timing = crate::blend::BlendTimer::start("timing.surgery.trim.overrun_ms");
1352 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1353 let overrun = crate::blend::planar_chart_overrun(surface, &|t| rail.evaluate(t), d0, d1)?;
1354 overrun_timing.stop();
1355 if overrun.is_some_and(|excursion| excursion > band) {
1356 return Ok(Some((rail, interpolated)));
1357 }
1358 }
1359 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1360 if exact_trim && rows.exact_extrusion {
1364 if let Some(line) = exact_planar_trim(&rail, surface, d0, d1)? {
1365 crate::blend::record_count("count.surgery.exact_trim", 1);
1366 return Ok(Some((rail, line)));
1367 }
1368 }
1369 if rows.revolution_axis.is_some() {
1374 let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1375 let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &interpolated, d0, d1);
1376 read_timing.stop();
1377 if march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
1378 crate::blend::record_count("count.surgery.exact_revolution_trim", 1);
1379 return Ok(Some((rail, interpolated)));
1380 }
1381 crate::blend::record_count("count.surgery.exact_revolution_trim_declined", 1);
1382 }
1383 let mut march_feet = crate::blend::track_fit::FootMemo::default();
1392 let mut refit_feet = crate::blend::track_fit::FootMemo::default();
1393 let march_read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.march_ms");
1394 let march_read = crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &interpolated, d0, d1, &mut march_feet);
1395 march_read_timing.stop();
1396 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1397 let declined = |why: KernelRefusal| -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1398 let stands = march_trim_stands(&march_read, floor);
1401 if stands {
1402 Ok(Some((rail.clone(), interpolated.clone())))
1403 } else {
1404 Err(why)
1405 }
1406 };
1407 let refit_timing = crate::blend::BlendTimer::start("timing.surgery.trim.refit_ms");
1408 let refit = super::closed::refit_closed_support_pcurve_read(&rail, &interpolated, surface, &mut refit_feet);
1409 refit_timing.stop();
1410 let _reads_timing = crate::blend::BlendTimer::start("timing.surgery.trim.reads_ms");
1411 match refit {
1412 Ok((fitted, refit_read)) => {
1413 let (march_standoff, refit_standoff) = match (&march_read, Ok::<_, KernelRefusal>(refit_read)) {
1430 (_, Err(error)) => return declined(error),
1431 (Err(error), Ok(_)) => {
1432 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch_unread", format!(
1433 "blend: a support trim's refit cannot be shown to stand on its rail's branch: the march's own \
1434 trim is unreadable ({})",
1435 error.message
1436 )));
1437 }
1438 (Ok((_, march_standoff, _, _)), Ok((_, refit_standoff, _, _))) => (*march_standoff, refit_standoff),
1439 };
1440 let excess_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.excess_ms");
1441 let excess = crate::blend::track_fit::standoff_excess_memo(
1442 surface, &|t| rail.evaluate(t), &interpolated, &fitted, d0, d1, &mut march_feet, &mut refit_feet,
1443 );
1444 excess_timing.stop();
1445 let excess = match excess {
1446 Ok(excess) => excess,
1447 Err(error) => return declined(error),
1448 };
1449 if !(excess <= floor) {
1450 return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch", format!(
1451 "blend: a support trim's refit stands up to {excess:.3e} farther off its rail than the march's own trim \
1452 at the same rail parameter (largest standoffs {refit_standoff:.3e} and {march_standoff:.3e}): it took \
1453 another branch of the carrier"
1454 )));
1455 }
1456 let nearest_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.nearest_ms");
1464 let absolute = crate::blend::track_fit::standoff_over_nearest_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut refit_feet);
1465 nearest_timing.stop();
1466 let absolute = match absolute {
1467 Ok(absolute) => absolute,
1468 Err(error) => return declined(error),
1469 };
1470 if !(absolute <= floor) {
1471 return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_off_rail", format!(
1472 "blend: a support trim's refit stands up to {absolute:.3e} farther off its rail than the rail stands \
1473 off its carrier: its foot is not the rail's nearest point"
1474 )));
1475 }
1476 Ok(Some((rail, fitted)))
1477 }
1478 Err(error) => declined(error),
1479 }
1480 };
1481 let trim_timing = crate::blend::BlendTimer::start("timing.surgery.trim_rails_ms");
1482 let cr_rail = trim_rail(
1483 cr_collapsed,
1484 &rows.cr,
1485 &rows.cr_pcurve,
1486 &first.face.surface,
1487 start_end.cr_parameter(),
1488 finish_end.cr_parameter(),
1489 )?;
1490 let cs_rail = trim_rail(
1491 cs_collapsed,
1492 &rows.cs,
1493 &rows.cs_pcurve,
1494 &second.face.surface,
1495 start_end.cs_parameter(),
1496 finish_end.cs_parameter(),
1497 )?;
1498 trim_timing.stop();
1499 let cr_domain = match &cr_rail {
1501 Some((cr, _)) => cr.domain().or_refuse(KernelStage::Refine, "domain")?,
1502 None => [start_end.cr_parameter(), finish_end.cr_parameter()],
1503 };
1504 let cs_domain = match &cs_rail {
1505 Some((cs, _)) => cs.domain().or_refuse(KernelStage::Refine, "domain")?,
1506 None => [start_end.cs_parameter(), finish_end.cs_parameter()],
1507 };
1508 let cr_at = |parameter: f64| match &cr_rail {
1510 Some((cr, _)) => cr.evaluate(parameter),
1511 None => rows.cr.evaluate(parameter),
1512 };
1513 let cs_at = |parameter: f64| match &cs_rail {
1514 Some((cs, _)) => cs.evaluate(parameter),
1515 None => rows.cs.evaluate(parameter),
1516 };
1517 let old_start = edge.start_vertex_id;
1518 let old_finish = edge.end_vertex_id;
1519
1520 let classify =
1531 |boundary_id: u64, boundary_param: f64, corner: u64| -> Result<RimResolution, KernelRefusal> {
1532 let boundary = result
1533 .edges
1534 .iter()
1535 .find(|candidate| candidate.id == boundary_id)
1536 .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_boundary_edge", "blend: end boundary edge missing during surgery"))?;
1537 let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
1538 let far_vertex = if boundary.start_vertex_id == corner {
1539 Some((boundary.end_vertex_id, boundary.t1, boundary.t0))
1540 } else if boundary.end_vertex_id == corner {
1541 Some((boundary.start_vertex_id, boundary.t0, boundary.t1))
1542 } else {
1543 None
1544 };
1545 if let Some((far_id, far_t, near_t)) = far_vertex {
1546 let far_point = result
1557 .vertices
1558 .iter()
1559 .find(|candidate| candidate.id == far_id)
1560 .map(|candidate| candidate.point)
1561 .ok_or(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"))?;
1562 let crossing = boundary.curve.evaluate_extended(boundary_param).or_refuse(KernelStage::Refine, "evaluate_extended")?;
1563 let _ = span;
1564 let past_far = crossing.sub(far_point).length();
1565 if past_far <= band {
1566 if !result
1568 .vertices
1569 .iter()
1570 .any(|candidate| candidate.id == far_id)
1571 {
1572 return Err(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"));
1573 }
1574 return Ok(RimResolution::Consumed(far_id));
1575 }
1576 if (boundary_param - far_t) * (far_t - near_t) > 0.0 {
1589 return Err(KernelRefusal::unsupported(KernelStage::Sew, "blend_wider_than_face", format!(
1590 "{BLEND_WIDER_THAN_FACE} its rail crosses edge {boundary_id} {past_far:.3e} \
1591 past that edge's far vertex, and a rail within {band:.3e} of it is the \
1592 most this surgery snaps onto the edge"
1593 )));
1594 }
1595 }
1596 Ok(RimResolution::Fresh)
1597 };
1598 let resolve_side = |plan: &EndPlan,
1602 side_first: bool,
1603 corner: u64|
1604 -> Result<RimResolution, KernelRefusal> {
1605 if let Some(vertex) = plan.planned_rim(side_first) {
1606 return Ok(RimResolution::Corner(vertex));
1607 }
1608 match plan {
1609 EndPlan::Free(free) => classify(
1610 if side_first {
1611 free.first_edge_id
1612 } else {
1613 free.second_edge_id
1614 },
1615 if side_first {
1616 free.first_edge_parameter
1617 } else {
1618 free.second_edge_parameter
1619 },
1620 corner,
1621 ),
1622 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => {
1623 Err(KernelRefusal::internal(KernelStage::Sew, "rim_vertices", "blend: planned end without rim vertices"))
1624 }
1625 }
1626 };
1627 let crossings_timing = crate::blend::BlendTimer::start("timing.surgery.crossings_ms");
1628 let start_first = resolve_side(&start_end, true, old_start)?;
1629 let start_second = resolve_side(&start_end, false, old_start)?;
1630 let finish_first = resolve_side(&finish_end, true, old_finish)?;
1631 let finish_second = resolve_side(&finish_end, false, old_finish)?;
1632 crossings_timing.stop();
1633 let _topology_timing = crate::blend::BlendTimer::start("timing.surgery.topology_ms");
1634
1635 let first_use_forward = first.coedge.forward;
1638 let blend_cr_forward = !first_use_forward;
1639
1640 let (sew_first, sew_second) = match (start_end.free(), finish_end.free(), &cr_rail, &cs_rail) {
1655 (Some(start_free), Some(finish_free), Some((cr, _)), Some((cs, _))) => (
1656 detect_row_coincidence(
1657 solid,
1658 first,
1659 edge.id,
1660 start_free.first_edge_id,
1661 finish_free.first_edge_id,
1662 start_first.is_consumed(),
1663 start_first.existing().unwrap_or(0),
1664 finish_first.is_consumed(),
1665 finish_first.existing().unwrap_or(0),
1666 cr,
1667 blend_cr_forward,
1668 band,
1669 )?,
1670 detect_row_coincidence(
1671 solid,
1672 second,
1673 edge.id,
1674 start_free.second_edge_id,
1675 finish_free.second_edge_id,
1676 start_second.is_consumed(),
1677 start_second.existing().unwrap_or(0),
1678 finish_second.is_consumed(),
1679 finish_second.existing().unwrap_or(0),
1680 cs,
1681 !blend_cr_forward,
1682 band,
1683 )?,
1684 ),
1685 _ => (None, None),
1686 };
1687 let (sew_first, sew_second): (Option<RowSew>, Option<RowSew>) =
1691 if first.face.id == second.face.id {
1692 (None, None)
1693 } else {
1694 (sew_first, sew_second)
1695 };
1696
1697 let mut resolve = |rim: &RimResolution, point: Result<Vec3, String>| -> Result<u64, KernelRefusal> {
1700 match rim.existing() {
1701 Some(id) => Ok(id),
1702 None => {
1703 let id = take_id();
1704 result.vertices.push(VertexRecord { id, point: point.or_refuse(KernelStage::Refine, "point")? });
1705 Ok(id)
1706 }
1707 }
1708 };
1709 let w1a = resolve(&start_first, cr_at(cr_domain[0]))?;
1710 let w1b = resolve(&finish_first, cr_at(cr_domain[1]))?;
1711 let w2a = resolve(&start_second, cs_at(cs_domain[0]))?;
1712 let w2b = resolve(&finish_second, cs_at(cs_domain[1]))?;
1713 let mut snap = [&sew_first, &sew_second]
1716 .into_iter()
1717 .flatten()
1718 .fold(0.0f64, |worst, sew| worst.max(sew.deviation));
1719 for (rim, vertex, row_end) in [
1720 (&start_first, w1a, cr_at(cr_domain[0]).or_refuse(KernelStage::Refine, "cr_at")?),
1721 (&finish_first, w1b, cr_at(cr_domain[1]).or_refuse(KernelStage::Refine, "cr_at")?),
1722 (&start_second, w2a, cs_at(cs_domain[0]).or_refuse(KernelStage::Refine, "cs_at")?),
1723 (&finish_second, w2b, cs_at(cs_domain[1]).or_refuse(KernelStage::Refine, "cs_at")?),
1724 ] {
1725 if !rim.is_consumed() {
1726 continue;
1727 }
1728 let point = result
1729 .vertices
1730 .iter()
1731 .find(|candidate| candidate.id == vertex)
1732 .ok_or(KernelRefusal::internal(KernelStage::Sew, "rim_vertex", "blend: consumed rim vertex missing"))?
1733 .point;
1734 snap = snap.max(row_end.sub(point).length());
1735 }
1736
1737 let cr_edge_id = match (&sew_first, &cr_rail) {
1742 (Some(sew), _) => Some(sew.edge_id),
1743 (None, None) => None,
1744 (None, Some((cr, _))) => {
1745 let id = take_id();
1746 result.edges.push(EdgeRecord {
1747 id,
1748 curve: cr.clone(),
1749 t0: cr_domain[0],
1750 t1: cr_domain[1],
1751 start_vertex_id: w1a,
1752 end_vertex_id: w1b,
1753 degenerate: false,
1754 name: None,
1755 });
1756 Some(id)
1757 }
1758 };
1759 let cs_edge_id = match (&sew_second, &cs_rail) {
1760 (Some(sew), _) => Some(sew.edge_id),
1761 (None, None) => None,
1762 (None, Some((cs, _))) => {
1763 let id = take_id();
1764 result.edges.push(EdgeRecord {
1765 id,
1766 curve: cs.clone(),
1767 t0: cs_domain[0],
1768 t1: cs_domain[1],
1769 start_vertex_id: w2a,
1770 end_vertex_id: w2b,
1771 degenerate: false,
1772 name: None,
1773 });
1774 Some(id)
1775 }
1776 };
1777 let mut commit_end_edge = |plan: &EndPlan,
1782 first_rim: u64,
1783 second_rim: u64|
1784 -> Result<Option<u64>, KernelRefusal> {
1785 match plan {
1786 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
1787 EndPlan::Free(free) => {
1788 let id = take_id();
1789 let domain = free.transverse_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1790 result.edges.push(EdgeRecord {
1791 id,
1792 curve: free.transverse_curve.clone(),
1793 t0: domain[0],
1794 t1: domain[1],
1795 start_vertex_id: first_rim,
1796 end_vertex_id: second_rim,
1797 degenerate: false,
1798 name: None,
1799 });
1800 Ok(Some(id))
1801 }
1802 }
1803 };
1804 let transverse_a_id = commit_end_edge(&start_end, w1a, w2a)?;
1805 let transverse_b_id = commit_end_edge(&finish_end, w1b, w2b)?;
1806
1807 for (mate, rail, sewn) in [
1814 (first, cr_edge_id.zip(cr_rail.as_ref()), sew_first.is_some()),
1815 (second, cs_edge_id.zip(cs_rail.as_ref()), sew_second.is_some()),
1816 ] {
1817 if sewn {
1818 continue;
1819 }
1820 let face = result
1821 .shells
1822 .iter_mut()
1823 .flat_map(|shell| &mut shell.faces)
1824 .find(|face| face.id == mate.face.id)
1825 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1826 let loop_record = &mut face.loops[mate.loop_index];
1827 let position = loop_record
1828 .coedges
1829 .iter()
1830 .position(|coedge| coedge.edge_id == edge.id)
1831 .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
1832 let Some((new_edge_id, (_, pcurve_forward))) = rail else {
1833 loop_record.coedges.remove(position);
1834 continue;
1835 };
1836 let old_forward = loop_record.coedges[position].forward;
1837 loop_record.coedges[position] = CoedgeRecord {
1838 id: loop_record.coedges[position].id,
1839 edge_id: new_edge_id,
1840 forward: old_forward,
1841 pcurve: if old_forward {
1842 pcurve_forward.clone()
1843 } else {
1844 pcurve_forward.reversed().or_refuse(KernelStage::Refine, "reversed")?
1845 },
1846 };
1847 }
1848
1849 for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
1854 let EndPlan::Cap(cap) = plan else {
1855 continue;
1856 };
1857 for (mate, rail_edge_id, leg, rim) in [
1858 (first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
1859 (second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
1860 ] {
1861 let rail_edge_id = rail_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "end_rail", "blend: a capped end lost its rail"))?;
1863 let face = result
1864 .shells
1865 .iter_mut()
1866 .flat_map(|shell| &mut shell.faces)
1867 .find(|face| face.id == mate.face.id)
1868 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during cap splice"))?;
1869 let loop_record = &mut face.loops[mate.loop_index];
1870 let count = loop_record.coedges.len();
1871 let rail_at = loop_record
1872 .coedges
1873 .iter()
1874 .position(|coedge| coedge.edge_id == rail_edge_id)
1875 .ok_or(KernelRefusal::internal(KernelStage::Sew, "rail_coedge", "blend: rail coedge lost during cap splice"))?;
1876 let rail_forward = loop_record.coedges[rail_at].forward;
1882 let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
1883 let (insert_at, forward, pcurve) = if end_is_traversal_end {
1884 (rail_at + 1, true, leg.1.clone())
1886 } else {
1887 (rail_at, false, leg.1.reversed().or_refuse(KernelStage::Refine, "reversed")?)
1889 };
1890 let _ = (count, rim);
1891 loop_record.coedges.insert(
1892 insert_at,
1893 CoedgeRecord {
1894 id: take_id(),
1895 edge_id: leg.0,
1896 forward,
1897 pcurve,
1898 },
1899 );
1900 }
1901 }
1902
1903 let mut consumed_edges: Vec<u64> = Vec::new();
1909 let start_free = start_end.free();
1910 let finish_free = finish_end.free();
1911 for (mate, sewn, sides) in [
1912 (
1913 first,
1914 sew_first.is_some(),
1915 [
1916 (&start_first, start_free.map(|free| free.first_edge_id)),
1917 (&finish_first, finish_free.map(|free| free.first_edge_id)),
1918 ],
1919 ),
1920 (
1921 second,
1922 sew_second.is_some(),
1923 [
1924 (&start_second, start_free.map(|free| free.second_edge_id)),
1925 (&finish_second, finish_free.map(|free| free.second_edge_id)),
1926 ],
1927 ),
1928 ] {
1929 for (cross, boundary_id) in sides {
1930 if !cross.is_consumed() {
1931 continue;
1932 }
1933 let Some(boundary_id) = boundary_id else {
1934 continue;
1935 };
1936 if !sewn {
1937 let face = result
1938 .shells
1939 .iter_mut()
1940 .flat_map(|shell| &mut shell.faces)
1941 .find(|face| face.id == mate.face.id)
1942 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1943 face.loops[mate.loop_index]
1944 .coedges
1945 .retain(|coedge| coedge.edge_id != boundary_id);
1946 }
1947 if !consumed_edges.contains(&boundary_id) {
1948 consumed_edges.push(boundary_id);
1949 }
1950 }
1951 }
1952 for sew in [&sew_first, &sew_second].into_iter().flatten() {
1954 for id in &sew.collapsed_edges {
1955 if !consumed_edges.contains(id) {
1956 consumed_edges.push(*id);
1957 }
1958 }
1959 }
1960
1961 let mut end_plans = Vec::with_capacity(2);
1970 for (end, transverse_id, corner, first_consumed, second_consumed) in [
1971 (
1972 start_end.free(),
1973 transverse_a_id,
1974 old_start,
1975 start_first.is_consumed(),
1976 start_second.is_consumed(),
1977 ),
1978 (
1979 finish_end.free(),
1980 transverse_b_id,
1981 old_finish,
1982 finish_first.is_consumed(),
1983 finish_second.is_consumed(),
1984 ),
1985 ] {
1986 let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
1987 continue;
1988 };
1989 let face = result
1990 .shells
1991 .iter()
1992 .flat_map(|shell| &shell.faces)
1993 .find(|face| face.id == end.end_face_id)
1994 .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_face", "blend: end face lost during surgery"))?;
1995 let Some(located) = locate_end_corner(
1998 &result,
1999 face,
2000 corner,
2001 end.first_edge_id,
2002 end.second_edge_id,
2003 ) else {
2004 return Err(KernelRefusal::internal(KernelStage::Sew, "end_face_corner", "blend: end-face corner (adjacent boundary coedges) not found"));
2005 };
2006 for pole in &located.pole_edge_ids {
2009 if !consumed_edges.contains(pole) {
2010 consumed_edges.push(*pole);
2011 }
2012 }
2013 end_plans.push((
2014 end.end_face_id,
2015 transverse_id,
2016 end.transverse_end_pcurve.clone(),
2017 located,
2018 first_consumed,
2019 second_consumed,
2020 ));
2021 }
2022
2023 for (rim_resolution, boundary, corner, rim) in [
2026 (
2027 &start_first,
2028 start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
2029 old_start,
2030 w1a,
2031 ),
2032 (
2033 &start_second,
2034 start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
2035 old_start,
2036 w2a,
2037 ),
2038 (
2039 &finish_first,
2040 finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
2041 old_finish,
2042 w1b,
2043 ),
2044 (
2045 &finish_second,
2046 finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
2047 old_finish,
2048 w2b,
2049 ),
2050 ] {
2051 if !rim_resolution.trims_boundary() {
2052 continue;
2053 }
2054 let Some((boundary_id, boundary_param)) = boundary else {
2055 continue;
2056 };
2057 trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
2058 }
2059
2060 for (end_face, transverse_id, transverse_end_pcurve, located, first_consumed, second_consumed) in
2063 end_plans
2064 {
2065 let forward = located.x_is_first;
2066 let pcurve = if forward {
2067 transverse_end_pcurve
2068 } else {
2069 transverse_end_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
2070 };
2071 let (x_consumed, y_consumed) = if located.x_is_first {
2072 (first_consumed, second_consumed)
2073 } else {
2074 (second_consumed, first_consumed)
2075 };
2076 let transverse_coedge = CoedgeRecord {
2077 id: take_id(),
2078 edge_id: transverse_id,
2079 forward,
2080 pcurve,
2081 };
2082 splice_end_corner(
2083 result,
2084 end_face,
2085 &located,
2086 transverse_coedge,
2087 x_consumed,
2088 y_consumed,
2089 )?;
2090 }
2091
2092 let (mid_u, station_uv) = match (&cr_rail, &cs_rail) {
2097 (Some((_, cr_pcurve)), _) => {
2098 let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
2099 (mid_u, cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2100 }
2101 (None, Some(_)) => {
2102 let mid_u = (cs_domain[0] + cs_domain[1]) * 0.5;
2103 (mid_u, rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2104 }
2105 (None, None) => (cr_domain[0], rows.cr_pcurve.evaluate(cr_domain[0]).or_refuse(KernelStage::Refine, "evaluate")?),
2106 };
2107 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
2108 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
2109 let out1 = if first.face.same_sense {
2110 n1
2111 } else {
2112 n1.scale(-1.0)
2113 };
2114 let same_sense = blend_normal.dot(out1) >= 0.0;
2115 let loop_id = take_id();
2116 let cr_forward = sew_first
2124 .as_ref()
2125 .map_or(blend_cr_forward, |sew| sew.forward);
2126 let cs_forward = sew_second
2127 .as_ref()
2128 .map_or(!blend_cr_forward, |sew| sew.forward);
2129 let mut end_slot = |plan: &EndPlan,
2139 free_edge_id: Option<u64>,
2140 natural_forward: bool|
2141 -> Result<Vec<CoedgeRecord>, KernelRefusal> {
2142 match plan {
2143 EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
2144 id: take_id(),
2145 edge_id: end.arc_edge_id,
2146 forward: true,
2147 pcurve: end.arc_blend_pcurve.clone(),
2148 }]),
2149 EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
2150 id: take_id(),
2151 edge_id: end.arc_edge_id,
2152 forward: true,
2153 pcurve: end.arc_blend_pcurve.clone(),
2154 }]),
2155 EndPlan::Miter(end) => end
2156 .edges
2157 .iter()
2158 .map(|(edge_id, forward, pcurve)| {
2159 Ok(CoedgeRecord {
2160 id: take_id(),
2161 edge_id: *edge_id,
2162 forward: *forward,
2163 pcurve: pcurve.clone(),
2164 })
2165 })
2166 .collect(),
2167 EndPlan::Free(free) => {
2168 let edge_id =
2169 free_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "transverse_edge", "blend: free end without its transverse edge"))?;
2170 Ok(vec![if natural_forward {
2171 CoedgeRecord {
2172 id: take_id(),
2173 edge_id,
2174 forward: true,
2175 pcurve: free.transverse_blend_pcurve.clone(),
2176 }
2177 } else {
2178 CoedgeRecord {
2179 id: take_id(),
2180 edge_id,
2181 forward: false,
2182 pcurve: free.transverse_blend_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?,
2183 }
2184 }])
2185 }
2186 }
2187 };
2188 let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
2189 let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
2190 let coedges = if blend_cr_forward {
2193 let mut coedges = Vec::new();
2194 if let Some(cr_edge_id) = cr_edge_id {
2195 coedges.push(CoedgeRecord {
2196 id: take_id(),
2197 edge_id: cr_edge_id,
2198 forward: cr_forward,
2199 pcurve: crate::sweep_topology::parameter_line(
2200 cr_domain[0],
2201 0.0,
2202 cr_domain[1],
2203 0.0,
2204 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2205 });
2206 }
2207 coedges.extend(finish_slot);
2208 if let Some(cs_edge_id) = cs_edge_id {
2209 coedges.push(CoedgeRecord {
2210 id: take_id(),
2211 edge_id: cs_edge_id,
2212 forward: cs_forward,
2213 pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
2214 });
2215 }
2216 coedges.extend(start_slot);
2217 coedges
2218 } else {
2219 let mut coedges = Vec::new();
2220 if let Some(cr_edge_id) = cr_edge_id {
2221 coedges.push(CoedgeRecord {
2222 id: take_id(),
2223 edge_id: cr_edge_id,
2224 forward: cr_forward,
2225 pcurve: crate::sweep_topology::parameter_line(
2226 cr_domain[1],
2227 0.0,
2228 cr_domain[0],
2229 0.0,
2230 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2231 });
2232 }
2233 coedges.extend(start_slot);
2234 if let Some(cs_edge_id) = cs_edge_id {
2235 coedges.push(CoedgeRecord {
2236 id: take_id(),
2237 edge_id: cs_edge_id,
2238 forward: cs_forward,
2239 pcurve: crate::sweep_topology::parameter_line(
2240 cs_domain[0],
2241 1.0,
2242 cs_domain[1],
2243 1.0,
2244 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2245 });
2246 }
2247 coedges.extend(finish_slot);
2248 coedges
2249 };
2250 let blend_face_id = take_id();
2251 let mut blend_face = FaceRecord {
2252 id: blend_face_id,
2253 surface: rows.surface,
2254 same_sense,
2255 loops: vec![LoopRecord {
2256 id: loop_id,
2257 coedges,
2258 }],
2259 name: name.map(|value| value.to_string()),
2260 };
2261 if rows.exact_extrusion {
2262 transpose_face(&mut blend_face)?;
2263 }
2264 let shell_index = result
2265 .shells
2266 .iter()
2267 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
2268 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
2269 for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
2274 if sew.is_some() {
2275 for shell in &mut result.shells {
2276 shell.faces.retain(|face| face.id != mate.face.id);
2277 }
2278 }
2279 }
2280 result.shells[shell_index].faces.push(blend_face);
2281
2282 result.edges.retain(|candidate| candidate.id != edge.id);
2283 result
2286 .edges
2287 .retain(|candidate| !consumed_edges.contains(&candidate.id));
2288 let mut identified = Vec::new();
2290 for (collapsed, from, to) in [(cr_collapsed, w1a, w1b), (cs_collapsed, w2a, w2b)] {
2291 if collapsed && from != to {
2292 identified.push((from, to));
2293 }
2294 }
2295 Ok(SewnStripe {
2296 cr_edge_id,
2297 cs_edge_id,
2298 blend_face_id,
2299 consumed: [sew_first.is_some(), sew_second.is_some()],
2300 snap,
2301 identified,
2302 })
2303}
2304
2305pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), KernelRefusal> {
2311 let surface = &face.surface;
2312 let rows_u = surface.control_points.len();
2313 let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
2314 let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
2315 for row in &surface.control_points {
2316 for (j, point) in row.iter().enumerate() {
2317 transposed[j].push(*point);
2318 }
2319 }
2320 face.surface = NurbsSurface::new(
2321 surface.degree_v,
2322 surface.degree_u,
2323 surface.knots_v.clone(),
2324 surface.knots_u.clone(),
2325 transposed,
2326 ).or_refuse(KernelStage::Refine, "new")?;
2327 for loop_record in &mut face.loops {
2328 for coedge in &mut loop_record.coedges {
2329 for control in &mut coedge.pcurve.control_points {
2330 std::mem::swap(&mut control.x, &mut control.y);
2331 }
2332 }
2333 }
2334 face.same_sense = !face.same_sense;
2335 Ok(())
2336}
2337
2338pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
2343 let used: rustc_hash::FxHashSet<u64> = result
2344 .edges
2345 .iter()
2346 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
2347 .collect();
2348 result
2349 .vertices
2350 .retain(|candidate| used.contains(&candidate.id));
2351}
2352
2353fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
2354 crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")
2355}
2356
2357