epiphany/spikes/editor-toolkit/round2-textkit/src/lib.rs

163 lines
9.3 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.

//! # round2-textkit — Packet 2A-i: the candidate-neutral text fixture set.
//!
//! **Nothing in this crate is canonical, and nothing here pre-empts the
//! `.tex` amendment.** `spec/ANALYSIS_TEXT_RUN_PRIMITIVES.md` (W3) §3E and §4
//! propose `ResolvedText` as a fourth resolved-layout primitive; that
//! amendment has not landed. Every type in [`types`] is a **non-canonical
//! spike mirror**, prefixed `Spike*`, that exists only so this crate — the
//! shape's first consumer (`ROUND2_TEXT_RECIPE.md` intro) — can exercise it
//! before the real thing exists. A type here proving awkward to populate is
//! itself the deliverable: pin 8 routes that awkwardness back to the
//! amendment as a finding (see [`findings`]).
//!
//! ## What this crate does
//!
//! 1. Resolves the two declared faces from an explicit path list, hashing
//! their bytes and failing loudly on any mismatch against the recipe's
//! recorded hashes ([`faces`]).
//! 2. Records every pin-9 shaping-identity field, including the two
//! separately-versioned Unicode components W3-F2 names ([`identity`]).
//! 3. Itemizes, shapes, and clusters the five committed fixture strings —
//! `unicode-bidi` for bidi itemization, `rustybuzz` for shaping in the
//! resolved face, `unicode-segmentation` for grapheme caret stops
//! ([`shape`], [`fixtures`]).
//! 4. Asserts every W3 §5 invariant on the result, in code, naming the
//! fixture and the invariant on failure ([`invariants`]).
//! 5. Writes `fixtures.json` and `FIXTURES_SUMMARY.md`, and exposes
//! [`output::FixtureFile::validate`] so a later packet can check the
//! loaded fixtures against literals restated here rather than against the
//! file's own other fields — the same discipline
//! `round1-candidates/harness`'s `OracleFile::validate` uses.
//! 6. Builds and validates the recipe §7 closing-paragraph hit-test *probe
//! table* (`(device point) -> (byte offset, affinity)`), from an
//! already-loaded, already-valid `fixtures.json` — [`hittest`],
//! `bin/generate_hittest.rs`, `hittest_probes.json`. This is
//! candidate-testing apparatus, not part of the candidate-neutral §3E
//! mirror, which is why it is a sibling file rather than extra fields on
//! `fixtures.json`'s own records.
//!
//! ## What this crate does not do
//!
//! It renders nothing. The SVG reference emitter (recipe §9) and the bounded
//! visual differential (recipe §10/§11) are separate packets (`round2-diff`
//! is the differential, built in parallel).
pub mod a11y;
pub mod faces;
pub mod fixtures;
pub mod hittest;
pub mod identity;
pub mod invariants;
pub mod output;
pub mod quantize;
pub mod shape;
pub mod types;
/// Em size, fixed for every fixture (recipe §3, **amended from the
/// original `0.64`**): `1.28` staff spaces = 128 device px at `scale = 100`.
///
/// The recipe originally pinned `0.64` (64 device px). That was wrong:
/// measured against TeX Gyre Pagella at 64 px em, the mid-height stem width
/// of a lowercase vertical (`l`/`i`/`n`, 84 font units at `upem = 1000`) is
/// 5.4 device px — inside `round2-diff`'s `EDGE_BAND_PX = 2` on *each* side
/// of an edge, i.e. a stem narrow enough that the whole stroke sits in the
/// antialiased band D1 is defined to be blind to. Doubling to `1.28` (128 px
/// em) doubles every stem to ~10.8 px (round strokes like `o`/`e` to ~12 px),
/// which leaves interior pixels D1 can actually decide. All five fixtures
/// still clear the 1920 px target at this size — measured device right edges
/// from the generated `fixtures.json`: F-A 1715.1, F-D 1290.7, F-E 1023.0,
/// F-B 809.1, F-C 597.0. F-A is the longest and clears the frame by ~205 px.
pub const EM_SIZE_STAFF_SPACE: f64 = 1.28;
/// The run's baseline origin in staff space (recipe §3): `1638/1024 =
/// 1.599609375`, i.e. device `(159.9609375, 540)` under `scale = 100` device
/// px per staff space, `target = 1920x1080`.
///
/// **Stated on the grid, not rounded onto it.** Invariant 5 requires every
/// position to sit exactly on the `1/1024` staff-space grid, and the recipe's
/// original `1.6` is not representable there (`1.6 × 1024 = 1638.4`). The
/// value used to be `1.6` and [`quantize::quantize_component`] moved it onto
/// the grid on the way past — which worked, and left the *stated* constant in
/// violation of the *stated* invariant. `1638.0 / 1024.0` is exact in `f32`
/// (1638 needs 11 mantissa bits) and in `f64`, so quantization is now a
/// no-op for it and the two agree at the source. Every fixture position is
/// numerically unchanged by this; it is the constant that was wrong, not the
/// output.
pub const RUN_ORIGIN_STAFF: (f32, f32) = (1638.0 / 1024.0, 0.0);
/// Pin 4's offscreen target, restated here (not read from any file) so a
/// validator checks it against a literal, exactly as
/// `round1-candidates/harness`'s `TARGET_WIDTH`/`TARGET_HEIGHT` do.
pub const TARGET_WIDTH: f64 = 1920.0;
pub const TARGET_HEIGHT: f64 = 1080.0;
/// Device pixels per staff space (recipe §3): `scale = 100`. Used by
/// [`hittest`] to convert caret-stop positions to device space, and (for the
/// same reason every other geometric constant here is a literal) restated
/// rather than derived.
pub const DEVICE_SCALE: f64 = 100.0;
/// The canonical-layout quantization grid this text mirror reuses (W3 §3E
/// invariant 5): `1/1024` staff space per unit, identical to
/// `epiphany_determinism::QuantizedCoord`.
pub const QUANTIZE_GRID: f64 = 1024.0;
/// Findings routed back to the W3 `.tex` amendment (recipe §12), discovered
/// during this packet's implementation. `W3_F1`/`W3_F2` are the two the
/// recipe already named before implementation began; anything past those is
/// new to this packet.
pub mod findings {
/// `TextFaceIdentity::version: Option<SemVer>` is the wrong type — real
/// font versions are not semver (recipe §12, precommitted). This crate's
/// [`crate::identity::SpikeTextFaceIdentity::version`] carries the raw
/// name-table string instead, `Option<String>`.
pub const W3_F1: &str = "TextFaceIdentity::version must be Option<String> (raw name-table \
text), not Option<SemVer> — real font versions are not semver.";
/// One `unicode_version` field cannot honestly name two independently
/// versioned components (bidi itemization, grapheme segmentation) (recipe
/// §12, precommitted). This crate's
/// [`crate::identity::SpikeTextShapingIdentity`] carries
/// `unicode_bidi` and `unicode_segmentation` as two separate fields.
pub const W3_F2: &str = "unicode_version must be two fields (bidi, segmentation), each \
naming its own implementation, crate version, and Unicode-data version — a single \
field cannot honestly name two independently-versioned components.";
/// **New in this packet.** `ShapedSegment::face: u32` has no value to
/// record when a run's codepoints are covered by *no* declared face
/// (F-C, U+0627). Invariant 2 requires segment source ranges to
/// *totally* partition the string — visual order may differ from
/// logical order, but the partition must be total — so an unresolved
/// span cannot simply have no segment; it needs a segment whose `face`
/// field can say "none of the chain." This crate's
/// [`crate::types::SpikeShapedSegment::face`] is `Option<u32>`, not
/// `u32`, for exactly this reason: `None` names "resolution walked the
/// whole declared chain and nothing covered this span," carrying the
/// same information invariant 4's unresolved-cluster marker carries, at
/// the segment granularity invariant 2 needs it at.
pub const W3_F3: &str = "ShapedSegment::face must be Option<u32>, not u32 — an unresolved \
span (no face in the declared chain covers it) still needs a segment for invariant 2's \
total partition, and that segment has no face index to report.";
/// **New in this packet, found by writing the wire format §3E does not
/// have.** `epiphany-layout-ir` has no `serde` dependency, so nothing in
/// §3E — `ResolvedText` included — can be serialized. Every consumer that
/// needs to persist, cache, dump, or send one across a process boundary
/// therefore has to hand-write a mirror, and the very first consumer to do
/// it (this crate) wrote one that was quietly lossy for two `Provenance`
/// fields until it was caught in review: a `Debug` rendering in place of
/// `source`, and a length in place of `dependencies`.
///
/// The mirror is fixed here (see [`crate::types::SpikeProvenance`]), but
/// the *shape of the mistake* is what routes back: an incremental-layout
/// cache and an out-of-process renderer are both plainly in W3's future,
/// each needs this exact conversion, and each will write it independently.
/// The amendment should say what `ResolvedText`'s serialized form is —
/// derive it, or specify a canonical byte form as Chapter 5 does for
/// `TypedObjectId` — rather than leave one per consumer.
pub const W3_F6: &str = "§3E defines no serialized form for ResolvedText, and \
epiphany-layout-ir carries no serde at all, so every consumer that must persist or send \
one hand-writes its own mirror — the first one written (this spike's) was lossy for two \
Provenance fields. The amendment should specify the serialized form once.";
}