mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Anchored objects: Relative to Spine mirrors custom positions on right-hand pages (#1702)
* Anchored objects: Relative to Spine mirrors custom positions on right-hand pages * Anchored objects: page side from the frame's spread position; footnote cache keyed by page side
This commit is contained in:
+28
-13
@@ -583,6 +583,9 @@ pub struct FrameSpec {
|
||||
pub grid: Option<(f64, f64)>,
|
||||
/// The frame is on a left page (for towards/away-from-spine alignment).
|
||||
pub left_page: bool,
|
||||
/// The frame is on a right page of a facing spread (spine-relative anchored objects mirror
|
||||
/// there; a single page has no spine).
|
||||
pub right_page: bool,
|
||||
/// The page and its margins in the frame's inner space (custom anchored objects).
|
||||
pub page_rect: Option<(Rect, Rect)>,
|
||||
}
|
||||
@@ -662,7 +665,10 @@ pub fn frame_specs(doc: &Document, sid: StoryId) -> Vec<FrameSpec> {
|
||||
let loc = doc.find(fid);
|
||||
let Some(item) = loc.as_ref().and_then(|l| doc.item_at(l)).or_else(|| doc.anchored_item(fid)) else { continue };
|
||||
let Some(tf) = item.text_frame() else { continue };
|
||||
let xf = loc.as_ref().map(|l| doc.parent_xf(l)).unwrap_or(designcraft_geom::Affine::IDENTITY) * item.xf;
|
||||
let parent = loc.as_ref().map(|l| doc.parent_xf(l)).unwrap_or(designcraft_geom::Affine::IDENTITY);
|
||||
let xf = parent * item.xf;
|
||||
// Where the frame is on the spread (a group's transform included), to find its page.
|
||||
let center_x = parent.transform_rect_bbox(item.bounds()).center().x;
|
||||
let inv = xf.inverse();
|
||||
let mut exclusions = Vec::new();
|
||||
let spread_ref = loc.as_ref().map(|l| l.spread);
|
||||
@@ -686,27 +692,27 @@ pub fn frame_specs(doc: &Document, sid: StoryId) -> Vec<FrameSpec> {
|
||||
r.is_finite() && r.x0 < a.x1 && r.x1 > a.x0 && r.y0 < a.y1 && r.y1 > a.y0
|
||||
});
|
||||
}
|
||||
let (page_name, page, left_page) = match (spread_ref, spread) {
|
||||
let (page_name, page, side) = match (spread_ref, spread) {
|
||||
(Some(designcraft_doc::SpreadRef::Doc(si)), Some(sp)) => {
|
||||
let c = item.bounds().center();
|
||||
let pi = sp.page_at_x(c.x).unwrap_or(0);
|
||||
let pi = sp.page_at_x(center_x).unwrap_or(0);
|
||||
let abs = doc.first_page_of_spread(si) + pi;
|
||||
(Some(doc.page_name(abs)), Some(abs), sp.pages.get(pi).is_some_and(|p| p.side == designcraft_doc::PageSide::Left))
|
||||
(Some(doc.page_name(abs)), Some(abs), sp.pages.get(pi).map(|p| p.side))
|
||||
}
|
||||
(Some(designcraft_doc::SpreadRef::Parent(_)), Some(sp)) => {
|
||||
let prefix = sp.parent.as_ref().map(|p| p.prefix.clone()).unwrap_or_else(|| "A".into());
|
||||
// The side of the parent page under the frame, so a folio there aligns to the spine of its own page.
|
||||
let under = sp.page_at_x(item.bounds().center().x).and_then(|pi| sp.pages.get(pi));
|
||||
let left = under.is_some_and(|p| p.side == designcraft_doc::PageSide::Left);
|
||||
(Some(prefix), None, left)
|
||||
let under = sp.page_at_x(center_x).and_then(|pi| sp.pages.get(pi));
|
||||
(Some(prefix), None, under.map(|p| p.side))
|
||||
}
|
||||
_ => (None, None, false),
|
||||
_ => (None, None, None),
|
||||
};
|
||||
let left_page = side == Some(designcraft_doc::PageSide::Left);
|
||||
let right_page = side == Some(designcraft_doc::PageSide::Right);
|
||||
let section_marker = page.map(|abs| section_marker(doc, abs));
|
||||
// The page under the frame and its margins, in inner space (bounding boxes).
|
||||
let page_rect = match (spread_ref, spread) {
|
||||
(_, Some(sp)) => sp
|
||||
.page_at_x(item.bounds().center().x)
|
||||
.page_at_x(center_x)
|
||||
.and_then(|pi| sp.pages.get(pi))
|
||||
.map(|pg| (inv.transform_rect_bbox(pg.bounds()), inv.transform_rect_bbox(pg.margin_rect()))),
|
||||
_ => None,
|
||||
@@ -773,6 +779,7 @@ pub fn frame_specs(doc: &Document, sid: StoryId) -> Vec<FrameSpec> {
|
||||
page,
|
||||
grid,
|
||||
left_page,
|
||||
right_page,
|
||||
page_rect,
|
||||
});
|
||||
}
|
||||
@@ -2004,6 +2011,7 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
|
||||
object_point,
|
||||
ref_point,
|
||||
keep_within_column,
|
||||
spine_relative,
|
||||
} => {
|
||||
use designcraft_doc::anchored::AnchorRelative as R;
|
||||
let col = ft.columns.get(l.column as usize).copied().unwrap_or(f.area);
|
||||
@@ -2015,15 +2023,21 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
|
||||
R::PageEdge => Some(f.page_rect.map_or(f.area, |p| p.0)),
|
||||
};
|
||||
let frac = |i: u8| ([0.0, 0.5, 1.0][(i % 3) as usize], [0.0, 0.5, 1.0][(i / 3).min(2) as usize]);
|
||||
let (rfx, rfy) = frac(*ref_point);
|
||||
let (ofx, ofy) = frac(*object_point);
|
||||
let (mut rfx, rfy) = frac(*ref_point);
|
||||
let (mut ofx, ofy) = frac(*object_point);
|
||||
let mut dx = *x_offset;
|
||||
// The settings describe a left-hand page; a right-hand page mirrors them
|
||||
// across the spine.
|
||||
if *spine_relative && f.right_page {
|
||||
(rfx, ofx, dx) = (1.0 - rfx, 1.0 - ofx, -dx);
|
||||
}
|
||||
let rx = area(*x_relative).map_or(o.x, |a| a.x0 + a.width() * rfx);
|
||||
let ry = area(*y_relative).map_or(l.baseline, |a| a.y0 + a.height() * rfy);
|
||||
let mut y = ry + y_offset - h * ofy;
|
||||
if *keep_within_column {
|
||||
y = y.clamp(col.y0, (col.y1 - h).max(col.y0));
|
||||
}
|
||||
Point::new(rx + x_offset - w * ofx, y)
|
||||
Point::new(rx + dx - w * ofx, y)
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -4649,6 +4663,7 @@ mod column_allocation_safety_tests {
|
||||
page: None,
|
||||
grid: None,
|
||||
left_page: false,
|
||||
right_page: false,
|
||||
page_rect: None,
|
||||
outline: None,
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ pub(crate) struct Notes {
|
||||
pub key: Option<usize>,
|
||||
pub placed: Vec<Placed>,
|
||||
/// Composed footnote text by (index, label, width).
|
||||
cache: HashMap<(usize, String, u64), (Arc<ComposedStory>, NoteMetrics)>,
|
||||
cache: HashMap<(usize, String, u64, bool, bool), (Arc<ComposedStory>, NoteMetrics)>,
|
||||
notes: Vec<Arc<designcraft_doc::Footnote>>,
|
||||
/// Footnotes span the columns of their frame.
|
||||
span: bool,
|
||||
@@ -153,7 +153,8 @@ impl Notes {
|
||||
fn compose(&mut self, doc: &Document, k: usize, w: f64, f: &FrameSpec, opts: &ComposeOptions) -> (Arc<ComposedStory>, NoteMetrics) {
|
||||
let o = &doc.footnote_options;
|
||||
let label = format!("{}{}", o.label(self.numbers[k], true), o.separator);
|
||||
let key = (k, label.clone(), w.to_bits());
|
||||
// The page side sets towards/away-from-spine alignment and spine-relative anchored objects.
|
||||
let key = (k, label.clone(), w.to_bits(), f.left_page, f.right_page);
|
||||
if let Some(c) = self.cache.get(&key) {
|
||||
return c.clone();
|
||||
}
|
||||
@@ -169,6 +170,7 @@ impl Notes {
|
||||
page: f.page,
|
||||
grid: None,
|
||||
left_page: f.left_page,
|
||||
right_page: f.right_page,
|
||||
page_rect: None,
|
||||
};
|
||||
let nopts = ComposeOptions {
|
||||
|
||||
@@ -121,7 +121,7 @@ impl CellLineExtent {
|
||||
}
|
||||
|
||||
/// Compose a cell story at width `w` (content space, top-left at the origin).
|
||||
fn compose_cell(doc: &Document, cell: &Cell, w: f64, opts: &ComposeOptions, left_page: bool) -> CellLayout {
|
||||
fn compose_cell(doc: &Document, cell: &Cell, w: f64, opts: &ComposeOptions, (left_page, right_page): (bool, bool)) -> CellLayout {
|
||||
let spec = FrameSpec {
|
||||
id: ItemId(0),
|
||||
area: Rect::new(0.0, 0.0, w.max(1.0), 1.0e6),
|
||||
@@ -134,6 +134,7 @@ fn compose_cell(doc: &Document, cell: &Cell, w: f64, opts: &ComposeOptions, left
|
||||
page: opts.page,
|
||||
grid: None,
|
||||
left_page,
|
||||
right_page,
|
||||
page_rect: None,
|
||||
};
|
||||
let cs = crate::compose(doc, &cell.text, std::slice::from_ref(&spec), opts);
|
||||
@@ -200,7 +201,7 @@ pub(crate) fn place_table(
|
||||
let (nr, nc) = (table.nrows(), table.ncols());
|
||||
let owners = table.owners();
|
||||
let is_owner = |r: usize, c: usize| owners[r * nc + c] == (r, c);
|
||||
let left_page = frames[cur.fi].left_page;
|
||||
let side = (frames[cur.fi].left_page, frames[cur.fi].right_page);
|
||||
// Compose every owner cell.
|
||||
let mut layouts: Vec<Option<CellLayout>> = Vec::with_capacity(nr * nc);
|
||||
for r in 0..nr {
|
||||
@@ -212,7 +213,7 @@ pub(crate) fn place_table(
|
||||
let cell = &table.cells[r * nc + c];
|
||||
let cs = (cell.col_span.max(1) as usize).min(nc - c);
|
||||
let w = colx[c + cs] - colx[c] - cell.insets[1] - cell.insets[3];
|
||||
layouts.push(Some(compose_cell(doc, cell, w, opts, left_page)));
|
||||
layouts.push(Some(compose_cell(doc, cell, w, opts, side)));
|
||||
}
|
||||
}
|
||||
// Row heights.
|
||||
@@ -813,7 +814,7 @@ mod height_tests {
|
||||
}
|
||||
|
||||
fn measure(cell: &Cell, width: f64) -> CellLayout {
|
||||
compose_cell(&Document::new(&NewDocument::default()), cell, width, &ComposeOptions::default(), false)
|
||||
compose_cell(&Document::new(&NewDocument::default()), cell, width, &ComposeOptions::default(), (false, false))
|
||||
}
|
||||
|
||||
fn compose_table(table: Table, height: f64) -> ComposedStory {
|
||||
@@ -1061,6 +1062,7 @@ mod height_tests {
|
||||
object_point: 0,
|
||||
ref_point: 0,
|
||||
keep_within_column: false,
|
||||
spine_relative: false,
|
||||
}),
|
||||
);
|
||||
assert!((measure(&cell, 92.0).content_h - original).abs() < 1e-6);
|
||||
|
||||
@@ -2752,6 +2752,7 @@ fn custom_anchors_do_not_change_bottom_baseline_fitting() {
|
||||
object_point: 0,
|
||||
ref_point: 0,
|
||||
keep_within_column: false,
|
||||
spine_relative: false,
|
||||
};
|
||||
d.story_mut(sid).unwrap().insert_object(0, designcraft_doc::AnchoredObject::new(it, pos));
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
@@ -6740,6 +6741,7 @@ fn anchor_box(d: &mut Document, sid: StoryId, at: usize, x: f64, y: f64, y_rel:
|
||||
object_point: 0,
|
||||
ref_point: 0,
|
||||
keep_within_column: false,
|
||||
spine_relative: false,
|
||||
};
|
||||
d.story_mut(sid).unwrap().insert_object(at, designcraft_doc::AnchoredObject::new(it, position));
|
||||
}
|
||||
@@ -6808,6 +6810,7 @@ fn anchored_object_wraps_with_its_contour_and_side() {
|
||||
object_point: 0,
|
||||
ref_point: 0,
|
||||
keep_within_column: false,
|
||||
spine_relative: false,
|
||||
};
|
||||
d.story_mut(a).unwrap().insert_object(0, designcraft_doc::AnchoredObject::new(it, position));
|
||||
let cs = compose_story(&d, b, &ComposeOptions::default());
|
||||
@@ -6821,6 +6824,70 @@ fn anchored_object_wraps_with_its_contour_and_side() {
|
||||
assert!(top > mid + 5.0, "top {top}, middle {mid}");
|
||||
}
|
||||
|
||||
/// Relative to Spine: a marginal note set up for a left-hand page (its right edge 12 pt left of
|
||||
/// the column) mirrors on the right-hand page (its left edge 12 pt right of the column), so the
|
||||
/// notes of a story that runs across a spread all sit in the outside margin. Placed as on a
|
||||
/// left-hand page, the right page's note sat in the inside margin and reached across the spine.
|
||||
#[test]
|
||||
fn spine_relative_anchored_notes_stay_in_the_outside_margin_on_both_pages() {
|
||||
use designcraft_doc::anchored::AnchorRelative as R;
|
||||
let compose_spread = |spine_relative: bool| {
|
||||
let mut d = Document::new(&NewDocument { pages: 3, ..Default::default() });
|
||||
let lid = d.default_layer();
|
||||
let sp = &d.spreads[1];
|
||||
let (l, r) = (sp.pages[0].clone(), sp.pages[1].clone());
|
||||
assert_eq!((l.side, r.side), (designcraft_doc::PageSide::Left, designcraft_doc::PageSide::Right));
|
||||
// Body frames leave a 120 pt outside margin for the notes.
|
||||
let left_rect = Rect::new(l.x + 120.0, 36.0, l.x + l.width - 36.0, 120.0);
|
||||
let right_rect = Rect::new(r.x + 36.0, 36.0, r.x + r.width - 120.0, 600.0);
|
||||
let text = [LOREM; 3].join(" ");
|
||||
let (lf, sid) = d.add_text_frame(SpreadRef::Doc(1), left_rect, lid, &text, ParaFormat::default()).unwrap();
|
||||
let (rf, _) = d.add_text_frame(SpreadRef::Doc(1), right_rect, lid, "", ParaFormat::default()).unwrap();
|
||||
d.thread(lf, rf).unwrap();
|
||||
let plain = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let second_line = |fid| plain.frame(fid).unwrap().lines[1].range.start;
|
||||
let (at_left, at_right) = (second_line(lf), second_line(rf));
|
||||
// Top-right corner of the note 12 pt left of the column's top-left, on the anchor's baseline.
|
||||
let position = designcraft_doc::AnchorPosition::Custom {
|
||||
x_relative: R::ColumnEdge,
|
||||
y_relative: R::Anchor,
|
||||
x_offset: -12.0,
|
||||
y_offset: 0.0,
|
||||
object_point: 2,
|
||||
ref_point: 0,
|
||||
keep_within_column: false,
|
||||
spine_relative,
|
||||
};
|
||||
for at in [at_right, at_left] {
|
||||
let note = designcraft_doc::Item::new(
|
||||
ItemId(d.alloc()),
|
||||
lid,
|
||||
designcraft_doc::Shape::Rectangle,
|
||||
designcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 72.0, 40.0)),
|
||||
);
|
||||
d.story_mut(sid).unwrap().insert_object(at, designcraft_doc::AnchoredObject::new(note, position.clone()));
|
||||
}
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
// Per page: the note's left and right edges, the column, and the page's left and right edges.
|
||||
let placed = |fid, page: &designcraft_doc::Page| {
|
||||
let ft = cs.frame(fid).unwrap();
|
||||
assert_eq!(ft.objects.len(), 1);
|
||||
let o = &ft.objects[0];
|
||||
((o.origin.x, o.origin.x + o.size.0), ft.columns[0], (page.x, page.x + page.width))
|
||||
};
|
||||
(placed(lf, &l), placed(rf, &r))
|
||||
};
|
||||
let near = |a: f64, b: f64| (a - b).abs() < 1e-6;
|
||||
|
||||
let (((lx0, lx1), lcol, (lp0, _)), ((rx0, rx1), rcol, (_, rp1))) = compose_spread(true);
|
||||
assert!(near(lx1, lcol.x0 - 12.0) && lx0 >= lp0, "left page: note {lx0}..{lx1}, column {lcol:?}");
|
||||
assert!(near(rx0, rcol.x1 + 12.0) && rx1 <= rp1, "right page: note {rx0}..{rx1}, column {rcol:?}");
|
||||
|
||||
// Without Relative to Spine both pages take the settings as written.
|
||||
let (_, ((rx0, rx1), rcol, _)) = compose_spread(false);
|
||||
assert!(near(rx1, rcol.x0 - 12.0), "right page as written: note {rx0}..{rx1}, column {rcol:?}");
|
||||
}
|
||||
|
||||
const OBJECT_LEN: usize = designcraft_doc::OBJECT_MARK.len_utf8();
|
||||
|
||||
#[test]
|
||||
@@ -7324,6 +7391,25 @@ fn parent_page_frames_know_which_side_of_the_spine_they_are_on() {
|
||||
assert!(!frame_specs(&d, right_story)[0].left_page);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_grouped_frame_takes_the_side_of_the_page_its_group_moves_it_to() {
|
||||
let mut d = Document::new(&NewDocument { pages: 3, ..Default::default() });
|
||||
let lid = d.default_layer();
|
||||
let (l, r) = (d.spreads[1].pages[0].clone(), d.spreads[1].pages[1].clone());
|
||||
let rect = Rect::new(l.x + 36.0, 36.0, l.x + l.width - 36.0, 300.0);
|
||||
let (fid, sid) = d.add_text_frame(SpreadRef::Doc(1), rect, lid, "Text", ParaFormat::default()).unwrap();
|
||||
// The frame's own coordinates are on the left page; its group's transform puts it on the right one.
|
||||
let mut g = designcraft_doc::Item::new(ItemId(d.alloc()), lid, designcraft_doc::Shape::Group, designcraft_geom::PathData::default());
|
||||
g.xf = designcraft_geom::Affine::translate((r.x - l.x, 0.0));
|
||||
let sp = Arc::make_mut(&mut d.spreads[1]);
|
||||
let at = sp.items.iter().position(|it| it.id == fid).unwrap();
|
||||
g.content = designcraft_doc::Content::Group { items: vec![sp.items.remove(at)] };
|
||||
sp.items.push(Arc::new(g));
|
||||
let spec = &frame_specs(&d, sid)[0];
|
||||
assert!(spec.right_page && !spec.left_page);
|
||||
assert_eq!(spec.page, Some(2));
|
||||
}
|
||||
|
||||
/// A right-to-left paragraph's first-line indent belongs at its start side, the right. Applied on
|
||||
/// the left it moved the first line the wrong way — out of the frame for the hanging indent a
|
||||
/// numbered list uses (right indent 10 mm, first line −10 mm), while the wrapped lines sat where
|
||||
|
||||
@@ -60,6 +60,11 @@ pub enum AnchorPosition {
|
||||
/// Keep Within Top/Bottom Column Boundaries.
|
||||
#[serde(default)]
|
||||
keep_within_column: bool,
|
||||
/// Relative to Spine: the settings describe a left-hand page, and on a right-hand page
|
||||
/// the reference point, the object point and the X offset mirror horizontally, so an
|
||||
/// object in the outside margin stays in the outside margin.
|
||||
#[serde(default)]
|
||||
spine_relative: bool,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Insert Anchored Object…",
|
||||
["Object", "Anchored Object"],
|
||||
None,
|
||||
"{ids, story?, pos? (default: the text insertion point), position?: inline|aboveLine|custom, yOffset?, align?: left|center|right, spaceBefore?, spaceAfter?, xRelative? | yRelative?: anchor|textFrame|columnEdge|pageMargin|pageEdge, xOffset?, objectPoint? | refPoint?: 0..8, keepWithinColumn?} — moves the items into the text",
|
||||
"{ids, story?, pos? (default: the text insertion point), position?: inline|aboveLine|custom, yOffset?, align?: left|center|right, spaceBefore?, spaceAfter?, xRelative? | yRelative?: anchor|textFrame|columnEdge|pageMargin|pageEdge, xOffset?, objectPoint? | refPoint?: 0..8, keepWithinColumn?, spineRelative? (custom: the settings describe a left-hand page and mirror on a right-hand page)} — moves the items into the text",
|
||||
has_doc,
|
||||
insert
|
||||
),
|
||||
@@ -27,7 +27,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Anchored Object Options…",
|
||||
["Object", "Anchored Object"],
|
||||
None,
|
||||
"{story?, index? (default: the object at the insertion point), position?: inline|aboveLine|custom, yOffset?, align?: left|center|right, spaceBefore?, spaceAfter?, xRelative? | yRelative?: anchor|textFrame|columnEdge|pageMargin|pageEdge, xOffset?, objectPoint? | refPoint?: 0..8, keepWithinColumn?}",
|
||||
"{story?, index? (default: the object at the insertion point), position?: inline|aboveLine|custom, yOffset?, align?: left|center|right, spaceBefore?, spaceAfter?, xRelative? | yRelative?: anchor|textFrame|columnEdge|pageMargin|pageEdge, xOffset?, objectPoint? | refPoint?: 0..8, keepWithinColumn?, spineRelative? (custom: the settings describe a left-hand page and mirror on a right-hand page)}",
|
||||
has_doc,
|
||||
options
|
||||
),
|
||||
@@ -80,11 +80,18 @@ fn position_param(p: &Value, base: &AnchorPosition) -> Result<AnchorPosition> {
|
||||
}
|
||||
("custom", b) => {
|
||||
use designcraft_doc::anchored::AnchorRelative as R;
|
||||
let (xr0, yr0, xo0, yo0, op0, rp0, k0) = match b {
|
||||
AnchorPosition::Custom { x_relative, y_relative, x_offset, y_offset, object_point, ref_point, keep_within_column } => {
|
||||
(*x_relative, *y_relative, *x_offset, *y_offset, *object_point, *ref_point, *keep_within_column)
|
||||
}
|
||||
_ => (R::TextFrame, R::Anchor, 0.0, 0.0, 0, 0, false),
|
||||
let (xr0, yr0, xo0, yo0, op0, rp0, k0, sr0) = match b {
|
||||
AnchorPosition::Custom {
|
||||
x_relative,
|
||||
y_relative,
|
||||
x_offset,
|
||||
y_offset,
|
||||
object_point,
|
||||
ref_point,
|
||||
keep_within_column,
|
||||
spine_relative,
|
||||
} => (*x_relative, *y_relative, *x_offset, *y_offset, *object_point, *ref_point, *keep_within_column, *spine_relative),
|
||||
_ => (R::TextFrame, R::Anchor, 0.0, 0.0, 0, 0, false, false),
|
||||
};
|
||||
let rel = |k: &str, d: R| -> Result<R> {
|
||||
match p.get(k) {
|
||||
@@ -102,6 +109,7 @@ fn position_param(p: &Value, base: &AnchorPosition) -> Result<AnchorPosition> {
|
||||
object_point: pt("objectPoint", op0),
|
||||
ref_point: pt("refPoint", rp0),
|
||||
keep_within_column: p.get("keepWithinColumn").and_then(Value::as_bool).unwrap_or(k0),
|
||||
spine_relative: p.get("spineRelative").and_then(Value::as_bool).unwrap_or(sr0),
|
||||
}
|
||||
}
|
||||
(o, _) => return Err(bad("anchored", format!("unknown position `{o}`"))),
|
||||
@@ -316,5 +324,12 @@ mod custom_tests {
|
||||
matches!(obj.position, designcraft_doc::AnchorPosition::Custom { x_offset, object_point: 2, .. } if x_offset == -6.0),
|
||||
"IDML Anchored position"
|
||||
);
|
||||
// Relative to Spine keeps the other custom settings.
|
||||
s.execute("anchored.options", &json!({"story": sid.0, "index": 0, "spineRelative": true})).unwrap();
|
||||
let st = s.doc().unwrap().doc.story(sid).unwrap().clone();
|
||||
assert!(matches!(
|
||||
st.objects[0].position,
|
||||
designcraft_doc::AnchorPosition::Custom { spine_relative: true, x_offset, object_point: 2, .. } if x_offset == -6.0
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2414,6 +2414,7 @@ impl<'a> Ex<'a> {
|
||||
object_point,
|
||||
ref_point,
|
||||
keep_within_column,
|
||||
spine_relative,
|
||||
} => El::new("AnchoredObjectSetting")
|
||||
.attr("AnchoredPosition", "Anchored")
|
||||
.attr("AnchorPoint", names::anchor_out(*object_point))
|
||||
@@ -2424,7 +2425,8 @@ impl<'a> Ex<'a> {
|
||||
// IDML measures a left-aligned object's X offset leftwards.
|
||||
.attr("AnchorXoffset", num(if *ref_point % 3 == 0 { 0.0 - *x_offset } else { *x_offset }))
|
||||
.attr("AnchorYoffset", num(*y_offset))
|
||||
.attr("PinPosition", bool_s(*keep_within_column)),
|
||||
.attr("PinPosition", bool_s(*keep_within_column))
|
||||
.attr("SpineRelative", bool_s(*spine_relative)),
|
||||
};
|
||||
e.push(setting);
|
||||
out.push(Node::El(e));
|
||||
|
||||
@@ -2449,6 +2449,7 @@ impl<'r> Importer<'r> {
|
||||
object_point: names::anchor_in(s("AnchorPoint")),
|
||||
ref_point: row * 3 + col,
|
||||
keep_within_column: s("PinPosition") == "true",
|
||||
spine_relative: s("SpineRelative") == "true",
|
||||
}
|
||||
}
|
||||
_ => designcraft_doc::AnchorPosition::Inline { y_offset: get("AnchorYoffset") },
|
||||
|
||||
@@ -1354,6 +1354,7 @@ fn anchored_objects_take_unset_settings_from_object_styles() {
|
||||
object_point,
|
||||
ref_point: 6,
|
||||
keep_within_column,
|
||||
spine_relative: false,
|
||||
};
|
||||
let positions: Vec<_> = s.objects.iter().map(|o| o.position.clone()).collect();
|
||||
assert_eq!(
|
||||
@@ -1366,6 +1367,44 @@ fn anchored_objects_take_unset_settings_from_object_styles() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Relative to Spine comes from the object style chain like the other anchored settings, an
|
||||
/// item's own setting wins, and export writes it back.
|
||||
#[test]
|
||||
fn anchored_spine_relative_comes_from_object_styles_and_round_trips() {
|
||||
use designcraft_doc::AnchorPosition;
|
||||
let styles = STYLES.replace(
|
||||
r#"<ObjectStyle Self="ObjectStyle/$ID/[None]" Name="$ID/[None]"/>"#,
|
||||
r#"<ObjectStyle Self="ObjectStyle/$ID/[None]" Name="$ID/[None]"/>
|
||||
<ObjectStyle Self="ObjectStyle/Side" Name="Side"><AnchoredObjectSetting SpineRelative="true"/></ObjectStyle>
|
||||
<ObjectStyle Self="ObjectStyle/Note" Name="Note"><Properties><BasedOn type="object">ObjectStyle/Side</BasedOn></Properties></ObjectStyle>"#,
|
||||
);
|
||||
let rect = |style: &str, setting: &str| {
|
||||
format!(
|
||||
r#"<Rectangle Self="r{style}" AppliedObjectStyle="ObjectStyle/{style}" ItemTransform="1 0 0 1 0 0">
|
||||
<Properties><PathGeometry><GeometryPathType PathOpen="false"><PathPointArray>
|
||||
<PathPointType Anchor="0 0"/><PathPointType Anchor="0 10"/><PathPointType Anchor="10 10"/><PathPointType Anchor="10 0"/>
|
||||
</PathPointArray></GeometryPathType></PathGeometry></Properties>
|
||||
<AnchoredObjectSetting AnchoredPosition="Anchored" {setting}/>
|
||||
</Rectangle>"#
|
||||
)
|
||||
};
|
||||
let story = format!(
|
||||
r#"<idPkg:Story xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging"><Story Self="s1">
|
||||
<ParagraphStyleRange><CharacterStyleRange><Content>a</Content>{}{}{}</CharacterStyleRange></ParagraphStyleRange>
|
||||
</Story></idPkg:Story>"#,
|
||||
rect("Note", ""),
|
||||
rect("Note", r#"SpineRelative="false""#),
|
||||
rect("$ID/[None]", ""),
|
||||
);
|
||||
let spine = |d: &Document| -> Vec<bool> {
|
||||
let s = d.stories.values().find(|s| !s.objects.is_empty()).unwrap();
|
||||
s.objects.iter().map(|o| matches!(o.position, AnchorPosition::Custom { spine_relative: true, .. })).collect()
|
||||
};
|
||||
let d = import_idml(&fixture_with_table_styles(&styles, &story)).unwrap();
|
||||
assert_eq!(spine(&d), [true, false, false]);
|
||||
assert_eq!(spine(&import_idml(&export_idml(&d)).unwrap()), [true, false, false]);
|
||||
}
|
||||
|
||||
/// IDML measures a left-aligned anchored object's X offset leftwards; the document model and
|
||||
/// composition measure every X offset rightwards.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user