mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Integration: anchored objects wrap through next's wrap_obstacles (#557 × next's wrap sides and contours)
#557 built an anchored object's exclusion as its offset bounding box; next's Exclusion also has a side and a contour, built by wrap_obstacles for page items. wrap::obstacles keeps the object and its parent → spread transform, and wrap::exclusions runs wrap_obstacles with it, so anchored objects get sides (spine sides by page), contours (Same As Clipping from the clip), custom boundaries, Jump Object and the wrap of a graphic inside, each tagged with the anchor byte (after). Without this the merge doesn't compile. Test: anchored_object_wraps_with_its_contour_and_side.
This commit is contained in:
@@ -6155,6 +6155,42 @@ fn anchored_object_wraps_another_storys_text() {
|
||||
assert!(lines_in_band(&moved, 200.0, 260.0).iter().all(|l| l.x0 >= left + 100.0 - 1e-6));
|
||||
}
|
||||
|
||||
/// An anchored object's wrap is built as a page item's: a contour on both sides of an oval sets
|
||||
/// text left and right of it, closer in where the oval narrows.
|
||||
#[test]
|
||||
fn anchored_object_wraps_with_its_contour_and_side() {
|
||||
use designcraft_doc::anchored::AnchorRelative as R;
|
||||
let long = [LOREM; 4].join(" ");
|
||||
let (mut d, a, fa) = doc_with("Anchor here.", Rect::new(0.0, 0.0, 300.0, 400.0), ParaAttrs::default());
|
||||
let lid = d.default_layer();
|
||||
let (_, b) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(320.0, 0.0, 620.0, 400.0), lid, &long, ParaFormat::default()).unwrap();
|
||||
let left = all_lines(&compose_story(&d, b, &ComposeOptions::default()))[0].x0;
|
||||
let area = d.item(fa).unwrap().text_area();
|
||||
let mut it =
|
||||
designcraft_doc::Item::new(ItemId(d.alloc()), lid, designcraft_doc::Shape::Oval, designcraft_geom::shapes::ellipse(Rect::new(0.0, 0.0, 100.0, 100.0)));
|
||||
it.wrap.mode = WrapMode::Contour;
|
||||
it.wrap.side = WrapSide::BothSides;
|
||||
let position = designcraft_doc::AnchorPosition::Custom {
|
||||
x_relative: R::TextFrame,
|
||||
y_relative: R::TextFrame,
|
||||
x_offset: left + 100.0 - area.x0,
|
||||
y_offset: 100.0 - area.y0,
|
||||
object_point: 0,
|
||||
ref_point: 0,
|
||||
keep_within_column: false,
|
||||
};
|
||||
d.story_mut(a).unwrap().insert_object(0, designcraft_doc::AnchoredObject::new(it, position));
|
||||
let cs = compose_story(&d, b, &ComposeOptions::default());
|
||||
let lines = all_lines(&cs);
|
||||
let pairs: Vec<(&Line, &Line)> = lines.windows(2).filter(|w| w[0].baseline == w[1].baseline).map(|w| (w[0], w[1])).collect();
|
||||
assert!(pairs.len() >= 3, "{} lines share a baseline", pairs.len());
|
||||
let centre = left + 150.0;
|
||||
assert!(pairs.iter().all(|(l, r)| l.x1 < centre && r.x0 > centre), "text on both sides");
|
||||
let top = pairs.first().unwrap().0.x1;
|
||||
let mid = pairs.iter().map(|(l, _)| l.x1).fold(f64::INFINITY, f64::min);
|
||||
assert!(top > mid + 5.0, "top {top}, middle {mid}");
|
||||
}
|
||||
|
||||
const OBJECT_LEN: usize = designcraft_doc::OBJECT_MARK.len_utf8();
|
||||
|
||||
#[test]
|
||||
|
||||
+22
-17
@@ -14,8 +14,8 @@
|
||||
//! is composed again with the objects where the previous pass placed them until they stop
|
||||
//! moving, at most [`MAX_PASSES`] more times.
|
||||
|
||||
use designcraft_doc::{AnchorPosition, AnchoredObject, Document, OBJECT_MARK, SpreadRef, Story, StoryId, WrapMode};
|
||||
use designcraft_geom::{Affine, Rect};
|
||||
use designcraft_doc::{AnchorPosition, AnchoredObject, Document, Item, OBJECT_MARK, PageSide, SpreadRef, Story, StoryId, WrapMode};
|
||||
use designcraft_geom::Affine;
|
||||
|
||||
use crate::{ComposeOptions, ComposedStory, Exclusion, FrameSpec};
|
||||
|
||||
@@ -28,11 +28,11 @@ const SETTLED: f64 = 0.01;
|
||||
|
||||
/// A wrapping anchored object where its story's composition placed it.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Obstacle {
|
||||
pub(crate) struct Obstacle<'a> {
|
||||
spread: SpreadRef,
|
||||
/// Wrap area (the object's bounds plus its wrap offsets) in spread space.
|
||||
rect: Rect,
|
||||
mode: WrapMode,
|
||||
item: &'a Item,
|
||||
/// The object's parent space → spread space.
|
||||
xf: Affine,
|
||||
/// Story byte of the anchor, for obstacles in their own story.
|
||||
anchor: Option<usize>,
|
||||
}
|
||||
@@ -86,7 +86,7 @@ fn story_spreads(doc: &Document, sid: StoryId) -> Vec<SpreadRef> {
|
||||
|
||||
/// The wrapping anchored objects `cs` (a composition of `story`) places. `own`: obstacles for the
|
||||
/// story itself (inline objects left out, anchors recorded).
|
||||
pub(crate) fn obstacles(doc: &Document, story: &Story, cs: &ComposedStory, own: bool) -> Vec<Obstacle> {
|
||||
pub(crate) fn obstacles<'a>(doc: &Document, story: &'a Story, cs: &ComposedStory, own: bool) -> Vec<Obstacle<'a>> {
|
||||
let mut out = Vec::new();
|
||||
if !has_wrapping_objects(story) {
|
||||
return out;
|
||||
@@ -105,11 +105,8 @@ pub(crate) fn obstacles(doc: &Document, story: &Story, cs: &ComposedStory, own:
|
||||
if own && matches!(obj.position, AnchorPosition::Inline { .. }) {
|
||||
continue;
|
||||
}
|
||||
let w = obj.item.wrap.offsets;
|
||||
let b = obj.item.bounds();
|
||||
let r = Rect::new(b.x0 - w[1], b.y0 - w[0], b.x1 + w[3], b.y1 + w[2]);
|
||||
let rect = (fxf * Affine::translate(o.origin.to_vec2())).transform_rect_bbox(r);
|
||||
if !rect.is_finite() {
|
||||
let xf = fxf * Affine::translate(o.origin.to_vec2());
|
||||
if !xf.transform_rect_bbox(obj.item.bounds()).is_finite() {
|
||||
continue;
|
||||
}
|
||||
let anchor = if own {
|
||||
@@ -118,14 +115,15 @@ pub(crate) fn obstacles(doc: &Document, story: &Story, cs: &ComposedStory, own:
|
||||
} else {
|
||||
None
|
||||
};
|
||||
out.push(Obstacle { spread: loc.spread, rect, mode: obj.item.wrap.mode, anchor });
|
||||
out.push(Obstacle { spread: loc.spread, item: &obj.item, xf, anchor });
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The exclusions `obs` add to each of `specs` (frames that ignore wrap, type on a path and
|
||||
/// frames the obstacle doesn't reach get none).
|
||||
/// frames the obstacle doesn't reach get none). They are built as for page items
|
||||
/// ([`crate::wrap_obstacles`]): sides, contours, custom boundaries, the wrap of a graphic inside.
|
||||
pub(crate) fn exclusions(doc: &Document, specs: &[FrameSpec], obs: &[Obstacle]) -> Vec<Vec<Exclusion>> {
|
||||
let mut out = vec![Vec::new(); specs.len()];
|
||||
if obs.is_empty() {
|
||||
@@ -138,11 +136,18 @@ pub(crate) fn exclusions(doc: &Document, specs: &[FrameSpec], obs: &[Obstacle])
|
||||
let Some(loc) = doc.find(spec.id) else { continue };
|
||||
let Some(item) = doc.item_at(&loc) else { continue };
|
||||
let inv = (doc.parent_xf(&loc) * doc.text_xf(item)).inverse();
|
||||
let sp = doc.spread(loc.spread);
|
||||
let left = |x: f64| sp.and_then(|sp| sp.page_at_x(x).and_then(|pi| sp.pages.get(pi))).is_some_and(|p| p.side == PageSide::Left);
|
||||
let a = spec.area;
|
||||
for ob in obs.iter().filter(|o| o.spread == loc.spread) {
|
||||
let r = inv.transform_rect_bbox(ob.rect);
|
||||
if r.is_finite() && r.x0 < a.x1 && r.x1 > a.x0 && r.y0 < a.y1 && r.y1 > a.y0 {
|
||||
ex.push(Exclusion { rect: r, mode: ob.mode, after: ob.anchor });
|
||||
let mut found = Vec::new();
|
||||
crate::wrap_obstacles(ob.item, ob.xf, inv, spec.id, &left, 0, &mut found);
|
||||
for mut e in found {
|
||||
let r = e.rect;
|
||||
if r.is_finite() && r.x0 < a.x1 && r.x1 > a.x0 && r.y0 < a.y1 && r.y1 > a.y0 {
|
||||
e.after = ob.anchor;
|
||||
ex.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user