1use super::*;
2
3struct SideCross {
5 vertex: u64,
8 consumed: bool,
12}
13
14pub fn blend_open_edge(
17 solid: &BrepSolid,
18 edge_id: u64,
19 radius: f64,
20 chamfer: bool,
21 name: Option<&str>,
22) -> Result<BrepSolid, String> {
23 if !(radius > 0.0) || !radius.is_finite() {
24 return Err("blend: radius must be positive".into());
25 }
26 blend_open_edge_impl(solid, edge_id, &|_| radius, chamfer, name)
27}
28
29pub(super) fn blend_open_edge_impl(
30 solid: &BrepSolid,
31 edge_id: u64,
32 radius_at: &dyn Fn(f64) -> f64,
33 chamfer: bool,
34 name: Option<&str>,
35) -> Result<BrepSolid, String> {
36 let edge = solid
37 .edges
38 .iter()
39 .find(|edge| edge.id == edge_id)
40 .ok_or_else(|| format!("blend: edge {edge_id} not found"))?;
41 if edge.start_vertex_id == edge.end_vertex_id {
42 return Err("blend: blend_open_edge requires an open edge".into());
43 }
44 let (first_face, first_loop, first_coedge) = locate_mate(solid, edge_id, None)?;
45 let (second_face, second_loop, second_coedge) =
46 locate_mate(solid, edge_id, Some((first_face.id, first_loop)))?;
47 let mid_radius = radius_at(edge.t0 + (edge.t1 - edge.t0) * 0.5);
48 let (rho1, rho2) = signed_radii(
49 edge,
50 first_face,
51 first_coedge,
52 second_face,
53 second_coedge,
54 mid_radius,
55 )?;
56 let first_mate = BlendMate {
57 face: first_face,
58 coedge: first_coedge,
59 loop_index: first_loop,
60 rho: rho1,
61 };
62 let second_mate = BlendMate {
63 face: second_face,
64 coedge: second_coedge,
65 loop_index: second_loop,
66 rho: rho2,
67 };
68 let debug = std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok();
76 if debug {
77 let sp = edge.curve.evaluate(edge.t0);
78 let ep = edge.curve.evaluate(edge.t1);
79 eprintln!(
80 "OPEN edge {} v{}->v{} first_face {} second_face {} p0={:?} p1={:?}",
81 edge.id,
82 edge.start_vertex_id,
83 edge.end_vertex_id,
84 first_face.id,
85 second_face.id,
86 sp,
87 ep
88 );
89 }
90 let compute = |overshoot_fraction: f64| -> Result<(FittedRows, Vec<EndSurgery>, bool), String> {
91 let stations = march_open_stations(
92 edge,
93 &first_mate,
94 &second_mate,
95 radius_at,
96 overshoot_fraction,
97 )?;
98 let parameters = station_parameters(&stations);
99 let rows = fit_open_rows(&stations, ¶meters, chamfer)?;
100 let mut ends = Vec::with_capacity(2);
101 let mut in_range = true;
102 for (vertex, at_start) in [(edge.start_vertex_id, true), (edge.end_vertex_id, false)] {
103 let first_edge_id = boundary_edge_at_vertex(solid, first_face, vertex, edge_id)?;
104 let second_edge_id = boundary_edge_at_vertex(solid, second_face, vertex, edge_id)?;
105 if first_edge_id == second_edge_id {
106 return Err("blend: mates share their end boundary edge (unsupported)".into());
107 }
108 let end_face = end_face_id(
109 solid,
110 first_face.id,
111 second_face.id,
112 first_edge_id,
113 second_edge_id,
114 )?;
115 let first_boundary = solid
116 .edges
117 .iter()
118 .find(|candidate| candidate.id == first_edge_id)
119 .ok_or("blend: end boundary edge missing")?;
120 let second_boundary = solid
121 .edges
122 .iter()
123 .find(|candidate| candidate.id == second_edge_id)
124 .ok_or("blend: end boundary edge missing")?;
125 let first_boundary_coedge = first_face
126 .loops
127 .iter()
128 .flat_map(|loop_record| &loop_record.coedges)
129 .find(|coedge| coedge.edge_id == first_edge_id)
130 .ok_or("blend: mate does not use its end boundary edge")?;
131 let second_boundary_coedge = second_face
132 .loops
133 .iter()
134 .flat_map(|loop_record| &loop_record.coedges)
135 .find(|coedge| coedge.edge_id == second_edge_id)
136 .ok_or("blend: mate does not use its end boundary edge")?;
137 let (cr_parameter, first_edge_parameter) =
138 match support_crossing(&rows.cr, first_boundary, at_start) {
139 Ok((s, t, _)) => (s, t),
140 Err(_) => support_crossing_uv(
141 &rows.cr_pcurve,
142 first_boundary,
143 first_boundary_coedge,
144 at_start,
145 )?,
146 };
147 let (cs_parameter, second_edge_parameter) =
148 match support_crossing(&rows.cs, second_boundary, at_start) {
149 Ok((s, t, _)) => (s, t),
150 Err(_) => support_crossing_uv(
151 &rows.cs_pcurve,
152 second_boundary,
153 second_boundary_coedge,
154 at_start,
155 )?,
156 };
157 const RIM_MARGIN: f64 = 2.0e-3;
161 let side_in_range = if at_start {
162 cr_parameter >= RIM_MARGIN && cs_parameter >= RIM_MARGIN
163 } else {
164 cr_parameter <= 1.0 - RIM_MARGIN && cs_parameter <= 1.0 - RIM_MARGIN
165 };
166 in_range &= side_in_range;
167 let end_record = solid
168 .shells
169 .iter()
170 .flat_map(|shell| &shell.faces)
171 .find(|face| face.id == end_face)
172 .ok_or("blend: end face missing")?;
173 if debug {
174 eprintln!(
175 " [os {overshoot_fraction:.2}] end v{vertex} at_start={at_start} end_face={end_face} first_boundary={first_edge_id}(t={first_edge_parameter:.4}) second_boundary={second_edge_id}(t={second_edge_parameter:.4}) cr={cr_parameter:.4} cs={cs_parameter:.4} in_range={side_in_range}"
176 );
177 }
178 let clamp01 = |value: f64| value.clamp(0.0, 1.0);
179 let (transverse, blend_pcurve, end_pcurve) = transverse_curve(
180 &rows,
181 end_record,
182 clamp01(cr_parameter),
183 clamp01(cs_parameter),
184 )?;
185 ends.push(EndSurgery {
186 cr_parameter,
187 cs_parameter,
188 first_edge_id,
189 first_edge_parameter,
190 second_edge_id,
191 second_edge_parameter,
192 end_face_id: end_face,
193 transverse_curve: transverse,
194 transverse_blend_pcurve: blend_pcurve,
195 transverse_end_pcurve: end_pcurve,
196 });
197 }
198 Ok((rows, ends, in_range))
199 };
200
201 let mut chosen: Option<(FittedRows, Vec<EndSurgery>)> = None;
204 let mut last_error: Option<String> = None;
205 for &overshoot_fraction in &[0.08f64, 0.16, 0.28, 0.45] {
206 match compute(overshoot_fraction) {
207 Ok((rows, ends, in_range)) => {
208 let fallback = chosen.is_none();
209 if in_range {
210 chosen = Some((rows, ends));
211 break;
212 } else if fallback {
213 chosen = Some((rows, ends));
214 }
215 }
216 Err(error) => last_error = Some(error),
217 }
218 }
219 let (rows, ends) = chosen.ok_or_else(|| {
220 last_error.unwrap_or_else(|| "blend: open march failed at every overshoot".into())
221 })?;
222
223 build_open_surgery(solid, edge, &first_mate, &second_mate, rows, ends, name)
224}
225
226fn build_open_surgery(
227 solid: &BrepSolid,
228 edge: &EdgeRecord,
229 first: &BlendMate,
230 second: &BlendMate,
231 rows: FittedRows,
232 ends: Vec<EndSurgery>,
233 name: Option<&str>,
234) -> Result<BrepSolid, String> {
235 let mut result = solid.clone();
236 let mut next_id = solid
237 .vertices
238 .iter()
239 .map(|vertex| vertex.id)
240 .chain(solid.edges.iter().map(|edge| edge.id))
241 .chain(
242 solid
243 .shells
244 .iter()
245 .flat_map(|shell| &shell.faces)
246 .flat_map(|face| {
247 face.loops
248 .iter()
249 .map(|loop_record| loop_record.id)
250 .chain(face.loops.iter().flat_map(|loop_record| {
251 loop_record.coedges.iter().map(|coedge| coedge.id)
252 }))
253 .chain(std::iter::once(face.id))
254 }),
255 )
256 .max()
257 .unwrap_or(0)
258 + 1;
259 let mut take_id = || {
260 let id = next_id;
261 next_id += 1;
262 id
263 };
264 let [start_end, finish_end] = match ends.try_into() {
265 Ok(pair) => pair,
266 Err(_) => return Err("blend: open surgery needs exactly two ends".into()),
267 };
268 let start_end: EndSurgery = start_end;
269 let finish_end: EndSurgery = finish_end;
270
271 let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, String> {
273 let (_, tail) = row.split(a)?;
274 let (middle, _) = tail.split(b)?;
275 Ok(middle)
276 };
277 let cr = trim_row(&rows.cr, start_end.cr_parameter, finish_end.cr_parameter)?;
278 let cr_pcurve = trim_row(
279 &rows.cr_pcurve,
280 start_end.cr_parameter,
281 finish_end.cr_parameter,
282 )?;
283 let cs = trim_row(&rows.cs, start_end.cs_parameter, finish_end.cs_parameter)?;
284 let cs_pcurve = trim_row(
285 &rows.cs_pcurve,
286 start_end.cs_parameter,
287 finish_end.cs_parameter,
288 )?;
289
290 let cr_domain = cr.domain()?;
291 let cs_domain = cs.domain()?;
292 let old_start = edge.start_vertex_id;
293 let old_finish = edge.end_vertex_id;
294
295 let classify =
303 |boundary_id: u64, boundary_param: f64, corner: u64| -> Result<SideCross, String> {
304 let boundary = result
305 .edges
306 .iter()
307 .find(|candidate| candidate.id == boundary_id)
308 .ok_or("blend: end boundary edge missing during surgery")?;
309 let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
310 let far_vertex = if boundary.start_vertex_id == corner {
311 Some((boundary.end_vertex_id, boundary.t1))
312 } else if boundary.end_vertex_id == corner {
313 Some((boundary.start_vertex_id, boundary.t0))
314 } else {
315 None
316 };
317 if let Some((far_id, far_t)) = far_vertex {
318 if (boundary_param - far_t).abs() <= span * 0.02 {
319 if !result
321 .vertices
322 .iter()
323 .any(|candidate| candidate.id == far_id)
324 {
325 return Err("blend: consumed boundary far vertex missing".into());
326 }
327 return Ok(SideCross {
328 vertex: far_id,
329 consumed: true,
330 });
331 }
332 }
333 Ok(SideCross {
334 vertex: 0,
335 consumed: false,
336 })
337 };
338 let start_first = classify(
339 start_end.first_edge_id,
340 start_end.first_edge_parameter,
341 old_start,
342 )?;
343 let start_second = classify(
344 start_end.second_edge_id,
345 start_end.second_edge_parameter,
346 old_start,
347 )?;
348 let finish_first = classify(
349 finish_end.first_edge_id,
350 finish_end.first_edge_parameter,
351 old_finish,
352 )?;
353 let finish_second = classify(
354 finish_end.second_edge_id,
355 finish_end.second_edge_parameter,
356 old_finish,
357 )?;
358
359 let mut resolve = |cross: &SideCross, point: Result<Vec3, String>| -> Result<u64, String> {
362 if cross.consumed {
363 Ok(cross.vertex)
364 } else {
365 let id = take_id();
366 result.vertices.push(VertexRecord { id, point: point? });
367 Ok(id)
368 }
369 };
370 let w1a = resolve(&start_first, cr.evaluate(cr_domain[0]))?;
371 let w1b = resolve(&finish_first, cr.evaluate(cr_domain[1]))?;
372 let w2a = resolve(&start_second, cs.evaluate(cs_domain[0]))?;
373 let w2b = resolve(&finish_second, cs.evaluate(cs_domain[1]))?;
374
375 let cr_edge_id = take_id();
376 let cs_edge_id = take_id();
377 result.edges.push(EdgeRecord {
378 id: cr_edge_id,
379 curve: cr.clone(),
380 t0: cr_domain[0],
381 t1: cr_domain[1],
382 start_vertex_id: w1a,
383 end_vertex_id: w1b,
384 degenerate: false,
385 name: None,
386 });
387 result.edges.push(EdgeRecord {
388 id: cs_edge_id,
389 curve: cs.clone(),
390 t0: cs_domain[0],
391 t1: cs_domain[1],
392 start_vertex_id: w2a,
393 end_vertex_id: w2b,
394 degenerate: false,
395 name: None,
396 });
397 let transverse_a_id = take_id();
398 let transverse_b_id = take_id();
399 let ta_domain = start_end.transverse_curve.domain()?;
400 let tb_domain = finish_end.transverse_curve.domain()?;
401 result.edges.push(EdgeRecord {
402 id: transverse_a_id,
403 curve: start_end.transverse_curve.clone(),
404 t0: ta_domain[0],
405 t1: ta_domain[1],
406 start_vertex_id: w1a,
407 end_vertex_id: w2a,
408 degenerate: false,
409 name: None,
410 });
411 result.edges.push(EdgeRecord {
412 id: transverse_b_id,
413 curve: finish_end.transverse_curve.clone(),
414 t0: tb_domain[0],
415 t1: tb_domain[1],
416 start_vertex_id: w1b,
417 end_vertex_id: w2b,
418 degenerate: false,
419 name: None,
420 });
421
422 let mut first_use_forward = true;
426 for (mate, new_edge_id, pcurve_forward, remember) in [
427 (first, cr_edge_id, &cr_pcurve, true),
428 (second, cs_edge_id, &cs_pcurve, false),
429 ] {
430 let face = result
431 .shells
432 .iter_mut()
433 .flat_map(|shell| &mut shell.faces)
434 .find(|face| face.id == mate.face.id)
435 .ok_or("blend: mate face lost during surgery")?;
436 let loop_record = &mut face.loops[mate.loop_index];
437 let position = loop_record
438 .coedges
439 .iter()
440 .position(|coedge| coedge.edge_id == edge.id)
441 .ok_or("blend: edge coedge lost during surgery")?;
442 let old_forward = loop_record.coedges[position].forward;
443 if remember {
444 first_use_forward = old_forward;
445 }
446 loop_record.coedges[position] = CoedgeRecord {
447 id: loop_record.coedges[position].id,
448 edge_id: new_edge_id,
449 forward: old_forward,
450 pcurve: if old_forward {
451 pcurve_forward.clone()
452 } else {
453 pcurve_forward.reversed()?
454 },
455 };
456 }
457
458 let mut consumed_edges: Vec<u64> = Vec::new();
462 for (mate, sides) in [
463 (
464 first,
465 [
466 (&start_first, start_end.first_edge_id),
467 (&finish_first, finish_end.first_edge_id),
468 ],
469 ),
470 (
471 second,
472 [
473 (&start_second, start_end.second_edge_id),
474 (&finish_second, finish_end.second_edge_id),
475 ],
476 ),
477 ] {
478 for (cross, boundary_id) in sides {
479 if !cross.consumed {
480 continue;
481 }
482 let face = result
483 .shells
484 .iter_mut()
485 .flat_map(|shell| &mut shell.faces)
486 .find(|face| face.id == mate.face.id)
487 .ok_or("blend: mate face lost during surgery")?;
488 face.loops[mate.loop_index]
489 .coedges
490 .retain(|coedge| coedge.edge_id != boundary_id);
491 if !consumed_edges.contains(&boundary_id) {
492 consumed_edges.push(boundary_id);
493 }
494 }
495 }
496
497 let traversal_vertices = |result: &BrepSolid, coedge: &CoedgeRecord| -> Option<(u64, u64)> {
502 let edge = result
503 .edges
504 .iter()
505 .find(|candidate| candidate.id == coedge.edge_id)?;
506 Some(if coedge.forward {
507 (edge.start_vertex_id, edge.end_vertex_id)
508 } else {
509 (edge.end_vertex_id, edge.start_vertex_id)
510 })
511 };
512 let mut end_plans = Vec::with_capacity(2);
514 for (end, transverse_id, corner, first_consumed, second_consumed) in [
515 (
516 &start_end,
517 transverse_a_id,
518 old_start,
519 start_first.consumed,
520 start_second.consumed,
521 ),
522 (
523 &finish_end,
524 transverse_b_id,
525 old_finish,
526 finish_first.consumed,
527 finish_second.consumed,
528 ),
529 ] {
530 let face = result
531 .shells
532 .iter()
533 .flat_map(|shell| &shell.faces)
534 .find(|face| face.id == end.end_face_id)
535 .ok_or("blend: end face lost during surgery")?;
536 let mut located = None;
537 'search: for (loop_index, loop_record) in face.loops.iter().enumerate() {
538 let count = loop_record.coedges.len();
539 for index in 0..count {
540 let this = &loop_record.coedges[index];
541 let next = &loop_record.coedges[(index + 1) % count];
542 let this_pair = (this.edge_id, next.edge_id);
543 let matches_pair = this_pair == (end.first_edge_id, end.second_edge_id)
544 || this_pair == (end.second_edge_id, end.first_edge_id);
545 if !matches_pair {
546 continue;
547 }
548 let Some((_, this_end)) = traversal_vertices(&result, this) else {
549 continue;
550 };
551 let Some((next_start, _)) = traversal_vertices(&result, next) else {
552 continue;
553 };
554 if this_end == corner && next_start == corner {
555 located = Some((
558 loop_index,
559 this.id,
560 next.id,
561 this.edge_id == end.first_edge_id,
562 ));
563 break 'search;
564 }
565 }
566 }
567 let Some((loop_index, x_coedge_id, y_coedge_id, x_is_first)) = located else {
568 return Err("blend: end-face corner (adjacent boundary coedges) not found".into());
569 };
570 end_plans.push((
571 end.end_face_id,
572 transverse_id,
573 end.transverse_end_pcurve.clone(),
574 loop_index,
575 x_coedge_id,
576 y_coedge_id,
577 x_is_first,
578 first_consumed,
579 second_consumed,
580 ));
581 }
582
583 for (cross, boundary_id, boundary_param, corner, rim) in [
586 (
587 &start_first,
588 start_end.first_edge_id,
589 start_end.first_edge_parameter,
590 old_start,
591 w1a,
592 ),
593 (
594 &start_second,
595 start_end.second_edge_id,
596 start_end.second_edge_parameter,
597 old_start,
598 w2a,
599 ),
600 (
601 &finish_first,
602 finish_end.first_edge_id,
603 finish_end.first_edge_parameter,
604 old_finish,
605 w1b,
606 ),
607 (
608 &finish_second,
609 finish_end.second_edge_id,
610 finish_end.second_edge_parameter,
611 old_finish,
612 w2b,
613 ),
614 ] {
615 if cross.consumed {
616 continue;
617 }
618 trim_edge_at(&mut result, boundary_id, boundary_param, corner, rim)?;
619 }
620
621 for (
624 end_face,
625 transverse_id,
626 transverse_end_pcurve,
627 loop_index,
628 x_coedge_id,
629 y_coedge_id,
630 x_is_first,
631 first_consumed,
632 second_consumed,
633 ) in end_plans
634 {
635 let forward = x_is_first;
636 let pcurve = if forward {
637 transverse_end_pcurve
638 } else {
639 transverse_end_pcurve.reversed()?
640 };
641 let x_consumed = if x_is_first {
642 first_consumed
643 } else {
644 second_consumed
645 };
646 let y_consumed = if x_is_first {
647 second_consumed
648 } else {
649 first_consumed
650 };
651 let coedge_id = take_id();
652 let mut transverse_coedge = Some(CoedgeRecord {
653 id: coedge_id,
654 edge_id: transverse_id,
655 forward,
656 pcurve,
657 });
658 let face = result
659 .shells
660 .iter_mut()
661 .flat_map(|shell| &mut shell.faces)
662 .find(|face| face.id == end_face)
663 .ok_or("blend: end face lost during surgery")?;
664 let loop_record = &mut face.loops[loop_index];
665 let count = loop_record.coedges.len();
666 let seg_index = loop_record
669 .coedges
670 .iter()
671 .position(|coedge| coedge.id == x_coedge_id)
672 .ok_or("blend: end-face corner coedge lost during surgery")?;
673 let y_index = (seg_index + 1) % count;
674 if loop_record.coedges[y_index].id != y_coedge_id {
675 return Err("blend: end-face corner pair no longer adjacent".into());
676 }
677 let mut rebuilt = Vec::with_capacity(count + 1);
678 for index in 0..count {
679 if index == seg_index {
680 if !x_consumed {
681 rebuilt.push(loop_record.coedges[index].clone());
682 }
683 rebuilt.push(transverse_coedge.take().expect("transverse spliced once"));
684 } else if index == y_index {
685 if !y_consumed {
686 rebuilt.push(loop_record.coedges[index].clone());
687 }
688 } else {
689 rebuilt.push(loop_record.coedges[index].clone());
690 }
691 }
692 loop_record.coedges = rebuilt;
693 }
694
695 let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
697 let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
698 let station_uv = cr_pcurve.evaluate(mid_u)?;
699 let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
700 let out1 = if first.face.same_sense {
701 n1
702 } else {
703 n1.scale(-1.0)
704 };
705 let same_sense = blend_normal.dot(out1) >= 0.0;
706 let loop_id = take_id();
707 let blend_cr_forward = !first_use_forward;
709 let coedges = if blend_cr_forward {
710 vec![
711 CoedgeRecord {
712 id: take_id(),
713 edge_id: cr_edge_id,
714 forward: true,
715 pcurve: crate::sweep_topology::parameter_line(
716 cr_domain[0],
717 0.0,
718 cr_domain[1],
719 0.0,
720 )?,
721 },
722 CoedgeRecord {
723 id: take_id(),
724 edge_id: transverse_b_id,
725 forward: true,
726 pcurve: finish_end.transverse_blend_pcurve.clone(),
727 },
728 CoedgeRecord {
729 id: take_id(),
730 edge_id: cs_edge_id,
731 forward: false,
732 pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
733 },
734 CoedgeRecord {
735 id: take_id(),
736 edge_id: transverse_a_id,
737 forward: false,
738 pcurve: start_end.transverse_blend_pcurve.reversed()?,
739 },
740 ]
741 } else {
742 vec![
743 CoedgeRecord {
744 id: take_id(),
745 edge_id: cr_edge_id,
746 forward: false,
747 pcurve: crate::sweep_topology::parameter_line(
748 cr_domain[1],
749 0.0,
750 cr_domain[0],
751 0.0,
752 )?,
753 },
754 CoedgeRecord {
755 id: take_id(),
756 edge_id: transverse_a_id,
757 forward: true,
758 pcurve: start_end.transverse_blend_pcurve.clone(),
759 },
760 CoedgeRecord {
761 id: take_id(),
762 edge_id: cs_edge_id,
763 forward: true,
764 pcurve: crate::sweep_topology::parameter_line(
765 cs_domain[0],
766 1.0,
767 cs_domain[1],
768 1.0,
769 )?,
770 },
771 CoedgeRecord {
772 id: take_id(),
773 edge_id: transverse_b_id,
774 forward: false,
775 pcurve: finish_end.transverse_blend_pcurve.reversed()?,
776 },
777 ]
778 };
779 let blend_face = FaceRecord {
780 id: take_id(),
781 surface: rows.surface,
782 same_sense,
783 loops: vec![LoopRecord {
784 id: loop_id,
785 coedges,
786 }],
787 name: name.map(|value| value.to_string()),
788 };
789 let shell_index = result
790 .shells
791 .iter()
792 .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
793 .ok_or("blend: mate shell lost during surgery")?;
794 result.shells[shell_index].faces.push(blend_face);
795
796 result.edges.retain(|candidate| candidate.id != edge.id);
797 result
800 .edges
801 .retain(|candidate| !consumed_edges.contains(&candidate.id));
802 let used: rustc_hash::FxHashSet<u64> = result
805 .edges
806 .iter()
807 .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
808 .collect();
809 result
810 .vertices
811 .retain(|candidate| used.contains(&candidate.id));
812 Ok(result)
813}
814
815fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, String> {
816 crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0)
817}