|
|
@ -2,133 +2,303 @@ |
|
|
|
|
|
|
|
|
|
|
|
use std::mem::MaybeUninit; |
|
|
|
use std::mem::MaybeUninit; |
|
|
|
|
|
|
|
|
|
|
|
use crate::AspectRatio; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// A 4-vector of `u8`s.
|
|
|
|
/// A 4-vector of `u8`s.
|
|
|
|
pub struct IntVec(citro3d_sys::C3D_IVec); |
|
|
|
#[doc(alias = "C3D_IVec")] |
|
|
|
|
|
|
|
pub struct IVec(citro3d_sys::C3D_IVec); |
|
|
|
|
|
|
|
|
|
|
|
/// A 4-vector of `f32`s.
|
|
|
|
/// A 4-vector of `f32`s.
|
|
|
|
pub struct FloatVec(citro3d_sys::C3D_FVec); |
|
|
|
#[doc(alias = "C3D_FVec")] |
|
|
|
|
|
|
|
pub struct FVec(citro3d_sys::C3D_FVec); |
|
|
|
|
|
|
|
|
|
|
|
/// A quaternion, internally represented the same way as [`FloatVec`].
|
|
|
|
/// A quaternion, internally represented the same way as [`FVec`].
|
|
|
|
pub struct Quaternion(citro3d_sys::C3D_FQuat); |
|
|
|
#[doc(alias = "C3D_FQuat")] |
|
|
|
|
|
|
|
pub struct FQuat(citro3d_sys::C3D_FQuat); |
|
|
|
|
|
|
|
|
|
|
|
/// A 4x4 row-major matrix of `f32`s.
|
|
|
|
/// A 4x4 row-major matrix of `f32`s.
|
|
|
|
|
|
|
|
#[doc(alias = "C3D_Mtx")] |
|
|
|
pub struct Matrix(citro3d_sys::C3D_Mtx); |
|
|
|
pub struct Matrix(citro3d_sys::C3D_Mtx); |
|
|
|
|
|
|
|
|
|
|
|
impl Matrix { |
|
|
|
impl Matrix { |
|
|
|
// TODO: does it make sense to have a helper that builds both left and right
|
|
|
|
/// Construct the identity matrix.
|
|
|
|
// eyes for stereoscopic at the same time?
|
|
|
|
#[doc(alias = "Mtx_Identity")] |
|
|
|
|
|
|
|
pub fn identity() -> Self { |
|
|
|
|
|
|
|
let mut out = MaybeUninit::uninit(); |
|
|
|
|
|
|
|
unsafe { |
|
|
|
|
|
|
|
citro3d_sys::Mtx_Identity(out.as_mut_ptr()); |
|
|
|
|
|
|
|
Self(out.assume_init()) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// Construct a projection matrix suitable for projecting 3D world space onto
|
|
|
|
/// Construct a projection matrix suitable for projecting 3D world space onto
|
|
|
|
/// the 3DS screens.
|
|
|
|
/// the 3DS screens.
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// # Parameters
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// * `vertical_fov_radians`: the vertical field of view, measured in radians
|
|
|
|
|
|
|
|
/// * `aspect_ratio`: The aspect ratio of the projection
|
|
|
|
|
|
|
|
/// * `orientation`: the orientation of the projection with respect to the screen
|
|
|
|
|
|
|
|
/// * `coordinates`: the handedness of the coordinate system to use
|
|
|
|
|
|
|
|
/// * `stereo`: if specified, the offset to displace the projection by
|
|
|
|
|
|
|
|
/// for stereoscopic rendering.
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// # Examples
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// ```
|
|
|
|
|
|
|
|
/// # use citro3d::math::*;
|
|
|
|
|
|
|
|
/// # use std::f32::consts::PI;
|
|
|
|
|
|
|
|
/// #
|
|
|
|
|
|
|
|
/// # let _runner = test_runner::GdbRunner::default();
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// let clip_planes = ClipPlanes {
|
|
|
|
|
|
|
|
/// near: 0.01,
|
|
|
|
|
|
|
|
/// far: 100.0,
|
|
|
|
|
|
|
|
/// };
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// let center = Matrix::perspective_projection(
|
|
|
|
|
|
|
|
/// PI / 4.0,
|
|
|
|
|
|
|
|
/// AspectRatio::BottomScreen,
|
|
|
|
|
|
|
|
/// ScreenOrientation::Rotated,
|
|
|
|
|
|
|
|
/// clip_planes,
|
|
|
|
|
|
|
|
/// CoordinateOrientation::LeftHanded,
|
|
|
|
|
|
|
|
/// None,
|
|
|
|
|
|
|
|
/// );
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// let right_eye = Matrix::perspective_projection(
|
|
|
|
|
|
|
|
/// PI / 4.0,
|
|
|
|
|
|
|
|
/// AspectRatio::BottomScreen,
|
|
|
|
|
|
|
|
/// ScreenOrientation::Rotated,
|
|
|
|
|
|
|
|
/// clip_planes,
|
|
|
|
|
|
|
|
/// CoordinateOrientation::LeftHanded,
|
|
|
|
|
|
|
|
/// Some(StereoDisplacement {
|
|
|
|
|
|
|
|
/// displacement: 1.0,
|
|
|
|
|
|
|
|
/// screen_depth: 2.0,
|
|
|
|
|
|
|
|
/// }),
|
|
|
|
|
|
|
|
/// );
|
|
|
|
|
|
|
|
/// ```
|
|
|
|
|
|
|
|
#[doc(alias = "Mtx_Persp")] |
|
|
|
|
|
|
|
#[doc(alias = "Mtx_PerspTilt")] |
|
|
|
pub fn perspective_projection( |
|
|
|
pub fn perspective_projection( |
|
|
|
vertical_fov: f32, |
|
|
|
vertical_fov_radians: f32, |
|
|
|
aspect_ratio: AspectRatio, |
|
|
|
aspect_ratio: AspectRatio, |
|
|
|
orientation: Orientation, |
|
|
|
orientation: ScreenOrientation, |
|
|
|
clip_plane: ClipPlane, |
|
|
|
clip_plane: ClipPlanes, |
|
|
|
stereo: Stereoscopic, |
|
|
|
coordinates: CoordinateOrientation, |
|
|
|
coordinates: CoordinateSystem, |
|
|
|
stereo: Option<StereoDisplacement>, |
|
|
|
) -> Self { |
|
|
|
) -> Self { |
|
|
|
let (make_mtx_persp, make_mtx_stereo); |
|
|
|
let mut result = MaybeUninit::uninit(); |
|
|
|
|
|
|
|
|
|
|
|
let initialize_mtx: &dyn Fn(_, _, _, _, _, _) -> _ = match stereo { |
|
|
|
|
|
|
|
Stereoscopic::Mono => { |
|
|
|
|
|
|
|
let make_mtx = match orientation { |
|
|
|
|
|
|
|
Orientation::Natural => citro3d_sys::Mtx_PerspTilt, |
|
|
|
|
|
|
|
Orientation::HardwareDefault => citro3d_sys::Mtx_Persp, |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
make_mtx_persp = move |a, b, c, d, e, f| unsafe { make_mtx(a, b, c, d, e, f) }; |
|
|
|
let left_handed = matches!(coordinates, CoordinateOrientation::LeftHanded); |
|
|
|
&make_mtx_persp |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
Stereoscopic::Stereo { |
|
|
|
|
|
|
|
interocular_distance, |
|
|
|
|
|
|
|
screen_depth, |
|
|
|
|
|
|
|
} => { |
|
|
|
|
|
|
|
let make_mtx = match orientation { |
|
|
|
|
|
|
|
Orientation::Natural => citro3d_sys::Mtx_PerspStereoTilt, |
|
|
|
|
|
|
|
Orientation::HardwareDefault => citro3d_sys::Mtx_PerspStereo, |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
make_mtx_stereo = move |a, b, c, d, e, f| unsafe { |
|
|
|
if let Some(stereo) = stereo { |
|
|
|
make_mtx(a, b, c, d, interocular_distance, screen_depth, e, f) |
|
|
|
let initialize_mtx = orientation.perpsective_stereo_builder(); |
|
|
|
}; |
|
|
|
unsafe { |
|
|
|
&make_mtx_stereo |
|
|
|
initialize_mtx( |
|
|
|
|
|
|
|
result.as_mut_ptr(), |
|
|
|
|
|
|
|
vertical_fov_radians, |
|
|
|
|
|
|
|
aspect_ratio.into(), |
|
|
|
|
|
|
|
clip_plane.near, |
|
|
|
|
|
|
|
clip_plane.far, |
|
|
|
|
|
|
|
stereo.displacement, |
|
|
|
|
|
|
|
stereo.screen_depth, |
|
|
|
|
|
|
|
left_handed, |
|
|
|
|
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
} else { |
|
|
|
|
|
|
|
let initialize_mtx = orientation.perspective_mono_builder(); |
|
|
|
let left_handed = matches!(coordinates, CoordinateSystem::LeftHanded); |
|
|
|
unsafe { |
|
|
|
|
|
|
|
|
|
|
|
let mut result = MaybeUninit::uninit(); |
|
|
|
|
|
|
|
initialize_mtx( |
|
|
|
initialize_mtx( |
|
|
|
result.as_mut_ptr(), |
|
|
|
result.as_mut_ptr(), |
|
|
|
vertical_fov, |
|
|
|
vertical_fov_radians, |
|
|
|
aspect_ratio.into(), |
|
|
|
aspect_ratio.into(), |
|
|
|
clip_plane.near, |
|
|
|
clip_plane.near, |
|
|
|
clip_plane.far, |
|
|
|
clip_plane.far, |
|
|
|
left_handed, |
|
|
|
left_handed, |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let inner = unsafe { result.assume_init() }; |
|
|
|
let inner = unsafe { result.assume_init() }; |
|
|
|
|
|
|
|
|
|
|
|
Self(inner) |
|
|
|
Self(inner) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Helper function to build both eyes' perspective projection matrices
|
|
|
|
|
|
|
|
/// at once. See [`perspective_projection`] for a description of each
|
|
|
|
|
|
|
|
/// parameter.
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// ```
|
|
|
|
|
|
|
|
/// # use std::f32::consts::PI;
|
|
|
|
|
|
|
|
/// # use citro3d::math::*;
|
|
|
|
|
|
|
|
/// #
|
|
|
|
|
|
|
|
/// # let _runner = test_runner::GdbRunner::default();
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// let (left_eye, right_eye) = Matrix::stereo_projections(
|
|
|
|
|
|
|
|
/// PI / 4.0,
|
|
|
|
|
|
|
|
/// AspectRatio::TopScreen,
|
|
|
|
|
|
|
|
/// ScreenOrientation::Rotated,
|
|
|
|
|
|
|
|
/// ClipPlanes {
|
|
|
|
|
|
|
|
/// near: 0.01,
|
|
|
|
|
|
|
|
/// far: 100.0,
|
|
|
|
|
|
|
|
/// },
|
|
|
|
|
|
|
|
/// CoordinateOrientation::LeftHanded,
|
|
|
|
|
|
|
|
/// StereoDisplacement::new(0.5, 2.0),
|
|
|
|
|
|
|
|
/// );
|
|
|
|
|
|
|
|
/// ```
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// [`perspective_projection`]: Self::perspective_projection
|
|
|
|
|
|
|
|
#[doc(alias = "Mtx_PerspStereo")] |
|
|
|
|
|
|
|
#[doc(alias = "Mtx_PerspStereoTilt")] |
|
|
|
|
|
|
|
pub fn stereo_projections( |
|
|
|
|
|
|
|
vertical_fov_radians: f32, |
|
|
|
|
|
|
|
aspect_ratio: AspectRatio, |
|
|
|
|
|
|
|
orientation: ScreenOrientation, |
|
|
|
|
|
|
|
clip_plane: ClipPlanes, |
|
|
|
|
|
|
|
coordinates: CoordinateOrientation, |
|
|
|
|
|
|
|
(left_eye, right_eye): (StereoDisplacement, StereoDisplacement), |
|
|
|
|
|
|
|
) -> (Self, Self) { |
|
|
|
|
|
|
|
let left = Self::perspective_projection( |
|
|
|
|
|
|
|
vertical_fov_radians, |
|
|
|
|
|
|
|
aspect_ratio, |
|
|
|
|
|
|
|
orientation, |
|
|
|
|
|
|
|
clip_plane, |
|
|
|
|
|
|
|
coordinates, |
|
|
|
|
|
|
|
Some(left_eye), |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
let right = Self::perspective_projection( |
|
|
|
|
|
|
|
vertical_fov_radians, |
|
|
|
|
|
|
|
aspect_ratio, |
|
|
|
|
|
|
|
orientation, |
|
|
|
|
|
|
|
clip_plane, |
|
|
|
|
|
|
|
coordinates, |
|
|
|
|
|
|
|
Some(right_eye), |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
(left, right) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
pub(crate) fn as_raw(&self) -> *const citro3d_sys::C3D_Mtx { |
|
|
|
pub(crate) fn as_raw(&self) -> *const citro3d_sys::C3D_Mtx { |
|
|
|
&self.0 |
|
|
|
&self.0 |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// Whether to use left-handed or right-handed coordinates for calculations.
|
|
|
|
/// The [orientation](https://en.wikipedia.org/wiki/Orientation_(geometry))
|
|
|
|
|
|
|
|
/// (or "handedness") of the coordinate system.
|
|
|
|
#[derive(Clone, Copy, Debug)] |
|
|
|
#[derive(Clone, Copy, Debug)] |
|
|
|
pub enum CoordinateSystem { |
|
|
|
pub enum CoordinateOrientation { |
|
|
|
|
|
|
|
/// A left-handed coordinate system.
|
|
|
|
LeftHanded, |
|
|
|
LeftHanded, |
|
|
|
|
|
|
|
/// A right-handed coordinate system.
|
|
|
|
RightHanded, |
|
|
|
RightHanded, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// Whether to rotate a projection to account for the 3DS screen configuration.
|
|
|
|
/// Whether to rotate a projection to account for the 3DS screen orientation.
|
|
|
|
/// Both screens on the 3DS are oriented such that the "top" of the screen is
|
|
|
|
/// Both screens on the 3DS are oriented such that the "top-left" of the screen
|
|
|
|
/// on the [left | right] ? side of the device when it's held normally, so
|
|
|
|
/// in framebuffer coordinates is the physical bottom-left of the screen
|
|
|
|
/// projections must account for this extra rotation to display in the correct
|
|
|
|
/// (i.e. the "width" is smaller than the "height").
|
|
|
|
/// orientation.
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug)] |
|
|
|
#[derive(Clone, Copy, Debug)] |
|
|
|
pub enum Orientation { |
|
|
|
pub enum ScreenOrientation { |
|
|
|
/// Rotate the projection 90° to account for the 3DS screen rotation.
|
|
|
|
/// Rotate 90° clockwise to account for the 3DS screen rotation. Most
|
|
|
|
Natural, |
|
|
|
/// applications will use this variant.
|
|
|
|
/// Don't rotate the projection at all.
|
|
|
|
Rotated, |
|
|
|
HardwareDefault, |
|
|
|
/// Do not apply any extra rotation to the projection.
|
|
|
|
|
|
|
|
None, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug)] |
|
|
|
impl ScreenOrientation { |
|
|
|
// TODO: better name
|
|
|
|
fn perspective_mono_builder( |
|
|
|
pub enum Stereoscopic { |
|
|
|
self, |
|
|
|
Mono, |
|
|
|
) -> unsafe extern "C" fn(*mut citro3d_sys::C3D_Mtx, f32, f32, f32, f32, bool) { |
|
|
|
Stereo { |
|
|
|
match self { |
|
|
|
interocular_distance: f32, |
|
|
|
Self::Rotated => citro3d_sys::Mtx_PerspTilt, |
|
|
|
// TODO: better name? At least docstring
|
|
|
|
Self::None => citro3d_sys::Mtx_Persp, |
|
|
|
screen_depth: f32, |
|
|
|
} |
|
|
|
}, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fn perpsective_stereo_builder( |
|
|
|
impl Stereoscopic { |
|
|
|
self, |
|
|
|
/// Flip the stereoscopic projection for the opposite eye.
|
|
|
|
) -> unsafe extern "C" fn(*mut citro3d_sys::C3D_Mtx, f32, f32, f32, f32, f32, f32, bool) { |
|
|
|
pub fn invert(self) -> Self { |
|
|
|
match self { |
|
|
|
|
|
|
|
Self::Rotated => citro3d_sys::Mtx_PerspStereoTilt, |
|
|
|
|
|
|
|
Self::None => citro3d_sys::Mtx_PerspStereo, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: orthographic projections
|
|
|
|
|
|
|
|
fn ortho_builder( |
|
|
|
|
|
|
|
self, |
|
|
|
|
|
|
|
) -> unsafe extern "C" fn(*mut citro3d_sys::C3D_Mtx, f32, f32, f32, f32, f32, f32, bool) { |
|
|
|
match self { |
|
|
|
match self { |
|
|
|
Self::Stereo { |
|
|
|
Self::Rotated => citro3d_sys::Mtx_OrthoTilt, |
|
|
|
interocular_distance, |
|
|
|
Self::None => citro3d_sys::Mtx_Ortho, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Configuration for calculating stereoscopic projections.
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug)] |
|
|
|
|
|
|
|
pub struct StereoDisplacement { |
|
|
|
|
|
|
|
/// The horizontal offset of the eye from center. Negative values
|
|
|
|
|
|
|
|
/// correspond to the left eye, and positive values to the right eye.
|
|
|
|
|
|
|
|
pub displacement: f32, |
|
|
|
|
|
|
|
/// The position of the screen, which determines the focal length. Objects
|
|
|
|
|
|
|
|
/// closer than this depth will appear to pop out of the screen, and objects
|
|
|
|
|
|
|
|
/// further than this will appear inside the screen.
|
|
|
|
|
|
|
|
pub screen_depth: f32, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl StereoDisplacement { |
|
|
|
|
|
|
|
/// Construct displacement for the left and right eyes simulataneously.
|
|
|
|
|
|
|
|
/// The given `interocular_distance` describes the distance between the two
|
|
|
|
|
|
|
|
/// rendered "eyes". A negative value will be treated the same as a positive
|
|
|
|
|
|
|
|
/// value of the same magnitude.
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// See struct documentation for details about the
|
|
|
|
|
|
|
|
/// [`screen_depth`](Self::screen_depth) parameter.
|
|
|
|
|
|
|
|
pub fn new(interocular_distance: f32, screen_depth: f32) -> (Self, Self) { |
|
|
|
|
|
|
|
let displacement = interocular_distance.abs() / 2.0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let left_eye = Self { |
|
|
|
|
|
|
|
displacement: -displacement, |
|
|
|
screen_depth, |
|
|
|
screen_depth, |
|
|
|
} => Self::Stereo { |
|
|
|
}; |
|
|
|
interocular_distance: -interocular_distance, |
|
|
|
let right_eye = Self { |
|
|
|
|
|
|
|
displacement, |
|
|
|
screen_depth, |
|
|
|
screen_depth, |
|
|
|
}, |
|
|
|
}; |
|
|
|
mono => mono, |
|
|
|
|
|
|
|
} |
|
|
|
(left_eye, right_eye) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Configuration for the [frustum](https://en.wikipedia.org/wiki/Viewing_frustum)
|
|
|
|
|
|
|
|
/// of a perspective projection.
|
|
|
|
#[derive(Clone, Copy, Debug)] |
|
|
|
#[derive(Clone, Copy, Debug)] |
|
|
|
pub struct ClipPlane { |
|
|
|
pub struct ClipPlanes { |
|
|
|
|
|
|
|
/// The z-depth of the near clip plane.
|
|
|
|
pub near: f32, |
|
|
|
pub near: f32, |
|
|
|
|
|
|
|
/// The z-depth of the far clip plane.
|
|
|
|
pub far: f32, |
|
|
|
pub far: f32, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// The aspect ratio of a projection plane.
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug)] |
|
|
|
|
|
|
|
#[non_exhaustive] |
|
|
|
|
|
|
|
pub enum AspectRatio { |
|
|
|
|
|
|
|
/// The aspect ratio of the 3DS' top screen (per-eye).
|
|
|
|
|
|
|
|
#[doc(alias = "C3D_AspectRatioTop")] |
|
|
|
|
|
|
|
TopScreen, |
|
|
|
|
|
|
|
/// The aspect ratio of the 3DS' bottom screen.
|
|
|
|
|
|
|
|
#[doc(alias = "C3D_AspectRatioBot")] |
|
|
|
|
|
|
|
BottomScreen, |
|
|
|
|
|
|
|
/// A custom aspect ratio (should be calcualted as `width / height`).
|
|
|
|
|
|
|
|
Other(f32), |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl From<AspectRatio> for f32 { |
|
|
|
|
|
|
|
fn from(ratio: AspectRatio) -> Self { |
|
|
|
|
|
|
|
match ratio { |
|
|
|
|
|
|
|
AspectRatio::TopScreen => citro3d_sys::C3D_AspectRatioTop as f32, |
|
|
|
|
|
|
|
AspectRatio::BottomScreen => citro3d_sys::C3D_AspectRatioBot as f32, |
|
|
|
|
|
|
|
AspectRatio::Other(ratio) => ratio, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|