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 let mut ends = Vec::with_capacity(2);
127 let mut in_range = true;
128 for (vertex, at_start) in [(edge.start_vertex_id, true), (edge.end_vertex_id, false)] {
129 let (end, side_in_range) = resolve_free_end(
130 solid,
131 edge_id,
132 &first_mate,
133 &second_mate,
134 &rows,
135 vertex,
136 at_start,
137 )?;
138 if debug {
139 eprintln!(
140 " [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}",
141 end.end_face_id,
142 end.first_edge_id,
143 end.first_edge_parameter,
144 end.second_edge_id,
145 end.second_edge_parameter,
146 end.cr_parameter,
147 end.cs_parameter,
148 );
149 }
150 in_range &= side_in_range;
151 ends.push(end);
152 }
153 let windows = [
158 shipped_window(¶meters, ends[0].cr_parameter, ends[1].cr_parameter),
159 shipped_window(¶meters, ends[0].cs_parameter, ends[1].cs_parameter),
160 ];
161 let (rails, rail_samples) =
162 rails_on_shipped_windows(&rows, ¶meters, &stations, [&first_mate.face.surface, &second_mate.face.surface], windows);
163 let walls = match (wall_check, windows) {
164 (false, _) => Vec::new(),
165 (true, [Some(first), Some(second)]) => open_wall_readings_why(
166 &rows,
167 ¶meters,
168 &stations,
169 [first[0].min(second[0]), first[1].max(second[1])],
170 &dense,
171 wall_radius.unwrap_or(f64::NAN),
172 ),
173 (true, _) => vec![Err("no shipped window to read".to_string())],
175 };
176 let first_unread = walls.iter().find_map(|wall| wall.as_ref().err().cloned());
177 let walls: Vec<Option<f64>> = if wall_check && windows.iter().any(|window| window.is_none()) {
178 Vec::new()
179 } else {
180 walls.into_iter().map(|wall| wall.ok()).collect()
181 };
182 let reading = OpenRungReading {
183 station_count,
184 stations: stations.len(),
185 rails,
186 rail_samples,
187 walls,
188 first_unread,
189 refine_exit: refinement.exit,
190 inserted: refinement.inserted,
191 };
192 Ok((rows, ends, in_range, reading))
193 };
194
195 let seeded_fraction: Option<f64> = (|| {
205 let span = edge.t1 - edge.t0;
206 let mut length = 0.0f64;
207 let mut previous = edge.curve.evaluate(edge.t0).ok()?;
208 for index in 1..=16 {
209 let t = edge.t0 + span * index as f64 / 16.0;
210 let point = edge.curve.evaluate(t).ok()?;
211 length += point.sub(previous).length();
212 previous = point;
213 }
214 if !(length > 0.0) || !length.is_finite() {
215 return None;
216 }
217 let mid_t = edge.t0 + span * 0.5;
218 let uv1 = edge_uv_on_face(first_mate.coedge, edge, mid_t).ok()?;
219 let uv2 = edge_uv_on_face(second_mate.coedge, edge, mid_t).ok()?;
220 let n1 = raw_normal(&first_mate.face.surface, uv1[0], uv1[1]).ok()?;
221 let n2 = raw_normal(&second_mate.face.surface, uv2[0], uv2[1]).ok()?;
222 let cos_alpha =
223 (rho1.signum() * rho2.signum() * n1.dot(n2)).clamp(-1.0, 1.0);
224 let tan_half = ((1.0 - cos_alpha).max(0.0) / (1.0 + cos_alpha).max(1e-9)).sqrt();
227 let end_radius = radius_at(edge.t0).abs().max(radius_at(edge.t1).abs());
228 let fraction = (1.5 * end_radius * tan_half / length).clamp(0.08, 0.45);
229 if !fraction.is_finite() || (fraction - 0.08).abs() < 1e-12 {
230 return None;
231 }
232 Some(fraction)
233 })();
234
235 let choose = |station_count: usize| -> Result<(FittedRows, Vec<EndSurgery>, OpenRungReading), KernelRefusal> {
242 let mut chosen: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
243 let mut last_error: Option<KernelRefusal> = None;
244 if let Some(fraction) = seeded_fraction {
248 match compute(fraction, station_count) {
249 Ok((rows, ends, true, reading)) => chosen = Some((rows, ends, reading)),
250 Ok(_) => {}
251 Err(error) if crate::blend::is_wall_fold(&error) => return Err(error),
252 Err(error) => last_error = Some(error),
253 }
254 }
255 if chosen.is_none() {
256 for &overshoot_fraction in &[0.08f64, 0.16, 0.28, 0.45] {
257 match compute(overshoot_fraction, station_count) {
258 Ok((rows, ends, in_range, reading)) => {
259 let fallback = chosen.is_none();
260 if in_range {
261 chosen = Some((rows, ends, reading));
262 break;
263 } else if fallback {
264 chosen = Some((rows, ends, reading));
265 }
266 }
267 Err(error) if crate::blend::is_wall_fold(&error) => return Err(error),
268 Err(error) => last_error = Some(error),
269 }
270 }
271 }
272 chosen.ok_or_else(|| {
273 last_error.unwrap_or_else(|| {
274 KernelRefusal::internal(
275 KernelStage::Refine,
276 "open_march",
277 "blend: open march failed at every overshoot",
278 )
279 })
280 })
281 };
282 let accepts = |reading: &OpenRungReading| {
295 reading.rails_read()
296 && reading.rails <= rail_bar
297 && (!wall_check || reading.worst_wall().is_some_and(|wall| wall <= wall_bar))
298 };
299 let meets_model = |reading: &OpenRungReading| reading.worst_wall().is_some_and(|wall| wall <= model);
300 let mut station_count = STATIONS;
301 let mut accepted: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
302 let mut unaccepted: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
305 let mut request_rungs = 0usize;
306 let mut unmet: Option<crate::BudgetReason> = None;
307 loop {
308 if accepted.is_some() {
311 request_rungs += 1;
312 }
313 let chosen = choose(station_count);
314 let (rows, ends, reading) = match chosen {
315 Ok(chosen) => chosen,
316 Err(_) if accepted.is_some() => {
318 unmet = Some(crate::BudgetReason::Incoherent);
319 break;
320 }
321 Err(error) => return Err(error),
322 };
323 #[cfg(not(test))]
324 let reject_request = false;
325 crate::blend::carve::carve_trace(format_args!(
326 "blend open edge {}: {} stations a rung ({} in all), rails {:.3e} (bar {rail_bar:.3e}), wall {:.3e} (bar {wall_bar:.3e}){}",
327 edge.id, reading.station_count, reading.stations, reading.rails,
328 reading.worst_wall().unwrap_or(f64::INFINITY),
329 if accepted.is_some() { ", a request rung" } else { "" }
330 ));
331 let accepted_now = accepts(&reading) && !reject_request;
332 if accepted.is_none() {
333 if accepted_now {
334 let met = !wall_check || meets_model(&reading);
335 accepted = Some((rows, ends, reading));
336 if met {
337 break;
338 }
339 } else if station_count < MAX_OPEN_STATIONS {
340 station_count *= 2;
341 continue;
342 } else {
343 crate::blend::record_blend_note(format!(
344 "blend: open edge {} at {} stations a rung ({} in all) — the top of the open march's \
345 ladder — still leaves its rails {:.3e} from their carriers (bar {rail_bar:.3e}) or its \
346 wall off the rolling ball (bar {wall_bar:.3e}); the wall ships as built, unaccepted",
347 edge.id, reading.station_count, reading.stations, reading.rails
348 ));
349 unaccepted = Some((rows, ends, reading));
350 break;
351 }
352 } else {
353 if !accepted_now {
354 unmet = Some(crate::BudgetReason::Rejected);
355 break;
356 }
357 let met = meets_model(&reading);
358 accepted = Some((rows, ends, reading));
359 if met {
360 unmet = None;
361 break;
362 }
363 }
364 if station_count < MAX_OPEN_STATIONS {
366 station_count *= 2;
367 } else {
368 unmet = Some(crate::BudgetReason::StationCeiling);
369 break;
370 }
371 }
372 let (shipped_accepted, (rows, ends, reading)) = match (accepted, unaccepted) {
373 (Some(rung), _) => (true, rung),
374 (None, Some(rung)) => (false, rung),
375 (None, None) => {
376 return Err(KernelRefusal::internal(KernelStage::Refine, "open_ladder", "blend: the open ladder ended without a rung"));
377 }
378 };
379 let report: Option<(crate::BudgetReason, f64, f64, String)> = if !shipped_accepted {
383 Some(acceptance_deficit(&reading, rail_bar, wall_bar, wall_check, reading.rails_read(), "no rung of the open ladder was accepted"))
384 } else if wall_check && !meets_model(&reading) {
385 let reason = unmet.unwrap_or(crate::BudgetReason::StationCeiling);
386 let declared = reading.worst_wall().unwrap_or(f64::INFINITY);
388 Some((reason, model, declared, String::new()))
389 } else {
390 None
391 };
392
393 let [start_end, finish_end] = match <[EndSurgery; 2]>::try_from(ends) {
394 Ok(pair) => pair,
395 Err(_) => return Err(KernelRefusal::internal(KernelStage::Sew, "open_surgery_ends", "blend: open surgery needs exactly two ends")),
396 };
397 let mut crossings = SpokeCrossings::default();
401 crossings.note(start_end.escalated || finish_end.escalated);
402 let mut result = solid.clone();
403 let mut take_id = fresh_id_source(solid);
404 let sewn = build_open_surgery(
405 solid,
406 &mut result,
407 &mut take_id,
408 edge,
409 &first_mate,
410 &second_mate,
411 rows,
412 [EndPlan::Free(start_end), EndPlan::Free(finish_end)],
413 name,
414 )?;
415 prune_orphan_vertices(&mut result);
416 crate::blend::fit_planar_charts_to_trims(solid, &mut result)?;
420 check_snap_closure(solid, &result, sewn.snap)?;
421 let built = crossings.gate(result)?;
422 let sewn_surface = built
425 .shells
426 .iter()
427 .flat_map(|shell| &shell.faces)
428 .find(|face| face.id == sewn.blend_face_id)
429 .map(|face| face.surface.clone())
430 .ok_or(KernelRefusal::internal(KernelStage::Sew, "open_blend_face", "blend: the sewn open blend face is not in the built body"))?;
431 if let Some((reason, bar, measured, deficit)) = report {
434 let rungs_used = if shipped_accepted { request_rungs } else { (reading.station_count / STATIONS).trailing_zeros() as usize };
440 let mut detail = format!(
441 "the shipped rung's local rail refinement ended {:?} with {} stations inserted",
442 reading.refine_exit, reading.inserted
443 );
444 if !deficit.is_empty() {
445 detail = format!("{deficit}; {detail}");
446 }
447 crate::blend::record_wall_model_report(crate::blend::WallModelReport {
448 name: name.map(str::to_string),
449 surface: sewn_surface,
450 request: bar,
451 residual: measured,
452 detail: Some(detail),
453 budget: crate::ApproximationBudget {
454 reason,
455 rounds_used: rungs_used,
456 rounds_limit: OPEN_REQUEST_RUNGS,
457 stations: reading.stations,
458 station_limit: REFINE_STATION_FACTOR * (MAX_OPEN_STATIONS + 1),
459 mechanism: crate::BudgetMechanism::StationRungs,
460 measured_component: crate::MeasuredComponent::Wall,
461 unread: 0,
462 },
463 });
464 }
465 Ok(built)
466}
467
468pub(in crate::blend) fn acceptance_deficit(
480 reading: &OpenRungReading,
481 rail_bar: f64,
482 wall_bar: f64,
483 wall_check: bool,
484 rails_read: bool,
485 what: &str,
486) -> (crate::BudgetReason, f64, f64, String) {
487 let rails = (rails_read && reading.rails.is_finite() && reading.rails >= 0.0).then_some(reading.rails);
488 let wall = if wall_check { reading.worst_wall() } else { None };
489 let unread = rails.is_none() || (wall_check && wall.is_none());
490 let mut worst = (0.0_f64, rail_bar);
491 for (value, bar) in [(rails, rail_bar), (wall, wall_bar)] {
492 if let Some(value) = value {
493 if value / bar > worst.0 / worst.1 {
494 worst = (value, bar);
495 }
496 }
497 }
498 let rail_text = match rails {
499 Some(value) => format!("rails {value:.3e} from their carriers (bar {rail_bar:.3e})"),
500 None => format!(
501 "rails UNREAD ({:?} over {} + {} samples) against bar {rail_bar:.3e}",
502 reading.rails, reading.rail_samples[0], reading.rail_samples[1]
503 ),
504 };
505 let wall_text = match (wall_check, wall) {
506 (false, _) => "the wall not judged (no constant-radius fillet provenance)".to_string(),
507 (true, Some(value)) => format!("wall {value:.3e} off the rolling ball (bar {wall_bar:.3e})"),
508 (true, None) => format!(
509 "wall UNREAD on {} of {} intervals against bar {wall_bar:.3e}{}",
510 reading.walls.iter().filter(|wall| !matches!(wall, Some(value) if value.is_finite())).count(),
511 reading.walls.len(),
512 reading.first_unread.as_deref().map(|why| format!(" (first: {why})")).unwrap_or_default()
513 ),
514 };
515 let reason = if unread { crate::BudgetReason::Unread } else { crate::BudgetReason::Unaccepted };
516 (reason, worst.1, worst.0, format!("{what}: {rail_text}, {wall_text}"))
517}
518
519const OPEN_WALL_SECTIONS: usize = 8;
522pub(in crate::blend) const OPEN_WALL_CENTRES: usize = 32;
523pub(in crate::blend) const OPEN_REQUEST_RUNGS: usize = (MAX_OPEN_STATIONS / STATIONS).trailing_zeros() as usize;
526
527#[derive(Clone, Debug)]
534pub(in crate::blend) struct OpenRungReading {
535 pub(in crate::blend) station_count: usize,
536 pub(in crate::blend) stations: usize,
537 pub(in crate::blend) rails: f64,
538 pub(in crate::blend) rail_samples: [usize; 2],
539 pub(in crate::blend) walls: Vec<Option<f64>>,
540 pub(in crate::blend) first_unread: Option<String>,
543 pub(in crate::blend) refine_exit: crate::blend::stations::StationRefineExit,
544 pub(in crate::blend) inserted: usize,
545}
546
547impl OpenRungReading {
548 pub(in crate::blend) fn rails_read(&self) -> bool {
552 self.rails.is_finite() && self.rails >= 0.0 && self.rail_samples.iter().all(|&samples| samples > 0)
553 }
554
555 pub(in crate::blend) fn worst_wall(&self) -> Option<f64> {
557 if self.walls.is_empty() {
558 return None;
559 }
560 self.walls.iter().try_fold(0.0_f64, |worst, wall| match wall {
561 Some(declared) if declared.is_finite() && *declared >= 0.0 => Some(worst.max(*declared)),
562 _ => None,
563 })
564 }
565}
566
567pub(in crate::blend) fn shipped_window(parameters: &[f64], a: f64, b: f64) -> Option<[f64; 2]> {
572 let (&first, &last) = (parameters.first()?, parameters.last()?);
573 let (low, high) = (a.min(b).max(first), a.max(b).min(last));
574 (low.is_finite() && high.is_finite() && low < high).then_some([low, high])
575}
576
577fn interval_at(parameters: &[f64], u: f64) -> Option<(usize, f64)> {
579 let last = parameters.len().checked_sub(2)?;
580 let interval = parameters.partition_point(|¶meter| parameter <= u).saturating_sub(1).min(last);
581 let (from, to) = (parameters[interval], parameters[interval + 1]);
582 (to > from).then(|| (interval, ((u - from) / (to - from)).clamp(0.0, 1.0)))
583}
584
585pub(in crate::blend) fn rails_on_shipped_windows(
594 rows: &FittedRows,
595 parameters: &[f64],
596 stations: &[Station],
597 carriers: [&NurbsSurface; 2],
598 windows: [Option<[f64; 2]>; 2],
599) -> (f64, [usize; 2]) {
600 let mut worst = 0.0_f64;
601 let mut samples = [0usize; 2];
602 for (side, (rail, carrier)) in [(&rows.cr, carriers[0]), (&rows.cs, carriers[1])].into_iter().enumerate() {
603 let Some([low, high]) = windows[side] else { continue };
604 let mut at = vec![low, high];
605 for pair in parameters.windows(2) {
606 for fraction in [0.25, 0.5, 0.75] {
607 let u = pair[0] + (pair[1] - pair[0]) * fraction;
608 if u > low && u < high {
609 at.push(u);
610 }
611 }
612 }
613 for u in at {
614 let off = interval_at(parameters, u)
615 .and_then(|(interval, fraction)| {
616 let (left, right) = (&stations[interval], &stations[interval + 1]);
617 let (a, b) = if side == 0 { (left.uv1, right.uv1) } else { (left.uv2, right.uv2) };
618 let seed = [a[0] + fraction * (b[0] - a[0]), a[1] + fraction * (b[1] - a[1])];
619 let point = rail.evaluate(u).ok()?;
620 crate::blend::stations::extended_foot_distance(carrier, point, seed).ok()
621 })
622 .filter(|off| off.is_finite())
623 .unwrap_or(f64::NAN);
624 samples[side] += 1;
625 if !worst.is_nan() && (off.is_nan() || off > worst) {
626 worst = off;
627 }
628 }
629 }
630 (worst, samples)
631}
632
633pub(in crate::blend) fn junction_stencil(
643 left_nodes: &[Vec3],
644 right_nodes: &[Vec3],
645 station_ts: &[f64],
646 left: usize,
647) -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
648 let half = OPEN_WALL_CENTRES / 2;
649 if left_nodes.len() != OPEN_WALL_CENTRES + 1 || right_nodes.len() != OPEN_WALL_CENTRES + 1 || left + 2 >= station_ts.len() {
650 return Err(format!("interval {left}: junction needs two {}-node intervals and their three station parameters", OPEN_WALL_CENTRES + 1));
651 }
652 let (t0, t1, t2) = (station_ts[left], station_ts[left + 1], station_ts[left + 2]);
653 let (w_left, w_right) = (t1 - t0, t2 - t1);
654 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()) {
655 return Err(format!("interval {left}: junction station parameters {t0}, {t1}, {t2} are not finite and strictly increasing"));
656 }
657 let nodes: Vec<Vec3> = left_nodes[half..].iter().chain(&right_nodes[1..=half]).copied().collect();
658 if w_left.to_bits() == w_right.to_bits() {
659 return Ok((nodes, None));
660 }
661 let abscissae: Vec<f64> = (half..=OPEN_WALL_CENTRES)
662 .map(|j| if j == OPEN_WALL_CENTRES { t1 } else { t0 + w_left * j as f64 / OPEN_WALL_CENTRES as f64 })
663 .chain((1..=half).map(|j| t1 + w_right * j as f64 / OPEN_WALL_CENTRES as f64))
664 .collect();
665 Ok((nodes, Some(abscissae)))
666}
667
668pub(in crate::blend) fn read_on_stencil(point: Vec3, nodes: &[Vec3], abscissae: Option<&[f64]>, step: usize, radius: f64) -> Result<(f64, f64), String> {
672 match abscissae {
673 None => crate::blend::chain::declared_wall_probe_why(point, nodes, step, radius),
674 Some(ts) => crate::blend::chain::declared_wall_probe_at_why(point, nodes, ts, step, radius),
675 }
676}
677
678pub(in crate::blend) fn exact_extrusion_wall_readings(
713 rows: &FittedRows,
714 parameters: &[f64],
715 window: [f64; 2],
716 radius: f64,
717) -> Vec<Result<f64, String>> {
718 let intervals = parameters.len().saturating_sub(1);
719 let overlapped: Vec<usize> =
720 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
721 let line = match (rows.exact_extrusion, rows.center.as_ref()) {
722 (true, Some(center)) => center.domain().and_then(|[a, b]| Ok((center.evaluate(a)?, center.evaluate(b)?))),
723 (false, _) => Err("the rows are not an exact extrusion".to_string()),
724 (true, None) => Err("the exact rows carry no centre line".to_string()),
725 };
726 let (anchor, direction) = match line.and_then(|(a, b)| b.sub(a).normalized().map(|d| (a, d))) {
727 Ok(line) => line,
728 Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
729 };
730 let Ok([v_low, v_high]) = rows.surface.domain_v() else {
731 return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
732 };
733 overlapped
734 .into_iter()
735 .map(|interval| -> Result<f64, String> {
736 let (from, to) = (parameters[interval], parameters[interval + 1]);
737 let inside = |u: f64| u >= window[0] && u <= window[1];
738 let mut probes: Vec<f64> =
739 (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
740 probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
741 if probes.is_empty() {
742 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
743 }
744 let mut worst = 0.0_f64;
745 for u in probes {
746 for k in 1..OPEN_WALL_SECTIONS {
747 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
748 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
749 let offset = point.sub(anchor);
750 let standoff = offset.sub(direction.scale(offset.dot(direction))).length();
751 let reading = (standoff - radius).abs();
752 if !reading.is_finite() {
753 return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
754 }
755 worst = worst.max(reading);
756 }
757 }
758 Ok(worst)
759 })
760 .collect()
761}
762
763pub(in crate::blend) fn exact_revolution_wall_readings(
774 rows: &FittedRows,
775 parameters: &[f64],
776 window: [f64; 2],
777 radius: f64,
778) -> Vec<Result<f64, String>> {
779 let intervals = parameters.len().saturating_sub(1);
780 let overlapped: Vec<usize> =
781 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
782 let circle = match (rows.revolution_axis, rows.center.as_ref()) {
783 (Some((origin, axis)), Some(center)) => center.domain().and_then(|[a, _]| center.evaluate(a)).map(|point| (origin, axis, point)),
784 (None, _) => Err("the rows are not an exact revolution".to_string()),
785 (Some(_), None) => Err("the exact rows carry no centre circle".to_string()),
786 };
787 let (origin, axis, centre_radius, centre_height) = match circle {
788 Ok((origin, axis, point)) => {
789 let offset = point.sub(origin);
790 let height = offset.dot(axis);
791 (origin, axis, offset.sub(axis.scale(height)).length(), height)
792 }
793 Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
794 };
795 let Ok([v_low, v_high]) = rows.surface.domain_v() else {
796 return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
797 };
798 overlapped
799 .into_iter()
800 .map(|interval| -> Result<f64, String> {
801 let (from, to) = (parameters[interval], parameters[interval + 1]);
802 let inside = |u: f64| u >= window[0] && u <= window[1];
803 let mut probes: Vec<f64> =
804 (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
805 probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
806 if probes.is_empty() {
807 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
808 }
809 let mut worst = 0.0_f64;
810 for u in probes {
811 for k in 1..OPEN_WALL_SECTIONS {
812 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
813 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
814 let offset = point.sub(origin);
815 let height = offset.dot(axis);
816 let radial = offset.sub(axis.scale(height)).length();
817 let standoff = (radial - centre_radius).hypot(height - centre_height);
818 let reading = (standoff - radius).abs();
819 if !reading.is_finite() {
820 return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
821 }
822 worst = worst.max(reading);
823 }
824 }
825 Ok(worst)
826 })
827 .collect()
828}
829
830pub(in crate::blend) fn open_wall_readings_why(
831 rows: &FittedRows,
832 parameters: &[f64],
833 stations: &[Station],
834 window: [f64; 2],
835 dense: &OpenDenseCentres,
836 radius: f64,
837) -> Vec<Result<f64, String>> {
838 let intervals = parameters.len().saturating_sub(1);
839 let overlapped: Vec<usize> =
840 (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
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 let nodes_of = |interval: usize| -> Result<Vec<Vec3>, String> {
846 let inner = dense.centres.get(interval).ok_or_else(|| format!("interval {interval}: no dense centre row"))?;
847 if dense.subdivisions != OPEN_WALL_CENTRES || inner.len() + 1 != OPEN_WALL_CENTRES {
848 return Err(format!("interval {interval}: dense row {} centres at {} subdivisions", inner.len(), dense.subdivisions));
849 }
850 let mut centres = Vec::with_capacity(OPEN_WALL_CENTRES + 1);
851 centres.push(stations[interval].center);
852 for (j, centre) in inner.iter().enumerate() {
853 centres.push(centre.ok_or_else(|| format!("interval {interval}: exact centre {} of {OPEN_WALL_CENTRES} unsolved", j + 1))?);
854 }
855 centres.push(stations[interval + 1].center);
856 Ok(centres)
857 };
858 let half = OPEN_WALL_CENTRES / 2;
859 let junction = |left: usize| -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
866 if dense.station_ts.len() != parameters.len() {
867 return Err(format!("interval {left}: junction without the native station parameters"));
868 }
869 junction_stencil(&nodes_of(left)?, &nodes_of(left + 1)?, &dense.station_ts, left)
870 };
871 overlapped
872 .into_iter()
873 .map(|interval| -> Result<f64, String> {
874 let centres = nodes_of(interval)?;
875 let (from, to) = (parameters[interval], parameters[interval + 1]);
876 let inside = |u: f64| u >= window[0] && u <= window[1];
877 let mut probes: Vec<(f64, usize, Option<(Vec<Vec3>, Option<Vec<f64>>)>)> = (2..OPEN_WALL_CENTRES)
880 .step_by(2)
881 .map(|step| (from + (to - from) * step as f64 / OPEN_WALL_CENTRES as f64, step))
882 .filter(|(u, _)| inside(*u))
883 .map(|(u, step)| (u, step, None))
884 .collect();
885 for end in window {
886 if end > from && end < to {
887 let fraction = (end - from) / (to - from);
888 let nearest = 2 * ((fraction * OPEN_WALL_CENTRES as f64 / 2.0).round() as usize);
889 #[cfg(not(test))]
890 let junction_off = false;
891 if junction_off {
892 probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
893 } else if nearest < 2 && interval > 0 {
894 probes.push((end, half, Some(junction(interval - 1)?)));
895 } else if nearest > OPEN_WALL_CENTRES - 2 && interval + 1 < intervals {
896 probes.push((end, half, Some(junction(interval)?)));
897 } else {
898 probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
899 }
900 }
901 }
902 if probes.is_empty() {
903 return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
904 }
905 let mut declared = 0.0_f64;
906 for (u, step, stencil) in probes {
907 let (nodes, abscissae, what) = match &stencil {
908 Some((nodes, None)) => (nodes, None, "junction"),
909 Some((nodes, Some(ts))) => (nodes, Some(ts), "native-abscissa junction"),
910 None => (¢res, None, "interval"),
911 };
912 for k in 1..OPEN_WALL_SECTIONS {
913 let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
914 let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
915 let read = if stencil.is_none() {
920 crate::blend::chain::declared_wall_probe_located_why(point, nodes, step, (u - from) / (to - from), radius)
921 } else {
922 read_on_stencil(point, nodes, abscissae.map(|ts| ts.as_slice()), step, radius)
923 };
924 let (reading, uncertainty) = read.map_err(|why| {
925 format!(
926 "interval {interval}: declared reader declined probe node {step} of the {what} stencil, section {k}/{OPEN_WALL_SECTIONS} \
927 (u {u:.17}): {why}"
928 )
929 })?;
930 declared = declared.max(reading + uncertainty);
931 }
932 }
933 if declared.is_finite() { Ok(declared) } else { Err(format!("interval {interval}: non-finite reading {declared}")) }
934 })
935 .collect()
936}
937
938
939pub(in crate::blend) fn fresh_id_source(solid: &BrepSolid) -> impl FnMut() -> u64 {
944 let mut next_id = solid
945 .vertices
946 .iter()
947 .map(|vertex| vertex.id)
948 .chain(solid.edges.iter().map(|edge| edge.id))
949 .chain(
950 solid
951 .shells
952 .iter()
953 .flat_map(|shell| &shell.faces)
954 .flat_map(|face| {
955 face.loops
956 .iter()
957 .map(|loop_record| loop_record.id)
958 .chain(face.loops.iter().flat_map(|loop_record| {
959 loop_record.coedges.iter().map(|coedge| coedge.id)
960 }))
961 .chain(std::iter::once(face.id))
962 }),
963 )
964 .max()
965 .unwrap_or(0)
966 + 1;
967 move || {
968 let id = next_id;
969 next_id += 1;
970 id
971 }
972}
973
974
975pub(in crate::blend) fn march_trim_stands(read: &Result<(f64, f64, usize, f64), KernelRefusal>, floor: f64) -> bool {
984 matches!(read, Ok((miss, standoff, _, _)) if *miss + *standoff <= floor)
985}
986
987fn exact_planar_trim(rail: &NurbsCurve, surface: &NurbsSurface, d0: f64, d1: f64) -> Result<Option<NurbsCurve>, KernelRefusal> {
997 if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || !(d1 > d0) {
998 return Ok(None);
999 }
1000 let feet_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.feet_ms");
1001 let mut control = Vec::with_capacity(2);
1002 for t in [d0, d1] {
1003 let point = rail.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
1004 let foot = crate::project_point_to_surface(surface, point).or_refuse(KernelStage::Refine, "project")?;
1005 control.push(crate::Vec4::from_point(crate::Vec3::new(foot.u, foot.v, 0.0), 1.0));
1006 }
1007 let line = NurbsCurve::new(1, vec![d0, d0, d1, d1], control).or_refuse(KernelStage::Refine, "new")?;
1008 feet_timing.stop();
1009 let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1010 let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &line, d0, d1);
1011 read_timing.stop();
1012 Ok(march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE).then_some(line))
1013}
1014
1015pub(in crate::blend) fn build_open_surgery(
1027 solid: &BrepSolid,
1028 result: &mut BrepSolid,
1029 take_id: &mut dyn FnMut() -> u64,
1030 edge: &EdgeRecord,
1031 first: &BlendMate,
1032 second: &BlendMate,
1033 rows: FittedRows,
1034 ends: [EndPlan; 2],
1035 name: Option<&str>,
1036) -> Result<SewnStripe, KernelRefusal> {
1037 let [start_end, finish_end] = ends;
1038
1039 let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, KernelRefusal> {
1046 let [low, high] = row.domain().or_refuse(KernelStage::Refine, "domain")?;
1047 let tail = if a == low { row.clone() } else { row.split(a).or_refuse(KernelStage::Refine, "split")?.1 };
1048 if b == high {
1049 return Ok(tail);
1050 }
1051 let (middle, _) = tail.split(b).or_refuse(KernelStage::Refine, "split")?;
1052 Ok(middle)
1053 };
1054 let kind = |plan: &EndPlan| match plan {
1055 EndPlan::Free(_) => "free",
1056 EndPlan::Corner(_) => "corner",
1057 EndPlan::Miter(_) => "miter",
1058 EndPlan::Cap(_) => "cap",
1059 };
1060 let describe_trim = |error: KernelRefusal| {
1061 error.with_message(|error| {
1062 format!(
1063 "{error} (edge {} rows trimmed for a {} start at cr {:.6}/cs {:.6} and a {} finish \
1064 at cr {:.6}/cs {:.6})",
1065 edge.id,
1066 kind(&start_end),
1067 start_end.cr_parameter(),
1068 start_end.cs_parameter(),
1069 kind(&finish_end),
1070 finish_end.cr_parameter(),
1071 finish_end.cs_parameter()
1072 )
1073 })
1074 };
1075 let band = consumed_band(solid);
1085 let exact_trim = crate::blend::exact_trim_on();
1086 let collapses = |row: &NurbsCurve, a: f64, b: f64| -> Result<bool, KernelRefusal> {
1087 let from = row.evaluate(a).or_refuse(KernelStage::Refine, "evaluate")?;
1088 Ok(row.evaluate(b).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band
1089 && row.evaluate(0.5 * (a + b)).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band)
1090 };
1091 let cr_collapsed = collapses(&rows.cr, start_end.cr_parameter(), finish_end.cr_parameter())?;
1092 let cs_collapsed = collapses(&rows.cs, start_end.cs_parameter(), finish_end.cs_parameter())?;
1093 if cr_collapsed || cs_collapsed {
1094 let unsupported = if start_end.free().is_some() && finish_end.free().is_some() {
1095 Some("neither end is a corner")
1096 } else if first.face.id == second.face.id {
1097 Some("both rails lie on one face")
1098 } else if matches!(start_end, EndPlan::Cap(_)) || matches!(finish_end, EndPlan::Cap(_)) {
1099 Some("a capped end has no corner to share the point with")
1100 } else {
1101 None
1102 };
1103 if let Some(reason) = unsupported {
1104 return Err(KernelRefusal::unsupported(KernelStage::Sew, super::RAIL_COLLAPSE_WHAT, format!(
1105 "{RAIL_COLLAPSE_UNSUPPORTED} edge {}'s rail shrinks to a point and {reason}",
1106 edge.id
1107 )));
1108 }
1109 }
1110 let trim_rail = |collapsed: bool,
1124 row: &NurbsCurve,
1125 pcurve: &NurbsCurve,
1126 surface: &NurbsSurface,
1127 a: f64,
1128 b: f64|
1129 -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1130 if collapsed {
1131 return Ok(None);
1132 }
1133 let rows_timing = crate::blend::BlendTimer::start("timing.surgery.trim.rows_ms");
1134 let rail = trim_row(row, a, b).map_err(describe_trim)?;
1135 let interpolated = trim_row(pcurve, a, b)?;
1136 rows_timing.stop();
1137 {
1138 let overrun_timing = crate::blend::BlendTimer::start("timing.surgery.trim.overrun_ms");
1139 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1140 let overrun = crate::blend::planar_chart_overrun(surface, &|t| rail.evaluate(t), d0, d1)?;
1141 overrun_timing.stop();
1142 if overrun.is_some_and(|excursion| excursion > band) {
1143 return Ok(Some((rail, interpolated)));
1144 }
1145 }
1146 let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1147 if exact_trim && rows.exact_extrusion {
1151 if let Some(line) = exact_planar_trim(&rail, surface, d0, d1)? {
1152 crate::blend::record_count("count.surgery.exact_trim", 1);
1153 return Ok(Some((rail, line)));
1154 }
1155 }
1156 if rows.revolution_axis.is_some() {
1161 let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1162 let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &interpolated, d0, d1);
1163 read_timing.stop();
1164 if march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
1165 crate::blend::record_count("count.surgery.exact_revolution_trim", 1);
1166 return Ok(Some((rail, interpolated)));
1167 }
1168 crate::blend::record_count("count.surgery.exact_revolution_trim_declined", 1);
1169 }
1170 let mut march_feet = crate::blend::track_fit::FootMemo::default();
1179 let mut refit_feet = crate::blend::track_fit::FootMemo::default();
1180 let march_read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.march_ms");
1181 let march_read = crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &interpolated, d0, d1, &mut march_feet);
1182 march_read_timing.stop();
1183 let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1184 let declined = |why: KernelRefusal| -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1185 let stands = march_trim_stands(&march_read, floor);
1188 if stands {
1189 Ok(Some((rail.clone(), interpolated.clone())))
1190 } else {
1191 Err(why)
1192 }
1193 };
1194 let refit_timing = crate::blend::BlendTimer::start("timing.surgery.trim.refit_ms");
1195 let refit = super::closed::refit_closed_support_pcurve_read(&rail, &interpolated, surface, &mut refit_feet);
1196 refit_timing.stop();
1197 let _reads_timing = crate::blend::BlendTimer::start("timing.surgery.trim.reads_ms");
1198 match refit {
1199 Ok((fitted, refit_read)) => {
1200 let (march_standoff, refit_standoff) = match (&march_read, Ok::<_, KernelRefusal>(refit_read)) {
1217 (_, Err(error)) => return declined(error),
1218 (Err(error), Ok(_)) => {
1219 return Err(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch_unread", format!(
1220 "blend: a support trim's refit cannot be shown to stand on its rail's branch: the march's own \
1221 trim is unreadable ({})",
1222 error.message
1223 )));
1224 }
1225 (Ok((_, march_standoff, _, _)), Ok((_, refit_standoff, _, _))) => (*march_standoff, refit_standoff),
1226 };
1227 let excess_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.excess_ms");
1228 let excess = crate::blend::track_fit::standoff_excess_memo(
1229 surface, &|t| rail.evaluate(t), &interpolated, &fitted, d0, d1, &mut march_feet, &mut refit_feet,
1230 );
1231 excess_timing.stop();
1232 let excess = match excess {
1233 Ok(excess) => excess,
1234 Err(error) => return declined(error),
1235 };
1236 if !(excess <= floor) {
1237 return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch", format!(
1238 "blend: a support trim's refit stands up to {excess:.3e} farther off its rail than the march's own trim \
1239 at the same rail parameter (largest standoffs {refit_standoff:.3e} and {march_standoff:.3e}): it took \
1240 another branch of the carrier"
1241 )));
1242 }
1243 let nearest_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.nearest_ms");
1251 let absolute = crate::blend::track_fit::standoff_over_nearest_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut refit_feet);
1252 nearest_timing.stop();
1253 let absolute = match absolute {
1254 Ok(absolute) => absolute,
1255 Err(error) => return declined(error),
1256 };
1257 if !(absolute <= floor) {
1258 return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_off_rail", format!(
1259 "blend: a support trim's refit stands up to {absolute:.3e} farther off its rail than the rail stands \
1260 off its carrier: its foot is not the rail's nearest point"
1261 )));
1262 }
1263 Ok(Some((rail, fitted)))
1264 }
1265 Err(error) => declined(error),
1266 }
1267 };
1268 let trim_timing = crate::blend::BlendTimer::start("timing.surgery.trim_rails_ms");
1269 let cr_rail = trim_rail(
1270 cr_collapsed,
1271 &rows.cr,
1272 &rows.cr_pcurve,
1273 &first.face.surface,
1274 start_end.cr_parameter(),
1275 finish_end.cr_parameter(),
1276 )?;
1277 let cs_rail = trim_rail(
1278 cs_collapsed,
1279 &rows.cs,
1280 &rows.cs_pcurve,
1281 &second.face.surface,
1282 start_end.cs_parameter(),
1283 finish_end.cs_parameter(),
1284 )?;
1285 trim_timing.stop();
1286 let cr_domain = match &cr_rail {
1288 Some((cr, _)) => cr.domain().or_refuse(KernelStage::Refine, "domain")?,
1289 None => [start_end.cr_parameter(), finish_end.cr_parameter()],
1290 };
1291 let cs_domain = match &cs_rail {
1292 Some((cs, _)) => cs.domain().or_refuse(KernelStage::Refine, "domain")?,
1293 None => [start_end.cs_parameter(), finish_end.cs_parameter()],
1294 };
1295 let cr_at = |parameter: f64| match &cr_rail {
1297 Some((cr, _)) => cr.evaluate(parameter),
1298 None => rows.cr.evaluate(parameter),
1299 };
1300 let cs_at = |parameter: f64| match &cs_rail {
1301 Some((cs, _)) => cs.evaluate(parameter),
1302 None => rows.cs.evaluate(parameter),
1303 };
1304 let old_start = edge.start_vertex_id;
1305 let old_finish = edge.end_vertex_id;
1306
1307 let classify =
1318 |boundary_id: u64, boundary_param: f64, corner: u64| -> Result<RimResolution, KernelRefusal> {
1319 let boundary = result
1320 .edges
1321 .iter()
1322 .find(|candidate| candidate.id == boundary_id)
1323 .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_boundary_edge", "blend: end boundary edge missing during surgery"))?;
1324 let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
1325 let far_vertex = if boundary.start_vertex_id == corner {
1326 Some((boundary.end_vertex_id, boundary.t1, boundary.t0))
1327 } else if boundary.end_vertex_id == corner {
1328 Some((boundary.start_vertex_id, boundary.t0, boundary.t1))
1329 } else {
1330 None
1331 };
1332 if let Some((far_id, far_t, near_t)) = far_vertex {
1333 let far_point = result
1344 .vertices
1345 .iter()
1346 .find(|candidate| candidate.id == far_id)
1347 .map(|candidate| candidate.point)
1348 .ok_or(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"))?;
1349 let crossing = boundary.curve.evaluate_extended(boundary_param).or_refuse(KernelStage::Refine, "evaluate_extended")?;
1350 let _ = span;
1351 let past_far = crossing.sub(far_point).length();
1352 if past_far <= band {
1353 if !result
1355 .vertices
1356 .iter()
1357 .any(|candidate| candidate.id == far_id)
1358 {
1359 return Err(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"));
1360 }
1361 return Ok(RimResolution::Consumed(far_id));
1362 }
1363 if (boundary_param - far_t) * (far_t - near_t) > 0.0 {
1376 return Err(KernelRefusal::unsupported(KernelStage::Sew, "blend_wider_than_face", format!(
1377 "{BLEND_WIDER_THAN_FACE} its rail crosses edge {boundary_id} {past_far:.3e} \
1378 past that edge's far vertex, and a rail within {band:.3e} of it is the \
1379 most this surgery snaps onto the edge"
1380 )));
1381 }
1382 }
1383 Ok(RimResolution::Fresh)
1384 };
1385 let resolve_side = |plan: &EndPlan,
1389 side_first: bool,
1390 corner: u64|
1391 -> Result<RimResolution, KernelRefusal> {
1392 if let Some(vertex) = plan.planned_rim(side_first) {
1393 return Ok(RimResolution::Corner(vertex));
1394 }
1395 match plan {
1396 EndPlan::Free(free) => classify(
1397 if side_first {
1398 free.first_edge_id
1399 } else {
1400 free.second_edge_id
1401 },
1402 if side_first {
1403 free.first_edge_parameter
1404 } else {
1405 free.second_edge_parameter
1406 },
1407 corner,
1408 ),
1409 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => {
1410 Err(KernelRefusal::internal(KernelStage::Sew, "rim_vertices", "blend: planned end without rim vertices"))
1411 }
1412 }
1413 };
1414 let crossings_timing = crate::blend::BlendTimer::start("timing.surgery.crossings_ms");
1415 let start_first = resolve_side(&start_end, true, old_start)?;
1416 let start_second = resolve_side(&start_end, false, old_start)?;
1417 let finish_first = resolve_side(&finish_end, true, old_finish)?;
1418 let finish_second = resolve_side(&finish_end, false, old_finish)?;
1419 crossings_timing.stop();
1420 let _topology_timing = crate::blend::BlendTimer::start("timing.surgery.topology_ms");
1421
1422 let first_use_forward = first.coedge.forward;
1425 let blend_cr_forward = !first_use_forward;
1426
1427 let (sew_first, sew_second) = match (start_end.free(), finish_end.free(), &cr_rail, &cs_rail) {
1442 (Some(start_free), Some(finish_free), Some((cr, _)), Some((cs, _))) => (
1443 detect_row_coincidence(
1444 solid,
1445 first,
1446 edge.id,
1447 start_free.first_edge_id,
1448 finish_free.first_edge_id,
1449 start_first.is_consumed(),
1450 start_first.existing().unwrap_or(0),
1451 finish_first.is_consumed(),
1452 finish_first.existing().unwrap_or(0),
1453 cr,
1454 blend_cr_forward,
1455 band,
1456 )?,
1457 detect_row_coincidence(
1458 solid,
1459 second,
1460 edge.id,
1461 start_free.second_edge_id,
1462 finish_free.second_edge_id,
1463 start_second.is_consumed(),
1464 start_second.existing().unwrap_or(0),
1465 finish_second.is_consumed(),
1466 finish_second.existing().unwrap_or(0),
1467 cs,
1468 !blend_cr_forward,
1469 band,
1470 )?,
1471 ),
1472 _ => (None, None),
1473 };
1474 let (sew_first, sew_second): (Option<RowSew>, Option<RowSew>) =
1478 if first.face.id == second.face.id {
1479 (None, None)
1480 } else {
1481 (sew_first, sew_second)
1482 };
1483
1484 let mut resolve = |rim: &RimResolution, point: Result<Vec3, String>| -> Result<u64, KernelRefusal> {
1487 match rim.existing() {
1488 Some(id) => Ok(id),
1489 None => {
1490 let id = take_id();
1491 result.vertices.push(VertexRecord { id, point: point.or_refuse(KernelStage::Refine, "point")? });
1492 Ok(id)
1493 }
1494 }
1495 };
1496 let w1a = resolve(&start_first, cr_at(cr_domain[0]))?;
1497 let w1b = resolve(&finish_first, cr_at(cr_domain[1]))?;
1498 let w2a = resolve(&start_second, cs_at(cs_domain[0]))?;
1499 let w2b = resolve(&finish_second, cs_at(cs_domain[1]))?;
1500 let mut snap = [&sew_first, &sew_second]
1503 .into_iter()
1504 .flatten()
1505 .fold(0.0f64, |worst, sew| worst.max(sew.deviation));
1506 for (rim, vertex, row_end) in [
1507 (&start_first, w1a, cr_at(cr_domain[0]).or_refuse(KernelStage::Refine, "cr_at")?),
1508 (&finish_first, w1b, cr_at(cr_domain[1]).or_refuse(KernelStage::Refine, "cr_at")?),
1509 (&start_second, w2a, cs_at(cs_domain[0]).or_refuse(KernelStage::Refine, "cs_at")?),
1510 (&finish_second, w2b, cs_at(cs_domain[1]).or_refuse(KernelStage::Refine, "cs_at")?),
1511 ] {
1512 if !rim.is_consumed() {
1513 continue;
1514 }
1515 let point = result
1516 .vertices
1517 .iter()
1518 .find(|candidate| candidate.id == vertex)
1519 .ok_or(KernelRefusal::internal(KernelStage::Sew, "rim_vertex", "blend: consumed rim vertex missing"))?
1520 .point;
1521 snap = snap.max(row_end.sub(point).length());
1522 }
1523
1524 let cr_edge_id = match (&sew_first, &cr_rail) {
1529 (Some(sew), _) => Some(sew.edge_id),
1530 (None, None) => None,
1531 (None, Some((cr, _))) => {
1532 let id = take_id();
1533 result.edges.push(EdgeRecord {
1534 id,
1535 curve: cr.clone(),
1536 t0: cr_domain[0],
1537 t1: cr_domain[1],
1538 start_vertex_id: w1a,
1539 end_vertex_id: w1b,
1540 degenerate: false,
1541 name: None,
1542 });
1543 Some(id)
1544 }
1545 };
1546 let cs_edge_id = match (&sew_second, &cs_rail) {
1547 (Some(sew), _) => Some(sew.edge_id),
1548 (None, None) => None,
1549 (None, Some((cs, _))) => {
1550 let id = take_id();
1551 result.edges.push(EdgeRecord {
1552 id,
1553 curve: cs.clone(),
1554 t0: cs_domain[0],
1555 t1: cs_domain[1],
1556 start_vertex_id: w2a,
1557 end_vertex_id: w2b,
1558 degenerate: false,
1559 name: None,
1560 });
1561 Some(id)
1562 }
1563 };
1564 let mut commit_end_edge = |plan: &EndPlan,
1569 first_rim: u64,
1570 second_rim: u64|
1571 -> Result<Option<u64>, KernelRefusal> {
1572 match plan {
1573 EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
1574 EndPlan::Free(free) => {
1575 let id = take_id();
1576 let domain = free.transverse_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1577 result.edges.push(EdgeRecord {
1578 id,
1579 curve: free.transverse_curve.clone(),
1580 t0: domain[0],
1581 t1: domain[1],
1582 start_vertex_id: first_rim,
1583 end_vertex_id: second_rim,
1584 degenerate: false,
1585 name: None,
1586 });
1587 Ok(Some(id))
1588 }
1589 }
1590 };
1591 let transverse_a_id = commit_end_edge(&start_end, w1a, w2a)?;
1592 let transverse_b_id = commit_end_edge(&finish_end, w1b, w2b)?;
1593
1594 for (mate, rail, sewn) in [
1601 (first, cr_edge_id.zip(cr_rail.as_ref()), sew_first.is_some()),
1602 (second, cs_edge_id.zip(cs_rail.as_ref()), sew_second.is_some()),
1603 ] {
1604 if sewn {
1605 continue;
1606 }
1607 let face = result
1608 .shells
1609 .iter_mut()
1610 .flat_map(|shell| &mut shell.faces)
1611 .find(|face| face.id == mate.face.id)
1612 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1613 let loop_record = &mut face.loops[mate.loop_index];
1614 let position = loop_record
1615 .coedges
1616 .iter()
1617 .position(|coedge| coedge.edge_id == edge.id)
1618 .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
1619 let Some((new_edge_id, (_, pcurve_forward))) = rail else {
1620 loop_record.coedges.remove(position);
1621 continue;
1622 };
1623 let old_forward = loop_record.coedges[position].forward;
1624 loop_record.coedges[position] = CoedgeRecord {
1625 id: loop_record.coedges[position].id,
1626 edge_id: new_edge_id,
1627 forward: old_forward,
1628 pcurve: if old_forward {
1629 pcurve_forward.clone()
1630 } else {
1631 pcurve_forward.reversed().or_refuse(KernelStage::Refine, "reversed")?
1632 },
1633 };
1634 }
1635
1636 for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
1641 let EndPlan::Cap(cap) = plan else {
1642 continue;
1643 };
1644 for (mate, rail_edge_id, leg, rim) in [
1645 (first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
1646 (second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
1647 ] {
1648 let rail_edge_id = rail_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "end_rail", "blend: a capped end lost its rail"))?;
1650 let face = result
1651 .shells
1652 .iter_mut()
1653 .flat_map(|shell| &mut shell.faces)
1654 .find(|face| face.id == mate.face.id)
1655 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during cap splice"))?;
1656 let loop_record = &mut face.loops[mate.loop_index];
1657 let count = loop_record.coedges.len();
1658 let rail_at = loop_record
1659 .coedges
1660 .iter()
1661 .position(|coedge| coedge.edge_id == rail_edge_id)
1662 .ok_or(KernelRefusal::internal(KernelStage::Sew, "rail_coedge", "blend: rail coedge lost during cap splice"))?;
1663 let rail_forward = loop_record.coedges[rail_at].forward;
1669 let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
1670 let (insert_at, forward, pcurve) = if end_is_traversal_end {
1671 (rail_at + 1, true, leg.1.clone())
1673 } else {
1674 (rail_at, false, leg.1.reversed().or_refuse(KernelStage::Refine, "reversed")?)
1676 };
1677 let _ = (count, rim);
1678 loop_record.coedges.insert(
1679 insert_at,
1680 CoedgeRecord {
1681 id: take_id(),
1682 edge_id: leg.0,
1683 forward,
1684 pcurve,
1685 },
1686 );
1687 }
1688 }
1689
1690 let mut consumed_edges: Vec<u64> = Vec::new();
1696 let start_free = start_end.free();
1697 let finish_free = finish_end.free();
1698 for (mate, sewn, sides) in [
1699 (
1700 first,
1701 sew_first.is_some(),
1702 [
1703 (&start_first, start_free.map(|free| free.first_edge_id)),
1704 (&finish_first, finish_free.map(|free| free.first_edge_id)),
1705 ],
1706 ),
1707 (
1708 second,
1709 sew_second.is_some(),
1710 [
1711 (&start_second, start_free.map(|free| free.second_edge_id)),
1712 (&finish_second, finish_free.map(|free| free.second_edge_id)),
1713 ],
1714 ),
1715 ] {
1716 for (cross, boundary_id) in sides {
1717 if !cross.is_consumed() {
1718 continue;
1719 }
1720 let Some(boundary_id) = boundary_id else {
1721 continue;
1722 };
1723 if !sewn {
1724 let face = result
1725 .shells
1726 .iter_mut()
1727 .flat_map(|shell| &mut shell.faces)
1728 .find(|face| face.id == mate.face.id)
1729 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1730 face.loops[mate.loop_index]
1731 .coedges
1732 .retain(|coedge| coedge.edge_id != boundary_id);
1733 }
1734 if !consumed_edges.contains(&boundary_id) {
1735 consumed_edges.push(boundary_id);
1736 }
1737 }
1738 }
1739 for sew in [&sew_first, &sew_second].into_iter().flatten() {
1741 for id in &sew.collapsed_edges {
1742 if !consumed_edges.contains(id) {
1743 consumed_edges.push(*id);
1744 }
1745 }
1746 }
1747
1748 let mut end_plans = Vec::with_capacity(2);
1757 for (end, transverse_id, corner, first_consumed, second_consumed) in [
1758 (
1759 start_end.free(),
1760 transverse_a_id,
1761 old_start,
1762 start_first.is_consumed(),
1763 start_second.is_consumed(),
1764 ),
1765 (
1766 finish_end.free(),
1767 transverse_b_id,
1768 old_finish,
1769 finish_first.is_consumed(),
1770 finish_second.is_consumed(),
1771 ),
1772 ] {
1773 let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
1774 continue;
1775 };
1776 let face = result
1777 .shells
1778 .iter()
1779 .flat_map(|shell| &shell.faces)
1780 .find(|face| face.id == end.end_face_id)
1781 .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_face", "blend: end face lost during surgery"))?;
1782 let Some(located) = locate_end_corner(
1785 &result,
1786 face,
1787 corner,
1788 end.first_edge_id,
1789 end.second_edge_id,
1790 ) else {
1791 return Err(KernelRefusal::internal(KernelStage::Sew, "end_face_corner", "blend: end-face corner (adjacent boundary coedges) not found"));
1792 };
1793 for pole in &located.pole_edge_ids {
1796 if !consumed_edges.contains(pole) {
1797 consumed_edges.push(*pole);
1798 }
1799 }
1800 end_plans.push((
1801 end.end_face_id,
1802 transverse_id,
1803 end.transverse_end_pcurve.clone(),
1804 located,
1805 first_consumed,
1806 second_consumed,
1807 ));
1808 }
1809
1810 for (rim_resolution, boundary, corner, rim) in [
1813 (
1814 &start_first,
1815 start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
1816 old_start,
1817 w1a,
1818 ),
1819 (
1820 &start_second,
1821 start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
1822 old_start,
1823 w2a,
1824 ),
1825 (
1826 &finish_first,
1827 finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
1828 old_finish,
1829 w1b,
1830 ),
1831 (
1832 &finish_second,
1833 finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
1834 old_finish,
1835 w2b,
1836 ),
1837 ] {
1838 if !rim_resolution.trims_boundary() {
1839 continue;
1840 }
1841 let Some((boundary_id, boundary_param)) = boundary else {
1842 continue;
1843 };
1844 trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
1845 }
1846
1847 for (end_face, transverse_id, transverse_end_pcurve, located, first_consumed, second_consumed) in
1850 end_plans
1851 {
1852 let forward = located.x_is_first;
1853 let pcurve = if forward {
1854 transverse_end_pcurve
1855 } else {
1856 transverse_end_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
1857 };
1858 let (x_consumed, y_consumed) = if located.x_is_first {
1859 (first_consumed, second_consumed)
1860 } else {
1861 (second_consumed, first_consumed)
1862 };
1863 let transverse_coedge = CoedgeRecord {
1864 id: take_id(),
1865 edge_id: transverse_id,
1866 forward,
1867 pcurve,
1868 };
1869 splice_end_corner(
1870 result,
1871 end_face,
1872 &located,
1873 transverse_coedge,
1874 x_consumed,
1875 y_consumed,
1876 )?;
1877 }
1878
1879 let (mid_u, station_uv) = match (&cr_rail, &cs_rail) {
1884 (Some((_, cr_pcurve)), _) => {
1885 let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
1886 (mid_u, cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
1887 }
1888 (None, Some(_)) => {
1889 let mid_u = (cs_domain[0] + cs_domain[1]) * 0.5;
1890 (mid_u, rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
1891 }
1892 (None, None) => (cr_domain[0], rows.cr_pcurve.evaluate(cr_domain[0]).or_refuse(KernelStage::Refine, "evaluate")?),
1893 };
1894 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
1895 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
1896 let out1 = if first.face.same_sense {
1897 n1
1898 } else {
1899 n1.scale(-1.0)
1900 };
1901 let same_sense = blend_normal.dot(out1) >= 0.0;
1902 let loop_id = take_id();
1903 let cr_forward = sew_first
1911 .as_ref()
1912 .map_or(blend_cr_forward, |sew| sew.forward);
1913 let cs_forward = sew_second
1914 .as_ref()
1915 .map_or(!blend_cr_forward, |sew| sew.forward);
1916 let mut end_slot = |plan: &EndPlan,
1926 free_edge_id: Option<u64>,
1927 natural_forward: bool|
1928 -> Result<Vec<CoedgeRecord>, KernelRefusal> {
1929 match plan {
1930 EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
1931 id: take_id(),
1932 edge_id: end.arc_edge_id,
1933 forward: true,
1934 pcurve: end.arc_blend_pcurve.clone(),
1935 }]),
1936 EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
1937 id: take_id(),
1938 edge_id: end.arc_edge_id,
1939 forward: true,
1940 pcurve: end.arc_blend_pcurve.clone(),
1941 }]),
1942 EndPlan::Miter(end) => end
1943 .edges
1944 .iter()
1945 .map(|(edge_id, forward, pcurve)| {
1946 Ok(CoedgeRecord {
1947 id: take_id(),
1948 edge_id: *edge_id,
1949 forward: *forward,
1950 pcurve: pcurve.clone(),
1951 })
1952 })
1953 .collect(),
1954 EndPlan::Free(free) => {
1955 let edge_id =
1956 free_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "transverse_edge", "blend: free end without its transverse edge"))?;
1957 Ok(vec![if natural_forward {
1958 CoedgeRecord {
1959 id: take_id(),
1960 edge_id,
1961 forward: true,
1962 pcurve: free.transverse_blend_pcurve.clone(),
1963 }
1964 } else {
1965 CoedgeRecord {
1966 id: take_id(),
1967 edge_id,
1968 forward: false,
1969 pcurve: free.transverse_blend_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?,
1970 }
1971 }])
1972 }
1973 }
1974 };
1975 let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
1976 let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
1977 let coedges = if blend_cr_forward {
1980 let mut coedges = Vec::new();
1981 if let Some(cr_edge_id) = cr_edge_id {
1982 coedges.push(CoedgeRecord {
1983 id: take_id(),
1984 edge_id: cr_edge_id,
1985 forward: cr_forward,
1986 pcurve: crate::sweep_topology::parameter_line(
1987 cr_domain[0],
1988 0.0,
1989 cr_domain[1],
1990 0.0,
1991 ).or_refuse(KernelStage::Refine, "parameter_line")?,
1992 });
1993 }
1994 coedges.extend(finish_slot);
1995 if let Some(cs_edge_id) = cs_edge_id {
1996 coedges.push(CoedgeRecord {
1997 id: take_id(),
1998 edge_id: cs_edge_id,
1999 forward: cs_forward,
2000 pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
2001 });
2002 }
2003 coedges.extend(start_slot);
2004 coedges
2005 } else {
2006 let mut coedges = Vec::new();
2007 if let Some(cr_edge_id) = cr_edge_id {
2008 coedges.push(CoedgeRecord {
2009 id: take_id(),
2010 edge_id: cr_edge_id,
2011 forward: cr_forward,
2012 pcurve: crate::sweep_topology::parameter_line(
2013 cr_domain[1],
2014 0.0,
2015 cr_domain[0],
2016 0.0,
2017 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2018 });
2019 }
2020 coedges.extend(start_slot);
2021 if let Some(cs_edge_id) = cs_edge_id {
2022 coedges.push(CoedgeRecord {
2023 id: take_id(),
2024 edge_id: cs_edge_id,
2025 forward: cs_forward,
2026 pcurve: crate::sweep_topology::parameter_line(
2027 cs_domain[0],
2028 1.0,
2029 cs_domain[1],
2030 1.0,
2031 ).or_refuse(KernelStage::Refine, "parameter_line")?,
2032 });
2033 }
2034 coedges.extend(finish_slot);
2035 coedges
2036 };
2037 let blend_face_id = take_id();
2038 let mut blend_face = FaceRecord {
2039 id: blend_face_id,
2040 surface: rows.surface,
2041 same_sense,
2042 loops: vec![LoopRecord {
2043 id: loop_id,
2044 coedges,
2045 }],
2046 name: name.map(|value| value.to_string()),
2047 };
2048 if rows.exact_extrusion {
2049 transpose_face(&mut blend_face)?;
2050 }
2051 let shell_index = result
2052 .shells
2053 .iter()
2054 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
2055 .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
2056 for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
2061 if sew.is_some() {
2062 for shell in &mut result.shells {
2063 shell.faces.retain(|face| face.id != mate.face.id);
2064 }
2065 }
2066 }
2067 result.shells[shell_index].faces.push(blend_face);
2068
2069 result.edges.retain(|candidate| candidate.id != edge.id);
2070 result
2073 .edges
2074 .retain(|candidate| !consumed_edges.contains(&candidate.id));
2075 let mut identified = Vec::new();
2077 for (collapsed, from, to) in [(cr_collapsed, w1a, w1b), (cs_collapsed, w2a, w2b)] {
2078 if collapsed && from != to {
2079 identified.push((from, to));
2080 }
2081 }
2082 Ok(SewnStripe {
2083 cr_edge_id,
2084 cs_edge_id,
2085 blend_face_id,
2086 consumed: [sew_first.is_some(), sew_second.is_some()],
2087 snap,
2088 identified,
2089 })
2090}
2091
2092pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), KernelRefusal> {
2098 let surface = &face.surface;
2099 let rows_u = surface.control_points.len();
2100 let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
2101 let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
2102 for row in &surface.control_points {
2103 for (j, point) in row.iter().enumerate() {
2104 transposed[j].push(*point);
2105 }
2106 }
2107 face.surface = NurbsSurface::new(
2108 surface.degree_v,
2109 surface.degree_u,
2110 surface.knots_v.clone(),
2111 surface.knots_u.clone(),
2112 transposed,
2113 ).or_refuse(KernelStage::Refine, "new")?;
2114 for loop_record in &mut face.loops {
2115 for coedge in &mut loop_record.coedges {
2116 for control in &mut coedge.pcurve.control_points {
2117 std::mem::swap(&mut control.x, &mut control.y);
2118 }
2119 }
2120 }
2121 face.same_sense = !face.same_sense;
2122 Ok(())
2123}
2124
2125pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
2130 let used: rustc_hash::FxHashSet<u64> = result
2131 .edges
2132 .iter()
2133 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
2134 .collect();
2135 result
2136 .vertices
2137 .retain(|candidate| used.contains(&candidate.id));
2138}
2139
2140fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
2141 crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")
2142}
2143