Skip to main content

cu29_units/
lib.rs

1//! Copper-native SI quantity wrappers.
2//!
3//! Feature flags:
4//! - `default` = `["std"]`
5//! - `std`: enables `uom/std`
6//! - `reflect`: enables `bevy_reflect` support on wrapper types
7//! - `textlogs`: compatibility no-op for downstream feature forwarding
8//!
9#![cfg_attr(not(feature = "std"), no_std)]
10
11extern crate alloc;
12
13pub use uom;
14
15macro_rules! define_storage_wrappers {
16    ($storage_mod:ident, $storage_ty:ty, [$(($id:literal, $unit_mod:ident, $quantity:ident, $symbol:literal),)+]) => {
17        pub mod $storage_mod {
18            use core::marker::PhantomData;
19            use serde::{Deserialize, Deserializer, Serialize, Serializer};
20
21            #[cfg(feature = "reflect")]
22            use bevy_reflect::Reflect;
23
24            macro_rules! define_quantity {
25                ($unit_mod_name:ident, $quantity_name:ident, $unit_symbol:literal) => {
26                    #[repr(transparent)]
27                    #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
28                    #[cfg_attr(feature = "reflect", derive(Reflect))]
29                    #[cfg_attr(feature = "reflect", reflect(from_reflect = false))]
30                    pub struct $quantity_name {
31                        pub value: $storage_ty,
32                    }
33
34                    impl $quantity_name {
35                        const STORAGE_UNIT: &'static str = $unit_symbol;
36
37                        #[inline]
38                        pub fn new<U>(value: $storage_ty) -> Self
39                        where
40                            U: uom::si::$unit_mod_name::Conversion<$storage_ty>,
41                        {
42                            Self::from_uom(uom::si::$storage_mod::$quantity_name::new::<U>(value))
43                        }
44
45                        #[inline]
46                        pub fn get<U>(&self) -> $storage_ty
47                        where
48                            U: uom::si::$unit_mod_name::Conversion<$storage_ty>,
49                        {
50                            (*self).to_uom().get::<U>()
51                        }
52
53                        #[inline]
54                        pub fn raw(&self) -> $storage_ty {
55                            self.value
56                        }
57
58                        #[inline]
59                        pub fn into_uom(self) -> uom::si::$storage_mod::$quantity_name {
60                            self.to_uom()
61                        }
62
63                        #[inline]
64                        pub fn as_uom(&self) -> uom::si::$storage_mod::$quantity_name {
65                            (*self).to_uom()
66                        }
67
68                        #[inline]
69                        pub fn from_uom(inner: uom::si::$storage_mod::$quantity_name) -> Self {
70                            Self::from_base_value(inner.value)
71                        }
72
73                        #[inline]
74                        fn to_uom(self) -> uom::si::$storage_mod::$quantity_name {
75                            uom::si::$storage_mod::$quantity_name {
76                                dimension: PhantomData,
77                                units: PhantomData,
78                                value: self.value,
79                            }
80                        }
81
82                        #[inline]
83                        fn from_base_value(value: $storage_ty) -> Self {
84                            Self { value }
85                        }
86                    }
87
88                    impl Default for $quantity_name {
89                        fn default() -> Self {
90                            Self::from_base_value(0.0 as $storage_ty)
91                        }
92                    }
93
94                    impl From<uom::si::$storage_mod::$quantity_name> for $quantity_name {
95                        fn from(value: uom::si::$storage_mod::$quantity_name) -> Self {
96                            Self::from_uom(value)
97                        }
98                    }
99
100                    impl From<$quantity_name> for uom::si::$storage_mod::$quantity_name {
101                        fn from(value: $quantity_name) -> Self {
102                            value.into_uom()
103                        }
104                    }
105
106                    impl core::ops::Add for $quantity_name {
107                        type Output = Self;
108
109                        fn add(self, rhs: Self) -> Self::Output {
110                            Self::from_base_value(self.raw() + rhs.raw())
111                        }
112                    }
113
114                    impl core::ops::AddAssign for $quantity_name {
115                        fn add_assign(&mut self, rhs: Self) {
116                            *self = *self + rhs;
117                        }
118                    }
119
120                    impl core::ops::Sub for $quantity_name {
121                        type Output = Self;
122
123                        fn sub(self, rhs: Self) -> Self::Output {
124                            Self::from_base_value(self.raw() - rhs.raw())
125                        }
126                    }
127
128                    impl core::ops::SubAssign for $quantity_name {
129                        fn sub_assign(&mut self, rhs: Self) {
130                            *self = *self - rhs;
131                        }
132                    }
133
134                    impl core::ops::Mul<$storage_ty> for $quantity_name {
135                        type Output = Self;
136
137                        fn mul(self, rhs: $storage_ty) -> Self::Output {
138                            Self::from_base_value(self.raw() * rhs)
139                        }
140                    }
141
142                    impl core::ops::MulAssign<$storage_ty> for $quantity_name {
143                        fn mul_assign(&mut self, rhs: $storage_ty) {
144                            *self = *self * rhs;
145                        }
146                    }
147
148                    impl core::ops::Div<$storage_ty> for $quantity_name {
149                        type Output = Self;
150
151                        fn div(self, rhs: $storage_ty) -> Self::Output {
152                            Self::from_base_value(self.raw() / rhs)
153                        }
154                    }
155
156                    impl core::ops::DivAssign<$storage_ty> for $quantity_name {
157                        fn div_assign(&mut self, rhs: $storage_ty) {
158                            *self = *self / rhs;
159                        }
160                    }
161
162                    impl core::ops::Neg for $quantity_name {
163                        type Output = Self;
164
165                        fn neg(self) -> Self::Output {
166                            Self::from_base_value(-self.raw())
167                        }
168                    }
169
170                    impl Serialize for $quantity_name {
171                        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
172                        where
173                            S: Serializer,
174                        {
175                            self.raw().serialize(serializer)
176                        }
177                    }
178
179                    impl<'de> Deserialize<'de> for $quantity_name {
180                        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
181                        where
182                            D: Deserializer<'de>,
183                        {
184                            let value = <$storage_ty>::deserialize(deserializer)?;
185                            Ok(Self::from_base_value(value))
186                        }
187                    }
188
189                    impl bincode::ValueDecode for $quantity_name {
190                        const DECODE: &'static bincode::ValueDecodeSpec = <$storage_ty as bincode::ValueDecode>::DECODE;
191                        const METADATA: &'static [bincode::value_decode::ValueMetadata] = &[
192                            bincode::value_decode::ValueMetadata::Quantity(
193                                cu29_value_types::QuantityMetadata::coherent(cu29_value_types::Quantity::$quantity_name),
194                            ),
195                        ];
196                    }
197
198                    impl cu29_traits::DebugScalarType for $quantity_name {
199                        fn debug_scalar_registration() -> cu29_traits::DebugScalarRegistration {
200                            cu29_traits::DebugScalarRegistration {
201                                type_path: core::any::type_name::<Self>(),
202                                scalar_kind: if stringify!($storage_ty) == "f32" {
203                                    cu29_traits::DebugScalarKind::F32
204                                } else {
205                                    cu29_traits::DebugScalarKind::F64
206                                },
207                                semantics: cu29_traits::DebugFieldSemantics::Quantity {
208                                    quantity_name: alloc::string::String::from(stringify!($quantity_name)),
209                                    unit_symbol: alloc::string::String::from(Self::STORAGE_UNIT),
210                                },
211                            }
212                        }
213                    }
214
215                    impl bincode::Encode for $quantity_name {
216                        fn encode<E: bincode::enc::Encoder>(
217                            &self,
218                            encoder: &mut E,
219                        ) -> Result<(), bincode::error::EncodeError> {
220                            bincode::Encode::encode(&self.raw(), encoder)
221                        }
222                    }
223
224                    impl<Context> bincode::Decode<Context> for $quantity_name {
225                        fn decode<D: bincode::de::Decoder<Context = Context>>(
226                            decoder: &mut D,
227                        ) -> Result<Self, bincode::error::DecodeError> {
228                            let value: $storage_ty = bincode::Decode::decode(decoder)?;
229                            Ok(Self::from_base_value(value))
230                        }
231                    }
232
233                    impl<'de, Context> bincode::BorrowDecode<'de, Context> for $quantity_name {
234                        fn borrow_decode<D: bincode::de::BorrowDecoder<'de, Context = Context>>(
235                            decoder: &mut D,
236                        ) -> Result<Self, bincode::error::DecodeError> {
237                            <Self as bincode::Decode<Context>>::decode(decoder)
238                        }
239                    }
240
241                    #[cfg(feature = "reflect")]
242                    impl bevy_reflect::FromReflect for $quantity_name {
243                        fn from_reflect(
244                            reflect: &dyn bevy_reflect::PartialReflect,
245                        ) -> Option<Self> {
246                            if let Some(existing) = reflect.try_downcast_ref::<Self>() {
247                                return Some(*existing);
248                            }
249
250                            reflect
251                                .try_downcast_ref::<$storage_ty>()
252                                .map(|value| Self::from_base_value(*value))
253                        }
254                    }
255                };
256            }
257
258            $(define_quantity!($unit_mod, $quantity, $symbol);)+
259
260            pub(crate) fn debug_scalar_registrations() -> alloc::vec::Vec<cu29_traits::DebugScalarRegistration> {
261                alloc::vec![$(<$quantity as cu29_traits::DebugScalarType>::debug_scalar_registration()),+]
262            }
263
264            #[cfg(all(test, feature = "reflect"))]
265            #[test]
266            fn test_quantity_symbols_and_scalar_encoding() {
267                let registrations = debug_scalar_registrations();
268                assert_eq!(registrations.len(), cu29_value_types::Quantity::ALL.len());
269                $(
270                    let quantity = cu29_value_types::QuantityMetadata::coherent(cu29_value_types::Quantity::$quantity);
271                    let registration = registrations.iter().find(|entry| entry.type_path == core::any::type_name::<$quantity>()).unwrap();
272                    let cu29_traits::DebugFieldSemantics::Quantity { unit_symbol, .. } = &registration.semantics else {
273                        panic!("expected quantity semantics");
274                    };
275                    assert_eq!(unit_symbol, quantity.storage_unit().symbol());
276                    assert_eq!(<$quantity as bincode::ValueDecode>::METADATA, &[bincode::value_decode::ValueMetadata::Quantity(quantity)]);
277                    for raw in [0.0, -1.25, 1234.5] {
278                        let value = $quantity::from_base_value(raw);
279                        let config = bincode::config::standard();
280                        let encoded = bincode::encode_to_vec(value, config).unwrap();
281                        assert_eq!(encoded, bincode::encode_to_vec(raw, config).unwrap());
282                        let (decoded, used): ($quantity, usize) = bincode::decode_from_slice(&encoded, config).unwrap();
283                        assert_eq!(decoded.raw(), raw);
284                        assert_eq!(used, encoded.len());
285                    }
286                )+
287            }
288
289
290        }
291    };
292}
293
294pub mod si {
295    pub use uom::si::{
296        ISQ, SI, absement, acceleration, action, amount_of_substance, angle, angular_absement,
297        angular_acceleration, angular_jerk, angular_momentum, angular_velocity, area,
298        areal_density_of_states, areal_heat_capacity, areal_mass_density, areal_number_density,
299        areal_number_rate, available_energy, capacitance, catalytic_activity,
300        catalytic_activity_concentration, curvature, diffusion_coefficient, dynamic_viscosity,
301        electric_charge, electric_charge_areal_density, electric_charge_linear_density,
302        electric_charge_volumetric_density, electric_current, electric_current_density,
303        electric_dipole_moment, electric_displacement_field, electric_field, electric_flux,
304        electric_permittivity, electric_potential, electric_quadrupole_moment,
305        electrical_conductance, electrical_conductivity, electrical_mobility,
306        electrical_resistance, electrical_resistivity, energy, force, frequency, frequency_drift,
307        heat_capacity, heat_flux_density, heat_transfer, inductance, information, information_rate,
308        inverse_velocity, jerk, kinematic_viscosity, length, linear_density_of_states,
309        linear_mass_density, linear_number_density, linear_number_rate, linear_power_density,
310        luminance, luminous_intensity, magnetic_field_strength, magnetic_flux,
311        magnetic_flux_density, magnetic_moment, magnetic_permeability, mass, mass_concentration,
312        mass_density, mass_flux, mass_per_energy, mass_rate, molality, molar_concentration,
313        molar_energy, molar_flux, molar_heat_capacity, molar_mass, molar_radioactivity,
314        molar_volume, moment_of_inertia, momentum, power, power_rate, pressure, radiant_exposure,
315        radioactivity, ratio, reciprocal_length, solid_angle, specific_area,
316        specific_heat_capacity, specific_power, specific_radioactivity, specific_volume,
317        surface_electric_current_density, surface_tension, temperature_coefficient,
318        temperature_gradient, temperature_interval, thermal_conductance, thermal_conductivity,
319        thermal_resistance, thermodynamic_temperature, time, torque, velocity, volume, volume_rate,
320        volumetric_density_of_states, volumetric_heat_capacity, volumetric_number_density,
321        volumetric_number_rate, volumetric_power_density,
322    };
323
324    cu29_value_types::__quantity_catalogue!(define_storage_wrappers, f32, f32);
325    cu29_value_types::__quantity_catalogue!(define_storage_wrappers, f64, f64);
326}
327
328/// Metadata and compile-time normalization support for constants declared in Copper RON files.
329#[doc(hidden)]
330pub mod constant {
331    /// Storage-independent metadata for a supported physical quantity.
332    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
333    pub struct QuantityDefinition {
334        pub quantity: Quantity,
335        pub quantity_name: &'static str,
336        pub rust_type_f32: &'static str,
337        pub rust_type_f64: &'static str,
338        pub coherent_unit: &'static str,
339        pub preferred_display_unit: &'static str,
340        pub compatible_units: &'static [&'static str],
341    }
342
343    macro_rules! define_constant_catalogue {
344        ($(($unit_mod:ident, $quantity:ident, $coherent_unit:ident, $display_unit:ident,
345            [$($alternative_unit:ident),* $(,)?])),+ $(,)?) => {
346            /// Physical quantities accepted by the top-level `constants:` configuration.
347            #[allow(non_camel_case_types)]
348            #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
349            pub enum Quantity {
350                $($unit_mod,)+
351            }
352
353            impl Quantity {
354                pub const fn name(self) -> &'static str {
355                    match self {
356                        $(Self::$unit_mod => stringify!($unit_mod),)+
357                    }
358                }
359            }
360
361            /// Unit names accepted by the top-level `constants:` configuration.
362            #[allow(non_camel_case_types)]
363            #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
364            pub enum Unit {
365                $($coherent_unit, $($alternative_unit,)*)+
366            }
367
368            impl Unit {
369                pub const fn name(self) -> &'static str {
370                    match self {
371                        $(
372                            Self::$coherent_unit => stringify!($coherent_unit),
373                            $(Self::$alternative_unit => stringify!($alternative_unit),)*
374                        )+
375                    }
376                }
377
378                pub fn from_name(name: &str) -> Option<Self> {
379                    match name {
380                        $(
381                            stringify!($coherent_unit) => Some(Self::$coherent_unit),
382                            $(stringify!($alternative_unit) => Some(Self::$alternative_unit),)*
383                        )+
384                        _ => None,
385                    }
386                }
387            }
388
389            pub const DEFINITIONS: &[QuantityDefinition] = &[
390                $(QuantityDefinition {
391                    quantity: Quantity::$unit_mod,
392                    quantity_name: stringify!($unit_mod),
393                    rust_type_f32: concat!("cu29::units::si::f32::", stringify!($quantity)),
394                    rust_type_f64: concat!("cu29::units::si::f64::", stringify!($quantity)),
395                    coherent_unit: stringify!($coherent_unit),
396                    preferred_display_unit: stringify!($display_unit),
397                    compatible_units: &[
398                        stringify!($coherent_unit),
399                        $(stringify!($alternative_unit),)*
400                    ],
401                },)+
402            ];
403
404            pub fn definition(quantity: Quantity) -> Option<&'static QuantityDefinition> {
405                DEFINITIONS
406                    .iter()
407                    .find(|definition| definition.quantity == quantity)
408            }
409
410            pub fn normalize_f32(quantity: Quantity, unit: Unit, value: f32) -> Option<f32> {
411                match quantity {
412                    $(Quantity::$unit_mod => match unit {
413                        Unit::$coherent_unit => Some(
414                            crate::si::f32::$quantity::new::<
415                                crate::uom::si::$unit_mod::$coherent_unit
416                            >(value).raw()
417                        ),
418                        $(Unit::$alternative_unit => Some(
419                            crate::si::f32::$quantity::new::<
420                                crate::uom::si::$unit_mod::$alternative_unit
421                            >(value).raw()
422                        ),)*
423                        _ => None,
424                    },)+
425                }
426            }
427
428            pub fn normalize_f64(quantity: Quantity, unit: Unit, value: f64) -> Option<f64> {
429                match quantity {
430                    $(Quantity::$unit_mod => match unit {
431                        Unit::$coherent_unit => Some(
432                            crate::si::f64::$quantity::new::<
433                                crate::uom::si::$unit_mod::$coherent_unit
434                            >(value).raw()
435                        ),
436                        $(Unit::$alternative_unit => Some(
437                            crate::si::f64::$quantity::new::<
438                                crate::uom::si::$unit_mod::$alternative_unit
439                            >(value).raw()
440                        ),)*
441                        _ => None,
442                    },)+
443                }
444            }
445        };
446
447    }
448
449    // Every coherent unit here maps one input unit to one unit in uom's underlying SI storage.
450    define_constant_catalogue! {
451        (length, Length, meter, meter,
452            [millimeter, centimeter, kilometer, inch, foot]),
453        (angle, Angle, radian, radian, [degree, revolution]),
454        (mass, Mass, kilogram, gram, [gram, milligram, pound]),
455        (time, Time, second, second, [millisecond, microsecond, minute, hour]),
456        (thermodynamic_temperature, ThermodynamicTemperature, kelvin, kelvin,
457            [degree_celsius, degree_fahrenheit, degree_rankine]),
458        (velocity, Velocity, meter_per_second, meter_per_second,
459            [kilometer_per_hour, foot_per_second, knot]),
460        (acceleration, Acceleration, meter_per_second_squared, meter_per_second_squared,
461            [standard_gravity, foot_per_second_squared]),
462        (angular_velocity, AngularVelocity, radian_per_second, radian_per_second,
463            [degree_per_second, revolution_per_minute]),
464        (angular_acceleration, AngularAcceleration, radian_per_second_squared,
465            radian_per_second_squared, [degree_per_second_squared]),
466        (frequency, Frequency, hertz, hertz, [kilohertz, megahertz]),
467        (force, Force, newton, newton, [kilonewton, pound_force]),
468        (pressure, Pressure, pascal, pascal,
469            [kilopascal, bar, pound_force_per_square_inch]),
470        (energy, Energy, joule, joule, [kilojoule, watt_hour]),
471        (power, Power, watt, watt, [kilowatt, horsepower]),
472        (electric_potential, ElectricPotential, volt, volt, [millivolt]),
473        (electric_current, ElectricCurrent, ampere, ampere, [milliampere]),
474        (ratio, Ratio, ratio, ratio, [percent]),
475        (area, Area, square_meter, square_meter,
476            [square_millimeter, square_centimeter]),
477        (volume, Volume, cubic_meter, cubic_meter, [liter, milliliter]),
478    }
479}
480
481use alloc::vec::Vec;
482
483/// Register every supported quantity in both scalar widths for the debugger.
484pub fn debug_scalar_registrations() -> Vec<cu29_traits::DebugScalarRegistration> {
485    let mut registrations = si::f32::debug_scalar_registrations();
486    registrations.extend(si::f64::debug_scalar_registrations());
487    registrations
488}
489
490#[cfg(all(test, feature = "reflect"))]
491mod tests {
492    use super::si::f32::Velocity;
493    use super::si::velocity::{kilometer_per_hour, meter_per_second};
494    use bevy_reflect::{PartialReflect, Reflect, ReflectRef};
495
496    #[derive(Reflect)]
497    #[reflect(from_reflect = false)]
498    struct Msg {
499        speed: Velocity,
500    }
501
502    #[test]
503    fn reflect_velocity_exposes_value_field() {
504        let msg = Msg {
505            speed: Velocity::new::<kilometer_per_hour>(36.0),
506        };
507
508        assert!(matches!(
509            msg.speed.as_partial_reflect().reflect_ref(),
510            ReflectRef::Struct(_)
511        ));
512        assert_eq!(msg.speed.get::<meter_per_second>(), 10.0);
513
514        let speed_reflected = match msg.as_partial_reflect().reflect_ref() {
515            ReflectRef::Struct(s) => s.field("speed").expect("speed field should exist"),
516            _ => panic!("expected struct reflection"),
517        };
518
519        let speed = speed_reflected
520            .try_downcast_ref::<Velocity>()
521            .expect("speed should downcast to cu29_units::si::f32::Velocity");
522        assert_eq!(speed.raw(), 10.0);
523        assert_eq!(speed.value, 10.0);
524        assert_eq!(speed.get::<meter_per_second>(), 10.0);
525    }
526}
527
528#[cfg(test)]
529mod storage_unit_tests {
530    use super::si::f32;
531    use bincode::ValueDecode;
532
533    fn symbol<T: ValueDecode>() -> &'static str {
534        let [bincode::value_decode::ValueMetadata::Quantity(quantity)] = T::METADATA else {
535            panic!("expected typed quantity metadata")
536        };
537        quantity.storage_unit().symbol()
538    }
539
540    #[test]
541    fn test_conventional_si_storage_units() {
542        assert_eq!(symbol::<f32::ElectricPotential>(), "V");
543        assert_eq!(symbol::<f32::MagneticFluxDensity>(), "T");
544        assert_eq!(symbol::<f32::Force>(), "N");
545        assert_eq!(symbol::<f32::Pressure>(), "Pa");
546        assert_eq!(symbol::<f32::Energy>(), "J");
547        assert_eq!(symbol::<f32::Torque>(), "N·m");
548        assert_eq!(symbol::<f32::Power>(), "W");
549        assert_eq!(symbol::<f32::Frequency>(), "Hz");
550        assert_eq!(symbol::<f32::AngularVelocity>(), "rad·s⁻¹");
551        assert_eq!(symbol::<f32::ElectricalResistance>(), "Ω");
552        assert_eq!(symbol::<f32::Absement>(), "m·s");
553        assert_eq!(symbol::<f32::Velocity>(), "m·s⁻¹");
554        assert_eq!(symbol::<f32::Acceleration>(), "m·s⁻²");
555        assert_eq!(symbol::<f32::Area>(), "m²");
556        assert_eq!(symbol::<f32::Volume>(), "m³");
557        assert_eq!(symbol::<f32::Mass>(), "kg");
558        assert_eq!(symbol::<f32::Angle>(), "rad");
559        assert_eq!(symbol::<f32::SolidAngle>(), "sr");
560        assert_eq!(symbol::<f32::Information>(), "bit");
561        assert_eq!(symbol::<f32::InformationRate>(), "bit·s⁻¹");
562        assert_eq!(symbol::<f32::Ratio>(), "1");
563    }
564
565    #[test]
566    fn test_named_units_match_storage_scale() {
567        assert_eq!(
568            f32::ElectricPotential::new::<super::si::electric_potential::millivolt>(1000.0).raw(),
569            1.0
570        );
571        assert_eq!(f32::Mass::new::<super::si::mass::gram>(1000.0).raw(), 1.0);
572        assert_eq!(
573            f32::Pressure::new::<super::si::pressure::kilopascal>(1.0).raw(),
574            1000.0
575        );
576    }
577}