use crate::charinfo::CharBox;
use crate::object::{TextRun, ladder_char_width};
const LOOKBACK: usize = 5;
#[must_use]
pub fn repeats_a_predecessor(
candidate: &TextRun,
preceding: &[TextRun],
chars: &[CharBox],
) -> bool {
preceding
.iter()
.rev()
.filter(|run| run.index != candidate.index)
.take(LOOKBACK)
.any(|earlier| same_object(earlier, candidate, chars))
}
#[must_use]
pub fn same_object(earlier: &TextRun, current: &TextRun, chars: &[CharBox]) -> bool {
let mut current_box = current.rect;
let earlier_box = earlier.rect;
let empty = |rect: kurbo::Rect| rect.x1 <= rect.x0 || rect.y1 <= rect.y0;
if empty(current_box) && empty(earlier_box) {
let x_diff = (current_box.x0 - earlier_box.x0).abs();
if let Some(reference) = chars.len().checked_sub(2).and_then(|i| chars.get(i))
&& x_diff > reference.char_box.width()
{
return false;
}
}
if !empty(current_box) || !empty(earlier_box) {
current_box = intersect(current_box, earlier_box);
if empty(current_box) {
return false;
}
if (current_box.width() - earlier_box.width()).abs() > earlier_box.width() / 2.0 {
return false;
}
#[expect(
clippy::float_cmp,
reason = "exact equality is the C++'s test, and a tolerance would \
make two objects at nearly the same size dedup each other"
)]
if current.font_size != earlier.font_size {
return false;
}
}
let count = current.count();
if count != earlier.count() {
return false;
}
if count == 0 {
return true;
}
let mut last_code = None;
for index in 0..count {
let (Some(a), Some(b)) = (current.item(index), earlier.item(index)) else {
return false;
};
if a.code != b.code {
return false;
}
last_code = Some(a.code);
}
let dx = earlier.position.x - current.position.x;
let dy = earlier.position.y - current.position.y;
let font_size = current.font_size;
let char_size = ladder_char_width(current, last_code);
let max_pre_size = current_box
.height()
.max(current_box.width())
.max(f64::from(font_size));
dx.abs() <= 0.9 * char_size.as_f64() * f64::from(font_size) / 1000.0
&& dy.abs() <= max_pre_size / 8.0
}
fn intersect(a: kurbo::Rect, b: kurbo::Rect) -> kurbo::Rect {
kurbo::Rect::new(
a.x0.max(b.x0),
a.y0.max(b.y0),
a.x1.min(b.x1),
a.y1.min(b.y1),
)
}
#[cfg(test)]
mod tests {
#![allow(
clippy::float_cmp,
clippy::indexing_slicing,
clippy::unreadable_literal,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::*;
use kurbo::Rect;
#[test]
fn two_empty_rects_never_reach_the_intersection_tests() {
let a = Rect::new(10.0, 10.0, 10.0, 10.0);
let b = Rect::new(10.0, 10.0, 10.0, 10.0);
assert!(a.x1 <= a.x0 && b.x1 <= b.x0);
}
#[test]
fn intersection_takes_the_inner_bounds() {
let a = Rect::new(0.0, 0.0, 10.0, 10.0);
let b = Rect::new(5.0, 5.0, 20.0, 20.0);
assert_eq!(intersect(a, b), Rect::new(5.0, 5.0, 10.0, 10.0));
let far = Rect::new(50.0, 50.0, 60.0, 60.0);
let result = intersect(a, far);
assert!(result.x1 <= result.x0);
}
}