loic/crates/loic-gui/src/timeline.rs
Kamal Tufekcic 4eb2362b20
All checks were successful
CI / build (push) Successful in 1m41s
CI / lint (push) Successful in 43s
CI / test (push) Successful in 2m7s
CI / publish (push) Successful in 12s
CI / release (push) Successful in 1m15s
initial commit
2026-09-12 10:37:55 +03:00

1348 lines
70 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! # The Cosmic Timeline
//!
//! The Observatory's horizontal bar from Creation to Annihilation: the whole
//! of the calendar's representable domain, some 10^154 Planck times of it,
//! laid across a single row of glyph cells and offered up for clicking.
//! Every click selects one segment of the bar and makes that segment the
//! whole bar; the view narrows by a factor of the segment count at each
//! descent, the descents compose, and they may be repeated until the span
//! beneath the pointer is narrower than a hundred Planck times, at which
//! point the Observatory declines to descend further, there being nothing
//! finer to show and no wish to pretend otherwise.
//!
//! The events of cosmic history are not painted on the bar all at once.
//! Each is withheld until the view has narrowed enough to give it room, so
//! that the widest views list the first stars and the death of the Sun, a
//! window a billion years wide lists the Snowball and the Cambrian, and a
//! window seventeen years wide lists the Whisper and the Unix epoch — each
//! event in the company of its own era, and never more of them than the
//! panel beneath the bar has lines. The curious observer who drills down is
//! rewarded. The incurious one sees a bar with a word at either end, which
//! is also a true statement about time, if a coarse one, and the calendar
//! has a mask for coarse.
//!
//! Every quantity here that is a time is an `I512` or a `U512`, and every
//! comparison, subtraction and quotient is performed on those integers in
//! full. The single exemption, tolerated because a GPU cannot be addressed
//! in any other tongue, is the last line of `TimelineView::position_of`,
//! which turns an exact ratio into a fraction of a bar's width and confesses
//! to the sin at length where it is committed.
use loic::{
As, I512, I512_ZERO, LOIC_TO_UNIX_OFFSET_SECONDS, U512, U512_ZERO, annihilation_planck,
creation_planck,
};
use std::str::FromStr;
use crate::events::EventId;
/// The first nineteen powers of ten, transcribed in full and looked up by
/// index rather than raised at runtime, so that no exponentiation is ever
/// entrusted to a floating-point unit, whose powers of ten are exact only
/// until they are not and which does not say when. Index `n` furnishes
/// 10^n, exactly, as a `u64`, from 10^0 to 10^18, the largest power of ten
/// that type can hold; that reaches every count of attoseconds,
/// femtoseconds, picoseconds, nanoseconds and microseconds that the span
/// label of `TimelineView::format_span` prints from the table, and it is
/// the first thing the event table's scientific-notation helpers reach for
/// before they resign themselves to multiplying by ten one power at a time.
const POWERS_OF_10: [u64; 19] = [
1, // 10^0
10, // 10^1
100, // 10^2
1_000, // 10^3
10_000, // 10^4
100_000, // 10^5
1_000_000, // 10^6
10_000_000, // 10^7
100_000_000, // 10^8
1_000_000_000, // 10^9
10_000_000_000, // 10^10
100_000_000_000, // 10^11
1_000_000_000_000, // 10^12
10_000_000_000_000, // 10^13
100_000_000_000_000, // 10^14
1_000_000_000_000_000, // 10^15
10_000_000_000_000_000, // 10^16
100_000_000_000_000_000, // 10^17
1_000_000_000_000_000_000, // 10^18
];
/// Planck times per second, transcribed in decimal: the integer part of
/// `PLANCK_DENOMINATOR / PLANCK_NUMERATOR`, which is 10^50 divided by
/// 5,391,247 — forty-four digits of it, approximately 1.855 × 10^43. The
/// reliquary is parsed once, when the event table is first built, into the
/// `I512` by which every second in that table is multiplied. It is kept as
/// a string because a string can be read against its derivation, which is
/// `derive_constants.sh` at the workspace root, and because a forty-four
/// digit literal is a thing the eye can check and the compiler cannot be
/// asked to have opinions about. The fraction of a Planck time that the
/// integer part leaves off — less than one — is the whole of what any
/// marker placed by this number is early by, per second of the interval it
/// was multiplied across.
const PLANCK_PER_SECOND_DECIMAL: &str = "18548584399861479171701834473545730700151560";
/// Planck times per Julian year, transcribed in decimal: the figure above
/// multiplied by `SECONDS_PER_JULIAN_YEAR`, 31,557,600, which is 365.25
/// days of 86,400 SI seconds and the only year this calendar acknowledges,
/// because it is a definition and not a measurement of anything that
/// wobbles. Fifty-one digits, approximately 5.853 × 10^50; derivation,
/// `derive_constants.sh` at the workspace root. Every event in the table
/// dated in years — which is most of them, the universe having been
/// unhurried for the greater part of its life — is placed by multiplying
/// its count of years by this integer, exactly, with no intermediate that a
/// floating-point unit was invited to round.
const PLANCK_PER_YEAR_DECIMAL: &str = "585348807057068615108897811582366751143102869856000";
/// The seconds by which the kernel's atomic clock leads civil time: the ten
/// that Coordinated Universal Time began with on the first of January 1972,
/// and the twenty-seven leap seconds inserted since, the last of them at
/// the close of 2016 — thirty-seven in all, every one of them a negotiation
/// with a rotating planet. The workspace README, section 1.2, holds all
/// thirty-seven in the contempt they have earned and discloses, in the same
/// breath, that the calendar's own epoch is pinned by exactly this many of
/// them: `CLOCK_TAI` on Linux is `CLOCK_REALTIME` plus this offset, and the
/// library's Unix offset is stated on that scale. This module needs the
/// number for the return journey, from the atomic scale the library counts
/// in to the civil scale the history books use, and takes it once, here,
/// to state the Whisper as a Unix timestamp.
const TAI_MINUS_UTC_SECONDS: i64 = 37;
/// The Whisper on the Unix scale: 1969-10-30 06:30:41 UTC, which is
/// 5,419,759 seconds before the Unix epoch and therefore negative, Unix
/// time not having begun yet. The library states the same instant on the
/// atomic scale, thirty-seven seconds ahead, as `LOIC_TO_UNIX_OFFSET_SECONDS`,
/// because `CLOCK_TAI` is what it reads; this module wants the civil number,
/// because the dates in the event table are civil ones, recorded by people
/// who did not know what a leap second was and would not have cared. The
/// relation between the two is one addition, performed here, once, by the
/// compiler.
///
/// A civil date is placed on the timeline exactly where `now()` would place
/// it on a kernel whose clock read that date with today's offset: its
/// distance from this constant in seconds, times the Planck times in a
/// second. That is the only arithmetic the calendar has for turning a
/// clock's reading into an instant, and the timeline uses it rather than
/// inventing a second arithmetic that could one day disagree with the
/// first.
const WHISPER_UNIX_SECONDS: i64 = -(LOIC_TO_UNIX_OFFSET_SECONDS + TAI_MINUS_UTC_SECONDS);
/// A compile-time proof that the civil timestamp of the Whisper, written
/// down here as every history of the ARPANET would give it, agrees with the
/// library's atomic offset plus the leap seconds it stands on. The two
/// arrive by independent roads — one transcribed from the record of the
/// first transmission, one derived from `CLOCK_TAI`'s arithmetic — and they
/// are meant to meet at −5,419,759. Were they not to, either the workspace
/// README's disclosure in section 1.2 or the library's constant would be
/// wrong, and the crate declines to build until someone says which; a
/// binary whose epoch is in dispute by thirty-seven seconds is not a
/// calendar, it is a rumour with a compiler.
const _WHISPER_UTC_AGREES_WITH_THE_LIBRARY: () = assert!(
WHISPER_UNIX_SECONDS == -5_419_759,
"the Whisper's civil second and the library's atomic offset disagree beyond the leap seconds"
);
/// Consult the table for 10^`exp`, and answer 1 — the zeroth power, the
/// multiplicative identity, the number that changes nothing it touches —
/// for any exponent the table does not hold, whether negative or past
/// eighteen, rather than a fraction the `u64` could not carry or an error
/// the caller could not use. The span label that consults it has already
/// shifted its exponent into the range of a unit prefix before asking, and
/// the shift, not this function, is where the label's honesty is decided.
fn pow10(exp: i32) -> u64 {
usize::try_from(exp)
.ok()
.and_then(|index| POWERS_OF_10.get(index))
.copied()
.unwrap_or(1)
}
/// The cell under a pixel offset: the offset divided by the cell's width,
/// clamped at zero from below and then truncated toward it, which for a
/// non-negative quotient is the floor, which is the index of the cell the
/// pixel lies in, which is the whole of the question. Two floating-point
/// operations are committed here, a division and a `max`, and both are
/// forgiven: the operands are pixel measurements, a few hundred wide at
/// most, which an `f32` carries to a precision no pixel can resolve, and
/// the result is delivered into a `usize` the moment it is known. Nothing
/// temporal passes through this function. It is a ruler, and a ruler is
/// permitted to be made of wood.
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "the floor of a non-negative quotient is the cell under the pixel; the cut selects it"
)]
fn cell_under(offset: f32, cell_width: f32) -> usize {
(offset / cell_width).max(0.0) as usize
}
/// A window onto the cosmic timeline: the contiguous stretch of
/// representable time, from `start` to `end` inclusive, that the bar is
/// currently showing. The full view is the whole domain, Creation to
/// Annihilation; every narrower view is one segment of some wider one,
/// reached by clicking, and the Observatory keeps the wider ones on a stack
/// so that the descent can be retraced one step at a time.
///
/// A view is a pair of instants and nothing else. Its width, its order of
/// magnitude, the position of any instant within it, and the label it
/// wears are all derived on demand from the two ends — in 512-bit
/// integers, with the one exception that `position_of` confesses to in
/// full. Nothing is cached, because nothing here is expensive enough to
/// deserve the risk of being stale.
#[derive(Clone)]
pub struct TimelineView {
/// The first instant the view shows, in Planck times since the Whisper:
/// negative for everything before 06:30:41 UTC on 30 October 1969,
/// which is most of the universe.
pub start: I512,
/// The last instant the view shows, in Planck times since the Whisper.
/// Never less than `start`; the difference between them is the view's
/// span, which for the full view does not fit in this type at all and
/// is computed unsigned for that reason.
pub end: I512,
}
impl TimelineView {
/// Manifest the widest view there is: `start` at Creation, the Big Bang
/// as the library locates it some 8.08 × 10^60 Planck times before the
/// Whisper, and `end` at Annihilation, the last coordinate 512 signed
/// bits can name, a little past 10^103 years hence. Between them lies
/// every instant the calendar can represent, which is the same as
/// saying every instant, since the calendar does not concede that there
/// are others. This is the view the Observatory opens on and the view
/// Escape returns it to.
pub fn full() -> Self {
Self {
start: *creation_planck(),
end: *annihilation_planck(),
}
}
/// The width of this view in Planck times, furnished as a `U512` because
/// the widest view is wider than a signed integer can say. Creation is
/// negative, Annihilation is `I512::MAX`, and their difference — the
/// library's `total_span`, some 6.7 × 10^153 — exceeds `I512::MAX` by
/// exactly the age of the universe, which is not a margin one waves
/// away. So the subtraction is performed on the unsigned bit patterns
/// and permitted to wrap, and for `end >= start` the wrapped difference
/// is the exact width at every zoom, including the one at which the
/// signed subtraction would have overflowed and, in a release build,
/// said nothing about it. Two's complement is a circle, and a circle has
/// no edge to fall off. The test `full_span_agrees_with_the_library`
/// holds this function to the library's own span, bit for bit.
pub fn span(&self) -> U512 {
self.end
.cast_unsigned()
.wrapping_sub(self.start.cast_unsigned())
}
/// The number of decimal digits in the span: `ilog10` plus one, an
/// integer stand-in for the view's order of magnitude that costs one
/// 512-bit logarithm and no floating-point at all. It is the coin in
/// which this module prices everything — which events are legible at
/// this zoom, which unit the span label speaks in, whether the
/// Observatory will consent to zoom further — because a threshold on a
/// digit count is a threshold on a power of ten, and a power of ten is
/// the only kind of "about" the calendar is prepared to be.
///
/// The full view has 154 digits. A span of zero, which no view has,
/// would be reported as one digit rather than as a panic; the guard
/// exists because `ilog10` is honest about the logarithm of nothing,
/// and this function prefers to be honest about something.
pub fn span_digits(&self) -> usize {
let span = self.span();
if span == U512_ZERO {
1
} else {
usize::try_from(span.ilog10()).map_or(1, |magnitude| magnitude + 1)
}
}
/// Whether this view is the full one: `start` at Creation and `end` at
/// Annihilation, both compared exactly, in all 512 bits, because a view
/// that is merely enormous is not the same thing as a view that is
/// everything. The Observatory asks this to decide whether the words
/// beneath the bar's ends read Creation and Annihilation or merely
/// Start and End, whether to offer the way back, and whether the
/// breadcrumb of fractions has anything yet to say.
pub fn is_full(&self) -> bool {
self.start == *creation_planck() && self.end == *annihilation_planck()
}
/// Descend into one segment of this view: divide the span into
/// `segment_count` equal parts and make the part numbered
/// `segment_index`, counting from zero at `start`, the whole of a new
/// view. Each descent narrows the window by the segment count — some
/// sixty at the Observatory's width — and the descents compose: forty
/// of them at fifty-eight segments apiece carry the full span's 154
/// digits down to about eighty-four, which is the walk the test
/// `position_is_exact_at_ends_and_midpoint_at_every_zoom` takes.
///
/// The division is integer division on the `U512` span, so the segments
/// are equal to the Planck time and the remainder falls past the last
/// of them, unshown; a view a few Planck times narrower than the bar
/// suggested is a smaller lie than a fractional Planck time would be,
/// and the calendar does not deal in those. The new view runs from
/// `start + index × segment` to `start + (index + 1) × segment`,
/// computed in `I512`, which the remark in the body explains is safe.
///
/// # Panics
///
/// Panics if `segment_index` does not fit an `i64` — a bar more than
/// nine quintillion cells wide, drawn on a monitor that does not exist —
/// or if `segment_count` is zero, which is a division by nothing and
/// the caller's confession that it drew no bar at all.
pub fn zoom_into(&self, segment_index: usize, segment_count: usize) -> Self {
let span = self.span();
let segment_span = span / (segment_count as u64).as_::<U512>();
// A segment is narrower than the span it divides, the widest span
// divided is the full one, and the full span divided by anything
// greater than one is below 2^511: so the segment's bit pattern,
// reinterpreted as signed, is its value, and the cast neither costs
// nor loses anything. Divided by one it would not be, and one
// segment is not a division but the view again; no bar asks for it.
let segment_span_i = segment_span.cast_signed();
let index = i64::try_from(segment_index).expect("no bar has more cells than an i64 holds");
let new_start = self.start + segment_span_i * index.as_::<I512>();
let new_end = new_start + segment_span_i;
Self {
start: new_start,
end: new_end,
}
}
/// Locate an instant within this view as a fraction of the way from
/// `start` to `end`: 0.0 at the first instant, 1.0 at the last, and
/// `None` for any instant the view does not contain, which is the
/// Observatory's cue to draw no caret rather than a caret in the
/// margin. The bar multiplies the fraction by its width in pixels and
/// places the caret there; the hit test reverses the same arithmetic to
/// find out which caret was struck.
///
/// # On the Reluctant Use of Floating-Point
///
/// This function commits the venial sin of `f64` arithmetic, but only in
/// its last line, and only for the conversion of one exact ratio into a
/// pixel ratio. The offset of the instant from `start` and the span of
/// the view are both computed in 512-bit integers, exactly; both are
/// then brought down to sixty-four bits by the same right shift, chosen
/// so that the span keeps its leading sixty-four bits and the offset
/// loses the same low bits; and only then is one divided by the other.
/// The ratio of the shifted quantities is exact to one part in 2^63,
/// which is finer than an `f64` can express, so the two conversions and
/// the one division are the only floating-point operations performed,
/// each faithful to one part in 2^53, and a pixel is one part in a few
/// thousand. The underlying temporal values remain in their integer
/// sanctity until the very moment we must speak to the GPU in its
/// native tongue, and the orders of magnitude discarded at that moment —
/// nearly all of them, in the full view — were never going to fit on a
/// monitor anyway; we shed no tears. The test
/// `position_is_exact_at_ends_and_midpoint_at_every_zoom` checks the
/// two ends and the midpoint of forty successive views against 0, 1 and
/// one half.
pub fn position_of(&self, planck_time: &I512) -> Option<f64> {
if planck_time < &self.start || planck_time > &self.end {
return None;
}
// `start <= planck_time` has just been established, so the wrapped
// unsigned difference is the exact offset — even in the full view,
// where the signed difference between an instant near Annihilation
// and Creation does not fit an I512 and a checked subtraction would
// have nothing honest to say. The same circle as `span`, walked for
// the same reason.
let offset = planck_time
.cast_unsigned()
.wrapping_sub(self.start.cast_unsigned());
let span = self.span();
if span == U512_ZERO {
return Some(0.0);
}
// Both quantities are brought down to 64 bits by the same shift: the
// span's bit length less sixty-four, saturating at zero for spans
// that already fit, so the span keeps its leading 64 bits and the
// offset — never the larger — loses the same low bits. Their ratio
// is then exact to one part in 2^63, which is more than an f64 can
// hold, and the division below is the only floating-point operation
// performed on the way to the pixel. The clamp is a courtesy to the
// rounding, not a correction of the arithmetic: the shifted offset
// cannot exceed the shifted span, so the quotient cannot exceed one.
let shift = (U512::BITS - span.leading_zeros()).saturating_sub(64);
let offset_top = (offset >> shift).as_::<u64>();
let span_top = (span >> shift).as_::<u64>();
Some((offset_top as f64 / span_top as f64).clamp(0.0, 1.0))
}
/// Pronounce the span of this view in a unit a human might recognise —
/// "~minutes", "~billions to trillions of years", "~10^-33 seconds" —
/// prefixed with a tilde, because the label is chosen by the span's
/// digit count and a digit count is honest only to within a factor of
/// ten. The Observatory prints it beneath the zoomed bar, centred, in
/// cyan. It is the one place the calendar utters the word "minutes",
/// and it does so under protest, with the tilde as its disclaimer.
///
/// The digit-count thresholds are derived from the Planck time constant
/// and from nothing else:
/// - `planck_per_second` = 10^50 / 5391247 = 1.855…×10^43 (44 digits)
/// - `planck_per_minute` = `planck_per_second` × 60 (46 digits)
/// - `planck_per_hour` = `planck_per_second` × 3600 (47 digits; the arm
/// below takes 47 and 48 together, hours to days)
/// - `planck_per_day` = `planck_per_second` × 86400 (49 digits)
/// - `planck_per_year` = `planck_per_second` × 31557600 (51 digits)
///
/// Therefore `span_digits` ≈ 44 + log10(seconds) for any span of a
/// second or more, and for a sub-second span the exponent of the seconds
/// is `span_digits` − 44, from which a prefix — micro, nano, pico,
/// femto, atto — is chosen by range and a power of ten from the table
/// is printed before it. Below 10^-30 seconds the function gives up on
/// prefixes and prints the exponent; below 10^10 Planck times it gives
/// up on seconds altogether and prints Planck times, which is the unit
/// the calendar wanted to use from the start.
pub fn format_span(&self) -> String {
let digits = self.span_digits();
// The thresholds, from `derive_constants.sh` at the workspace root.
// `planck_per_second` has 44 digits, so a span of:
// 44 digits is about a second;
// 46 digits about a minute, sixty of those;
// 47 to 48 digits an hour to a day;
// 49 to 50 digits days to months, a range the calendar labels
// "days to years" so as not to say the other word;
// 51 digits about a year, the Julian one;
// 60 digits about a billion years.
// The arms below are these numbers and nothing cleverer.
match digits {
0..=10 => format!("~10^{digits} Planck times"),
11..=43 => {
// The sub-second range: 11 to 43 digits is 10^-33 to 10^-1
// seconds, and the exponent of the seconds is the digit
// count less the forty-four digits of one second.
let seconds_exp = i32::try_from(digits).expect("a digit count beyond an i32") - 44;
if seconds_exp < -30 {
format!("~10^{seconds_exp} seconds")
} else if seconds_exp < -15 {
// Attoseconds, 10^-18 s: eighteen is added to the
// exponent to name the count of them.
format!("~{} attoseconds", pow10(seconds_exp + 18))
} else if seconds_exp < -9 {
// Femtoseconds, 10^-15 s: fifteen added.
format!("~{} femtoseconds", pow10(seconds_exp + 15))
} else if seconds_exp < -6 {
// Picoseconds, 10^-12 s: twelve added.
format!("~{} picoseconds", pow10(seconds_exp + 12))
} else if seconds_exp < -3 {
// Nanoseconds, 10^-9 s: nine added — the kernel's own
// unit, and the last one it can vouch for.
format!("~{} nanoseconds", pow10(seconds_exp + 9))
} else {
// Microseconds, 10^-6 s: six added.
format!("~{} microseconds", pow10(seconds_exp + 6))
}
}
44..=45 => "~seconds".to_string(),
46 => "~minutes".to_string(),
47..=48 => "~hours to days".to_string(),
49..=50 => "~days to years".to_string(),
51..=53 => "~years to millennia".to_string(),
54..=59 => "~millions to billions of years".to_string(),
60..=70 => "~billions to trillions of years".to_string(),
_ => format!("~10^{} years", digits - 51),
}
}
}
/// One event of cosmic history as the timeline knows it: a name for the
/// list beneath the bar, a coordinate for the caret upon it, an identifier
/// for the passage that opens when the caret is clicked, and a threshold
/// that says how far in the observer must have zoomed to be shown it at
/// all. The table of them is `cosmic_events`. The events themselves are not
/// invented here, only placed, and the placing is done in the calendar's
/// own integers from the numbers the passages in `events` give.
pub struct CosmicEvent {
/// The identifier that ties this marker to its passage — the same
/// `EventId` that `events::get_passage` answers to, so that a caret and
/// the text behind it cannot come apart.
pub id: EventId,
/// The short name printed in the list beneath the bar, in amber, behind
/// a dash, at most eight of them at once.
pub name: &'static str,
/// The event's coordinate, in Planck times since the Whisper: negative
/// for everything that happened before 06:30:41 UTC on 30 October 1969,
/// which is nearly all of it, and positive for the handful of things
/// that have happened since and the many that have not happened yet.
pub planck_time: I512,
/// The widest view, measured in digits of its span, that shows this
/// event: it appears when `view.span_digits() <= max_span_digits` and
/// is withheld from anything wider. A small number is an event that
/// demands a close look; a large one is visible from far off. The
/// numbers are tiered by hand so that no view, at any zoom, lists more
/// events than the panel beneath the bar has lines, and a test holds
/// them to it.
pub max_span_digits: usize,
}
/// The most events any view lists, and the number of lines the panel
/// beneath the bar has for listing them: eight. The thresholds in
/// `cosmic_events` are tiered so that no view, at any zoom, has more to say
/// than it has lines to say it in, and the test
/// `no_view_lists_more_events_than_the_panel_holds` slides a window of
/// every width from 10^1 to 10^154 Planck times along the chronology to
/// make sure. The Observatory takes the first `EVENT_LINES` of whatever is
/// visible, which the thresholds guarantee is all of it; the take is a
/// statement, not a truncation.
pub const EVENT_LINES: usize = 8;
/// The chronology: every event the timeline can show, from the end of the
/// Planck epoch to the evaporation of the last black hole, in the order it
/// happened or is expected to, each placed in Planck times since the
/// Whisper by integer arithmetic on the numbers its passage gives. Built
/// on first request and kept for the life of the process by
/// `visible_events`, the universe's past not being expected to change
/// between frames.
///
/// # Integer Arithmetic for Temporal Constants
///
/// Every conversion herein is performed in `I512`, and the seductive
/// imprecision of floating-point is not merely avoided but never offered.
/// A time given in scientific notation — 10^-36 seconds, say — is split
/// into a mantissa and an exponent; the mantissa is multiplied by the
/// Planck times in a second, and the product is scaled by a power of ten
/// from the table, multiplying for a positive exponent and dividing for a
/// negative one. Years are multiplied by the Planck times in a Julian year.
/// Dates given as "so many years ago" are restated from the Whisper, which
/// is the present as far as the calendar is concerned; the helper
/// `years_before_present_to_planck` says why that matters. Civil dates —
/// the Julian calendar, ENIAC, the genesis block — are placed by their Unix
/// timestamps through the same offset the library applies to the kernel's
/// clock, so that a marker that names a day sits on that day, and the test
/// `civil_dates_land_on_their_days` holds it there.
///
/// # On the Thresholds
///
/// An event's `max_span_digits` says how far in you must have zoomed to be
/// shown it. The numbers are tiered by hand so that the whole past fits its
/// eight lines at the widest views (the first stars, the Solar System, the
/// Sun's death) and the rest surface as the view narrows, each in the
/// company of its own era: a window a billion years wide lists the Snowball
/// and the Cambrian, a window seventeen years wide lists the Whisper and
/// the Unix epoch. The test `no_view_lists_more_events_than_the_panel_holds`
/// is the proof that the tiers hold, and the reason the numbers are what
/// they are and not rounder.
///
/// # Panics
///
/// Panics if either decimal reliquary at the top of this module fails to
/// parse as an `I512`, which is to say if someone has edited forty-four or
/// fifty-one digits and introduced a letter; the table is built once, so
/// the panic would arrive with the first frame and not later. No entry in
/// the table can panic on its own: every exponent handed to the helpers is
/// a small literal, far inside the ranges their conversions guard.
#[allow(
clippy::too_many_lines,
reason = "a table of sixty events and their derivations is one table; cut into pieces it would stop reading as the chronology it is"
)]
pub fn cosmic_events() -> Vec<CosmicEvent> {
let planck_per_second = I512::from_str(PLANCK_PER_SECOND_DECIMAL).unwrap();
let planck_per_year = I512::from_str(PLANCK_PER_YEAR_DECIMAL).unwrap();
// The present, as the table understands it: the age of the universe in
// Julian years at the Whisper, 13,806,491,023. Read the digits — all
// eleven — and you have read the Boltzmann constant, 1.380649 × 10⁻²³
// J/K, mantissa then exponent, straight through with the sign dropped:
// the exchange rate between information and heat, buried in the age of
// everything (workspace README, section 2.1). The library carries it as
// `AGE_UNIVERSE_JULIAN_YEARS`; the derivation is `derive_constants.sh`
// at the workspace root. Every "years ago" in this table is subtracted
// from this number, not from the year you are reading it in.
let present_years: i64 = 13_806_491_023;
// Seconds after the Big Bang, given in scientific notation as
// mantissa × 10^exponent, to Planck times since the Whisper: the first
// instants of the universe are too brief for any other notation and too
// sacred for any other arithmetic. Integer throughout.
let seconds_sci_to_planck = |mantissa: i64, exponent: i32| -> I512 {
// The product is reassociated so that the scaling by the power of
// ten comes last, on the largest operand:
// planck = seconds × planck_per_second
// = mantissa × 10^exp × planck_per_second
// = mantissa × planck_per_second × 10^exp
// For the sub-second times this helper exists for, exp is negative
// and the last step is a division by 10^(−exp) of a number already
// forty-odd digits wide, which is why the multiplication precedes
// it: dividing first would leave nothing to multiply.
let base = planck_per_second * (mantissa).as_::<I512>();
let ten = 10i64.as_::<I512>();
// A positive exponent multiplies, a negative one divides; the table
// reaches 10^18, and anything beyond it — the Grand Unification
// epoch at 10^-36 seconds, the end of inflation at 10^-32 — is
// applied a power at a time by a fold, thirty-odd divisions by ten
// standing in for the one division by a number the table declines
// to hold.
let planck_offset = if let Ok(exp) = usize::try_from(exponent) {
POWERS_OF_10.get(exp).map_or_else(
|| (0..exp).fold(base, |acc, _| acc * ten),
|&power| base * power.as_::<I512>(),
)
} else {
let exp = usize::try_from(-exponent).expect("a negated negative exponent is positive");
POWERS_OF_10.get(exp).map_or_else(
|| (0..exp).fold(base, |acc, _| acc / ten),
|&power| base / power.as_::<I512>(),
)
};
creation_planck() + planck_offset
};
// Whole seconds after the Big Bang to Planck times since the Whisper:
// one multiplication, exact, which is the reason the second was
// transcribed to forty-four digits rather than approximated to two.
let seconds_to_planck =
|seconds: i64| -> I512 { creation_planck() + planck_per_second * (seconds).as_::<I512>() };
// Whole Julian years after the Big Bang, likewise: the year's Planck
// count, all fifty-one digits of it, times the count of years.
let years_to_planck =
|years: i64| -> I512 { creation_planck() + planck_per_year * (years).as_::<I512>() };
// Years after the Big Bang with a magnitude attached: mantissa × 10^exp,
// the ten applied one power at a time in `I512`, so that
// `years_exp_to_planck(1, 14)` is 10^14 years after the Big Bang — the
// death of the last stars — and `years_exp_to_planck(1, 100)` is the
// googol at which the last black holes go, a count of years no `i64`
// could have carried to this closure.
let years_exp_to_planck = |mantissa: i64, exponent: u32| -> I512 {
let mut years = (mantissa).as_::<I512>();
for _ in 0..exponent {
years *= (10i64).as_::<I512>();
}
*creation_planck() + planck_per_year * years
};
// Years before the Whisper to Planck times. "Present" is the epoch,
// 1969, not the year you are reading this in: the calendar has exactly
// one present and it has already happened. A date given as "N years
// ago" from a later vantage must be restated from the Whisper's; for
// anything older than a million years the restatement is invisible,
// and for anything younger it is not, which is why the civil dates
// below do not pass through this helper at all.
let years_before_present_to_planck =
|years_ago: i64| -> I512 { years_to_planck(present_years - years_ago) };
// Years after the Whisper to Planck times: the same arithmetic with the
// sign reversed, for the future the calendar has already scheduled.
let years_after_present_to_planck =
|years_future: i64| -> I512 { years_to_planck(present_years + years_future) };
// Years before the present in scientific notation, mantissa × 10^exp
// years ago, for the ages of the Earth and its early life, which are
// known to two or three figures and given here as exactly that many.
let years_ago_exp_to_planck = |mantissa: i64, exponent: u32| -> I512 {
let mut years_ago = (mantissa).as_::<I512>();
for _ in 0..exponent {
years_ago *= (10i64).as_::<I512>();
}
*creation_planck() + planck_per_year * ((present_years).as_::<I512>() - years_ago)
};
// Years after the present in scientific notation, mantissa × 10^exp
// years hence: the oceans, Andromeda, the red giant, given to the
// figures the models share and not one more.
let years_future_exp_to_planck = |mantissa: i64, exponent: u32| -> I512 {
let mut years_future = (mantissa).as_::<I512>();
for _ in 0..exponent {
years_future *= (10i64).as_::<I512>();
}
*creation_planck() + planck_per_year * ((present_years).as_::<I512>() + years_future)
};
// A civil instant, given as its Unix timestamp in UTC, placed from the
// Whisper by the calendar's own arithmetic — the timestamp's distance
// from `WHISPER_UNIX_SECONDS`, in seconds, times the Planck times in a
// second — which is exactly what `now()` does to the kernel's clock,
// and so exactly where `now()` would have placed the moment had it been
// running then. Exact to the second, as the dates in the passages are;
// a marker that names a day sits on that day, and one that names a
// second sits on the second.
let civil_to_planck = |unix_seconds: i64| -> I512 {
planck_per_second * (unix_seconds - WHISPER_UNIX_SECONDS).as_::<I512>()
};
vec![
// ════════════════════════════════════════════════════════════════════
// THE PLANCK ERA: the first 10^-43 seconds, one Planck time, the unit the calendar counts in
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::PlanckEpochEnds,
name: "Planck Epoch ends",
planck_time: *creation_planck() + (1i64).as_::<I512>(),
max_span_digits: 5,
},
// ════════════════════════════════════════════════════════════════════
// THE GRAND UNIFICATION ERA: 10^-43 to 10^-36 seconds, three forces still on speaking terms
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::GrandUnificationEpoch,
name: "Grand Unification Epoch",
planck_time: seconds_sci_to_planck(1, -36), // 1e-36 seconds
max_span_digits: 15,
},
// ════════════════════════════════════════════════════════════════════
// THE ELECTROWEAK ERA: 10^-36 to 10^-12 seconds, inflation, and the Higgs field turning on
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::InflationEnds,
name: "Inflation ends",
planck_time: seconds_sci_to_planck(1, -32), // 1e-32 seconds
max_span_digits: 20,
},
CosmicEvent {
id: EventId::ElectroweakEpoch,
name: "Electroweak Epoch",
planck_time: seconds_sci_to_planck(1, -12), // 1e-12 seconds
max_span_digits: 35,
},
// ════════════════════════════════════════════════════════════════════
// THE QUARK ERA: 10^-12 to 10^-5 seconds, a plasma in which nothing has yet been confined
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::QuarkEpoch,
name: "Quark Epoch",
planck_time: seconds_sci_to_planck(1, -6), // 1e-6 seconds
max_span_digits: 40,
},
// ════════════════════════════════════════════════════════════════════
// THE HADRON ERA: 10^-5 seconds to one second, the quarks confined and the antimatter gone
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::HadronEpoch,
name: "Hadron Epoch",
planck_time: seconds_sci_to_planck(1, -4), // 1e-4 seconds
max_span_digits: 42,
},
CosmicEvent {
id: EventId::NeutrinoDecoupling,
name: "Neutrino decoupling",
planck_time: seconds_to_planck(1), // 1 second
max_span_digits: 46,
},
// ════════════════════════════════════════════════════════════════════
// NUCLEOSYNTHESIS: one second to twenty minutes, the whole universe run as one reactor
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::NucleosynthesisBegins,
name: "Nucleosynthesis begins",
planck_time: seconds_to_planck(10), // 10 seconds
max_span_digits: 47,
},
CosmicEvent {
id: EventId::NucleosynthesisEnds,
name: "Nucleosynthesis ends",
planck_time: seconds_to_planck(1200), // 20 minutes
max_span_digits: 49,
},
// ════════════════════════════════════════════════════════════════════
// THE PHOTON ERA: twenty minutes to 380,000 years, opaque, cooling, and nothing scheduled
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::CoolerThanTheSunsCore,
name: "Cooler than the Sun's core",
// In the radiation era the temperature falls as the inverse
// square root of the age, T ≈ 1.3 × 10^10 K / sqrt(t / 1 s); the
// Sun's core, 15.7 million K, is crossed at ~7 × 10^5 s, and the
// marker sits there, a week and a day in.
planck_time: seconds_to_planck(700_000), // ~8 days
max_span_digits: 51,
},
CosmicEvent {
id: EventId::MatterRadiationEquality,
name: "Matter-radiation equality",
planck_time: years_to_planck(47_000), // 47,000 years
max_span_digits: 56,
},
CosmicEvent {
id: EventId::Recombination,
name: "Recombination (CMB)",
planck_time: years_to_planck(380_000), // 380,000 years
max_span_digits: 58,
},
// ════════════════════════════════════════════════════════════════════
// THE DARK AGES: 380,000 to some 200 million years, and not one photon that was new
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::DarkAgesBegin,
name: "Dark Ages begin",
planck_time: years_to_planck(380_000), // 380,000 years
max_span_digits: 60,
},
// ════════════════════════════════════════════════════════════════════
// COSMIC DAWN: some 200 to 500 million years, the first stars and the first galaxies
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::FirstStarsIgnite,
name: "First stars ignite",
planck_time: years_to_planck(200_000_000), // 200 million years
max_span_digits: 63,
},
CosmicEvent {
id: EventId::FirstGalaxiesForm,
name: "First galaxies form",
planck_time: years_to_planck(300_000_000), // 300 million years
max_span_digits: 60,
},
// ════════════════════════════════════════════════════════════════════
// THE STELLIFEROUS ERA: the age of stars, from reionization to the noon and the afternoon
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::ReionizationComplete,
name: "Reionization complete",
planck_time: years_to_planck(1_000_000_000), // 1 billion years
max_span_digits: 61,
},
CosmicEvent {
id: EventId::CosmicNoon,
name: "Cosmic noon",
planck_time: years_to_planck(3_500_000_000), // z ≈ 2, 3.5 billion years
max_span_digits: 62,
},
CosmicEvent {
id: EventId::ExpansionAccelerates,
name: "Expansion begins to accelerate",
planck_time: years_to_planck(7_500_000_000), // z ≈ 0.7, ~6 billion years ago
max_span_digits: 62,
},
// ════════════════════════════════════════════════════════════════════
// THE SOLAR SYSTEM: some 9.2 billion years after the Big Bang, an arm, a cloud, a star
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::SolarSystemForms,
name: "Solar System forms",
planck_time: years_ago_exp_to_planck(46, 8), // 4.6 billion years ago
max_span_digits: 63,
},
CosmicEvent {
id: EventId::EarthForms,
name: "Earth forms",
planck_time: years_ago_exp_to_planck(454, 7), // 4.54 billion years ago
max_span_digits: 61,
},
CosmicEvent {
id: EventId::MoonForms,
name: "Moon-forming impact",
planck_time: years_ago_exp_to_planck(45, 8), // 4.5 billion years ago
max_span_digits: 60,
},
// ════════════════════════════════════════════════════════════════════
// LIFE ON EARTH: from a chemistry nobody understands to the extinctions that made room
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::FirstLife,
name: "First life emerges",
planck_time: years_ago_exp_to_planck(38, 8), // 3.8 billion years ago
max_span_digits: 62,
},
CosmicEvent {
id: EventId::GreatOxidation,
name: "Great Oxidation Event",
planck_time: years_ago_exp_to_planck(243, 7), // 2.43 billion years ago
max_span_digits: 60,
},
CosmicEvent {
id: EventId::FirstEukaryotes,
name: "First eukaryotes",
planck_time: years_ago_exp_to_planck(165, 7), // 1.65 billion years ago
max_span_digits: 60,
},
CosmicEvent {
id: EventId::SnowballEarth,
name: "Snowball Earth",
planck_time: years_before_present_to_planck(717_000_000), // Sturtian onset
max_span_digits: 60,
},
CosmicEvent {
id: EventId::CambrianExplosion,
name: "Cambrian Explosion",
planck_time: years_before_present_to_planck(539_000_000), // 538.8 million years ago
max_span_digits: 60,
},
CosmicEvent {
id: EventId::PermianExtinction,
name: "The Great Dying (Permian)",
planck_time: years_before_present_to_planck(252_000_000), // 251.9 million years ago
max_span_digits: 60,
},
CosmicEvent {
id: EventId::DinosaurExtinction,
name: "Dinosaur extinction",
planck_time: years_before_present_to_planck(66_000_000), // 66 million years ago
max_span_digits: 61,
},
// ════════════════════════════════════════════════════════════════════
// HUMAN HISTORY: three hundred thousand years, the last two percent of them written down
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::HomoSapiens,
name: "Homo sapiens",
planck_time: years_before_present_to_planck(300_000), // Jebel Irhoud
max_span_digits: 59,
},
CosmicEvent {
id: EventId::FirstFarmers,
name: "First farmers",
planck_time: years_before_present_to_planck(11_500), // Fertile Crescent
max_span_digits: 57,
},
CosmicEvent {
id: EventId::FirstWriting,
name: "First writing",
planck_time: years_before_present_to_planck(5400), // 5400 years ago
max_span_digits: 56,
},
CosmicEvent {
id: EventId::JulianCalendar,
name: "Julian calendar",
planck_time: civil_to_planck(-63_555_926_400), // 1 January 45 BC (Julian)
max_span_digits: 55,
},
CosmicEvent {
id: EventId::LeibnizBinary,
name: "Leibniz explains binary",
planck_time: civil_to_planck(-8_417_952_000), // April 1703
max_span_digits: 54,
},
CosmicEvent {
id: EventId::PlanckUnits,
name: "Planck units proposed",
planck_time: civil_to_planck(-2_228_688_000), // 18 May 1899
max_span_digits: 53,
},
CosmicEvent {
id: EventId::FirstComputer,
name: "First electronic computer",
planck_time: civil_to_planck(-759_283_200), // 10 December 1945, ENIAC's first run
max_span_digits: 52,
},
CosmicEvent {
id: EventId::FirstTransistor,
name: "First transistor",
planck_time: civil_to_planck(-695_692_800), // 16 December 1947
max_span_digits: 52,
},
// ════════════════════════════════════════════════════════════════════
// THE WHISPER AND BEYOND: around epoch zero, where the count changes sign
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::ArpanetWhisper,
name: "The Whisper (ARPANET)",
planck_time: I512_ZERO, // the epoch itself: zero, by definition and by decree
max_span_digits: 54,
},
CosmicEvent {
id: EventId::UnixEpoch,
name: "Unix epoch",
planck_time: civil_to_planck(0), // 1 January 1970 00:00:00 UTC
max_span_digits: 52,
},
CosmicEvent {
id: EventId::FirstEmail,
name: "First email",
planck_time: years_after_present_to_planck(2), // late 1971; the month is not recorded
max_span_digits: 52,
},
CosmicEvent {
id: EventId::FirstLeapSecond,
name: "First leap second",
planck_time: civil_to_planck(78_796_800), // 1972-06-30 23:59:60 ends here
max_span_digits: 52,
},
CosmicEvent {
id: EventId::FirstWebsite,
name: "First website",
planck_time: civil_to_planck(681_436_800), // 6 August 1991
max_span_digits: 52,
},
CosmicEvent {
id: EventId::FirstSmartphone,
name: "iPhone released",
planck_time: civil_to_planck(1_183_075_200), // 29 June 2007
max_span_digits: 52,
},
CosmicEvent {
id: EventId::FirstBitcoinBlock,
name: "Bitcoin genesis block",
planck_time: civil_to_planck(1_231_006_505), // 3 January 2009 18:15:05 UTC
max_span_digits: 52,
},
CosmicEvent {
id: EventId::UnixTimeOverflows,
name: "32-bit time overflows",
planck_time: civil_to_planck(2_147_483_648), // 19 January 2038 03:14:08 UTC
max_span_digits: 52,
},
// ════════════════════════════════════════════════════════════════════
// THE FAR FUTURE: Voyager, the continents, the oceans, Andromeda, the Sun, the last stars
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::VoyagerPassesGliese445,
name: "Voyager 1 passes Gliese 445",
planck_time: years_after_present_to_planck(40_000), // ~40,000 years from now
max_span_digits: 57,
},
CosmicEvent {
id: EventId::PangaeaProxima,
name: "Pangaea Proxima",
planck_time: years_future_exp_to_planck(25, 7), // 250 million years from now
max_span_digits: 60,
},
CosmicEvent {
id: EventId::OceansEvaporate,
name: "Oceans evaporate",
planck_time: years_future_exp_to_planck(1, 9), // ~1 billion years from now
max_span_digits: 60,
},
CosmicEvent {
id: EventId::AndromedaArrives,
name: "Andromeda arrives (even odds)",
planck_time: years_future_exp_to_planck(45, 8), // 4.5 billion years from now
max_span_digits: 62,
},
CosmicEvent {
id: EventId::SunRedGiant,
name: "Sun becomes red giant",
planck_time: years_future_exp_to_planck(5, 9), // 5 billion years from now
max_span_digits: 65,
},
CosmicEvent {
id: EventId::LocalGroupAlone,
name: "Local Group alone",
planck_time: years_exp_to_planck(1, 11), // 10^11 years after Big Bang
max_span_digits: 64,
},
CosmicEvent {
id: EventId::LastStarsDie,
name: "Last stars die",
planck_time: years_exp_to_planck(1, 14), // 10^14 years after Big Bang
max_span_digits: 70,
},
// ════════════════════════════════════════════════════════════════════
// THE DEGENERATE ERA: the remnants cooling, the planets stripped, the galaxies unbound
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::DegenerateEra,
name: "Degenerate Era",
planck_time: years_exp_to_planck(1, 15), // 10^15 years
max_span_digits: 75,
},
CosmicEvent {
id: EventId::GalaxiesDisperse,
name: "Galaxies disperse",
planck_time: years_exp_to_planck(1, 19), // 10^19 years
max_span_digits: 80,
},
// ════════════════════════════════════════════════════════════════════
// THE BLACK HOLE ERA: if the protons decay, only holes are left; if not, the rocks go round
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::ProtonDecayComplete,
name: "Proton decay (if any) complete",
planck_time: years_exp_to_planck(1, 40), // 10^40 years
max_span_digits: 100,
},
CosmicEvent {
id: EventId::BlackHoleEraBegins,
name: "Black Hole Era begins",
planck_time: years_exp_to_planck(1, 41), // 10^41 years (after proton decay)
max_span_digits: 102,
},
CosmicEvent {
id: EventId::RocksBecomeSpheres,
name: "Rocks flow into spheres",
planck_time: years_exp_to_planck(1, 65), // 10^65 years, if protons persist
max_span_digits: 118,
},
// ════════════════════════════════════════════════════════════════════
// THE DARK ERA: the holes evaporate, the dwarfs leak away, the bits run out
// ════════════════════════════════════════════════════════════════════
CosmicEvent {
id: EventId::StellarBlackHolesEvaporate,
name: "Stellar black holes evaporate",
planck_time: years_exp_to_planck(1, 67), // 10^67 years
max_span_digits: 120,
},
CosmicEvent {
id: EventId::WhiteDwarfsErode,
name: "White dwarfs erode away",
planck_time: years_exp_to_planck(1, 78), // 10^78 years
max_span_digits: 130,
},
CosmicEvent {
id: EventId::SagittariusAEvaporates,
name: "Sagittarius A* evaporates",
planck_time: years_exp_to_planck(1, 87), // ~10^87 years at its present mass
max_span_digits: 140,
},
CosmicEvent {
id: EventId::SupermassiveBlackHolesEvaporate,
name: "Supermassive black holes evaporate",
planck_time: years_exp_to_planck(1, 100), // 10^100 years
max_span_digits: 150,
},
]
}
/// The events this view is entitled to show: those whose threshold admits
/// the view's digit count and whose coordinate lies within its two ends,
/// in the table's order, which is chronological, so that the list beneath
/// the bar reads in the order things happened. Furnished as references
/// into a table built once and kept for the life of the process — the
/// Observatory asks this once per frame, the frames are not throttled, and
/// the past does not change often enough to justify placing sixty events
/// in `I512` again each time. What changes is the view, and the view is
/// the only argument.
pub fn visible_events(view: &TimelineView) -> Vec<&'static CosmicEvent> {
// The table, built on first request and never again: sixty events,
// sixty 512-bit placements, one `LazyLock`, and a `'static` lifetime
// that is the truth, since the process cannot outlive its own past.
use std::sync::LazyLock;
static EVENTS: LazyLock<Vec<CosmicEvent>> = LazyLock::new(cosmic_events);
let span_digits = view.span_digits();
EVENTS
.iter()
.filter(|event| {
// An event is shown when the view has narrowed enough to admit
// it — its threshold at or above the span's digit count — and
// when it lies between the view's two ends, inclusive. The first
// test is about the observer; the second is about the event.
span_digits <= event.max_span_digits
&& event.planck_time >= view.start
&& event.planck_time <= view.end
})
.collect()
}
/// The outcome of a click on the bar, reduced to the two numbers the zoom
/// needs: which segment was struck and how many there were to strike. The
/// pair is what `TimelineView::zoom_into` consumes, and what the breadcrumb
/// above the bar prints, one-indexed, as the fraction the observer chose.
#[derive(Clone, Copy)]
pub struct TimelineHit {
/// The segment struck, counting from zero at the bar's left end and
/// never reaching `segment_count`: the bar's right edge is folded into
/// the last segment rather than into a segment that does not exist.
pub segment_index: usize,
/// How many segments the bar was divided into when it was struck — one
/// per glyph cell between the brackets, at the Observatory's width.
pub segment_count: usize,
}
impl TimelineHit {
/// Resolve a click into the segment it landed in. `bar_x` is the pixel
/// at which the bar's first segment begins, just inside the opening
/// bracket; `bar_width` is the width in pixels of all the segments
/// together; `click_x` is where the pointer was released; and
/// `segment_count` is how many equal segments the caller drew, which
/// the Observatory takes to be the number of glyph cells between the
/// brackets. A click outside the bar's horizontal extent yields `None`
/// — the vertical test belongs to the caller, which knows where the
/// bar is and this function does not. Inside it, the offset from the
/// bar's start is divided by the width of one segment and floored, and
/// the result is clamped to the last segment, so that the bar's right
/// edge, which the division would assign to a segment past the end,
/// belongs to the segment it borders.
///
/// # Panics
///
/// In a debug build, panics if `segment_count` is zero, since
/// `segment_count - 1` then underflows a `usize`; a release build wraps
/// instead and answers segment zero of no segments. A bar with no
/// segments has nothing to be clicked, and the caller that drew one has
/// already asked a question this function cannot frame.
pub fn from_click(
bar_x: f32,
bar_width: f32,
click_x: f32,
segment_count: usize,
) -> Option<Self> {
if click_x < bar_x || click_x > bar_x + bar_width {
return None;
}
let relative_x = click_x - bar_x;
let segment_width = bar_width / segment_count as f32;
let segment_index = cell_under(relative_x, segment_width);
Some(Self {
segment_index: segment_index.min(segment_count - 1),
segment_count,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The full view knows itself: constructed from Creation and
/// Annihilation, it answers `is_full`, which is the least a view can do.
#[test]
fn test_full_view() {
let view = TimelineView::full();
assert!(view.is_full());
}
/// One descent into the first of ten segments is no longer the full
/// view and is narrower than it — narrower by the factor of ten the
/// segment count promised, though the test asks only for narrower.
#[test]
fn test_zoom_into() {
let view = TimelineView::full();
let zoomed = view.zoom_into(0, 10);
assert!(!zoomed.is_full());
assert!(zoomed.span() < view.span());
}
/// The full view lists nothing: every event on the table demands a
/// closer look than 154 digits of span affords.
#[test]
fn test_event_visibility() {
let full = TimelineView::full();
let visible = visible_events(&full);
// In the full view, 154 digits wide, nothing is legible: the most
// patient event on the table, the last black hole, waits for 150.
// The assertion is looser than the fact, and the fact is empty.
assert!(visible.is_empty() || visible.len() < 5);
}
/// The full view's width is the library's own total span, bit for bit,
/// even though no I512 can hold the difference of its two ends: the
/// wrapped unsigned subtraction here and the unsigned addition in the
/// library arrive at the same 154 digits by different roads.
#[test]
fn full_span_agrees_with_the_library() {
use loic::total_span;
assert_eq!(TimelineView::full().span(), *total_span());
}
/// The position of an instant is exact at both ends and at the midpoint
/// of every view, from the full span down forty zoom levels: the ratio
/// is taken on the leading bits of both quantities, never on their
/// decimal strings, so no zoom level is quietly off by a power of ten,
/// and the midpoint — the span halved by a shift and added to the start
/// — lands within a millionth of a millionth of one half at every depth.
#[test]
fn position_is_exact_at_ends_and_midpoint_at_every_zoom() {
let mut view = TimelineView::full();
for _ in 0..40 {
let half = (view.span() >> 1u32).cast_signed();
let midpoint = view.start + half;
assert_eq!(view.position_of(&view.start), Some(0.0));
assert_eq!(view.position_of(&view.end), Some(1.0));
let position = view
.position_of(&midpoint)
.expect("a midpoint that is not inside its view is not a midpoint");
assert!(
(position - 0.5).abs() < 1e-12,
"midpoint at {position}, not one half"
);
view = view.zoom_into(17, 58);
}
}
/// The table is a chronology: every event is at or after the one before
/// it, so the panel's list, which takes the table's order, reads in the
/// order things happened. A table that placed the Cambrian after the
/// dinosaurs would still render; it would merely be wrong, in the way
/// this test exists to refuse.
#[test]
fn events_are_chronological() {
let events = cosmic_events();
for pair in events.windows(2) {
assert!(
pair[0].planck_time <= pair[1].planck_time,
"{} is listed before {} in the table and happens after it in the universe",
pair[0].name,
pair[1].name
);
}
}
/// The panel lists `EVENT_LINES` events and no more. A view with `d`
/// span digits is narrower than 10^d Planck times and lists the events
/// whose threshold admits `d`; so for every `d` from 1 to the full
/// span's 154, no window of width 10^d may contain more than
/// `EVENT_LINES` such events. This is the sliding window over the
/// chronology, in the calendar's own integers — offsets from Creation
/// as `U512`, widths as exact powers of ten — and it is the reason the
/// thresholds are what they are and not rounder.
#[test]
fn no_view_lists_more_events_than_the_panel_holds() {
let events = cosmic_events();
let creation = creation_planck().cast_unsigned();
let ten = 10u8.as_::<U512>();
for digits in 1..=154u32 {
let width = ten.pow(digits);
let level = usize::try_from(digits).expect("one hundred and fifty-four fits a usize");
let offsets: Vec<U512> = events
.iter()
.filter(|event| event.max_span_digits >= level)
.map(|event| event.planck_time.cast_unsigned().wrapping_sub(creation))
.collect();
let mut first = 0;
for (last, &offset) in offsets.iter().enumerate() {
while offset - offsets[first] > width {
first += 1;
}
assert!(
last - first < EVENT_LINES,
"a {digits}-digit view lists {} events; the panel has {EVENT_LINES} lines",
last - first + 1
);
}
}
}
/// Civil dates sit exactly where the calendar's own arithmetic puts
/// them: the Unix epoch is 5,419,759 seconds after the Whisper, the
/// genesis block is its timestamp plus that, and the transistor, which
/// is dated before the epoch, lands twenty-two years before it —
/// twenty-one and most of another, which integer division calls
/// twenty-one — and not, as "years before present" would have it from
/// a later present, somewhere in the nineteenth century.
#[test]
fn civil_dates_land_on_their_days() {
let events = cosmic_events();
let planck_per_second = I512::from_str(PLANCK_PER_SECOND_DECIMAL).unwrap();
let seconds_from_whisper = |id: EventId| -> I512 {
let event = events
.iter()
.find(|event| event.id == id)
.expect("an event with a passage and no place in the table");
event.planck_time / planck_per_second
};
assert_eq!(
seconds_from_whisper(EventId::UnixEpoch),
5_419_759i64.as_::<I512>()
);
assert_eq!(
seconds_from_whisper(EventId::FirstBitcoinBlock),
(1_231_006_505i64 + 5_419_759).as_::<I512>()
);
assert_eq!(
seconds_from_whisper(EventId::FirstTransistor),
(-695_692_800i64 + 5_419_759).as_::<I512>()
);
let seconds_per_julian_year = 31_557_600i64.as_::<I512>();
assert_eq!(
seconds_from_whisper(EventId::FirstTransistor) / seconds_per_julian_year,
(-21i64).as_::<I512>()
);
}
}