Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean up approximation code #1084

Merged
merged 9 commits into from
Sep 14, 2022
Merged
208 changes: 25 additions & 183 deletions crates/fj-kernel/src/algorithms/approx/curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,19 @@
//! done, to give the caller (who knows the boundary anyway) more options on how
//! to further process the approximation.

use std::cmp::max;

use fj_math::{Circle, Point, Scalar};

use crate::{
objects::{Curve, GlobalCurve, Vertex},
path::GlobalPath,
objects::{Curve, GlobalCurve},
path::RangeOnPath,
};

use super::{Approx, ApproxCache, ApproxPoint, Tolerance};

impl Approx for (&Curve, RangeOnCurve) {
impl Approx for (&Curve, RangeOnPath) {
type Approximation = CurveApprox;

fn approx_with_cache(
self,
tolerance: Tolerance,
tolerance: impl Into<Tolerance>,
cache: &mut ApproxCache,
) -> Self::Approximation {
let (curve, range) = self;
Expand All @@ -39,19 +35,18 @@ impl Approx for (&Curve, RangeOnCurve) {
curve.path().point_from_path_coords(point.local_form);
ApproxPoint::new(point_surface, point.global_form)
.with_source((*curve, point.local_form))
})
.collect();
});

CurveApprox { points }
CurveApprox::empty().with_points(points)
}
}

impl Approx for (&GlobalCurve, RangeOnCurve) {
impl Approx for (&GlobalCurve, RangeOnPath) {
type Approximation = GlobalCurveApprox;

fn approx_with_cache(
self,
tolerance: Tolerance,
tolerance: impl Into<Tolerance>,
cache: &mut ApproxCache,
) -> Self::Approximation {
let (curve, range) = self;
Expand All @@ -60,190 +55,37 @@ impl Approx for (&GlobalCurve, RangeOnCurve) {
return approx;
}

let points = match curve.path() {
GlobalPath::Circle(circle) => {
approx_circle(&circle, range, tolerance)
}
GlobalPath::Line(_) => vec![],
};

let points = curve.path().approx(range, tolerance);
cache.insert_global_curve(curve, GlobalCurveApprox { points })
}
}

/// Approximate a circle
///
/// `tolerance` specifies how much the approximation is allowed to deviate
/// from the circle.
fn approx_circle(
circle: &Circle<3>,
range: impl Into<RangeOnCurve>,
tolerance: Tolerance,
) -> Vec<ApproxPoint<1>> {
let mut points = Vec::new();

let radius = circle.a().magnitude();
let range = range.into();

// To approximate the circle, we use a regular polygon for which
// the circle is the circumscribed circle. The `tolerance`
// parameter is the maximum allowed distance between the polygon
// and the circle. This is the same as the difference between
// the circumscribed circle and the incircle.

let n = number_of_vertices_for_circle(tolerance, radius, range.length());

for i in 1..n {
let angle = range.start().position().t
+ (Scalar::TAU / n as f64 * i as f64) * range.direction();

let point_curve = Point::from([angle]);
let point_global = circle.point_from_circle_coords(point_curve);

points.push(ApproxPoint::new(point_curve, point_global));
}

if range.is_reversed() {
points.reverse();
}

points
}

fn number_of_vertices_for_circle(
tolerance: Tolerance,
radius: Scalar,
range: Scalar,
) -> u64 {
let n = (range / (Scalar::ONE - (tolerance.inner() / radius)).acos() / 2.)
.ceil()
.into_u64();

max(n, 3)
}

/// The range on which a curve should be approximated
#[derive(Clone, Copy, Debug)]
pub struct RangeOnCurve {
boundary: [Vertex; 2],
is_reversed: bool,
}

impl RangeOnCurve {
/// Construct an instance of `RangeOnCurve`
///
/// Ranges are normalized on construction, meaning that the order of
/// vertices passed to this constructor does not influence the range that is
/// constructed.
///
/// This is done to prevent bugs during mesh construction: The curve
/// approximation code is regularly faced with ranges that are reversed
/// versions of each other. This can lead to slightly different
/// approximations, which in turn leads to the aforementioned invalid
/// meshes.
///
/// The caller can use `is_reversed` to determine, if the range was reversed
/// during normalization, to adjust the approximation accordingly.
pub fn new([a, b]: [Vertex; 2]) -> Self {
let (boundary, is_reversed) = if a < b {
([a, b], false)
} else {
([b, a], true)
};

Self {
boundary,
is_reversed,
}
}

/// Indicate whether the range was reversed during normalization
pub fn is_reversed(&self) -> bool {
self.is_reversed
}

/// Access the start of the range
pub fn start(&self) -> Vertex {
self.boundary[0]
}

/// Access the end of the range
pub fn end(&self) -> Vertex {
self.boundary[1]
}

/// Compute the signed length of the range
pub fn signed_length(&self) -> Scalar {
(self.end().position() - self.start().position()).t
}

/// Compute the absolute length of the range
pub fn length(&self) -> Scalar {
self.signed_length().abs()
}

/// Compute the direction of the range
///
/// Returns a [`Scalar`] that is zero or +/- one.
pub fn direction(&self) -> Scalar {
self.signed_length().sign()
}
}

/// An approximation of a [`Curve`]
#[derive(Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct CurveApprox {
/// The points that approximate the curve
pub points: Vec<ApproxPoint<2>>,
}

