Item 6 (part 3): real time-axis behavior (E-A)
The layout time axis was inert: TimeAxisModel carried bare Vec<SpringSlotId>,
project()/affected_slots() ignored their arguments (returning the first slot /
all slots), nothing populated it, and nothing consumed it.
Now it carries ordered SlotPlacement { time, slot } entries and has real
behavior:
- project(time) returns the slot covering a time (greatest placement at or
before it; first when the query precedes them all);
- affected_slots(range) returns the slots in a half-open time range;
- slots() lists them in time order;
- with_placements populates and sorts the axis from resolved spring slots.
The spacing stage (to_constrained) now populates each region's axis from its
spring slots and carries the populated axis on ConstrainedLayoutRegion, so the
axis is a real, consumed artifact. Tests cover project/affected_slots semantics
and that spacing produces a per-region axis whose project() is a genuine
function of the queried time. DECISIONS updated.
This completes item 6 (and the whole v0 follow-up list, items 1-6 / M1-M5).
(The slot times are still the prototype's wall-clock spacing columns; mapping a
metric region's measure/beat grid to musical times is the next layer.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f105b53599
commit
691f527e07
|
|
@ -192,6 +192,21 @@ object is covered); the provenance-preservation contract itself is unchanged.
|
||||||
required for correct incremental-layout cache invalidation (Chapter 7
|
required for correct incremental-layout cache invalidation (Chapter 7
|
||||||
§"Incremental Layout").
|
§"Incremental Layout").
|
||||||
|
|
||||||
|
- **The time axis has real behavior (M5 follow-up).** Previously the
|
||||||
|
`TimeAxisModel` carried bare `Vec<SpringSlotId>` and `project`/`affected_slots`
|
||||||
|
ignored their arguments (returning the first slot / all slots) — inert payload.
|
||||||
|
Each axis now holds ordered `SlotPlacement { time, slot }` entries:
|
||||||
|
`project(time)` returns the slot *covering* a time (the greatest placement at or
|
||||||
|
before it), `affected_slots(range)` returns the slots in a half-open time range,
|
||||||
|
and `slots()` lists them in time order. The spacing stage (`to_constrained`)
|
||||||
|
populates each region's axis from its resolved spring slots
|
||||||
|
(`TimeAxisModel::with_placements`), and the populated axis is carried on
|
||||||
|
`ConstrainedLayoutRegion`, so the axis is a real, consumed artifact rather than
|
||||||
|
an empty placeholder. (The slot *times* are still the prototype's wall-clock
|
||||||
|
spacing columns; mapping a metric region's measure/beat grid to musical times
|
||||||
|
is the next layer, but the axis machinery now genuinely consumes whatever times
|
||||||
|
the spacing assigns.)
|
||||||
|
|
||||||
## Pass 11 candidates (ambiguities for the spec, not resolved in code)
|
## Pass 11 candidates (ambiguities for the spec, not resolved in code)
|
||||||
|
|
||||||
1. **Agent E's stated dependency set vs. the edit-barrier types.** The QUICKSTART
|
1. **Agent E's stated dependency set vs. the edit-barrier types.** The QUICKSTART
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ use crate::logical::{LogicalLayoutIR, ScoreVersion};
|
||||||
use crate::provenance::{manifestation_layout_id, LayoutObjectId, Provenance};
|
use crate::provenance::{manifestation_layout_id, LayoutObjectId, Provenance};
|
||||||
use crate::solver::SpringSlotId;
|
use crate::solver::SpringSlotId;
|
||||||
use crate::spatial::{BoundingBox, Point, Rect, StaffSpace};
|
use crate::spatial::{BoundingBox, Point, Rect, StaffSpace};
|
||||||
use crate::time_axis::TimePoint;
|
use crate::time_axis::{SlotPlacement, TimeAxisModel, TimePoint};
|
||||||
use crate::vertical_band::{inter_staff_gap_id, VerticalBand, VerticalBandId};
|
use crate::vertical_band::{inter_staff_gap_id, VerticalBand, VerticalBandId};
|
||||||
|
|
||||||
/// A stable identifier for a glyph-level object (Chapter 7: `GlyphObjectId`).
|
/// A stable identifier for a glyph-level object (Chapter 7: `GlyphObjectId`).
|
||||||
|
|
@ -78,10 +78,14 @@ pub struct GlyphStyle {
|
||||||
pub rgba: u32,
|
pub rgba: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub struct ConstrainedLayoutRegion {
|
pub struct ConstrainedLayoutRegion {
|
||||||
pub provenance: Provenance,
|
pub provenance: Provenance,
|
||||||
pub glyphs: Vec<GlyphObjectId>,
|
pub glyphs: Vec<GlyphObjectId>,
|
||||||
|
/// The region's time axis, populated with the time→slot placements of this
|
||||||
|
/// region's spring slots (Chapter 7 §"The Time Axis"): `time_axis.project`
|
||||||
|
/// maps a musical/wall-clock time to the slot covering it.
|
||||||
|
pub time_axis: TimeAxisModel,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
|
@ -414,15 +418,17 @@ pub fn try_to_constrained(
|
||||||
let mut staff_members: BTreeMap<StaffId, Vec<GlyphObjectId>> = BTreeMap::new();
|
let mut staff_members: BTreeMap<StaffId, Vec<GlyphObjectId>> = BTreeMap::new();
|
||||||
let mut margin_members: Vec<GlyphObjectId> = Vec::new();
|
let mut margin_members: Vec<GlyphObjectId> = Vec::new();
|
||||||
let mut region_glyphs = Vec::new();
|
let mut region_glyphs = Vec::new();
|
||||||
|
let mut region_placements: Vec<SlotPlacement> = Vec::new();
|
||||||
|
|
||||||
for (provenance, staff) in specs {
|
for (provenance, staff) in specs {
|
||||||
let band = band_of(staff);
|
let band = band_of(staff);
|
||||||
let glyph = make_glyph(provenance, column, band);
|
let glyph = make_glyph(provenance, column, band);
|
||||||
column += 1;
|
column += 1;
|
||||||
let gid = glyph.id();
|
let gid = glyph.id();
|
||||||
|
let time = TimePoint::WallClock(WallClockTime(column - 1));
|
||||||
horizontal_slots.push(SpringSlot {
|
horizontal_slots.push(SpringSlot {
|
||||||
id: glyph.horizontal_slot,
|
id: glyph.horizontal_slot,
|
||||||
time: TimePoint::WallClock(WallClockTime(column - 1)),
|
time: time.clone(),
|
||||||
min_width: StaffSpace(1.0),
|
min_width: StaffSpace(1.0),
|
||||||
preferred_width: StaffSpace(1.5),
|
preferred_width: StaffSpace(1.5),
|
||||||
max_width: None,
|
max_width: None,
|
||||||
|
|
@ -430,6 +436,10 @@ pub fn try_to_constrained(
|
||||||
compress_factor: 1.0,
|
compress_factor: 1.0,
|
||||||
members: vec![gid],
|
members: vec![gid],
|
||||||
});
|
});
|
||||||
|
region_placements.push(SlotPlacement {
|
||||||
|
time,
|
||||||
|
slot: glyph.horizontal_slot,
|
||||||
|
});
|
||||||
region_glyphs.push(gid);
|
region_glyphs.push(gid);
|
||||||
match staff {
|
match staff {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
|
|
@ -463,6 +473,9 @@ pub fn try_to_constrained(
|
||||||
constrained_regions.push(ConstrainedLayoutRegion {
|
constrained_regions.push(ConstrainedLayoutRegion {
|
||||||
provenance: region.provenance.clone(),
|
provenance: region.provenance.clone(),
|
||||||
glyphs: region_glyphs,
|
glyphs: region_glyphs,
|
||||||
|
// The region's kind-only logical axis, now populated with the real
|
||||||
|
// time→slot placements resolved during spacing.
|
||||||
|
time_axis: region.time_axis.clone().with_placements(region_placements),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -513,9 +526,41 @@ fn make_glyph(provenance: &Provenance, column: i64, band: VerticalBandId) -> Gly
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::logical::to_logical;
|
use crate::logical::to_logical;
|
||||||
|
use crate::time_axis::TimeAxis;
|
||||||
use epiphany_core::generators::valid_score_rich;
|
use epiphany_core::generators::valid_score_rich;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spacing_populates_a_consumable_time_axis_per_region() {
|
||||||
|
let c = to_constrained(&to_logical(&valid_score_rich(11)));
|
||||||
|
assert!(!c.regions.is_empty());
|
||||||
|
for region in &c.regions {
|
||||||
|
// The axis is no longer inert: it carries one placement per slot the
|
||||||
|
// region produced, and the slots come back in time order.
|
||||||
|
let region_slots: Vec<SpringSlotId> =
|
||||||
|
region.glyphs.iter().map(|g| SpringSlotId(g.0)).collect();
|
||||||
|
assert_eq!(region.time_axis.slots(), region_slots);
|
||||||
|
|
||||||
|
// project() consumes the time argument: each slot's own time projects
|
||||||
|
// back to that slot (the covering placement) — not a constant.
|
||||||
|
for placement in region.time_axis.placements() {
|
||||||
|
assert_eq!(
|
||||||
|
region.time_axis.project(placement.time.clone()),
|
||||||
|
placement.slot
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// A non-trivial region distinguishes its slots by time (so project is
|
||||||
|
// genuinely a function of the query, not "always the first slot").
|
||||||
|
if region.time_axis.placements().len() >= 2 {
|
||||||
|
let p = region.time_axis.placements();
|
||||||
|
assert_ne!(
|
||||||
|
region.time_axis.project(p[0].time.clone()),
|
||||||
|
region.time_axis.project(p[1].time.clone())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Band membership is a correct partition: every glyph names an existing
|
/// Band membership is a correct partition: every glyph names an existing
|
||||||
/// band, no glyph is a member of two bands, and a glyph's `vertical_band`
|
/// band, no glyph is a member of two bands, and a glyph's `vertical_band`
|
||||||
/// equals the band that lists it — so a glyph is never placed in another
|
/// equals the band that lists it — so a glyph is never placed in another
|
||||||
|
|
|
||||||
|
|
@ -37,24 +37,35 @@ pub enum TimeRange {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Metric-axis projection data. The prototype populates slots during spacing.
|
/// One placement on the time axis: the spring slot occupying a given time.
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Default)]
|
/// Placements are held in ascending time order, so the axis maps a queried time
|
||||||
pub struct MetricTimeAxis {
|
/// to the slot covering it (the greatest placement at or before the query).
|
||||||
pub slots: Vec<SpringSlotId>,
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct SlotPlacement {
|
||||||
|
pub time: TimePoint,
|
||||||
|
pub slot: SpringSlotId,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Proportional-axis projection data.
|
/// Metric-axis projection data: the time→slot placements, populated during
|
||||||
|
/// spacing (a region's measure/beat grid resolves to ordered spring slots).
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Default)]
|
||||||
|
pub struct MetricTimeAxis {
|
||||||
|
pub placements: Vec<SlotPlacement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proportional-axis projection data: horizontal position is linear in
|
||||||
|
/// wall-clock time, and the placements map each slot's time onto the axis.
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub struct ProportionalTimeAxis {
|
pub struct ProportionalTimeAxis {
|
||||||
pub duration_ns: i64,
|
pub duration_ns: i64,
|
||||||
pub space_per_second: StaffSpace,
|
pub space_per_second: StaffSpace,
|
||||||
pub slots: Vec<SpringSlotId>,
|
pub placements: Vec<SlotPlacement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Aleatoric-axis projection data. Slot order is topological layer order.
|
/// Aleatoric-axis projection data. Placement order is topological layer order.
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Default)]
|
#[derive(Clone, PartialEq, Eq, Debug, Default)]
|
||||||
pub struct AleatoricTimeAxis {
|
pub struct AleatoricTimeAxis {
|
||||||
pub slots: Vec<SpringSlotId>,
|
pub placements: Vec<SlotPlacement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The canonical representation of a region's time axis (Chapter 7). The
|
/// The canonical representation of a region's time axis (Chapter 7). The
|
||||||
|
|
@ -83,12 +94,26 @@ pub enum TimeAxisKind {
|
||||||
Registered(TimeAxisRegistryId),
|
Registered(TimeAxisRegistryId),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compares two [`TimePoint`]s of the *same* kind; mixed kinds are
|
||||||
|
/// incomparable (`None`), which a uniform region never produces.
|
||||||
|
fn time_cmp(a: &TimePoint, b: &TimePoint) -> Option<core::cmp::Ordering> {
|
||||||
|
match (a, b) {
|
||||||
|
(TimePoint::Musical(x), TimePoint::Musical(y)) => Some(x.cmp(y)),
|
||||||
|
(TimePoint::WallClock(x), TimePoint::WallClock(y)) => Some(x.cmp(y)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Dynamic time-axis interface used by spacing implementations. The tagged
|
/// Dynamic time-axis interface used by spacing implementations. The tagged
|
||||||
/// [`TimeAxisModel`] remains the canonical representation.
|
/// [`TimeAxisModel`] remains the canonical representation.
|
||||||
pub trait TimeAxis: Send + Sync {
|
pub trait TimeAxis: Send + Sync {
|
||||||
fn kind(&self) -> TimeAxisKind;
|
fn kind(&self) -> TimeAxisKind;
|
||||||
|
/// The spring slot covering `time`: the placement with the greatest time at
|
||||||
|
/// or before `time` (or the first placement if `time` precedes them all).
|
||||||
fn project(&self, time: TimePoint) -> SpringSlotId;
|
fn project(&self, time: TimePoint) -> SpringSlotId;
|
||||||
fn slots(&self) -> &[SpringSlotId];
|
/// The spring slots in time order.
|
||||||
|
fn slots(&self) -> Vec<SpringSlotId>;
|
||||||
|
/// The spring slots whose time falls in the half-open `range` `[start, end)`.
|
||||||
fn affected_slots(&self, range: TimeRange) -> Vec<SpringSlotId>;
|
fn affected_slots(&self, range: TimeRange) -> Vec<SpringSlotId>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,6 +127,38 @@ impl TimeAxisModel {
|
||||||
TimeAxisModel::Registered(id, _) => TimeAxisKind::Registered(*id),
|
TimeAxisModel::Registered(id, _) => TimeAxisKind::Registered(*id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The axis's time→slot placements, in ascending time order.
|
||||||
|
pub fn placements(&self) -> &[SlotPlacement] {
|
||||||
|
match self {
|
||||||
|
TimeAxisModel::Metric(axis) => &axis.placements,
|
||||||
|
TimeAxisModel::Proportional(axis) => &axis.placements,
|
||||||
|
TimeAxisModel::Aleatoric(axis) => &axis.placements,
|
||||||
|
TimeAxisModel::Registered(_, _) => &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns this axis populated with `placements` (sorted into ascending time
|
||||||
|
/// order). A `Registered` axis is opaque and returned unchanged. This is how
|
||||||
|
/// the spacing stage drives the axis from its resolved spring slots.
|
||||||
|
pub fn with_placements(self, mut placements: Vec<SlotPlacement>) -> Self {
|
||||||
|
placements.sort_by(|a, b| time_cmp(&a.time, &b.time).unwrap_or(core::cmp::Ordering::Equal));
|
||||||
|
match self {
|
||||||
|
TimeAxisModel::Metric(mut axis) => {
|
||||||
|
axis.placements = placements;
|
||||||
|
TimeAxisModel::Metric(axis)
|
||||||
|
}
|
||||||
|
TimeAxisModel::Proportional(mut axis) => {
|
||||||
|
axis.placements = placements;
|
||||||
|
TimeAxisModel::Proportional(axis)
|
||||||
|
}
|
||||||
|
TimeAxisModel::Aleatoric(mut axis) => {
|
||||||
|
axis.placements = placements;
|
||||||
|
TimeAxisModel::Aleatoric(axis)
|
||||||
|
}
|
||||||
|
other @ TimeAxisModel::Registered(_, _) => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TimeAxis for TimeAxisModel {
|
impl TimeAxis for TimeAxisModel {
|
||||||
|
|
@ -109,21 +166,47 @@ impl TimeAxis for TimeAxisModel {
|
||||||
TimeAxisModel::kind(self)
|
TimeAxisModel::kind(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn project(&self, _time: TimePoint) -> SpringSlotId {
|
fn project(&self, time: TimePoint) -> SpringSlotId {
|
||||||
self.slots().first().copied().unwrap_or(SpringSlotId(0))
|
let placements = self.placements();
|
||||||
|
// The greatest placement at or before `time` (the slot covering it)...
|
||||||
|
let covering = placements.iter().rfind(|p| {
|
||||||
|
matches!(
|
||||||
|
time_cmp(&p.time, &time),
|
||||||
|
Some(core::cmp::Ordering::Less | core::cmp::Ordering::Equal)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
// ...or the first placement when `time` precedes them all.
|
||||||
|
covering
|
||||||
|
.or_else(|| placements.first())
|
||||||
|
.map(|p| p.slot)
|
||||||
|
.unwrap_or(SpringSlotId(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn slots(&self) -> &[SpringSlotId] {
|
fn slots(&self) -> Vec<SpringSlotId> {
|
||||||
match self {
|
self.placements().iter().map(|p| p.slot).collect()
|
||||||
TimeAxisModel::Metric(axis) => &axis.slots,
|
|
||||||
TimeAxisModel::Proportional(axis) => &axis.slots,
|
|
||||||
TimeAxisModel::Aleatoric(axis) => &axis.slots,
|
|
||||||
TimeAxisModel::Registered(_, _) => &[],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn affected_slots(&self, _range: TimeRange) -> Vec<SpringSlotId> {
|
fn affected_slots(&self, range: TimeRange) -> Vec<SpringSlotId> {
|
||||||
self.slots().to_vec()
|
let (start, end) = match range {
|
||||||
|
TimeRange::Musical { start, end } => {
|
||||||
|
(TimePoint::Musical(start), TimePoint::Musical(end))
|
||||||
|
}
|
||||||
|
TimeRange::WallClock { start, end } => {
|
||||||
|
(TimePoint::WallClock(start), TimePoint::WallClock(end))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.placements()
|
||||||
|
.iter()
|
||||||
|
.filter(|p| {
|
||||||
|
let at_or_after_start = matches!(
|
||||||
|
time_cmp(&p.time, &start),
|
||||||
|
Some(core::cmp::Ordering::Greater | core::cmp::Ordering::Equal)
|
||||||
|
);
|
||||||
|
let before_end = matches!(time_cmp(&p.time, &end), Some(core::cmp::Ordering::Less));
|
||||||
|
at_or_after_start && before_end
|
||||||
|
})
|
||||||
|
.map(|p| p.slot)
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,7 +219,7 @@ pub fn time_axis_of(region: &Region) -> TimeAxisModel {
|
||||||
RegionTimeModel::Proportional(p) => TimeAxisModel::Proportional(ProportionalTimeAxis {
|
RegionTimeModel::Proportional(p) => TimeAxisModel::Proportional(ProportionalTimeAxis {
|
||||||
duration_ns: p.duration.0,
|
duration_ns: p.duration.0,
|
||||||
space_per_second: StaffSpace(1.0),
|
space_per_second: StaffSpace(1.0),
|
||||||
slots: Vec::new(),
|
placements: Vec::new(),
|
||||||
}),
|
}),
|
||||||
RegionTimeModel::Aleatoric(_) => TimeAxisModel::Aleatoric(AleatoricTimeAxis::default()),
|
RegionTimeModel::Aleatoric(_) => TimeAxisModel::Aleatoric(AleatoricTimeAxis::default()),
|
||||||
}
|
}
|
||||||
|
|
@ -156,7 +239,7 @@ mod tests {
|
||||||
TimeAxisModel::Proportional(ProportionalTimeAxis {
|
TimeAxisModel::Proportional(ProportionalTimeAxis {
|
||||||
duration_ns: 42,
|
duration_ns: 42,
|
||||||
space_per_second: StaffSpace(1.0),
|
space_per_second: StaffSpace(1.0),
|
||||||
slots: vec![],
|
placements: vec![],
|
||||||
})
|
})
|
||||||
.kind(),
|
.kind(),
|
||||||
TimeAxisKind::Proportional
|
TimeAxisKind::Proportional
|
||||||
|
|
@ -171,4 +254,59 @@ mod tests {
|
||||||
TimeAxisKind::Registered(r)
|
TimeAxisKind::Registered(r)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn project_and_affected_slots_consume_placements() {
|
||||||
|
// Built out of time order; `with_placements` sorts by time.
|
||||||
|
let axis = TimeAxisModel::Metric(MetricTimeAxis::default()).with_placements(vec![
|
||||||
|
SlotPlacement {
|
||||||
|
time: TimePoint::WallClock(WallClockTime(10)),
|
||||||
|
slot: SpringSlotId(1),
|
||||||
|
},
|
||||||
|
SlotPlacement {
|
||||||
|
time: TimePoint::WallClock(WallClockTime(30)),
|
||||||
|
slot: SpringSlotId(3),
|
||||||
|
},
|
||||||
|
SlotPlacement {
|
||||||
|
time: TimePoint::WallClock(WallClockTime(20)),
|
||||||
|
slot: SpringSlotId(2),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Slots come back in ascending time order.
|
||||||
|
assert_eq!(
|
||||||
|
axis.slots(),
|
||||||
|
vec![SpringSlotId(1), SpringSlotId(2), SpringSlotId(3)]
|
||||||
|
);
|
||||||
|
// project() returns the covering slot (greatest time <= query)...
|
||||||
|
assert_eq!(
|
||||||
|
axis.project(TimePoint::WallClock(WallClockTime(25))),
|
||||||
|
SpringSlotId(2)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
axis.project(TimePoint::WallClock(WallClockTime(30))),
|
||||||
|
SpringSlotId(3)
|
||||||
|
);
|
||||||
|
// ...and the first slot when the query precedes every placement.
|
||||||
|
assert_eq!(
|
||||||
|
axis.project(TimePoint::WallClock(WallClockTime(5))),
|
||||||
|
SpringSlotId(1)
|
||||||
|
);
|
||||||
|
// affected_slots() respects the half-open range [10, 30): excludes 30.
|
||||||
|
assert_eq!(
|
||||||
|
axis.affected_slots(TimeRange::WallClock {
|
||||||
|
start: WallClockTime(10),
|
||||||
|
end: WallClockTime(30),
|
||||||
|
}),
|
||||||
|
vec![SpringSlotId(1), SpringSlotId(2)]
|
||||||
|
);
|
||||||
|
|
||||||
|
// An empty axis projects to the default slot and has no slots.
|
||||||
|
let empty = TimeAxisModel::Metric(MetricTimeAxis::default());
|
||||||
|
assert_eq!(
|
||||||
|
empty.project(TimePoint::WallClock(WallClockTime(0))),
|
||||||
|
SpringSlotId(0)
|
||||||
|
);
|
||||||
|
assert!(empty.slots().is_empty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ pub fn gen_time_axis_model(rng: &mut Rng) -> TimeAxisModel {
|
||||||
1 => TimeAxisModel::Proportional(ProportionalTimeAxis {
|
1 => TimeAxisModel::Proportional(ProportionalTimeAxis {
|
||||||
duration_ns: rng.range(0, 1 << 40) as i64,
|
duration_ns: rng.range(0, 1 << 40) as i64,
|
||||||
space_per_second: gen_staff_space(rng),
|
space_per_second: gen_staff_space(rng),
|
||||||
slots: vec![],
|
placements: vec![],
|
||||||
}),
|
}),
|
||||||
2 => TimeAxisModel::Aleatoric(AleatoricTimeAxis::default()),
|
2 => TimeAxisModel::Aleatoric(AleatoricTimeAxis::default()),
|
||||||
_ => TimeAxisModel::Registered(
|
_ => TimeAxisModel::Registered(
|
||||||
|
|
@ -536,6 +536,7 @@ pub fn gen_constrained_layout_region(rng: &mut Rng) -> ConstrainedLayoutRegion {
|
||||||
glyphs: (0..rng.range_usize(0, 3))
|
glyphs: (0..rng.range_usize(0, 3))
|
||||||
.map(|_| gen_glyph_object_id(rng))
|
.map(|_| gen_glyph_object_id(rng))
|
||||||
.collect(),
|
.collect(),
|
||||||
|
time_axis: gen_time_axis_model(rng),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue