1use crate::mass_properties::parameter_space_area;
2use crate::topology::{adaptive_coedge_error, BrepSolid, CoedgeRecord, EdgeRecord};
3use crate::{
4 build_pcurve_on_surface, interpolate_curve, KernelTolerances, NurbsCurve, NurbsSurface, Vec3,
5};
6use rustc_hash::FxHashMap as HashMap;
7
8fn trimmed_curve(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<NurbsCurve, String> {
9 let [start, end] = curve.domain()?;
10 let epsilon = (1e-9 * (end - start)).max(2e-9);
14 let mut result = curve.clone();
15 if t0 > start + epsilon && t0 < end - epsilon {
16 result = result.split(t0)?.1;
17 }
18 let domain = result.domain()?;
19 if t1 < domain[1] - epsilon && t1 > domain[0] + epsilon {
20 result = result.split(t1)?.0;
21 } else if std::env::var("BREP_DEBUG_SUBRANGE").is_ok() && t1 < domain[1] - epsilon {
22 eprintln!("abnormal subrange skip in coalesce trimmed_curve");
23 }
24 Ok(result)
25}
26
27fn curvature_at(curve: &NurbsCurve, parameter: f64) -> Result<f64, String> {
28 let derivatives = curve.derivatives(parameter, 2)?;
29 let speed = derivatives[1].length();
30 if speed <= 1e-12 {
31 return Ok(0.0);
32 }
33 Ok(derivatives[1].cross(derivatives[2]).length() / speed.powi(3))
34}
35
36fn constrain_curve_endpoints(
44 mut curve: NurbsCurve,
45 start: Vec3,
46 end: Vec3,
47) -> Result<NurbsCurve, String> {
48 let [domain_start, domain_end] = curve.domain()?;
49 let first_weight = curve
50 .control_points
51 .first()
52 .ok_or("cannot constrain an empty curve")?
53 .w;
54 let last_weight = curve
55 .control_points
56 .last()
57 .ok_or("cannot constrain an empty curve")?
58 .w;
59 if first_weight.abs() <= 1e-15 || last_weight.abs() <= 1e-15 {
60 return Err("cannot constrain a curve endpoint with zero weight".into());
61 }
62 let last = curve.control_points.len() - 1;
63 curve.control_points[0].x = start.x * first_weight;
64 curve.control_points[0].y = start.y * first_weight;
65 curve.control_points[0].z = start.z * first_weight;
66 curve.control_points[last].x = end.x * last_weight;
67 curve.control_points[last].y = end.y * last_weight;
68 curve.control_points[last].z = end.z * last_weight;
69 let endpoint_epsilon = 1e-10;
70 if curve.evaluate(domain_start)?.sub(start).length() > endpoint_epsilon
71 || curve.evaluate(domain_end)?.sub(end).length() > endpoint_epsilon
72 {
73 return Err("continuation curve is not clamped at its topology endpoints".into());
74 }
75 Ok(curve)
76}
77
78fn concatenate_sampled_pieces(
82 first: &NurbsCurve,
83 second: &NurbsCurve,
84 split: f64,
85 tolerance: f64,
86) -> Result<Option<NurbsCurve>, String> {
87 let first_domain = first.domain()?;
88 let second_domain = second.domain()?;
89 let samples_per_piece = 48;
90 let mut points = Vec::<Vec3>::with_capacity(samples_per_piece * 2 + 1);
91 let mut parameters = Vec::with_capacity(samples_per_piece * 2 + 1);
92 for index in 0..=samples_per_piece {
93 let fraction = index as f64 / samples_per_piece as f64;
94 points.push(
95 first.evaluate(first_domain[0] + (first_domain[1] - first_domain[0]) * fraction)?,
96 );
97 parameters.push(split * fraction);
98 }
99 for index in 1..=samples_per_piece {
100 let fraction = index as f64 / samples_per_piece as f64;
101 points.push(
102 second.evaluate(second_domain[0] + (second_domain[1] - second_domain[0]) * fraction)?,
103 );
104 parameters.push(split + (1.0 - split) * fraction);
105 }
106 let joined = interpolate_curve(&points, first.degree.min(second.degree), ¶meters)?;
107 for index in 0..=64 {
108 let fraction = index as f64 / 64.0;
109 let expected = if fraction <= split {
110 let local = if split <= 1e-12 {
111 0.0
112 } else {
113 fraction / split
114 };
115 first.evaluate(first_domain[0] + (first_domain[1] - first_domain[0]) * local)?
116 } else {
117 let local = (fraction - split) / (1.0 - split);
118 second.evaluate(second_domain[0] + (second_domain[1] - second_domain[0]) * local)?
119 };
120 if joined.evaluate(fraction)?.sub(expected).length() > tolerance {
121 return Ok(None);
122 }
123 }
124 Ok(Some(joined))
125}
126
127fn concatenate_compatible_fitted_pieces(
128 first: &NurbsCurve,
129 second: &NurbsCurve,
130 split: f64,
131 tolerance: f64,
132) -> Result<Option<NurbsCurve>, String> {
133 let first_domain = first.domain()?;
134 let second_domain = second.domain()?;
135 let first_curvature = curvature_at(first, first_domain[1])?;
136 let second_curvature = curvature_at(second, second_domain[0])?;
137 let curvature_scale = first_curvature.abs().max(second_curvature.abs()).max(1.0);
138 if (first_curvature - second_curvature).abs() > curvature_scale * 2e-3 {
143 return Ok(None);
144 }
145 concatenate_sampled_pieces(first, second, split, tolerance)
146}
147
148pub fn concatenate_exact_curve_pieces(
150 first: &NurbsCurve,
151 second: &NurbsCurve,
152 split: f64,
153 tolerance: f64,
154) -> Result<Option<NurbsCurve>, String> {
155 if first.degree != second.degree || split <= 1e-9 || split >= 1.0 - 1e-9 {
156 return Ok(None);
157 }
158 let degree = first.degree;
159 let [first_start, first_end] = first.domain()?;
160 let [second_start, second_end] = second.domain()?;
161 let first_span = first_end - first_start;
162 let second_span = second_end - second_start;
163 if first_span <= 1e-9 || second_span <= 1e-9 {
164 return Ok(None);
165 }
166 let first_end_control = first.control_points[first.control_points.len() - 1];
167 let second_start_control = second.control_points[0];
168 let second_scale = first_end_control.w / second_start_control.w;
169 let scaled_second = second
170 .control_points
171 .iter()
172 .map(|control| control.scale(second_scale))
173 .collect::<Vec<_>>();
174 let scaled_start = scaled_second[0];
175 let scale = 1.0f64
176 .max(first_end_control.x.abs())
177 .max(first_end_control.y.abs())
178 .max(first_end_control.z.abs())
179 .max(first_end_control.w.abs());
180 let homogeneous_tolerance = (1e-10f64).max(tolerance) * scale;
181 if (first_end_control.x - scaled_start.x).abs() > homogeneous_tolerance
182 || (first_end_control.y - scaled_start.y).abs() > homogeneous_tolerance
183 || (first_end_control.z - scaled_start.z).abs() > homogeneous_tolerance
184 || (first_end_control.w - scaled_start.w).abs() > homogeneous_tolerance
185 {
186 return Ok(None);
187 }
188 let remap = |curve: &NurbsCurve, from_start, from_end, to_start, to_end| {
189 curve
190 .knots
191 .iter()
192 .map(|knot| {
193 to_start + (to_end - to_start) * (knot - from_start) / (from_end - from_start)
194 })
195 .collect::<Vec<_>>()
196 };
197 let first_knots = remap(first, first_start, first_end, 0.0, split);
198 let second_knots = remap(second, second_start, second_end, split, 1.0);
199 let mut knots = first_knots[..first_knots.len() - degree - 1].to_vec();
200 knots.extend(std::iter::repeat_n(split, degree));
201 knots.extend_from_slice(&second_knots[degree + 1..]);
202 let mut controls = first.control_points.clone();
203 controls.extend_from_slice(&scaled_second[1..]);
204 let joined = NurbsCurve::new(degree, knots, controls)?;
205 for fraction in [0.0, 0.23, 0.61, 1.0] {
206 if joined
207 .evaluate(split * fraction)?
208 .sub(first.evaluate(first_start + first_span * fraction)?)
209 .length()
210 > tolerance
211 || joined
212 .evaluate(split + (1.0 - split) * fraction)?
213 .sub(second.evaluate(second_start + second_span * fraction)?)
214 .length()
215 > tolerance
216 {
217 return Ok(None);
218 }
219 }
220 Ok(Some(joined))
221}
222
223fn pcurve_matches_edge(
224 surface: &NurbsSurface,
225 pcurve: &NurbsCurve,
226 curve: &NurbsCurve,
227 reversed: bool,
228 tolerance: f64,
229) -> Result<bool, String> {
230 let [p0, p1] = pcurve.domain()?;
231 let [c0, c1] = curve.domain()?;
232 let geometry_tolerance = (tolerance * 1.5).max(1e-4).min(3e-3);
236 for index in 0..=32 {
237 let fraction = index as f64 / 32.0;
238 let uv = pcurve.evaluate(p0 + (p1 - p0) * fraction)?;
239 let curve_fraction = if reversed { 1.0 - fraction } else { fraction };
240 if surface
241 .evaluate(uv.x, uv.y)?
242 .sub(curve.evaluate(c0 + (c1 - c0) * curve_fraction)?)
243 .length()
244 > geometry_tolerance
245 {
246 return Ok(false);
247 }
248 }
249 Ok(true)
250}
251
252#[derive(Clone, Copy)]
253struct UseLocation {
254 face: usize,
255 loop_index: usize,
256 coedge: usize,
257}
258
259fn replace_adjacent(
260 coedges: &mut Vec<CoedgeRecord>,
261 first_index: usize,
262 replacement: CoedgeRecord,
263) -> bool {
264 if coedges.len() < 2 {
265 return false;
266 }
267 if first_index + 1 < coedges.len() {
268 coedges.splice(first_index..first_index + 2, [replacement]);
269 } else {
270 coedges.pop();
271 coedges[0] = replacement;
272 }
273 true
274}
275
276fn traversal_vertex_ids(edge: &EdgeRecord, coedge: &CoedgeRecord) -> (u64, u64) {
277 if coedge.forward {
278 (edge.start_vertex_id, edge.end_vertex_id)
279 } else {
280 (edge.end_vertex_id, edge.start_vertex_id)
281 }
282}
283
284const CONTINUATION_TURN: f64 = 1e-3;
302
303const EXACT_CLOSING_TURN: f64 = 1.4142135623730951e-5;
323
324pub fn merge_curve_continuation_edges(
330 solid: &BrepSolid,
331 tolerance: f64,
332) -> Result<BrepSolid, String> {
333 let mut result = solid.clone();
334 let mut rejected = rustc_hash::FxHashSet::<(u64, u64)>::default();
335 loop {
336 let edges = result
337 .edges
338 .iter()
339 .map(|edge| (edge.id, edge))
340 .collect::<HashMap<_, _>>();
341 let vertices = result
342 .vertices
343 .iter()
344 .map(|vertex| (vertex.id, vertex.point))
345 .collect::<HashMap<_, _>>();
346 let faces = result
347 .shells
348 .iter()
349 .flat_map(|shell| shell.faces.iter())
350 .collect::<Vec<_>>();
351 let mut uses: HashMap<u64, Vec<UseLocation>> = HashMap::default();
352 for (face_index, face) in faces.iter().enumerate() {
353 for (loop_index, loop_record) in face.loops.iter().enumerate() {
354 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
355 uses.entry(coedge.edge_id).or_default().push(UseLocation {
356 face: face_index,
357 loop_index,
358 coedge: coedge_index,
359 });
360 }
361 }
362 }
363 let mut accepted = None;
364 'faces: for (face_index, face) in faces.iter().enumerate() {
365 for (loop_index, loop_record) in face.loops.iter().enumerate() {
366 if loop_record.coedges.len() < 2 {
367 continue;
368 }
369 for first_index in 0..loop_record.coedges.len() {
370 let second_index = (first_index + 1) % loop_record.coedges.len();
371 let first = &loop_record.coedges[first_index];
372 let second = &loop_record.coedges[second_index];
373 let first_edge = edges[&first.edge_id];
374 let second_edge = edges[&second.edge_id];
375 let pair_key = if first.edge_id < second.edge_id {
376 (first.edge_id, second.edge_id)
377 } else {
378 (second.edge_id, first.edge_id)
379 };
380 if rejected.contains(&pair_key) {
381 continue;
382 }
383 if first_edge.degenerate || second_edge.degenerate {
384 continue;
385 }
386 let (_, first_end) = traversal_vertex_ids(first_edge, first);
387 let (second_start, _) = traversal_vertex_ids(second_edge, second);
388 if first_end != second_start {
389 continue;
390 }
391 let Some(first_uses) = uses.get(&first.edge_id) else {
392 continue;
393 };
394 let Some(second_uses) = uses.get(&second.edge_id) else {
395 continue;
396 };
397 if first_uses.len() != 2 || second_uses.len() != 2 {
398 continue;
399 }
400 let first_mate = first_uses
401 .iter()
402 .find(|location| {
403 location.face != face_index
404 || location.loop_index != loop_index
405 || location.coedge != first_index
406 })
407 .copied()
408 .unwrap();
409 let second_mate = second_uses
410 .iter()
411 .find(|location| {
412 location.face != face_index
413 || location.loop_index != loop_index
414 || location.coedge != second_index
415 })
416 .copied()
417 .unwrap();
418 let same_loop =
426 first_mate.face == face_index && first_mate.loop_index == loop_index;
427 if same_loop
428 && (first_mate.coedge == first_index
429 || first_mate.coedge == second_index
430 || second_mate.coedge == first_index
431 || second_mate.coedge == second_index)
432 {
433 continue;
434 }
435 if first_mate.face != second_mate.face
436 || first_mate.loop_index != second_mate.loop_index
437 || (second_mate.coedge + 1)
438 % faces[second_mate.face].loops[second_mate.loop_index]
439 .coedges
440 .len()
441 != first_mate.coedge
442 {
443 continue;
444 }
445 let first_parameter = if first.forward {
446 first_edge.t1
447 } else {
448 first_edge.t0
449 };
450 let second_parameter = if second.forward {
451 second_edge.t0
452 } else {
453 second_edge.t1
454 };
455 let mut first_tangent = first_edge.curve.derivatives(first_parameter, 1)?[1];
456 let mut second_tangent = second_edge.curve.derivatives(second_parameter, 1)?[1];
457 if !first.forward {
458 first_tangent = first_tangent.scale(-1.0);
459 }
460 if !second.forward {
461 second_tangent = second_tangent.scale(-1.0);
462 }
463 let first_tangent = first_tangent.normalized()?;
464 let second_tangent = second_tangent.normalized()?;
465 let turn = first_tangent
469 .cross(second_tangent)
470 .length()
471 .atan2(first_tangent.dot(second_tangent));
472 let closes = traversal_vertex_ids(first_edge, first).0
483 == traversal_vertex_ids(second_edge, second).1;
484 let band = if closes {
485 EXACT_CLOSING_TURN
486 } else {
487 CONTINUATION_TURN
488 };
489 if turn.abs() > band {
490 continue;
491 }
492 let mut first_curve =
493 trimmed_curve(&first_edge.curve, first_edge.t0, first_edge.t1)?;
494 let mut second_curve =
495 trimmed_curve(&second_edge.curve, second_edge.t0, second_edge.t1)?;
496 if !first.forward {
497 first_curve = first_curve.reversed()?;
498 }
499 if !second.forward {
500 second_curve = second_curve.reversed()?;
501 }
502 let first_span = {
503 let domain = first_curve.domain()?;
504 domain[1] - domain[0]
505 };
506 let second_span = {
507 let domain = second_curve.domain()?;
508 domain[1] - domain[0]
509 };
510 let split = first_span / (first_span + second_span);
511 let curve = if let Some(curve) = concatenate_exact_curve_pieces(
512 &first_curve,
513 &second_curve,
514 split,
515 tolerance,
516 )? {
517 curve
518 } else if let Some(curve) = concatenate_compatible_fitted_pieces(
519 &first_curve,
520 &second_curve,
521 split,
522 tolerance,
523 )? {
524 curve
525 } else {
526 continue;
527 };
528 let (start_vertex_id, _) = traversal_vertex_ids(first_edge, first);
529 let (_, end_vertex_id) = traversal_vertex_ids(second_edge, second);
530 let Some(start_point) = vertices.get(&start_vertex_id).copied() else {
531 continue;
532 };
533 let Some(end_point) = vertices.get(&end_vertex_id).copied() else {
534 continue;
535 };
536 let curve = constrain_curve_endpoints(curve, start_point, end_point)?;
537 let (mut face_pcurve, face_pcurve_rebuilt) = if let Some(pcurve) =
538 concatenate_exact_curve_pieces(
539 &first.pcurve,
540 &second.pcurve,
541 split,
542 tolerance,
543 )? {
544 (pcurve, false)
545 } else {
546 (build_pcurve_on_surface(&face.surface, &curve)?, true)
547 };
548 let mate_loop = &faces[first_mate.face].loops[first_mate.loop_index];
549 let second_mate_coedge = &mate_loop.coedges[second_mate.coedge];
550 let first_mate_coedge = &mate_loop.coedges[first_mate.coedge];
551 let (mut mate_pcurve, mate_pcurve_rebuilt) = if let Some(pcurve) =
552 concatenate_exact_curve_pieces(
553 &second_mate_coedge.pcurve,
554 &first_mate_coedge.pcurve,
555 1.0 - split,
556 tolerance,
557 )? {
558 (pcurve, false)
559 } else {
560 (
561 build_pcurve_on_surface(
562 &faces[first_mate.face].surface,
563 &curve.reversed()?,
564 )?,
565 true,
566 )
567 };
568 let mut pcurve_rebuilt = face_pcurve_rebuilt || mate_pcurve_rebuilt;
569 if !pcurve_matches_edge(&face.surface, &face_pcurve, &curve, false, tolerance)?
570 {
571 face_pcurve = concatenate_sampled_pieces(
572 &first.pcurve,
573 &second.pcurve,
574 split,
575 tolerance * 10.0,
576 )?
577 .unwrap_or(build_pcurve_on_surface(&face.surface, &curve)?);
578 pcurve_rebuilt = true;
579 }
580 if !pcurve_matches_edge(
581 &faces[first_mate.face].surface,
582 &mate_pcurve,
583 &curve,
584 true,
585 tolerance,
586 )? {
587 mate_pcurve = concatenate_sampled_pieces(
588 &second_mate_coedge.pcurve,
589 &first_mate_coedge.pcurve,
590 1.0 - split,
591 tolerance * 10.0,
592 )?
593 .unwrap_or(build_pcurve_on_surface(
594 &faces[first_mate.face].surface,
595 &curve.reversed()?,
596 )?);
597 pcurve_rebuilt = true;
598 }
599 if !pcurve_matches_edge(&face.surface, &face_pcurve, &curve, false, tolerance)?
600 || !pcurve_matches_edge(
601 &faces[first_mate.face].surface,
602 &mate_pcurve,
603 &curve,
604 true,
605 tolerance,
606 )?
607 {
608 face_pcurve = build_pcurve_on_surface(&face.surface, &curve)?;
614 mate_pcurve = build_pcurve_on_surface(
615 &faces[first_mate.face].surface,
616 &curve.reversed()?,
617 )?;
618 pcurve_rebuilt = true;
619 }
632 let [candidate_t0, candidate_t1] = curve.domain()?;
637 let candidate_edge = EdgeRecord {
638 id: first.edge_id,
639 curve: curve.clone(),
640 t0: candidate_t0,
641 t1: candidate_t1,
642 start_vertex_id,
643 end_vertex_id,
644 degenerate: false,
645 name: first_edge.name.clone(),
648 };
649 let pcurve_contract =
650 KernelTolerances::for_solid(&result, 1e-7).pcurve_consistency;
651 if adaptive_coedge_error(
652 &face.surface,
653 &face_pcurve,
654 &curve,
655 &candidate_edge,
656 true,
657 pcurve_contract,
658 )? > pcurve_contract
659 || adaptive_coedge_error(
660 &faces[first_mate.face].surface,
661 &mate_pcurve,
662 &curve,
663 &candidate_edge,
664 false,
665 pcurve_contract,
666 )? > pcurve_contract
667 {
668 rejected.insert(pair_key);
669 continue;
670 }
671 accepted = Some((
672 face_index,
673 loop_index,
674 first_index,
675 first_mate,
676 second_mate,
677 first.edge_id,
678 second.edge_id,
679 curve,
680 face_pcurve,
681 mate_pcurve,
682 pcurve_rebuilt,
683 start_vertex_id,
684 end_vertex_id,
685 ));
686 break 'faces;
687 }
688 }
689 }
690 let Some((
691 face_index,
692 loop_index,
693 first_index,
694 first_mate,
695 second_mate,
696 first_edge_id,
697 second_edge_id,
698 curve,
699 face_pcurve,
700 mate_pcurve,
701 pcurve_rebuilt,
702 start_vertex_id,
703 end_vertex_id,
704 )) = accepted
705 else {
706 break;
707 };
708 let before_face = parameter_space_area(faces[face_index])?;
709 let before_mate = parameter_space_area(faces[first_mate.face])?;
710 drop(faces);
711 drop(edges);
712 let mut candidate = result.clone();
713 let new_edge_id = candidate
714 .edges
715 .iter()
716 .map(|edge| edge.id)
717 .max()
718 .unwrap_or(0)
719 + 1;
720 let domain = curve.domain()?;
721 let merged_name = candidate
722 .edges
723 .iter()
724 .find(|edge| edge.id == first_edge_id)
725 .and_then(|edge| edge.name.clone());
726 candidate.edges.push(EdgeRecord {
727 id: new_edge_id,
728 curve,
729 t0: domain[0],
730 t1: domain[1],
731 start_vertex_id,
732 end_vertex_id,
733 degenerate: false,
734 name: merged_name,
735 });
736 let mut next_coedge_id = candidate
737 .shells
738 .iter()
739 .flat_map(|shell| &shell.faces)
740 .flat_map(|face| &face.loops)
741 .flat_map(|loop_record| &loop_record.coedges)
742 .map(|coedge| coedge.id)
743 .max()
744 .unwrap_or(0)
745 + 1;
746 let face_replacement = CoedgeRecord {
747 id: next_coedge_id,
748 edge_id: new_edge_id,
749 forward: true,
750 pcurve: face_pcurve,
751 };
752 next_coedge_id += 1;
753 let mate_replacement = CoedgeRecord {
754 id: next_coedge_id,
755 edge_id: new_edge_id,
756 forward: false,
757 pcurve: mate_pcurve,
758 };
759 let mut candidate_faces = candidate
760 .shells
761 .iter_mut()
762 .flat_map(|shell| shell.faces.iter_mut())
763 .collect::<Vec<_>>();
764 let splices = {
771 let face_splice = (face_index, loop_index, first_index, face_replacement);
772 let mate_splice = (
773 first_mate.face,
774 first_mate.loop_index,
775 second_mate.coedge,
776 mate_replacement,
777 );
778 if face_splice.0 == mate_splice.0
779 && face_splice.1 == mate_splice.1
780 && mate_splice.2 > face_splice.2
781 {
782 [mate_splice, face_splice]
783 } else {
784 [face_splice, mate_splice]
785 }
786 };
787 for (splice_face, splice_loop, splice_index, replacement) in splices {
788 replace_adjacent(
789 &mut candidate_faces[splice_face].loops[splice_loop].coedges,
790 splice_index,
791 replacement,
792 );
793 }
794 let area_tolerance_ratio = if pcurve_rebuilt { 5e-3 } else { 3e-6 };
798 let preserves_area = |before: f64, after: f64| {
799 (after - before).abs() <= 1e-9f64.max(before.abs() * area_tolerance_ratio)
800 && (before.abs() <= 1e-12 || before.is_sign_positive() == after.is_sign_positive())
801 };
802 if !preserves_area(
803 before_face,
804 parameter_space_area(candidate_faces[face_index])?,
805 ) || !preserves_area(
806 before_mate,
807 parameter_space_area(candidate_faces[first_mate.face])?,
808 ) {
809 rejected.insert(if first_edge_id < second_edge_id {
812 (first_edge_id, second_edge_id)
813 } else {
814 (second_edge_id, first_edge_id)
815 });
816 continue;
817 }
818 drop(candidate_faces);
819 candidate
820 .edges
821 .retain(|edge| edge.id != first_edge_id && edge.id != second_edge_id);
822 let used_vertices = candidate
823 .edges
824 .iter()
825 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
826 .collect::<rustc_hash::FxHashSet<_>>();
827 candidate
828 .vertices
829 .retain(|vertex| used_vertices.contains(&vertex.id));
830 result = candidate;
835 }
836 Ok(result)
837}
838
839