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 let mut worst = 0.0_f64;
701 let mut samples = [0usize; 2];
702 let affine = rows.exact_extrusion && affine_reads_on();
703 for (side, (rail, carrier)) in [(&rows.cr, carriers[0]), (&rows.cs, carriers[1])].into_iter().enumerate() {
704 let Some([low, high]) = windows[side] else { continue };
705 if let Some(off) = affine.then(|| affine_rail_standoff(rail, carrier, [low, high])).flatten() {
708 crate::blend::record_count("count.read.affine_rails", 1);
709 samples[side] += 2;
710 if !worst.is_nan() && off > worst {
711 worst = off;
712 }
713 continue;
714 }
715 let mut at = vec![low, high];
716 for pair in parameters.windows(2) {
717 for fraction in [0.25, 0.5, 0.75] {
718 let u = pair[0] + (pair[1] - pair[0]) * fraction;
719 if u > low && u < high {
720 at.push(u);
721 }
722 }
723 }
724 for u in at {
725 let off = interval_at(parameters, u)
726 .and_then(|(interval, fraction)| {
727 let (left, right) = (&stations[interval], &stations[interval + 1]);
728 let (a, b) = if side == 0 { (left.uv1, right.uv1) } else { (left.uv2, right.uv2) };
729 let seed = [a[0] + fraction * (b[0] - a[0]), a[1] + fraction * (b[1] - a[1])];
730 let point = rail.evaluate(u).ok()?;
731 crate::blend::stations::extended_foot_distance(carrier, point, seed).ok()
732 })
733 .filter(|off| off.is_finite())
734 .unwrap_or(f64::NAN);
735 samples[side] += 1;
736 if !worst.is_nan() && (off.is_nan() || off > worst) {
737 worst = off;
738 }
739 }
740 }
741 (worst, samples)
742}
743
744pub(in crate::blend) fn junction_stencil(
754 left_nodes: &[Vec3],
755 right_nodes: &[Vec3],
756 station_ts: &[f64],
757 left: usize,
758) -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
759 let half = OPEN_WALL_CENTRES / 2;
760 if left_nodes.len() != OPEN_WALL_CENTRES + 1 || right_nodes.len() != OPEN_WALL_CENTRES + 1 || left + 2 >= station_ts.len() {
761 return Err(format!("interval {left}: junction needs two {}-node intervals and their three station parameters", OPEN_WALL_CENTRES + 1));
762 }
763 let (t0, t1, t2) = (station_ts[left], station_ts[left + 1], station_ts[left + 2]);
764 let (w_left, w_right) = (t1 - t0, t2 - t1);
765 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()) {
766 return Err(format!("interval {left}: junction station parameters {t0}, {t1}, {t2} are not finite and strictly increasing"));
767 }
768 let nodes: Vec<Vec3> = left_nodes[half..].iter().chain(&right_nodes[1..=half]).copied().collect();
769 if w_left.to_bits() == w_right.to_bits() {
770 return Ok((nodes, None));
771 }
772 let abscissae: Vec<f64> = (half..=OPEN_WALL_CENTRES)
773 .map(|j| if j == OPEN_WALL_CENTRES { t1 } else { t0 + w_left * j as f64 / OPEN_WALL_CENTRES as f64 })
774 .chain((1..=half).map(|j| t1 + w_right * j as f64 / OPEN_WALL_CENTRES as f64))
775 .collect();
776 Ok((nodes, Some(abscissae)))
777}
778
779pub(in crate::blend) fn read_on_stencil(point: Vec3, nodes: &[Vec3], abscissae: Option<&[f64]>, step: usize, radius: f64) -> Result<(f64, f64), String> {
783 match abscissae {
784 None => crate::blend::chain::declared_wall_probe_why(point, nodes, step, radius),
785 Some(ts) => crate::blend::chain::declared_wall_probe_at_why(point, nodes, ts, step, radius),
786 }
787}
788
789pub(in crate::blend) fn exact_extrusion_wall_readings(
824 rows: &FittedRows,
825 parameters: &[f64],
826 window: [f64; 2],
827 radius: f64,
828) -> Vec<Result<f64, String>> {
829 let intervals = parameters.len().saturating_sub(1);
830 let overlapped: Vec<usize> =
831 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
832 let line = match (rows.exact_extrusion, rows.center.as_ref()) {
833 (true, Some(center)) => center.domain().and_then(|[a, b]| Ok((center.evaluate(a)?, center.evaluate(b)?))),
834 (false, _) => Err("the rows are not an exact extrusion".to_string()),
835 (true, None) => Err("the exact rows carry no centre line".to_string()),
836 };
837 let (anchor, direction) = match line.and_then(|(a, b)| b.sub(a).normalized().map(|d| (a, d))) {
838 Ok(line) => line,
839 Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
840 };
841 let Ok([v_low, v_high]) = rows.surface.domain_v() else {
842 return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
843 };
844 overlapped
845 .into_iter()
846 .map(|interval| -> Result<f64, String> {
847 let (from, to) = (parameters[interval], parameters[interval + 1]);
848 let inside = |u: f64| u >= window[0] && u <= window[1];
849 let mut probes: Vec<f64> =
850 (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
851 probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
852 if probes.is_empty() {
853 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
854 }
855 let mut worst = 0.0_f64;
856 for u in probes {
857 for k in 1..OPEN_WALL_SECTIONS {
858 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
859 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
860 let offset = point.sub(anchor);
861 let standoff = offset.sub(direction.scale(offset.dot(direction))).length();
862 let reading = (standoff - radius).abs();
863 if !reading.is_finite() {
864 return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
865 }
866 worst = worst.max(reading);
867 }
868 }
869 Ok(worst)
870 })
871 .collect()
872}
873
874pub(in crate::blend) fn exact_revolution_wall_readings(
885 rows: &FittedRows,
886 parameters: &[f64],
887 window: [f64; 2],
888 radius: f64,
889) -> Vec<Result<f64, String>> {
890 let intervals = parameters.len().saturating_sub(1);
891 let overlapped: Vec<usize> =
892 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
893 let circle = match (rows.revolution_axis, rows.center.as_ref()) {
894 (Some((origin, axis)), Some(center)) => center.domain().and_then(|[a, _]| center.evaluate(a)).map(|point| (origin, axis, point)),
895 (None, _) => Err("the rows are not an exact revolution".to_string()),
896 (Some(_), None) => Err("the exact rows carry no centre circle".to_string()),
897 };
898 let (origin, axis, centre_radius, centre_height) = match circle {
899 Ok((origin, axis, point)) => {
900 let offset = point.sub(origin);
901 let height = offset.dot(axis);
902 (origin, axis, offset.sub(axis.scale(height)).length(), height)
903 }
904 Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
905 };
906 let Ok([v_low, v_high]) = rows.surface.domain_v() else {
907 return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
908 };
909 overlapped
910 .into_iter()
911 .map(|interval| -> Result<f64, String> {
912 let (from, to) = (parameters[interval], parameters[interval + 1]);
913 let inside = |u: f64| u >= window[0] && u <= window[1];
914 let mut probes: Vec<f64> =
915 (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
916 probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
917 if probes.is_empty() {
918 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
919 }
920 let mut worst = 0.0_f64;
921 for u in probes {
922 for k in 1..OPEN_WALL_SECTIONS {
923 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
924 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
925 let offset = point.sub(origin);
926 let height = offset.dot(axis);
927 let radial = offset.sub(axis.scale(height)).length();
928 let standoff = (radial - centre_radius).hypot(height - centre_height);
929 let reading = (standoff - radius).abs();
930 if !reading.is_finite() {
931 return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
932 }
933 worst = worst.max(reading);
934 }
935 }
936 Ok(worst)
937 })
938 .collect()
939}
940
941pub(in crate::blend) fn open_wall_readings_why(
942 rows: &FittedRows,
943 parameters: &[f64],
944 stations: &[Station],
945 window: [f64; 2],
946 dense: &OpenDenseCentres,
947 radius: f64,
948) -> Vec<Result<f64, String>> {
949 let intervals = parameters.len().saturating_sub(1);
950 let overlapped: Vec<usize> =
951 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
952 let Ok([v_low, v_high]) = rows.surface.domain_v() else {
953 return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
954 };
955 let nodes_of = |interval: usize| -> Result<Vec<Vec3>, String> {
957 let inner = dense.centres.get(interval).ok_or_else(|| format!("interval {interval}: no dense centre row"))?;
958 if dense.subdivisions != OPEN_WALL_CENTRES || inner.len() + 1 != OPEN_WALL_CENTRES {
959 return Err(format!("interval {interval}: dense row {} centres at {} subdivisions", inner.len(), dense.subdivisions));
960 }
961 let mut centres = Vec::with_capacity(OPEN_WALL_CENTRES + 1);
962 centres.push(stations[interval].center);
963 for (j, centre) in inner.iter().enumerate() {
964 centres.push(centre.ok_or_else(|| format!("interval {interval}: exact centre {} of {OPEN_WALL_CENTRES} unsolved", j + 1))?);
965 }
966 centres.push(stations[interval + 1].center);
967 Ok(centres)
968 };
969 let half = OPEN_WALL_CENTRES / 2;
970 let junction = |left: usize| -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
977 if dense.station_ts.len() != parameters.len() {
978 return Err(format!("interval {left}: junction without the native station parameters"));
979 }
980 junction_stencil(&nodes_of(left)?, &nodes_of(left + 1)?, &dense.station_ts, left)
981 };
982 overlapped
983 .into_iter()
984 .map(|interval| -> Result<f64, String> {
985 let centres = nodes_of(interval)?;
986 let (from, to) = (parameters[interval], parameters[interval + 1]);
987 let inside = |u: f64| u >= window[0] && u <= window[1];
988 let mut probes: Vec<(f64, usize, Option<(Vec<Vec3>, Option<Vec<f64>>)>)> = (2..OPEN_WALL_CENTRES)
991 .step_by(2)
992 .map(|step| (from + (to - from) * step as f64 / OPEN_WALL_CENTRES as f64, step))
993 .filter(|(u, _)| inside(*u))
994 .map(|(u, step)| (u, step, None))
995 .collect();
996 for end in window {
997 if end > from && end < to {
998 let fraction = (end - from) / (to - from);
999 let nearest = 2 * ((fraction * OPEN_WALL_CENTRES as f64 / 2.0).round() as usize);
1000 #[cfg(not(test))]
1001 let junction_off = false;
1002 if junction_off {
1003 probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1004 } else if nearest < 2 && interval > 0 {
1005 probes.push((end, half, Some(junction(interval - 1)?)));
1006 } else if nearest > OPEN_WALL_CENTRES - 2 && interval + 1 < intervals {
1007 probes.push((end, half, Some(junction(interval)?)));
1008 } else {
1009 probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1010 }
1011 }
1012 }
1013 if probes.is_empty() {
1014 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1015 }
1016 let mut declared = 0.0_f64;
1017 for (u, step, stencil) in probes {
1018 let (nodes, abscissae, what) = match &stencil {
1019 Some((nodes, None)) => (nodes, None, "junction"),
1020 Some((nodes, Some(ts))) => (nodes, Some(ts), "native-abscissa junction"),
1021 None => (¢res, None, "interval"),
1022 };
1023 for k in 1..OPEN_WALL_SECTIONS {
1024 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1025 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1026 let read = if stencil.is_none() {
1031 crate::blend::chain::declared_wall_probe_located_why(point, nodes, step, (u - from) / (to - from), radius)
1032 } else {
1033 read_on_stencil(point, nodes, abscissae.map(|ts| ts.as_slice()), step, radius)
1034 };
1035 let (reading, uncertainty) = read.map_err(|why| {
1036 format!(
1037 "interval {interval}: declared reader declined probe node {step} of the {what} stencil, section {k}/{OPEN_WALL_SECTIONS} \
1038 (u {u:.17}): {why}"
1039 )
1040 })?;
1041 declared = declared.max(reading + uncertainty);
1042 }
1043 }
1044 if declared.is_finite() { Ok(declared) } else { Err(format!("interval {interval}: non-finite reading {declared}")) }
1045 })
1046 .collect()
1047}
1048
1049
1050pub(in crate::blend) fn fresh_id_source(solid: &BrepSolid) -> impl FnMut() -> u64 {
1055 let mut next_id = solid
1056 .vertices
1057 .iter()
1058 .map(|vertex| vertex.id)
1059 .chain(solid.edges.iter().map(|edge| edge.id))
1060 .chain(
1061 solid
1062 .shells
1063 .iter()
1064 .flat_map(|shell| &shell.faces)
1065 .flat_map(|face| {
1066 face.loops
1067 .iter()
1068 .map(|loop_record| loop_record.id)
1069 .chain(face.loops.iter().flat_map(|loop_record| {
1070 loop_record.coedges.iter().map(|coedge| coedge.id)
1071 }))
1072 .chain(std::iter::once(face.id))
1073 }),
1074 )
1075 .max()
1076 .unwrap_or(0)
1077 + 1;
1078 move || {
1079 let id = next_id;
1080 next_id += 1;
1081 id
1082 }
1083}
1084
1085
1086pub(in crate::blend) fn march_trim_stands(read: &Result<(f64, f64, usize, f64), KernelRefusal>, floor: f64) -> bool {
1095 matches!(read, Ok((miss, standoff, _, _)) if *miss + *standoff <= floor)
1096}
1097
1098fn exact_planar_trim(rail: &NurbsCurve, surface: &NurbsSurface, d0: f64, d1: f64) -> Result<Option<NurbsCurve>, KernelRefusal> {
1108 if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || !(d1 > d0) {
1109 return Ok(None);
1110 }
1111 let feet_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.feet_ms");
1112 let mut control = Vec::with_capacity(2);
1113 for t in [d0, d1] {
1114 let point = rail.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
1115 let foot = crate::project_point_to_surface(surface, point).or_refuse(KernelStage::Refine, "project")?;
1116 control.push(crate::Vec4::from_point(crate::Vec3::new(foot.u, foot.v, 0.0), 1.0));
1117 }
1118 let line = NurbsCurve::new(1, vec![d0, d0, d1, d1], control).or_refuse(KernelStage::Refine, "new")?;
1119 feet_timing.stop();
1120 let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1121 let read = match affine_reads_on().then(|| affine_trim_read(rail, surface, &line, d0, d1)).flatten() {
1122 Some(read) => {
1123 crate::blend::record_count("count.read.affine_trims", 1);
1124 Ok(read)
1125 }
1126 None => crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &line, d0, d1),
1127 };
1128 read_timing.stop();
1129 Ok(march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE).then_some(line))
1130}
1131
1132fn affine_trim_read(rail: &NurbsCurve, surface: &NurbsSurface, pcurve: &NurbsCurve, d0: f64, d1: f64) -> Option<(f64, f64, usize, f64)> {
1140 if !affine_curve(rail) || !affine_curve(pcurve) {
1141 return None;
1142 }
1143 let (origin, normal) = affine_plane(surface)?;
1144 let (mut miss, mut standoff, mut worst) = (0.0f64, 0.0f64, d0);
1145 for t in [d0, d1] {
1146 let point = rail.evaluate(t).ok()?;
1147 let foot = point.sub(normal.scale(point.sub(origin).dot(normal)));
1148 let uv = pcurve.evaluate(t).ok()?;
1149 let image = crate::blend::track_fit::carrier_point(surface, uv.x, uv.y).ok()?;
1150 let (residual, off) = (image.sub(foot).length(), point.sub(foot).length());
1151 if !(residual.is_finite() && off.is_finite()) {
1152 return None;
1153 }
1154 if residual > miss {
1155 (miss, worst) = (residual, t);
1156 }
1157 standoff = standoff.max(off);
1158 }
1159 Some((miss, standoff, 2, worst))
1160}
1161
1162pub(in crate::blend) fn build_open_surgery(
1174 solid: &BrepSolid,
1175 result: &mut BrepSolid,
1176 take_id: &mut dyn FnMut() -> u64,
1177 edge: &EdgeRecord,
1178 first: &BlendMate,
1179 second: &BlendMate,
1180 rows: FittedRows,
1181 ends: [EndPlan; 2],
1182 name: Option<&str>,
1183) -> Result<SewnStripe, KernelRefusal> {
1184 let [start_end, finish_end] = ends;
1185
1186 let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, KernelRefusal> {
1193 let [low, high] = row.domain().or_refuse(KernelStage::Refine, "domain")?;
1194 let tail = if a == low { row.clone() } else { row.split(a).or_refuse(KernelStage::Refine, "split")?.1 };
1195 if b == high {
1196 return Ok(tail);
1197 }
1198 let (middle, _) = tail.split(b).or_refuse(KernelStage::Refine, "split")?;
1199 Ok(middle)
1200 };
1201 let kind = |plan: &EndPlan| match plan {
1202 EndPlan::Free(_) => "free",
1203 EndPlan::Corner(_) => "corner",
1204 EndPlan::Miter(_) => "miter",
1205 EndPlan::Cap(_) => "cap",
1206 };
1207 let describe_trim = |error: KernelRefusal| {
1208 error.with_message(|error| {
1209 format!(
1210 "{error} (edge {} rows trimmed for a {} start at cr {:.6}/cs {:.6} and a {} finish \
1211 at cr {:.6}/cs {:.6})",
1212 edge.id,
1213 kind(&start_end),
1214 start_end.cr_parameter(),
1215 start_end.cs_parameter(),
1216 kind(&finish_end),
1217 finish_end.cr_parameter(),
1218 finish_end.cs_parameter()
1219 )
1220 })
1221 };
1222 let band = consumed_band(solid);
1232 let exact_trim = crate::blend::exact_trim_on();
1233 let collapses = |row: &NurbsCurve, a: f64, b: f64| -> Result<bool, KernelRefusal> {
1234 let from = row.evaluate(a).or_refuse(KernelStage::Refine, "evaluate")?;
1235 Ok(row.evaluate(b).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band
1236 && row.evaluate(0.5 * (a + b)).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band)
1237 };
1238 let cr_collapsed = collapses(&rows.cr, start_end.cr_parameter(), finish_end.cr_parameter())?;
1239 let cs_collapsed = collapses(&rows.cs, start_end.cs_parameter(), finish_end.cs_parameter())?;
1240 if cr_collapsed || cs_collapsed {
1241 let unsupported = if start_end.free().is_some() && finish_end.free().is_some() {
1242 Some("neither end is a corner")
1243 } else if first.face.id == second.face.id {
1244 Some("both rails lie on one face")
1245 } else if matches!(start_end, EndPlan::Cap(_)) || matches!(finish_end, EndPlan::Cap(_)) {
1246 Some("a capped end has no corner to share the point with")
1247 } else {
1248 None
1249 };
1250 if let Some(reason) = unsupported {
1251 return Err(KernelRefusal::unsupported(KernelStage::Sew, super::RAIL_COLLAPSE_WHAT, format!(
1252 "{RAIL_COLLAPSE_UNSUPPORTED} edge {}'s rail shrinks to a point and {reason}",
1253 edge.id
1254 )));
1255 }
1256 }
1257 let trim_rail = |collapsed: bool,
1271 row: &NurbsCurve,
1272 pcurve: &NurbsCurve,
1273 surface: &NurbsSurface,
1274 a: f64,
1275 b: f64|
1276 -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1277 if collapsed {
1278 return Ok(None);
1279 }
1280 let rows_timing = crate::blend::BlendTimer::start("timing.surgery.trim.rows_ms");
1281 let rail = trim_row(row, a, b).map_err(describe_trim)?;
1282 let interpolated = trim_row(pcurve, a, b)?;
1283 rows_timing.stop();
1284 {
1285 let overrun_timing = crate::blend::BlendTimer::start("timing.surgery.trim.overrun_ms");
1286 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1287 let overrun = crate::blend::planar_chart_overrun(surface, &|t| rail.evaluate(t), d0, d1)?;
1288 overrun_timing.stop();
1289 if overrun.is_some_and(|excursion| excursion > band) {
1290 return Ok(Some((rail, interpolated)));
1291 }
1292 }
1293 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1294 if exact_trim && rows.exact_extrusion {
1298 if let Some(line) = exact_planar_trim(&rail, surface, d0, d1)? {
1299 crate::blend::record_count("count.surgery.exact_trim", 1);
1300 return Ok(Some((rail, line)));
1301 }
1302 }
1303 if rows.revolution_axis.is_some() {
1308 let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1309 let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &interpolated, d0, d1);
1310 read_timing.stop();
1311 if march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
1312 crate::blend::record_count("count.surgery.exact_revolution_trim", 1);
1313 return Ok(Some((rail, interpolated)));
1314 }
1315 crate::blend::record_count("count.surgery.exact_revolution_trim_declined", 1);
1316 }
1317 let mut march_feet = crate::blend::track_fit::FootMemo::default();
1326 let mut refit_feet = crate::blend::track_fit::FootMemo::default();
1327 let march_read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.march_ms");
1328 let march_read = crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &interpolated, d0, d1, &mut march_feet);
1329 march_read_timing.stop();
1330 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1331 let declined = |why: KernelRefusal| -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1332 let stands = march_trim_stands(&march_read, floor);
1335 if stands {
1336 Ok(Some((rail.clone(), interpolated.clone())))
1337 } else {
1338 Err(why)
1339 }
1340 };
1341 let refit_timing = crate::blend::BlendTimer::start("timing.surgery.trim.refit_ms");
1342 let refit = super::closed::refit_closed_support_pcurve_read(&rail, &interpolated, surface, &mut refit_feet);
1343 refit_timing.stop();
1344 let _reads_timing = crate::blend::BlendTimer::start("timing.surgery.trim.reads_ms");
1345 match refit {
1346 Ok((fitted, refit_read)) => {
1347 let (march_standoff, refit_standoff) = match (&march_read, Ok::<_, KernelRefusal>(refit_read)) {
1364 (_, Err(error)) => return declined(error),
1365 (Err(error), Ok(_)) => {
1366 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch_unread", format!(
1367 "blend: a support trim's refit cannot be shown to stand on its rail's branch: the march's own \
1368 trim is unreadable ({})",
1369 error.message
1370 )));
1371 }
1372 (Ok((_, march_standoff, _, _)), Ok((_, refit_standoff, _, _))) => (*march_standoff, refit_standoff),
1373 };
1374 let excess_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.excess_ms");
1375 let excess = crate::blend::track_fit::standoff_excess_memo(
1376 surface, &|t| rail.evaluate(t), &interpolated, &fitted, d0, d1, &mut march_feet, &mut refit_feet,
1377 );
1378 excess_timing.stop();
1379 let excess = match excess {
1380 Ok(excess) => excess,
1381 Err(error) => return declined(error),
1382 };
1383 if !(excess <= floor) {
1384 return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch", format!(
1385 "blend: a support trim's refit stands up to {excess:.3e} farther off its rail than the march's own trim \
1386 at the same rail parameter (largest standoffs {refit_standoff:.3e} and {march_standoff:.3e}): it took \
1387 another branch of the carrier"
1388 )));
1389 }
1390 let nearest_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.nearest_ms");
1398 let absolute = crate::blend::track_fit::standoff_over_nearest_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut refit_feet);
1399 nearest_timing.stop();
1400 let absolute = match absolute {
1401 Ok(absolute) => absolute,
1402 Err(error) => return declined(error),
1403 };
1404 if !(absolute <= floor) {
1405 return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_off_rail", format!(
1406 "blend: a support trim's refit stands up to {absolute:.3e} farther off its rail than the rail stands \
1407 off its carrier: its foot is not the rail's nearest point"
1408 )));
1409 }
1410 Ok(Some((rail, fitted)))
1411 }
1412 Err(error) => declined(error),
1413 }
1414 };
1415 let trim_timing = crate::blend::BlendTimer::start("timing.surgery.trim_rails_ms");
1416 let cr_rail = trim_rail(
1417 cr_collapsed,
1418 &rows.cr,
1419 &rows.cr_pcurve,
1420 &first.face.surface,
1421 start_end.cr_parameter(),
1422 finish_end.cr_parameter(),
1423 )?;
1424 let cs_rail = trim_rail(
1425 cs_collapsed,
1426 &rows.cs,
1427 &rows.cs_pcurve,
1428 &second.face.surface,
1429 start_end.cs_parameter(),
1430 finish_end.cs_parameter(),
1431 )?;
1432 trim_timing.stop();
1433 let cr_domain = match &cr_rail {
1435 Some((cr, _)) => cr.domain().or_refuse(KernelStage::Refine, "domain")?,
1436 None => [start_end.cr_parameter(), finish_end.cr_parameter()],
1437 };
1438 let cs_domain = match &cs_rail {
1439 Some((cs, _)) => cs.domain().or_refuse(KernelStage::Refine, "domain")?,
1440 None => [start_end.cs_parameter(), finish_end.cs_parameter()],
1441 };
1442 let cr_at = |parameter: f64| match &cr_rail {
1444 Some((cr, _)) => cr.evaluate(parameter),
1445 None => rows.cr.evaluate(parameter),
1446 };
1447 let cs_at = |parameter: f64| match &cs_rail {
1448 Some((cs, _)) => cs.evaluate(parameter),
1449 None => rows.cs.evaluate(parameter),
1450 };
1451 let old_start = edge.start_vertex_id;
1452 let old_finish = edge.end_vertex_id;
1453
1454 let classify =
1465 |boundary_id: u64, boundary_param: f64, corner: u64| -> Result<RimResolution, KernelRefusal> {
1466 let boundary = result
1467 .edges
1468 .iter()
1469 .find(|candidate| candidate.id == boundary_id)
1470 .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_boundary_edge", "blend: end boundary edge missing during surgery"))?;
1471 let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
1472 let far_vertex = if boundary.start_vertex_id == corner {
1473 Some((boundary.end_vertex_id, boundary.t1, boundary.t0))
1474 } else if boundary.end_vertex_id == corner {
1475 Some((boundary.start_vertex_id, boundary.t0, boundary.t1))
1476 } else {
1477 None
1478 };
1479 if let Some((far_id, far_t, near_t)) = far_vertex {
1480 let far_point = result
1491 .vertices
1492 .iter()
1493 .find(|candidate| candidate.id == far_id)
1494 .map(|candidate| candidate.point)
1495 .ok_or(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"))?;
1496 let crossing = boundary.curve.evaluate_extended(boundary_param).or_refuse(KernelStage::Refine, "evaluate_extended")?;
1497 let _ = span;
1498 let past_far = crossing.sub(far_point).length();
1499 if past_far <= band {
1500 if !result
1502 .vertices
1503 .iter()
1504 .any(|candidate| candidate.id == far_id)
1505 {
1506 return Err(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"));
1507 }
1508 return Ok(RimResolution::Consumed(far_id));
1509 }
1510 if (boundary_param - far_t) * (far_t - near_t) > 0.0 {
1523 return Err(KernelRefusal::unsupported(KernelStage::Sew, "blend_wider_than_face", format!(
1524 "{BLEND_WIDER_THAN_FACE} its rail crosses edge {boundary_id} {past_far:.3e} \
1525 past that edge's far vertex, and a rail within {band:.3e} of it is the \
1526 most this surgery snaps onto the edge"
1527 )));
1528 }
1529 }
1530 Ok(RimResolution::Fresh)
1531 };
1532 let resolve_side = |plan: &EndPlan,
1536 side_first: bool,
1537 corner: u64|
1538 -> Result<RimResolution, KernelRefusal> {
1539 if let Some(vertex) = plan.planned_rim(side_first) {
1540 return Ok(RimResolution::Corner(vertex));
1541 }
1542 match plan {
1543 EndPlan::Free(free) => classify(
1544 if side_first {
1545 free.first_edge_id
1546 } else {
1547 free.second_edge_id
1548 },
1549 if side_first {
1550 free.first_edge_parameter
1551 } else {
1552 free.second_edge_parameter
1553 },
1554 corner,
1555 ),
1556 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => {
1557 Err(KernelRefusal::internal(KernelStage::Sew, "rim_vertices", "blend: planned end without rim vertices"))
1558 }
1559 }
1560 };
1561 let crossings_timing = crate::blend::BlendTimer::start("timing.surgery.crossings_ms");
1562 let start_first = resolve_side(&start_end, true, old_start)?;
1563 let start_second = resolve_side(&start_end, false, old_start)?;
1564 let finish_first = resolve_side(&finish_end, true, old_finish)?;
1565 let finish_second = resolve_side(&finish_end, false, old_finish)?;
1566 crossings_timing.stop();
1567 let _topology_timing = crate::blend::BlendTimer::start("timing.surgery.topology_ms");
1568
1569 let first_use_forward = first.coedge.forward;
1572 let blend_cr_forward = !first_use_forward;
1573
1574 let (sew_first, sew_second) = match (start_end.free(), finish_end.free(), &cr_rail, &cs_rail) {
1589 (Some(start_free), Some(finish_free), Some((cr, _)), Some((cs, _))) => (
1590 detect_row_coincidence(
1591 solid,
1592 first,
1593 edge.id,
1594 start_free.first_edge_id,
1595 finish_free.first_edge_id,
1596 start_first.is_consumed(),
1597 start_first.existing().unwrap_or(0),
1598 finish_first.is_consumed(),
1599 finish_first.existing().unwrap_or(0),
1600 cr,
1601 blend_cr_forward,
1602 band,
1603 )?,
1604 detect_row_coincidence(
1605 solid,
1606 second,
1607 edge.id,
1608 start_free.second_edge_id,
1609 finish_free.second_edge_id,
1610 start_second.is_consumed(),
1611 start_second.existing().unwrap_or(0),
1612 finish_second.is_consumed(),
1613 finish_second.existing().unwrap_or(0),
1614 cs,
1615 !blend_cr_forward,
1616 band,
1617 )?,
1618 ),
1619 _ => (None, None),
1620 };
1621 let (sew_first, sew_second): (Option<RowSew>, Option<RowSew>) =
1625 if first.face.id == second.face.id {
1626 (None, None)
1627 } else {
1628 (sew_first, sew_second)
1629 };
1630
1631 let mut resolve = |rim: &RimResolution, point: Result<Vec3, String>| -> Result<u64, KernelRefusal> {
1634 match rim.existing() {
1635 Some(id) => Ok(id),
1636 None => {
1637 let id = take_id();
1638 result.vertices.push(VertexRecord { id, point: point.or_refuse(KernelStage::Refine, "point")? });
1639 Ok(id)
1640 }
1641 }
1642 };
1643 let w1a = resolve(&start_first, cr_at(cr_domain[0]))?;
1644 let w1b = resolve(&finish_first, cr_at(cr_domain[1]))?;
1645 let w2a = resolve(&start_second, cs_at(cs_domain[0]))?;
1646 let w2b = resolve(&finish_second, cs_at(cs_domain[1]))?;
1647 let mut snap = [&sew_first, &sew_second]
1650 .into_iter()
1651 .flatten()
1652 .fold(0.0f64, |worst, sew| worst.max(sew.deviation));
1653 for (rim, vertex, row_end) in [
1654 (&start_first, w1a, cr_at(cr_domain[0]).or_refuse(KernelStage::Refine, "cr_at")?),
1655 (&finish_first, w1b, cr_at(cr_domain[1]).or_refuse(KernelStage::Refine, "cr_at")?),
1656 (&start_second, w2a, cs_at(cs_domain[0]).or_refuse(KernelStage::Refine, "cs_at")?),
1657 (&finish_second, w2b, cs_at(cs_domain[1]).or_refuse(KernelStage::Refine, "cs_at")?),
1658 ] {
1659 if !rim.is_consumed() {
1660 continue;
1661 }
1662 let point = result
1663 .vertices
1664 .iter()
1665 .find(|candidate| candidate.id == vertex)
1666 .ok_or(KernelRefusal::internal(KernelStage::Sew, "rim_vertex", "blend: consumed rim vertex missing"))?
1667 .point;
1668 snap = snap.max(row_end.sub(point).length());
1669 }
1670
1671 let cr_edge_id = match (&sew_first, &cr_rail) {
1676 (Some(sew), _) => Some(sew.edge_id),
1677 (None, None) => None,
1678 (None, Some((cr, _))) => {
1679 let id = take_id();
1680 result.edges.push(EdgeRecord {
1681 id,
1682 curve: cr.clone(),
1683 t0: cr_domain[0],
1684 t1: cr_domain[1],
1685 start_vertex_id: w1a,
1686 end_vertex_id: w1b,
1687 degenerate: false,
1688 name: None,
1689 });
1690 Some(id)
1691 }
1692 };
1693 let cs_edge_id = match (&sew_second, &cs_rail) {
1694 (Some(sew), _) => Some(sew.edge_id),
1695 (None, None) => None,
1696 (None, Some((cs, _))) => {
1697 let id = take_id();
1698 result.edges.push(EdgeRecord {
1699 id,
1700 curve: cs.clone(),
1701 t0: cs_domain[0],
1702 t1: cs_domain[1],
1703 start_vertex_id: w2a,
1704 end_vertex_id: w2b,
1705 degenerate: false,
1706 name: None,
1707 });
1708 Some(id)
1709 }
1710 };
1711 let mut commit_end_edge = |plan: &EndPlan,
1716 first_rim: u64,
1717 second_rim: u64|
1718 -> Result<Option<u64>, KernelRefusal> {
1719 match plan {
1720 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
1721 EndPlan::Free(free) => {
1722 let id = take_id();
1723 let domain = free.transverse_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1724 result.edges.push(EdgeRecord {
1725 id,
1726 curve: free.transverse_curve.clone(),
1727 t0: domain[0],
1728 t1: domain[1],
1729 start_vertex_id: first_rim,
1730 end_vertex_id: second_rim,
1731 degenerate: false,
1732 name: None,
1733 });
1734 Ok(Some(id))
1735 }
1736 }
1737 };
1738 let transverse_a_id = commit_end_edge(&start_end, w1a, w2a)?;
1739 let transverse_b_id = commit_end_edge(&finish_end, w1b, w2b)?;
1740
1741 for (mate, rail, sewn) in [
1748 (first, cr_edge_id.zip(cr_rail.as_ref()), sew_first.is_some()),
1749 (second, cs_edge_id.zip(cs_rail.as_ref()), sew_second.is_some()),
1750 ] {
1751 if sewn {
1752 continue;
1753 }
1754 let face = result
1755 .shells
1756 .iter_mut()
1757 .flat_map(|shell| &mut shell.faces)
1758 .find(|face| face.id == mate.face.id)
1759 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1760 let loop_record = &mut face.loops[mate.loop_index];
1761 let position = loop_record
1762 .coedges
1763 .iter()
1764 .position(|coedge| coedge.edge_id == edge.id)
1765 .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
1766 let Some((new_edge_id, (_, pcurve_forward))) = rail else {
1767 loop_record.coedges.remove(position);
1768 continue;
1769 };
1770 let old_forward = loop_record.coedges[position].forward;
1771 loop_record.coedges[position] = CoedgeRecord {
1772 id: loop_record.coedges[position].id,
1773 edge_id: new_edge_id,
1774 forward: old_forward,
1775 pcurve: if old_forward {
1776 pcurve_forward.clone()
1777 } else {
1778 pcurve_forward.reversed().or_refuse(KernelStage::Refine, "reversed")?
1779 },
1780 };
1781 }
1782
1783 for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
1788 let EndPlan::Cap(cap) = plan else {
1789 continue;
1790 };
1791 for (mate, rail_edge_id, leg, rim) in [
1792 (first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
1793 (second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
1794 ] {
1795 let rail_edge_id = rail_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "end_rail", "blend: a capped end lost its rail"))?;
1797 let face = result
1798 .shells
1799 .iter_mut()
1800 .flat_map(|shell| &mut shell.faces)
1801 .find(|face| face.id == mate.face.id)
1802 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during cap splice"))?;
1803 let loop_record = &mut face.loops[mate.loop_index];
1804 let count = loop_record.coedges.len();
1805 let rail_at = loop_record
1806 .coedges
1807 .iter()
1808 .position(|coedge| coedge.edge_id == rail_edge_id)
1809 .ok_or(KernelRefusal::internal(KernelStage::Sew, "rail_coedge", "blend: rail coedge lost during cap splice"))?;
1810 let rail_forward = loop_record.coedges[rail_at].forward;
1816 let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
1817 let (insert_at, forward, pcurve) = if end_is_traversal_end {
1818 (rail_at + 1, true, leg.1.clone())
1820 } else {
1821 (rail_at, false, leg.1.reversed().or_refuse(KernelStage::Refine, "reversed")?)
1823 };
1824 let _ = (count, rim);
1825 loop_record.coedges.insert(
1826 insert_at,
1827 CoedgeRecord {
1828 id: take_id(),
1829 edge_id: leg.0,
1830 forward,
1831 pcurve,
1832 },
1833 );
1834 }
1835 }
1836
1837 let mut consumed_edges: Vec<u64> = Vec::new();
1843 let start_free = start_end.free();
1844 let finish_free = finish_end.free();
1845 for (mate, sewn, sides) in [
1846 (
1847 first,
1848 sew_first.is_some(),
1849 [
1850 (&start_first, start_free.map(|free| free.first_edge_id)),
1851 (&finish_first, finish_free.map(|free| free.first_edge_id)),
1852 ],
1853 ),
1854 (
1855 second,
1856 sew_second.is_some(),
1857 [
1858 (&start_second, start_free.map(|free| free.second_edge_id)),
1859 (&finish_second, finish_free.map(|free| free.second_edge_id)),
1860 ],
1861 ),
1862 ] {
1863 for (cross, boundary_id) in sides {
1864 if !cross.is_consumed() {
1865 continue;
1866 }
1867 let Some(boundary_id) = boundary_id else {
1868 continue;
1869 };
1870 if !sewn {
1871 let face = result
1872 .shells
1873 .iter_mut()
1874 .flat_map(|shell| &mut shell.faces)
1875 .find(|face| face.id == mate.face.id)
1876 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1877 face.loops[mate.loop_index]
1878 .coedges
1879 .retain(|coedge| coedge.edge_id != boundary_id);
1880 }
1881 if !consumed_edges.contains(&boundary_id) {
1882 consumed_edges.push(boundary_id);
1883 }
1884 }
1885 }
1886 for sew in [&sew_first, &sew_second].into_iter().flatten() {
1888 for id in &sew.collapsed_edges {
1889 if !consumed_edges.contains(id) {
1890 consumed_edges.push(*id);
1891 }
1892 }
1893 }
1894
1895 let mut end_plans = Vec::with_capacity(2);
1904 for (end, transverse_id, corner, first_consumed, second_consumed) in [
1905 (
1906 start_end.free(),
1907 transverse_a_id,
1908 old_start,
1909 start_first.is_consumed(),
1910 start_second.is_consumed(),
1911 ),
1912 (
1913 finish_end.free(),
1914 transverse_b_id,
1915 old_finish,
1916 finish_first.is_consumed(),
1917 finish_second.is_consumed(),
1918 ),
1919 ] {
1920 let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
1921 continue;
1922 };
1923 let face = result
1924 .shells
1925 .iter()
1926 .flat_map(|shell| &shell.faces)
1927 .find(|face| face.id == end.end_face_id)
1928 .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_face", "blend: end face lost during surgery"))?;
1929 let Some(located) = locate_end_corner(
1932 &result,
1933 face,
1934 corner,
1935 end.first_edge_id,
1936 end.second_edge_id,
1937 ) else {
1938 return Err(KernelRefusal::internal(KernelStage::Sew, "end_face_corner", "blend: end-face corner (adjacent boundary coedges) not found"));
1939 };
1940 for pole in &located.pole_edge_ids {
1943 if !consumed_edges.contains(pole) {
1944 consumed_edges.push(*pole);
1945 }
1946 }
1947 end_plans.push((
1948 end.end_face_id,
1949 transverse_id,
1950 end.transverse_end_pcurve.clone(),
1951 located,
1952 first_consumed,
1953 second_consumed,
1954 ));
1955 }
1956
1957 for (rim_resolution, boundary, corner, rim) in [
1960 (
1961 &start_first,
1962 start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
1963 old_start,
1964 w1a,
1965 ),
1966 (
1967 &start_second,
1968 start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
1969 old_start,
1970 w2a,
1971 ),
1972 (
1973 &finish_first,
1974 finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
1975 old_finish,
1976 w1b,
1977 ),
1978 (
1979 &finish_second,
1980 finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
1981 old_finish,
1982 w2b,
1983 ),
1984 ] {
1985 if !rim_resolution.trims_boundary() {
1986 continue;
1987 }
1988 let Some((boundary_id, boundary_param)) = boundary else {
1989 continue;
1990 };
1991 trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
1992 }
1993
1994 for (end_face, transverse_id, transverse_end_pcurve, located, first_consumed, second_consumed) in
1997 end_plans
1998 {
1999 let forward = located.x_is_first;
2000 let pcurve = if forward {
2001 transverse_end_pcurve
2002 } else {
2003 transverse_end_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
2004 };
2005 let (x_consumed, y_consumed) = if located.x_is_first {
2006 (first_consumed, second_consumed)
2007 } else {
2008 (second_consumed, first_consumed)
2009 };
2010 let transverse_coedge = CoedgeRecord {
2011 id: take_id(),
2012 edge_id: transverse_id,
2013 forward,
2014 pcurve,
2015 };
2016 splice_end_corner(
2017 result,
2018 end_face,
2019 &located,
2020 transverse_coedge,
2021 x_consumed,
2022 y_consumed,
2023 )?;
2024 }
2025
2026 let (mid_u, station_uv) = match (&cr_rail, &cs_rail) {
2031 (Some((_, cr_pcurve)), _) => {
2032 let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
2033 (mid_u, cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2034 }
2035 (None, Some(_)) => {
2036 let mid_u = (cs_domain[0] + cs_domain[1]) * 0.5;
2037 (mid_u, rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2038 }
2039 (None, None) => (cr_domain[0], rows.cr_pcurve.evaluate(cr_domain[0]).or_refuse(KernelStage::Refine, "evaluate")?),
2040 };
2041 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
2042 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
2043 let out1 = if first.face.same_sense {
2044 n1
2045 } else {
2046 n1.scale(-1.0)
2047 };
2048 let same_sense = blend_normal.dot(out1) >= 0.0;
2049 let loop_id = take_id();
2050 let cr_forward = sew_first
2058 .as_ref()
2059 .map_or(blend_cr_forward, |sew| sew.forward);
2060 let cs_forward = sew_second
2061 .as_ref()
2062 .map_or(!blend_cr_forward, |sew| sew.forward);
2063 let mut end_slot = |plan: &EndPlan,
2073 free_edge_id: Option<u64>,
2074 natural_forward: bool|
2075 -> Result<Vec<CoedgeRecord>, KernelRefusal> {
2076 match plan {
2077 EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
2078 id: take_id(),
2079 edge_id: end.arc_edge_id,
2080 forward: true,
2081 pcurve: end.arc_blend_pcurve.clone(),
2082 }]),
2083 EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
2084 id: take_id(),
2085 edge_id: end.arc_edge_id,
2086 forward: true,
2087 pcurve: end.arc_blend_pcurve.clone(),
2088 }]),
2089 EndPlan::Miter(end) => end
2090 .edges
2091 .iter()
2092 .map(|(edge_id, forward, pcurve)| {
2093 Ok(CoedgeRecord {
2094 id: take_id(),
2095 edge_id: *edge_id,
2096 forward: *forward,
2097 pcurve: pcurve.clone(),
2098 })
2099 })
2100 .collect(),
2101 EndPlan::Free(free) => {
2102 let edge_id =
2103 free_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "transverse_edge", "blend: free end without its transverse edge"))?;
2104 Ok(vec![if natural_forward {
2105 CoedgeRecord {
2106 id: take_id(),
2107 edge_id,
2108 forward: true,
2109 pcurve: free.transverse_blend_pcurve.clone(),
2110 }
2111 } else {
2112 CoedgeRecord {
2113 id: take_id(),
2114 edge_id,
2115 forward: false,
2116 pcurve: free.transverse_blend_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?,
2117 }
2118 }])
2119 }
2120 }
2121 };
2122 let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
2123 let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
2124 let coedges = if blend_cr_forward {
2127 let mut coedges = Vec::new();
2128 if let Some(cr_edge_id) = cr_edge_id {
2129 coedges.push(CoedgeRecord {
2130 id: take_id(),
2131 edge_id: cr_edge_id,
2132 forward: cr_forward,
2133 pcurve: crate::sweep_topology::parameter_line(
2134 cr_domain[0],
2135 0.0,
2136 cr_domain[1],
2137 0.0,
2138 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2139 });
2140 }
2141 coedges.extend(finish_slot);
2142 if let Some(cs_edge_id) = cs_edge_id {
2143 coedges.push(CoedgeRecord {
2144 id: take_id(),
2145 edge_id: cs_edge_id,
2146 forward: cs_forward,
2147 pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
2148 });
2149 }
2150 coedges.extend(start_slot);
2151 coedges
2152 } else {
2153 let mut coedges = Vec::new();
2154 if let Some(cr_edge_id) = cr_edge_id {
2155 coedges.push(CoedgeRecord {
2156 id: take_id(),
2157 edge_id: cr_edge_id,
2158 forward: cr_forward,
2159 pcurve: crate::sweep_topology::parameter_line(
2160 cr_domain[1],
2161 0.0,
2162 cr_domain[0],
2163 0.0,
2164 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2165 });
2166 }
2167 coedges.extend(start_slot);
2168 if let Some(cs_edge_id) = cs_edge_id {
2169 coedges.push(CoedgeRecord {
2170 id: take_id(),
2171 edge_id: cs_edge_id,
2172 forward: cs_forward,
2173 pcurve: crate::sweep_topology::parameter_line(
2174 cs_domain[0],
2175 1.0,
2176 cs_domain[1],
2177 1.0,
2178 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2179 });
2180 }
2181 coedges.extend(finish_slot);
2182 coedges
2183 };
2184 let blend_face_id = take_id();
2185 let mut blend_face = FaceRecord {
2186 id: blend_face_id,
2187 surface: rows.surface,
2188 same_sense,
2189 loops: vec![LoopRecord {
2190 id: loop_id,
2191 coedges,
2192 }],
2193 name: name.map(|value| value.to_string()),
2194 };
2195 if rows.exact_extrusion {
2196 transpose_face(&mut blend_face)?;
2197 }
2198 let shell_index = result
2199 .shells
2200 .iter()
2201 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
2202 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
2203 for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
2208 if sew.is_some() {
2209 for shell in &mut result.shells {
2210 shell.faces.retain(|face| face.id != mate.face.id);
2211 }
2212 }
2213 }
2214 result.shells[shell_index].faces.push(blend_face);
2215
2216 result.edges.retain(|candidate| candidate.id != edge.id);
2217 result
2220 .edges
2221 .retain(|candidate| !consumed_edges.contains(&candidate.id));
2222 let mut identified = Vec::new();
2224 for (collapsed, from, to) in [(cr_collapsed, w1a, w1b), (cs_collapsed, w2a, w2b)] {
2225 if collapsed && from != to {
2226 identified.push((from, to));
2227 }
2228 }
2229 Ok(SewnStripe {
2230 cr_edge_id,
2231 cs_edge_id,
2232 blend_face_id,
2233 consumed: [sew_first.is_some(), sew_second.is_some()],
2234 snap,
2235 identified,
2236 })
2237}
2238
2239pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), KernelRefusal> {
2245 let surface = &face.surface;
2246 let rows_u = surface.control_points.len();
2247 let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
2248 let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
2249 for row in &surface.control_points {
2250 for (j, point) in row.iter().enumerate() {
2251 transposed[j].push(*point);
2252 }
2253 }
2254 face.surface = NurbsSurface::new(
2255 surface.degree_v,
2256 surface.degree_u,
2257 surface.knots_v.clone(),
2258 surface.knots_u.clone(),
2259 transposed,
2260 ).or_refuse(KernelStage::Refine, "new")?;
2261 for loop_record in &mut face.loops {
2262 for coedge in &mut loop_record.coedges {
2263 for control in &mut coedge.pcurve.control_points {
2264 std::mem::swap(&mut control.x, &mut control.y);
2265 }
2266 }
2267 }
2268 face.same_sense = !face.same_sense;
2269 Ok(())
2270}
2271
2272pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
2277 let used: rustc_hash::FxHashSet<u64> = result
2278 .edges
2279 .iter()
2280 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
2281 .collect();
2282 result
2283 .vertices
2284 .retain(|candidate| used.contains(&candidate.id));
2285}
2286
2287fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
2288 crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")
2289}
2290