impl CurveApprox {
/// Create an empty instance of `CurveApprox`
pub fn empty() -> Self {
Self { points: Vec::new() }
}

/// Add points to the approximation
pub fn with_points(
mut self,
points: impl IntoIterator<Item = ApproxPoint<2>>,
) -> Self {
self.points.extend(points);
self
}
}

/// An approximation of a [`GlobalCurve`]
#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct GlobalCurveApprox {
/// The points that approximate the curve
pub points: Vec<ApproxPoint<1>>,
}

#[cfg(test)]
mod tests {
use fj_math::Scalar;

use crate::algorithms::approx::Tolerance;

#[test]
fn number_of_vertices_for_circle() {
verify_result(50., 100., Scalar::TAU, 3);
verify_result(50., 100., Scalar::PI, 3);
verify_result(10., 100., Scalar::TAU, 7);
verify_result(10., 100., Scalar::PI, 4);
verify_result(1., 100., Scalar::TAU, 23);
verify_result(1., 100., Scalar::PI, 12);

fn verify_result(
tolerance: impl Into<Tolerance>,
radius: impl Into<Scalar>,
range: impl Into<Scalar>,
n: u64,
) {
let tolerance = tolerance.into();
let radius = radius.into();
let range = range.into();

assert_eq!(
n,
super::number_of_vertices_for_circle(tolerance, radius, range)
);

assert!(calculate_error(radius, range, n) <= tolerance.inner());
if n > 3 {
assert!(
calculate_error(radius, range, n - 1) >= tolerance.inner()
);
}
}

fn calculate_error(radius: Scalar, range: Scalar, n: u64) -> Scalar {
radius - radius * (range / Scalar::from_u64(n) / 2.).cos()
}
}
}
5 changes: 4 additions & 1 deletion crates/fj-kernel/src/algorithms/approx/cycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ impl Approx for &Cycle {

fn approx_with_cache(
self,
tolerance: Tolerance,
tolerance: impl Into<Tolerance>,
cache: &mut ApproxCache,
) -> Self::Approximation {
let tolerance = tolerance.into();

let half_edges = self
.half_edges()
.map(|half_edge| half_edge.approx_with_cache(tolerance, cache))
.collect();

CycleApprox { half_edges }
}
}
Expand Down
11 changes: 4 additions & 7 deletions crates/fj-kernel/src/algorithms/approx/edge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,20 @@
//! approximations are usually used to build cycle approximations, and this way,
//! the caller doesn't have to call with duplicate vertices.

use crate::objects::HalfEdge;
use crate::{objects::HalfEdge, path::RangeOnPath};

