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 )?;
117 let parameters = station_parameters(&stations);
118 let rows = fit_open_rows(
119 &stations,
120 ¶meters,
121 chamfer,
122 Some(vertex_indices),
123 extrusion_direction(edge, &first_mate, &second_mate),
124 )?;
125 let mut ends = Vec::with_capacity(2);
126 let mut in_range = true;
127 for (vertex, at_start) in [(edge.start_vertex_id, true), (edge.end_vertex_id, false)] {
128 let (end, side_in_range) = resolve_free_end(
129 solid,
130 edge_id,
131 &first_mate,
132 &second_mate,
133 &rows,
134 vertex,
135 at_start,
136 )?;
137 if debug {
138 eprintln!(
139 " [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}",
140 end.end_face_id,
141 end.first_edge_id,
142 end.first_edge_parameter,
143 end.second_edge_id,
144 end.second_edge_parameter,
145 end.cr_parameter,
146 end.cs_parameter,
147 );
148 }
149 in_range &= side_in_range;
150 ends.push(end);
151 }
152 let windows = [
157 shipped_window(¶meters, ends[0].cr_parameter, ends[1].cr_parameter),
158 shipped_window(¶meters, ends[0].cs_parameter, ends[1].cs_parameter),
159 ];
160 let (rails, rail_samples) =
161 rails_on_shipped_windows(&rows, ¶meters, &stations, [&first_mate.face.surface, &second_mate.face.surface], windows);
162 let walls = match (wall_check, windows) {
163 (false, _) => Vec::new(),
164 (true, [Some(first), Some(second)]) => open_wall_readings_why(
165 &rows,
166 ¶meters,
167 &stations,
168 [first[0].min(second[0]), first[1].max(second[1])],
169 &dense,
170 wall_radius.unwrap_or(f64::NAN),
171 ),
172 (true, _) => vec![Err("no shipped window to read".to_string())],
174 };
175 let first_unread = walls.iter().find_map(|wall| wall.as_ref().err().cloned());
176 let walls: Vec<Option<f64>> = if wall_check && windows.iter().any(|window| window.is_none()) {
177 Vec::new()
178 } else {
179 walls.into_iter().map(|wall| wall.ok()).collect()
180 };
181 let reading = OpenRungReading {
182 station_count,
183 stations: stations.len(),
184 rails,
185 rail_samples,
186 walls,
187 first_unread,
188 refine_exit: refinement.exit,
189 inserted: refinement.inserted,
190 };
191 Ok((rows, ends, in_range, reading))
192 };
193
194 let seeded_fraction: Option<f64> = (|| {
204 let span = edge.t1 - edge.t0;
205 let mut length = 0.0f64;
206 let mut previous = edge.curve.evaluate(edge.t0).ok()?;
207 for index in 1..=16 {
208 let t = edge.t0 + span * index as f64 / 16.0;
209 let point = edge.curve.evaluate(t).ok()?;
210 length += point.sub(previous).length();
211 previous = point;
212 }
213 if !(length > 0.0) || !length.is_finite() {
214 return None;
215 }
216 let mid_t = edge.t0 + span * 0.5;
217 let uv1 = edge_uv_on_face(first_mate.coedge, edge, mid_t).ok()?;
218 let uv2 = edge_uv_on_face(second_mate.coedge, edge, mid_t).ok()?;
219 let n1 = raw_normal(&first_mate.face.surface, uv1[0], uv1[1]).ok()?;
220 let n2 = raw_normal(&second_mate.face.surface, uv2[0], uv2[1]).ok()?;
221 let cos_alpha =
222 (rho1.signum() * rho2.signum() * n1.dot(n2)).clamp(-1.0, 1.0);
223 let tan_half = ((1.0 - cos_alpha).max(0.0) / (1.0 + cos_alpha).max(1e-9)).sqrt();
226 let end_radius = radius_at(edge.t0).abs().max(radius_at(edge.t1).abs());
227 let fraction = (1.5 * end_radius * tan_half / length).clamp(0.08, 0.45);
228 if !fraction.is_finite() || (fraction - 0.08).abs() < 1e-12 {
229 return None;
230 }
231 Some(fraction)
232 })();
233
234 let choose = |station_count: usize| -> Result<(FittedRows, Vec<EndSurgery>, OpenRungReading), KernelRefusal> {
241 let mut chosen: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
242 let mut last_error: Option<KernelRefusal> = None;
243 if let Some(fraction) = seeded_fraction {
247 match compute(fraction, station_count) {
248 Ok((rows, ends, true, reading)) => chosen = Some((rows, ends, reading)),
249 Ok(_) => {}
250 Err(error) if crate::blend::is_wall_fold(&error) => return Err(error),
251 Err(error) => last_error = Some(error),
252 }
253 }
254 if chosen.is_none() {
255 for &overshoot_fraction in &[0.08f64, 0.16, 0.28, 0.45] {
256 match compute(overshoot_fraction, station_count) {
257 Ok((rows, ends, in_range, reading)) => {
258 let fallback = chosen.is_none();
259 if in_range {
260 chosen = Some((rows, ends, reading));
261 break;
262 } else if fallback {
263 chosen = Some((rows, ends, reading));
264 }
265 }
266 Err(error) if crate::blend::is_wall_fold(&error) => return Err(error),
267 Err(error) => last_error = Some(error),
268 }
269 }
270 }
271 chosen.ok_or_else(|| {
272 last_error.unwrap_or_else(|| {
273 KernelRefusal::internal(
274 KernelStage::Refine,
275 "open_march",
276 "blend: open march failed at every overshoot",
277 )
278 })
279 })
280 };
281 let accepts = |reading: &OpenRungReading| {
294 reading.rails_read()
295 && reading.rails <= rail_bar
296 && (!wall_check || reading.worst_wall().is_some_and(|wall| wall <= wall_bar))
297 };
298 let meets_model = |reading: &OpenRungReading| reading.worst_wall().is_some_and(|wall| wall <= model);
299 let mut station_count = STATIONS;
300 let mut accepted: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
301 let mut unaccepted: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
304 let mut request_rungs = 0usize;
305 let mut unmet: Option<crate::BudgetReason> = None;
306 loop {
307 if accepted.is_some() {
310 request_rungs += 1;
311 }
312 let chosen = choose(station_count);
313 let (rows, ends, reading) = match chosen {
314 Ok(chosen) => chosen,
315 Err(_) if accepted.is_some() => {
317 unmet = Some(crate::BudgetReason::Incoherent);
318 break;
319 }
320 Err(error) => return Err(error),
321 };
322 #[cfg(not(test))]
323 let reject_request = false;
324 crate::blend::carve::carve_trace(format_args!(
325 "blend open edge {}: {} stations a rung ({} in all), rails {:.3e} (bar {rail_bar:.3e}), wall {:.3e} (bar {wall_bar:.3e}){}",
326 edge.id, reading.station_count, reading.stations, reading.rails,
327 reading.worst_wall().unwrap_or(f64::INFINITY),
328 if accepted.is_some() { ", a request rung" } else { "" }
329 ));
330 let accepted_now = accepts(&reading) && !reject_request;
331 if accepted.is_none() {
332 if accepted_now {
333 let met = !wall_check || meets_model(&reading);
334 accepted = Some((rows, ends, reading));
335 if met {
336 break;
337 }
338 } else if station_count < MAX_OPEN_STATIONS {
339 station_count *= 2;
340 continue;
341 } else {
342 crate::blend::record_blend_note(format!(
343 "blend: open edge {} at {} stations a rung ({} in all) — the top of the open march's \
344 ladder — still leaves its rails {:.3e} from their carriers (bar {rail_bar:.3e}) or its \
345 wall off the rolling ball (bar {wall_bar:.3e}); the wall ships as built, unaccepted",
346 edge.id, reading.station_count, reading.stations, reading.rails
347 ));
348 unaccepted = Some((rows, ends, reading));
349 break;
350 }
351 } else {
352 if !accepted_now {
353 unmet = Some(crate::BudgetReason::Rejected);
354 break;
355 }
356 let met = meets_model(&reading);
357 accepted = Some((rows, ends, reading));
358 if met {
359 unmet = None;
360 break;
361 }
362 }
363 if station_count < MAX_OPEN_STATIONS {
365 station_count *= 2;
366 } else {
367 unmet = Some(crate::BudgetReason::StationCeiling);
368 break;
369 }
370 }
371 let (shipped_accepted, (rows, ends, reading)) = match (accepted, unaccepted) {
372 (Some(rung), _) => (true, rung),
373 (None, Some(rung)) => (false, rung),
374 (None, None) => {
375 return Err(KernelRefusal::internal(KernelStage::Refine, "open_ladder", "blend: the open ladder ended without a rung"));
376 }
377 };
378 let report: Option<(crate::BudgetReason, f64, f64, String)> = if !shipped_accepted {
382 Some(acceptance_deficit(&reading, rail_bar, wall_bar, wall_check, reading.rails_read(), "no rung of the open ladder was accepted"))
383 } else if wall_check && !meets_model(&reading) {
384 let reason = unmet.unwrap_or(crate::BudgetReason::StationCeiling);
385 let declared = reading.worst_wall().unwrap_or(f64::INFINITY);
387 Some((reason, model, declared, String::new()))
388 } else {
389 None
390 };
391
392 let [start_end, finish_end] = match <[EndSurgery; 2]>::try_from(ends) {
393 Ok(pair) => pair,
394 Err(_) => return Err(KernelRefusal::internal(KernelStage::Sew, "open_surgery_ends", "blend: open surgery needs exactly two ends")),
395 };
396 let mut crossings = SpokeCrossings::default();
400 crossings.note(start_end.escalated || finish_end.escalated);
401 let mut result = solid.clone();
402 let mut take_id = fresh_id_source(solid);
403 let sewn = build_open_surgery(
404 solid,
405 &mut result,
406 &mut take_id,
407 edge,
408 &first_mate,
409 &second_mate,
410 rows,
411 [EndPlan::Free(start_end), EndPlan::Free(finish_end)],
412 name,
413 )?;
414 prune_orphan_vertices(&mut result);
415 crate::blend::fit_planar_charts_to_trims(solid, &mut result)?;
419 check_snap_closure(solid, &result, sewn.snap)?;
420 let built = crossings.gate(result)?;
421 let sewn_surface = built
424 .shells
425 .iter()
426 .flat_map(|shell| &shell.faces)
427 .find(|face| face.id == sewn.blend_face_id)
428 .map(|face| face.surface.clone())
429 .ok_or(KernelRefusal::internal(KernelStage::Sew, "open_blend_face", "blend: the sewn open blend face is not in the built body"))?;
430 if let Some((reason, bar, measured, deficit)) = report {
433 let rungs_used = if shipped_accepted { request_rungs } else { (reading.station_count / STATIONS).trailing_zeros() as usize };
439 let mut detail = format!(
440 "the shipped rung's local rail refinement ended {:?} with {} stations inserted",
441 reading.refine_exit, reading.inserted
442 );
443 if !deficit.is_empty() {
444 detail = format!("{deficit}; {detail}");
445 }
446 crate::blend::record_wall_model_report(crate::blend::WallModelReport {
447 name: name.map(str::to_string),
448 surface: sewn_surface,
449 request: bar,
450 residual: measured,
451 detail: Some(detail),
452 budget: crate::ApproximationBudget {
453 reason,
454 rounds_used: rungs_used,
455 rounds_limit: OPEN_REQUEST_RUNGS,
456 stations: reading.stations,
457 station_limit: REFINE_STATION_FACTOR * (MAX_OPEN_STATIONS + 1),
458 mechanism: crate::BudgetMechanism::StationRungs,
459 measured_component: crate::MeasuredComponent::Wall,
460 unread: 0,
461 },
462 });
463 }
464 Ok(built)
465}
466
467pub(in crate::blend) fn acceptance_deficit(
479 reading: &OpenRungReading,
480 rail_bar: f64,
481 wall_bar: f64,
482 wall_check: bool,
483 rails_read: bool,
484 what: &str,
485) -> (crate::BudgetReason, f64, f64, String) {
486 let rails = (rails_read && reading.rails.is_finite() && reading.rails >= 0.0).then_some(reading.rails);
487 let wall = if wall_check { reading.worst_wall() } else { None };
488 let unread = rails.is_none() || (wall_check && wall.is_none());
489 let mut worst = (0.0_f64, rail_bar);
490 for (value, bar) in [(rails, rail_bar), (wall, wall_bar)] {
491 if let Some(value) = value {
492 if value / bar > worst.0 / worst.1 {
493 worst = (value, bar);
494 }
495 }
496 }
497 let rail_text = match rails {
498 Some(value) => format!("rails {value:.3e} from their carriers (bar {rail_bar:.3e})"),
499 None => format!(
500 "rails UNREAD ({:?} over {} + {} samples) against bar {rail_bar:.3e}",
501 reading.rails, reading.rail_samples[0], reading.rail_samples[1]
502 ),
503 };
504 let wall_text = match (wall_check, wall) {
505 (false, _) => "the wall not judged (no constant-radius fillet provenance)".to_string(),
506 (true, Some(value)) => format!("wall {value:.3e} off the rolling ball (bar {wall_bar:.3e})"),
507 (true, None) => format!(
508 "wall UNREAD on {} of {} intervals against bar {wall_bar:.3e}{}",
509 reading.walls.iter().filter(|wall| !matches!(wall, Some(value) if value.is_finite())).count(),
510 reading.walls.len(),
511 reading.first_unread.as_deref().map(|why| format!(" (first: {why})")).unwrap_or_default()
512 ),
513 };
514 let reason = if unread { crate::BudgetReason::Unread } else { crate::BudgetReason::Unaccepted };
515 (reason, worst.1, worst.0, format!("{what}: {rail_text}, {wall_text}"))
516}
517
518const OPEN_WALL_SECTIONS: usize = 8;
521pub(in crate::blend) const OPEN_WALL_CENTRES: usize = 32;
522pub(in crate::blend) const OPEN_REQUEST_RUNGS: usize = (MAX_OPEN_STATIONS / STATIONS).trailing_zeros() as usize;
525
526#[derive(Clone, Debug)]
533pub(in crate::blend) struct OpenRungReading {
534 pub(in crate::blend) station_count: usize,
535 pub(in crate::blend) stations: usize,
536 pub(in crate::blend) rails: f64,
537 pub(in crate::blend) rail_samples: [usize; 2],
538 pub(in crate::blend) walls: Vec<Option<f64>>,
539 pub(in crate::blend) first_unread: Option<String>,
542 pub(in crate::blend) refine_exit: crate::blend::stations::StationRefineExit,
543 pub(in crate::blend) inserted: usize,
544}
545
546impl OpenRungReading {
547 pub(in crate::blend) fn rails_read(&self) -> bool {
551 self.rails.is_finite() && self.rails >= 0.0 && self.rail_samples.iter().all(|&samples| samples > 0)
552 }
553
554 pub(in crate::blend) fn worst_wall(&self) -> Option<f64> {
556 if self.walls.is_empty() {
557 return None;
558 }
559 self.walls.iter().try_fold(0.0_f64, |worst, wall| match wall {
560 Some(declared) if declared.is_finite() && *declared >= 0.0 => Some(worst.max(*declared)),
561 _ => None,
562 })
563 }
564}
565
566pub(in crate::blend) fn shipped_window(parameters: &[f64], a: f64, b: f64) -> Option<[f64; 2]> {
571 let (&first, &last) = (parameters.first()?, parameters.last()?);
572 let (low, high) = (a.min(b).max(first), a.max(b).min(last));
573 (low.is_finite() && high.is_finite() && low < high).then_some([low, high])
574}
575
576fn interval_at(parameters: &[f64], u: f64) -> Option<(usize, f64)> {
578 let last = parameters.len().checked_sub(2)?;
579 let interval = parameters.partition_point(|¶meter| parameter <= u).saturating_sub(1).min(last);
580 let (from, to) = (parameters[interval], parameters[interval + 1]);
581 (to > from).then(|| (interval, ((u - from) / (to - from)).clamp(0.0, 1.0)))
582}
583
584pub(in crate::blend) fn rails_on_shipped_windows(
593 rows: &FittedRows,
594 parameters: &[f64],
595 stations: &[Station],
596 carriers: [&NurbsSurface; 2],
597 windows: [Option<[f64; 2]>; 2],
598) -> (f64, [usize; 2]) {
599 let mut worst = 0.0_f64;
600 let mut samples = [0usize; 2];
601 for (side, (rail, carrier)) in [(&rows.cr, carriers[0]), (&rows.cs, carriers[1])].into_iter().enumerate() {
602 let Some([low, high]) = windows[side] else { continue };
603 let mut at = vec![low, high];
604 for pair in parameters.windows(2) {
605 for fraction in [0.25, 0.5, 0.75] {
606 let u = pair[0] + (pair[1] - pair[0]) * fraction;
607 if u > low && u < high {
608 at.push(u);
609 }
610 }
611 }
612 for u in at {
613 let off = interval_at(parameters, u)
614 .and_then(|(interval, fraction)| {
615 let (left, right) = (&stations[interval], &stations[interval + 1]);
616 let (a, b) = if side == 0 { (left.uv1, right.uv1) } else { (left.uv2, right.uv2) };
617 let seed = [a[0] + fraction * (b[0] - a[0]), a[1] + fraction * (b[1] - a[1])];
618 let point = rail.evaluate(u).ok()?;
619 crate::blend::stations::extended_foot_distance(carrier, point, seed).ok()
620 })
621 .filter(|off| off.is_finite())
622 .unwrap_or(f64::NAN);
623 samples[side] += 1;
624 if !worst.is_nan() && (off.is_nan() || off > worst) {
625 worst = off;
626 }
627 }
628 }
629 (worst, samples)
630}
631
632pub(in crate::blend) fn junction_stencil(
642 left_nodes: &[Vec3],
643 right_nodes: &[Vec3],
644 station_ts: &[f64],
645 left: usize,
646) -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
647 let half = OPEN_WALL_CENTRES / 2;
648 if left_nodes.len() != OPEN_WALL_CENTRES + 1 || right_nodes.len() != OPEN_WALL_CENTRES + 1 || left + 2 >= station_ts.len() {
649 return Err(format!("interval {left}: junction needs two {}-node intervals and their three station parameters", OPEN_WALL_CENTRES + 1));
650 }
651 let (t0, t1, t2) = (station_ts[left], station_ts[left + 1], station_ts[left + 2]);
652 let (w_left, w_right) = (t1 - t0, t2 - t1);
653 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()) {
654 return Err(format!("interval {left}: junction station parameters {t0}, {t1}, {t2} are not finite and strictly increasing"));
655 }
656 let nodes: Vec<Vec3> = left_nodes[half..].iter().chain(&right_nodes[1..=half]).copied().collect();
657 if w_left.to_bits() == w_right.to_bits() {
658 return Ok((nodes, None));
659 }
660 let abscissae: Vec<f64> = (half..=OPEN_WALL_CENTRES)
661 .map(|j| if j == OPEN_WALL_CENTRES { t1 } else { t0 + w_left * j as f64 / OPEN_WALL_CENTRES as f64 })
662 .chain((1..=half).map(|j| t1 + w_right * j as f64 / OPEN_WALL_CENTRES as f64))
663 .collect();
664 Ok((nodes, Some(abscissae)))
665}
666
667pub(in crate::blend) fn read_on_stencil(point: Vec3, nodes: &[Vec3], abscissae: Option<&[f64]>, step: usize, radius: f64) -> Result<(f64, f64), String> {
671 match abscissae {
672 None => crate::blend::chain::declared_wall_probe_why(point, nodes, step, radius),
673 Some(ts) => crate::blend::chain::declared_wall_probe_at_why(point, nodes, ts, step, radius),
674 }
675}
676
677pub(in crate::blend) fn open_wall_readings_why(
704 rows: &FittedRows,
705 parameters: &[f64],
706 stations: &[Station],
707 window: [f64; 2],
708 dense: &OpenDenseCentres,
709 radius: f64,
710) -> Vec<Result<f64, String>> {
711 let intervals = parameters.len().saturating_sub(1);
712 let overlapped: Vec<usize> =
713 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
714 let Ok([v_low, v_high]) = rows.surface.domain_v() else {
715 return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
716 };
717 let nodes_of = |interval: usize| -> Result<Vec<Vec3>, String> {
719 let inner = dense.centres.get(interval).ok_or_else(|| format!("interval {interval}: no dense centre row"))?;
720 if dense.subdivisions != OPEN_WALL_CENTRES || inner.len() + 1 != OPEN_WALL_CENTRES {
721 return Err(format!("interval {interval}: dense row {} centres at {} subdivisions", inner.len(), dense.subdivisions));
722 }
723 let mut centres = Vec::with_capacity(OPEN_WALL_CENTRES + 1);
724 centres.push(stations[interval].center);
725 for (j, centre) in inner.iter().enumerate() {
726 centres.push(centre.ok_or_else(|| format!("interval {interval}: exact centre {} of {OPEN_WALL_CENTRES} unsolved", j + 1))?);
727 }
728 centres.push(stations[interval + 1].center);
729 Ok(centres)
730 };
731 let half = OPEN_WALL_CENTRES / 2;
732 let junction = |left: usize| -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
739 if dense.station_ts.len() != parameters.len() {
740 return Err(format!("interval {left}: junction without the native station parameters"));
741 }
742 junction_stencil(&nodes_of(left)?, &nodes_of(left + 1)?, &dense.station_ts, left)
743 };
744 overlapped
745 .into_iter()
746 .map(|interval| -> Result<f64, String> {
747 let centres = nodes_of(interval)?;
748 let (from, to) = (parameters[interval], parameters[interval + 1]);
749 let inside = |u: f64| u >= window[0] && u <= window[1];
750 let mut probes: Vec<(f64, usize, Option<(Vec<Vec3>, Option<Vec<f64>>)>)> = (2..OPEN_WALL_CENTRES)
753 .step_by(2)
754 .map(|step| (from + (to - from) * step as f64 / OPEN_WALL_CENTRES as f64, step))
755 .filter(|(u, _)| inside(*u))
756 .map(|(u, step)| (u, step, None))
757 .collect();
758 for end in window {
759 if end > from && end < to {
760 let fraction = (end - from) / (to - from);
761 let nearest = 2 * ((fraction * OPEN_WALL_CENTRES as f64 / 2.0).round() as usize);
762 #[cfg(not(test))]
763 let junction_off = false;
764 if junction_off {
765 probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
766 } else if nearest < 2 && interval > 0 {
767 probes.push((end, half, Some(junction(interval - 1)?)));
768 } else if nearest > OPEN_WALL_CENTRES - 2 && interval + 1 < intervals {
769 probes.push((end, half, Some(junction(interval)?)));
770 } else {
771 probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
772 }
773 }
774 }
775 if probes.is_empty() {
776 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
777 }
778 let mut declared = 0.0_f64;
779 for (u, step, stencil) in probes {
780 let (nodes, abscissae, what) = match &stencil {
781 Some((nodes, None)) => (nodes, None, "junction"),
782 Some((nodes, Some(ts))) => (nodes, Some(ts), "native-abscissa junction"),
783 None => (¢res, None, "interval"),
784 };
785 for k in 1..OPEN_WALL_SECTIONS {
786 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
787 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
788 let read = if stencil.is_none() {
793 crate::blend::chain::declared_wall_probe_located_why(point, nodes, step, (u - from) / (to - from), radius)
794 } else {
795 read_on_stencil(point, nodes, abscissae.map(|ts| ts.as_slice()), step, radius)
796 };
797 let (reading, uncertainty) = read.map_err(|why| {
798 format!(
799 "interval {interval}: declared reader declined probe node {step} of the {what} stencil, section {k}/{OPEN_WALL_SECTIONS} \
800 (u {u:.17}): {why}"
801 )
802 })?;
803 declared = declared.max(reading + uncertainty);
804 }
805 }
806 if declared.is_finite() { Ok(declared) } else { Err(format!("interval {interval}: non-finite reading {declared}")) }
807 })
808 .collect()
809}
810
811
812pub(in crate::blend) fn fresh_id_source(solid: &BrepSolid) -> impl FnMut() -> u64 {
817 let mut next_id = solid
818 .vertices
819 .iter()
820 .map(|vertex| vertex.id)
821 .chain(solid.edges.iter().map(|edge| edge.id))
822 .chain(
823 solid
824 .shells
825 .iter()
826 .flat_map(|shell| &shell.faces)
827 .flat_map(|face| {
828 face.loops
829 .iter()
830 .map(|loop_record| loop_record.id)
831 .chain(face.loops.iter().flat_map(|loop_record| {
832 loop_record.coedges.iter().map(|coedge| coedge.id)
833 }))
834 .chain(std::iter::once(face.id))
835 }),
836 )
837 .max()
838 .unwrap_or(0)
839 + 1;
840 move || {
841 let id = next_id;
842 next_id += 1;
843 id
844 }
845}
846
847
848pub(in crate::blend) fn march_trim_stands(read: &Result<(f64, f64, usize, f64), KernelRefusal>, floor: f64) -> bool {
857 matches!(read, Ok((miss, standoff, _, _)) if *miss + *standoff <= floor)
858}
859
860pub(in crate::blend) fn build_open_surgery(
872 solid: &BrepSolid,
873 result: &mut BrepSolid,
874 take_id: &mut dyn FnMut() -> u64,
875 edge: &EdgeRecord,
876 first: &BlendMate,
877 second: &BlendMate,
878 rows: FittedRows,
879 ends: [EndPlan; 2],
880 name: Option<&str>,
881) -> Result<SewnStripe, KernelRefusal> {
882 let [start_end, finish_end] = ends;
883
884 let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, KernelRefusal> {
891 let [low, high] = row.domain().or_refuse(KernelStage::Refine, "domain")?;
892 let tail = if a == low { row.clone() } else { row.split(a).or_refuse(KernelStage::Refine, "split")?.1 };
893 if b == high {
894 return Ok(tail);
895 }
896 let (middle, _) = tail.split(b).or_refuse(KernelStage::Refine, "split")?;
897 Ok(middle)
898 };
899 let kind = |plan: &EndPlan| match plan {
900 EndPlan::Free(_) => "free",
901 EndPlan::Corner(_) => "corner",
902 EndPlan::Miter(_) => "miter",
903 EndPlan::Cap(_) => "cap",
904 };
905 let describe_trim = |error: KernelRefusal| {
906 error.with_message(|error| {
907 format!(
908 "{error} (edge {} rows trimmed for a {} start at cr {:.6}/cs {:.6} and a {} finish \
909 at cr {:.6}/cs {:.6})",
910 edge.id,
911 kind(&start_end),
912 start_end.cr_parameter(),
913 start_end.cs_parameter(),
914 kind(&finish_end),
915 finish_end.cr_parameter(),
916 finish_end.cs_parameter()
917 )
918 })
919 };
920 let band = consumed_band(solid);
930 let collapses = |row: &NurbsCurve, a: f64, b: f64| -> Result<bool, KernelRefusal> {
931 let from = row.evaluate(a).or_refuse(KernelStage::Refine, "evaluate")?;
932 Ok(row.evaluate(b).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band
933 && row.evaluate(0.5 * (a + b)).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band)
934 };
935 let cr_collapsed = collapses(&rows.cr, start_end.cr_parameter(), finish_end.cr_parameter())?;
936 let cs_collapsed = collapses(&rows.cs, start_end.cs_parameter(), finish_end.cs_parameter())?;
937 if cr_collapsed || cs_collapsed {
938 let unsupported = if start_end.free().is_some() && finish_end.free().is_some() {
939 Some("neither end is a corner")
940 } else if first.face.id == second.face.id {
941 Some("both rails lie on one face")
942 } else if matches!(start_end, EndPlan::Cap(_)) || matches!(finish_end, EndPlan::Cap(_)) {
943 Some("a capped end has no corner to share the point with")
944 } else {
945 None
946 };
947 if let Some(reason) = unsupported {
948 return Err(KernelRefusal::unsupported(KernelStage::Sew, super::RAIL_COLLAPSE_WHAT, format!(
949 "{RAIL_COLLAPSE_UNSUPPORTED} edge {}'s rail shrinks to a point and {reason}",
950 edge.id
951 )));
952 }
953 }
954 let trim_rail = |collapsed: bool,
968 row: &NurbsCurve,
969 pcurve: &NurbsCurve,
970 surface: &NurbsSurface,
971 a: f64,
972 b: f64|
973 -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
974 if collapsed {
975 return Ok(None);
976 }
977 let rail = trim_row(row, a, b).map_err(describe_trim)?;
978 let interpolated = trim_row(pcurve, a, b)?;
979 {
980 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
981 if crate::blend::planar_chart_overrun(surface, &|t| rail.evaluate(t), d0, d1)?.is_some_and(|excursion| excursion > band) {
982 return Ok(Some((rail, interpolated)));
983 }
984 }
985 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
990 let march_read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &interpolated, d0, d1);
991 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
992 let declined = |why: KernelRefusal| -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
993 let stands = march_trim_stands(&march_read, floor);
996 if stands {
997 Ok(Some((rail.clone(), interpolated.clone())))
998 } else {
999 Err(why)
1000 }
1001 };
1002 let refit = super::closed::refit_closed_support_pcurve(&rail, &interpolated, surface);
1003 match refit {
1004 Ok(fitted) => {
1005 let refit_read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &fitted, d0, d1);
1019 let (march_standoff, refit_standoff) = match (&march_read, refit_read) {
1020 (_, Err(error)) => return declined(error),
1021 (Err(error), Ok(_)) => {
1022 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch_unread", format!(
1023 "blend: a support trim's refit cannot be shown to stand on its rail's branch: the march's own \
1024 trim is unreadable ({})",
1025 error.message
1026 )));
1027 }
1028 (Ok((_, march_standoff, _, _)), Ok((_, refit_standoff, _, _))) => (*march_standoff, refit_standoff),
1029 };
1030 let excess = match crate::blend::track_fit::standoff_excess(surface, &|t| rail.evaluate(t), &interpolated, &fitted, d0, d1) {
1031 Ok(excess) => excess,
1032 Err(error) => return declined(error),
1033 };
1034 if !(excess <= floor) {
1035 return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch", format!(
1036 "blend: a support trim's refit stands up to {excess:.3e} farther off its rail than the march's own trim \
1037 at the same rail parameter (largest standoffs {refit_standoff:.3e} and {march_standoff:.3e}): it took \
1038 another branch of the carrier"
1039 )));
1040 }
1041 let absolute = match crate::blend::track_fit::standoff_over_nearest(surface, &|t| rail.evaluate(t), &fitted, d0, d1) {
1049 Ok(absolute) => absolute,
1050 Err(error) => return declined(error),
1051 };
1052 if !(absolute <= floor) {
1053 return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_off_rail", format!(
1054 "blend: a support trim's refit stands up to {absolute:.3e} farther off its rail than the rail stands \
1055 off its carrier: its foot is not the rail's nearest point"
1056 )));
1057 }
1058 Ok(Some((rail, fitted)))
1059 }
1060 Err(error) => declined(error),
1061 }
1062 };
1063 let cr_rail = trim_rail(
1064 cr_collapsed,
1065 &rows.cr,
1066 &rows.cr_pcurve,
1067 &first.face.surface,
1068 start_end.cr_parameter(),
1069 finish_end.cr_parameter(),
1070 )?;
1071 let cs_rail = trim_rail(
1072 cs_collapsed,
1073 &rows.cs,
1074 &rows.cs_pcurve,
1075 &second.face.surface,
1076 start_end.cs_parameter(),
1077 finish_end.cs_parameter(),
1078 )?;
1079 let cr_domain = match &cr_rail {
1081 Some((cr, _)) => cr.domain().or_refuse(KernelStage::Refine, "domain")?,
1082 None => [start_end.cr_parameter(), finish_end.cr_parameter()],
1083 };
1084 let cs_domain = match &cs_rail {
1085 Some((cs, _)) => cs.domain().or_refuse(KernelStage::Refine, "domain")?,
1086 None => [start_end.cs_parameter(), finish_end.cs_parameter()],
1087 };
1088 let cr_at = |parameter: f64| match &cr_rail {
1090 Some((cr, _)) => cr.evaluate(parameter),
1091 None => rows.cr.evaluate(parameter),
1092 };
1093 let cs_at = |parameter: f64| match &cs_rail {
1094 Some((cs, _)) => cs.evaluate(parameter),
1095 None => rows.cs.evaluate(parameter),
1096 };
1097 let old_start = edge.start_vertex_id;
1098 let old_finish = edge.end_vertex_id;
1099
1100 let classify =
1111 |boundary_id: u64, boundary_param: f64, corner: u64| -> Result<RimResolution, KernelRefusal> {
1112 let boundary = result
1113 .edges
1114 .iter()
1115 .find(|candidate| candidate.id == boundary_id)
1116 .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_boundary_edge", "blend: end boundary edge missing during surgery"))?;
1117 let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
1118 let far_vertex = if boundary.start_vertex_id == corner {
1119 Some((boundary.end_vertex_id, boundary.t1, boundary.t0))
1120 } else if boundary.end_vertex_id == corner {
1121 Some((boundary.start_vertex_id, boundary.t0, boundary.t1))
1122 } else {
1123 None
1124 };
1125 if let Some((far_id, far_t, near_t)) = far_vertex {
1126 let far_point = result
1137 .vertices
1138 .iter()
1139 .find(|candidate| candidate.id == far_id)
1140 .map(|candidate| candidate.point)
1141 .ok_or(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"))?;
1142 let crossing = boundary.curve.evaluate_extended(boundary_param).or_refuse(KernelStage::Refine, "evaluate_extended")?;
1143 let _ = span;
1144 let past_far = crossing.sub(far_point).length();
1145 if past_far <= band {
1146 if !result
1148 .vertices
1149 .iter()
1150 .any(|candidate| candidate.id == far_id)
1151 {
1152 return Err(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"));
1153 }
1154 return Ok(RimResolution::Consumed(far_id));
1155 }
1156 if (boundary_param - far_t) * (far_t - near_t) > 0.0 {
1169 return Err(KernelRefusal::unsupported(KernelStage::Sew, "blend_wider_than_face", format!(
1170 "{BLEND_WIDER_THAN_FACE} its rail crosses edge {boundary_id} {past_far:.3e} \
1171 past that edge's far vertex, and a rail within {band:.3e} of it is the \
1172 most this surgery snaps onto the edge"
1173 )));
1174 }
1175 }
1176 Ok(RimResolution::Fresh)
1177 };
1178 let resolve_side = |plan: &EndPlan,
1182 side_first: bool,
1183 corner: u64|
1184 -> Result<RimResolution, KernelRefusal> {
1185 if let Some(vertex) = plan.planned_rim(side_first) {
1186 return Ok(RimResolution::Corner(vertex));
1187 }
1188 match plan {
1189 EndPlan::Free(free) => classify(
1190 if side_first {
1191 free.first_edge_id
1192 } else {
1193 free.second_edge_id
1194 },
1195 if side_first {
1196 free.first_edge_parameter
1197 } else {
1198 free.second_edge_parameter
1199 },
1200 corner,
1201 ),
1202 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => {
1203 Err(KernelRefusal::internal(KernelStage::Sew, "rim_vertices", "blend: planned end without rim vertices"))
1204 }
1205 }
1206 };
1207 let start_first = resolve_side(&start_end, true, old_start)?;
1208 let start_second = resolve_side(&start_end, false, old_start)?;
1209 let finish_first = resolve_side(&finish_end, true, old_finish)?;
1210 let finish_second = resolve_side(&finish_end, false, old_finish)?;
1211
1212 let first_use_forward = first.coedge.forward;
1215 let blend_cr_forward = !first_use_forward;
1216
1217 let (sew_first, sew_second) = match (start_end.free(), finish_end.free(), &cr_rail, &cs_rail) {
1232 (Some(start_free), Some(finish_free), Some((cr, _)), Some((cs, _))) => (
1233 detect_row_coincidence(
1234 solid,
1235 first,
1236 edge.id,
1237 start_free.first_edge_id,
1238 finish_free.first_edge_id,
1239 start_first.is_consumed(),
1240 start_first.existing().unwrap_or(0),
1241 finish_first.is_consumed(),
1242 finish_first.existing().unwrap_or(0),
1243 cr,
1244 blend_cr_forward,
1245 band,
1246 )?,
1247 detect_row_coincidence(
1248 solid,
1249 second,
1250 edge.id,
1251 start_free.second_edge_id,
1252 finish_free.second_edge_id,
1253 start_second.is_consumed(),
1254 start_second.existing().unwrap_or(0),
1255 finish_second.is_consumed(),
1256 finish_second.existing().unwrap_or(0),
1257 cs,
1258 !blend_cr_forward,
1259 band,
1260 )?,
1261 ),
1262 _ => (None, None),
1263 };
1264 let (sew_first, sew_second): (Option<RowSew>, Option<RowSew>) =
1268 if first.face.id == second.face.id {
1269 (None, None)
1270 } else {
1271 (sew_first, sew_second)
1272 };
1273
1274 let mut resolve = |rim: &RimResolution, point: Result<Vec3, String>| -> Result<u64, KernelRefusal> {
1277 match rim.existing() {
1278 Some(id) => Ok(id),
1279 None => {
1280 let id = take_id();
1281 result.vertices.push(VertexRecord { id, point: point.or_refuse(KernelStage::Refine, "point")? });
1282 Ok(id)
1283 }
1284 }
1285 };
1286 let w1a = resolve(&start_first, cr_at(cr_domain[0]))?;
1287 let w1b = resolve(&finish_first, cr_at(cr_domain[1]))?;
1288 let w2a = resolve(&start_second, cs_at(cs_domain[0]))?;
1289 let w2b = resolve(&finish_second, cs_at(cs_domain[1]))?;
1290 let mut snap = [&sew_first, &sew_second]
1293 .into_iter()
1294 .flatten()
1295 .fold(0.0f64, |worst, sew| worst.max(sew.deviation));
1296 for (rim, vertex, row_end) in [
1297 (&start_first, w1a, cr_at(cr_domain[0]).or_refuse(KernelStage::Refine, "cr_at")?),
1298 (&finish_first, w1b, cr_at(cr_domain[1]).or_refuse(KernelStage::Refine, "cr_at")?),
1299 (&start_second, w2a, cs_at(cs_domain[0]).or_refuse(KernelStage::Refine, "cs_at")?),
1300 (&finish_second, w2b, cs_at(cs_domain[1]).or_refuse(KernelStage::Refine, "cs_at")?),
1301 ] {
1302 if !rim.is_consumed() {
1303 continue;
1304 }
1305 let point = result
1306 .vertices
1307 .iter()
1308 .find(|candidate| candidate.id == vertex)
1309 .ok_or(KernelRefusal::internal(KernelStage::Sew, "rim_vertex", "blend: consumed rim vertex missing"))?
1310 .point;
1311 snap = snap.max(row_end.sub(point).length());
1312 }
1313
1314 let cr_edge_id = match (&sew_first, &cr_rail) {
1319 (Some(sew), _) => Some(sew.edge_id),
1320 (None, None) => None,
1321 (None, Some((cr, _))) => {
1322 let id = take_id();
1323 result.edges.push(EdgeRecord {
1324 id,
1325 curve: cr.clone(),
1326 t0: cr_domain[0],
1327 t1: cr_domain[1],
1328 start_vertex_id: w1a,
1329 end_vertex_id: w1b,
1330 degenerate: false,
1331 name: None,
1332 });
1333 Some(id)
1334 }
1335 };
1336 let cs_edge_id = match (&sew_second, &cs_rail) {
1337 (Some(sew), _) => Some(sew.edge_id),
1338 (None, None) => None,
1339 (None, Some((cs, _))) => {
1340 let id = take_id();
1341 result.edges.push(EdgeRecord {
1342 id,
1343 curve: cs.clone(),
1344 t0: cs_domain[0],
1345 t1: cs_domain[1],
1346 start_vertex_id: w2a,
1347 end_vertex_id: w2b,
1348 degenerate: false,
1349 name: None,
1350 });
1351 Some(id)
1352 }
1353 };
1354 let mut commit_end_edge = |plan: &EndPlan,
1359 first_rim: u64,
1360 second_rim: u64|
1361 -> Result<Option<u64>, KernelRefusal> {
1362 match plan {
1363 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
1364 EndPlan::Free(free) => {
1365 let id = take_id();
1366 let domain = free.transverse_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1367 result.edges.push(EdgeRecord {
1368 id,
1369 curve: free.transverse_curve.clone(),
1370 t0: domain[0],
1371 t1: domain[1],
1372 start_vertex_id: first_rim,
1373 end_vertex_id: second_rim,
1374 degenerate: false,
1375 name: None,
1376 });
1377 Ok(Some(id))
1378 }
1379 }
1380 };
1381 let transverse_a_id = commit_end_edge(&start_end, w1a, w2a)?;
1382 let transverse_b_id = commit_end_edge(&finish_end, w1b, w2b)?;
1383
1384 for (mate, rail, sewn) in [
1391 (first, cr_edge_id.zip(cr_rail.as_ref()), sew_first.is_some()),
1392 (second, cs_edge_id.zip(cs_rail.as_ref()), sew_second.is_some()),
1393 ] {
1394 if sewn {
1395 continue;
1396 }
1397 let face = result
1398 .shells
1399 .iter_mut()
1400 .flat_map(|shell| &mut shell.faces)
1401 .find(|face| face.id == mate.face.id)
1402 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1403 let loop_record = &mut face.loops[mate.loop_index];
1404 let position = loop_record
1405 .coedges
1406 .iter()
1407 .position(|coedge| coedge.edge_id == edge.id)
1408 .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
1409 let Some((new_edge_id, (_, pcurve_forward))) = rail else {
1410 loop_record.coedges.remove(position);
1411 continue;
1412 };
1413 let old_forward = loop_record.coedges[position].forward;
1414 loop_record.coedges[position] = CoedgeRecord {
1415 id: loop_record.coedges[position].id,
1416 edge_id: new_edge_id,
1417 forward: old_forward,
1418 pcurve: if old_forward {
1419 pcurve_forward.clone()
1420 } else {
1421 pcurve_forward.reversed().or_refuse(KernelStage::Refine, "reversed")?
1422 },
1423 };
1424 }
1425
1426 for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
1431 let EndPlan::Cap(cap) = plan else {
1432 continue;
1433 };
1434 for (mate, rail_edge_id, leg, rim) in [
1435 (first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
1436 (second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
1437 ] {
1438 let rail_edge_id = rail_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "end_rail", "blend: a capped end lost its rail"))?;
1440 let face = result
1441 .shells
1442 .iter_mut()
1443 .flat_map(|shell| &mut shell.faces)
1444 .find(|face| face.id == mate.face.id)
1445 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during cap splice"))?;
1446 let loop_record = &mut face.loops[mate.loop_index];
1447 let count = loop_record.coedges.len();
1448 let rail_at = loop_record
1449 .coedges
1450 .iter()
1451 .position(|coedge| coedge.edge_id == rail_edge_id)
1452 .ok_or(KernelRefusal::internal(KernelStage::Sew, "rail_coedge", "blend: rail coedge lost during cap splice"))?;
1453 let rail_forward = loop_record.coedges[rail_at].forward;
1459 let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
1460 let (insert_at, forward, pcurve) = if end_is_traversal_end {
1461 (rail_at + 1, true, leg.1.clone())
1463 } else {
1464 (rail_at, false, leg.1.reversed().or_refuse(KernelStage::Refine, "reversed")?)
1466 };
1467 let _ = (count, rim);
1468 loop_record.coedges.insert(
1469 insert_at,
1470 CoedgeRecord {
1471 id: take_id(),
1472 edge_id: leg.0,
1473 forward,
1474 pcurve,
1475 },
1476 );
1477 }
1478 }
1479
1480 let mut consumed_edges: Vec<u64> = Vec::new();
1486 let start_free = start_end.free();
1487 let finish_free = finish_end.free();
1488 for (mate, sewn, sides) in [
1489 (
1490 first,
1491 sew_first.is_some(),
1492 [
1493 (&start_first, start_free.map(|free| free.first_edge_id)),
1494 (&finish_first, finish_free.map(|free| free.first_edge_id)),
1495 ],
1496 ),
1497 (
1498 second,
1499 sew_second.is_some(),
1500 [
1501 (&start_second, start_free.map(|free| free.second_edge_id)),
1502 (&finish_second, finish_free.map(|free| free.second_edge_id)),
1503 ],
1504 ),
1505 ] {
1506 for (cross, boundary_id) in sides {
1507 if !cross.is_consumed() {
1508 continue;
1509 }
1510 let Some(boundary_id) = boundary_id else {
1511 continue;
1512 };
1513 if !sewn {
1514 let face = result
1515 .shells
1516 .iter_mut()
1517 .flat_map(|shell| &mut shell.faces)
1518 .find(|face| face.id == mate.face.id)
1519 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1520 face.loops[mate.loop_index]
1521 .coedges
1522 .retain(|coedge| coedge.edge_id != boundary_id);
1523 }
1524 if !consumed_edges.contains(&boundary_id) {
1525 consumed_edges.push(boundary_id);
1526 }
1527 }
1528 }
1529 for sew in [&sew_first, &sew_second].into_iter().flatten() {
1531 for id in &sew.collapsed_edges {
1532 if !consumed_edges.contains(id) {
1533 consumed_edges.push(*id);
1534 }
1535 }
1536 }
1537
1538 let mut end_plans = Vec::with_capacity(2);
1547 for (end, transverse_id, corner, first_consumed, second_consumed) in [
1548 (
1549 start_end.free(),
1550 transverse_a_id,
1551 old_start,
1552 start_first.is_consumed(),
1553 start_second.is_consumed(),
1554 ),
1555 (
1556 finish_end.free(),
1557 transverse_b_id,
1558 old_finish,
1559 finish_first.is_consumed(),
1560 finish_second.is_consumed(),
1561 ),
1562 ] {
1563 let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
1564 continue;
1565 };
1566 let face = result
1567 .shells
1568 .iter()
1569 .flat_map(|shell| &shell.faces)
1570 .find(|face| face.id == end.end_face_id)
1571 .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_face", "blend: end face lost during surgery"))?;
1572 let Some(located) = locate_end_corner(
1575 &result,
1576 face,
1577 corner,
1578 end.first_edge_id,
1579 end.second_edge_id,
1580 ) else {
1581 return Err(KernelRefusal::internal(KernelStage::Sew, "end_face_corner", "blend: end-face corner (adjacent boundary coedges) not found"));
1582 };
1583 for pole in &located.pole_edge_ids {
1586 if !consumed_edges.contains(pole) {
1587 consumed_edges.push(*pole);
1588 }
1589 }
1590 end_plans.push((
1591 end.end_face_id,
1592 transverse_id,
1593 end.transverse_end_pcurve.clone(),
1594 located,
1595 first_consumed,
1596 second_consumed,
1597 ));
1598 }
1599
1600 for (rim_resolution, boundary, corner, rim) in [
1603 (
1604 &start_first,
1605 start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
1606 old_start,
1607 w1a,
1608 ),
1609 (
1610 &start_second,
1611 start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
1612 old_start,
1613 w2a,
1614 ),
1615 (
1616 &finish_first,
1617 finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
1618 old_finish,
1619 w1b,
1620 ),
1621 (
1622 &finish_second,
1623 finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
1624 old_finish,
1625 w2b,
1626 ),
1627 ] {
1628 if !rim_resolution.trims_boundary() {
1629 continue;
1630 }
1631 let Some((boundary_id, boundary_param)) = boundary else {
1632 continue;
1633 };
1634 trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
1635 }
1636
1637 for (end_face, transverse_id, transverse_end_pcurve, located, first_consumed, second_consumed) in
1640 end_plans
1641 {
1642 let forward = located.x_is_first;
1643 let pcurve = if forward {
1644 transverse_end_pcurve
1645 } else {
1646 transverse_end_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
1647 };
1648 let (x_consumed, y_consumed) = if located.x_is_first {
1649 (first_consumed, second_consumed)
1650 } else {
1651 (second_consumed, first_consumed)
1652 };
1653 let transverse_coedge = CoedgeRecord {
1654 id: take_id(),
1655 edge_id: transverse_id,
1656 forward,
1657 pcurve,
1658 };
1659 splice_end_corner(
1660 result,
1661 end_face,
1662 &located,
1663 transverse_coedge,
1664 x_consumed,
1665 y_consumed,
1666 )?;
1667 }
1668
1669 let (mid_u, station_uv) = match (&cr_rail, &cs_rail) {
1674 (Some((_, cr_pcurve)), _) => {
1675 let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
1676 (mid_u, cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
1677 }
1678 (None, Some(_)) => {
1679 let mid_u = (cs_domain[0] + cs_domain[1]) * 0.5;
1680 (mid_u, rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
1681 }
1682 (None, None) => (cr_domain[0], rows.cr_pcurve.evaluate(cr_domain[0]).or_refuse(KernelStage::Refine, "evaluate")?),
1683 };
1684 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
1685 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
1686 let out1 = if first.face.same_sense {
1687 n1
1688 } else {
1689 n1.scale(-1.0)
1690 };
1691 let same_sense = blend_normal.dot(out1) >= 0.0;
1692 let loop_id = take_id();
1693 let cr_forward = sew_first
1701 .as_ref()
1702 .map_or(blend_cr_forward, |sew| sew.forward);
1703 let cs_forward = sew_second
1704 .as_ref()
1705 .map_or(!blend_cr_forward, |sew| sew.forward);
1706 let mut end_slot = |plan: &EndPlan,
1716 free_edge_id: Option<u64>,
1717 natural_forward: bool|
1718 -> Result<Vec<CoedgeRecord>, KernelRefusal> {
1719 match plan {
1720 EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
1721 id: take_id(),
1722 edge_id: end.arc_edge_id,
1723 forward: true,
1724 pcurve: end.arc_blend_pcurve.clone(),
1725 }]),
1726 EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
1727 id: take_id(),
1728 edge_id: end.arc_edge_id,
1729 forward: true,
1730 pcurve: end.arc_blend_pcurve.clone(),
1731 }]),
1732 EndPlan::Miter(end) => end
1733 .edges
1734 .iter()
1735 .map(|(edge_id, forward, pcurve)| {
1736 Ok(CoedgeRecord {
1737 id: take_id(),
1738 edge_id: *edge_id,
1739 forward: *forward,
1740 pcurve: pcurve.clone(),
1741 })
1742 })
1743 .collect(),
1744 EndPlan::Free(free) => {
1745 let edge_id =
1746 free_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "transverse_edge", "blend: free end without its transverse edge"))?;
1747 Ok(vec![if natural_forward {
1748 CoedgeRecord {
1749 id: take_id(),
1750 edge_id,
1751 forward: true,
1752 pcurve: free.transverse_blend_pcurve.clone(),
1753 }
1754 } else {
1755 CoedgeRecord {
1756 id: take_id(),
1757 edge_id,
1758 forward: false,
1759 pcurve: free.transverse_blend_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?,
1760 }
1761 }])
1762 }
1763 }
1764 };
1765 let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
1766 let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
1767 let coedges = if blend_cr_forward {
1770 let mut coedges = Vec::new();
1771 if let Some(cr_edge_id) = cr_edge_id {
1772 coedges.push(CoedgeRecord {
1773 id: take_id(),
1774 edge_id: cr_edge_id,
1775 forward: cr_forward,
1776 pcurve: crate::sweep_topology::parameter_line(
1777 cr_domain[0],
1778 0.0,
1779 cr_domain[1],
1780 0.0,
1781 ).or_refuse(KernelStage::Refine, "parameter_line")?,
1782 });
1783 }
1784 coedges.extend(finish_slot);
1785 if let Some(cs_edge_id) = cs_edge_id {
1786 coedges.push(CoedgeRecord {
1787 id: take_id(),
1788 edge_id: cs_edge_id,
1789 forward: cs_forward,
1790 pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
1791 });
1792 }
1793 coedges.extend(start_slot);
1794 coedges
1795 } else {
1796 let mut coedges = Vec::new();
1797 if let Some(cr_edge_id) = cr_edge_id {
1798 coedges.push(CoedgeRecord {
1799 id: take_id(),
1800 edge_id: cr_edge_id,
1801 forward: cr_forward,
1802 pcurve: crate::sweep_topology::parameter_line(
1803 cr_domain[1],
1804 0.0,
1805 cr_domain[0],
1806 0.0,
1807 ).or_refuse(KernelStage::Refine, "parameter_line")?,
1808 });
1809 }
1810 coedges.extend(start_slot);
1811 if let Some(cs_edge_id) = cs_edge_id {
1812 coedges.push(CoedgeRecord {
1813 id: take_id(),
1814 edge_id: cs_edge_id,
1815 forward: cs_forward,
1816 pcurve: crate::sweep_topology::parameter_line(
1817 cs_domain[0],
1818 1.0,
1819 cs_domain[1],
1820 1.0,
1821 ).or_refuse(KernelStage::Refine, "parameter_line")?,
1822 });
1823 }
1824 coedges.extend(finish_slot);
1825 coedges
1826 };
1827 let blend_face_id = take_id();
1828 let mut blend_face = FaceRecord {
1829 id: blend_face_id,
1830 surface: rows.surface,
1831 same_sense,
1832 loops: vec![LoopRecord {
1833 id: loop_id,
1834 coedges,
1835 }],
1836 name: name.map(|value| value.to_string()),
1837 };
1838 if rows.exact_extrusion {
1839 transpose_face(&mut blend_face)?;
1840 }
1841 let shell_index = result
1842 .shells
1843 .iter()
1844 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
1845 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
1846 for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
1851 if sew.is_some() {
1852 for shell in &mut result.shells {
1853 shell.faces.retain(|face| face.id != mate.face.id);
1854 }
1855 }
1856 }
1857 result.shells[shell_index].faces.push(blend_face);
1858
1859 result.edges.retain(|candidate| candidate.id != edge.id);
1860 result
1863 .edges
1864 .retain(|candidate| !consumed_edges.contains(&candidate.id));
1865 let mut identified = Vec::new();
1867 for (collapsed, from, to) in [(cr_collapsed, w1a, w1b), (cs_collapsed, w2a, w2b)] {
1868 if collapsed && from != to {
1869 identified.push((from, to));
1870 }
1871 }
1872 Ok(SewnStripe {
1873 cr_edge_id,
1874 cs_edge_id,
1875 blend_face_id,
1876 consumed: [sew_first.is_some(), sew_second.is_some()],
1877 snap,
1878 identified,
1879 })
1880}
1881
1882pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), KernelRefusal> {
1888 let surface = &face.surface;
1889 let rows_u = surface.control_points.len();
1890 let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
1891 let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
1892 for row in &surface.control_points {
1893 for (j, point) in row.iter().enumerate() {
1894 transposed[j].push(*point);
1895 }
1896 }
1897 face.surface = NurbsSurface::new(
1898 surface.degree_v,
1899 surface.degree_u,
1900 surface.knots_v.clone(),
1901 surface.knots_u.clone(),
1902 transposed,
1903 ).or_refuse(KernelStage::Refine, "new")?;
1904 for loop_record in &mut face.loops {
1905 for coedge in &mut loop_record.coedges {
1906 for control in &mut coedge.pcurve.control_points {
1907 std::mem::swap(&mut control.x, &mut control.y);
1908 }
1909 }
1910 }
1911 face.same_sense = !face.same_sense;
1912 Ok(())
1913}
1914
1915pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
1920 let used: rustc_hash::FxHashSet<u64> = result
1921 .edges
1922 .iter()
1923 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
1924 .collect();
1925 result
1926 .vertices
1927 .retain(|candidate| used.contains(&candidate.id));
1928}
1929
1930fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
1931 crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")
1932}
1933