From 5f6ee139da3e22e0c7ecbbdd9d7bddd8a437ff1f Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 12 Oct 2026 05:56:35 +0000 Subject: [PATCH] =?UTF-8?q?Integration:=20anchored=20objects=20wrap=20thro?= =?UTF-8?q?ugh=20next's=20wrap=5Fobstacles=20(#557=20=C3=97=20next's=20wra?= =?UTF-8?q?p=20sides=20and=20contours)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #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. --- crates/compose/src/tests.rs | 36 ++++++++++++++++++++++++++++++++++ crates/compose/src/wrap.rs | 39 +++++++++++++++++++++---------------- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index 26c170cc..7304c72d 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -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] diff --git a/crates/compose/src/wrap.rs b/crates/compose/src/wrap.rs index 31ec719d..57657213 100644 --- a/crates/compose/src/wrap.rs +++ b/crates/compose/src/wrap.rs @@ -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, } @@ -86,7 +86,7 @@ fn story_spreads(doc: &Document, sid: StoryId) -> Vec { /// 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 { +pub(crate) fn obstacles<'a>(doc: &Document, story: &'a Story, cs: &ComposedStory, own: bool) -> Vec> { 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> { 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); + } } } }