diff --git a/all-is-cubes-base/src/math/color.rs b/all-is-cubes-base/src/math/color.rs index bcd4c76a5..cf68cc639 100644 --- a/all-is-cubes-base/src/math/color.rs +++ b/all-is-cubes-base/src/math/color.rs @@ -2,17 +2,17 @@ use core::fmt; use core::iter::Sum; -use core::ops::{Add, AddAssign, Mul, Sub}; +use core::ops::{Add, AddAssign, Mul}; use euclid::{vec3, Vector3D}; -use ordered_float::{FloatIsNan, NotNan}; +use ordered_float::NotNan; /// Acts as polyfill for float methods #[cfg(not(feature = "std"))] #[allow(unused_imports)] use num_traits::float::Float as _; -use crate::notnan; +use crate::math::{NotPositiveSign, PositiveSign}; /// Allows writing a constant [`Rgb`] color value, provided that its components are float /// literals. @@ -21,11 +21,14 @@ use crate::notnan; #[macro_export] macro_rules! rgb_const { ($r:literal, $g:literal, $b:literal) => { - $crate::math::Rgb::new_nn( - $crate::notnan!($r), - $crate::notnan!($g), - $crate::notnan!($b), - ) + // const block ensures all panics are compile-time + const { + $crate::math::Rgb::new_ps( + $crate::math::PositiveSign::::new_strict($r), + $crate::math::PositiveSign::::new_strict($g), + $crate::math::PositiveSign::::new_strict($b), + ) + } }; } @@ -34,43 +37,44 @@ macro_rules! rgb_const { #[macro_export] macro_rules! rgba_const { ($r:literal, $g:literal, $b:literal, $a:literal) => { - $crate::math::Rgba::new_nn( - $crate::notnan!($r), - $crate::notnan!($g), - $crate::notnan!($b), - $crate::notnan!($a), - ) + // const block ensures all panics are compile-time + const { + $crate::math::Rgba::new_ps( + $crate::math::PositiveSign::::new_strict($r), + $crate::math::PositiveSign::::new_strict($g), + $crate::math::PositiveSign::::new_strict($b), + $crate::math::PositiveSign::::new_strict($a), + ) + } }; } /// A floating-point RGB color value. /// -/// * Each component may be considered to have a nominal range of 0 to 1, but larger -/// values are permitted — corresponding to bright light sources and other such -/// things which it is reasonable to “overexpose”. (No meaning is given to negative -/// values, but they are permitted.) -/// * NaN is banned so that [`Eq`] may be implemented. (Infinities are permitted.) -/// * Color values are linear (gamma = 1), but use the same RGB primaries as sRGB +/// * Each color component must have a nonnegative, non-NaN value. +/// Depending on the application, they may be considered to have a nominal +/// range of 0 to 1, or unbounded. +/// * Color components are linear (gamma = 1), but use the same RGB primaries as sRGB /// (Rec. 709). #[derive(Clone, Copy, Eq, Hash, PartialEq)] -pub struct Rgb(Vector3D, Intensity>); +pub struct Rgb(Vector3D, Intensity>); /// A floating-point RGBA color value. /// -/// * Each color component may be considered to have a nominal range of 0 to 1, but -/// larger values are permitted — corresponding to bright light sources and other such -/// things which it is reasonable to “overexpose”. (No meaning is given to negative -/// values, but they are permitted.) -/// * NaN is banned so that [`Eq`] may be implemented. (Infinities are permitted.) -/// * Color values are linear (gamma = 1), but use the same RGB primaries as sRGB +/// * Each color component must have a nonnegative, non-NaN value. +/// Depending on the application, they may be considered to have a nominal +/// range of 0 to 1, or unbounded. +/// * The alpha must have a non-NaN value. +/// * Color components are linear (gamma = 1), but use the same RGB primaries as sRGB /// (Rec. 709). /// * The alpha is not premultiplied. -/// * Alpha values less than zero and greater than one will be treated equivalently to +/// * Alpha values less than zero and greater than one will usually be treated equivalently to /// zero and one, respectively, but are preserved rather than clipped. +/// (TODO: Constrain it to the 0-1 range to reduce hazards.) #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct Rgba { rgb: Rgb, - alpha: NotNan, + alpha: PositiveSign, } /// Unit-of-measure type for vectors that contain color channels. @@ -80,17 +84,17 @@ pub struct Rgba { #[derive(Debug, Eq, PartialEq)] pub enum Intensity {} -// NotNan::zero() and one() exist, but only via traits, which can't be used in const -const NN0: NotNan = notnan!(0.0); -const NN1: NotNan = notnan!(1.0); +// convenience alias +const PS0: PositiveSign = as num_traits::ConstZero>::ZERO; +const PS1: PositiveSign = as num_traits::ConstOne>::ONE; impl Rgb { /// Black; the constant equal to `Rgb::new(0., 0., 0.).unwrap()`. - pub const ZERO: Rgb = Rgb(vec3(NN0, NN0, NN0)); + pub const ZERO: Rgb = Rgb(vec3(PS0, PS0, PS0)); /// Nominal white; the constant equal to `Rgb::new(1., 1., 1.).unwrap()`. /// /// Note that brighter values may exist; the color system “supports HDR”. - pub const ONE: Rgb = Rgb(vec3(NN1, NN1, NN1)); + pub const ONE: Rgb = Rgb(vec3(PS1, PS1, PS1)); /// Pure red that is as bright as it can be, /// while being a sRGB color that is the same luminance as the other colors in this set. @@ -103,22 +107,24 @@ impl Rgb { /// (That turns out to be 100% blue, `#0000FF`.) pub const UNIFORM_LUMINANCE_BLUE: Rgb = Rgb::from_srgb8([0x00, 0x00, 0xFF]); - /// Constructs a color from components. Panics if any component is NaN. - /// No other range checks are performed. + /// Constructs a color from components. + /// + /// Panics if any component is NaN. + /// Clamps any component that is negative. #[inline] #[track_caller] pub const fn new(r: f32, g: f32, b: f32) -> Self { match Self::try_new(vec3(r, g, b)) { Ok(color) => color, - Err(_) => panic!("color components may not be NaN"), + Err(_) => panic!("color component out of range"), } } - const fn try_new(value: Vector3D) -> Result { + const fn try_new(value: Vector3D) -> Result> { match ( - new_nn_f32(value.x), - new_nn_f32(value.y), - new_nn_f32(value.z), + PositiveSign::::try_new(value.x), + PositiveSign::::try_new(value.y), + PositiveSign::::try_new(value.z), ) { (Ok(r), Ok(g), Ok(b)) => Ok(Self(vec3(r, g, b))), (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(e), @@ -126,12 +132,12 @@ impl Rgb { } /// Constructs a color from components that have already been checked for not being - /// NaN. + /// NaN or negative. /// /// Note: This exists primarily to assist the [`rgb_const!`] macro and may be renamed /// or replaced in future versions. #[inline] - pub const fn new_nn(r: NotNan, g: NotNan, b: NotNan) -> Self { + pub const fn new_ps(r: PositiveSign, g: PositiveSign, b: PositiveSign) -> Self { Self(vec3(r, g, b)) } @@ -145,13 +151,13 @@ impl Rgb { /// Adds an alpha component to produce an [Rgba] color. #[inline] - pub const fn with_alpha(self, alpha: NotNan) -> Rgba { + pub const fn with_alpha(self, alpha: PositiveSign) -> Rgba { Rgba { rgb: self, alpha } } /// Adds an alpha component of `1.0` (fully opaque) to produce an [Rgba] color. #[inline] pub const fn with_alpha_one(self) -> Rgba { - self.with_alpha(NN1) + self.with_alpha(PS1) } /// Adds an alpha component of `1.0` (fully opaque) to produce an [Rgba] color. @@ -160,22 +166,22 @@ impl Rgb { #[inline] #[must_use] pub const fn with_alpha_one_if_has_no_alpha(self) -> Rgba { - self.with_alpha(NN1) + self.with_alpha(PS1) } /// Returns the red color component. Values are linear (gamma = 1). #[inline] - pub const fn red(self) -> NotNan { + pub const fn red(self) -> PositiveSign { self.0.x } /// Returns the green color component. Values are linear (gamma = 1). #[inline] - pub const fn green(self) -> NotNan { + pub const fn green(self) -> PositiveSign { self.0.y } /// Returns the blue color component. Values are linear (gamma = 1). #[inline] - pub const fn blue(self) -> NotNan { + pub const fn blue(self) -> PositiveSign { self.0.z } @@ -223,39 +229,52 @@ impl Rgb { #[inline] #[must_use] pub fn clamp(self) -> Self { - Self(self.0.map(|c| c.clamp(NN0, NN1))) + Self(self.0.map(|c| c.clamp(PS0, PS1))) + } + + /// Subtract `other` from `self`; if any component would be negative, it is zero instead. + #[inline] + #[must_use] + pub fn saturating_sub(self, other: Self) -> Self { + Self(vec3( + self.red().saturating_sub(other.red()), + self.green().saturating_sub(other.green()), + self.blue().saturating_sub(other.blue()), + )) } } impl Rgba { /// Transparent black (all components zero); identical to /// `Rgba::new(0.0, 0.0, 0.0, 0.0)` except for being a constant. - pub const TRANSPARENT: Rgba = Rgb::ZERO.with_alpha(NN0); + pub const TRANSPARENT: Rgba = Rgb::ZERO.with_alpha(PS0); /// Black; identical to `Rgba::new(0.0, 0.0, 0.0, 1.0)` except for being a constant. pub const BLACK: Rgba = Rgb::ZERO.with_alpha_one(); /// White; identical to `Rgba::new(1.0, 1.0, 1.0, 1.0)` except for being a constant. pub const WHITE: Rgba = Rgb::ONE.with_alpha_one(); - /// Constructs a color from components. Panics if any component is NaN. + /// Constructs a color from components. Panics if any component is NaN or negative. /// No other range checks are performed. #[inline] #[track_caller] pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self { - match new_nn_f32(a) { - Ok(a) => Rgb::new(r, g, b).with_alpha(a), - Err(_) => panic!("alpha may not be NaN"), - } + Rgb::new(r, g, b).with_alpha(PositiveSign::::new_strict(a)) } /// Constructs a color from components that have already been checked for not being - /// NaN. + /// NaN or negative. /// - /// Note: This exists primarily to assist the [`rgb_const!`] macro and may be renamed + /// Note: This exists primarily to assist the [`rgba_const!`] macro and may be renamed /// or replaced in future versions. #[inline] - pub const fn new_nn(r: NotNan, g: NotNan, b: NotNan, a: NotNan) -> Self { + pub const fn new_ps( + r: PositiveSign, + g: PositiveSign, + b: PositiveSign, + alpha: PositiveSign, + ) -> Self { Self { - rgb: Rgb::new_nn(r, g, b), - alpha: a, + rgb: Rgb::new_ps(r, g, b), + alpha, } } @@ -278,17 +297,17 @@ impl Rgba { /// Returns the red color component. Values are linear (gamma = 1) and not premultiplied. #[inline] - pub const fn red(self) -> NotNan { + pub const fn red(self) -> PositiveSign { self.rgb.red() } /// Returns the green color component. Values are linear (gamma = 1) and not premultiplied. #[inline] - pub const fn green(self) -> NotNan { + pub const fn green(self) -> PositiveSign { self.rgb.green() } /// Returns the blue color component. Values are linear (gamma = 1) and not premultiplied. #[inline] - pub const fn blue(self) -> NotNan { + pub const fn blue(self) -> PositiveSign { self.rgb.blue() } /// Returns the alpha component. @@ -297,7 +316,7 @@ impl Rgba { /// allowed and may be returned by this method, but alpha test methods will treat // them equivalently to zero and one. #[inline] - pub const fn alpha(self) -> NotNan { + pub const fn alpha(self) -> PositiveSign { self.alpha } @@ -305,13 +324,13 @@ impl Rgba { /// zero or less. #[inline] pub fn fully_transparent(self) -> bool { - self.alpha() <= NN0 + self.alpha() <= PS0 } /// Returns whether this color is fully opaque, or has an alpha component of /// one or greater. #[inline] pub fn fully_opaque(self) -> bool { - self.alpha() >= NN1 + self.alpha() >= PS1 } /// Returns the [`OpacityCategory`] which this color's alpha fits into. /// This returns the same information as [`Rgba::fully_transparent`] combined with @@ -379,7 +398,7 @@ impl Rgba { /// Converts sRGB 8-bits-per-component color to the corresponding linear [`Rgba`] value. #[inline] pub const fn from_srgb8(rgba: [u8; 4]) -> Self { - Self::new_nn( + Self::new_ps( component_from_srgb8_const(rgba[0]), component_from_srgb8_const(rgba[1]), component_from_srgb8_const(rgba[2]), @@ -393,7 +412,7 @@ impl Rgba { pub fn clamp(self) -> Self { Self { rgb: self.rgb.clamp(), - alpha: self.alpha.clamp(NN0, NN1), + alpha: self.alpha.min(PS1), } } @@ -409,26 +428,26 @@ impl Rgba { pub fn reflect(self, illumination: Rgb) -> Rgb { // TODO: do this math without any NaN checks or negative/amplified values. // by introducing a dedicated RgbaReflectance type with constrained components? - self.to_rgb() * illumination * self.alpha + self.to_rgb() * illumination * PositiveSign::::new_clamped(self.alpha.into_inner()) } } -impl From, Intensity>> for Rgb { +impl From, Intensity>> for Rgb { #[inline] - fn from(value: Vector3D, Intensity>) -> Self { + fn from(value: Vector3D, Intensity>) -> Self { Self(value) } } -impl From<[NotNan; 3]> for Rgb { +impl From<[PositiveSign; 3]> for Rgb { #[inline] - fn from(value: [NotNan; 3]) -> Self { + fn from(value: [PositiveSign; 3]) -> Self { Self(value.into()) } } -impl From<[NotNan; 4]> for Rgba { +impl From<[PositiveSign; 4]> for Rgba { #[inline] - fn from(value: [NotNan; 4]) -> Self { + fn from(value: [PositiveSign; 4]) -> Self { let [r, g, b, alpha] = value; Self { rgb: Rgb::from([r, g, b]), @@ -440,28 +459,42 @@ impl From<[NotNan; 4]> for Rgba { impl From for Vector3D { #[inline] fn from(value: Rgb) -> Self { - value.0.map(NotNan::into_inner) + value.0.map(PositiveSign::::into_inner) } } -impl From for [NotNan; 3] { +impl From for [PositiveSign; 3] { #[inline] fn from(value: Rgb) -> Self { value.0.into() } } +impl From for [PositiveSign; 4] { + #[inline] + fn from(value: Rgba) -> Self { + let [r, g, b]: [PositiveSign; 3] = value.rgb.into(); + [r, g, b, value.alpha] + } +} + +impl From for [NotNan; 3] { + #[inline] + fn from(value: Rgb) -> Self { + value.0.map(PositiveSign::into).into() + } +} impl From for [NotNan; 4] { #[inline] fn from(value: Rgba) -> Self { let [r, g, b]: [NotNan; 3] = value.rgb.into(); - [r, g, b, value.alpha] + [r, g, b, value.alpha.into()] } } impl From for [f32; 3] { #[inline] fn from(value: Rgb) -> Self { - value.0.map(NotNan::into_inner).into() + value.0.map(PositiveSign::::into_inner).into() } } impl From for [f32; 4] { @@ -472,7 +505,7 @@ impl From for [f32; 4] { } impl TryFrom> for Rgb { - type Error = FloatIsNan; + type Error = NotPositiveSign; #[inline] fn try_from(value: Vector3D) -> Result { Ok(Self(vec3( @@ -496,13 +529,6 @@ impl AddAssign for Rgb { self.0 += other.0; } } -impl Sub for Rgb { - type Output = Self; - #[inline] - fn sub(self, other: Self) -> Self { - Self(self.0 - other.0) - } -} /// Multiplies two color values componentwise. impl Mul for Rgb { type Output = Self; @@ -513,21 +539,26 @@ impl Mul for Rgb { } } /// Multiplies this color value by a scalar. -impl Mul> for Rgb { +impl Mul> for Rgb { type Output = Self; /// Multiplies this color value by a scalar. #[inline] - fn mul(self, scalar: NotNan) -> Self { + fn mul(self, scalar: PositiveSign) -> Self { Self(self.0 * scalar) } } -/// Multiplies this color value by a scalar. Panics if the scalar is NaN. +/// Multiplies this color value by a scalar. +/// +/// Panics if the scalar is NaN. Returns zero if the scalar is negative. +// TODO: consider removing this panic risk impl Mul for Rgb { type Output = Self; - /// Multiplies this color value by a scalar. Panics if the scalar is NaN. + /// Multiplies this color value by a scalar. + /// + /// Panics if the scalar is NaN. Returns zero if the scalar is negative. #[inline] fn mul(self, scalar: f32) -> Self { - Self(self.0 * NotNan::new(scalar).unwrap()) + Self(self.0 * PositiveSign::::new_clamped(scalar)) } } @@ -574,7 +605,7 @@ impl fmt::Debug for Rgba { #[allow(clippy::missing_inline_in_public_items)] impl<'a> arbitrary::Arbitrary<'a> for Rgb { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - Ok(Rgb::new_nn(u.arbitrary()?, u.arbitrary()?, u.arbitrary()?)) + Ok(Rgb::new_ps(u.arbitrary()?, u.arbitrary()?, u.arbitrary()?)) } fn size_hint(depth: usize) -> (usize, Option) { @@ -586,7 +617,7 @@ impl<'a> arbitrary::Arbitrary<'a> for Rgb { #[allow(clippy::missing_inline_in_public_items)] impl<'a> arbitrary::Arbitrary<'a> for Rgba { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - Ok(Rgba::new_nn( + Ok(Rgba::new_ps( u.arbitrary()?, u.arbitrary()?, u.arbitrary()?, @@ -640,10 +671,11 @@ mod rerun { } } +/// Apply the sRGB encoding function. Do not use this on alpha values. #[inline] -fn component_to_srgb(c: NotNan) -> f32 { +fn component_to_srgb(c: PositiveSign) -> f32 { // Source: (version as of Feb 3, 2020) - // Strip NotNan + // Strip wrapper let c = c.into_inner(); // Apply sRGB gamma curve if c <= 0.0031308 { @@ -654,45 +686,45 @@ fn component_to_srgb(c: NotNan) -> f32 { } #[inline] -fn component_to_srgb8(c: NotNan) -> u8 { +fn component_to_srgb8(c: PositiveSign) -> u8 { + // out of range values will be clamped by `as u8` (component_to_srgb(c) * 255.).round() as u8 } #[cfg(test)] // only used to validate the lookup tables -fn component_from_linear8_arithmetic(c: u8) -> NotNan { +fn component_from_linear8_arithmetic(c: u8) -> f32 { // TODO: make this const when Rust `const_fn_floating_point_arithmetic` is stable, // and we can do away with the lookup tables. - NotNan::from(c) / NotNan::from(255u8) + f32::from(c) / 255.0 } #[inline] -const fn component_from_linear8_const(c: u8) -> NotNan { - // Safety: the table may be inspected to contain no NaNs. - unsafe { NotNan::new_unchecked(CONST_LINEAR_LOOKUP_TABLE[c as usize]) } +const fn component_from_linear8_const(c: u8) -> PositiveSign { + // Safety: the table may be inspected to contain no NaNs or negatives. + unsafe { PositiveSign::new_unchecked(CONST_LINEAR_LOOKUP_TABLE[c as usize]) } } /// Implements sRGB decoding using the standard arithmetic. #[cfg(test)] // only used to validate the lookup tables -fn component_from_srgb8_arithmetic(c: u8) -> NotNan { +fn component_from_srgb8_arithmetic(c: u8) -> f32 { // Source: (version as of Feb 3, 2020) // Convert to float let c = f32::from(c) / 255.0; // Apply sRGB gamma curve - let c = if c <= 0.04045 { + if c <= 0.04045 { c * (25. / 323.) } else { // Use pure-Rust implementation from `libm` to avoid platform-dependent rounding // which would be inconsistent with our hardcoded lookup table. libm::powf((200. * c + 11.) / 211., 12. / 5.) - }; - NotNan::new(c).unwrap() + } } /// Implements sRGB decoding using a lookup table. #[inline] -const fn component_from_srgb8_const(c: u8) -> NotNan { - // Safety: the table may be inspected to contain no NaNs. - unsafe { NotNan::new_unchecked(CONST_SRGB_LOOKUP_TABLE[c as usize]) } +const fn component_from_srgb8_const(c: u8) -> PositiveSign { + // Safety: the table may be inspected to contain no negative or NaN values. + unsafe { PositiveSign::new_unchecked(CONST_SRGB_LOOKUP_TABLE[c as usize]) } } /// Reduces alpha/opacity values to only three possibilities, by conflating all alphas @@ -818,19 +850,6 @@ const CONST_LINEAR_LOOKUP_TABLE: &[f32; 256] = &[ 0.9882353, 0.99215686, 0.99607843, 1.0, ]; -// Const replacement for `NotNan::new()` -const fn new_nn_f32(value: f32) -> Result, FloatIsNan> { - #![allow(clippy::eq_op)] - // This condition is true if and only if the value is NaN - // TODO: Replace this with `.is_nan()` after Rust 1.83 is released with `const_float_classify`. - if value != value { - Err(FloatIsNan) - } else { - // SAFETY: We just checked the only safety condition. - Ok(unsafe { NotNan::new_unchecked(value) }) - } -} - #[cfg(test)] mod tests { use super::*; @@ -849,7 +868,7 @@ mod tests { // Test saturation assert_eq!( - Rgba::new(0.5, -1.0, 10.0, 1.0).to_srgb8(), + Rgba::new(0.5, -0.0, 10.0, 1.0).to_srgb8(), [188, 0, 255, 255] ); } @@ -909,9 +928,8 @@ mod tests { #[test] fn check_const_srgb_table() { - let generated_table: Vec = (0..=u8::MAX) - .map(|component| component_from_srgb8_arithmetic(component).into_inner()) - .collect(); + let generated_table: Vec = + (0..=u8::MAX).map(component_from_srgb8_arithmetic).collect(); print!("const CONST_SRGB_LOOKUP_TABLE: [f32; 256] = ["); for i in 0..=u8::MAX { if i % 6 == 0 { @@ -928,7 +946,7 @@ mod tests { #[test] fn check_const_linear_table() { let generated_table: Vec = (0..=u8::MAX) - .map(|component| component_from_linear8_arithmetic(component).into_inner()) + .map(component_from_linear8_arithmetic) .collect(); print!("const CONST_LINEAR_LOOKUP_TABLE: [f32; 256] = ["); for i in 0..=u8::MAX { diff --git a/all-is-cubes-content/src/alg.rs b/all-is-cubes-content/src/alg.rs index ab8229d2b..1383a44ce 100644 --- a/all-is-cubes-content/src/alg.rs +++ b/all-is-cubes-content/src/alg.rs @@ -11,7 +11,7 @@ use all_is_cubes::block::{Atom, Block, Primitive, Resolution, AIR}; use all_is_cubes::euclid::vec3; use all_is_cubes::math::{ Cube, CubeFace, Face6, FaceMap, FreeCoordinate, FreePoint, GridAab, GridCoordinate, GridPoint, - GridSizeCoord, GridVector, Gridgid, NotNan, Vol, + GridSizeCoord, GridVector, Gridgid, PositiveSign, Vol, }; use all_is_cubes::space::{CubeTransaction, SetCubeError, Space, SpaceTransaction}; @@ -185,7 +185,10 @@ pub(crate) fn space_to_transaction_copy( /// If the computation is NaN or the block is not an atom, it is returned unchanged. pub(crate) fn scale_color(mut block: Block, scalar: f64, quantization: f64) -> Block { let scalar = (scalar / quantization).round() * quantization; - match (block.primitive_mut(), NotNan::new(scalar as f32)) { + match ( + block.primitive_mut(), + PositiveSign::::try_from(scalar as f32), + ) { (Primitive::Atom(Atom { color, .. }), Ok(scalar)) => { *color = (color.to_rgb() * scalar).with_alpha(color.alpha()); } diff --git a/all-is-cubes-content/src/city/exhibits/color.rs b/all-is-cubes-content/src/city/exhibits/color.rs index b157457e2..ae4cb9ed9 100644 --- a/all-is-cubes-content/src/city/exhibits/color.rs +++ b/all-is-cubes-content/src/city/exhibits/color.rs @@ -26,7 +26,9 @@ fn COLORS(ctx: Context<'_>) { let color = Rgb::from( color_point .to_vector() - .map(|s| NotNan::new(s as f32 / (gradient_resolution - 1) as f32).unwrap()) + .map(|s| { + PositiveSign::::new_strict(s as f32 / (gradient_resolution - 1) as f32) + }) .cast_unit(), ); let color_srgb = color.with_alpha_one().to_srgb8(); diff --git a/all-is-cubes-content/src/city/exhibits/move_modifier.rs b/all-is-cubes-content/src/city/exhibits/move_modifier.rs index 71b720728..34b41445f 100644 --- a/all-is-cubes-content/src/city/exhibits/move_modifier.rs +++ b/all-is-cubes-content/src/city/exhibits/move_modifier.rs @@ -56,7 +56,7 @@ fn PROJECTILE(ctx: Context<'_>) { // This will require getting `Move` tick actions to cooperate with `Composite`. let launcher = Block::builder() .display_name(literal!("Launcher")) - .color(Rgb::UNIFORM_LUMINANCE_RED.with_alpha(notnan!(1.0))) + .color(Rgb::UNIFORM_LUMINANCE_RED.with_alpha(ps32(1.0))) .animation_hint(block::AnimationHint::replacement( block::AnimationChange::Shape, )) diff --git a/all-is-cubes-content/src/city/exhibits/prelude.rs b/all-is-cubes-content/src/city/exhibits/prelude.rs index 9cb84bee3..b7043f755 100644 --- a/all-is-cubes-content/src/city/exhibits/prelude.rs +++ b/all-is-cubes-content/src/city/exhibits/prelude.rs @@ -28,8 +28,8 @@ pub(super) use all_is_cubes::euclid::{ pub(super) use all_is_cubes::linking::{BlockProvider, InGenError}; pub(super) use all_is_cubes::listen::ListenableSource; pub(super) use all_is_cubes::math::{ - notnan, rgb_const, rgba_const, Cube, Face6, FaceMap, FreeCoordinate, GridAab, GridCoordinate, - GridPoint, GridRotation, GridSize, GridVector, Gridgid, NotNan, Rgb, Rgba, + ps32, rgb_const, rgba_const, Cube, Face6, FaceMap, FreeCoordinate, GridAab, GridCoordinate, + GridPoint, GridRotation, GridSize, GridVector, Gridgid, PositiveSign, Rgb, Rgba, }; pub(super) use all_is_cubes::op::Operation; pub(super) use all_is_cubes::space::{self, Space, SpacePhysics, SpaceTransaction}; diff --git a/all-is-cubes-content/src/city/exhibits/resolutions.rs b/all-is-cubes-content/src/city/exhibits/resolutions.rs index 0a3aa924f..0dd1d80f9 100644 --- a/all-is-cubes-content/src/city/exhibits/resolutions.rs +++ b/all-is-cubes-content/src/city/exhibits/resolutions.rs @@ -33,12 +33,11 @@ fn RESOLUTIONS(ctx: Context<'_>) { p.lower_bounds() .to_vector() .map(|s| { - NotNan::new( + ps32( (s / GridCoordinate::from(rescale)) as f32 / f32::from(u16::from(resolution) / rescale - 1) .max(1.), ) - .unwrap() }) .cast_unit(), ); diff --git a/all-is-cubes-content/src/city/exhibits/transparency.rs b/all-is-cubes-content/src/city/exhibits/transparency.rs index 67d87d367..fd78e99d6 100644 --- a/all-is-cubes-content/src/city/exhibits/transparency.rs +++ b/all-is-cubes-content/src/city/exhibits/transparency.rs @@ -17,7 +17,7 @@ fn TRANSPARENCY_LARGE(_: Context<'_>) { Rgb::new(0.5, 0.5, 1.0), Rgb::new(0.9, 0.9, 0.9), ]; - let alphas = [0.25, 0.5, 0.75, 0.95]; + let alphas = [0.25, 0.5, 0.75, 0.95].map(ps32); for (rot, color) in GridRotation::CLOCKWISE.iterate().zip(&colors) { let windowpane = GridAab::from_lower_upper([-1, 0, 3], [2, alphas.len() as GridCoordinate, 4]); @@ -25,11 +25,7 @@ fn TRANSPARENCY_LARGE(_: Context<'_>) { windowpane .transform(rot.to_positive_octant_transform(1)) .unwrap(), - |Cube { y, .. }| { - Some(Block::from( - color.with_alpha(NotNan::new(alphas[y as usize]).unwrap()), - )) - }, + |Cube { y, .. }| Some(Block::from(color.with_alpha(alphas[y as usize]))), )?; } diff --git a/all-is-cubes-content/src/clouds.rs b/all-is-cubes-content/src/clouds.rs index 0d8df0015..712da1a08 100644 --- a/all-is-cubes-content/src/clouds.rs +++ b/all-is-cubes-content/src/clouds.rs @@ -1,7 +1,7 @@ //! Cloud generation. use all_is_cubes::block::{Block, BlockCollision, AIR}; -use all_is_cubes::math::{GridAab, GridCoordinate, GridPoint, NotNan, Rgb}; +use all_is_cubes::math::{ps32, GridAab, GridCoordinate, GridPoint, Rgb}; use all_is_cubes::space::{SetCubeError, Space}; use crate::alg::NoiseFnExt as _; @@ -24,7 +24,7 @@ pub fn clouds(region: GridAab, space: &mut Space, density: f32) -> Result<(), Se fn cloud_block(alpha: f32) -> Block { Block::builder() .display_name("Cloud") - .color(Rgb::ONE.with_alpha(NotNan::new(alpha).unwrap())) + .color(Rgb::ONE.with_alpha(ps32(alpha))) .collision(if alpha >= 1.0 { BlockCollision::Hard } else { diff --git a/all-is-cubes-content/src/landscape.rs b/all-is-cubes-content/src/landscape.rs index 6299ea2a0..4351ba0ea 100644 --- a/all-is-cubes-content/src/landscape.rs +++ b/all-is-cubes-content/src/landscape.rs @@ -17,7 +17,7 @@ use all_is_cubes::block::{ AIR, }; use all_is_cubes::linking::{BlockModule, BlockProvider, DefaultProvision, GenError, InGenError}; -use all_is_cubes::math::{notnan, Cube, FreeCoordinate, GridAab, GridCoordinate, GridVector, Rgb}; +use all_is_cubes::math::{ps32, Cube, FreeCoordinate, GridAab, GridCoordinate, GridVector, Rgb}; use all_is_cubes::space::Sky; use all_is_cubes::space::{SetCubeError, Space}; use all_is_cubes::universe::UniverseTransaction; @@ -109,7 +109,7 @@ impl DefaultProvision for LandscapeBlocks { fn blades() -> Block { Block::builder() .display_name("Grass Blades") - .color(palette::GRASS.with_alpha(notnan!(0.1))) + .color(palette::GRASS.with_alpha(ps32(0.1))) .collision(BlockCollision::None) .build() } diff --git a/all-is-cubes-gpu/src/in_wgpu/space.rs b/all-is-cubes-gpu/src/in_wgpu/space.rs index 7ceb58bb6..48d18fbdd 100644 --- a/all-is-cubes-gpu/src/in_wgpu/space.rs +++ b/all-is-cubes-gpu/src/in_wgpu/space.rs @@ -6,14 +6,14 @@ use std::sync::{atomic, mpsc, Arc, Mutex, Weak}; use std::time::Duration; use itertools::Itertools as _; +use num_traits::ConstZero as _; use all_is_cubes::chunking::ChunkPos; use all_is_cubes::content::palette; -use all_is_cubes::euclid::num::Zero as _; use all_is_cubes::listen::{Listen as _, Listener}; use all_is_cubes::math::{ rgba_const, Cube, Face6, FreeCoordinate, FreePoint, GridAab, GridCoordinate, GridPoint, - GridSize, GridVector, NotNan, Rgb, Wireframe as _, + GridSize, GridVector, PositiveSign, Rgb, Wireframe as _, }; use all_is_cubes::raycast::Ray; #[cfg(feature = "rerun")] @@ -980,8 +980,10 @@ impl ParticleSet { let mut tmp: Vec = Vec::with_capacity(24); // TODO: inefficient allocation per object crate::wireframe_vertices::( &mut tmp, - Rgb::ONE - .with_alpha(NotNan::new(0.9f32.powf(self.age as f32)).unwrap_or(NotNan::zero())), + Rgb::ONE.with_alpha( + PositiveSign::::try_from(0.9f32.powf(self.age as f32)) + .unwrap_or(PositiveSign::ZERO), + ), &self.fluff.position.aab().expand(0.004 * (self.age as f64)), ); tmp.into_iter() diff --git a/all-is-cubes-mesh/src/dynamic/chunked_mesh/tests.rs b/all-is-cubes-mesh/src/dynamic/chunked_mesh/tests.rs index c9951ae30..587df6dc4 100644 --- a/all-is-cubes-mesh/src/dynamic/chunked_mesh/tests.rs +++ b/all-is-cubes-mesh/src/dynamic/chunked_mesh/tests.rs @@ -8,7 +8,7 @@ use all_is_cubes::chunking::ChunkPos; use all_is_cubes::color_block; use all_is_cubes::content::make_some_blocks; use all_is_cubes::listen::Listener as _; -use all_is_cubes::math::{notnan, GridPoint, NotNan}; +use all_is_cubes::math::{ps32, GridPoint, NotNan}; use all_is_cubes::math::{Cube, FreePoint, GridAab, GridCoordinate}; use all_is_cubes::space::{BlockIndex, Space, SpaceChange, SpaceTransaction}; use all_is_cubes::time; @@ -284,7 +284,7 @@ fn graphics_options_change() { assert_eq!(vertices, Some(24)); // Change options so that the mesh should disappear - options.transparency = TransparencyOption::Threshold(notnan!(0.5)); + options.transparency = TransparencyOption::Threshold(ps32(0.5)); tester.camera.set_options(options.clone()); vertices = None; diff --git a/all-is-cubes-mesh/src/tests.rs b/all-is-cubes-mesh/src/tests.rs index a8047856c..cfb44d219 100644 --- a/all-is-cubes-mesh/src/tests.rs +++ b/all-is-cubes-mesh/src/tests.rs @@ -8,12 +8,11 @@ use all_is_cubes::block::{self, Block, Resolution::*, AIR}; use all_is_cubes::color_block; use all_is_cubes::content::{make_some_blocks, make_some_voxel_blocks}; use all_is_cubes::euclid::{point3, Point3D, Vector3D}; +use all_is_cubes::math::{ps32, Cube, Rgb}; use all_is_cubes::math::{ - notnan, Face6::{self, *}, FaceMap, FreeCoordinate, GridAab, GridRotation, Rgba, }; -use all_is_cubes::math::{Cube, Rgb}; use all_is_cubes::space::{Space, SpacePhysics}; use all_is_cubes::universe::Universe; use all_is_cubes_render::camera::TransparencyOption; @@ -70,7 +69,7 @@ fn test_block_mesh_threshold(block: Block) -> BlockMesh { &block.evaluate().unwrap(), &Allocator::new(), &MeshOptions { - transparency: TransparencyOption::Threshold(notnan!(0.5)), + transparency: TransparencyOption::Threshold(ps32(0.5)), ..MeshOptions::dont_care_for_test() }, ) diff --git a/all-is-cubes-port/src/stl.rs b/all-is-cubes-port/src/stl.rs index f47c2eae8..409109583 100644 --- a/all-is-cubes-port/src/stl.rs +++ b/all-is-cubes-port/src/stl.rs @@ -7,7 +7,7 @@ use stl_io::Triangle; use all_is_cubes::block; use all_is_cubes::euclid::Vector3D; -use all_is_cubes::math::{notnan, Cube, FreeCoordinate}; +use all_is_cubes::math::{ps32, Cube, FreeCoordinate}; use all_is_cubes::space::Space; use all_is_cubes::universe::PartialUniverse; use all_is_cubes::util::YieldProgress; @@ -81,7 +81,7 @@ pub(crate) fn block_to_stl_triangles(block: &block::EvaluatedBlock) -> Vec mesh::MeshOptions { let mut g = GraphicsOptions::default(); - g.transparency = all_is_cubes_render::camera::TransparencyOption::Threshold(notnan!(0.01)); + g.transparency = all_is_cubes_render::camera::TransparencyOption::Threshold(ps32(0.01)); mesh::MeshOptions::new(&g) } diff --git a/all-is-cubes-ui/src/apps/input.rs b/all-is-cubes-ui/src/apps/input.rs index 5102a4d97..21b59602d 100644 --- a/all-is-cubes-ui/src/apps/input.rs +++ b/all-is-cubes-ui/src/apps/input.rs @@ -3,7 +3,6 @@ reason = "module is private; https://github.com/rust-lang/rust-clippy/issues/8524" )] - use alloc::vec::Vec; use core::time::Duration; use std::collections::{HashMap, HashSet}; @@ -11,7 +10,7 @@ use std::collections::{HashMap, HashSet}; use all_is_cubes::character::Character; use all_is_cubes::euclid::{Point2D, Vector2D}; use all_is_cubes::listen::{ListenableCell, ListenableSource}; -use all_is_cubes::math::{notnan, FreeCoordinate, FreeVector}; +use all_is_cubes::math::{ps32, FreeCoordinate, FreeVector}; use all_is_cubes::time::Tick; use all_is_cubes::universe::{Handle, Universe}; use all_is_cubes_render::camera::{ @@ -375,7 +374,7 @@ impl InputProcessor { options.transparency = match options.transparency { TransparencyOption::Surface => TransparencyOption::Volumetric, TransparencyOption::Volumetric => { - TransparencyOption::Threshold(notnan!(0.5)) + TransparencyOption::Threshold(ps32(0.5)) } TransparencyOption::Threshold(_) => TransparencyOption::Surface, _ => TransparencyOption::Surface, // TODO: either stop doing cycle-commands or put it on the enum so it can be exhaustive diff --git a/all-is-cubes-ui/src/ui_content/options.rs b/all-is-cubes-ui/src/ui_content/options.rs index 36cf1cef1..b5eeb68a2 100644 --- a/all-is-cubes-ui/src/ui_content/options.rs +++ b/all-is-cubes-ui/src/ui_content/options.rs @@ -4,7 +4,7 @@ use alloc::vec::Vec; use core::fmt; use all_is_cubes::arcstr::{self, literal}; -use all_is_cubes::math::{notnan, Face6}; +use all_is_cubes::math::{ps32, Face6}; use all_is_cubes_render::camera::{self, AntialiasingOption, GraphicsOptions}; use crate::apps::ControlMessage; @@ -71,7 +71,7 @@ pub(crate) fn graphics_options_widgets( [ camera::TransparencyOption::Surface, camera::TransparencyOption::Volumetric, - camera::TransparencyOption::Threshold(notnan!(0.5)), + camera::TransparencyOption::Threshold(ps32(0.5)), ], ), ]); diff --git a/all-is-cubes/src/block/eval/derived.rs b/all-is-cubes/src/block/eval/derived.rs index 1d1ab7916..793de1380 100644 --- a/all-is-cubes/src/block/eval/derived.rs +++ b/all-is-cubes/src/block/eval/derived.rs @@ -3,7 +3,6 @@ use core::ops; use itertools::Itertools; use euclid::Vector3D; -use ordered_float::NotNan; /// Acts as polyfill for float methods #[cfg(not(feature = "std"))] @@ -14,7 +13,9 @@ use crate::block::{ self, Resolution::{self, R1}, }; -use crate::math::{Cube, Face6, FaceMap, GridAab, Intensity, OpacityCategory, Rgb, Rgba, Vol}; +use crate::math::{ + Cube, Face6, FaceMap, GridAab, Intensity, OpacityCategory, PositiveSign, Rgb, Rgba, Vol, +}; use crate::raytracer; #[cfg(doc)] @@ -234,8 +235,7 @@ impl VoxSum { // Note that by dividing the alpha by the full surface area, not the count, // we handle the case where the voxel data doesn't cover the full block and // uncounted pixels should act as if they are transparent. - NotNan::new(self.alpha_sum / (surface_area)) - .expect("Recursive block alpha computation produced NaN"), + PositiveSign::::new_clamped(self.alpha_sum / (surface_area)), ) } } diff --git a/all-is-cubes/src/block/modifier/composite.rs b/all-is-cubes/src/block/modifier/composite.rs index 6922a7389..96d196947 100644 --- a/all-is-cubes/src/block/modifier/composite.rs +++ b/all-is-cubes/src/block/modifier/composite.rs @@ -3,15 +3,15 @@ reason = "module is private; https://github.com/rust-lang/rust-clippy/issues/8524" )] +use alloc::vec; use core::mem; -use alloc::vec; -use ordered_float::NotNan; +use num_traits::Zero; use crate::block::{ self, Block, BlockCollision, Evoxel, Evoxels, MinEval, Modifier, Resolution::R1, AIR, }; -use crate::math::{Cube, GridAab, GridCoordinate, GridRotation, GridSize, Rgb, Vol}; +use crate::math::{Cube, GridAab, GridCoordinate, GridRotation, GridSize, PositiveSign, Rgb, Vol}; use crate::op::Operation; use crate::universe; @@ -444,31 +444,31 @@ impl CompositeOperator { fn alpha_blend( self, source: Rgb, - sa: NotNan, + sa: PositiveSign, destination: Rgb, - da: NotNan, - ) -> (Rgb, NotNan) { + da: PositiveSign, + ) -> (Rgb, PositiveSign) { match self { Self::Over => { // TODO: Surely this is not the only place we have implemented rgba blending? // Note that this math would be simpler if we used premultiplied alpha. - let sa_complement = NotNan::new(1. - sa.into_inner()).unwrap(); + let sa_complement = PositiveSign::::new_clamped(1. - sa.into_inner()); let rgb = source * sa + destination * sa_complement; (rgb, sa + sa_complement * da) } Self::In => (source, sa * da), Self::Out => { - let da_complement = NotNan::new(1. - da.into_inner()).unwrap(); + let da_complement = PositiveSign::::new_clamped(1. - da.into_inner()); (source, sa * da_complement) } Self::Atop => { - let sa_complement = NotNan::new(1. - sa.into_inner()).unwrap(); + let sa_complement = PositiveSign::::new_clamped(1. - sa.into_inner()); let rgb = source * sa + destination * sa_complement; let out_alpha = da; - if out_alpha == 0.0 { + if out_alpha.is_zero() { // we wouldn't have to do this if we used premultiplied alpha :/ (Rgb::ZERO, out_alpha) } else { @@ -683,7 +683,7 @@ mod tests { use super::*; use crate::block::{EvKey, EvaluatedBlock, Resolution::*}; use crate::content::{make_slab, make_some_blocks}; - use crate::math::Rgba; + use crate::math::{ps32, Rgba}; use crate::space::Space; use crate::time; use crate::universe::Universe; @@ -720,7 +720,7 @@ mod tests { Evoxel { // color doesn't matter, except that at zero alpha it should be the canonical zero // for convenience of testing. (TODO: maybe `Rgba` should enforce that or be premultiplied.) - color: Rgb::ZERO.with_alpha(NotNan::new(alpha).unwrap()), + color: Rgb::ZERO.with_alpha(ps32(alpha)), emission, selectable: true, collision: Hard, diff --git a/all-is-cubes/src/block/modifier/move.rs b/all-is-cubes/src/block/modifier/move.rs index e58aa2f2d..0ec4e8490 100644 --- a/all-is-cubes/src/block/modifier/move.rs +++ b/all-is-cubes/src/block/modifier/move.rs @@ -231,10 +231,9 @@ mod tests { use super::*; use crate::block::{Composite, EvaluatedBlock, Resolution::*, VoxelOpacityMask}; use crate::content::make_some_blocks; - use crate::math::{notnan, rgba_const, FaceMap, GridPoint, OpacityCategory, Rgb, Rgba}; + use crate::math::{ps32, rgba_const, FaceMap, GridPoint, OpacityCategory, Rgb, Rgba}; use crate::space::Space; use crate::universe::Universe; - use ordered_float::NotNan; use pretty_assertions::assert_eq; #[test] @@ -266,14 +265,14 @@ mod tests { recursion: 0 }, derived: block::Derived { - color: color.to_rgb().with_alpha(NotNan::new(2. / 3.).unwrap()), + color: color.to_rgb().with_alpha(ps32(2. / 3.)), face_colors: FaceMap { - nx: color.to_rgb().with_alpha(notnan!(0.5)), - ny: color.to_rgb().with_alpha(notnan!(1.0)), - nz: color.to_rgb().with_alpha(notnan!(0.5)), - px: color.to_rgb().with_alpha(notnan!(0.5)), - py: color.to_rgb().with_alpha(notnan!(1.0)), - pz: color.to_rgb().with_alpha(notnan!(0.5)), + nx: color.to_rgb().with_alpha(ps32(0.5)), + ny: color.to_rgb().with_alpha(ps32(1.0)), + nz: color.to_rgb().with_alpha(ps32(0.5)), + px: color.to_rgb().with_alpha(ps32(0.5)), + py: color.to_rgb().with_alpha(ps32(1.0)), + pz: color.to_rgb().with_alpha(ps32(0.5)), }, light_emission: Rgb::ZERO, opaque: FaceMap::splat(false).with(Face6::PY, true), @@ -323,14 +322,14 @@ mod tests { Vol::repeat(expected_bounds, Evoxel::from_block(&ev_original)) ), derived: block::Derived { - color: color.to_rgb().with_alpha(NotNan::new(2. / 3.).unwrap()), + color: color.to_rgb().with_alpha(ps32(2. / 3.)), face_colors: FaceMap { - nx: color.to_rgb().with_alpha(notnan!(0.5)), - ny: color.to_rgb().with_alpha(notnan!(1.0)), - nz: color.to_rgb().with_alpha(notnan!(0.5)), - px: color.to_rgb().with_alpha(notnan!(0.5)), - py: color.to_rgb().with_alpha(notnan!(1.0)), - pz: color.to_rgb().with_alpha(notnan!(0.5)), + nx: color.to_rgb().with_alpha(ps32(0.5)), + ny: color.to_rgb().with_alpha(ps32(1.0)), + nz: color.to_rgb().with_alpha(ps32(0.5)), + px: color.to_rgb().with_alpha(ps32(0.5)), + py: color.to_rgb().with_alpha(ps32(1.0)), + pz: color.to_rgb().with_alpha(ps32(0.5)), }, light_emission: Rgb::ZERO, opaque: FaceMap::splat(false).with(Face6::PY, true), diff --git a/all-is-cubes/src/block/tests.rs b/all-is-cubes/src/block/tests.rs index f9ee2acba..ccff03dc4 100644 --- a/all-is-cubes/src/block/tests.rs +++ b/all-is-cubes/src/block/tests.rs @@ -16,7 +16,7 @@ use crate::block::{ use crate::content::make_some_blocks; use crate::listen::{self, NullListener, Sink}; use crate::math::{ - notnan, Cube, Face6, FaceMap, GridAab, GridPoint, GridRotation, GridVector, Intensity, NotNan, + ps32, Cube, Face6, FaceMap, GridAab, GridPoint, GridRotation, GridVector, Intensity, OpacityCategory, Rgb, Rgba, Vol, }; use crate::space::{Space, SpaceTransaction}; @@ -292,8 +292,9 @@ mod eval { .unwrap() .build_into(&mut universe); - let total_emission = voxel_block.evaluate().unwrap().light_emission(); - let difference: Vector3D = (total_emission - atom_emission).into(); + let total_emission: Vector3D = + voxel_block.evaluate().unwrap().light_emission().into(); + let difference: Vector3D = total_emission - atom_emission.into(); assert!( difference.length() < 0.0001, "reflectance = {reflectance:?}\n\ @@ -314,9 +315,9 @@ mod eval { let block = Block::builder() .voxels_fn(resolution, |point| { Block::from(voxel_color.with_alpha(if point.x == 0 && point.z == 0 { - NotNan::new(alpha).unwrap() + ps32(alpha) } else { - notnan!(1.0) + ps32(1.0) })) }) .unwrap() @@ -328,13 +329,11 @@ mod eval { // the light paths with opaque surfaces. assert_eq!( e.color(), - voxel_color.with_alpha( - NotNan::new(1.0 - (alpha / (f32::from(resolution).powi(2) * 3.0))).unwrap() - ) + voxel_color.with_alpha(ps32(1.0 - (alpha / (f32::from(resolution).powi(2) * 3.0)))) ); // This is the sum of the transparency of one voxel on one of the six faces - let one_face_transparency = voxel_color - .with_alpha(NotNan::new(1.0 - (alpha / f32::from(resolution).powi(2))).unwrap()); + let one_face_transparency = + voxel_color.with_alpha(ps32(1.0 - (alpha / f32::from(resolution).powi(2)))); assert_eq!( e.face_colors(), FaceMap { @@ -370,7 +369,7 @@ mod eval { let mut universe = Universe::new(); let c1 = Rgb::new(1.0, 0.0, 0.0); let c2 = Rgb::new(0.0, 1.0, 0.0); - let colors = [c1.with_alpha_one(), c2.with_alpha(notnan!(0.5))]; + let colors = [c1.with_alpha_one(), c2.with_alpha(ps32(0.5))]; let block = Block::builder() .voxels_fn(R2, |cube| Block::from(colors[cube.y as usize])) .unwrap() diff --git a/all-is-cubes/src/camera.rs b/all-is-cubes/src/camera.rs index 5a2ed46cf..0cab9d6b1 100644 --- a/all-is-cubes/src/camera.rs +++ b/all-is-cubes/src/camera.rs @@ -4,8 +4,7 @@ use euclid::{ point3, vec3, Angle, Point2D, Point3D, RigidTransform3D, Rotation3D, Size2D, Transform3D, }; use itertools::Itertools as _; -use num_traits::One; -use ordered_float::NotNan; +use num_traits::ConstOne as _; /// Acts as polyfill for float methods #[cfg(not(feature = "std"))] @@ -14,7 +13,7 @@ use num_traits::float::Float as _; use crate::math::{ self, Aab, Axis, Cube, FreeCoordinate, FreePoint, FreeVector, GridAab, LineVertex, Octant, - OctantMask, Rgba, + OctantMask, PositiveSign, Rgba, }; use crate::raycast::Ray; @@ -71,7 +70,7 @@ pub struct Camera { /// Scale factor for scene brightness. /// Calculated from `options.exposure` by [`Self::set_options`]. - exposure_value: NotNan, + exposure_value: PositiveSign, } /// Basic creation and mutation. @@ -165,11 +164,11 @@ impl Camera { /// This may or may not affect [`Self::exposure()`] depending on the current /// graphics options. pub fn set_measured_exposure(&mut self, value: f32) { - if let Ok(value) = NotNan::new(value) { + if let Ok(value) = PositiveSign::::try_from(value) { match (&self.options.exposure, &self.options.lighting_display) { (ExposureOption::Fixed(_), _) => { /* nothing to do */ } (ExposureOption::Automatic, LightingOption::None) => { - self.exposure_value = NotNan::one(); + self.exposure_value = PositiveSign::ONE; } (ExposureOption::Automatic, _) => { self.exposure_value = value; @@ -357,7 +356,7 @@ impl Camera { /// It may or may not be equal to the last /// [`set_measured_exposure()`](Self::set_measured_exposure), /// depending on the graphics options. - pub fn exposure(&self) -> NotNan { + pub fn exposure(&self) -> PositiveSign { self.exposure_value } diff --git a/all-is-cubes/src/camera/graphics_options.rs b/all-is-cubes/src/camera/graphics_options.rs index 8ac7f67eb..86dbba05b 100644 --- a/all-is-cubes/src/camera/graphics_options.rs +++ b/all-is-cubes/src/camera/graphics_options.rs @@ -1,9 +1,9 @@ use core::fmt; -use num_traits::One; +use num_traits::ConstOne as _; use ordered_float::NotNan; -use crate::math::{notnan, FreeCoordinate, Rgb, Rgba}; +use crate::math::{notnan, FreeCoordinate, PositiveSign, Rgb, Rgba}; use crate::util::ShowStatus; #[cfg(doc)] @@ -133,7 +133,7 @@ impl GraphicsOptions { fov_y: notnan!(90.), // TODO: Change tone mapping default once we have a good implementation. tone_mapping: ToneMappingOperator::Clamp, - exposure: ExposureOption::Fixed(notnan!(1.)), + exposure: ExposureOption::Fixed(PositiveSign::::ONE), bloom_intensity: notnan!(0.), view_distance: notnan!(200.), lighting_display: LightingOption::None, @@ -329,7 +329,7 @@ impl ToneMappingOperator { pub enum ExposureOption { /// Constant exposure; light values in the scene are multiplied by this value /// before the tone mapping operator is applied. - Fixed(NotNan), + Fixed(PositiveSign), /// Exposure adjusts to compensate for the actual brightness of the scene. /// /// Note: If [`GraphicsOptions::lighting_display`] is disabled, @@ -347,17 +347,17 @@ impl fmt::Debug for ExposureOption { } impl ExposureOption { - pub(crate) fn initial(&self) -> NotNan { + pub(crate) fn initial(&self) -> PositiveSign { match *self { ExposureOption::Fixed(value) => value, - ExposureOption::Automatic => NotNan::one(), + ExposureOption::Automatic => PositiveSign::::ONE, } } } impl Default for ExposureOption { fn default() -> Self { - ExposureOption::Fixed(NotNan::one()) + ExposureOption::Fixed(PositiveSign::::ONE) } } @@ -398,7 +398,7 @@ pub enum TransparencyOption { Volumetric, /// Alpha above or below the given threshold value will be rounded to fully opaque /// or fully transparent, respectively. - Threshold(NotNan), + Threshold(PositiveSign), } impl TransparencyOption { @@ -476,7 +476,7 @@ impl AntialiasingOption { #[cfg(test)] mod tests { use super::*; - use crate::math::{rgba_const, OpacityCategory}; + use crate::math::{ps32, rgba_const, OpacityCategory}; use pretty_assertions::assert_eq; #[test] @@ -536,7 +536,7 @@ mod tests { GraphicsOptions { fog: FogOption::None, tone_mapping: ToneMappingOperator::Clamp, - exposure: ExposureOption::Fixed(NotNan::one()), + exposure: ExposureOption::Fixed(PositiveSign::::ONE), bloom_intensity: NotNan::from(0u8), lighting_display: LightingOption::None, antialiasing: AntialiasingOption::None, @@ -550,7 +550,7 @@ mod tests { for transparency in &[ TransparencyOption::Surface, TransparencyOption::Volumetric, - TransparencyOption::Threshold(notnan!(0.5)), + TransparencyOption::Threshold(ps32(0.5)), ] { assert_eq!( transparency.will_output_alpha(), diff --git a/all-is-cubes/src/camera/tests.rs b/all-is-cubes/src/camera/tests.rs index 793d99d5c..9e4004ce8 100644 --- a/all-is-cubes/src/camera/tests.rs +++ b/all-is-cubes/src/camera/tests.rs @@ -6,7 +6,7 @@ use crate::camera::{ look_at_y_up, Camera, ExposureOption, FrustumPoints, GraphicsOptions, LightingOption, ViewTransform, Viewport, }; -use crate::math::{notnan, rgba_const, Aab, NotNan}; +use crate::math::{ps32, rgba_const, Aab, NotNan}; #[test] fn camera_bad_viewport_doesnt_panic() { @@ -110,7 +110,7 @@ fn post_process() { assert_eq!(camera.post_process_color(color), color); // Try exposure - options.exposure = ExposureOption::Fixed(notnan!(0.5)); + options.exposure = ExposureOption::Fixed(ps32(0.5)); camera.set_options(options); assert_eq!( camera.post_process_color(color), @@ -130,7 +130,7 @@ fn exposure_automatic_active() { ); camera.set_measured_exposure(7.0); - assert_eq!(camera.exposure(), notnan!(7.0)); + assert_eq!(camera.exposure(), ps32(7.0)); } #[test] @@ -145,7 +145,7 @@ fn exposure_automatic_disabled_when_lighting_is_disabled() { ); camera.set_measured_exposure(7.0); - assert_eq!(camera.exposure(), notnan!(1.0)); // ignoring measured + assert_eq!(camera.exposure(), ps32(1.0)); // ignoring measured } #[test] diff --git a/all-is-cubes/src/raytracer.rs b/all-is-cubes/src/raytracer.rs index 41b849509..55c9ebf51 100644 --- a/all-is-cubes/src/raytracer.rs +++ b/all-is-cubes/src/raytracer.rs @@ -21,7 +21,6 @@ use manyfmt::Fmt; reason = "unclear why this warns even though it is needed" )] use num_traits::float::Float as _; -use ordered_float::NotNan; #[cfg(feature = "auto-threads")] use rayon::iter::{IntoParallelIterator as _, ParallelIterator as _}; @@ -37,7 +36,7 @@ use crate::camera::{Camera, GraphicsOptions, TransparencyOption}; use crate::math::Euclid as _; use crate::math::{ rgb_const, smoothstep, Cube, Face6, Face7, FreeCoordinate, FreePoint, FreeVector, GridAab, - GridMatrix, Intensity, Rgb, Rgba, Vol, + GridMatrix, Intensity, PositiveSign, Rgb, Rgba, Vol, }; use crate::raycast::{self, Ray, RayIsh}; use crate::space::{BlockIndex, BlockSky, PackedLight, Sky, Space, SpaceBlockData}; @@ -629,7 +628,7 @@ fn apply_transmittance(color: Rgba, thickness: f32) -> (Rgba, f32) { // Convert back to alpha. // TODO: skip NaN check ... this may require refactoring Surface usage. // We might also benefit from an "UncheckedRgba" concept. - let alpha = NotNan::new(1.0 - depth_transmittance).unwrap(); + let alpha = PositiveSign::::new_clamped(1.0 - depth_transmittance); let modified_color = color.to_rgb().with_alpha(alpha); // Compute how the emission should be scaled to account for internal absorption and thickness. diff --git a/all-is-cubes/src/raytracer/accum.rs b/all-is-cubes/src/raytracer/accum.rs index ec03dd1f4..368c6d1a2 100644 --- a/all-is-cubes/src/raytracer/accum.rs +++ b/all-is-cubes/src/raytracer/accum.rs @@ -1,11 +1,10 @@ //! [`Accumulate`] and output formats of the raytracer. use euclid::Vector3D; -use ordered_float::NotNan; use crate::block::Resolution; use crate::camera::GraphicsOptions; -use crate::math::{notnan, rgb_const, Intensity, Rgb, Rgba}; +use crate::math::{ps32, rgb_const, Intensity, PositiveSign, Rgb, Rgba}; use crate::space::SpaceBlockData; /// Borrowed data which may be used to customize the result of raytracing. @@ -252,7 +251,7 @@ impl From for Rgba { let non_premultiplied_color = buf.light / color_alpha; Rgb::try_from(non_premultiplied_color) .unwrap_or_else(|_| rgb_const!(1.0, 0.0, 0.0)) - .with_alpha(NotNan::new(color_alpha).unwrap_or(notnan!(1.0))) + .with_alpha(PositiveSign::::try_from(color_alpha).unwrap_or(ps32(1.0))) } } } diff --git a/all-is-cubes/src/rerun_glue.rs b/all-is-cubes/src/rerun_glue.rs index ebea019ae..884583a0d 100644 --- a/all-is-cubes/src/rerun_glue.rs +++ b/all-is-cubes/src/rerun_glue.rs @@ -157,9 +157,9 @@ fn annotation_context() -> archetypes::AnnotationContext { (C::MeshVizEdgeNx, "", Axis::X.color().with_alpha_one()), (C::MeshVizEdgeNy, "", Axis::Y.color().with_alpha_one()), (C::MeshVizEdgeNz, "", Axis::Z.color().with_alpha_one()), - (C::MeshVizEdgePx, "", (Rgb::ONE * 0.2 - Axis::X.color()).with_alpha_one()), - (C::MeshVizEdgePy, "", (Rgb::ONE * 0.2 - Axis::Y.color()).with_alpha_one()), - (C::MeshVizEdgePz, "", (Rgb::ONE * 0.2 - Axis::Z.color()).with_alpha_one()), + (C::MeshVizEdgePx, "", (Rgb::ONE * 0.2).saturating_sub(Axis::X.color()).with_alpha_one()), + (C::MeshVizEdgePy, "", (Rgb::ONE * 0.2).saturating_sub(Axis::Y.color()).with_alpha_one()), + (C::MeshVizEdgePz, "", (Rgb::ONE * 0.2).saturating_sub(Axis::Z.color()).with_alpha_one()), ]; archetypes::AnnotationContext::new(descs.into_iter().map(|(id, label, color)| { diff --git a/all-is-cubes/src/save/schema.rs b/all-is-cubes/src/save/schema.rs index ee715f1f6..6d7d82972 100644 --- a/all-is-cubes/src/save/schema.rs +++ b/all-is-cubes/src/save/schema.rs @@ -20,11 +20,10 @@ use alloc::vec::Vec; use core::num::NonZeroU16; use arcstr::ArcStr; -use ordered_float::NotNan; use serde::{Deserialize, Serialize}; use crate::block::Block; -use crate::math::{Aab, Face6, GridAab, GridCoordinate, GridRotation}; +use crate::math::{Aab, Face6, GridAab, GridCoordinate, GridRotation, NotNan, PositiveSign}; use crate::save::compress::{GzSerde, Leu16}; use crate::time::Schedule; use crate::universe::Handle; @@ -366,9 +365,9 @@ pub(crate) struct IconRowSerV1 { //------------------------------------------------------------------------------------------------// // Schema corresponding to the `math` module -type RgbSer = [NotNan; 3]; +type RgbSer = [PositiveSign; 3]; -type RgbaSer = [NotNan; 4]; +type RgbaSer = [PositiveSign; 4]; //------------------------------------------------------------------------------------------------// // Schema corresponding to the `op` module diff --git a/all-is-cubes/src/space/light/data.rs b/all-is-cubes/src/space/light/data.rs index 5b3f15288..041d86ae9 100644 --- a/all-is-cubes/src/space/light/data.rs +++ b/all-is-cubes/src/space/light/data.rs @@ -12,7 +12,7 @@ use euclid::default::Vector3D; )] use num_traits::float::Float as _; -use crate::math::{NotNan, Rgb}; +use crate::math::{PositiveSign, Rgb}; #[cfg(doc)] use crate::space::{self, Space}; @@ -109,10 +109,10 @@ impl PackedLight { /// Returns the light level. #[inline] pub fn value(&self) -> Rgb { - Rgb::new_nn( - Self::scalar_out_nn(self.value.x), - Self::scalar_out_nn(self.value.y), - Self::scalar_out_nn(self.value.z), + Rgb::new_ps( + Self::scalar_out_ps(self.value.x), + Self::scalar_out_ps(self.value.y), + Self::scalar_out_ps(self.value.z), ) } @@ -183,9 +183,9 @@ impl PackedLight { } #[inline(always)] - fn scalar_in(value: NotNan) -> PackedLightScalar { + fn scalar_in(value: PositiveSign) -> PackedLightScalar { // Note that `as` is a saturating cast. - (value.log2() * Self::LOG_SCALE + Self::LOG_OFFSET) as PackedLightScalar + (value.into_inner().log2() * Self::LOG_SCALE + Self::LOG_OFFSET) as PackedLightScalar } /// Convert a `PackedLightScalar` value to a linear color component value. @@ -201,9 +201,9 @@ impl PackedLight { } #[inline(always)] - fn scalar_out_nn(value: PackedLightScalar) -> NotNan { + fn scalar_out_ps(value: PackedLightScalar) -> PositiveSign { // Safety: a test verifies that `scalar_out` can never return NaN. - unsafe { NotNan::new_unchecked(Self::scalar_out(value)) } + unsafe { PositiveSign::new_unchecked(Self::scalar_out(value)) } } } @@ -259,7 +259,7 @@ impl From for PackedLight { #[cfg(test)] mod tests { use super::*; - use crate::math::notnan; + use crate::math::ps32; use core::iter::once; fn packed_light_test_values() -> impl Iterator { @@ -294,16 +294,17 @@ mod tests { #[test] fn packed_light_roundtrip() { for i in PackedLightScalar::MIN..PackedLightScalar::MAX { - assert_eq!(i, PackedLight::scalar_in(PackedLight::scalar_out_nn(i))); + assert_eq!(i, PackedLight::scalar_in(PackedLight::scalar_out_ps(i))); } } - /// Safety test: we want to skip the NaN checks for constructing `Rgb` - /// from `PackedLight`, so it had better not be NaN for any possible input. + /// Safety test: we want to skip the NaN/sign checks for constructing `Rgb` + /// from `PackedLight`, so it had better be valid for any possible input. #[test] - fn packed_light_always_finite() { + fn packed_light_always_positive() { for i in PackedLightScalar::MIN..PackedLightScalar::MAX { - assert!(PackedLight::scalar_out(i).is_finite(), "{}", i); + let value = PackedLight::scalar_out(i); + assert!(value.is_finite() && value.is_sign_positive(), "{}", i); } } @@ -312,11 +313,13 @@ mod tests { fn packed_light_clipping_in() { assert_eq!( [ - PackedLight::scalar_in(notnan!(-1.)), - PackedLight::scalar_in(notnan!(1e-30)), - PackedLight::scalar_in(notnan!(1e+30)), + PackedLight::scalar_in(ps32(-0.0)), + PackedLight::scalar_in(ps32(0.0)), + PackedLight::scalar_in(ps32(1e-30)), + PackedLight::scalar_in(ps32(1e+30)), + PackedLight::scalar_in(ps32(f32::INFINITY)), ], - [0, 0, 255], + [0, 0, 0, 255, 255], ); } diff --git a/all-is-cubes/src/space/light/updater.rs b/all-is-cubes/src/space/light/updater.rs index 671d29b8f..20c441ba5 100644 --- a/all-is-cubes/src/space/light/updater.rs +++ b/all-is-cubes/src/space/light/updater.rs @@ -5,8 +5,8 @@ use alloc::boxed::Box; use alloc::vec::Vec; use core::cmp::Ordering; use core::{fmt, mem}; -use euclid::Vector3D; +use euclid::Vector3D; use manyfmt::Fmt; #[cfg(feature = "auto-threads")] @@ -14,7 +14,9 @@ use rayon::iter::{IntoParallelRefMutIterator as _, ParallelIterator as _}; use super::debug::LightComputeOutput; use crate::block::{self, EvaluatedBlock}; -use crate::math::{Cube, CubeFace, Face6, Face7, FaceMap, NotNan, OpacityCategory, Rgb, Rgba, Vol}; +use crate::math::{ + Cube, CubeFace, Face6, Face7, FaceMap, OpacityCategory, PositiveSign, Rgb, Rgba, Vol, +}; use crate::raycast::Ray; use crate::space::light::{ chart::LightChart, LightUpdateQueue, LightUpdateRayInfo, LightUpdateRequest, Priority, @@ -822,7 +824,7 @@ impl LightBuffer { fn finish(&self, origin_is_opaque: bool) -> PackedLight { // if total_rays is zero then incoming_light is zero so the result will be zero. // We just need to avoid dividing by zero. - let scale = NotNan::new(1.0 / self.total_ray_weight.max(1.0)).unwrap(); + let scale = PositiveSign::::new_clamped(1.0 / self.total_ray_weight.max(1.0)); let new_light_value: PackedLight = if self.total_rays > 0 { PackedLight::some(self.incoming_light * scale) } else if origin_is_opaque { diff --git a/test-renderers/src/test_cases.rs b/test-renderers/src/test_cases.rs index b255c845a..c2d1ef32d 100644 --- a/test-renderers/src/test_cases.rs +++ b/test-renderers/src/test_cases.rs @@ -15,7 +15,7 @@ use all_is_cubes::color_block; use all_is_cubes::euclid::{point3, size2, size3, vec2, vec3, Point2D, Size2D, Size3D, Vector3D}; use all_is_cubes::listen::{ListenableCell, ListenableSource}; use all_is_cubes::math::{ - notnan, rgb_const, rgba_const, Axis, Cube, Face6, FreeCoordinate, GridAab, GridCoordinate, + ps32, rgb_const, rgba_const, Axis, Cube, Face6, FreeCoordinate, GridAab, GridCoordinate, GridPoint, GridRotation, GridVector, NotNan, Rgb, Rgba, Vol, }; use all_is_cubes::space::{self, LightPhysics, Space}; @@ -501,8 +501,8 @@ async fn follow_options_change(mut context: RenderTestContext) { options_1.fov_y = NotNan::from(90); let mut options_2 = options_1.clone(); options_2.fov_y = NotNan::from(70); - options_2.exposure = ExposureOption::Fixed(notnan!(1.5)); - options_2.transparency = TransparencyOption::Threshold(notnan!(0.1)); + options_2.exposure = ExposureOption::Fixed(ps32(1.5)); + options_2.transparency = TransparencyOption::Threshold(ps32(0.1)); let options_cell = ListenableCell::new(options_1); let cameras: StandardCameras = StandardCameras::new( @@ -930,7 +930,7 @@ async fn template(mut context: RenderTestContext, template_name: &'static str) { async fn tone_mapping(mut context: RenderTestContext, (tmo, exposure): (ToneMappingOperator, f32)) { let mut options = tone_mapping_test_options(); options.tone_mapping = tmo; - options.exposure = ExposureOption::Fixed(NotNan::new(exposure).unwrap()); + options.exposure = ExposureOption::Fixed(ps32(exposure)); let scene = StandardCameras::from_constant_for_test( options, Viewport::with_scale(1.0, vec2(256, 320)),