#[derive(Debug, Clone, Copy)]
pub enum Item {
Box(f32),
Glue {
width: f32,
stretch: f32,
fil: f32,
shrink: f32,
},
Penalty {
width: f32,
penalty: f32,
flagged: bool,
hyphen: bool,
},
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Skip {
pub width: f32,
pub stretch: f32,
pub fil: f32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Line {
pub start: usize,
pub end: usize,
pub ratio: f32,
pub hyphen: bool,
}
const INF_BAD: f64 = 10000.0;
const LINE_PENALTY: f64 = 10.0;
const ADJ_DEMERITS: f64 = 10000.0;
const DOUBLE_HYPHEN_DEMERITS: f64 = 10000.0;
const FINAL_HYPHEN_DEMERITS: f64 = 5000.0;
const PRETOLERANCE: f64 = 100.0;
const TOLERANCE: f64 = 9999.0;
fn badness(t: f64, s: f64) -> f64 {
if t <= 0.0 {
return 0.0;
}
if s <= 0.0 {
return INF_BAD;
}
let r = (t * 297.0 / s).floor();
if r > 1290.0 {
INF_BAD
} else {
((r * r * r + 131072.0) / 262144.0).floor()
}
}
#[derive(Clone, Copy)]
struct Node {
at: usize,
fitness: u8,
total: f64,
prev: Option<usize>,
flagged: bool,
ratio: f32,
}
pub fn break_lines(items: &[Item], hsize: f32, left: Skip, right: Skip, emergency: f32) -> Vec<Line> {
try_pass(items, hsize, left, right, PRETOLERANCE, Pass::First)
.or_else(|| try_pass(items, hsize, left, right, TOLERANCE, if emergency > 0.0 { Pass::Second } else { Pass::Final(0.0) }))
.or_else(|| if emergency > 0.0 { try_pass(items, hsize, left, right, TOLERANCE, Pass::Final(emergency)) } else { None })
.unwrap_or_default()
}
#[derive(Clone, Copy, PartialEq)]
enum Pass {
First,
Second,
Final(f32),
}
fn try_pass(items: &[Item], hsize: f32, left: Skip, right: Skip, threshold: f64, pass: Pass) -> Option<Vec<Line>> {
let second = pass != Pass::First;
let (last_pass, background) = match pass {
Pass::Final(e) => (true, e as f64),
_ => (false, 0.0),
};
let n = items.len();
let (mut w, mut st, mut fil, mut sh) = (vec![0.0f64; n + 1], vec![0.0f64; n + 1], vec![0.0f64; n + 1], vec![0.0f64; n + 1]);
for (k, item) in items.iter().enumerate() {
let (dw, ds, df, dk) = match *item {
Item::Box(x) => (x, 0.0, 0.0, 0.0),
Item::Glue { width, stretch, fil, shrink } => (width, stretch, fil, shrink),
Item::Penalty { .. } => (0.0, 0.0, 0.0, 0.0),
};
w[k + 1] = w[k] + dw as f64;
st[k + 1] = st[k] + ds as f64;
fil[k + 1] = fil[k] + df as f64;
sh[k + 1] = sh[k] + dk as f64;
}
let start_after = |at: usize| -> usize {
let mut s = if at == usize::MAX { 0 } else { at + 1 };
while s < n && !matches!(items[s], Item::Box(_)) {
s += 1;
}
s
};
let mut nodes = vec![Node { at: usize::MAX, fitness: 2, total: 0.0, prev: None, flagged: false, ratio: 0.0 }];
let mut active = vec![0usize];
for b in 0..n {
let (pen, pen_width, flagged) = match items[b] {
Item::Glue { .. } if b > 0 && matches!(items[b - 1], Item::Box(_)) => (0.0, 0.0, false),
Item::Penalty { penalty, width, flagged, hyphen } if penalty < 10000.0 && (second || !hyphen) => (penalty as f64, width as f64, flagged),
_ => continue,
};
let forced = pen <= -10000.0;
let last = b == n - 1;
let mut best: [Option<(f64, usize, f32)>; 4] = [None; 4];
let mut kept = Vec::with_capacity(active.len());
let mut dropped: Option<(f64, usize, f32)> = None;
for &a in &active {
let node = nodes[a];
let s = start_after(node.at);
let natural = if s <= b { w[b] - w[s] } else { 0.0 } + pen_width + (left.width + right.width) as f64;
let shortfall = hsize as f64 - natural;
let (stretch, fils, shrink) = if s <= b {
(st[b] - st[s] + (left.stretch + right.stretch) as f64, fil[b] - fil[s] + (left.fil + right.fil) as f64, sh[b] - sh[s])
} else {
((left.stretch + right.stretch) as f64, (left.fil + right.fil) as f64, 0.0)
};
let (bad, fitness, ratio, overfull) = if shortfall >= 0.0 {
if fils > 0.0 {
(0.0, 2u8, 0.0, false)
} else {
let bad = badness(shortfall, stretch + background);
let f = if bad > 99.0 {
0
} else if bad > 12.0 {
1
} else {
2
};
(bad, f, if stretch > 0.0 { (shortfall / stretch) as f32 } else { 0.0 }, false)
}
} else if -shortfall > shrink {
(INF_BAD + 1.0, 3, -1.0, true)
} else {
let bad = badness(-shortfall, shrink);
(bad, if bad > 12.0 { 3 } else { 2 }, (shortfall / shrink) as f32, false)
};
if !(overfull || forced) {
kept.push(a);
}
if bad > threshold {
if last_pass && (overfull || forced) && dropped.is_none_or(|d| node.total < d.0) {
dropped = Some((node.total, a, ratio));
}
continue;
}
let mut d = LINE_PENALTY + bad;
d = if d >= 10000.0 { 1e8 } else { d * d };
if pen > 0.0 {
d += pen * pen;
} else if pen > -10000.0 {
d -= pen * pen;
}
if node.flagged {
if flagged {
d += DOUBLE_HYPHEN_DEMERITS;
} else if last {
d += FINAL_HYPHEN_DEMERITS;
}
}
if (fitness as i32 - node.fitness as i32).abs() > 1 {
d += ADJ_DEMERITS;
}
let total = node.total + d;
let slot = &mut best[fitness as usize];
if slot.is_none_or(|s| total <= s.0) {
*slot = Some((total, a, ratio));
}
}
active = kept;
if best.iter().all(Option::is_none) && active.is_empty() {
if let Some((total, a, ratio)) = dropped {
best[3] = Some((total, a, ratio));
}
}
for (fitness, slot) in best.iter().enumerate() {
if let Some((total, a, ratio)) = *slot {
nodes.push(Node { at: b, fitness: fitness as u8, total, prev: Some(a), flagged, ratio });
active.push(nodes.len() - 1);
}
}
if active.is_empty() {
return None;
}
}
let end = active.iter().copied().filter(|&a| nodes[a].at == n - 1).min_by(|&a, &b| nodes[a].total.total_cmp(&nodes[b].total))?;
let mut lines = Vec::new();
let mut k = end;
while let Some(p) = nodes[k].prev {
let at = nodes[k].at;
let hyphen = matches!(items[at], Item::Penalty { width, .. } if width > 0.0);
lines.push(Line { start: start_after(nodes[p].at), end: at, ratio: nodes[k].ratio, hyphen });
k = p;
}
lines.reverse();
Some(lines)
}
#[cfg(test)]
mod tests {
use super::*;
fn words(ws: &[f32]) -> Vec<Item> {
let mut items = Vec::new();
for (k, &x) in ws.iter().enumerate() {
if k > 0 {
items.push(Item::Glue { width: 3.0, stretch: 0.0, fil: 0.0, shrink: 0.0 });
}
items.push(Item::Box(x));
}
items.push(Item::Penalty { width: 0.0, penalty: 10000.0, flagged: false, hyphen: false });
items.push(Item::Glue { width: 0.0, stretch: 0.0, fil: 1.0, shrink: 0.0 });
items.push(Item::Penalty { width: 0.0, penalty: -10000.0, flagged: false, hyphen: false });
items
}
#[test]
fn ragged_lines() {
let items = words(&[10.0, 10.0, 10.0, 10.0]);
let right = Skip { width: 0.0, stretch: 20.0, fil: 0.0 };
let lines = break_lines(&items, 25.0, Skip::default(), right, 0.0);
assert_eq!(lines.len(), 2, "{lines:?}");
assert_eq!((lines[0].start, lines[0].end), (0, 3));
}
}