| 382 |
382 |
|
//! that one page: audiofiles' library browser and goingson's filters are the
|
| 383 |
383 |
|
//! same shape.
|
| 384 |
384 |
|
//!
|
|
385 |
+ |
//! 0.32.0 is the slider's real shape. **The data of a slider is a fraction and
|
|
386 |
+ |
//! a function taking numbers to numbers** (Max, 2026-08-21), so [`Curve`]
|
|
387 |
+ |
//! arrives and [`Field::curve`] with it. [`Field::min`] and [`Field::max`] were
|
|
388 |
+ |
//! never the control's extent: a slider's extent is always 0 to 1, and the
|
|
389 |
+ |
//! bounds are `f(0)` and `f(1)`. Linear is the constant-slope case, which is
|
|
390 |
+ |
//! why the mapping was invisible — under it the extent and the bounds coincide
|
|
391 |
+ |
//! numerically — and why four renderers each hard-coded it without anyone
|
|
392 |
+ |
//! deciding to.
|
|
393 |
+ |
//!
|
|
394 |
+ |
//! - It is not a scale flag on a range. The question this replaced asked
|
|
395 |
+ |
//! whether to name a decoration; what was unnamed is half of what a slider
|
|
396 |
+ |
//! *is*, which is why the member is a mapping and not an adjective.
|
|
397 |
+ |
//! - **The step spacing rides on the curve.** Max, in the same breath: if the
|
|
398 |
+ |
//! family is prescriptive anyway, the granularity belongs in it. On a slider
|
|
399 |
+ |
//! the two are one decision, and holding them apart is what let a 0-to-1
|
|
400 |
+ |
//! threshold ship as a two-position control. [`Field::step`] narrows to the
|
|
401 |
+ |
//! *typed* kinds, where there is no mapping to decide with.
|
|
402 |
+ |
//! - A closed family rather than `fn(f64) -> f64`, which is the literal reading
|
|
403 |
+ |
//! and does not cross the description boundary: a fn pointer cannot be
|
|
404 |
+ |
//! emitted into a browser and cannot be compared or hashed meaningfully,
|
|
405 |
+ |
//! which [`Field`] needs. Nothing measured wants an arbitrary function — one
|
|
406 |
+ |
//! non-linear shape across five controls, and no second shape.
|
|
407 |
+ |
//! - The mapping computes here rather than in each renderer
|
|
408 |
+ |
//! ([`Curve::value_at`], [`Curve::position_of`]), so a terminal's bar, an
|
|
409 |
+ |
//! egui slider and a browser's input cannot disagree about where a value
|
|
410 |
+ |
//! sits. This crate otherwise describes rather than computes; four copies of
|
|
411 |
+ |
//! two formulas is the cost of holding that line here.
|
|
412 |
+ |
//!
|
|
413 |
+ |
//! Additive: [`Curve::Linear`] with no step is what every range meant before,
|
|
414 |
+ |
//! so no existing site changes meaning. Consumers: audiofiles' ADSR envelope
|
|
415 |
+ |
//! (three logarithmic times) and its storage cap picker.
|
|
416 |
+ |
//!
|
| 385 |
417 |
|
//! 0.31.0 finishes the file field. [`FieldKind::File`] arrived at 0.11.0
|
| 386 |
418 |
|
//! carrying neither an accepted-types list nor a multiplicity flag, and said so
|
| 387 |
419 |
|
//! in its own doc: `accept` appeared at zero sites in either app, and a member
|
| 3734 |
3766 |
|
/// [`max_length`]: Field::max_length
|
| 3735 |
3767 |
|
/// [`min`]: Field::min
|
| 3736 |
3768 |
|
/// [`max`]: Field::max
|
|
3769 |
+ |
/// How a slider's position becomes its value, and how finely it moves.
|
|
3770 |
+ |
///
|
|
3771 |
+ |
/// **The data of a slider is a fraction and a function taking numbers to
|
|
3772 |
+ |
/// numbers.** Stated by Max 2026-08-21, and it corrects a reading this crate
|
|
3773 |
+ |
/// had carried since [`FieldKind::Range`] arrived at 0.28.0:
|
|
3774 |
+ |
/// [`min`](Field::min) and [`max`](Field::max) were never the control's extent.
|
|
3775 |
+ |
/// A slider's extent is always 0 to 1 — a thumb at 40% of a track — and the
|
|
3776 |
+ |
/// bounds are `f(0)` and `f(1)`. Linear is the constant-slope case, which is
|
|
3777 |
+ |
/// exactly why nobody noticed the function was there: when `f` is
|
|
3778 |
+ |
/// `min + t * (max - min)` the extent and the bounds coincide numerically and
|
|
3779 |
+ |
/// the mapping is invisible.
|
|
3780 |
+ |
///
|
|
3781 |
+ |
/// So this is not a scale flag bolted onto a range. Every range described
|
|
3782 |
+ |
/// before it had a mapping, and four renderers each hard-coded the same one.
|
|
3783 |
+ |
///
|
|
3784 |
+ |
/// # Why a closed family and not a function
|
|
3785 |
+ |
///
|
|
3786 |
+ |
/// `fn(f64) -> f64` is the literal reading and it does not survive the
|
|
3787 |
+ |
/// description boundary. A fn pointer cannot be emitted into a browser, and it
|
|
3788 |
+ |
/// cannot be compared or hashed in a way that means anything, which this struct
|
|
3789 |
+ |
/// needs. A named family is the same semantics with arbitrary closures given
|
|
3790 |
+ |
/// up, and nothing measured wants one: the tree has a single non-linear shape
|
|
3791 |
+ |
/// across five controls and no second shape at all.
|
|
3792 |
+ |
///
|
|
3793 |
+ |
/// # Why the step is here
|
|
3794 |
+ |
///
|
|
3795 |
+ |
/// Max, in the same breath: if the family is prescriptive anyway, the step
|
|
3796 |
+ |
/// spacing belongs in it. On a slider the granularity and the mapping are one
|
|
3797 |
+ |
/// decision — a curve chosen without saying how finely it moves is half an
|
|
3798 |
+ |
/// answer — and holding them apart is what let a 0-to-1 threshold ship as a
|
|
3799 |
+ |
/// two-position control, since the host default of 1 was applied to a mapping
|
|
3800 |
+ |
/// nobody had named. It also un-overloads [`Field::step`], which stays as it
|
|
3801 |
+ |
/// was for a *typed* value, where there is no mapping and the granularity is a
|
|
3802 |
+ |
/// plain fact about the number.
|
|
3803 |
+ |
///
|
|
3804 |
+ |
/// A future curve carrying a fact of its own — an exponent, an inflection —
|
|
3805 |
+ |
/// puts it in its own variant rather than on the struct, which is the second
|
|
3806 |
+ |
/// reason this shape is right.
|
|
3807 |
+ |
///
|
|
3808 |
+ |
/// **The step is in the value's own units under every curve.** What a curve
|
|
3809 |
+ |
/// changes is the mapping, not the units the granularity is measured in: a step
|
|
3810 |
+ |
/// of `0.001` on an envelope time is three decimals whether the track is
|
|
3811 |
+ |
/// logarithmic or not, and a renderer that reads the step for display precision
|
|
3812 |
+ |
/// keeps reading it the same way.
|
|
3813 |
+ |
///
|
|
3814 |
+ |
/// Added 0.32.0, from audiofiles' ADSR envelope and its storage cap picker.
|
|
3815 |
+ |
#[non_exhaustive]
|
|
3816 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
3817 |
+ |
pub enum Curve<'a> {
|
|
3818 |
+ |
/// Constant slope: `f(t) = min + t * (max - min)`.
|
|
3819 |
+ |
///
|
|
3820 |
+ |
/// What every described range meant before this enum existed, and the
|
|
3821 |
+ |
/// default, so a site that says nothing is correct unchanged.
|
|
3822 |
+ |
Linear {
|
|
3823 |
+ |
/// The granularity, in the value's own units. `None` is the host's own.
|
|
3824 |
+ |
step: Option<&'a str>,
|
|
3825 |
+ |
},
|
|
3826 |
+ |
/// Constant ratio: `f(t) = min * (max / min).powf(t)`.
|
|
3827 |
+ |
///
|
|
3828 |
+ |
/// The mapping for a question whose extent spans orders of magnitude and
|
|
3829 |
+ |
/// whose interesting half is the small end. audiofiles' envelope times run
|
|
3830 |
+ |
/// 0.001 to 5 seconds, where a 5 ms attack and a 50 ms attack are audibly
|
|
3831 |
+ |
/// different instruments and a linear track puts both inside its first one
|
|
3832 |
+ |
/// percent.
|
|
3833 |
+ |
///
|
|
3834 |
+ |
/// # It needs positive bounds
|
|
3835 |
+ |
///
|
|
3836 |
+ |
/// A constant ratio is undefined across zero, so this asks for `min > 0`.
|
|
3837 |
+ |
/// A range that does not have that is mapped [`Linear`](Self::Linear)ly
|
|
3838 |
+ |
/// instead — see [`value_at`](Self::value_at). Stated rather than enforced,
|
|
3839 |
+ |
/// the way every other constraint in this crate is, and it is not a
|
|
3840 |
+ |
/// hypothetical: an envelope's sustain is a 0-to-1 level and is linear for
|
|
3841 |
+ |
/// this reason rather than by oversight.
|
|
3842 |
+ |
Logarithmic {
|
|
3843 |
+ |
/// The granularity, in the value's own units. `None` is the host's own.
|
|
3844 |
+ |
step: Option<&'a str>,
|
|
3845 |
+ |
},
|
|
3846 |
+ |
}
|
|
3847 |
+ |
|
|
3848 |
+ |
impl Default for Curve<'_> {
|
|
3849 |
+ |
fn default() -> Self {
|
|
3850 |
+ |
Self::Linear { step: None }
|
|
3851 |
+ |
}
|
|
3852 |
+ |
}
|
|
3853 |
+ |
|
|
3854 |
+ |
impl<'a> Curve<'a> {
|
|
3855 |
+ |
/// The granularity this curve moves in, whichever curve it is.
|
|
3856 |
+ |
///
|
|
3857 |
+ |
/// Every variant carries one, so reading it does not need a match at each
|
|
3858 |
+ |
/// of the four renderers.
|
|
3859 |
+ |
#[must_use]
|
|
3860 |
+ |
pub const fn step(self) -> Option<&'a str> {
|
|
3861 |
+ |
// No wildcard: `#[non_exhaustive]` binds downstream, not here, so a
|
|
3862 |
+ |
// curve added later has to answer this rather than fall through to a
|
|
3863 |
+ |
// granularity nobody chose.
|
|
3864 |
+ |
match self {
|
|
3865 |
+ |
Self::Linear { step } | Self::Logarithmic { step } => step,
|
|
3866 |
+ |
}
|
|
3867 |
+ |
}
|
|
3868 |
+ |
|
|
3869 |
+ |
/// Whether this curve maps as a constant ratio *given these bounds*.
|
|
3870 |
+ |
///
|
|
3871 |
+ |
/// The bounds are the argument because [`Logarithmic`](Self::Logarithmic)
|
|
3872 |
+ |
/// is a request rather than a guarantee: it needs `0 < min < max`, and a
|
|
3873 |
+ |
/// range that does not have that is drawn linearly. A renderer asks this
|
|
3874 |
+ |
/// instead of matching on the variant, so the fallback is decided in one
|
|
3875 |
+ |
/// place rather than four.
|
|
3876 |
+ |
#[must_use]
|
|
3877 |
+ |
pub fn is_ratio(self, min: f64, max: f64) -> bool {
|
|
3878 |
+ |
matches!(self, Self::Logarithmic { .. }) && min > 0.0 && max > min
|
|
3879 |
+ |
}
|
|
3880 |
+ |
|
|
3881 |
+ |
/// The value at a position along the track, where `position` is 0 to 1.
|
|
3882 |
+ |
///
|
|
3883 |
+ |
/// `f`. The whole point of the type, and it lives here rather than in each
|
|
3884 |
+ |
/// renderer so that a terminal's bar, an egui slider and a browser's input
|
|
3885 |
+ |
/// cannot disagree about where a value sits.
|
|
3886 |
+ |
///
|
|
3887 |
+ |
/// A position outside 0 to 1 is clamped, and bounds that are equal or
|
|
3888 |
+ |
/// inverted give `min` back: a track with no extent has one value on it.
|
|
3889 |
+ |
#[must_use]
|
|
3890 |
+ |
pub fn value_at(self, position: f64, min: f64, max: f64) -> f64 {
|
|
3891 |
+ |
let position = position.clamp(0.0, 1.0);
|
|
3892 |
+ |
// NaN named rather than fallen through: `max <= min` is false for a NaN
|
|
3893 |
+ |
// bound, so without it a track with no numbers on it would be mapped as
|
|
3894 |
+ |
// if it had two.
|
|
3895 |
+ |
if max <= min || min.is_nan() || max.is_nan() {
|
|
3896 |
+ |
return min;
|
|
3897 |
+ |
}
|
|
3898 |
+ |
if self.is_ratio(min, max) {
|
|
3899 |
+ |
min * (max / min).powf(position)
|
|
3900 |
+ |
} else {
|
|
3901 |
+ |
position.mul_add(max - min, min)
|
|
3902 |
+ |
}
|
|
3903 |
+ |
}
|
|
3904 |
+ |
|
|
3905 |
+ |
/// The position a value sits at, where the answer is 0 to 1.
|
|
3906 |
+ |
///
|
|
3907 |
+ |
/// `f` inverted, which is what a renderer needs to *draw* a value it was
|
|
3908 |
+ |
/// handed. Same clamping and the same degenerate answer as
|
|
3909 |
+ |
/// [`value_at`](Self::value_at).
|
|
3910 |
+ |
#[must_use]
|
|
3911 |
+ |
pub fn position_of(self, value: f64, min: f64, max: f64) -> f64 {
|
|
3912 |
+ |
if max <= min || min.is_nan() || max.is_nan() {
|
|
3913 |
+ |
return 0.0;
|
|
3914 |
+ |
}
|
|
3915 |
+ |
let value = value.clamp(min, max);
|
|
3916 |
+ |
let position = if self.is_ratio(min, max) {
|
|
3917 |
+ |
(value / min).ln() / (max / min).ln()
|
|
3918 |
+ |
} else {
|
|
3919 |
+ |
(value - min) / (max - min)
|
|
3920 |
+ |
};
|
|
3921 |
+ |
position.clamp(0.0, 1.0)
|
|
3922 |
+ |
}
|
|
3923 |
+ |
}
|
|
3924 |
+ |
|
| 3737 |
3925 |
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
| 3738 |
3926 |
|
pub struct Field<'a> {
|
| 3739 |
3927 |
|
/// What kind of value it takes.
|
| 3821 |
4009 |
|
/// Absent means the host's own granularity, which is the honest default
|
| 3822 |
4010 |
|
/// rather than a missing value: a webview's `<input>` steps by 1 unless told
|
| 3823 |
4011 |
|
/// otherwise, and that is the browser's rule and not this crate's to
|
| 3824 |
|
- |
/// restate. It matters most to [`FieldKind::Range`], where the host default
|
| 3825 |
|
- |
/// turns a 0-to-1 threshold into a two-position control, and it is not
|
| 3826 |
|
- |
/// exclusive to it: a stepped [`Number`](FieldKind::Number) is the same fact
|
| 3827 |
|
- |
/// about a typed value.
|
|
4012 |
+ |
/// restate.
|
| 3828 |
4013 |
|
///
|
| 3829 |
|
- |
/// Added 0.28.0 with [`FieldKind::Range`].
|
|
4014 |
+ |
/// # It is the granularity of a *typed* value
|
|
4015 |
+ |
///
|
|
4016 |
+ |
/// [`FieldKind::Range`] reads its own from [`curve`](Self::curve) and
|
|
4017 |
+ |
/// ignores this, as of 0.32.0. Until then this member served both, and
|
|
4018 |
+ |
/// serving both is what the split fixes: on a slider the granularity and
|
|
4019 |
+ |
/// the mapping are one decision, and on a typed number there is no mapping
|
|
4020 |
+ |
/// to decide with. See [`Curve`], "Why the step is here".
|
|
4021 |
+ |
///
|
|
4022 |
+ |
/// Added 0.28.0 with [`FieldKind::Range`], and narrowed away from it at
|
|
4023 |
+ |
/// 0.32.0.
|
| 3830 |
4024 |
|
pub step: Option<&'a str>,
|
|
4025 |
+ |
/// How a slider's position becomes its value, and how finely it moves.
|
|
4026 |
+ |
///
|
|
4027 |
+ |
/// [`FieldKind::Range`]'s, and nothing else reads it: a typed number has a
|
|
4028 |
+ |
/// granularity but no mapping, and takes [`step`](Self::step) instead.
|
|
4029 |
+ |
///
|
|
4030 |
+ |
/// Defaults to [`Curve::Linear`] with no step, which is what every range
|
|
4031 |
+ |
/// described before 0.32.0 meant, so this member is additive and no
|
|
4032 |
+ |
/// existing site changes meaning.
|
|
4033 |
+ |
///
|
|
4034 |
+ |
/// Added 0.32.0.
|
|
4035 |
+ |
pub curve: Curve<'a>,
|
| 3831 |
4036 |
|
/// Whether the field lives behind a "more options" disclosure.
|
| 3832 |
4037 |
|
pub extended: bool,
|
| 3833 |
4038 |
|
}
|
| 3851 |
4056 |
|
min: None,
|
| 3852 |
4057 |
|
max: None,
|
| 3853 |
4058 |
|
step: None,
|
|
4059 |
+ |
curve: Curve::Linear { step: None },
|
| 3854 |
4060 |
|
extended: false,
|
| 3855 |
4061 |
|
}
|
| 3856 |
4062 |
|
}
|
| 3863 |
4069 |
|
/// has a slider with nothing to slide across. Taking them as arguments is
|
| 3864 |
4070 |
|
/// what makes that unsayable.
|
| 3865 |
4071 |
|
///
|
| 3866 |
|
- |
/// [`step`](Self::step) stays a field rather than a fourth argument. It is
|
| 3867 |
|
- |
/// genuinely optional — the host's granularity is a real answer — and the
|
| 3868 |
|
- |
/// two bounds are not.
|
|
4072 |
+ |
/// The granularity stays a field rather than a fourth argument, and since
|
|
4073 |
+ |
/// 0.32.0 it is [`curve`](Self::curve)'s: it is genuinely optional — the
|
|
4074 |
+ |
/// host's own is a real answer — and the two bounds are not.
|
| 3869 |
4075 |
|
#[must_use]
|
| 3870 |
4076 |
|
pub const fn range(name: &'a str, label: &'a str, min: &'a str, max: &'a str) -> Self {
|
| 3871 |
4077 |
|
Self {
|
| 6235 |
6441 |
|
assert!(Awaiting::of(40 * 1024 * 1024).is_determinate());
|
| 6236 |
6442 |
|
assert_eq!(Awaiting::of(7).amount, Some(7));
|
| 6237 |
6443 |
|
}
|
|
6444 |
+ |
|
|
6445 |
+ |
// A slider is a fraction and a mapping
|
|
6446 |
+ |
//
|
|
6447 |
+ |
// `Curve` is the one thing in this crate that computes rather than
|
|
6448 |
+ |
// describes, and it does so because four renderers would otherwise each
|
|
6449 |
+ |
// write these two formulas and drift. So the formulas are pinned here.
|
|
6450 |
+ |
|
|
6451 |
+ |
/// The bounds of audiofiles' envelope attack, the curve's first consumer.
|
|
6452 |
+ |
const ATTACK: (f64, f64) = (0.001, 5.0);
|
|
6453 |
+ |
|
|
6454 |
+ |
#[test]
|
|
6455 |
+ |
fn a_curve_is_linear_with_no_step_until_a_field_says_otherwise() {
|
|
6456 |
+ |
assert_eq!(Curve::default(), Curve::Linear { step: None });
|
|
6457 |
+ |
let plain = Field::range("t", "T", "0", "1");
|
|
6458 |
+ |
assert_eq!(plain.curve, Curve::Linear { step: None });
|
|
6459 |
+ |
assert_eq!(plain.curve.step(), None);
|
|
6460 |
+ |
}
|
|
6461 |
+ |
|
|
6462 |
+ |
#[test]
|
|
6463 |
+ |
fn every_curve_carries_its_own_granularity() {
|
|
6464 |
+ |
assert_eq!(Curve::Linear { step: Some("0.01") }.step(), Some("0.01"));
|
|
6465 |
+ |
assert_eq!(
|
|
6466 |
+ |
Curve::Logarithmic {
|
|
6467 |
+ |
step: Some("0.001")
|
|
6468 |
+ |
}
|
|
6469 |
+ |
.step(),
|
|
6470 |
+ |
Some("0.001")
|
|
6471 |
+ |
);
|
|
6472 |
+ |
}
|
|
6473 |
+ |
|
|
6474 |
+ |
#[test]
|
|
6475 |
+ |
fn both_ends_of_the_track_are_the_bounds_under_either_curve() {
|
|
6476 |
+ |
// `min` and `max` are `f(0)` and `f(1)`. That is the whole reframe, and
|
|
6477 |
+ |
// it has to hold for a mapping that is not the identity or the bounds
|
|
6478 |
+ |
// have stopped meaning what the field says they mean.
|
|
6479 |
+ |
let (min, max) = ATTACK;
|
|
6480 |
+ |
for curve in [
|
|
6481 |
+ |
Curve::Linear { step: None },
|
|
6482 |
+ |
Curve::Logarithmic { step: None },
|
|
6483 |
+ |
] {
|
|
6484 |
+ |
assert!((curve.value_at(0.0, min, max) - min).abs() < 1e-12);
|
|
6485 |
+ |
assert!((curve.value_at(1.0, min, max) - max).abs() < 1e-12);
|
|
6486 |
+ |
}
|
|
6487 |
+ |
}
|
|
6488 |
+ |
|
|
6489 |
+ |
#[test]
|
|
6490 |
+ |
fn a_linear_midpoint_is_the_average_and_a_ratio_midpoint_is_the_geometric_mean() {
|
|
6491 |
+ |
let (min, max) = ATTACK;
|
|
6492 |
+ |
let linear = Curve::Linear { step: None }.value_at(0.5, min, max);
|
|
6493 |
+ |
assert!((linear - 2.5005).abs() < 1e-9);
|
|
6494 |
+ |
|
|
6495 |
+ |
// The reason the envelope is not linear: half way along a log track is
|
|
6496 |
+ |
// 70 ms, and half way along a linear one is 2.5 seconds. Every attack a
|
|
6497 |
+ |
// sampler is actually played with lives below the first.
|
|
6498 |
+ |
let ratio = Curve::Logarithmic { step: None }.value_at(0.5, min, max);
|
|
6499 |
+ |
assert!((ratio - (min * max).sqrt()).abs() < 1e-12);
|
|
6500 |
+ |
assert!(ratio < 0.08);
|
|
6501 |
+ |
}
|
|
6502 |
+ |
|
|
6503 |
+ |
#[test]
|
|
6504 |
+ |
fn a_position_and_a_value_round_trip_under_either_curve() {
|
|
6505 |
+ |
let (min, max) = ATTACK;
|
|
6506 |
+ |
for curve in [
|
|
6507 |
+ |
Curve::Linear { step: None },
|
|
6508 |
+ |
Curve::Logarithmic { step: None },
|
|
6509 |
+ |
] {
|
|
6510 |
+ |
for position in [0.0, 0.1, 0.25, 0.5, 0.75, 0.99, 1.0] {
|
|
6511 |
+ |
let back = curve.position_of(curve.value_at(position, min, max), min, max);
|
|
6512 |
+ |
assert!(
|
|
6513 |
+ |
(back - position).abs() < 1e-9,
|
|
6514 |
+ |
"{curve:?} lost {position} (got {back})"
|
|
6515 |
+ |
);
|
|
6516 |
+ |
}
|
|
6517 |
+ |
}
|
|
6518 |
+ |
}
|
|
6519 |
+ |
|
|
6520 |
+ |
#[test]
|
|
6521 |
+ |
fn a_ratio_curve_across_zero_is_drawn_linearly_rather_than_refused() {
|
|
6522 |
+ |
// An envelope's sustain is a 0-to-1 level. A constant ratio is
|
|
6523 |
+ |
// undefined there, and the answer is the linear mapping rather than a
|
|
6524 |
+ |
// NaN reaching a renderer that would paint it.
|
|
6525 |
+ |
let curve = Curve::Logarithmic { step: None };
|
|
6526 |
+ |
assert!(!curve.is_ratio(0.0, 1.0));
|
|
6527 |
+ |
assert!((curve.value_at(0.5, 0.0, 1.0) - 0.5).abs() < 1e-12);
|
|
6528 |
+ |
assert!(curve.value_at(0.5, -96.0, -20.0).is_finite());
|
|
6529 |
+ |
assert!(curve.is_ratio(ATTACK.0, ATTACK.1));
|
|
6530 |
+ |
}
|
|
6531 |
+ |
|
|
6532 |
+ |
#[test]
|
|
6533 |
+ |
fn a_track_with_no_extent_has_one_value_on_it() {
|
|
6534 |
+ |
for curve in [
|
|
6535 |
+ |
Curve::Linear { step: None },
|
|
6536 |
+ |
Curve::Logarithmic { step: None },
|
|
6537 |
+ |
] {
|
|
6538 |
+ |
assert!((curve.value_at(0.7, 4.0, 4.0) - 4.0).abs() < f64::EPSILON);
|
|
6539 |
+ |
assert!(curve.position_of(4.0, 4.0, 4.0).abs() < f64::EPSILON);
|
|
6540 |
+ |
// Inverted bounds are the same degenerate answer, not a negative
|
|
6541 |
+ |
// extent a renderer would draw backwards.
|
|
6542 |
+ |
assert!((curve.value_at(0.7, 9.0, 2.0) - 9.0).abs() < f64::EPSILON);
|
|
6543 |
+ |
}
|
|
6544 |
+ |
}
|
|
6545 |
+ |
|
|
6546 |
+ |
#[test]
|
|
6547 |
+ |
fn a_position_or_a_value_outside_the_track_is_clamped_to_it() {
|
|
6548 |
+ |
let (min, max) = ATTACK;
|
|
6549 |
+ |
let curve = Curve::Logarithmic { step: None };
|
|
6550 |
+ |
assert!((curve.value_at(-3.0, min, max) - min).abs() < 1e-12);
|
|
6551 |
+ |
assert!((curve.value_at(4.0, min, max) - max).abs() < 1e-12);
|
|
6552 |
+ |
assert!(curve.position_of(0.0, min, max).abs() < 1e-12);
|
|
6553 |
+ |
assert!((curve.position_of(500.0, min, max) - 1.0).abs() < 1e-12);
|
|
6554 |
+ |
}
|
|
6555 |
+ |
|
|
6556 |
+ |
#[test]
|
|
6557 |
+ |
fn a_typed_number_keeps_its_own_step_and_a_range_reads_its_curve() {
|
|
6558 |
+ |
// The split the 0.32.0 narrowing is: two granularities that were one
|
|
6559 |
+ |
// member, and the kinds that take them do not overlap.
|
|
6560 |
+ |
let typed = Field {
|
|
6561 |
+ |
step: Some("5"),
|
|
6562 |
+ |
..Field::new(FieldKind::Number, "port", "Port")
|
|
6563 |
+ |
};
|
|
6564 |
+ |
assert_eq!(typed.step, Some("5"));
|
|
6565 |
+ |
|
|
6566 |
+ |
let slid = Field {
|
|
6567 |
+ |
curve: Curve::Logarithmic {
|
|
6568 |
+ |
step: Some("0.001"),
|
|
6569 |
+ |
},
|
|
6570 |
+ |
..Field::range("attack", "Attack", "0.001", "5")
|
|
6571 |
+ |
};
|
|
6572 |
+ |
assert_eq!(slid.step, None);
|
|
6573 |
+ |
assert_eq!(slid.curve.step(), Some("0.001"));
|
|
6574 |
+ |
}
|
| 6238 |
6575 |
|
}
|