mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 22:35:26 +00:00
Compose: vertical justification moves an object kept within its column with its line (#2720)
* Text wrap: an object kept within its column moves with its line Vertical justification treated an object positioned from its anchor and kept within its column as fixed, so its wrap stopped Center, Bottom and Justify where the object started, although the object moves down with its anchor's line until it reaches the column's bottom. A line now moves down at most the room above the object plus the distance the object can move before the column stops it. The story's object-mark positions are now collected once per composition instead of once per frame that holds an anchored object. * Compose: Justify keeps the room under an object held at the column's top An object kept within its column and held at the column's top moves only after its anchor's row has moved down far enough to bring it off the top. Justify took that distance off a later line's room even when the anchor's row moved less, so with the anchor in the first row, which stays in place, the later line stopped that distance short of the object's wrap. A line now moves its room plus how far the anchor's row moves past that distance.
This commit is contained in:
1 parent
67a5cb273e
commit
0f792e1fe9
2 files changed
+158
-30
No files matched your search
+72
-30
@@ -3626,6 +3626,8 @@ struct FinishInput<'a> {
|
||||
fn finish_composition(input: FinishInput<'_>) -> ComposedStory {
|
||||
let FinishInput { doc, story, frames, cols, notes, out, boxes, span_paras, split_paras, grid_paras, styles_tab } = input;
|
||||
notes.finish(doc, story, frames, cols, out);
|
||||
// The story byte of each anchored object's mark, found once for every frame that holds one.
|
||||
let mut marks: Option<Vec<usize>> = None;
|
||||
// Ranges, content heights and vertical justification.
|
||||
for (fi, ft) in out.frames.iter_mut().enumerate() {
|
||||
let f = &frames[fi];
|
||||
@@ -3641,9 +3643,12 @@ fn finish_composition(input: FinishInput<'_>) -> ComposedStory {
|
||||
let before: Vec<f64> = ft.tables.iter().map(|t| ft.lines.get(t.line).map_or(0.0, |l| l.baseline)).collect();
|
||||
let spanned = ft.columns.len() > 1 && ft.lines.iter().any(|l| span_paras.get(l.para) == Some(&true))
|
||||
|| ft.lines.iter().any(|l| split_paras.get(l.para) == Some(&true));
|
||||
let anchors: Vec<usize> =
|
||||
if ft.objects.is_empty() { Vec::new() } else { story.text.match_indices(designcraft_doc::OBJECT_MARK).map(|(i, _)| i).collect() };
|
||||
vertical_justify(ft, f, spanned, &doc.footnote_options, grid_paras, &anchors);
|
||||
let anchors: &[usize] = if ft.objects.is_empty() {
|
||||
&[]
|
||||
} else {
|
||||
marks.get_or_insert_with(|| story.text.match_indices(designcraft_doc::OBJECT_MARK).map(|(i, _)| i).collect())
|
||||
};
|
||||
vertical_justify(ft, f, spanned, &doc.footnote_options, grid_paras, anchors);
|
||||
for (t, b) in ft.tables.iter_mut().zip(before) {
|
||||
let now = ft.lines.get(t.line).map_or(b, |l| l.baseline);
|
||||
t.shift(now - b);
|
||||
@@ -6077,18 +6082,19 @@ fn vertical_justify(
|
||||
None => true,
|
||||
};
|
||||
// Each row moves no further than a text wrap under it allows. An object that moves with its
|
||||
// anchor's row (Justify) limits how much further than that row a later row moves.
|
||||
// anchor's row (Justify) lets a later row move its room plus how far the object moves.
|
||||
let mut row_of = vec![None; ft.lines.len()];
|
||||
for (&i, &k) in idx.iter().zip(&rows) {
|
||||
row_of[i] = Some(k);
|
||||
}
|
||||
let follow: Vec<(usize, usize, f64)> = follow
|
||||
let follow: Vec<(usize, usize, f64, f64)> = follow
|
||||
.iter()
|
||||
.filter_map(|&(i, a, room)| Some((row_of.get(i).copied().flatten()?, row_of.get(a).copied().flatten()?, room.max(0.0))))
|
||||
.filter_map(|&(i, a, room, pin)| Some((row_of.get(i).copied().flatten()?, row_of.get(a).copied().flatten()?, room.max(0.0), pin)))
|
||||
.collect();
|
||||
// The object moves only once its anchor's row has moved `pin`.
|
||||
let clear = |shift: &[f64]| {
|
||||
idx.iter().zip(&rows).all(|(&i, &k)| shift[k] <= rooms[i].max(0.0) + 1e-9)
|
||||
&& follow.iter().all(|&(k, m, room)| shift[k] - shift[m] <= room + 1e-9)
|
||||
&& follow.iter().all(|&(k, m, room, pin)| shift[k] <= room + (shift[m] - pin).max(0.0) + 1e-9)
|
||||
};
|
||||
// The lowest lines are tested first: they are the ones a narrowing outline stops.
|
||||
let fits = |shift: &[f64]| {
|
||||
@@ -6206,13 +6212,16 @@ fn vertical_justify(
|
||||
/// box. Beside or under a contour, the line moves down only as far as its band stays clear of the
|
||||
/// contour on its way down (to 0.01 pt). Only wraps that reach into a line's column (or the
|
||||
/// columns it spans) count, as when the lines are set. An anchored object that moves with the
|
||||
/// lines being justified (inline, above the line, or positioned from its anchor and not kept
|
||||
/// within its column) is skipped, except under Justify, where it moves with the row of its
|
||||
/// anchor's line: its wrap is taken where the object sits before the lines move, and its rooms go
|
||||
/// in the second list as (line, anchor's line, room), so a line moves down at most that much
|
||||
/// further than the anchor's row. Justify keeps the first row in place, so its room stays
|
||||
/// infinite. `anchors` holds the story byte of each anchored object's mark, by object index.
|
||||
fn wrap_rooms(ft: &FrameText, f: &FrameSpec, c: u32, spanned: bool, anchors: &[usize]) -> (Vec<f64>, Vec<(usize, usize, f64)>) {
|
||||
/// lines being justified (inline, above the line, or positioned from its anchor) is skipped,
|
||||
/// except under Justify, where it moves with the row of its anchor's line: its wrap is taken where
|
||||
/// the object sits before the lines move, and its rooms go in the second list as (line, anchor's
|
||||
/// line, room, pin), so a line moves down at most the room plus how far the anchor's row moves
|
||||
/// past `pin`. An object positioned from its anchor and kept within its column moves with its
|
||||
/// anchor's row until it stops at the column's bottom, so in every mode a line moves down at most
|
||||
/// the room under it plus the distance the object can move. Held at the column's top, the object
|
||||
/// moves only after its anchor's row has moved `pin` (0 for the other objects), and until then a
|
||||
/// line keeps the whole room. Justify keeps the first row in place, so its room stays infinite. `anchors` holds the story byte of each anchored object's mark, by object index.
|
||||
fn wrap_rooms(ft: &FrameText, f: &FrameSpec, c: u32, spanned: bool, anchors: &[usize]) -> (Vec<f64>, Vec<(usize, usize, f64, f64)>) {
|
||||
const EPS: f64 = 1e-6;
|
||||
// One-sided wraps also block the column on the other side of the object: the x-range wrap
|
||||
// `e` keeps text out of in column `col`, from the object's x-range `a..b` there (None when that
|
||||
@@ -6247,23 +6256,43 @@ fn wrap_rooms(ft: &FrameText, f: &FrameSpec, c: u32, spanned: bool, anchors: &[u
|
||||
}
|
||||
}
|
||||
// An own-story anchored object whose anchor is on one of these lines and whose position
|
||||
// follows its line moves with the text: the index in `ft.lines` of its anchor's line, and how
|
||||
// much lower that line was when the wrap was taken (an earlier pass placed the object after
|
||||
// moving the lines).
|
||||
let moves_with_text = |e: &Exclusion| -> Option<(usize, f64)> {
|
||||
// follows its line moves with the text: the index in `ft.lines` of its anchor's line, how
|
||||
// much lower the object was when the wrap was taken (an earlier pass placed the object after
|
||||
// moving the lines), and, for one kept within its column, how far its line moves before the
|
||||
// object leaves the column's top and how far the object then moves before it stops at the
|
||||
// column's bottom (none and infinite for the others).
|
||||
let moves_with_text = |e: &Exclusion| -> Option<(usize, f64, f64, f64)> {
|
||||
let o = *by_anchor.get(&e.after?)?;
|
||||
let follows = match &o.object.position {
|
||||
designcraft_doc::AnchorPosition::Inline { .. } | designcraft_doc::AnchorPosition::AboveLine { .. } => true,
|
||||
// Kept within the column, it can stop at the column's edge instead of following.
|
||||
designcraft_doc::AnchorPosition::Custom { y_relative, keep_within_column, .. } => {
|
||||
*y_relative == designcraft_doc::anchored::AnchorRelative::Anchor && !keep_within_column
|
||||
let l = ft.lines.get(o.line)?;
|
||||
let now = l.baseline;
|
||||
let dy = e.anchor_baseline.map_or(0.0, |b| b - now);
|
||||
match &o.object.position {
|
||||
designcraft_doc::AnchorPosition::Inline { .. } | designcraft_doc::AnchorPosition::AboveLine { .. } => {
|
||||
Some((o.line, dy, 0.0, f64::INFINITY))
|
||||
}
|
||||
};
|
||||
let now = ft.lines.get(o.line).map_or(0.0, |l| l.baseline);
|
||||
follows.then(|| (o.line, e.anchor_baseline.map_or(0.0, |b| b - now)))
|
||||
designcraft_doc::AnchorPosition::Custom { y_relative, keep_within_column, y_offset, object_point, .. }
|
||||
if *y_relative == designcraft_doc::anchored::AnchorRelative::Anchor =>
|
||||
{
|
||||
if !keep_within_column {
|
||||
return Some((o.line, dy, 0.0, f64::INFINITY));
|
||||
}
|
||||
// Placed as the lines' final positions place it: from its anchor's baseline,
|
||||
// held between the column's top and bottom.
|
||||
let col = ft.columns.get(l.column as usize).copied().unwrap_or(f.area);
|
||||
let h = o.size.1;
|
||||
let ofy = [0.0, 0.5, 1.0][(*object_point / 3).min(2) as usize];
|
||||
let (lo, hi) = (col.y0, (col.y1 - h).max(col.y0));
|
||||
let place = |b: f64| b + y_offset - h * ofy;
|
||||
let raw = place(now);
|
||||
let y = raw.clamp(lo, hi);
|
||||
let then = e.anchor_baseline.map_or(y, |b| place(b).clamp(lo, hi));
|
||||
Some((o.line, then - y, (lo - raw).max(0.0), (hi - y).max(0.0)))
|
||||
}
|
||||
designcraft_doc::AnchorPosition::Custom { .. } => None,
|
||||
}
|
||||
};
|
||||
// Each wrap's object is looked up once, not for every line.
|
||||
let moving: Vec<Option<(usize, f64)>> = f.exclusions.iter().map(moves_with_text).collect();
|
||||
let moving: Vec<Option<(usize, f64, f64, f64)>> = f.exclusions.iter().map(moves_with_text).collect();
|
||||
// Justify keeps the first row (its segments and a table on them) in place, so wraps under it
|
||||
// set no limit.
|
||||
let first = ft.lines.iter().find(|l| in_col(l.column)).map(|l| l.baseline);
|
||||
@@ -6290,13 +6319,15 @@ fn wrap_rooms(ft: &FrameText, f: &FrameSpec, c: u32, spanned: bool, anchors: &[u
|
||||
let col = Rect { x0: col.x0.min(x0), x1: col.x1.max(x1), ..col };
|
||||
let meets = |(a, b): (f64, f64)| a < x1 - EPS && b > x0 + EPS;
|
||||
for (e, &anchor) in f.exclusions.iter().zip(&moving).filter(|(e, _)| e.wraps_line(start)) {
|
||||
if anchor.is_some() && !justify {
|
||||
// Center and Bottom move every row as far: only an object kept within its column, which
|
||||
// can stop at the column's edges, holds the lines back.
|
||||
if anchor.is_some_and(|(_, _, _, slack)| slack == f64::INFINITY) && !justify {
|
||||
continue;
|
||||
}
|
||||
// The wrap where its object sits before the lines move (moved up by `dy`), and how
|
||||
// far it lets this line move down (relative to its anchor's row for an object that
|
||||
// moves with the text).
|
||||
let dy = anchor.map_or(0.0, |(_, dy)| dy);
|
||||
let dy = anchor.map_or(0.0, |(_, dy, _, _)| dy);
|
||||
let cap = if anchor.is_some() { f64::INFINITY } else { room };
|
||||
let mut room_e = f64::INFINITY;
|
||||
let r = Rect { y0: e.rect.y0 - dy, y1: e.rect.y1 - dy, ..e.rect };
|
||||
@@ -6335,7 +6366,18 @@ fn wrap_rooms(ft: &FrameText, f: &FrameSpec, c: u32, spanned: bool, anchors: &[u
|
||||
}
|
||||
}
|
||||
match anchor {
|
||||
Some((a, _)) if room_e.is_finite() => follow.push((line, a, room_e)),
|
||||
// An object kept within its column moves once its line has moved `pin` (to leave
|
||||
// the column's top) and stops after moving `slack` (at the column's bottom).
|
||||
Some((a, _, pin, slack)) if room_e.is_finite() => {
|
||||
if room_e >= pin {
|
||||
if justify {
|
||||
follow.push((line, a, room_e, pin));
|
||||
}
|
||||
room = room.min(room_e + slack);
|
||||
} else {
|
||||
room = room.min(room_e);
|
||||
}
|
||||
}
|
||||
Some(_) => {}
|
||||
None => room = room.min(room_e),
|
||||
}
|
||||
|
||||
@@ -890,6 +890,92 @@ fn vertical_justification_stops_at_an_anchored_object_kept_within_its_column() {
|
||||
);
|
||||
}
|
||||
|
||||
/// An object positioned from its anchor and kept within its column moves with its anchor's line
|
||||
/// until it reaches the column's bottom, so Bottom moves the lines above it down that far, not
|
||||
/// only as far as where the object starts.
|
||||
#[test]
|
||||
fn vertical_justification_moves_an_object_kept_within_its_column_until_it_stops() {
|
||||
use designcraft_doc::VerticalJustification::{Bottom, Center, Justify};
|
||||
for vj in [Bottom, Center, Justify] {
|
||||
let (mut d, sid, fid) = doc_with("One\nTwo", Rect::new(0.0, 0.0, 300.0, 400.0), ParaAttrs::default());
|
||||
d.item_mut(fid).unwrap().text_frame_mut().unwrap().options.vertical_justification = vj;
|
||||
// Hung 40 pt under the first baseline, under the second line, and far above the column's
|
||||
// bottom.
|
||||
anchor_box(&mut d, sid, 1, 0.0, 40.0, true, WrapMode::BoundingBox);
|
||||
if let designcraft_doc::AnchorPosition::Custom { keep_within_column, .. } =
|
||||
&mut std::sync::Arc::make_mut(&mut d.story_mut(sid).unwrap().objects[0]).position
|
||||
{
|
||||
*keep_within_column = true;
|
||||
}
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let lines = all_lines(&cs);
|
||||
let o = &cs.frames[0].objects[0];
|
||||
let (top, bottom) = (o.origin.y, o.origin.y + 60.0);
|
||||
let l = lines[1];
|
||||
assert!(l.baseline <= top + 1e-6, "{vj:?}: the second line at {} meets the object at {top}..{bottom}", l.baseline);
|
||||
match vj {
|
||||
// The object stops at the column's bottom and the second line comes down onto it.
|
||||
Bottom => {
|
||||
assert!((bottom - 400.0).abs() < 1e-6, "{vj:?}: the object stopped at {top}..{bottom}, above the column's bottom");
|
||||
assert!(l.baseline > top - 0.01, "{vj:?}: the second line stopped at {}, above the object at {top}", l.baseline);
|
||||
}
|
||||
// Justify keeps the first line, and the object anchored in it, in place; the second
|
||||
// line comes down onto the object.
|
||||
Justify => {
|
||||
assert!((top - lines[0].baseline - 40.0).abs() < 1e-6, "{vj:?}: the object moved to {top}..{bottom}");
|
||||
assert!(l.baseline > top - 0.01, "{vj:?}: the second line stopped at {}, above the object at {top}", l.baseline);
|
||||
}
|
||||
// Center moves the lines half of the room under them: the object moves with them.
|
||||
_ => assert!(bottom < 400.0 - 1e-6 && l.baseline > 150.0, "{vj:?}: second line at {}, object at {top}..{bottom}", l.baseline),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An object anchored in the first line, positioned from its anchor and kept within its column,
|
||||
/// is held at the column's top. Justify keeps the first line in place, so the object stays there,
|
||||
/// and the second line comes down onto the wider lower part of its contour.
|
||||
#[test]
|
||||
fn vertical_justify_keeps_the_room_under_an_object_held_at_the_columns_top() {
|
||||
use designcraft_doc::anchored::AnchorRelative as R;
|
||||
let (mut d, sid, fid) = doc_with("One\nTwo", Rect::new(0.0, 0.0, 300.0, 400.0), ParaAttrs::default());
|
||||
d.item_mut(fid).unwrap().text_frame_mut().unwrap().options.vertical_justification = designcraft_doc::VerticalJustification::Justify;
|
||||
// Beside the lines at x 250..300 down to y 100, then x 100..300 down to y 160.
|
||||
let pts = [
|
||||
Point::new(150.0, 0.0),
|
||||
Point::new(200.0, 0.0),
|
||||
Point::new(200.0, 160.0),
|
||||
Point::new(0.0, 160.0),
|
||||
Point::new(0.0, 100.0),
|
||||
Point::new(150.0, 100.0),
|
||||
];
|
||||
let lid = d.default_layer();
|
||||
let path = designcraft_geom::path::PathData::single(designcraft_geom::path::SubPath::polyline(&pts, true));
|
||||
let mut it = designcraft_doc::Item::new(ItemId(d.alloc()), lid, designcraft_doc::Shape::Polygon, path);
|
||||
it.wrap.mode = WrapMode::Contour;
|
||||
it.wrap.side = WrapSide::BothSides;
|
||||
// 50 pt above the first baseline, so the column's top holds it.
|
||||
let position = designcraft_doc::AnchorPosition::Custom {
|
||||
x_relative: R::TextFrame,
|
||||
y_relative: R::Anchor,
|
||||
x_offset: 100.0,
|
||||
y_offset: -50.0,
|
||||
object_point: 0,
|
||||
ref_point: 0,
|
||||
keep_within_column: true,
|
||||
spine_relative: false,
|
||||
};
|
||||
d.story_mut(sid).unwrap().insert_object(1, designcraft_doc::AnchoredObject::new(it, position));
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let lines = all_lines(&cs);
|
||||
assert_eq!(lines.len(), 2);
|
||||
let top = cs.frames[0].objects[0].origin.y;
|
||||
assert!(lines[0].baseline < 50.0 && top.abs() < 1e-6, "the object is held at the column's top: {top}, {}", lines[0].baseline);
|
||||
let wide = top + 100.0;
|
||||
let b = lines[1].baseline;
|
||||
assert!(b <= wide + 1e-6, "Justify moved the second line to {b}, into the contour's wider part at {wide}");
|
||||
assert!(b > wide - 0.01, "Justify stopped the second line at {b}, short of the contour's wider part at {wide}");
|
||||
}
|
||||
|
||||
/// Justify keeps the first line in place and moves later lines further, so an object anchored in
|
||||
/// the first line and hung under the last one still stops the lines after its anchor.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user