mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 12:43:34 +00:00
New circular primitives: Arc2d
, CircularSector
, CircularSegment
(#13482)
# Objective Adopted #11748 ## Solution I've rebased on main to fix the merge conflicts. ~~Not quite ready to merge yet~~ * Clippy is happy and the tests are passing, but... * ~~The new shapes in `examples/2d/2d_shapes.rs` don't look right at all~~ Never mind, looks like radians and degrees just got mixed up at some point? * I have updated one doc comment based on a review in the original PR. --------- Co-authored-by: Alexis "spectria" Horizon <spectria.limina@gmail.com> Co-authored-by: Alexis "spectria" Horizon <118812919+spectria-limina@users.noreply.github.com> Co-authored-by: Joona Aalto <jondolf.dev@gmail.com> Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Ben Harper <ben@tukom.org>
This commit is contained in:
parent
da1e6e63ff
commit
ec01c2dc45
11 changed files with 1485 additions and 16 deletions
11
Cargo.toml
11
Cargo.toml
|
@ -423,6 +423,17 @@ description = "Renders a 2d mesh"
|
||||||
category = "2D Rendering"
|
category = "2D Rendering"
|
||||||
wasm = true
|
wasm = true
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "mesh2d_arcs"
|
||||||
|
path = "examples/2d/mesh2d_arcs.rs"
|
||||||
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
[package.metadata.example.mesh2d_arcs]
|
||||||
|
name = "Arc 2D Meshes"
|
||||||
|
description = "Demonstrates UV-mapping of the circular segment and sector primitives"
|
||||||
|
category = "2D Rendering"
|
||||||
|
wasm = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "mesh2d_manual"
|
name = "mesh2d_manual"
|
||||||
path = "examples/2d/mesh2d_manual.rs"
|
path = "examples/2d/mesh2d_manual.rs"
|
||||||
|
|
|
@ -18,6 +18,7 @@ approx = { version = "0.5", optional = true }
|
||||||
rand = { version = "0.8", features = [
|
rand = { version = "0.8", features = [
|
||||||
"alloc",
|
"alloc",
|
||||||
], default-features = false, optional = true }
|
], default-features = false, optional = true }
|
||||||
|
smallvec = { version = "1.11" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
approx = "0.5"
|
approx = "0.5"
|
||||||
|
@ -26,6 +27,8 @@ rand = "0.8"
|
||||||
rand_chacha = "0.3"
|
rand_chacha = "0.3"
|
||||||
# Enable the approx feature when testing.
|
# Enable the approx feature when testing.
|
||||||
bevy_math = { path = ".", version = "0.14.0-dev", features = ["approx"] }
|
bevy_math = { path = ".", version = "0.14.0-dev", features = ["approx"] }
|
||||||
|
glam = { version = "0.27", features = ["approx"] }
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["rand"]
|
default = ["rand"]
|
||||||
|
|
|
@ -2,11 +2,15 @@
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
primitives::{
|
primitives::{
|
||||||
BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, Ellipse, Line2d, Plane2d, Polygon,
|
Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, CircularSegment,
|
||||||
Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d,
|
Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d,
|
||||||
|
Triangle2d,
|
||||||
},
|
},
|
||||||
Dir2, Mat2, Rotation2d, Vec2,
|
Dir2, Mat2, Rotation2d, Vec2,
|
||||||
};
|
};
|
||||||
|
use std::f32::consts::{FRAC_PI_2, PI, TAU};
|
||||||
|
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use super::{Aabb2d, Bounded2d, BoundingCircle};
|
use super::{Aabb2d, Bounded2d, BoundingCircle};
|
||||||
|
|
||||||
|
@ -24,6 +28,120 @@ impl Bounded2d for Circle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute the axis-aligned bounding points of a rotated arc, used for computing the AABB of arcs and derived shapes.
|
||||||
|
// The return type has room for 7 points so that the CircularSector code can add an additional point.
|
||||||
|
#[inline]
|
||||||
|
fn arc_bounding_points(arc: Arc2d, rotation: impl Into<Rotation2d>) -> SmallVec<[Vec2; 7]> {
|
||||||
|
// Otherwise, the extreme points will always be either the endpoints or the axis-aligned extrema of the arc's circle.
|
||||||
|
// We need to compute which axis-aligned extrema are actually contained within the rotated arc.
|
||||||
|
let mut bounds = SmallVec::<[Vec2; 7]>::new();
|
||||||
|
let rotation = rotation.into();
|
||||||
|
bounds.push(rotation * arc.left_endpoint());
|
||||||
|
bounds.push(rotation * arc.right_endpoint());
|
||||||
|
|
||||||
|
// The half-angles are measured from a starting point of π/2, being the angle of Vec2::Y.
|
||||||
|
// Compute the normalized angles of the endpoints with the rotation taken into account, and then
|
||||||
|
// check if we are looking for an angle that is between or outside them.
|
||||||
|
let left_angle = (FRAC_PI_2 + arc.half_angle + rotation.as_radians()).rem_euclid(TAU);
|
||||||
|
let right_angle = (FRAC_PI_2 - arc.half_angle + rotation.as_radians()).rem_euclid(TAU);
|
||||||
|
let inverted = left_angle < right_angle;
|
||||||
|
for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] {
|
||||||
|
let angle = extremum.to_angle().rem_euclid(TAU);
|
||||||
|
// If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them.
|
||||||
|
// There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis.
|
||||||
|
// But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine.
|
||||||
|
#[allow(clippy::nonminimal_bool)]
|
||||||
|
if !inverted && angle >= right_angle && angle <= left_angle
|
||||||
|
|| inverted && (angle >= right_angle || angle <= left_angle)
|
||||||
|
{
|
||||||
|
bounds.push(extremum * arc.radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bounded2d for Arc2d {
|
||||||
|
fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rotation2d>) -> Aabb2d {
|
||||||
|
// If our arc covers more than a circle, just return the bounding box of the circle.
|
||||||
|
if self.half_angle >= PI {
|
||||||
|
return Circle::new(self.radius).aabb_2d(translation, rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
Aabb2d::from_point_cloud(translation, 0.0, &arc_bounding_points(*self, rotation))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounding_circle(
|
||||||
|
&self,
|
||||||
|
translation: Vec2,
|
||||||
|
rotation: impl Into<Rotation2d>,
|
||||||
|
) -> BoundingCircle {
|
||||||
|
// There are two possibilities for the bounding circle.
|
||||||
|
if self.is_major() {
|
||||||
|
// If the arc is major, then the widest distance between two points is a diameter of the arc's circle;
|
||||||
|
// therefore, that circle is the bounding radius.
|
||||||
|
BoundingCircle::new(translation, self.radius)
|
||||||
|
} else {
|
||||||
|
// Otherwise, the widest distance between two points is the chord,
|
||||||
|
// so a circle of that diameter around the midpoint will contain the entire arc.
|
||||||
|
let center = rotation.into() * self.chord_midpoint();
|
||||||
|
BoundingCircle::new(center + translation, self.half_chord_length())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bounded2d for CircularSector {
|
||||||
|
fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rotation2d>) -> Aabb2d {
|
||||||
|
// If our sector covers more than a circle, just return the bounding box of the circle.
|
||||||
|
if self.half_angle() >= PI {
|
||||||
|
return Circle::new(self.radius()).aabb_2d(translation, rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we use the same logic as for Arc2d, above, just with the circle's center as an additional possibility.
|
||||||
|
let mut bounds = arc_bounding_points(self.arc, rotation);
|
||||||
|
bounds.push(Vec2::ZERO);
|
||||||
|
|
||||||
|
Aabb2d::from_point_cloud(translation, 0.0, &bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounding_circle(
|
||||||
|
&self,
|
||||||
|
translation: Vec2,
|
||||||
|
rotation: impl Into<Rotation2d>,
|
||||||
|
) -> BoundingCircle {
|
||||||
|
if self.arc.is_major() {
|
||||||
|
// If the arc is major, that is, greater than a semicircle,
|
||||||
|
// then bounding circle is just the circle defining the sector.
|
||||||
|
BoundingCircle::new(translation, self.arc.radius)
|
||||||
|
} else {
|
||||||
|
// However, when the arc is minor,
|
||||||
|
// we need our bounding circle to include both endpoints of the arc as well as the circle center.
|
||||||
|
// This means we need the circumcircle of those three points.
|
||||||
|
// The circumcircle will always have a greater curvature than the circle itself, so it will contain
|
||||||
|
// the entire circular sector.
|
||||||
|
Triangle2d::new(
|
||||||
|
Vec2::ZERO,
|
||||||
|
self.arc.left_endpoint(),
|
||||||
|
self.arc.right_endpoint(),
|
||||||
|
)
|
||||||
|
.bounding_circle(translation, rotation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bounded2d for CircularSegment {
|
||||||
|
fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rotation2d>) -> Aabb2d {
|
||||||
|
self.arc.aabb_2d(translation, rotation)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounding_circle(
|
||||||
|
&self,
|
||||||
|
translation: Vec2,
|
||||||
|
rotation: impl Into<Rotation2d>,
|
||||||
|
) -> BoundingCircle {
|
||||||
|
self.arc.bounding_circle(translation, rotation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Bounded2d for Ellipse {
|
impl Bounded2d for Ellipse {
|
||||||
fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rotation2d>) -> Aabb2d {
|
fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rotation2d>) -> Aabb2d {
|
||||||
let rotation: Rotation2d = rotation.into();
|
let rotation: Rotation2d = rotation.into();
|
||||||
|
@ -321,13 +439,16 @@ impl Bounded2d for Capsule2d {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, TAU};
|
||||||
|
|
||||||
|
use approx::assert_abs_diff_eq;
|
||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
bounding::Bounded2d,
|
bounding::Bounded2d,
|
||||||
primitives::{
|
primitives::{
|
||||||
Capsule2d, Circle, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle,
|
Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, Plane2d,
|
||||||
RegularPolygon, Segment2d, Triangle2d,
|
Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d,
|
||||||
},
|
},
|
||||||
Dir2,
|
Dir2,
|
||||||
};
|
};
|
||||||
|
@ -346,6 +467,294 @@ mod tests {
|
||||||
assert_eq!(bounding_circle.radius(), 1.0);
|
assert_eq!(bounding_circle.radius(), 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// Arcs and circular segments have the same bounding shapes so they share test cases.
|
||||||
|
fn arc_and_segment() {
|
||||||
|
struct TestCase {
|
||||||
|
name: &'static str,
|
||||||
|
arc: Arc2d,
|
||||||
|
translation: Vec2,
|
||||||
|
rotation: f32,
|
||||||
|
aabb_min: Vec2,
|
||||||
|
aabb_max: Vec2,
|
||||||
|
bounding_circle_center: Vec2,
|
||||||
|
bounding_circle_radius: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The apothem of an arc covering 1/6th of a circle.
|
||||||
|
let apothem = f32::sqrt(3.0) / 2.0;
|
||||||
|
let tests = [
|
||||||
|
// Test case: a basic minor arc
|
||||||
|
TestCase {
|
||||||
|
name: "1/6th circle untransformed",
|
||||||
|
arc: Arc2d::from_radians(1.0, FRAC_PI_3),
|
||||||
|
translation: Vec2::ZERO,
|
||||||
|
rotation: 0.0,
|
||||||
|
aabb_min: Vec2::new(-0.5, apothem),
|
||||||
|
aabb_max: Vec2::new(0.5, 1.0),
|
||||||
|
bounding_circle_center: Vec2::new(0.0, apothem),
|
||||||
|
bounding_circle_radius: 0.5,
|
||||||
|
},
|
||||||
|
// Test case: a smaller arc, verifying that radius scaling works
|
||||||
|
TestCase {
|
||||||
|
name: "1/6th circle with radius 0.5",
|
||||||
|
arc: Arc2d::from_radians(0.5, FRAC_PI_3),
|
||||||
|
translation: Vec2::ZERO,
|
||||||
|
rotation: 0.0,
|
||||||
|
aabb_min: Vec2::new(-0.25, apothem / 2.0),
|
||||||
|
aabb_max: Vec2::new(0.25, 0.5),
|
||||||
|
bounding_circle_center: Vec2::new(0.0, apothem / 2.0),
|
||||||
|
bounding_circle_radius: 0.25,
|
||||||
|
},
|
||||||
|
// Test case: a larger arc, verifying that radius scaling works
|
||||||
|
TestCase {
|
||||||
|
name: "1/6th circle with radius 2.0",
|
||||||
|
arc: Arc2d::from_radians(2.0, FRAC_PI_3),
|
||||||
|
translation: Vec2::ZERO,
|
||||||
|
rotation: 0.0,
|
||||||
|
aabb_min: Vec2::new(-1.0, 2.0 * apothem),
|
||||||
|
aabb_max: Vec2::new(1.0, 2.0),
|
||||||
|
bounding_circle_center: Vec2::new(0.0, 2.0 * apothem),
|
||||||
|
bounding_circle_radius: 1.0,
|
||||||
|
},
|
||||||
|
// Test case: translation of a minor arc
|
||||||
|
TestCase {
|
||||||
|
name: "1/6th circle translated",
|
||||||
|
arc: Arc2d::from_radians(1.0, FRAC_PI_3),
|
||||||
|
translation: Vec2::new(2.0, 3.0),
|
||||||
|
rotation: 0.0,
|
||||||
|
aabb_min: Vec2::new(1.5, 3.0 + apothem),
|
||||||
|
aabb_max: Vec2::new(2.5, 4.0),
|
||||||
|
bounding_circle_center: Vec2::new(2.0, 3.0 + apothem),
|
||||||
|
bounding_circle_radius: 0.5,
|
||||||
|
},
|
||||||
|
// Test case: rotation of a minor arc
|
||||||
|
TestCase {
|
||||||
|
name: "1/6th circle rotated",
|
||||||
|
arc: Arc2d::from_radians(1.0, FRAC_PI_3),
|
||||||
|
translation: Vec2::ZERO,
|
||||||
|
// Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis.
|
||||||
|
rotation: FRAC_PI_6,
|
||||||
|
aabb_min: Vec2::new(-apothem, 0.5),
|
||||||
|
aabb_max: Vec2::new(0.0, 1.0),
|
||||||
|
// The exact coordinates here are not obvious, but can be computed by constructing
|
||||||
|
// an altitude from the midpoint of the chord to the y-axis and using the right triangle
|
||||||
|
// similarity theorem.
|
||||||
|
bounding_circle_center: Vec2::new(-apothem / 2.0, apothem.powi(2)),
|
||||||
|
bounding_circle_radius: 0.5,
|
||||||
|
},
|
||||||
|
// Test case: handling of axis-aligned extrema
|
||||||
|
TestCase {
|
||||||
|
name: "1/4er circle rotated to be axis-aligned",
|
||||||
|
arc: Arc2d::from_radians(1.0, FRAC_PI_2),
|
||||||
|
translation: Vec2::ZERO,
|
||||||
|
// Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis.
|
||||||
|
rotation: -FRAC_PI_4,
|
||||||
|
aabb_min: Vec2::ZERO,
|
||||||
|
aabb_max: Vec2::splat(1.0),
|
||||||
|
bounding_circle_center: Vec2::splat(0.5),
|
||||||
|
bounding_circle_radius: f32::sqrt(2.0) / 2.0,
|
||||||
|
},
|
||||||
|
// Test case: a basic major arc
|
||||||
|
TestCase {
|
||||||
|
name: "5/6th circle untransformed",
|
||||||
|
arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
|
||||||
|
translation: Vec2::ZERO,
|
||||||
|
rotation: 0.0,
|
||||||
|
aabb_min: Vec2::new(-1.0, -apothem),
|
||||||
|
aabb_max: Vec2::new(1.0, 1.0),
|
||||||
|
bounding_circle_center: Vec2::ZERO,
|
||||||
|
bounding_circle_radius: 1.0,
|
||||||
|
},
|
||||||
|
// Test case: a translated major arc
|
||||||
|
TestCase {
|
||||||
|
name: "5/6th circle translated",
|
||||||
|
arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
|
||||||
|
translation: Vec2::new(2.0, 3.0),
|
||||||
|
rotation: 0.0,
|
||||||
|
aabb_min: Vec2::new(1.0, 3.0 - apothem),
|
||||||
|
aabb_max: Vec2::new(3.0, 4.0),
|
||||||
|
bounding_circle_center: Vec2::new(2.0, 3.0),
|
||||||
|
bounding_circle_radius: 1.0,
|
||||||
|
},
|
||||||
|
// Test case: a rotated major arc, with inverted left/right angles
|
||||||
|
TestCase {
|
||||||
|
name: "5/6th circle rotated",
|
||||||
|
arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
|
||||||
|
translation: Vec2::ZERO,
|
||||||
|
// Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis.
|
||||||
|
rotation: FRAC_PI_6,
|
||||||
|
aabb_min: Vec2::new(-1.0, -1.0),
|
||||||
|
aabb_max: Vec2::new(1.0, 1.0),
|
||||||
|
bounding_circle_center: Vec2::ZERO,
|
||||||
|
bounding_circle_radius: 1.0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for test in tests {
|
||||||
|
println!("subtest case: {}", test.name);
|
||||||
|
let segment: CircularSegment = test.arc.into();
|
||||||
|
|
||||||
|
let arc_aabb = test.arc.aabb_2d(test.translation, test.rotation);
|
||||||
|
assert_abs_diff_eq!(test.aabb_min, arc_aabb.min);
|
||||||
|
assert_abs_diff_eq!(test.aabb_max, arc_aabb.max);
|
||||||
|
let segment_aabb = segment.aabb_2d(test.translation, test.rotation);
|
||||||
|
assert_abs_diff_eq!(test.aabb_min, segment_aabb.min);
|
||||||
|
assert_abs_diff_eq!(test.aabb_max, segment_aabb.max);
|
||||||
|
|
||||||
|
let arc_bounding_circle = test.arc.bounding_circle(test.translation, test.rotation);
|
||||||
|
assert_abs_diff_eq!(test.bounding_circle_center, arc_bounding_circle.center);
|
||||||
|
assert_abs_diff_eq!(test.bounding_circle_radius, arc_bounding_circle.radius());
|
||||||
|
let segment_bounding_circle = segment.bounding_circle(test.translation, test.rotation);
|
||||||
|
assert_abs_diff_eq!(test.bounding_circle_center, segment_bounding_circle.center);
|
||||||
|
assert_abs_diff_eq!(
|
||||||
|
test.bounding_circle_radius,
|
||||||
|
segment_bounding_circle.radius()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn circular_sector() {
|
||||||
|
struct TestCase {
|
||||||
|
name: &'static str,
|
||||||
|
arc: Arc2d,
|
||||||
|
translation: Vec2,
|
||||||
|
rotation: f32,
|
||||||
|
aabb_min: Vec2,
|
||||||
|
aabb_max: Vec2,
|
||||||
|
bounding_circle_center: Vec2,
|
||||||
|
bounding_circle_radius: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The apothem of an arc covering 1/6th of a circle.
|
||||||
|
let apothem = f32::sqrt(3.0) / 2.0;
|
||||||
|
let inv_sqrt_3 = f32::sqrt(3.0).recip();
|
||||||
|
let tests = [
|
||||||
|
// Test case: An sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center
|
||||||
|
TestCase {
|
||||||
|
name: "1/3rd circle",
|
||||||
|
arc: Arc2d::from_radians(1.0, TAU / 3.0),
|
||||||
|
translation: Vec2::ZERO,
|
||||||
|
rotation: 0.0,
|
||||||
|
aabb_min: Vec2::new(-apothem, 0.0),
|
||||||
|
aabb_max: Vec2::new(apothem, 1.0),
|
||||||
|
bounding_circle_center: Vec2::new(0.0, 0.5),
|
||||||
|
bounding_circle_radius: apothem,
|
||||||
|
},
|
||||||
|
// The remaining test cases are selected as for arc_and_segment.
|
||||||
|
TestCase {
|
||||||
|
name: "1/6th circle untransformed",
|
||||||
|
arc: Arc2d::from_radians(1.0, FRAC_PI_3),
|
||||||
|
translation: Vec2::ZERO,
|
||||||
|
rotation: 0.0,
|
||||||
|
aabb_min: Vec2::new(-0.5, 0.0),
|
||||||
|
aabb_max: Vec2::new(0.5, 1.0),
|
||||||
|
// The bounding circle is a circumcircle of an equilateral triangle with side length 1.
|
||||||
|
// The distance from the corner to the center of such a triangle is 1/sqrt(3).
|
||||||
|
bounding_circle_center: Vec2::new(0.0, inv_sqrt_3),
|
||||||
|
bounding_circle_radius: inv_sqrt_3,
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "1/6th circle with radius 0.5",
|
||||||
|
arc: Arc2d::from_radians(0.5, FRAC_PI_3),
|
||||||
|
translation: Vec2::ZERO,
|
||||||
|
rotation: 0.0,
|
||||||
|
aabb_min: Vec2::new(-0.25, 0.0),
|
||||||
|
aabb_max: Vec2::new(0.25, 0.5),
|
||||||
|
bounding_circle_center: Vec2::new(0.0, inv_sqrt_3 / 2.0),
|
||||||
|
bounding_circle_radius: inv_sqrt_3 / 2.0,
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "1/6th circle with radius 2.0",
|
||||||
|
arc: Arc2d::from_radians(2.0, FRAC_PI_3),
|
||||||
|
translation: Vec2::ZERO,
|
||||||
|
rotation: 0.0,
|
||||||
|
aabb_min: Vec2::new(-1.0, 0.0),
|
||||||
|
aabb_max: Vec2::new(1.0, 2.0),
|
||||||
|
bounding_circle_center: Vec2::new(0.0, 2.0 * inv_sqrt_3),
|
||||||
|
bounding_circle_radius: 2.0 * inv_sqrt_3,
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "1/6th circle translated",
|
||||||
|
arc: Arc2d::from_radians(1.0, FRAC_PI_3),
|
||||||
|
translation: Vec2::new(2.0, 3.0),
|
||||||
|
rotation: 0.0,
|
||||||
|
aabb_min: Vec2::new(1.5, 3.0),
|
||||||
|
aabb_max: Vec2::new(2.5, 4.0),
|
||||||
|
bounding_circle_center: Vec2::new(2.0, 3.0 + inv_sqrt_3),
|
||||||
|
bounding_circle_radius: inv_sqrt_3,
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "1/6th circle rotated",
|
||||||
|
arc: Arc2d::from_radians(1.0, FRAC_PI_3),
|
||||||
|
translation: Vec2::ZERO,
|
||||||
|
// Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis.
|
||||||
|
rotation: FRAC_PI_6,
|
||||||
|
aabb_min: Vec2::new(-apothem, 0.0),
|
||||||
|
aabb_max: Vec2::new(0.0, 1.0),
|
||||||
|
// The x-coordinate is now the inradius of the equilateral triangle, which is sqrt(3)/2.
|
||||||
|
bounding_circle_center: Vec2::new(-inv_sqrt_3 / 2.0, 0.5),
|
||||||
|
bounding_circle_radius: inv_sqrt_3,
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "1/4er circle rotated to be axis-aligned",
|
||||||
|
arc: Arc2d::from_radians(1.0, FRAC_PI_2),
|
||||||
|
translation: Vec2::ZERO,
|
||||||
|
// Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis.
|
||||||
|
rotation: -FRAC_PI_4,
|
||||||
|
aabb_min: Vec2::ZERO,
|
||||||
|
aabb_max: Vec2::splat(1.0),
|
||||||
|
bounding_circle_center: Vec2::splat(0.5),
|
||||||
|
bounding_circle_radius: f32::sqrt(2.0) / 2.0,
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "5/6th circle untransformed",
|
||||||
|
arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
|
||||||
|
translation: Vec2::ZERO,
|
||||||
|
rotation: 0.0,
|
||||||
|
aabb_min: Vec2::new(-1.0, -apothem),
|
||||||
|
aabb_max: Vec2::new(1.0, 1.0),
|
||||||
|
bounding_circle_center: Vec2::ZERO,
|
||||||
|
bounding_circle_radius: 1.0,
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "5/6th circle translated",
|
||||||
|
arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
|
||||||
|
translation: Vec2::new(2.0, 3.0),
|
||||||
|
rotation: 0.0,
|
||||||
|
aabb_min: Vec2::new(1.0, 3.0 - apothem),
|
||||||
|
aabb_max: Vec2::new(3.0, 4.0),
|
||||||
|
bounding_circle_center: Vec2::new(2.0, 3.0),
|
||||||
|
bounding_circle_radius: 1.0,
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "5/6th circle rotated",
|
||||||
|
arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
|
||||||
|
translation: Vec2::ZERO,
|
||||||
|
// Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis.
|
||||||
|
rotation: FRAC_PI_6,
|
||||||
|
aabb_min: Vec2::new(-1.0, -1.0),
|
||||||
|
aabb_max: Vec2::new(1.0, 1.0),
|
||||||
|
bounding_circle_center: Vec2::ZERO,
|
||||||
|
bounding_circle_radius: 1.0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for test in tests {
|
||||||
|
println!("subtest case: {}", test.name);
|
||||||
|
let sector: CircularSector = test.arc.into();
|
||||||
|
|
||||||
|
let aabb = sector.aabb_2d(test.translation, test.rotation);
|
||||||
|
assert_abs_diff_eq!(test.aabb_min, aabb.min);
|
||||||
|
assert_abs_diff_eq!(test.aabb_max, aabb.max);
|
||||||
|
|
||||||
|
let bounding_circle = sector.bounding_circle(test.translation, test.rotation);
|
||||||
|
assert_abs_diff_eq!(test.bounding_circle_center, bounding_circle.center);
|
||||||
|
assert_abs_diff_eq!(test.bounding_circle_radius, bounding_circle.radius());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ellipse() {
|
fn ellipse() {
|
||||||
let ellipse = Ellipse::new(1.0, 0.5);
|
let ellipse = Ellipse::new(1.0, 0.5);
|
||||||
|
|
|
@ -234,7 +234,7 @@ impl std::ops::Mul<Dir2> for Rotation2d {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "approx")]
|
#[cfg(any(feature = "approx", test))]
|
||||||
impl approx::AbsDiffEq for Dir2 {
|
impl approx::AbsDiffEq for Dir2 {
|
||||||
type Epsilon = f32;
|
type Epsilon = f32;
|
||||||
fn default_epsilon() -> f32 {
|
fn default_epsilon() -> f32 {
|
||||||
|
@ -245,7 +245,7 @@ impl approx::AbsDiffEq for Dir2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "approx")]
|
#[cfg(any(feature = "approx", test))]
|
||||||
impl approx::RelativeEq for Dir2 {
|
impl approx::RelativeEq for Dir2 {
|
||||||
fn default_max_relative() -> f32 {
|
fn default_max_relative() -> f32 {
|
||||||
f32::EPSILON
|
f32::EPSILON
|
||||||
|
@ -256,7 +256,7 @@ impl approx::RelativeEq for Dir2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "approx")]
|
#[cfg(any(feature = "approx", test))]
|
||||||
impl approx::UlpsEq for Dir2 {
|
impl approx::UlpsEq for Dir2 {
|
||||||
fn default_max_ulps() -> u32 {
|
fn default_max_ulps() -> u32 {
|
||||||
4
|
4
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::f32::consts::PI;
|
use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, PI};
|
||||||
|
|
||||||
use super::{Measured2d, Primitive2d, WindingOrder};
|
use super::{Measured2d, Primitive2d, WindingOrder};
|
||||||
use crate::{Dir2, Vec2};
|
use crate::{Dir2, Vec2};
|
||||||
|
@ -67,6 +67,639 @@ impl Measured2d for Circle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A primitive representing an arc between two points on a circle.
|
||||||
|
///
|
||||||
|
/// An arc has no area.
|
||||||
|
/// If you want to include the portion of a circle's area swept out by the arc,
|
||||||
|
/// use the pie-shaped [`CircularSector`].
|
||||||
|
/// If you want to include only the space inside the convex hull of the arc,
|
||||||
|
/// use the bowl-shaped [`CircularSegment`].
|
||||||
|
///
|
||||||
|
/// The arc is drawn starting from [`Vec2::Y`], extending by `half_angle` radians on
|
||||||
|
/// either side. The center of the circle is the origin [`Vec2::ZERO`]. Note that this
|
||||||
|
/// means that the origin may not be within the `Arc2d`'s convex hull.
|
||||||
|
///
|
||||||
|
/// **Warning:** Arcs with negative angle or radius, or with angle greater than an entire circle, are not officially supported.
|
||||||
|
/// It is recommended to normalize arcs to have an angle in [0, 2π].
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
#[doc(alias("CircularArc", "CircleArc"))]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct Arc2d {
|
||||||
|
/// The radius of the circle
|
||||||
|
pub radius: f32,
|
||||||
|
/// Half the angle defining the arc
|
||||||
|
pub half_angle: f32,
|
||||||
|
}
|
||||||
|
impl Primitive2d for Arc2d {}
|
||||||
|
|
||||||
|
impl Default for Arc2d {
|
||||||
|
/// Returns the default [`Arc2d`] with radius `0.5`, covering one third of a circle
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
radius: 0.5,
|
||||||
|
half_angle: 2.0 * FRAC_PI_3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Arc2d {
|
||||||
|
/// Create a new [`Arc2d`] from a `radius` and a `half_angle`
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn new(radius: f32, half_angle: f32) -> Self {
|
||||||
|
Self { radius, half_angle }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`Arc2d`] from a `radius` and an `angle` in radians
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn from_radians(radius: f32, angle: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
radius,
|
||||||
|
half_angle: angle / 2.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`Arc2d`] from a `radius` and an `angle` in degrees.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn from_degrees(radius: f32, angle: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
radius,
|
||||||
|
half_angle: angle.to_radians() / 2.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`Arc2d`] from a `radius` and a `fraction` of a single turn.
|
||||||
|
///
|
||||||
|
/// For instance, `0.5` turns is a semicircle.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn from_turns(radius: f32, fraction: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
radius,
|
||||||
|
half_angle: fraction * PI,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the angle of the arc
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn angle(&self) -> f32 {
|
||||||
|
self.half_angle * 2.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the arc
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn length(&self) -> f32 {
|
||||||
|
self.angle() * self.radius
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the right-hand end point of the arc
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn right_endpoint(&self) -> Vec2 {
|
||||||
|
self.radius * Vec2::from_angle(FRAC_PI_2 - self.half_angle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the left-hand end point of the arc
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn left_endpoint(&self) -> Vec2 {
|
||||||
|
self.radius * Vec2::from_angle(FRAC_PI_2 + self.half_angle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the endpoints of the arc
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn endpoints(&self) -> [Vec2; 2] {
|
||||||
|
[self.left_endpoint(), self.right_endpoint()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the midpoint of the arc
|
||||||
|
#[inline]
|
||||||
|
pub fn midpoint(&self) -> Vec2 {
|
||||||
|
self.radius * Vec2::Y
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get half the distance between the endpoints (half the length of the chord)
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn half_chord_length(&self) -> f32 {
|
||||||
|
self.radius * f32::sin(self.half_angle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the distance between the endpoints (the length of the chord)
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn chord_length(&self) -> f32 {
|
||||||
|
2.0 * self.half_chord_length()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the midpoint of the two endpoints (the midpoint of the chord)
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn chord_midpoint(&self) -> Vec2 {
|
||||||
|
self.apothem() * Vec2::Y
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the apothem of this arc, that is,
|
||||||
|
/// the distance from the center of the circle to the midpoint of the chord, in the direction of the midpoint of the arc.
|
||||||
|
/// Equivalently, the [`radius`](Self::radius) minus the [`sagitta`](Self::sagitta).
|
||||||
|
///
|
||||||
|
/// Note that for a [`major`](Self::is_major) arc, the apothem will be negative.
|
||||||
|
#[inline(always)]
|
||||||
|
// Naming note: Various sources are inconsistent as to whether the apothem is the segment between the center and the
|
||||||
|
// midpoint of a chord, or the length of that segment. Given this confusion, we've opted for the definition
|
||||||
|
// used by Wolfram MathWorld, which is the distance rather than the segment.
|
||||||
|
pub fn apothem(&self) -> f32 {
|
||||||
|
let sign = if self.is_minor() { 1.0 } else { -1.0 };
|
||||||
|
sign * f32::sqrt(self.radius.powi(2) - self.half_chord_length().powi(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the sagitta of this arc, that is,
|
||||||
|
/// the length of the line between the midpoints of the arc and its chord.
|
||||||
|
/// Equivalently, the height of the triangle whose base is the chord and whose apex is the midpoint of the arc.
|
||||||
|
///
|
||||||
|
/// The sagitta is also the sum of the [`radius`](Self::radius) and the [`apothem`](Self::apothem).
|
||||||
|
pub fn sagitta(&self) -> f32 {
|
||||||
|
self.radius - self.apothem()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produces true if the arc is at most half a circle.
|
||||||
|
///
|
||||||
|
/// **Note:** This is not the negation of [`is_major`](Self::is_major): an exact semicircle is both major and minor.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn is_minor(&self) -> bool {
|
||||||
|
self.half_angle <= FRAC_PI_2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produces true if the arc is at least half a circle.
|
||||||
|
///
|
||||||
|
/// **Note:** This is not the negation of [`is_minor`](Self::is_minor): an exact semicircle is both major and minor.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn is_major(&self) -> bool {
|
||||||
|
self.half_angle >= FRAC_PI_2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A primitive representing a circular sector: a pie slice of a circle.
|
||||||
|
///
|
||||||
|
/// The segment is positioned so that it always includes [`Vec2::Y`] and is vertically symmetrical.
|
||||||
|
/// To orient the sector differently, apply a rotation.
|
||||||
|
/// The sector is drawn with the center of its circle at the origin [`Vec2::ZERO`].
|
||||||
|
///
|
||||||
|
/// **Warning:** Circular sectors with negative angle or radius, or with angle greater than an entire circle, are not officially supported.
|
||||||
|
/// We recommend normalizing circular sectors to have an angle in [0, 2π].
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct CircularSector {
|
||||||
|
/// The arc defining the sector
|
||||||
|
#[cfg_attr(feature = "serialize", serde(flatten))]
|
||||||
|
pub arc: Arc2d,
|
||||||
|
}
|
||||||
|
impl Primitive2d for CircularSector {}
|
||||||
|
|
||||||
|
impl Default for CircularSector {
|
||||||
|
/// Returns the default [`CircularSector`] with radius `0.5` and covering a third of a circle
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::from(Arc2d::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Arc2d> for CircularSector {
|
||||||
|
fn from(arc: Arc2d) -> Self {
|
||||||
|
Self { arc }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CircularSector {
|
||||||
|
/// Create a new [`CircularSector`] from a `radius` and an `angle`
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn new(radius: f32, angle: f32) -> Self {
|
||||||
|
Self::from(Arc2d::new(radius, angle))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`CircularSector`] from a `radius` and an `angle` in radians.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn from_radians(radius: f32, angle: f32) -> Self {
|
||||||
|
Self::from(Arc2d::from_radians(radius, angle))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`CircularSector`] from a `radius` and an `angle` in degrees.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn from_degrees(radius: f32, angle: f32) -> Self {
|
||||||
|
Self::from(Arc2d::from_degrees(radius, angle))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`CircularSector`] from a `radius` and a number of `turns` of a circle.
|
||||||
|
///
|
||||||
|
/// For instance, `0.5` turns is a semicircle.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn from_turns(radius: f32, fraction: f32) -> Self {
|
||||||
|
Self::from(Arc2d::from_turns(radius, fraction))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get half the angle of the sector
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn half_angle(&self) -> f32 {
|
||||||
|
self.arc.half_angle
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the angle of the sector
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn angle(&self) -> f32 {
|
||||||
|
self.arc.angle()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the radius of the sector
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn radius(&self) -> f32 {
|
||||||
|
self.arc.radius
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the arc defining the sector
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn arc_length(&self) -> f32 {
|
||||||
|
self.arc.length()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get half the length of the chord defined by the sector
|
||||||
|
///
|
||||||
|
/// See [`Arc2d::half_chord_length`]
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn half_chord_length(&self) -> f32 {
|
||||||
|
self.arc.half_chord_length()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the chord defined by the sector
|
||||||
|
///
|
||||||
|
/// See [`Arc2d::chord_length`]
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn chord_length(&self) -> f32 {
|
||||||
|
self.arc.chord_length()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the midpoint of the chord defined by the sector
|
||||||
|
///
|
||||||
|
/// See [`Arc2d::chord_midpoint`]
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn chord_midpoint(&self) -> Vec2 {
|
||||||
|
self.arc.chord_midpoint()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the apothem of this sector
|
||||||
|
///
|
||||||
|
/// See [`Arc2d::apothem`]
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn apothem(&self) -> f32 {
|
||||||
|
self.arc.apothem()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the sagitta of this sector
|
||||||
|
///
|
||||||
|
/// See [`Arc2d::sagitta`]
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn sagitta(&self) -> f32 {
|
||||||
|
self.arc.sagitta()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the area of this sector
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn area(&self) -> f32 {
|
||||||
|
self.arc.radius.powi(2) * self.arc.half_angle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A primitive representing a circular segment:
|
||||||
|
/// the area enclosed by the arc of a circle and its chord (the line between its endpoints).
|
||||||
|
///
|
||||||
|
/// The segment is drawn starting from [`Vec2::Y`], extending equally on either side.
|
||||||
|
/// To orient the segment differently, apply a rotation.
|
||||||
|
/// The segment is drawn with the center of its circle at the origin [`Vec2::ZERO`].
|
||||||
|
/// When positioning a segment, the [`apothem`](Self::apothem) function may be particularly useful.
|
||||||
|
///
|
||||||
|
/// **Warning:** Circular segments with negative angle or radius, or with angle greater than an entire circle, are not officially supported.
|
||||||
|
/// We recommend normalizing circular segments to have an angle in [0, 2π].
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct CircularSegment {
|
||||||
|
/// The arc defining the segment
|
||||||
|
#[cfg_attr(feature = "serialize", serde(flatten))]
|
||||||
|
pub arc: Arc2d,
|
||||||
|
}
|
||||||
|
impl Primitive2d for CircularSegment {}
|
||||||
|
|
||||||
|
impl Default for CircularSegment {
|
||||||
|
/// Returns the default [`CircularSegment`] with radius `0.5` and covering a third of a circle
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::from(Arc2d::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Arc2d> for CircularSegment {
|
||||||
|
fn from(arc: Arc2d) -> Self {
|
||||||
|
Self { arc }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CircularSegment {
|
||||||
|
/// Create a new [`CircularSegment`] from a `radius`, and an `angle`
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn new(radius: f32, angle: f32) -> Self {
|
||||||
|
Self::from(Arc2d::new(radius, angle))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`CircularSegment`] from a `radius` and an `angle` in radians.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn from_radians(radius: f32, angle: f32) -> Self {
|
||||||
|
Self::from(Arc2d::from_radians(radius, angle))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`CircularSegment`] from a `radius` and an `angle` in degrees.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn from_degrees(radius: f32, angle: f32) -> Self {
|
||||||
|
Self::from(Arc2d::from_degrees(radius, angle))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`CircularSegment`] from a `radius` and a number of `turns` of a circle.
|
||||||
|
///
|
||||||
|
/// For instance, `0.5` turns is a semicircle.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn from_turns(radius: f32, fraction: f32) -> Self {
|
||||||
|
Self::from(Arc2d::from_turns(radius, fraction))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the half-angle of the segment
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn half_angle(&self) -> f32 {
|
||||||
|
self.arc.half_angle
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the angle of the segment
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn angle(&self) -> f32 {
|
||||||
|
self.arc.angle()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the radius of the segment
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn radius(&self) -> f32 {
|
||||||
|
self.arc.radius
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the arc defining the segment
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn arc_length(&self) -> f32 {
|
||||||
|
self.arc.length()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get half the length of the segment's base, also known as its chord
|
||||||
|
#[inline(always)]
|
||||||
|
#[doc(alias = "half_base_length")]
|
||||||
|
pub fn half_chord_length(&self) -> f32 {
|
||||||
|
self.arc.half_chord_length()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the segment's base, also known as its chord
|
||||||
|
#[inline(always)]
|
||||||
|
#[doc(alias = "base_length")]
|
||||||
|
#[doc(alias = "base")]
|
||||||
|
pub fn chord_length(&self) -> f32 {
|
||||||
|
self.arc.chord_length()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the midpoint of the segment's base, also known as its chord
|
||||||
|
#[inline(always)]
|
||||||
|
#[doc(alias = "base_midpoint")]
|
||||||
|
pub fn chord_midpoint(&self) -> Vec2 {
|
||||||
|
self.arc.chord_midpoint()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the apothem of this segment,
|
||||||
|
/// which is the signed distance between the segment and the center of its circle
|
||||||
|
///
|
||||||
|
/// See [`Arc2d::apothem`]
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn apothem(&self) -> f32 {
|
||||||
|
self.arc.apothem()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the length of the sagitta of this segment, also known as its height
|
||||||
|
///
|
||||||
|
/// See [`Arc2d::sagitta`]
|
||||||
|
#[inline(always)]
|
||||||
|
#[doc(alias = "height")]
|
||||||
|
pub fn sagitta(&self) -> f32 {
|
||||||
|
self.arc.sagitta()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the area of this segment
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn area(&self) -> f32 {
|
||||||
|
0.5 * self.arc.radius.powi(2) * (self.arc.angle() - self.arc.angle().sin())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod arc_tests {
|
||||||
|
use std::f32::consts::FRAC_PI_4;
|
||||||
|
|
||||||
|
use approx::assert_abs_diff_eq;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
struct ArcTestCase {
|
||||||
|
radius: f32,
|
||||||
|
half_angle: f32,
|
||||||
|
angle: f32,
|
||||||
|
length: f32,
|
||||||
|
right_endpoint: Vec2,
|
||||||
|
left_endpoint: Vec2,
|
||||||
|
endpoints: [Vec2; 2],
|
||||||
|
midpoint: Vec2,
|
||||||
|
half_chord_length: f32,
|
||||||
|
chord_length: f32,
|
||||||
|
chord_midpoint: Vec2,
|
||||||
|
apothem: f32,
|
||||||
|
sagitta: f32,
|
||||||
|
is_minor: bool,
|
||||||
|
is_major: bool,
|
||||||
|
sector_area: f32,
|
||||||
|
segment_area: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArcTestCase {
|
||||||
|
fn check_arc(&self, arc: Arc2d) {
|
||||||
|
assert_abs_diff_eq!(self.radius, arc.radius);
|
||||||
|
assert_abs_diff_eq!(self.half_angle, arc.half_angle);
|
||||||
|
assert_abs_diff_eq!(self.angle, arc.angle());
|
||||||
|
assert_abs_diff_eq!(self.length, arc.length());
|
||||||
|
assert_abs_diff_eq!(self.right_endpoint, arc.right_endpoint());
|
||||||
|
assert_abs_diff_eq!(self.left_endpoint, arc.left_endpoint());
|
||||||
|
assert_abs_diff_eq!(self.endpoints[0], arc.endpoints()[0]);
|
||||||
|
assert_abs_diff_eq!(self.endpoints[1], arc.endpoints()[1]);
|
||||||
|
assert_abs_diff_eq!(self.midpoint, arc.midpoint());
|
||||||
|
assert_abs_diff_eq!(self.half_chord_length, arc.half_chord_length());
|
||||||
|
assert_abs_diff_eq!(self.chord_length, arc.chord_length(), epsilon = 0.00001);
|
||||||
|
assert_abs_diff_eq!(self.chord_midpoint, arc.chord_midpoint());
|
||||||
|
assert_abs_diff_eq!(self.apothem, arc.apothem());
|
||||||
|
assert_abs_diff_eq!(self.sagitta, arc.sagitta());
|
||||||
|
assert_eq!(self.is_minor, arc.is_minor());
|
||||||
|
assert_eq!(self.is_major, arc.is_major());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_sector(&self, sector: CircularSector) {
|
||||||
|
assert_abs_diff_eq!(self.radius, sector.radius());
|
||||||
|
assert_abs_diff_eq!(self.half_angle, sector.half_angle());
|
||||||
|
assert_abs_diff_eq!(self.angle, sector.angle());
|
||||||
|
assert_abs_diff_eq!(self.half_chord_length, sector.half_chord_length());
|
||||||
|
assert_abs_diff_eq!(self.chord_length, sector.chord_length(), epsilon = 0.00001);
|
||||||
|
assert_abs_diff_eq!(self.chord_midpoint, sector.chord_midpoint());
|
||||||
|
assert_abs_diff_eq!(self.apothem, sector.apothem());
|
||||||
|
assert_abs_diff_eq!(self.sagitta, sector.sagitta());
|
||||||
|
assert_abs_diff_eq!(self.sector_area, sector.area());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_segment(&self, segment: CircularSegment) {
|
||||||
|
assert_abs_diff_eq!(self.radius, segment.radius());
|
||||||
|
assert_abs_diff_eq!(self.half_angle, segment.half_angle());
|
||||||
|
assert_abs_diff_eq!(self.angle, segment.angle());
|
||||||
|
assert_abs_diff_eq!(self.half_chord_length, segment.half_chord_length());
|
||||||
|
assert_abs_diff_eq!(self.chord_length, segment.chord_length(), epsilon = 0.00001);
|
||||||
|
assert_abs_diff_eq!(self.chord_midpoint, segment.chord_midpoint());
|
||||||
|
assert_abs_diff_eq!(self.apothem, segment.apothem());
|
||||||
|
assert_abs_diff_eq!(self.sagitta, segment.sagitta());
|
||||||
|
assert_abs_diff_eq!(self.segment_area, segment.area());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_angle() {
|
||||||
|
let tests = ArcTestCase {
|
||||||
|
radius: 1.0,
|
||||||
|
half_angle: 0.0,
|
||||||
|
angle: 0.0,
|
||||||
|
length: 0.0,
|
||||||
|
left_endpoint: Vec2::Y,
|
||||||
|
right_endpoint: Vec2::Y,
|
||||||
|
endpoints: [Vec2::Y, Vec2::Y],
|
||||||
|
midpoint: Vec2::Y,
|
||||||
|
half_chord_length: 0.0,
|
||||||
|
chord_length: 0.0,
|
||||||
|
chord_midpoint: Vec2::Y,
|
||||||
|
apothem: 1.0,
|
||||||
|
sagitta: 0.0,
|
||||||
|
is_minor: true,
|
||||||
|
is_major: false,
|
||||||
|
sector_area: 0.0,
|
||||||
|
segment_area: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
tests.check_arc(Arc2d::new(1.0, 0.0));
|
||||||
|
tests.check_sector(CircularSector::new(1.0, 0.0));
|
||||||
|
tests.check_segment(CircularSegment::new(1.0, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_radius() {
|
||||||
|
let tests = ArcTestCase {
|
||||||
|
radius: 0.0,
|
||||||
|
half_angle: FRAC_PI_4,
|
||||||
|
angle: FRAC_PI_2,
|
||||||
|
length: 0.0,
|
||||||
|
left_endpoint: Vec2::ZERO,
|
||||||
|
right_endpoint: Vec2::ZERO,
|
||||||
|
endpoints: [Vec2::ZERO, Vec2::ZERO],
|
||||||
|
midpoint: Vec2::ZERO,
|
||||||
|
half_chord_length: 0.0,
|
||||||
|
chord_length: 0.0,
|
||||||
|
chord_midpoint: Vec2::ZERO,
|
||||||
|
apothem: 0.0,
|
||||||
|
sagitta: 0.0,
|
||||||
|
is_minor: true,
|
||||||
|
is_major: false,
|
||||||
|
sector_area: 0.0,
|
||||||
|
segment_area: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
tests.check_arc(Arc2d::new(0.0, FRAC_PI_4));
|
||||||
|
tests.check_sector(CircularSector::new(0.0, FRAC_PI_4));
|
||||||
|
tests.check_segment(CircularSegment::new(0.0, FRAC_PI_4));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quarter_circle() {
|
||||||
|
let sqrt_half: f32 = f32::sqrt(0.5);
|
||||||
|
let tests = ArcTestCase {
|
||||||
|
radius: 1.0,
|
||||||
|
half_angle: FRAC_PI_4,
|
||||||
|
angle: FRAC_PI_2,
|
||||||
|
length: FRAC_PI_2,
|
||||||
|
left_endpoint: Vec2::new(-sqrt_half, sqrt_half),
|
||||||
|
right_endpoint: Vec2::splat(sqrt_half),
|
||||||
|
endpoints: [Vec2::new(-sqrt_half, sqrt_half), Vec2::splat(sqrt_half)],
|
||||||
|
midpoint: Vec2::Y,
|
||||||
|
half_chord_length: sqrt_half,
|
||||||
|
chord_length: f32::sqrt(2.0),
|
||||||
|
chord_midpoint: Vec2::new(0.0, sqrt_half),
|
||||||
|
apothem: sqrt_half,
|
||||||
|
sagitta: 1.0 - sqrt_half,
|
||||||
|
is_minor: true,
|
||||||
|
is_major: false,
|
||||||
|
sector_area: FRAC_PI_4,
|
||||||
|
segment_area: FRAC_PI_4 - 0.5,
|
||||||
|
};
|
||||||
|
|
||||||
|
tests.check_arc(Arc2d::from_turns(1.0, 0.25));
|
||||||
|
tests.check_sector(CircularSector::from_turns(1.0, 0.25));
|
||||||
|
tests.check_segment(CircularSegment::from_turns(1.0, 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn half_circle() {
|
||||||
|
let tests = ArcTestCase {
|
||||||
|
radius: 1.0,
|
||||||
|
half_angle: FRAC_PI_2,
|
||||||
|
angle: PI,
|
||||||
|
length: PI,
|
||||||
|
left_endpoint: Vec2::NEG_X,
|
||||||
|
right_endpoint: Vec2::X,
|
||||||
|
endpoints: [Vec2::NEG_X, Vec2::X],
|
||||||
|
midpoint: Vec2::Y,
|
||||||
|
half_chord_length: 1.0,
|
||||||
|
chord_length: 2.0,
|
||||||
|
chord_midpoint: Vec2::ZERO,
|
||||||
|
apothem: 0.0,
|
||||||
|
sagitta: 1.0,
|
||||||
|
is_minor: true,
|
||||||
|
is_major: true,
|
||||||
|
sector_area: FRAC_PI_2,
|
||||||
|
segment_area: FRAC_PI_2,
|
||||||
|
};
|
||||||
|
|
||||||
|
tests.check_arc(Arc2d::from_radians(1.0, PI));
|
||||||
|
tests.check_sector(CircularSector::from_radians(1.0, PI));
|
||||||
|
tests.check_segment(CircularSegment::from_radians(1.0, PI));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_circle() {
|
||||||
|
let tests = ArcTestCase {
|
||||||
|
radius: 1.0,
|
||||||
|
half_angle: PI,
|
||||||
|
angle: 2.0 * PI,
|
||||||
|
length: 2.0 * PI,
|
||||||
|
left_endpoint: Vec2::NEG_Y,
|
||||||
|
right_endpoint: Vec2::NEG_Y,
|
||||||
|
endpoints: [Vec2::NEG_Y, Vec2::NEG_Y],
|
||||||
|
midpoint: Vec2::Y,
|
||||||
|
half_chord_length: 0.0,
|
||||||
|
chord_length: 0.0,
|
||||||
|
chord_midpoint: Vec2::NEG_Y,
|
||||||
|
apothem: -1.0,
|
||||||
|
sagitta: 2.0,
|
||||||
|
is_minor: false,
|
||||||
|
is_major: true,
|
||||||
|
sector_area: PI,
|
||||||
|
segment_area: PI,
|
||||||
|
};
|
||||||
|
|
||||||
|
tests.check_arc(Arc2d::from_degrees(1.0, 360.0));
|
||||||
|
tests.check_sector(CircularSector::from_degrees(1.0, 360.0));
|
||||||
|
tests.check_segment(CircularSegment::from_degrees(1.0, 360.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An ellipse primitive
|
/// An ellipse primitive
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
|
|
@ -386,7 +386,7 @@ impl std::ops::Mul<Vec2> for Rotation2d {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "approx")]
|
#[cfg(any(feature = "approx", test))]
|
||||||
impl approx::AbsDiffEq for Rotation2d {
|
impl approx::AbsDiffEq for Rotation2d {
|
||||||
type Epsilon = f32;
|
type Epsilon = f32;
|
||||||
fn default_epsilon() -> f32 {
|
fn default_epsilon() -> f32 {
|
||||||
|
@ -397,7 +397,7 @@ impl approx::AbsDiffEq for Rotation2d {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "approx")]
|
#[cfg(any(feature = "approx", test))]
|
||||||
impl approx::RelativeEq for Rotation2d {
|
impl approx::RelativeEq for Rotation2d {
|
||||||
fn default_max_relative() -> f32 {
|
fn default_max_relative() -> f32 {
|
||||||
f32::EPSILON
|
f32::EPSILON
|
||||||
|
@ -408,7 +408,7 @@ impl approx::RelativeEq for Rotation2d {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "approx")]
|
#[cfg(any(feature = "approx", test))]
|
||||||
impl approx::UlpsEq for Rotation2d {
|
impl approx::UlpsEq for Rotation2d {
|
||||||
fn default_max_ulps() -> u32 {
|
fn default_max_ulps() -> u32 {
|
||||||
4
|
4
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::f32::consts::FRAC_PI_2;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
mesh::primitives::dim3::triangle3d,
|
mesh::primitives::dim3::triangle3d,
|
||||||
mesh::{Indices, Mesh},
|
mesh::{Indices, Mesh},
|
||||||
|
@ -5,9 +7,12 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{MeshBuilder, Meshable};
|
use super::{MeshBuilder, Meshable};
|
||||||
use bevy_math::primitives::{
|
use bevy_math::{
|
||||||
Annulus, Capsule2d, Circle, Ellipse, Rectangle, RegularPolygon, Triangle2d, Triangle3d,
|
primitives::{
|
||||||
WindingOrder,
|
Annulus, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Rectangle,
|
||||||
|
RegularPolygon, Triangle2d, Triangle3d, WindingOrder,
|
||||||
|
},
|
||||||
|
FloatExt, Vec2,
|
||||||
};
|
};
|
||||||
use wgpu::PrimitiveTopology;
|
use wgpu::PrimitiveTopology;
|
||||||
|
|
||||||
|
@ -73,6 +78,287 @@ impl From<Circle> for Mesh {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specifies how to generate UV-mappings for the [`CircularSector`] and [`CircularSegment`] shapes.
|
||||||
|
///
|
||||||
|
/// Currently the only variant is `Mask`, which is good for showing a portion of a texture that includes
|
||||||
|
/// the entire circle, particularly the same texture will be displayed with different fractions of a
|
||||||
|
/// complete circle.
|
||||||
|
///
|
||||||
|
/// It's expected that more will be added in the future, such as a variant that causes the texture to be
|
||||||
|
/// scaled to fit the bounding box of the shape, which would be good for packed textures only including the
|
||||||
|
/// portion of the circle that is needed to display.
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum CircularMeshUvMode {
|
||||||
|
/// Treats the shape as a mask over a circle of equal size and radius,
|
||||||
|
/// with the center of the circle at the center of the texture.
|
||||||
|
Mask {
|
||||||
|
/// Angle by which to rotate the shape when generating the UV map.
|
||||||
|
angle: f32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CircularMeshUvMode {
|
||||||
|
fn default() -> Self {
|
||||||
|
CircularMeshUvMode::Mask { angle: 0.0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A builder used for creating a [`Mesh`] with a [`CircularSector`] shape.
|
||||||
|
///
|
||||||
|
/// The resulting mesh will have a UV-map such that the center of the circle is
|
||||||
|
/// at the center of the texture.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CircularSectorMeshBuilder {
|
||||||
|
/// The sector shape.
|
||||||
|
pub sector: CircularSector,
|
||||||
|
/// The number of vertices used for the arc portion of the sector mesh.
|
||||||
|
/// The default is `32`.
|
||||||
|
#[doc(alias = "vertices")]
|
||||||
|
pub resolution: usize,
|
||||||
|
/// The UV mapping mode
|
||||||
|
pub uv_mode: CircularMeshUvMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CircularSectorMeshBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
sector: CircularSector::default(),
|
||||||
|
resolution: 32,
|
||||||
|
uv_mode: CircularMeshUvMode::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CircularSectorMeshBuilder {
|
||||||
|
/// Creates a new [`CircularSectorMeshBuilder`] from a given sector
|
||||||
|
#[inline]
|
||||||
|
pub fn new(sector: CircularSector) -> Self {
|
||||||
|
Self {
|
||||||
|
sector,
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the number of vertices used for the sector mesh.
|
||||||
|
#[inline]
|
||||||
|
#[doc(alias = "vertices")]
|
||||||
|
pub const fn resolution(mut self, resolution: usize) -> Self {
|
||||||
|
self.resolution = resolution;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the uv mode used for the sector mesh
|
||||||
|
#[inline]
|
||||||
|
pub const fn uv_mode(mut self, uv_mode: CircularMeshUvMode) -> Self {
|
||||||
|
self.uv_mode = uv_mode;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a [`Mesh`] based on the configuration in `self`.
|
||||||
|
pub fn build(&self) -> Mesh {
|
||||||
|
let mut indices = Vec::with_capacity((self.resolution - 1) * 3);
|
||||||
|
let mut positions = Vec::with_capacity(self.resolution + 1);
|
||||||
|
let normals = vec![[0.0, 0.0, 1.0]; self.resolution + 1];
|
||||||
|
let mut uvs = Vec::with_capacity(self.resolution + 1);
|
||||||
|
|
||||||
|
let CircularMeshUvMode::Mask { angle: uv_angle } = self.uv_mode;
|
||||||
|
|
||||||
|
// Push the center of the circle.
|
||||||
|
positions.push([0.0; 3]);
|
||||||
|
uvs.push([0.5; 2]);
|
||||||
|
|
||||||
|
let first_angle = FRAC_PI_2 - self.sector.half_angle();
|
||||||
|
let last_angle = FRAC_PI_2 + self.sector.half_angle();
|
||||||
|
let last_i = (self.resolution - 1) as f32;
|
||||||
|
for i in 0..self.resolution {
|
||||||
|
let angle = f32::lerp(first_angle, last_angle, i as f32 / last_i);
|
||||||
|
|
||||||
|
// Compute the vertex
|
||||||
|
let vertex = self.sector.radius() * Vec2::from_angle(angle);
|
||||||
|
// Compute the UV coordinate by taking the modified angle's unit vector, negating the Y axis, and rescaling and centering it at (0.5, 0.5).
|
||||||
|
// We accomplish the Y axis flip by negating the angle.
|
||||||
|
let uv =
|
||||||
|
Vec2::from_angle(-(angle + uv_angle)).mul_add(Vec2::splat(0.5), Vec2::splat(0.5));
|
||||||
|
|
||||||
|
positions.push([vertex.x, vertex.y, 0.0]);
|
||||||
|
uvs.push([uv.x, uv.y]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 1..(self.resolution as u32) {
|
||||||
|
// Index 0 is the center.
|
||||||
|
indices.extend_from_slice(&[0, i, i + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mesh::new(
|
||||||
|
PrimitiveTopology::TriangleList,
|
||||||
|
RenderAssetUsages::default(),
|
||||||
|
)
|
||||||
|
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
||||||
|
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||||
|
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||||
|
.with_inserted_indices(Indices::U32(indices))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Meshable for CircularSector {
|
||||||
|
type Output = CircularSectorMeshBuilder;
|
||||||
|
|
||||||
|
fn mesh(&self) -> Self::Output {
|
||||||
|
CircularSectorMeshBuilder {
|
||||||
|
sector: *self,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CircularSector> for Mesh {
|
||||||
|
/// Converts this sector into a [`Mesh`] using a default [`CircularSectorMeshBuilder`].
|
||||||
|
///
|
||||||
|
/// See the documentation of [`CircularSectorMeshBuilder`] for more details.
|
||||||
|
fn from(sector: CircularSector) -> Self {
|
||||||
|
sector.mesh().build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CircularSectorMeshBuilder> for Mesh {
|
||||||
|
fn from(sector: CircularSectorMeshBuilder) -> Self {
|
||||||
|
sector.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A builder used for creating a [`Mesh`] with a [`CircularSegment`] shape.
|
||||||
|
///
|
||||||
|
/// The resulting mesh will have a UV-map such that the center of the circle is
|
||||||
|
/// at the center of the texture.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct CircularSegmentMeshBuilder {
|
||||||
|
/// The segment shape.
|
||||||
|
pub segment: CircularSegment,
|
||||||
|
/// The number of vertices used for the arc portion of the segment mesh.
|
||||||
|
/// The default is `32`.
|
||||||
|
#[doc(alias = "vertices")]
|
||||||
|
pub resolution: usize,
|
||||||
|
/// The UV mapping mode
|
||||||
|
pub uv_mode: CircularMeshUvMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CircularSegmentMeshBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
segment: CircularSegment::default(),
|
||||||
|
resolution: 32,
|
||||||
|
uv_mode: CircularMeshUvMode::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CircularSegmentMeshBuilder {
|
||||||
|
/// Creates a new [`CircularSegmentMeshBuilder`] from a given segment
|
||||||
|
#[inline]
|
||||||
|
pub fn new(segment: CircularSegment) -> Self {
|
||||||
|
Self {
|
||||||
|
segment,
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the number of vertices used for the segment mesh.
|
||||||
|
#[inline]
|
||||||
|
#[doc(alias = "vertices")]
|
||||||
|
pub const fn resolution(mut self, resolution: usize) -> Self {
|
||||||
|
self.resolution = resolution;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the uv mode used for the segment mesh
|
||||||
|
#[inline]
|
||||||
|
pub const fn uv_mode(mut self, uv_mode: CircularMeshUvMode) -> Self {
|
||||||
|
self.uv_mode = uv_mode;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a [`Mesh`] based on the configuration in `self`.
|
||||||
|
pub fn build(&self) -> Mesh {
|
||||||
|
let mut indices = Vec::with_capacity((self.resolution - 1) * 3);
|
||||||
|
let mut positions = Vec::with_capacity(self.resolution + 1);
|
||||||
|
let normals = vec![[0.0, 0.0, 1.0]; self.resolution + 1];
|
||||||
|
let mut uvs = Vec::with_capacity(self.resolution + 1);
|
||||||
|
|
||||||
|
let CircularMeshUvMode::Mask { angle: uv_angle } = self.uv_mode;
|
||||||
|
|
||||||
|
// Push the center of the chord.
|
||||||
|
let midpoint_vertex = self.segment.chord_midpoint();
|
||||||
|
positions.push([midpoint_vertex.x, midpoint_vertex.y, 0.0]);
|
||||||
|
// Compute the UV coordinate of the midpoint vertex.
|
||||||
|
// This is similar to the computation inside the loop for the arc vertices,
|
||||||
|
// but the vertex angle is PI/2, and we must scale by the ratio of the apothem to the radius
|
||||||
|
// to correctly position the vertex.
|
||||||
|
let midpoint_uv = Vec2::from_angle(-uv_angle - FRAC_PI_2).mul_add(
|
||||||
|
Vec2::splat(0.5 * (self.segment.apothem() / self.segment.radius())),
|
||||||
|
Vec2::splat(0.5),
|
||||||
|
);
|
||||||
|
uvs.push([midpoint_uv.x, midpoint_uv.y]);
|
||||||
|
|
||||||
|
let first_angle = FRAC_PI_2 - self.segment.half_angle();
|
||||||
|
let last_angle = FRAC_PI_2 + self.segment.half_angle();
|
||||||
|
let last_i = (self.resolution - 1) as f32;
|
||||||
|
for i in 0..self.resolution {
|
||||||
|
let angle = f32::lerp(first_angle, last_angle, i as f32 / last_i);
|
||||||
|
|
||||||
|
// Compute the vertex
|
||||||
|
let vertex = self.segment.radius() * Vec2::from_angle(angle);
|
||||||
|
// Compute the UV coordinate by taking the modified angle's unit vector, negating the Y axis, and rescaling and centering it at (0.5, 0.5).
|
||||||
|
// We accomplish the Y axis flip by negating the angle.
|
||||||
|
let uv =
|
||||||
|
Vec2::from_angle(-(angle + uv_angle)).mul_add(Vec2::splat(0.5), Vec2::splat(0.5));
|
||||||
|
|
||||||
|
positions.push([vertex.x, vertex.y, 0.0]);
|
||||||
|
uvs.push([uv.x, uv.y]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 1..(self.resolution as u32) {
|
||||||
|
// Index 0 is the midpoint of the chord.
|
||||||
|
indices.extend_from_slice(&[0, i, i + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mesh::new(
|
||||||
|
PrimitiveTopology::TriangleList,
|
||||||
|
RenderAssetUsages::default(),
|
||||||
|
)
|
||||||
|
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
|
||||||
|
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
|
||||||
|
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
|
||||||
|
.with_inserted_indices(Indices::U32(indices))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Meshable for CircularSegment {
|
||||||
|
type Output = CircularSegmentMeshBuilder;
|
||||||
|
|
||||||
|
fn mesh(&self) -> Self::Output {
|
||||||
|
CircularSegmentMeshBuilder {
|
||||||
|
segment: *self,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CircularSegment> for Mesh {
|
||||||
|
/// Converts this sector into a [`Mesh`] using a default [`CircularSegmentMeshBuilder`].
|
||||||
|
///
|
||||||
|
/// See the documentation of [`CircularSegmentMeshBuilder`] for more details.
|
||||||
|
fn from(segment: CircularSegment) -> Self {
|
||||||
|
segment.mesh().build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CircularSegmentMeshBuilder> for Mesh {
|
||||||
|
fn from(sector: CircularSegmentMeshBuilder) -> Self {
|
||||||
|
sector.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Meshable for RegularPolygon {
|
impl Meshable for RegularPolygon {
|
||||||
type Output = Mesh;
|
type Output = Mesh;
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
mod dim2;
|
mod dim2;
|
||||||
pub use dim2::{CircleMeshBuilder, EllipseMeshBuilder};
|
pub use dim2::*;
|
||||||
|
|
||||||
mod dim3;
|
mod dim3;
|
||||||
pub use dim3::*;
|
pub use dim3::*;
|
||||||
|
|
|
@ -12,7 +12,7 @@ fn main() {
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
const X_EXTENT: f32 = 600.;
|
const X_EXTENT: f32 = 800.;
|
||||||
|
|
||||||
fn setup(
|
fn setup(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
|
@ -23,6 +23,8 @@ fn setup(
|
||||||
|
|
||||||
let shapes = [
|
let shapes = [
|
||||||
Mesh2dHandle(meshes.add(Circle { radius: 50.0 })),
|
Mesh2dHandle(meshes.add(Circle { radius: 50.0 })),
|
||||||
|
Mesh2dHandle(meshes.add(CircularSector::new(50.0, 1.0))),
|
||||||
|
Mesh2dHandle(meshes.add(CircularSegment::new(50.0, 1.25))),
|
||||||
Mesh2dHandle(meshes.add(Ellipse::new(25.0, 50.0))),
|
Mesh2dHandle(meshes.add(Ellipse::new(25.0, 50.0))),
|
||||||
Mesh2dHandle(meshes.add(Annulus::new(25.0, 50.0))),
|
Mesh2dHandle(meshes.add(Annulus::new(25.0, 50.0))),
|
||||||
Mesh2dHandle(meshes.add(Capsule2d::new(25.0, 50.0))),
|
Mesh2dHandle(meshes.add(Capsule2d::new(25.0, 50.0))),
|
||||||
|
|
124
examples/2d/mesh2d_arcs.rs
Normal file
124
examples/2d/mesh2d_arcs.rs
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
//! Demonstrates UV mappings of the [`CircularSector`] and [`CircularSegment`] primitives.
|
||||||
|
//!
|
||||||
|
//! Also draws the bounding boxes and circles of the primitives.
|
||||||
|
use std::f32::consts::FRAC_PI_2;
|
||||||
|
|
||||||
|
use bevy::{
|
||||||
|
color::palettes::css::{BLUE, DARK_SLATE_GREY, RED},
|
||||||
|
math::bounding::{Bounded2d, BoundingVolume},
|
||||||
|
prelude::*,
|
||||||
|
render::mesh::{CircularMeshUvMode, CircularSectorMeshBuilder, CircularSegmentMeshBuilder},
|
||||||
|
sprite::MaterialMesh2dBundle,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins(DefaultPlugins)
|
||||||
|
.add_systems(Startup, setup)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
draw_bounds::<CircularSector>,
|
||||||
|
draw_bounds::<CircularSegment>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
struct DrawBounds<Shape: Bounded2d + Send + Sync + 'static>(Shape);
|
||||||
|
|
||||||
|
fn setup(
|
||||||
|
mut commands: Commands,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||||
|
) {
|
||||||
|
let material = materials.add(asset_server.load("branding/icon.png"));
|
||||||
|
|
||||||
|
commands.spawn(Camera2dBundle {
|
||||||
|
camera: Camera {
|
||||||
|
clear_color: ClearColorConfig::Custom(DARK_SLATE_GREY.into()),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
|
||||||
|
const UPPER_Y: f32 = 50.0;
|
||||||
|
const LOWER_Y: f32 = -50.0;
|
||||||
|
const FIRST_X: f32 = -450.0;
|
||||||
|
const OFFSET: f32 = 100.0;
|
||||||
|
const NUM_SLICES: i32 = 8;
|
||||||
|
|
||||||
|
// This draws NUM_SLICES copies of the Bevy logo as circular sectors and segments,
|
||||||
|
// with successively larger angles up to a complete circle.
|
||||||
|
for i in 0..NUM_SLICES {
|
||||||
|
let fraction = (i + 1) as f32 / NUM_SLICES as f32;
|
||||||
|
|
||||||
|
let sector = CircularSector::from_turns(40.0, fraction);
|
||||||
|
// We want to rotate the circular sector so that the sectors appear clockwise from north.
|
||||||
|
// We must rotate it both in the Transform and in the mesh's UV mappings.
|
||||||
|
let sector_angle = -sector.half_angle();
|
||||||
|
let sector_mesh =
|
||||||
|
CircularSectorMeshBuilder::new(sector).uv_mode(CircularMeshUvMode::Mask {
|
||||||
|
angle: sector_angle,
|
||||||
|
});
|
||||||
|
commands.spawn((
|
||||||
|
MaterialMesh2dBundle {
|
||||||
|
mesh: meshes.add(sector_mesh).into(),
|
||||||
|
material: material.clone(),
|
||||||
|
transform: Transform {
|
||||||
|
translation: Vec3::new(FIRST_X + OFFSET * i as f32, 2.0 * UPPER_Y, 0.0),
|
||||||
|
rotation: Quat::from_rotation_z(sector_angle),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
DrawBounds(sector),
|
||||||
|
));
|
||||||
|
|
||||||
|
let segment = CircularSegment::from_turns(40.0, fraction);
|
||||||
|
// For the circular segment, we will draw Bevy charging forward, which requires rotating the
|
||||||
|
// shape and texture by 90 degrees.
|
||||||
|
//
|
||||||
|
// Note that this may be unintuitive; it may feel like we should rotate the texture by the
|
||||||
|
// opposite angle to preserve the orientation of Bevy. But the angle is not the angle of the
|
||||||
|
// texture itself, rather it is the angle at which the vertices are mapped onto the texture.
|
||||||
|
// so it is the negative of what you might otherwise expect.
|
||||||
|
let segment_angle = -FRAC_PI_2;
|
||||||
|
let segment_mesh =
|
||||||
|
CircularSegmentMeshBuilder::new(segment).uv_mode(CircularMeshUvMode::Mask {
|
||||||
|
angle: -segment_angle,
|
||||||
|
});
|
||||||
|
commands.spawn((
|
||||||
|
MaterialMesh2dBundle {
|
||||||
|
mesh: meshes.add(segment_mesh).into(),
|
||||||
|
material: material.clone(),
|
||||||
|
transform: Transform {
|
||||||
|
translation: Vec3::new(FIRST_X + OFFSET * i as f32, LOWER_Y, 0.0),
|
||||||
|
rotation: Quat::from_rotation_z(segment_angle),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
DrawBounds(segment),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_bounds<Shape: Bounded2d + Send + Sync + 'static>(
|
||||||
|
q: Query<(&DrawBounds<Shape>, &GlobalTransform)>,
|
||||||
|
mut gizmos: Gizmos,
|
||||||
|
) {
|
||||||
|
for (shape, transform) in &q {
|
||||||
|
let (_, rotation, translation) = transform.to_scale_rotation_translation();
|
||||||
|
let translation = translation.truncate();
|
||||||
|
let rotation = rotation.to_euler(EulerRot::XYZ).2;
|
||||||
|
|
||||||
|
let aabb = shape.0.aabb_2d(translation, rotation);
|
||||||
|
gizmos.rect_2d(aabb.center(), 0.0, aabb.half_size() * 2.0, RED);
|
||||||
|
|
||||||
|
let bounding_circle = shape.0.bounding_circle(translation, rotation);
|
||||||
|
gizmos.circle_2d(bounding_circle.center, bounding_circle.radius(), BLUE);
|
||||||
|
}
|
||||||
|
}
|
|
@ -102,6 +102,7 @@ Example | Description
|
||||||
[2D Shapes](../examples/2d/2d_shapes.rs) | Renders simple 2D primitive shapes like circles and polygons
|
[2D Shapes](../examples/2d/2d_shapes.rs) | Renders simple 2D primitive shapes like circles and polygons
|
||||||
[2D Viewport To World](../examples/2d/2d_viewport_to_world.rs) | Demonstrates how to use the `Camera::viewport_to_world_2d` method
|
[2D Viewport To World](../examples/2d/2d_viewport_to_world.rs) | Demonstrates how to use the `Camera::viewport_to_world_2d` method
|
||||||
[2D Wireframe](../examples/2d/wireframe_2d.rs) | Showcases wireframes for 2d meshes
|
[2D Wireframe](../examples/2d/wireframe_2d.rs) | Showcases wireframes for 2d meshes
|
||||||
|
[Arc 2D Meshes](../examples/2d/mesh2d_arcs.rs) | Demonstrates UV-mapping of the circular segment and sector primitives
|
||||||
[Custom glTF vertex attribute 2D](../examples/2d/custom_gltf_vertex_attribute.rs) | Renders a glTF mesh in 2D with a custom vertex attribute
|
[Custom glTF vertex attribute 2D](../examples/2d/custom_gltf_vertex_attribute.rs) | Renders a glTF mesh in 2D with a custom vertex attribute
|
||||||
[Manual Mesh 2D](../examples/2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis
|
[Manual Mesh 2D](../examples/2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis
|
||||||
[Mesh 2D](../examples/2d/mesh2d.rs) | Renders a 2d mesh
|
[Mesh 2D](../examples/2d/mesh2d.rs) | Renders a 2d mesh
|
||||||
|
|
Loading…
Reference in a new issue