diff --git a/crates/fj-core/src/algorithms/approx/curve.rs b/crates/fj-core/src/algorithms/approx/curve.rs index 444e675d8..b5741923b 100644 --- a/crates/fj-core/src/algorithms/approx/curve.rs +++ b/crates/fj-core/src/algorithms/approx/curve.rs @@ -1,12 +1,9 @@ use std::collections::BTreeMap; -use fj_math::{Circle, Point}; +use fj_math::{Circle, Line, Point}; use crate::{ - geometry::{ - curves::line::Line, CurveBoundary, Geometry, Path, SurfaceGeom, - Tolerance, - }, + geometry::{CurveBoundary, Geometry, Path, SurfaceGeom, Tolerance}, storage::Handle, topology::{Curve, Surface}, }; diff --git a/crates/fj-core/src/algorithms/approx/line.rs b/crates/fj-core/src/algorithms/approx/line.rs index 628eeb7d0..e0d2e1a68 100644 --- a/crates/fj-core/src/algorithms/approx/line.rs +++ b/crates/fj-core/src/algorithms/approx/line.rs @@ -1,6 +1,4 @@ -use fj_math::Point; - -use crate::geometry::curves::line::Line; +use fj_math::{Line, Point}; /// Approximate a line /// diff --git a/crates/fj-core/src/algorithms/intersect/line_segment.rs b/crates/fj-core/src/algorithms/intersect/line_segment.rs index 8d596c4f8..101395c02 100644 --- a/crates/fj-core/src/algorithms/intersect/line_segment.rs +++ b/crates/fj-core/src/algorithms/intersect/line_segment.rs @@ -1,6 +1,4 @@ -use fj_math::{Aabb, LineSegment, Point, Scalar, Vector}; - -use crate::geometry::curves::line::Line; +use fj_math::{Aabb, Line, LineSegment, Point, Scalar, Vector}; /// An intersection between a [`Line`] and a [`LineSegment`] #[derive(Debug, Eq, PartialEq)] @@ -68,9 +66,7 @@ impl LineSegmentIntersection { #[cfg(test)] mod tests { - use fj_math::{LineSegment, Point, Scalar, Vector}; - - use crate::geometry::curves::line::Line; + use fj_math::{Line, LineSegment, Point, Scalar, Vector}; use super::LineSegmentIntersection; diff --git a/crates/fj-core/src/geometry/curves/line.rs b/crates/fj-core/src/geometry/curves/line.rs index 31ec95123..c0a4a43af 100644 --- a/crates/fj-core/src/geometry/curves/line.rs +++ b/crates/fj-core/src/geometry/curves/line.rs @@ -1,180 +1,9 @@ //! # Geometry code specific to lines -use fj_math::{LineSegment, Point, Scalar, Transform, Triangle, Vector}; +use fj_math::{Line, LineSegment, Point}; use crate::geometry::{traits::GenPolyline, CurveBoundary, Tolerance}; -/// An n-dimensional line, defined by an origin and a direction -/// -/// The dimensionality of the line is defined by the const generic `D` -/// parameter. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Ord, PartialOrd)] -#[repr(C)] -pub struct Line { - origin: Point, - direction: Vector, -} - -impl Line { - /// Create a line from a point and a vector - /// - /// # Panics - /// - /// Panics, if `direction` has a length of zero. - pub fn from_origin_and_direction( - origin: Point, - direction: Vector, - ) -> Self { - assert!( - direction.magnitude() != Scalar::ZERO, - "Can't construct `Line`. Direction is zero: {direction:?}" - ); - - Self { origin, direction } - } - - /// Create a line from two points - /// - /// Also returns the lines coordinates of the provided points on the new - /// line. - /// - /// # Panics - /// - /// Panics, if the points are coincident. - pub fn from_points( - points: [impl Into>; 2], - ) -> (Self, [Point<1>; 2]) { - let [a, b] = points.map(Into::into); - - let line = Self::from_origin_and_direction(a, b - a); - let coords = [[0.], [1.]].map(Point::from); - - (line, coords) - } - - /// Create a line from two points that include line coordinates - /// - /// # Panics - /// - /// Panics, if the points are coincident. - pub fn from_points_with_line_coords( - points: [(impl Into>, impl Into>); 2], - ) -> Self { - let [(a_line, a_global), (b_line, b_global)] = - points.map(|(point_line, point_global)| { - (point_line.into(), point_global.into()) - }); - - let direction = (b_global - a_global) / (b_line - a_line).t; - let origin = a_global + direction * -a_line.t; - - Self::from_origin_and_direction(origin, direction) - } - - /// Access the origin of the line - /// - /// The origin is a point on the line which, together with the `direction` - /// field, defines the line fully. The origin also defines the origin of the - /// line's 1-dimensional coordinate system. - pub fn origin(&self) -> Point { - self.origin - } - - /// Access the direction of the line - /// - /// The length of this vector defines the unit of the line's curve - /// coordinate system. The coordinate `1.` is always where the direction - /// vector points, from `origin`. - pub fn direction(&self) -> Vector { - self.direction - } - - /// Determine if this line is coincident with another line - /// - /// # Implementation Note - /// - /// This method only returns `true`, if the lines are precisely coincident. - /// This will probably not be enough going forward, but it'll do for now. - pub fn is_coincident_with(&self, other: &Self) -> bool { - let other_origin_is_not_on_self = { - let a = other.origin; - let b = self.origin; - let c = self.origin + self.direction; - - // The triangle is valid only, if the three points are not on the - // same line. - Triangle::from_points([a, b, c]).is_valid() - }; - - if other_origin_is_not_on_self { - return false; - } - - let d1 = self.direction.normalize(); - let d2 = other.direction.normalize(); - - d1 == d2 || d1 == -d2 - } - - /// Create a new instance that is reversed - #[must_use] - pub fn reverse(mut self) -> Self { - self.origin += self.direction; - self.direction = -self.direction; - self - } - - /// Convert a `D`-dimensional point to line coordinates - /// - /// Projects the point onto the line before the conversion. This is done to - /// make this method robust against floating point accuracy issues. - /// - /// Callers are advised to be careful about the points they pass, as the - /// point not being on the line, intentional or not, will never result in an - /// error. - pub fn point_to_line_coords(&self, point: impl Into>) -> Point<1> { - Point { - coords: self.vector_to_line_coords(point.into() - self.origin), - } - } - - /// Convert a `D`-dimensional vector to line coordinates - pub fn vector_to_line_coords( - &self, - vector: impl Into>, - ) -> Vector<1> { - let t = vector.into().scalar_projection_onto(&self.direction) - / self.direction.magnitude(); - Vector::from([t]) - } - - /// Convert a point in line coordinates into a `D`-dimensional point - pub fn point_from_line_coords( - &self, - point: impl Into>, - ) -> Point { - self.origin + self.vector_from_line_coords(point.into().coords) - } - - /// Convert a vector in line coordinates into a `D`-dimensional vector - pub fn vector_from_line_coords( - &self, - vector: impl Into>, - ) -> Vector { - self.direction * vector.into().t - } -} - -impl Line<3> { - /// # Transform the line - pub fn transform(&self, transform: &Transform) -> Self { - Self::from_origin_and_direction( - transform.transform_point(&self.origin()), - transform.transform_vector(&self.direction()), - ) - } -} - impl GenPolyline for Line { fn origin(&self) -> Point { self.origin() @@ -197,72 +26,3 @@ impl GenPolyline for Line { boundary.inner.into() } } - -#[cfg(test)] -mod tests { - use approx::assert_abs_diff_eq; - use fj_math::{Point, Scalar, Vector}; - - use super::Line; - - #[test] - fn from_points_with_line_coords() { - let line = Line::from_points_with_line_coords([ - ([0.], [0., 0.]), - ([1.], [1., 0.]), - ]); - assert_eq!(line.origin(), Point::from([0., 0.])); - assert_eq!(line.direction(), Vector::from([1., 0.])); - - let line = Line::from_points_with_line_coords([ - ([1.], [0., 1.]), - ([0.], [1., 1.]), - ]); - assert_eq!(line.origin(), Point::from([1., 1.])); - assert_eq!(line.direction(), Vector::from([-1., 0.])); - - let line = Line::from_points_with_line_coords([ - ([-1.], [0., 2.]), - ([0.], [1., 2.]), - ]); - assert_eq!(line.origin(), Point::from([1., 2.])); - assert_eq!(line.direction(), Vector::from([1., 0.])); - } - - #[test] - fn is_coincident_with() { - let (line, _) = Line::from_points([[0., 0.], [1., 0.]]); - - let (a, _) = Line::from_points([[0., 0.], [1., 0.]]); - let (b, _) = Line::from_points([[0., 0.], [-1., 0.]]); - let (c, _) = Line::from_points([[0., 1.], [1., 1.]]); - - assert!(line.is_coincident_with(&a)); - assert!(line.is_coincident_with(&b)); - assert!(!line.is_coincident_with(&c)); - } - - #[test] - fn convert_point_to_line_coords() { - let line = Line { - origin: Point::from([1., 2., 3.]), - direction: Vector::from([2., 3., 5.]), - }; - - verify(line, -1.); - verify(line, 0.); - verify(line, 1.); - verify(line, 2.); - - fn verify(line: Line<3>, t: f64) { - let point = line.point_from_line_coords([t]); - let t_result = line.point_to_line_coords(point); - - assert_abs_diff_eq!( - Point::from([t]), - t_result, - epsilon = Scalar::from(1e-8) - ); - } - } -} diff --git a/crates/fj-core/src/geometry/path.rs b/crates/fj-core/src/geometry/path.rs index bf6e431d3..5b7bfb021 100644 --- a/crates/fj-core/src/geometry/path.rs +++ b/crates/fj-core/src/geometry/path.rs @@ -2,9 +2,7 @@ //! //! See [`Path`]. -use fj_math::{Circle, Point, Scalar, Transform, Vector}; - -use super::curves::line::Line; +use fj_math::{Circle, Line, Point, Scalar, Transform, Vector}; /// A path through surface (2D) or global (3D) space #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] diff --git a/crates/fj-core/src/geometry/surface.rs b/crates/fj-core/src/geometry/surface.rs index 795443618..d7177d8a4 100644 --- a/crates/fj-core/src/geometry/surface.rs +++ b/crates/fj-core/src/geometry/surface.rs @@ -111,10 +111,10 @@ impl SurfaceGeom { #[cfg(test)] mod tests { - use fj_math::{Point, Vector}; + use fj_math::{Line, Point, Vector}; use pretty_assertions::assert_eq; - use crate::geometry::{curves::line::Line, Path, SurfaceGeom, Tolerance}; + use crate::geometry::{Path, SurfaceGeom, Tolerance}; #[test] fn point_from_surface_coords() { diff --git a/crates/fj-core/src/operations/sweep/path.rs b/crates/fj-core/src/operations/sweep/path.rs index 57035b8fe..b707bd26a 100644 --- a/crates/fj-core/src/operations/sweep/path.rs +++ b/crates/fj-core/src/operations/sweep/path.rs @@ -1,7 +1,7 @@ -use fj_math::{Circle, Vector}; +use fj_math::{Circle, Line, Vector}; use crate::{ - geometry::{curves::line::Line, Path, SurfaceGeom}, + geometry::{Path, SurfaceGeom}, operations::build::BuildSurface, storage::Handle, topology::Surface, diff --git a/crates/fj-math/src/lib.rs b/crates/fj-math/src/lib.rs index e3e07e66f..f5e392c67 100644 --- a/crates/fj-math/src/lib.rs +++ b/crates/fj-math/src/lib.rs @@ -36,6 +36,7 @@ mod arc; mod bivector; mod circle; mod coordinates; +mod line; mod line_segment; mod point; mod poly_chain; @@ -50,6 +51,7 @@ pub use self::{ bivector::Bivector, circle::Circle, coordinates::{Uv, Xyz, T}, + line::Line, line_segment::LineSegment, point::Point, poly_chain::PolyChain, diff --git a/crates/fj-math/src/line.rs b/crates/fj-math/src/line.rs new file mode 100644 index 000000000..024f28841 --- /dev/null +++ b/crates/fj-math/src/line.rs @@ -0,0 +1,242 @@ +use crate::{Point, Scalar, Transform, Triangle, Vector}; + +/// An n-dimensional line, defined by an origin and a direction +/// +/// The dimensionality of the line is defined by the const generic `D` +/// parameter. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Ord, PartialOrd)] +#[repr(C)] +pub struct Line { + origin: Point, + direction: Vector, +} + +impl Line { + /// Create a line from a point and a vector + /// + /// # Panics + /// + /// Panics, if `direction` has a length of zero. + pub fn from_origin_and_direction( + origin: Point, + direction: Vector, + ) -> Self { + assert!( + direction.magnitude() != Scalar::ZERO, + "Can't construct `Line`. Direction is zero: {direction:?}" + ); + + Self { origin, direction } + } + + /// Create a line from two points + /// + /// Also returns the lines coordinates of the provided points on the new + /// line. + /// + /// # Panics + /// + /// Panics, if the points are coincident. + pub fn from_points( + points: [impl Into>; 2], + ) -> (Self, [Point<1>; 2]) { + let [a, b] = points.map(Into::into); + + let line = Self::from_origin_and_direction(a, b - a); + let coords = [[0.], [1.]].map(Point::from); + + (line, coords) + } + + /// Create a line from two points that include line coordinates + /// + /// # Panics + /// + /// Panics, if the points are coincident. + pub fn from_points_with_line_coords( + points: [(impl Into>, impl Into>); 2], + ) -> Self { + let [(a_line, a_global), (b_line, b_global)] = + points.map(|(point_line, point_global)| { + (point_line.into(), point_global.into()) + }); + + let direction = (b_global - a_global) / (b_line - a_line).t; + let origin = a_global + direction * -a_line.t; + + Self::from_origin_and_direction(origin, direction) + } + + /// Access the origin of the line + /// + /// The origin is a point on the line which, together with the `direction` + /// field, defines the line fully. The origin also defines the origin of the + /// line's 1-dimensional coordinate system. + pub fn origin(&self) -> Point { + self.origin + } + + /// Access the direction of the line + /// + /// The length of this vector defines the unit of the line's curve + /// coordinate system. The coordinate `1.` is always where the direction + /// vector points, from `origin`. + pub fn direction(&self) -> Vector { + self.direction + } + + /// Determine if this line is coincident with another line + /// + /// # Implementation Note + /// + /// This method only returns `true`, if the lines are precisely coincident. + /// This will probably not be enough going forward, but it'll do for now. + pub fn is_coincident_with(&self, other: &Self) -> bool { + let other_origin_is_not_on_self = { + let a = other.origin; + let b = self.origin; + let c = self.origin + self.direction; + + // The triangle is valid only, if the three points are not on the + // same line. + Triangle::from_points([a, b, c]).is_valid() + }; + + if other_origin_is_not_on_self { + return false; + } + + let d1 = self.direction.normalize(); + let d2 = other.direction.normalize(); + + d1 == d2 || d1 == -d2 + } + + /// Create a new instance that is reversed + #[must_use] + pub fn reverse(mut self) -> Self { + self.origin += self.direction; + self.direction = -self.direction; + self + } + + /// Convert a `D`-dimensional point to line coordinates + /// + /// Projects the point onto the line before the conversion. This is done to + /// make this method robust against floating point accuracy issues. + /// + /// Callers are advised to be careful about the points they pass, as the + /// point not being on the line, intentional or not, will never result in an + /// error. + pub fn point_to_line_coords(&self, point: impl Into>) -> Point<1> { + Point { + coords: self.vector_to_line_coords(point.into() - self.origin), + } + } + + /// Convert a `D`-dimensional vector to line coordinates + pub fn vector_to_line_coords( + &self, + vector: impl Into>, + ) -> Vector<1> { + let t = vector.into().scalar_projection_onto(&self.direction) + / self.direction.magnitude(); + Vector::from([t]) + } + + /// Convert a point in line coordinates into a `D`-dimensional point + pub fn point_from_line_coords( + &self, + point: impl Into>, + ) -> Point { + self.origin + self.vector_from_line_coords(point.into().coords) + } + + /// Convert a vector in line coordinates into a `D`-dimensional vector + pub fn vector_from_line_coords( + &self, + vector: impl Into>, + ) -> Vector { + self.direction * vector.into().t + } +} + +impl Line<3> { + /// # Transform the line + pub fn transform(&self, transform: &Transform) -> Self { + Self::from_origin_and_direction( + transform.transform_point(&self.origin()), + transform.transform_vector(&self.direction()), + ) + } +} + +#[cfg(test)] +mod tests { + use approx::assert_abs_diff_eq; + + use crate::{Point, Scalar, Vector}; + + use super::Line; + + #[test] + fn from_points_with_line_coords() { + let line = Line::from_points_with_line_coords([ + ([0.], [0., 0.]), + ([1.], [1., 0.]), + ]); + assert_eq!(line.origin(), Point::from([0., 0.])); + assert_eq!(line.direction(), Vector::from([1., 0.])); + + let line = Line::from_points_with_line_coords([ + ([1.], [0., 1.]), + ([0.], [1., 1.]), + ]); + assert_eq!(line.origin(), Point::from([1., 1.])); + assert_eq!(line.direction(), Vector::from([-1., 0.])); + + let line = Line::from_points_with_line_coords([ + ([-1.], [0., 2.]), + ([0.], [1., 2.]), + ]); + assert_eq!(line.origin(), Point::from([1., 2.])); + assert_eq!(line.direction(), Vector::from([1., 0.])); + } + + #[test] + fn is_coincident_with() { + let (line, _) = Line::from_points([[0., 0.], [1., 0.]]); + + let (a, _) = Line::from_points([[0., 0.], [1., 0.]]); + let (b, _) = Line::from_points([[0., 0.], [-1., 0.]]); + let (c, _) = Line::from_points([[0., 1.], [1., 1.]]); + + assert!(line.is_coincident_with(&a)); + assert!(line.is_coincident_with(&b)); + assert!(!line.is_coincident_with(&c)); + } + + #[test] + fn convert_point_to_line_coords() { + let line = Line { + origin: Point::from([1., 2., 3.]), + direction: Vector::from([2., 3., 5.]), + }; + + verify(line, -1.); + verify(line, 0.); + verify(line, 1.); + verify(line, 2.); + + fn verify(line: Line<3>, t: f64) { + let point = line.point_from_line_coords([t]); + let t_result = line.point_to_line_coords(point); + + assert_abs_diff_eq!( + Point::from([t]), + t_result, + epsilon = Scalar::from(1e-8) + ); + } + } +}