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
284pub fn merge_curve_continuation_edges(
289 solid: &BrepSolid,
290 tolerance: f64,
291) -> Result<BrepSolid, String> {
292 let mut result = solid.clone();
293 let mut rejected = rustc_hash::FxHashSet::<(u64, u64)>::default();
294 loop {
295 let edges = result
296 .edges
297 .iter()
298 .map(|edge| (edge.id, edge))
299 .collect::<HashMap<_, _>>();
300 let vertices = result
301 .vertices
302 .iter()
303 .map(|vertex| (vertex.id, vertex.point))
304 .collect::<HashMap<_, _>>();
305 let faces = result
306 .shells
307 .iter()
308 .flat_map(|shell| shell.faces.iter())
309 .collect::<Vec<_>>();
310 let mut uses: HashMap<u64, Vec<UseLocation>> = HashMap::default();
311 for (face_index, face) in faces.iter().enumerate() {
312 for (loop_index, loop_record) in face.loops.iter().enumerate() {
313 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
314 uses.entry(coedge.edge_id).or_default().push(UseLocation {
315 face: face_index,
316 loop_index,
317 coedge: coedge_index,
318 });
319 }
320 }
321 }
322 let mut accepted = None;
323 'faces: for (face_index, face) in faces.iter().enumerate() {
324 for (loop_index, loop_record) in face.loops.iter().enumerate() {
325 if loop_record.coedges.len() < 2 {
326 continue;
327 }
328 for first_index in 0..loop_record.coedges.len() {
329 let second_index = (first_index + 1) % loop_record.coedges.len();
330 let first = &loop_record.coedges[first_index];
331 let second = &loop_record.coedges[second_index];
332 let first_edge = edges[&first.edge_id];
333 let second_edge = edges[&second.edge_id];
334 let pair_key = if first.edge_id < second.edge_id {
335 (first.edge_id, second.edge_id)
336 } else {
337 (second.edge_id, first.edge_id)
338 };
339 if rejected.contains(&pair_key) {
340 continue;
341 }
342 if first_edge.degenerate || second_edge.degenerate {
343 continue;
344 }
345 let (_, first_end) = traversal_vertex_ids(first_edge, first);
346 let (second_start, _) = traversal_vertex_ids(second_edge, second);
347 if first_end != second_start {
348 continue;
349 }
350 let Some(first_uses) = uses.get(&first.edge_id) else {
351 continue;
352 };
353 let Some(second_uses) = uses.get(&second.edge_id) else {
354 continue;
355 };
356 if first_uses.len() != 2 || second_uses.len() != 2 {
357 continue;
358 }
359 let first_mate = first_uses
360 .iter()
361 .find(|location| {
362 location.face != face_index
363 || location.loop_index != loop_index
364 || location.coedge != first_index
365 })
366 .copied()
367 .unwrap();
368 let second_mate = second_uses
369 .iter()
370 .find(|location| {
371 location.face != face_index
372 || location.loop_index != loop_index
373 || location.coedge != second_index
374 })
375 .copied()
376 .unwrap();
377 if first_mate.face == face_index && first_mate.loop_index == loop_index {
383 continue;
384 }
385 if first_mate.face != second_mate.face
386 || first_mate.loop_index != second_mate.loop_index
387 || (second_mate.coedge + 1)
388 % faces[second_mate.face].loops[second_mate.loop_index]
389 .coedges
390 .len()
391 != first_mate.coedge
392 {
393 continue;
394 }
395 let first_parameter = if first.forward {
396 first_edge.t1
397 } else {
398 first_edge.t0
399 };
400 let second_parameter = if second.forward {
401 second_edge.t0
402 } else {
403 second_edge.t1
404 };
405 let mut first_tangent = first_edge.curve.derivatives(first_parameter, 1)?[1];
406 let mut second_tangent = second_edge.curve.derivatives(second_parameter, 1)?[1];
407 if !first.forward {
408 first_tangent = first_tangent.scale(-1.0);
409 }
410 if !second.forward {
411 second_tangent = second_tangent.scale(-1.0);
412 }
413 let first_tangent = first_tangent.normalized()?;
414 let second_tangent = second_tangent.normalized()?;
415 if first_tangent.dot(second_tangent) < 1.0 - 1e-10 {
416 continue;
417 }
418 let mut first_curve =
419 trimmed_curve(&first_edge.curve, first_edge.t0, first_edge.t1)?;
420 let mut second_curve =
421 trimmed_curve(&second_edge.curve, second_edge.t0, second_edge.t1)?;
422 if !first.forward {
423 first_curve = first_curve.reversed()?;
424 }
425 if !second.forward {
426 second_curve = second_curve.reversed()?;
427 }
428 let first_span = {
429 let domain = first_curve.domain()?;
430 domain[1] - domain[0]
431 };
432 let second_span = {
433 let domain = second_curve.domain()?;
434 domain[1] - domain[0]
435 };
436 let split = first_span / (first_span + second_span);
437 let curve = if let Some(curve) = concatenate_exact_curve_pieces(
438 &first_curve,
439 &second_curve,
440 split,
441 tolerance,
442 )? {
443 curve
444 } else if let Some(curve) = concatenate_compatible_fitted_pieces(
445 &first_curve,
446 &second_curve,
447 split,
448 tolerance,
449 )? {
450 curve
451 } else {
452 continue;
453 };
454 let (start_vertex_id, _) = traversal_vertex_ids(first_edge, first);
455 let (_, end_vertex_id) = traversal_vertex_ids(second_edge, second);
456 let Some(start_point) = vertices.get(&start_vertex_id).copied() else {
457 continue;
458 };
459 let Some(end_point) = vertices.get(&end_vertex_id).copied() else {
460 continue;
461 };
462 let curve = constrain_curve_endpoints(curve, start_point, end_point)?;
463 let (mut face_pcurve, face_pcurve_rebuilt) = if let Some(pcurve) =
464 concatenate_exact_curve_pieces(
465 &first.pcurve,
466 &second.pcurve,
467 split,
468 tolerance,
469 )? {
470 (pcurve, false)
471 } else {
472 (build_pcurve_on_surface(&face.surface, &curve)?, true)
473 };
474 let mate_loop = &faces[first_mate.face].loops[first_mate.loop_index];
475 let second_mate_coedge = &mate_loop.coedges[second_mate.coedge];
476 let first_mate_coedge = &mate_loop.coedges[first_mate.coedge];
477 let (mut mate_pcurve, mate_pcurve_rebuilt) = if let Some(pcurve) =
478 concatenate_exact_curve_pieces(
479 &second_mate_coedge.pcurve,
480 &first_mate_coedge.pcurve,
481 1.0 - split,
482 tolerance,
483 )? {
484 (pcurve, false)
485 } else {
486 (
487 build_pcurve_on_surface(
488 &faces[first_mate.face].surface,
489 &curve.reversed()?,
490 )?,
491 true,
492 )
493 };
494 let mut pcurve_rebuilt = face_pcurve_rebuilt || mate_pcurve_rebuilt;
495 if !pcurve_matches_edge(&face.surface, &face_pcurve, &curve, false, tolerance)?
496 {
497 face_pcurve = concatenate_sampled_pieces(
498 &first.pcurve,
499 &second.pcurve,
500 split,
501 tolerance * 10.0,
502 )?
503 .unwrap_or(build_pcurve_on_surface(&face.surface, &curve)?);
504 pcurve_rebuilt = true;
505 }
506 if !pcurve_matches_edge(
507 &faces[first_mate.face].surface,
508 &mate_pcurve,
509 &curve,
510 true,
511 tolerance,
512 )? {
513 mate_pcurve = concatenate_sampled_pieces(
514 &second_mate_coedge.pcurve,
515 &first_mate_coedge.pcurve,
516 1.0 - split,
517 tolerance * 10.0,
518 )?
519 .unwrap_or(build_pcurve_on_surface(
520 &faces[first_mate.face].surface,
521 &curve.reversed()?,
522 )?);
523 pcurve_rebuilt = true;
524 }
525 if !pcurve_matches_edge(&face.surface, &face_pcurve, &curve, false, tolerance)?
526 || !pcurve_matches_edge(
527 &faces[first_mate.face].surface,
528 &mate_pcurve,
529 &curve,
530 true,
531 tolerance,
532 )?
533 {
534 face_pcurve = build_pcurve_on_surface(&face.surface, &curve)?;
540 mate_pcurve = build_pcurve_on_surface(
541 &faces[first_mate.face].surface,
542 &curve.reversed()?,
543 )?;
544 pcurve_rebuilt = true;
545 if !pcurve_matches_edge(
546 &face.surface,
547 &face_pcurve,
548 &curve,
549 false,
550 tolerance,
551 )? || !pcurve_matches_edge(
552 &faces[first_mate.face].surface,
553 &mate_pcurve,
554 &curve,
555 true,
556 tolerance,
557 )? {
558 continue;
559 }
560 }
561 let [candidate_t0, candidate_t1] = curve.domain()?;
566 let candidate_edge = EdgeRecord {
567 id: first.edge_id,
568 curve: curve.clone(),
569 t0: candidate_t0,
570 t1: candidate_t1,
571 start_vertex_id,
572 end_vertex_id,
573 degenerate: false,
574 name: first_edge.name.clone(),
577 };
578 let pcurve_contract =
579 KernelTolerances::for_solid(&result, 1e-7).pcurve_consistency;
580 if adaptive_coedge_error(
581 &face.surface,
582 &face_pcurve,
583 &curve,
584 &candidate_edge,
585 true,
586 pcurve_contract,
587 )? > pcurve_contract
588 || adaptive_coedge_error(
589 &faces[first_mate.face].surface,
590 &mate_pcurve,
591 &curve,
592 &candidate_edge,
593 false,
594 pcurve_contract,
595 )? > pcurve_contract
596 {
597 rejected.insert(pair_key);
598 continue;
599 }
600 accepted = Some((
601 face_index,
602 loop_index,
603 first_index,
604 first_mate,
605 second_mate,
606 first.edge_id,
607 second.edge_id,
608 curve,
609 face_pcurve,
610 mate_pcurve,
611 pcurve_rebuilt,
612 start_vertex_id,
613 end_vertex_id,
614 ));
615 break 'faces;
616 }
617 }
618 }
619 let Some((
620 face_index,
621 loop_index,
622 first_index,
623 first_mate,
624 second_mate,
625 first_edge_id,
626 second_edge_id,
627 curve,
628 face_pcurve,
629 mate_pcurve,
630 pcurve_rebuilt,
631 start_vertex_id,
632 end_vertex_id,
633 )) = accepted
634 else {
635 break;
636 };
637 let before_face = parameter_space_area(faces[face_index])?;
638 let before_mate = parameter_space_area(faces[first_mate.face])?;
639 drop(faces);
640 drop(edges);
641 let mut candidate = result.clone();
642 let new_edge_id = candidate
643 .edges
644 .iter()
645 .map(|edge| edge.id)
646 .max()
647 .unwrap_or(0)
648 + 1;
649 let domain = curve.domain()?;
650 let merged_name = candidate
651 .edges
652 .iter()
653 .find(|edge| edge.id == first_edge_id)
654 .and_then(|edge| edge.name.clone());
655 candidate.edges.push(EdgeRecord {
656 id: new_edge_id,
657 curve,
658 t0: domain[0],
659 t1: domain[1],
660 start_vertex_id,
661 end_vertex_id,
662 degenerate: false,
663 name: merged_name,
664 });
665 let mut next_coedge_id = candidate
666 .shells
667 .iter()
668 .flat_map(|shell| &shell.faces)
669 .flat_map(|face| &face.loops)
670 .flat_map(|loop_record| &loop_record.coedges)
671 .map(|coedge| coedge.id)
672 .max()
673 .unwrap_or(0)
674 + 1;
675 let face_replacement = CoedgeRecord {
676 id: next_coedge_id,
677 edge_id: new_edge_id,
678 forward: true,
679 pcurve: face_pcurve,
680 };
681 next_coedge_id += 1;
682 let mate_replacement = CoedgeRecord {
683 id: next_coedge_id,
684 edge_id: new_edge_id,
685 forward: false,
686 pcurve: mate_pcurve,
687 };
688 let mut candidate_faces = candidate
689 .shells
690 .iter_mut()
691 .flat_map(|shell| shell.faces.iter_mut())
692 .collect::<Vec<_>>();
693 replace_adjacent(
694 &mut candidate_faces[face_index].loops[loop_index].coedges,
695 first_index,
696 face_replacement,
697 );
698 replace_adjacent(
699 &mut candidate_faces[first_mate.face].loops[first_mate.loop_index].coedges,
700 second_mate.coedge,
701 mate_replacement,
702 );
703 let area_tolerance_ratio = if pcurve_rebuilt { 5e-3 } else { 3e-6 };
707 let preserves_area = |before: f64, after: f64| {
708 (after - before).abs() <= 1e-9f64.max(before.abs() * area_tolerance_ratio)
709 && (before.abs() <= 1e-12 || before.is_sign_positive() == after.is_sign_positive())
710 };
711 if !preserves_area(
712 before_face,
713 parameter_space_area(candidate_faces[face_index])?,
714 ) || !preserves_area(
715 before_mate,
716 parameter_space_area(candidate_faces[first_mate.face])?,
717 ) {
718 rejected.insert(if first_edge_id < second_edge_id {
721 (first_edge_id, second_edge_id)
722 } else {
723 (second_edge_id, first_edge_id)
724 });
725 continue;
726 }
727 drop(candidate_faces);
728 candidate
729 .edges
730 .retain(|edge| edge.id != first_edge_id && edge.id != second_edge_id);
731 let used_vertices = candidate
732 .edges
733 .iter()
734 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
735 .collect::<rustc_hash::FxHashSet<_>>();
736 candidate
737 .vertices
738 .retain(|vertex| used_vertices.contains(&vertex.id));
739 result = candidate;
744 }
745 Ok(result)
746}
747
748#[cfg(test)]
749mod tests {
750 use super::*;
751 use crate::{apply_edge_splits, build_imprints, make_box_brep, ImprintOptions, Vec3};
752
753 #[test]
754 fn endpoint_constraint_is_exact_for_clamped_fitted_curve() {
755 let points = (0..=12)
756 .map(|index| {
757 let x = index as f64 / 12.0;
758 Vec3::new(x, x * x, 0.0)
759 })
760 .collect::<Vec<_>>();
761 let parameters = (0..=12)
762 .map(|index| index as f64 / 12.0)
763 .collect::<Vec<_>>();
764 let curve = interpolate_curve(&points, 8, ¶meters).unwrap();
765 let start = Vec3::new(-2e-4, 3e-4, 0.0);
766 let end = Vec3::new(1.0002, 0.9997, 0.0);
767 let curve = constrain_curve_endpoints(curve, start, end).unwrap();
768 let [t0, t1] = curve.domain().unwrap();
769 assert!(curve.evaluate(t0).unwrap().sub(start).length() < 1e-12);
770 assert!(curve.evaluate(t1).unwrap().sub(end).length() < 1e-12);
771 }
772
773 #[test]
774 fn exact_line_pieces_concatenate_without_refitting() {
775 let line = crate::make_line(Vec3::default(), Vec3::new(10.0, 0.0, 0.0)).unwrap();
776 let (first, second) = line.split(0.4).unwrap();
777 let joined = concatenate_exact_curve_pieces(&first, &second, 0.4, 1e-9)
778 .unwrap()
779 .unwrap();
780 for index in 0..=20 {
781 let parameter = index as f64 / 20.0;
782 assert!(
783 joined
784 .evaluate(parameter)
785 .unwrap()
786 .sub(line.evaluate(parameter).unwrap())
787 .length()
788 < 1e-10
789 );
790 }
791 }
792
793 #[test]
794 fn rational_arc_pieces_concatenate_on_the_same_exact_curve() {
795 let arc = crate::make_arc(
796 Vec3::new(1.0, -2.0, 3.0),
797 Vec3::new(1.0, 0.0, 0.0),
798 Vec3::new(0.0, 1.0, 0.0),
799 5.0,
800 -0.4,
801 4.7,
802 )
803 .unwrap();
804 let (first, second) = arc.split(0.37).unwrap();
805 let joined = concatenate_exact_curve_pieces(&first, &second, 0.37, 1e-9)
806 .unwrap()
807 .unwrap();
808 for index in 0..=50 {
809 let parameter = index as f64 / 50.0;
810 assert!(
811 joined
812 .evaluate(parameter)
813 .unwrap()
814 .sub(arc.evaluate(parameter).unwrap())
815 .length()
816 < 1e-9
817 );
818 }
819 }
820
821 #[test]
822 fn fragmented_box_edges_collapse_on_both_incident_faces() {
823 let first = make_box_brep(Vec3::default(), 4.0, 4.0, 4.0).unwrap();
824 let second = make_box_brep(Vec3::new(2.0, 1.0, 1.0), 4.0, 4.0, 4.0).unwrap();
825 let imprint = build_imprints(&first, &second, &ImprintOptions::default()).unwrap();
826 let split = apply_edge_splits(&first, 0, &imprint).unwrap();
827 assert!(split.edges.len() > first.edges.len());
828 let merged = merge_curve_continuation_edges(&split, 1e-7).unwrap();
829 assert_eq!(merged.edges.len(), first.edges.len());
830 assert!(merged.validate().is_empty());
831 }
832}