mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
text: a work limit on Knuth-Plass; past it break_paragraph falls back to greedy (#868)
This commit is contained in:
1 parent
eadfaf85ed
commit
ce4f4816a1
3 files changed
+41
-3
No files matched your search
@@ -4,6 +4,11 @@ use super::{Item, Kind, Line, Params, Sums, clamp_measure, fitness_class};
|
||||
|
||||
/// Give up (return `None`) beyond this many active breaks.
|
||||
const MAX_ACTIVE: usize = 4096;
|
||||
/// Give up (return `None`) after this much work, counted as one for each break considered and one
|
||||
/// for each active break it is tried against. A paragraph of 400,000 items with 30 active breaks
|
||||
/// takes some 12,000,000 on a pass; the limit is where a pass runs for seconds, and past it the
|
||||
/// caller falls back to [`super::greedy`], which keeps every item.
|
||||
pub(super) const MAX_WORK: u64 = 1 << 27;
|
||||
/// Ratios beyond this are rated as this (an unstretchable line is infinitely loose).
|
||||
const MAX_RATIO: f64 = 10_000.0;
|
||||
/// Demerits of an overfull line accepted because nothing else reaches a break.
|
||||
@@ -37,8 +42,14 @@ struct Candidate {
|
||||
|
||||
/// One total-fit pass over `items`. `width(k)` is the measure of line `k`; set
|
||||
/// [`Params::uniform_from`] when it varies. Returns `None` when no set of breaks keeps every line
|
||||
/// within [`Params::tolerance`] (unless [`Params::overfull`]), or past the active-break limit.
|
||||
/// within [`Params::tolerance`] (unless [`Params::overfull`]), or past the active-break or work limit.
|
||||
pub fn knuth_plass(items: &[Item], width: &dyn Fn(usize) -> f64, p: &Params) -> Option<Vec<Line>> {
|
||||
knuth_plass_within(items, width, p, &mut MAX_WORK.clone())
|
||||
}
|
||||
|
||||
/// [`knuth_plass`] spending at most `work` (left over on return), so that the passes of one
|
||||
/// paragraph share a budget.
|
||||
pub(super) fn knuth_plass_within(items: &[Item], width: &dyn Fn(usize) -> f64, p: &Params, work: &mut u64) -> Option<Vec<Line>> {
|
||||
let p = &p.sanitized();
|
||||
let width = |k: usize| clamp_measure(width(k));
|
||||
let s = Sums::new(items);
|
||||
@@ -57,6 +68,7 @@ pub fn knuth_plass(items: &[Item], width: &dyn Fn(usize) -> f64, p: &Params) ->
|
||||
if !s.is_break(b) {
|
||||
continue;
|
||||
}
|
||||
*work = work.checked_sub(active.len() as u64 + 1)?;
|
||||
let item = items.get(b);
|
||||
let forced = s.forced(b);
|
||||
let flagged = item.is_some_and(Item::is_flagged);
|
||||
|
||||
@@ -29,6 +29,7 @@ use std::ops::Range;
|
||||
|
||||
pub use greedy::{greedy, greedy_with};
|
||||
pub use knuth_plass::knuth_plass;
|
||||
use knuth_plass::{MAX_WORK, knuth_plass_within};
|
||||
|
||||
/// Number of elasticity tiers: word space, letter space, glyph scale.
|
||||
pub const TIERS: usize = 3;
|
||||
@@ -345,11 +346,17 @@ pub struct Breaks {
|
||||
|
||||
/// [`knuth_plass`] at each of `tolerances` in turn, the last pass also accepting overfull lines;
|
||||
/// [`greedy`] if every pass fails or `tolerances` is empty. The last pass fails only past the
|
||||
/// active-break limit (see [`Params::uniform_from`]).
|
||||
/// active-break limit (see [`Params::uniform_from`]) or the passes' shared work limit, which
|
||||
/// only a paragraph of many hundreds of thousands of items reaches. Nothing is left out then: the
|
||||
/// greedy breaker lays out every item, in linear time.
|
||||
pub fn break_paragraph(items: &[Item], width: &dyn Fn(usize) -> f64, params: &Params, tolerances: &[f64]) -> Breaks {
|
||||
break_paragraph_within(items, width, params, tolerances, MAX_WORK)
|
||||
}
|
||||
|
||||
fn break_paragraph_within(items: &[Item], width: &dyn Fn(usize) -> f64, params: &Params, tolerances: &[f64], mut work: u64) -> Breaks {
|
||||
for (pass, &tolerance) in tolerances.iter().enumerate() {
|
||||
let overfull = params.overfull || pass + 1 == tolerances.len();
|
||||
if let Some(lines) = knuth_plass(items, width, &Params { tolerance, overfull, ..*params }) {
|
||||
if let Some(lines) = knuth_plass_within(items, width, &Params { tolerance, overfull, ..*params }, &mut work) {
|
||||
return Breaks { lines, method: Method::KnuthPlass { pass }, balanced: false };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,3 +492,22 @@ fn next_start_is_the_first_box_or_forced_break_after_each_item() {
|
||||
assert_eq!(s.next_start(b), scan, "after {b}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Past its work limit the paragraph is broken greedily, with every item laid out and no error or
|
||||
/// cut: a limit of a few units stands for the real one, which a hostile paragraph of hundreds of
|
||||
/// thousands of items would reach (and the test never builds).
|
||||
#[test]
|
||||
fn a_paragraph_past_the_work_limit_falls_back_to_greedy_with_every_item() {
|
||||
let p = vectorcraft(false);
|
||||
let para = Para::new(COPY, &p, Some(50.0));
|
||||
let w = |_| 150.0;
|
||||
let within = break_paragraph_within(¶.items, &w, &p, &PASSES, MAX_WORK);
|
||||
assert!(matches!(within.method, Method::KnuthPlass { .. }), "{:?}", within.method);
|
||||
let limited = break_paragraph_within(¶.items, &w, &p, &PASSES, 20);
|
||||
assert_eq!(limited.method, Method::Greedy);
|
||||
assert_eq!(limited.lines, greedy(¶.items, &w, &p));
|
||||
assert_covers(¶.items, &limited.lines);
|
||||
// The work is shared by the passes: what one spends the next doesn't have.
|
||||
let mut work = 30;
|
||||
assert!(knuth_plass_within(¶.items, &w, &Params { tolerance: 1.0, ..p }, &mut work).is_none());
|
||||
}
|
||||
Reference in new issue
Block a user