diff --git a/crates/eps/src/import/ate.rs b/crates/eps/src/import/ate.rs index ff5b2906e..7c90b15aa 100644 --- a/crates/eps/src/import/ate.rs +++ b/crates/eps/src/import/ate.rs @@ -618,7 +618,7 @@ impl Story { let r = match self.frames.first()? { FrameKind::Area(s) => bounds(s)?, FrameKind::OnPath { segments, .. } => bounds(segments)?.inflate(size * 1.5, size * 1.5), - FrameKind::Point(_) => { + FrameKind::Point(m) => { let longest = self.text.split(['\r', '\x03']).map(|l| l.chars().count()).max().unwrap_or(0) as f64; let reach = longest * size + size; let points: Vec = self.line_starts(template).into_iter().map(|(_, (x, y))| to_doc * Point::new(x, y)).collect(); @@ -626,7 +626,19 @@ impl Story { let (x, y) = self.place(template); to_doc * Point::new(x, y) }); - points.iter().fold(Rect::from_points(first, first), |r, p| r.union_pt(*p)).inflate(reach, size * 1.5) + // Along the baseline as far as a line reaches, across it a line and a half: turned + // with the type (a dimension's label turned a quarter runs down the page). + // (The canvas is y down, the art y up: see [`to_art`].) + let along = to_doc * Point::new(m[0], -m[1]) - to_doc * Point::ZERO; + let along = if along.hypot() > 1e-9 && along.is_finite() { along / along.hypot() } else { kurbo::Vec2::new(1.0, 0.0) }; + let across = kurbo::Vec2::new(-along.y, along.x); + let mut r = Rect::from_points(first, first); + for p in points.iter().chain([&first]) { + for (t, n) in [(-reach, -1.5), (-reach, 1.5), (reach, -1.5), (reach, 1.5)] { + r = r.union_pt(*p + along * t + across * (n * size)); + } + } + r } }; r.is_finite().then_some(r) @@ -841,6 +853,23 @@ mod tests { assert_eq!(justify(b"/0 1 /33 0"), Justify::Right); } + /// Where point type lies runs along its baseline: a label turned a quarter (as a vertical + /// dimension's is) lies in a tall box, not a wide one. + #[test] + fn point_type_lies_along_its_baseline() { + let para = + Texts { stories: vec![], fonts: vec![], defaults: Val::Dict(BTreeMap::new()), para_defaults: Val::Dict(BTreeMap::new()), frames: vec![] } + .para(None); + let mut s = story(" 80 mm ", &[0.0], ¶); + let size = |s: &Story| s.region((0.0, 0.0), Affine::IDENTITY).map(|r| (r.width(), r.height())).unwrap(); + let (w, h) = size(&s); + assert!(w > 3.0 * h, "level: {w} × {h}"); + // Turned −90° on the canvas (y down): the baseline runs down the page. + s.frames = vec![FrameKind::Point([0.0, 1.0, -1.0, 0.0, 0.0, 0.0])]; + let (w, h) = size(&s); + assert!(h > 3.0 * w, "turned: {w} × {h}"); + } + /// A right-to-left paragraph is laid out right to left even when it has no right-to-left /// letters (a dimension's label " 120.5 mm " reads "mm 120.5"); a left-to-right one, or one /// that doesn't say, goes by its letters. diff --git a/crates/eps/src/import/native/plan.rs b/crates/eps/src/import/native/plan.rs index a89ad0dd9..cc2772f8e 100644 --- a/crates/eps/src/import/native/plan.rs +++ b/crates/eps/src/import/native/plan.rs @@ -1,5 +1,5 @@ use super::layers::closed_where_ends_meet; -use super::slots::{Content, SlotInfo, assign, covered, extent, join_pieces, letters, same_type}; +use super::slots::{Content, SlotInfo, assign, covered, extent, join_pieces, letters, same_type, words}; use super::{Affine, Arc, Node, NodeId, NodeKind, Point, Rect, Story, TextKind}; /// Match page objects to the nearest slot whose story describes them, including hidden slots. @@ -103,28 +103,28 @@ pub(super) fn plan( // with its words? (A copied object's lines are where its original's are, so they don't say.) let hidden_type = |object: &[Arc]| { let Some(r) = extent(object) else { return false }; - let words: Vec = - object.iter().filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some(letters(&t.plain_text())) } else { None }).collect(); + let texts: Vec> = + object.iter().filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some(words(&t.plain_text())) } else { None }).collect(); hidden_at.iter().any(|(k, place)| { place.intersect(r).area() >= 0.5 * r.area().max(1e-9) - && lines - .get(*k) - .and_then(Option::as_ref) - .is_some_and(|l| !words.is_empty() && words.iter().all(|w| l.iter().any(|(text, _)| letters(text).contains(w.as_str())))) + && lines.get(*k).and_then(Option::as_ref).is_some_and(|l| { + !texts.is_empty() + && texts.iter().all(|ws| !ws.is_empty() && ws.iter().all(|w| l.iter().any(|(text, _)| letters(text).contains(w.as_str())))) + }) }) }; // The letters of each slot's story, in lower case. let spelled: Vec = stories.iter().map(|s| s.as_ref().map(|s| letters(s.text()).to_ascii_lowercase()).unwrap_or_default()).collect(); - // For each slot: is its made type where `object` is, with the letters of one of `object`'s texts in - // its story? All are false unless such slots have the letters of every text between them. + // For each slot: is its made type where `object` is, with the words of one of `object`'s texts in + // its story? All are false unless such slots have the words of every text between them. let spelling = |object: &[Arc], content: &[Content]| -> Vec { let r = extent(object); - let words: Vec = object + let texts: Vec> = object .iter() - .filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some(letters(&t.plain_text()).to_ascii_lowercase()) } else { None }) - .filter(|w| !w.is_empty()) + .filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some(words(&t.plain_text().to_ascii_lowercase())) } else { None }) + .filter(|ws| !ws.is_empty()) .collect(); - let has = |k: usize| spelled.get(k).is_some_and(|s| words.is_empty() || words.iter().any(|w| s.contains(w.as_str()))); + let has = |k: usize| spelled.get(k).is_some_and(|s| texts.is_empty() || texts.iter().any(|ws| ws.iter().all(|w| s.contains(w.as_str())))); let near: Vec = content .iter() .enumerate() @@ -133,7 +133,7 @@ pub(super) fn plan( _ => false, }) .collect(); - let every = words.iter().all(|w| near.iter().zip(&spelled).any(|(n, s)| *n && s.contains(w.as_str()))); + let every = texts.iter().flatten().all(|w| near.iter().zip(&spelled).any(|(n, s)| *n && s.contains(w.as_str()))); if every { near } else { vec![] } }; let mut spare: Vec>> = vec![]; @@ -150,7 +150,7 @@ pub(super) fn plan( Some(c) => *c = Content::Page(object.clone()), None => {} }, - // What lies where made type is and has that type's letters is that type as the page draws it. + // What lies where made type is and has that type's words is that type as the page draws it. None if covered(object, &content, spells) => { let r = extent(object).unwrap_or(Rect::ZERO); for (k, c) in content.iter_mut().enumerate() { diff --git a/crates/eps/src/import/native/slots.rs b/crates/eps/src/import/native/slots.rs index 524fbcc81..1548a2e9a 100644 --- a/crates/eps/src/import/native/slots.rs +++ b/crates/eps/src/import/native/slots.rs @@ -323,6 +323,13 @@ pub(super) fn letters(s: &str) -> String { s.chars().filter(char::is_ascii_alphanumeric).collect() } +/// The [`letters`] of each word of `s`, the empty ones left out. Type is compared word by word: +/// the page draws a right-to-left paragraph's words in the order they show (" 120.5 mm " as +/// "mm 120.5"), its text object keeps them in the order they are typed. +pub(super) fn words(s: &str) -> Vec { + s.split_whitespace().map(letters).filter(|w| !w.is_empty()).collect() +} + /// Does `n` hold type that is where `story` lays one of its lines out: the line, or a piece of it /// (the page splits a line where its kerning or its styles change), on that line's baseline and /// after its start? How far from where the line starts it is, if so. diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 7d804142c..c0fa58c22 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -779,6 +779,25 @@ fn type_of_the_page_with_the_letters_of_made_type_where_it_lies_is_that_type() { } } +/// A right-to-left paragraph's page type has its words in the order they show, not the order they +/// are typed (a dimension's label " 120.5 mm " is drawn "mm 120.5"): it is still that text +/// object's type as the page draws it, not a second copy of it with the nearest text object. +#[test] +fn type_of_the_page_with_the_words_of_made_type_in_another_order_is_that_type() { + let doc = text_document(&[("Hi there\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]); + let art = native_with_text(&layer("Art", true, &text_object(0)), &doc); + let page = "0 0 0 1 setcmykcolor /Helvetica findfont 14 scalefont setfont 58.5 58.5 moveto (there Hi) show"; + let r = import(&eps(page, &art)).unwrap(); + let mut all = vec![]; + r.document.walk(|n| { + if let NodeKind::Text(t) = &n.kind { + all.push(t.plain_text()); + } + }); + assert_eq!(all, ["Hi there"], "{:?}", r.warnings); + assert!(!r.warnings.iter().any(|w| w.contains("with another's")), "{:?}", r.warnings); +} + /// Page type of no text object that lies where the made type of two text objects is, with the /// letters of one of them in each of its pieces, is the type of both as the page draws it. #[test]