Skip to content

Commit

Permalink
refactor: reorganize geometry
Browse files Browse the repository at this point in the history
- separate modules for 3D anlge and signed angle
- move cartesian logic into intern/geom/cartesian.rs
- keep the Cartesian struct itself in public/keypoints.rs
  • Loading branch information
jakmeier committed Dec 8, 2023
1 parent 195f839 commit 9d79cb3
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 273 deletions.
176 changes: 5 additions & 171 deletions bouncy_instructor/src/intern/geom.rs
Original file line number Diff line number Diff line change
@@ -1,174 +1,8 @@
//! Geometry primitives.

use std::f32::consts::{PI, TAU};
pub(crate) use angle3d::Angle3d;
pub(crate) use signed_angle::SignedAngle;

/// A direction in 3D space.
#[derive(Clone, Copy, PartialEq, Debug)]
pub(crate) struct Angle3d {
/// angle to z-axis, -PI to PI
pub azimuth: SignedAngle,
/// angle to y axis, -PI to PI
pub polar: SignedAngle,
}

/// Represents angles from -PI (exclusive) to PI (inclusive)
#[derive(Clone, Copy, PartialEq)]
pub(crate) struct SignedAngle(pub(crate) f32);

impl Angle3d {
pub(crate) fn new(azimuth: SignedAngle, polar: SignedAngle) -> Self {
Self { azimuth, polar }
}

pub(crate) const ZERO: Self = Angle3d {
azimuth: SignedAngle::ZERO,
polar: SignedAngle::ZERO,
};

#[allow(dead_code)]
pub(crate) fn degree(azimuth: f32, polar: f32) -> Self {
Self {
azimuth: SignedAngle::degree(azimuth),
polar: SignedAngle::degree(polar),
}
}

#[allow(dead_code)]
pub(crate) fn radian(azimuth: f32, polar: f32) -> Self {
Self {
azimuth: SignedAngle::radian(azimuth),
polar: SignedAngle::radian(polar),
}
}

/// Distance in a sphere with r = 0.5, result is in [0.0,1.0]
pub(crate) fn distance(&self, other: &Self) -> f32 {
let a = self.polar.sin() * other.polar.sin() * (self.azimuth - other.azimuth).cos();
let b = self.polar.cos() * other.polar.cos();
// Distance in unit sphere
let dist = (2.0 - 2.0 * (a + b)).sqrt();
dist / 2.0
}

/// Mirrors left/right, doesn't affect up/down or forward/backward
pub(crate) fn x_mirror(&self) -> Self {
Self {
azimuth: self.azimuth.mirror(),
polar: self.polar,
}
}
}

impl SignedAngle {
pub(crate) const ZERO: Self = SignedAngle(0.0);

pub(crate) fn degree(alpha: f32) -> Self {
Self(alpha.to_radians()).ensure_signed()
}

pub(crate) fn as_degree(&self) -> f32 {
self.0.to_degrees()
}

/// Returns a copy of the angle where values are guaranteed to be in (-PI and PI]
#[inline]
fn ensure_signed(mut self) -> Self {
self.0 = self.0 % TAU;
// maybe branching here is bad for performance?
// no performance testing was done so far
if self.0 > PI {
self.0 -= TAU;
} else if self.0 <= -PI {
self.0 += TAU;
}
self
}

pub(crate) fn abs(mut self) -> Self {
self.0 = self.0.abs();
self
}

pub(crate) fn radian(alpha: f32) -> Self {
Self(alpha).ensure_signed()
}

fn mirror(self) -> SignedAngle {
Self(-*self).ensure_signed()
}
}

impl std::ops::Deref for SignedAngle {
type Target = f32;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl std::ops::Add for SignedAngle {
type Output = Self;

fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0).ensure_signed()
}
}

impl std::ops::Sub for SignedAngle {
type Output = Self;

fn sub(self, rhs: Self) -> Self::Output {
Self(self.0 - rhs.0).ensure_signed()
}
}

impl std::fmt::Debug for SignedAngle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let alpha = self.as_degree();
write!(f, "{alpha:.2}°")
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;

use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4};

/// Tests `SignedAngle::degree`
///
/// The inputs of are in ° in [f32::MIN, f32::MAX]
/// The internal representation must be in radians in (-PI, +PI].
#[test]
fn test_angle_degree_to_radian() {
assert_float_angle_eq(0.0, SignedAngle::degree(0.0));
assert_float_angle_eq(FRAC_PI_4, SignedAngle::degree(45.0));
assert_float_angle_eq(-FRAC_PI_4, SignedAngle::degree(-45.0));
assert_float_angle_eq(-FRAC_PI_4, SignedAngle::degree(315.0));
assert_float_angle_eq(PI, SignedAngle::degree(-180.0));
assert_float_angle_eq(PI, SignedAngle::degree(180.0));
}

#[test]
fn test_mirror_signed_angle() {
assert_eq!(SignedAngle::ZERO, SignedAngle::ZERO.mirror());
assert_eq!(SignedAngle(PI), SignedAngle(PI).mirror());
assert_angle_eq(
SignedAngle(FRAC_PI_2),
SignedAngle(3.0 * FRAC_PI_2).mirror(),
);
assert_angle_eq(
SignedAngle::degree(60.0),
SignedAngle::degree(300.0).mirror(),
);
assert_angle_eq(
SignedAngle(FRAC_PI_3),
SignedAngle(FRAC_PI_3).mirror().mirror(),
);
assert_angle_eq(
SignedAngle(FRAC_PI_4),
SignedAngle(FRAC_PI_4).mirror().mirror(),
);
}
}
mod angle3d;
mod signed_angle;
mod cartesian;
54 changes: 54 additions & 0 deletions bouncy_instructor/src/intern/geom/angle3d.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use super::SignedAngle;

