use std::fmt;
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
pub const EMISSION_DECIMAL_PLACES: u32 = 4;
const EMISSION_FACTOR: f64 = 10_000.0;
const PT_PER_MM: f64 = 72.0 / 25.4;
pub const DEFAULT_SCREEN_DPI: f64 = 96.0;
#[derive(Clone, Copy, PartialEq, PartialOrd, Default)]
pub struct Pt(f64);
impl Pt {
pub const ZERO: Self = Self(0.0);
#[must_use]
pub const fn new(value: f64) -> Self {
Self(value)
}
#[must_use]
pub fn from_mm(mm: f64) -> Self {
Self(mm * PT_PER_MM)
}
#[must_use]
pub fn from_px(px: f64, dpi: f64) -> Self {
assert!(dpi > 0.0, "DPI must be positive, got {dpi}");
Self(px * (72.0 / dpi))
}
#[must_use]
pub fn from_px_default(px: f64) -> Self {
Self::from_px(px, DEFAULT_SCREEN_DPI)
}
#[must_use]
pub const fn get(self) -> f64 {
self.0
}
#[must_use]
pub fn round_for_emission(self) -> Self {
Self((self.0 * EMISSION_FACTOR).round() / EMISSION_FACTOR)
}
#[must_use]
pub fn to_mm(self) -> f64 {
self.0 / PT_PER_MM
}
#[must_use]
pub fn to_px(self, dpi: f64) -> f64 {
self.0 * (dpi / 72.0)
}
#[must_use]
pub fn abs(self) -> Self {
Self(self.0.abs())
}
#[must_use]
pub fn min(self, other: Self) -> Self {
Self(self.0.min(other.0))
}
#[must_use]
pub fn max(self, other: Self) -> Self {
Self(self.0.max(other.0))
}
#[must_use]
pub fn clamp(self, min: Self, max: Self) -> Self {
Self(self.0.clamp(min.0, max.0))
}
#[must_use]
pub fn is_finite(self) -> bool {
self.0.is_finite()
}
}
impl fmt::Debug for Pt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:.4}pt", self.0)
}
}
impl fmt::Display for Pt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:.4}pt", self.0)
}
}
impl Add for Pt {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self(self.0 + rhs.0)
}
}
impl AddAssign for Pt {
fn add_assign(&mut self, rhs: Self) {
self.0 += rhs.0;
}
}
impl Sub for Pt {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self(self.0 - rhs.0)
}
}
impl SubAssign for Pt {
fn sub_assign(&mut self, rhs: Self) {
self.0 -= rhs.0;
}
}
impl Mul<f64> for Pt {
type Output = Self;
fn mul(self, rhs: f64) -> Self {
Self(self.0 * rhs)
}
}
impl Mul<Pt> for f64 {
type Output = Pt;
fn mul(self, rhs: Pt) -> Pt {
Pt(self * rhs.0)
}
}
impl MulAssign<f64> for Pt {
fn mul_assign(&mut self, rhs: f64) {
self.0 *= rhs;
}
}
impl Div<f64> for Pt {
type Output = Self;
fn div(self, rhs: f64) -> Self {
Self(self.0 / rhs)
}
}
impl Div<Pt> for Pt {
type Output = f64;
fn div(self, rhs: Pt) -> f64 {
self.0 / rhs.0
}
}
impl DivAssign<f64> for Pt {
fn div_assign(&mut self, rhs: f64) {
self.0 /= rhs;
}
}
impl Neg for Pt {
type Output = Self;
fn neg(self) -> Self {
Self(-self.0)
}
}
impl From<f64> for Pt {
fn from(value: f64) -> Self {
Self(value)
}
}
impl From<Pt> for f64 {
fn from(pt: Pt) -> Self {
pt.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mm_to_pt_conversion() {
let pt = Pt::from_mm(25.4);
assert!((pt.get() - 72.0).abs() < 1e-10);
}
#[test]
fn px_to_pt_at_96dpi() {
let pt = Pt::from_px(96.0, 96.0);
assert!((pt.get() - 72.0).abs() < 1e-10);
}
#[test]
fn px_to_pt_at_72dpi() {
let pt = Pt::from_px(72.0, 72.0);
assert!((pt.get() - 72.0).abs() < 1e-10);
}
#[test]
fn px_default_uses_96dpi() {
assert_eq!(Pt::from_px_default(100.0), Pt::from_px(100.0, 96.0));
}
#[test]
fn round_for_emission_precision() {
assert_eq!(Pt::new(12.345_678_9).round_for_emission().get(), 12.3457);
}
#[test]
fn round_for_emission_exact_values_unchanged() {
assert_eq!(Pt::new(12.0).round_for_emission().get(), 12.0);
}
#[test]
fn pt_arithmetic() {
let a = Pt::new(10.0);
let b = Pt::new(3.0);
assert_eq!((a + b).get(), 13.0);
assert_eq!((a - b).get(), 7.0);
assert_eq!((a * 2.0).get(), 20.0);
assert_eq!((2.0 * a).get(), 20.0);
assert_eq!((a / 2.0).get(), 5.0);
assert_eq!(a / b, 10.0 / 3.0);
assert_eq!((-a).get(), -10.0);
}
#[test]
fn pt_to_mm_roundtrip() {
let mm = 210.0;
assert!((Pt::from_mm(mm).to_mm() - mm).abs() < 1e-10);
}
#[test]
fn pt_min_max_clamp() {
let a = Pt::new(5.0);
let b = Pt::new(10.0);
assert_eq!(a.min(b), a);
assert_eq!(a.max(b), b);
assert_eq!(Pt::new(15.0).clamp(a, b), b);
assert_eq!(Pt::new(1.0).clamp(a, b), a);
}
#[test]
#[should_panic(expected = "DPI must be positive")]
fn px_zero_dpi_panics() {
let _ = Pt::from_px(100.0, 0.0);
}
#[test]
fn display_format() {
assert_eq!(format!("{}", Pt::new(12.5)), "12.5000pt");
}
}