diff --git a/crates/fj-core/src/algorithms/approx/curve/approx.rs b/crates/fj-core/src/algorithms/approx/curve/approx.rs index 6bbb008a4..c97fd7419 100644 --- a/crates/fj-core/src/algorithms/approx/curve/approx.rs +++ b/crates/fj-core/src/algorithms/approx/curve/approx.rs @@ -1,3 +1,7 @@ +use fj_math::Point; + +use crate::geometry::CurveBoundary; + use super::CurveApproxSegment; /// Partial approximation of a curve @@ -19,6 +23,15 @@ impl CurveApprox { self } + /// Reduce the approximation to the subset defined by the provided boundary + pub fn make_subset(&mut self, boundary: &CurveBoundary>) { + for segment in &mut self.segments { + segment.make_subset(boundary.normalize()); + } + + self.segments.retain(|segment| !segment.is_empty()); + } + /// Merge the provided segment into the approximation pub fn merge( &mut self, diff --git a/crates/fj-core/src/algorithms/approx/curve/cache.rs b/crates/fj-core/src/algorithms/approx/curve/cache.rs index c1913324a..93c9db331 100644 --- a/crates/fj-core/src/algorithms/approx/curve/cache.rs +++ b/crates/fj-core/src/algorithms/approx/curve/cache.rs @@ -25,20 +25,13 @@ impl CurveApproxCache { ) -> Option { let curve = HandleWrapper::from(curve.clone()); - // Approximations within the cache are all stored in normalized form. If - // the caller requests a non-normalized boundary, that means we need to - // adjust the result before we return it, so let's remember whether the - // normalization resulted in a reversal. - let was_already_normalized = boundary.is_normalized(); - let normalized_boundary = boundary.normalize(); - let mut approx = self.inner.get(&curve).cloned()?; + approx.make_subset(boundary); - approx - .segments - .retain(|segment| segment.boundary == normalized_boundary); - - if !was_already_normalized { + // Approximations within the cache are stored in normalized form. If the + // caller requests a non-normalized boundary, that means we need to + // adjust the result before we return it. + if !boundary.is_normalized() { approx.reverse(); } @@ -53,6 +46,10 @@ impl CurveApproxCache { ) -> CurveApproxSegment { let curve = HandleWrapper::from(curve); + // Overlapping approximations need to result in the same points, + // regardless of what direction those overlapping approximations happen + // to have. To make sure this is always the case, we normalize each new + // approximated segment before doing *anything* with it. new_segment.normalize(); let (approx, segment) = match self.inner.remove(&curve) { @@ -256,4 +253,112 @@ pub mod tests { }) ); } + + #[test] + fn partial_match_that_overlaps_start() { + let mut services = Services::new(); + + let mut cache = CurveApproxCache::default(); + let curve = Curve::new().insert(&mut services); + + cache.insert( + curve.clone(), + CurveApproxSegment { + boundary: CurveBoundary::from([[0.], [1.]]), + points: vec![ + ApproxPoint::new([0.125], [0.125, 0.125, 0.125]), + ApproxPoint::new([0.375], [0.375, 0.375, 0.375]), + ApproxPoint::new([0.625], [0.625, 0.625, 0.625]), + ApproxPoint::new([0.875], [0.875, 0.875, 0.875]), + ], + } + .clone(), + ); + + let cached = cache.get(&curve, &CurveBoundary::from([[-0.5], [0.5]])); + assert_eq!( + cached, + Some(CurveApprox { + segments: vec![CurveApproxSegment { + boundary: CurveBoundary::from([[0.], [0.5]]), + points: vec![ + ApproxPoint::new([0.125], [0.125, 0.125, 0.125]), + ApproxPoint::new([0.375], [0.375, 0.375, 0.375]), + ], + }] + }) + ); + } + + #[test] + fn partial_match_that_overlaps_end() { + let mut services = Services::new(); + + let mut cache = CurveApproxCache::default(); + let curve = Curve::new().insert(&mut services); + + cache.insert( + curve.clone(), + CurveApproxSegment { + boundary: CurveBoundary::from([[0.], [1.]]), + points: vec![ + ApproxPoint::new([0.125], [0.125, 0.125, 0.125]), + ApproxPoint::new([0.375], [0.375, 0.375, 0.375]), + ApproxPoint::new([0.625], [0.625, 0.625, 0.625]), + ApproxPoint::new([0.875], [0.875, 0.875, 0.875]), + ], + } + .clone(), + ); + + let cached = cache.get(&curve, &CurveBoundary::from([[0.5], [1.5]])); + assert_eq!( + cached, + Some(CurveApprox { + segments: vec![CurveApproxSegment { + boundary: CurveBoundary::from([[0.5], [1.0]]), + points: vec![ + ApproxPoint::new([0.625], [0.625, 0.625, 0.625]), + ApproxPoint::new([0.875], [0.875, 0.875, 0.875]), + ], + }] + }) + ); + } + + #[test] + fn partial_match_in_the_middle() { + let mut services = Services::new(); + + let mut cache = CurveApproxCache::default(); + let curve = Curve::new().insert(&mut services); + + cache.insert( + curve.clone(), + CurveApproxSegment { + boundary: CurveBoundary::from([[0.], [1.]]), + points: vec![ + ApproxPoint::new([0.125], [0.125, 0.125, 0.125]), + ApproxPoint::new([0.375], [0.375, 0.375, 0.375]), + ApproxPoint::new([0.625], [0.625, 0.625, 0.625]), + ApproxPoint::new([0.875], [0.875, 0.875, 0.875]), + ], + } + .clone(), + ); + + let cached = cache.get(&curve, &CurveBoundary::from([[0.25], [0.75]])); + assert_eq!( + cached, + Some(CurveApprox { + segments: vec![CurveApproxSegment { + boundary: CurveBoundary::from([[0.25], [0.75]]), + points: vec![ + ApproxPoint::new([0.375], [0.375, 0.375, 0.375]), + ApproxPoint::new([0.625], [0.625, 0.625, 0.625]), + ], + }] + }) + ); + } } diff --git a/crates/fj-core/src/algorithms/approx/curve/segment.rs b/crates/fj-core/src/algorithms/approx/curve/segment.rs index d77d2cbb6..b0383ec4e 100644 --- a/crates/fj-core/src/algorithms/approx/curve/segment.rs +++ b/crates/fj-core/src/algorithms/approx/curve/segment.rs @@ -20,6 +20,23 @@ pub struct CurveApproxSegment { } impl CurveApproxSegment { + /// Indicate whether the segment is empty + pub fn is_empty(&self) -> bool { + let is_empty = { + let [min, max] = self.boundary.inner; + min >= max + }; + + if is_empty { + assert!( + self.points.is_empty(), + "Empty approximation still has points" + ); + } + + is_empty + } + /// Indicate whether the segment is normalized pub fn is_normalized(&self) -> bool { self.boundary.is_normalized() @@ -44,6 +61,32 @@ impl CurveApproxSegment { self } + + /// Reduce the approximation to the subset defined by the provided boundary + pub fn make_subset(&mut self, boundary: CurveBoundary>) { + assert!( + self.boundary.is_normalized(), + "Expected normalized segment for making subset." + ); + assert!( + boundary.is_normalized(), + "Expected subset to be defined by normalized boundary." + ); + + let [min, max] = boundary.inner; + + self.boundary.inner = { + let [self_min, self_max] = self.boundary.inner; + + let min = cmp::max(self_min, min); + let max = cmp::min(self_max, max); + + [min, max] + }; + + self.points + .retain(|point| point.local_form > min && point.local_form < max); + } } impl Ord for CurveApproxSegment {