Reworked Segment types into their cartesian forms (#17404)

# Objective

Segment2d and Segment3d are currently hard to work with because unlike
many other primary shapes, they are bound to the origin.
The objective of this PR is to allow these segments to exist anywhere in
cartesian space, making them much more useful in a variety of contexts.

## Solution

Reworking the existing segment type's internal fields and methods to
allow them to exist anywhere in cartesian space.
I have done both reworks for 2d and 3d segments but I was unsure if I
should just have it all here or not so feel free to tell me how I should
proceed, for now I have only pushed Segment2d changes.

As I am not a very seasoned contributor, this first implementation is
very likely sloppy and will need some additional work from my end, I am
open to all criticisms and willing to work to get this to bevy's
standards.

## Testing

I am not very familiar with the standards of testing. Of course my
changes had to pass the thorough existing tests for primitive shapes.
I also checked the gizmo 2d shapes intersection example and everything
looked fine.

I did add a few utility methods to the types that have no tests yet. I
am willing to implement some if it is deemed necessary

## Migration Guide

The segment type constructors changed so if someone previously created a
Segment2d with a direction and length they would now need to use the
`from_direction` constructor

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Joona Aalto <jondolf.dev@gmail.com>
This commit is contained in:
Sigma-dev 2025-01-19 04:54:45 +01:00 committed by GitHub
parent 21f1e3045c
commit 7c8da1c05d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 200 additions and 89 deletions

View File

@ -540,10 +540,7 @@ where
} }
// draw normal of the plane (orthogonal to the plane itself) // draw normal of the plane (orthogonal to the plane itself)
let normal = primitive.normal; let normal = primitive.normal;
let normal_segment = Segment2d { let normal_segment = Segment2d::from_direction_and_length(normal, HALF_MIN_LINE_LEN * 2.);
direction: normal,
half_length: HALF_MIN_LINE_LEN,
};
self.primitive_2d( self.primitive_2d(
&normal_segment, &normal_segment,
// offset the normal so it starts on the plane line // offset the normal so it starts on the plane line
@ -577,8 +574,8 @@ where
{ {
gizmos: &'a mut GizmoBuffer<Config, Clear>, gizmos: &'a mut GizmoBuffer<Config, Clear>,
direction: Dir2, // Direction of the line segment point1: Vec2, // First point of the segment
half_length: f32, // Half-length of the line segment point2: Vec2, // Second point of the segment
isometry: Isometry2d, // isometric transformation of the line segment isometry: Isometry2d, // isometric transformation of the line segment
color: Color, // color of the line segment color: Color, // color of the line segment
@ -616,8 +613,8 @@ where
) -> Self::Output<'_> { ) -> Self::Output<'_> {
Segment2dBuilder { Segment2dBuilder {
gizmos: self, gizmos: self,
direction: primitive.direction, point1: primitive.point1(),
half_length: primitive.half_length, point2: primitive.point2(),
isometry: isometry.into(), isometry: isometry.into(),
color: color.into(), color: color.into(),
@ -637,14 +634,16 @@ where
return; return;
} }
let direction = self.direction * self.half_length; let segment = Segment2d::new(self.point1, self.point2)
let start = self.isometry * (-direction); .rotated(self.isometry.rotation)
let end = self.isometry * direction; .translated(self.isometry.translation);
if self.draw_arrow { if self.draw_arrow {
self.gizmos.arrow_2d(start, end, self.color); self.gizmos
.arrow_2d(segment.point1(), segment.point2(), self.color);
} else { } else {
self.gizmos.line_2d(start, end, self.color); self.gizmos
.line_2d(segment.point1(), segment.point2(), self.color);
} }
} }
} }

View File

@ -228,9 +228,11 @@ where
return; return;
} }
let isometry = isometry.into(); let isometry: Isometry3d = isometry.into();
let direction = primitive.direction.as_vec3(); let transformed = primitive
self.line(isometry * direction, isometry * (-direction), color); .rotated(isometry.rotation)
.translated(isometry.translation.into());
self.line(transformed.point1(), transformed.point2(), color);
} }
} }

View File

