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:
aiongg
2026-10-12 05:56:35 +00:00
parent 9a95cee172
commit 5f6ee139da
2 changed files with 58 additions and 17 deletions
+36
View File
@@ -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
View File
@@ -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);
}
}
}
}