mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-30 21:26:48 -06:00
Merge pull request #1949 from garrytan/fix/settle-chained-own-story-wraps
compose: settle a story's own anchored wraps by object count, not 4 passes
This commit is contained in:
@@ -7327,6 +7327,44 @@ fn anchored_object_wraps_its_own_storys_later_lines() {
|
||||
assert!((o.origin.y - top).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// Each object's wrap pushes the anchors after it down, so where each object lands depends on the
|
||||
/// wraps of all those before it; every object still ends up wrapping the lines it hangs over.
|
||||
#[test]
|
||||
fn chained_anchored_objects_all_wrap_their_own_storys_lines() {
|
||||
const OBJECTS: usize = 8;
|
||||
let text = [LOREM; 12].join(" ");
|
||||
let (mut d, sid, _) = doc_with(&text, Rect::new(0.0, 0.0, 300.0, 3000.0), ParaAttrs::default());
|
||||
let plain = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let lines = all_lines(&plain);
|
||||
assert!(lines.len() > 2 + 6 * OBJECTS);
|
||||
let left = lines[0].x0;
|
||||
// One object every six lines, each hanging from its anchor's baseline over the lines below;
|
||||
// inserted last first so the earlier offsets still hold.
|
||||
let ats: Vec<usize> = (0..OBJECTS).map(|k| lines[2 + 6 * k].range.start + 3).collect();
|
||||
for &at in ats.iter().rev() {
|
||||
anchor_box(&mut d, sid, at, 0.0, 0.0, true, WrapMode::BoundingBox);
|
||||
}
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
assert!(!cs.is_overset());
|
||||
let lines = all_lines(&cs);
|
||||
let anchors: Vec<usize> = d.story(sid).unwrap().text.match_indices(designcraft_doc::OBJECT_MARK).map(|(i, _)| i).collect();
|
||||
let objects = &cs.frames[0].objects;
|
||||
assert_eq!(objects.len(), OBJECTS);
|
||||
for o in objects {
|
||||
let anchor_line = lines.iter().find(|l| l.range.contains(&anchors[o.index])).unwrap();
|
||||
assert!((o.origin.y - anchor_line.baseline).abs() < 1e-6, "object {} at {}, anchor line at {}", o.index, o.origin.y, anchor_line.baseline);
|
||||
let under = lines_in_band(&cs, o.origin.y, o.origin.y + 60.0);
|
||||
assert!(under.len() >= 2);
|
||||
assert!(
|
||||
under.iter().all(|l| l.x0 >= left + 100.0 - 1e-6),
|
||||
"object {} at {}: {:?}",
|
||||
o.index,
|
||||
o.origin.y,
|
||||
under.iter().map(|l| (l.baseline, l.x0)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchored_object_without_wrap_leaves_layout_unchanged() {
|
||||
let long = [LOREM; 3].join(" ");
|
||||
|
||||
@@ -12,16 +12,27 @@
|
||||
//! that come after the line holding its anchor; that line and the lines before it are never
|
||||
//! wrapped by it (an inline object sits in its line and wraps none of its own story). The story
|
||||
//! is composed again with the objects where the previous pass placed them until they stop
|
||||
//! moving, at most [`MAX_PASSES`] more times.
|
||||
//! moving. An object's wrap can push the anchors of the objects after it, so a chain of n such
|
||||
//! objects needs n + 1 more compositions: the k-th pass places the (k + 1)-th object where the
|
||||
//! wraps before it leave its anchor, and the last pass wraps around the last object. The story
|
||||
//! is composed at most that many more times (at least [`MIN_PASSES`]), and never more than
|
||||
//! [`MAX_PASSES`].
|
||||
|
||||
use designcraft_doc::{AnchorPosition, AnchoredObject, Document, Item, OBJECT_MARK, PageSide, SpreadRef, Story, StoryId, WrapMode};
|
||||
use designcraft_geom::Affine;
|
||||
|
||||
use crate::{ComposeOptions, ComposedStory, Exclusion, FrameSpec};
|
||||
|
||||
/// Extra compositions of a story with its own anchored objects' wrap; a layout that keeps moving
|
||||
/// its objects (keep options, vertical justification) stops here.
|
||||
pub(crate) const MAX_PASSES: usize = 4;
|
||||
/// Most extra compositions of a story with its own anchored objects' wrap, however many objects
|
||||
/// it has; a layout that keeps moving its objects (keep options, vertical justification) stops
|
||||
/// sooner, after one per object and one more (but never fewer than [`MIN_PASSES`]).
|
||||
pub(crate) const MAX_PASSES: usize = 64;
|
||||
|
||||
/// Fewest extra compositions allowed before giving up on a layout that keeps moving: a single
|
||||
/// object that keep options or vertical justification move still gets the room it had before.
|
||||
const MIN_PASSES: usize = 4;
|
||||
// `clamp` needs min <= max.
|
||||
const _: () = assert!(MIN_PASSES <= MAX_PASSES);
|
||||
|
||||
/// How far an obstacle may move between passes and still count as settled (points).
|
||||
const SETTLED: f64 = 0.01;
|
||||
@@ -199,8 +210,9 @@ pub(crate) fn compose_among(doc: &Document, sid: StoryId, opts: &ComposeOptions,
|
||||
if !has_wrapping_objects(st) {
|
||||
return out;
|
||||
}
|
||||
let own = st.objects.iter().filter(|o| wraps(o) && !matches!(o.position, AnchorPosition::Inline { .. })).count();
|
||||
let mut prev = vec![Vec::new(); specs.len()];
|
||||
for _ in 0..MAX_PASSES {
|
||||
for _ in 0..(own + 1).clamp(MIN_PASSES, MAX_PASSES) {
|
||||
let added = exclusions(doc, &specs, &obstacles(doc, st, &out, true));
|
||||
if settled(&added, &prev) {
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user