@ -1,6 +1,7 @@
//! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives). //! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives).
use crate::{ use crate::{
bounding::BoundingVolume,
ops, ops,
primitives::{ primitives::{
Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d,
@ -265,18 +266,15 @@ impl Bounded2d for Line2d {
impl Bounded2d for Segment2d { impl Bounded2d for Segment2d {
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d { fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
let isometry = isometry.into(); Aabb2d::from_point_cloud(isometry, &[self.point1(), self.point2()])
// Rotate the segment by `rotation`
let direction = isometry.rotation * *self.direction;
let half_size = (self.half_length * direction).abs();
Aabb2d::new(isometry.translation, half_size)
} }
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle { fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
let isometry = isometry.into(); let isometry: Isometry2d = isometry.into();
BoundingCircle::new(isometry.translation, self.half_length) let local_center = self.center();
let radius = local_center.distance(self.point1());
let local_circle = BoundingCircle::new(local_center, radius);
local_circle.transformed_by(isometry.translation, isometry.rotation)
} }
} }
@ -336,8 +334,8 @@ impl Bounded2d for Triangle2d {
if let Some((point1, point2)) = side_opposite_to_non_acute { if let Some((point1, point2)) = side_opposite_to_non_acute {
// The triangle is obtuse or right, so the minimum bounding circle's diameter is equal to the longest side. // The triangle is obtuse or right, so the minimum bounding circle's diameter is equal to the longest side.
// We can compute the minimum bounding circle from the line segment of the longest side. // We can compute the minimum bounding circle from the line segment of the longest side.
let (segment, center) = Segment2d::from_points(point1, point2); let segment = Segment2d::new(point1, point2);
segment.bounding_circle(isometry * Isometry2d::from_translation(center)) segment.bounding_circle(isometry)
} else { } else {
// The triangle is acute, so the smallest bounding circle is the circumcircle. // The triangle is acute, so the smallest bounding circle is the circumcircle.
let (Circle { radius }, circumcenter) = self.circumcircle(); let (Circle { radius }, circumcenter) = self.circumcircle();
@ -417,11 +415,10 @@ impl Bounded2d for Capsule2d {
let isometry = isometry.into(); let isometry = isometry.into();
// Get the line segment between the semicircles of the rotated capsule // Get the line segment between the semicircles of the rotated capsule
let segment = Segment2d { let segment = Segment2d::from_direction_and_length(
// Multiplying a normalized vector (Vec2::Y) with a rotation returns a normalized vector. isometry.rotation * Dir2::Y,
direction: isometry.rotation * Dir2::Y, self.half_length * 2.,
half_length: self.half_length, );
};
let (a, b) = (segment.point1(), segment.point2()); let (a, b) = (segment.point1(), segment.point2());
// Expand the line segment by the capsule radius to get the capsule half-extents // Expand the line segment by the capsule radius to get the capsule half-extents
@ -886,7 +883,7 @@ mod tests {
fn segment() { fn segment() {
let translation = Vec2::new(2.0, 1.0); let translation = Vec2::new(2.0, 1.0);
let isometry = Isometry2d::from_translation(translation); let isometry = Isometry2d::from_translation(translation);
let segment = Segment2d::from_points(Vec2::new(-1.0, -0.5), Vec2::new(1.0, 0.5)).0; let segment = Segment2d::new(Vec2::new(-1.0, -0.5), Vec2::new(1.0, 0.5));
let aabb = segment.aabb_2d(isometry); let aabb = segment.aabb_2d(isometry);
assert_eq!(aabb.min, Vec2::new(1.0, 0.5)); assert_eq!(aabb.min, Vec2::new(1.0, 0.5));

View File

@ -348,7 +348,10 @@ mod tests {
#[test] #[test]
fn segment() { fn segment() {
let extrusion = Extrusion::new(Segment2d::new(Dir2::new_unchecked(Vec2::NEG_Y), 3.), 4.0); let extrusion = Extrusion::new(
Segment2d::from_direction_and_length(Dir2::new_unchecked(Vec2::NEG_Y), 3.),
4.0,
);
let translation = Vec3::new(3., 4., 5.); let translation = Vec3::new(3., 4., 5.);
let rotation = Quat::from_rotation_x(FRAC_PI_4); let rotation = Quat::from_rotation_x(FRAC_PI_4);
let isometry = Isometry3d::new(translation, rotation); let isometry = Isometry3d::new(translation, rotation);

View File

@ -1,7 +1,7 @@
//! Contains [`Bounded3d`] implementations for [geometric primitives](crate::primitives). //! Contains [`Bounded3d`] implementations for [geometric primitives](crate::primitives).
use crate::{ use crate::{
bounding::{Bounded2d, BoundingCircle}, bounding::{Bounded2d, BoundingCircle, BoundingVolume},
ops, ops,
primitives::{ primitives::{
Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d,
@ -76,18 +76,13 @@ impl Bounded3d for Line3d {
impl Bounded3d for Segment3d { impl Bounded3d for Segment3d {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d { fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into(); Aabb3d::from_point_cloud(isometry, [self.point1(), self.point2()].iter().copied())
// Rotate the segment by `rotation`
let direction = isometry.rotation * *self.direction;
let half_size = (self.half_length * direction).abs();
Aabb3d::new(isometry.translation, half_size)
} }
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere { fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
let isometry = isometry.into(); let isometry = isometry.into();
BoundingSphere::new(isometry.translation, self.half_length) let local_sphere = BoundingSphere::new(self.center(), self.length() / 2.);
local_sphere.transformed_by(isometry.translation, isometry.rotation)
} }
} }
@ -464,8 +459,7 @@ mod tests {
fn segment() { fn segment() {
let translation = Vec3::new(2.0, 1.0, 0.0); let translation = Vec3::new(2.0, 1.0, 0.0);
let segment = let segment = Segment3d::new(Vec3::new(-1.0, -0.5, 0.0), Vec3::new(1.0, 0.5, 0.0));
Segment3d::from_points(Vec3::new(-1.0, -0.5, 0.0), Vec3::new(1.0, 0.5, 0.0)).0;
let aabb = segment.aabb_3d(translation); let aabb = segment.aabb_3d(translation);
assert_eq!(aabb.min, Vec3A::new(1.0, 0.5, 0.0)); assert_eq!(aabb.min, Vec3A::new(1.0, 0.5, 0.0));

View File

@ -5,7 +5,7 @@ use thiserror::Error;
use super::{Measured2d, Primitive2d, WindingOrder}; use super::{Measured2d, Primitive2d, WindingOrder};
use crate::{ use crate::{
ops::{self, FloatPow}, ops::{self, FloatPow},
Dir2, Vec2, Dir2, Rot2, Vec2,
}; };
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
@ -1221,21 +1221,17 @@ impl Primitive2d for Line2d {}
)] )]
#[doc(alias = "LineSegment2d")] #[doc(alias = "LineSegment2d")]
pub struct Segment2d { pub struct Segment2d {
/// The direction of the line segment /// The endpoints of the line segment.
pub direction: Dir2, pub vertices: [Vec2; 2],
/// Half the length of the line segment. The segment extends by this amount in both
/// the given direction and its opposite direction
pub half_length: f32,
} }
impl Primitive2d for Segment2d {} impl Primitive2d for Segment2d {}
impl Segment2d { impl Segment2d {
/// Create a new `Segment2d` from a direction and full length of the segment /// Create a new `Segment2d` from its endpoints
#[inline(always)] #[inline(always)]
pub fn new(direction: Dir2, length: f32) -> Self { pub const fn new(point1: Vec2, point2: Vec2) -> Self {
Self { Self {
direction, vertices: [point1, point2],
half_length: length / 2.0,
} }
} }
@ -1245,27 +1241,85 @@ impl Segment2d {
/// ///
/// Panics if `point1 == point2` /// Panics if `point1 == point2`
#[inline(always)] #[inline(always)]
#[deprecated(since = "0.16.0", note = "Use the `new` constructor instead")]
pub fn from_points(point1: Vec2, point2: Vec2) -> (Self, Vec2) { pub fn from_points(point1: Vec2, point2: Vec2) -> (Self, Vec2) {
let diff = point2 - point1; (Self::new(point1, point2), (point1 + point2) / 2.)
let length = diff.length(); }
( /// Create a new `Segment2d` at the origin from a `direction` and `length`
// We are dividing by the length here, so the vector is normalized. #[inline(always)]
Self::new(Dir2::new_unchecked(diff / length), length), pub fn from_direction_and_length(direction: Dir2, length: f32) -> Segment2d {
(point1 + point2) / 2., let half_length = length / 2.;
) Self::new(direction * -half_length, direction * half_length)
} }
/// Get the position of the first point on the line segment /// Get the position of the first point on the line segment
#[inline(always)] #[inline(always)]
pub fn point1(&self) -> Vec2 { pub fn point1(&self) -> Vec2 {
*self.direction * -self.half_length self.vertices[0]
} }
/// Get the position of the second point on the line segment /// Get the position of the second point on the line segment
#[inline(always)] #[inline(always)]
pub fn point2(&self) -> Vec2 { pub fn point2(&self) -> Vec2 {
*self.direction * self.half_length self.vertices[1]
}
/// Get the segment's center
#[inline(always)]
#[doc(alias = "midpoint")]
pub fn center(&self) -> Vec2 {
(self.point1() + self.point2()) / 2.
}
/// Get the segment's length
#[inline(always)]
pub fn length(&self) -> f32 {
self.point1().distance(self.point2())
}
/// Get the segment translated by the given vector
#[inline(always)]
pub fn translated(&self, translation: Vec2) -> Segment2d {
Self::new(self.point1() + translation, self.point2() + translation)
}
/// Compute a new segment, based on the original segment rotated around the origin
#[inline(always)]
pub fn rotated(&self, rotation: Rot2) -> Segment2d {
Segment2d::new(rotation * self.point1(), rotation * self.point2())
}
/// Compute a new segment, based on the original segment rotated around a given point
#[inline(always)]
pub fn rotated_around(&self, rotation: Rot2, point: Vec2) -> Segment2d {
// We offset our segment so that our segment is rotated as if from the origin, then we can apply the offset back
let offset = self.translated(-point);
let rotated = offset.rotated(rotation);
rotated.translated(point)
}
/// Compute a new segment, based on the original segment rotated around its center
#[inline(always)]
pub fn rotated_around_center(&self, rotation: Rot2) -> Segment2d {
self.rotated_around(rotation, self.center())
}
/// Get the segment with its center at the origin
#[inline(always)]
pub fn centered(&self) -> Segment2d {
let center = self.center();
self.translated(-center)
}
/// Get the segment with a new length
#[inline(always)]
pub fn resized(&self, length: f32) -> Segment2d {
let offset_from_origin = self.center();
let centered = self.centered();
let ratio = length / self.length();
let segment = Segment2d::new(centered.point1() * ratio, centered.point2() * ratio);
segment.translated(offset_from_origin)
} }
} }

View File

@ -359,22 +359,25 @@ impl Primitive3d for Line3d {}
reflect(Serialize, Deserialize) reflect(Serialize, Deserialize)
)] )]
pub struct Segment3d { pub struct Segment3d {
/// The direction of the line /// The endpoints of the line segment.
pub direction: Dir3, pub vertices: [Vec3; 2],
/// Half the length of the line segment. The segment extends by this amount in both
/// the given direction and its opposite direction
pub half_length: f32,
} }
impl Primitive3d for Segment3d {} impl Primitive3d for Segment3d {}
impl Segment3d { impl Segment3d {
/// Create a new `Segment3d` from its endpoints
#[inline(always)]
pub const fn new(point1: Vec3, point2: Vec3) -> Self {
Self {
vertices: [point1, point2],
}
}
/// Create a new `Segment3d` from a direction and full length of the segment /// Create a new `Segment3d` from a direction and full length of the segment
#[inline(always)] #[inline(always)]
pub fn new(direction: Dir3, length: f32) -> Self { pub fn from_direction_and_length(direction: Dir3, length: f32) -> Self {
Self { let half_length = length / 2.;
direction, Self::new(direction * -half_length, direction * half_length)
half_length: length / 2.0,
}
} }
/// Create a new `Segment3d` from its endpoints and compute its geometric center /// Create a new `Segment3d` from its endpoints and compute its geometric center
@ -383,27 +386,81 @@ impl Segment3d {
/// ///
/// Panics if `point1 == point2` /// Panics if `point1 == point2`
#[inline(always)] #[inline(always)]
#[deprecated(since = "0.16.0", note = "Use the `new` constructor instead")]
pub fn from_points(point1: Vec3, point2: Vec3) -> (Self, Vec3) { pub fn from_points(point1: Vec3, point2: Vec3) -> (Self, Vec3) {
let diff = point2 - point1; (Self::new(point1, point2), (point1 + point2) / 2.)
let length = diff.length();
(
// We are dividing by the length here, so the vector is normalized.
Self::new(Dir3::new_unchecked(diff / length), length),
(point1 + point2) / 2.,
)
} }
/// Get the position of the first point on the line segment /// Get the position of the first point on the line segment
#[inline(always)] #[inline(always)]
pub fn point1(&self) -> Vec3 { pub fn point1(&self) -> Vec3 {
*self.direction * -self.half_length self.vertices[0]
} }
/// Get the position of the second point on the line segment /// Get the position of the second point on the line segment
#[inline(always)] #[inline(always)]
pub fn point2(&self) -> Vec3 { pub fn point2(&self) -> Vec3 {
*self.direction * self.half_length self.vertices[1]
}
/// Get the center of the segment
#[inline(always)]
#[doc(alias = "midpoint")]
pub fn center(&self) -> Vec3 {
(self.point1() + self.point2()) / 2.
}
/// Get the length of the segment
#[inline(always)]
pub fn length(&self) -> f32 {
self.point1().distance(self.point2())
}
/// Get the segment translated by a vector
#[inline(always)]
pub fn translated(&self, translation: Vec3) -> Segment3d {
Self::new(self.point1() + translation, self.point2() + translation)
}
/// Compute a new segment, based on the original segment rotated around the origin
#[inline(always)]
pub fn rotated(&self, rotation: Quat) -> Segment3d {
Segment3d::new(
rotation.mul_vec3(self.point1()),
rotation.mul_vec3(self.point2()),
)
}
/// Compute a new segment, based on the original segment rotated around a given point
#[inline(always)]
pub fn rotated_around(&self, rotation: Quat, point: Vec3) -> Segment3d {
// We offset our segment so that our segment is rotated as if from the origin, then we can apply the offset back
let offset = self.translated(-point);
let rotated = offset.rotated(rotation);
rotated.translated(point)
}
/// Compute a new segment, based on the original segment rotated around its center
#[inline(always)]
pub fn rotated_around_center(&self, rotation: Quat) -> Segment3d {
self.rotated_around(rotation, self.center())
}
/// Get the segment offset so that it's center is at the origin
#[inline(always)]
pub fn centered(&self) -> Segment3d {
let center = self.center();
self.translated(-center)
}
/// Get the segment with a new length
#[inline(always)]
pub fn resized(&self, length: f32) -> Segment3d {
let offset_from_origin = self.center();
let centered = self.centered();
let ratio = length / self.length();
let segment = Segment3d::new(centered.point1() * ratio, centered.point2() * ratio);
segment.translated(offset_from_origin)
} }
} }

View File

@ -230,7 +230,10 @@ fn setup(mut commands: Commands) {
commands.spawn(( commands.spawn((
Transform::from_xyz(-OFFSET_X, -OFFSET_Y, 0.), Transform::from_xyz(-OFFSET_X, -OFFSET_Y, 0.),
Shape::Line(Segment2d::new(Dir2::from_xy(1., 0.3).unwrap(), 90.)), Shape::Line(Segment2d::from_direction_and_length(
Dir2::from_xy(1., 0.3).unwrap(),
90.,
)),
Spin, Spin,
DesiredVolume::Circle, DesiredVolume::Circle,
Intersects::default(), Intersects::default(),

View File

@ -187,12 +187,14 @@ const LINE2D: Line2d = Line2d { direction: Dir2::X };
const LINE3D: Line3d = Line3d { direction: Dir3::X }; const LINE3D: Line3d = Line3d { direction: Dir3::X };
const SEGMENT_2D: Segment2d = Segment2d { const SEGMENT_2D: Segment2d = Segment2d {
direction: Dir2::X, vertices: [Vec2::new(-BIG_2D / 2., 0.), Vec2::new(BIG_2D / 2., 0.)],
half_length: BIG_2D,
}; };
const SEGMENT_3D: Segment3d = Segment3d { const SEGMENT_3D: Segment3d = Segment3d {
direction: Dir3::X, vertices: [
half_length: BIG_3D, Vec3::new(-BIG_3D / 2., 0., 0.),
Vec3::new(BIG_3D / 2., 0., 0.),
],
}; };
const POLYLINE_2D: Polyline2d<4> = Polyline2d { const POLYLINE_2D: Polyline2d<4> = Polyline2d {