use super::{
curve::{CurveApprox, RangeOnCurve},
Approx, ApproxCache, ApproxPoint, Tolerance,
};
use super::{curve::CurveApprox, Approx, ApproxCache, ApproxPoint, Tolerance};

impl Approx for &HalfEdge {
type Approximation = HalfEdgeApprox;

fn approx_with_cache(
self,
tolerance: Tolerance,
tolerance: impl Into<Tolerance>,
cache: &mut ApproxCache,
) -> Self::Approximation {
let &[a, b] = self.vertices();
let range = RangeOnCurve::new([a, b]);
let range = RangeOnPath::new([a, b].map(|vertex| vertex.position()));

let first = ApproxPoint::new(
a.surface_form().position(),
Expand Down
8 changes: 6 additions & 2 deletions crates/fj-kernel/src/algorithms/approx/face.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ impl Approx for &Faces {

fn approx_with_cache(
self,
tolerance: Tolerance,
tolerance: impl Into<Tolerance>,
cache: &mut ApproxCache,
) -> Self::Approximation {
let tolerance = tolerance.into();

let approx = self
.into_iter()
.map(|face| face.approx_with_cache(tolerance, cache))
Expand Down Expand Up @@ -67,9 +69,11 @@ impl Approx for &Face {

fn approx_with_cache(
self,
tolerance: Tolerance,
tolerance: impl Into<Tolerance>,
cache: &mut ApproxCache,
) -> Self::Approximation {
let tolerance = tolerance.into();

// Curved faces whose curvature is not fully defined by their edges
// are not supported yet. For that reason, we can fully ignore `face`'s
// `surface` field and just pass the edges to `Self::for_edges`.
Expand Down
4 changes: 2 additions & 2 deletions crates/fj-kernel/src/algorithms/approx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ pub trait Approx: Sized {
///
/// `tolerance` defines how far the approximation is allowed to deviate from
/// the actual object.
fn approx(self, tolerance: Tolerance) -> Self::Approximation {
fn approx(self, tolerance: impl Into<Tolerance>) -> Self::Approximation {
let mut cache = ApproxCache::new();
self.approx_with_cache(tolerance, &mut cache)
}

/// Approximate the object, using the provided cache
fn approx_with_cache(
self,
tolerance: Tolerance,
tolerance: impl Into<Tolerance>,
cache: &mut ApproxCache,
) -> Self::Approximation;
}
Expand Down
2 changes: 1 addition & 1 deletion crates/fj-kernel/src/algorithms/approx/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ impl Approx for &Shell {

fn approx_with_cache(
self,
tolerance: Tolerance,
tolerance: impl Into<Tolerance>,
cache: &mut ApproxCache,
) -> Self::Approximation {
self.faces().approx_with_cache(tolerance, cache)
Expand Down
2 changes: 1 addition & 1 deletion crates/fj-kernel/src/algorithms/approx/sketch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ impl Approx for &Sketch {

fn approx_with_cache(
self,
tolerance: Tolerance,
tolerance: impl Into<Tolerance>,
cache: &mut ApproxCache,
) -> Self::Approximation {
self.faces().approx_with_cache(tolerance, cache)
Expand Down
4 changes: 3 additions & 1 deletion crates/fj-kernel/src/algorithms/approx/solid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ impl Approx for &Solid {

fn approx_with_cache(
self,
tolerance: Tolerance,
tolerance: impl Into<Tolerance>,
cache: &mut ApproxCache,
) -> Self::Approximation {
let tolerance = tolerance.into();

self.shells()
.flat_map(|shell| shell.approx_with_cache(tolerance, cache))
.collect()
Expand Down
3 changes: 2 additions & 1 deletion crates/fj-kernel/src/objects/surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ pub struct Surface {

impl Surface {
/// Construct a `Surface` from two paths that define its coordinate system
pub fn new(u: GlobalPath, v: Vector<3>) -> Self {
pub fn new(u: GlobalPath, v: impl Into<Vector<3>>) -> Self {
let v = v.into();
Self { u, v }
}

Expand Down
Loading