142 lines
4.9 KiB
Rust
142 lines
4.9 KiB
Rust
//! The tight axis-aligned bounding box of a typed outline (Editor T4-pre W2
|
|
//! test g4): used only to prove the bundled outlines' drawn ink is contained
|
|
//! by their declared `BRAVURA_METRICS` bounding box — the cross-table
|
|
//! consistency `layout-ir/src/glyph.rs:107-109` asserts in prose ("the
|
|
//! reserved advances/bboxes and the drawn ink agree") but no test in the
|
|
//! tree checked directly from parsed outline geometry before this packet.
|
|
|
|
use epiphany_layout_ir::PathCommand;
|
|
|
|
/// The exact bounding box `[left, bottom, right, top]` of the ink a sequence
|
|
/// of typed path commands draws, computed from real cubic-bezier extrema —
|
|
/// not just control points, which routinely lie outside a curve's own tight
|
|
/// bounds — matching what `tools/extract_bravura_outlines.py`'s
|
|
/// `fontTools.pens.boundsPen.BoundsPen` computes at generation time (it
|
|
/// overrides `curveToOne` with the same real-extrema calculation, unlike its
|
|
/// `ControlBoundsPen` base class). Returns `None` for an empty command list.
|
|
pub(crate) fn outline_extent(commands: &[PathCommand]) -> Option<[f32; 4]> {
|
|
let mut min_x = f32::INFINITY;
|
|
let mut min_y = f32::INFINITY;
|
|
let mut max_x = f32::NEG_INFINITY;
|
|
let mut max_y = f32::NEG_INFINITY;
|
|
let mut cur = (0.0f32, 0.0f32);
|
|
let mut any = false;
|
|
for cmd in commands {
|
|
match cmd {
|
|
PathCommand::MoveTo(p) | PathCommand::LineTo(p) => {
|
|
let (x, y) = (p.x.0, p.y.0);
|
|
min_x = min_x.min(x);
|
|
min_y = min_y.min(y);
|
|
max_x = max_x.max(x);
|
|
max_y = max_y.max(y);
|
|
cur = (x, y);
|
|
any = true;
|
|
}
|
|
PathCommand::CurveTo {
|
|
control1,
|
|
control2,
|
|
to,
|
|
} => {
|
|
let (x0, y0) = cur;
|
|
let (lo_x, hi_x) = cubic_extrema_1d(x0, control1.x.0, control2.x.0, to.x.0);
|
|
let (lo_y, hi_y) = cubic_extrema_1d(y0, control1.y.0, control2.y.0, to.y.0);
|
|
min_x = min_x.min(lo_x);
|
|
max_x = max_x.max(hi_x);
|
|
min_y = min_y.min(lo_y);
|
|
max_y = max_y.max(hi_y);
|
|
any = true;
|
|
cur = (to.x.0, to.y.0);
|
|
}
|
|
PathCommand::Close => {}
|
|
}
|
|
}
|
|
if any {
|
|
Some([min_x, min_y, max_x, max_y])
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// The `[min, max]` extent of a 1-D cubic bezier `p0..p3` over `t in [0,1]`:
|
|
/// the two endpoints plus any interior critical point of the derivative (a
|
|
/// root of the quadratic `B'(t)/3 = a*t^2 + b*t + c`).
|
|
fn cubic_extrema_1d(p0: f32, p1: f32, p2: f32, p3: f32) -> (f32, f32) {
|
|
let mut lo = p0.min(p3);
|
|
let mut hi = p0.max(p3);
|
|
let d0 = p1 - p0;
|
|
let d1 = p2 - p1;
|
|
let d2 = p3 - p2;
|
|
let a = d0 - 2.0 * d1 + d2;
|
|
let b = 2.0 * (d1 - d0);
|
|
let c = d0;
|
|
let mut consider = |t: f32| {
|
|
if (0.0..=1.0).contains(&t) {
|
|
let mt = 1.0 - t;
|
|
let v =
|
|
mt * mt * mt * p0 + 3.0 * mt * mt * t * p1 + 3.0 * mt * t * t * p2 + t * t * t * p3;
|
|
lo = lo.min(v);
|
|
hi = hi.max(v);
|
|
}
|
|
};
|
|
if a.abs() < 1e-12 {
|
|
if b.abs() > 1e-12 {
|
|
consider(-c / b);
|
|
}
|
|
} else {
|
|
let disc = b * b - 4.0 * a * c;
|
|
if disc >= 0.0 {
|
|
let sqrt_disc = disc.sqrt();
|
|
consider((-b + sqrt_disc) / (2.0 * a));
|
|
consider((-b - sqrt_disc) / (2.0 * a));
|
|
}
|
|
}
|
|
(lo, hi)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use epiphany_layout_ir::Point;
|
|
|
|
#[test]
|
|
fn straight_line_extent_is_its_endpoints() {
|
|
let cmds = vec![
|
|
PathCommand::MoveTo(Point::new(0.0, 0.0)),
|
|
PathCommand::LineTo(Point::new(2.0, 3.0)),
|
|
];
|
|
assert_eq!(outline_extent(&cmds), Some([0.0, 0.0, 2.0, 3.0]));
|
|
}
|
|
|
|
#[test]
|
|
fn empty_commands_have_no_extent() {
|
|
assert_eq!(outline_extent(&[]), None);
|
|
}
|
|
|
|
#[test]
|
|
fn curve_extrema_are_found_beyond_the_endpoints() {
|
|
// The classic "hump" cubic (P0=(0,0), P1=(0,1), P2=(1,1), P3=(1,0)):
|
|
// its peak y at t=0.5 is 0.75, well past either endpoint's y=0 — a
|
|
// control-point-only (or endpoint-only) bound would miss it.
|
|
let cmds = vec![
|
|
PathCommand::MoveTo(Point::new(0.0, 0.0)),
|
|
PathCommand::CurveTo {
|
|
control1: Point::new(0.0, 1.0),
|
|
control2: Point::new(1.0, 1.0),
|
|
to: Point::new(1.0, 0.0),
|
|
},
|
|
];
|
|
let ext = outline_extent(&cmds).unwrap();
|
|
assert!(
|
|
(ext[3] - 0.75).abs() < 1e-5,
|
|
"expected top ~0.75, got {}",
|
|
ext[3]
|
|
);
|
|
assert_eq!(ext[0], 0.0, "left stays at the endpoints' x");
|
|
assert_eq!(ext[2], 1.0, "right stays at the endpoints' x");
|
|
assert_eq!(
|
|
ext[1], 0.0,
|
|
"bottom is the (only) minimum, at both endpoints"
|
|
);
|
|
}
|
|
}
|