/// A direction in 3D space.
#[derive(Clone, Copy, PartialEq, Debug)]
pub(crate) struct Angle3d {
/// angle to z-axis, -PI to PI
pub azimuth: SignedAngle,
/// angle to y axis, -PI to PI
pub polar: SignedAngle,
}

impl Angle3d {
pub(crate) fn new(azimuth: SignedAngle, polar: SignedAngle) -> Self {
Self { azimuth, polar }
}

pub(crate) const ZERO: Self = Angle3d {
azimuth: SignedAngle::ZERO,
polar: SignedAngle::ZERO,
};

#[allow(dead_code)]
pub(crate) fn degree(azimuth: f32, polar: f32) -> Self {
Self {
azimuth: SignedAngle::degree(azimuth),
polar: SignedAngle::degree(polar),
}
}

#[allow(dead_code)]
pub(crate) fn radian(azimuth: f32, polar: f32) -> Self {
Self {
azimuth: SignedAngle::radian(azimuth),
polar: SignedAngle::radian(polar),
}
}

/// Distance in a sphere with r = 0.5, result is in [0.0,1.0]
pub(crate) fn distance(&self, other: &Self) -> f32 {
let a = self.polar.sin() * other.polar.sin() * (self.azimuth - other.azimuth).cos();
let b = self.polar.cos() * other.polar.cos();
// Distance in unit sphere
let dist = (2.0 - 2.0 * (a + b)).sqrt();
dist / 2.0
}

/// Mirrors left/right, doesn't affect up/down or forward/backward
pub(crate) fn x_mirror(&self) -> Self {
Self {
azimuth: self.azimuth.mirror(),
polar: self.polar,
}
}
}
103 changes: 103 additions & 0 deletions bouncy_instructor/src/intern/geom/cartesian.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use super::SignedAngle;
use crate::keypoints::Cartesian3d;

impl Cartesian3d {
/// The polar angle is measured against the y-axis, which goes from the
/// ground to the sky.
///
/// The polar angle is between 0° and +180°, with 0° pointing to
/// the ground, 180° to the sky.
///
/// Returned values are in radian, hence [0, PI]
pub(crate) fn polar_angle(&self, other: Cartesian3d) -> SignedAngle {
// only the sign of dy matters here, and we want it to grow down when we
// use acos to compute the polar angle
let dx = other.x - self.x;
let dy = other.y - self.y;
let dz = other.z - self.z;

let r = (dx.powi(2) + dy.powi(2) + dz.powi(2)).sqrt();
if !r.is_normal() {
// Handle vectors of lengths very close to zero, NaN, or infinity.
// Returning 0° is as good as any other angle.
return SignedAngle(0.0);
}
// note: potentially this could be computed more efficiently
// note 2: what about Math.acos() instead of wasm ?
SignedAngle::radian((dy / r).acos())
}

/// The azimuth is the clock-wise angle to the negative z-axis.
///
/// The azimuth is between -180° and 180°. Someone facing the camera has an
/// azimuth of 0°, which is also known as north.
///
/// Returned values are in radian, (-PI to PI].
///
/// Just like in cartography, east is +90° (PI/2) and west is -90° (-PI/2)
/// for the dancer. However, in videos, the angles are therefore
/// counter-clock-wise as seen by the camera.
///
/// Note that in the keypoint coordinate system, the x-axis grows to the
/// right. In a (non-mirrored) video this means we see the left arm of the
/// dance (west) in the positive x-direction. Which is the opposite of how
/// angles grow in our spherical coordinates. Also confusing, the positive
/// z-axis faces south, not north.
pub(crate) fn azimuth(&self, other: Cartesian3d) -> SignedAngle {
// usually you should expect other - self, but we need to flip both signs
let dz = self.z - other.z;
let dx = self.x - other.x;
let r = dx.hypot(dz);
if !r.is_normal() {
// Handle vectors of lengths very close to zero, NaN, or infinity.
// Returning 0° is as good as any other angle.
return SignedAngle(0.0);
}
// note: potentially this could be computed more efficiently, esp. the sign
// note 2: what about Math.acos() instead of wasm ?
SignedAngle(dx.signum() * (dz / r).acos())
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::assert_angle_eq;

#[test]
fn test_cartesian_to_angle() {
// input, azimuth, polar

// in keypoint coordinates, the negative x direction is the right hand
// of the dancer, which is the positive angle direction
check_cartesian_to_angle(Cartesian3d::new(1.0, 0.0, 0.0), -90.0, 90.0);
check_cartesian_to_angle(Cartesian3d::new(-1.0, 0.0, 0.0), 90.0, 90.0);

// down is 0° (and camera y grows down)
check_cartesian_to_angle(Cartesian3d::new(0.0, 1.0, 0.0), 0.0, 0.0);
// up is 180°
check_cartesian_to_angle(Cartesian3d::new(0.0, -1.0, 0.0), 0.0, 180.0);

// away from the camera means south => azimuth = 180°
check_cartesian_to_angle(Cartesian3d::new(0.0, 0.0, 1.0), 180.0, 90.0);
// to the camera means north => azimuth = 0
check_cartesian_to_angle(Cartesian3d::new(0.0, 0.0, -1.0), 0.0, 90.0);
}

#[track_caller]
fn check_cartesian_to_angle(
cartesian: Cartesian3d,
expected_azimuth: f32,
expected_polar: f32,
) {
let origin = Cartesian3d::new(0.0, 0.0, 0.0);
assert_angle_eq(
SignedAngle::degree(expected_azimuth),
origin.azimuth(cartesian),
);
assert_angle_eq(
SignedAngle::degree(expected_polar),
origin.polar_angle(cartesian),
);
}
}
Loading

0 comments on commit 9d79cb3

Please sign in to comment.