mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-03 07:01:09 -06:00
Merge remote-tracking branch 'origin/main' into paragraph-shading-indesign
# Conflicts: # ROADMAP.md
This commit is contained in:
@@ -358,6 +358,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-10-24** · Anchored text frames, such as marginal notes, compose where their host story places them: they align to the baseline grid at that position, and page-number markers and spine-relative alignment inside them read the host's page. They composed against a grid measured from their stored position and on no page, so a note set to Only Align First Line to Grid could sit a fraction of a line off the body text's grid line. No readiness percentage change.
|
||||
- **2026-10-22** · Paragraph Style Options › Paragraph Shading and Paragraph Border are laid out as InDesign's: headed with the page's name; Tint is a spinner with InDesign's presets and has Overprint beside it (shading, border and gap; a new attribute each, kept through IDML; PDF export warns it doesn't overprint yet); Corner Size and Shape is InDesign's two-by-two grid of corner icons, sizes and shape menus showing each shape's glyph, with the link in the middle; offsets and weights have spinners. Found in the Learn Skills Daily course, chapter 41. No readiness percentage change.
|
||||
- **2026-10-24** · New documents start with Hyphenate Capitalized Words, Hyphenate Last Word and Hyphenate Across Column off in [Basic Paragraph], as in InDesign. Opened and imported files keep the values they carry. No readiness percentage change.
|
||||
- **2026-10-24** · The baseline grid's Relative To setting applies: with Top Margin, the grid starts at each page's own top margin in text composition, on the canvas and for snapping. It started at the page top, so grid-aligned text sat on the wrong lines and a page could fit one line fewer. No readiness percentage change.
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
//! An entry is valid while the inputs it was composed from are the *same allocations*: the story
|
||||
//! `Arc` (and its revision), the styles `Arc`, the frame items' `Arc`s and the `Arc`s of every
|
||||
//! wrapping item on the frames' spreads, and the same for every story whose anchored objects wrap
|
||||
//! its text ([`crate::wrap::neighbours`]). Entries hold clones of those `Arc`s, so the pointers
|
||||
//! its text ([`crate::wrap::neighbours`]). An anchored frame's entry also records where its host
|
||||
//! story's composition places it. Entries hold clones of those `Arc`s, so the pointers
|
||||
//! can't be reused by other allocations while the entry lives.
|
||||
|
||||
use std::collections::HashMap;
|
||||
@@ -99,11 +100,12 @@ impl Cache {
|
||||
|
||||
/// [`Cache::get`] for any [`ShownOn`].
|
||||
pub fn get_on(&self, doc: &Document, sid: StoryId, on: ShownOn) -> Arc<ComposedStory> {
|
||||
self.get_with(doc, sid, on, true)
|
||||
self.get_with(doc, sid, on, true, 0)
|
||||
}
|
||||
|
||||
/// [`Cache::get_on`], wrapped by other stories' anchored objects only when `cross` is set.
|
||||
fn get_with(&self, doc: &Document, sid: StoryId, on: ShownOn, cross: bool) -> Arc<ComposedStory> {
|
||||
/// [`Cache::get_on`], wrapped by other stories' anchored objects only when `cross` is set, for
|
||||
/// a story `depth` hosts down from the story asked for.
|
||||
fn get_with(&self, doc: &Document, sid: StoryId, on: ShownOn, cross: bool, depth: usize) -> Arc<ComposedStory> {
|
||||
let Some(story) = doc.stories.get(&sid) else { return Arc::new(ComposedStory { story: sid, ..Default::default() }) };
|
||||
let (mut sig, mut keep_items) = signature(doc, story);
|
||||
let neighbours = if cross { crate::wrap::neighbours(doc, sid) } else { Vec::new() };
|
||||
@@ -117,6 +119,24 @@ impl Cache {
|
||||
keep_stories.push(ns.clone());
|
||||
}
|
||||
}
|
||||
// Anchored frames compose where their host story places them, so the host is composed
|
||||
// first and the placements are part of the signature.
|
||||
let placed = if depth < crate::placed::MAX_DEPTH {
|
||||
crate::placed::placements(doc, sid, &mut |host| self.get_with(doc, host, ShownOn::Own, true, depth + 1))
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
for (id, p) in &placed {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
(id, p.spread, p.xf.as_coeffs().map(f64::to_bits)).hash(&mut h);
|
||||
// The placed spread's pages give the frame its page, side and page rect.
|
||||
for pg in doc.spread(p.spread).map_or(&[][..], |sp| &sp.pages[..]) {
|
||||
let m = &pg.margins;
|
||||
(pg.side as u8, [pg.x, pg.width, pg.height, m.top, m.bottom, m.inside, m.outside].map(f64::to_bits)).hash(&mut h);
|
||||
}
|
||||
sig.push(h.finish() as usize);
|
||||
}
|
||||
let key = (sid, on, !neighbours.is_empty());
|
||||
// The key holds the page's index, which a renumbering doesn't change: its name (section
|
||||
// start number, style, prefix) is part of the signature.
|
||||
@@ -209,11 +229,11 @@ impl Cache {
|
||||
// Parent-page items are composed per page: markers show its name, variables need its index.
|
||||
let page = page.filter(|_| with_vars);
|
||||
let running = running.then(|| self.running_index(doc));
|
||||
let others: Vec<Arc<ComposedStory>> = neighbours.iter().map(|n| self.neighbour(doc, *n)).collect();
|
||||
let others: Vec<Arc<ComposedStory>> = neighbours.iter().map(|n| self.neighbour(doc, *n, depth)).collect();
|
||||
let out = Arc::new(crate::wrap::compose_among(
|
||||
doc,
|
||||
sid,
|
||||
&ComposeOptions { page_name, section_marker, page, running, label: None, xrefs: with_xrefs.then(|| self.xref_index(doc)) },
|
||||
&ComposeOptions { page_name, section_marker, page, running, label: None, xrefs: with_xrefs.then(|| self.xref_index(doc)), placed },
|
||||
&others.iter().map(|o| &**o).collect::<Vec<_>>(),
|
||||
));
|
||||
let mut g = self.map.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
@@ -295,13 +315,13 @@ impl Cache {
|
||||
/// Story `sid` composed for the wrap of its anchored objects in other stories: without other
|
||||
/// stories' anchored objects (no cycles), and outside the cache when it needs the document-wide
|
||||
/// indexes (they compose every story).
|
||||
fn neighbour(&self, doc: &Document, sid: StoryId) -> Arc<ComposedStory> {
|
||||
fn neighbour(&self, doc: &Document, sid: StoryId, depth: usize) -> Arc<ComposedStory> {
|
||||
let indexed =
|
||||
doc.stories.get(&sid).is_some_and(|st| crate::xref::has_xrefs(st) || (crate::vars::has_vars(st) && crate::vars::has_running(doc, st)));
|
||||
if indexed {
|
||||
Arc::new(crate::wrap::compose_among(doc, sid, &ComposeOptions::default(), &[]))
|
||||
} else {
|
||||
self.get_with(doc, sid, ShownOn::Own, false)
|
||||
self.get_with(doc, sid, ShownOn::Own, false, depth)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ pub mod optical;
|
||||
mod outline;
|
||||
mod overlay;
|
||||
mod parabox;
|
||||
mod placed;
|
||||
mod ruby;
|
||||
pub mod shape;
|
||||
pub mod shatai;
|
||||
@@ -50,6 +51,7 @@ use crate::notes::Notes;
|
||||
pub use crate::notes::{find_note, hit_note, hit_note_with, note_caret};
|
||||
pub use crate::outline::FrameOutline;
|
||||
use crate::parabox::ParaBox;
|
||||
pub use crate::placed::Placement;
|
||||
use crate::shape::{Glyph, StyleTable, SubstCtx};
|
||||
pub use crate::table::{BorderHit, PlacedCell, StrokeSeg, TableFrag, cell_caret, find_cell, hit_border, hit_cell, hit_cell_with};
|
||||
|
||||
@@ -629,6 +631,9 @@ pub struct ComposeOptions {
|
||||
pub label: Option<String>,
|
||||
/// Anchor pages for cross-references (needed when the story contains cross-references).
|
||||
pub xrefs: Option<Arc<xref::XrefIndex>>,
|
||||
/// Where the story's anchored frames sit on their spreads; an anchored frame not listed is
|
||||
/// composed where it is stored, on no page.
|
||||
pub placed: Vec<(ItemId, Placement)>,
|
||||
}
|
||||
|
||||
/// Nesting depth past which wrap obstacles inside groups are not collected.
|
||||
@@ -661,21 +666,34 @@ fn wrap_obstacles(it: &Item, parent: Affine, inv: Affine, skip: ItemId, left_pag
|
||||
|
||||
/// Build the frame specs of a story from the document (geometry, wrap, page names, grid).
|
||||
pub fn frame_specs(doc: &Document, sid: StoryId) -> Vec<FrameSpec> {
|
||||
frame_specs_placed(doc, sid, &[])
|
||||
}
|
||||
|
||||
/// [`frame_specs`] with anchored frames where their host story placed them (`placed`).
|
||||
pub(crate) fn frame_specs_placed(doc: &Document, sid: StoryId, placed: &[(ItemId, Placement)]) -> Vec<FrameSpec> {
|
||||
let Some(st) = doc.story(sid) else { return vec![] };
|
||||
let mut out = Vec::with_capacity(st.frames.len());
|
||||
for &fid in &st.frames {
|
||||
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 parent = loc.as_ref().map(|l| doc.parent_xf(l)).unwrap_or(designcraft_geom::Affine::IDENTITY);
|
||||
// An anchored frame takes its page, page side, page rect and grid from where it is placed.
|
||||
let placement = if loc.is_none() { placed.iter().find(|(id, _)| *id == fid).map(|(_, p)| *p) } else { None };
|
||||
let parent = match (&loc, placement) {
|
||||
(Some(l), _) => doc.parent_xf(l),
|
||||
(None, Some(p)) => p.xf,
|
||||
(None, None) => 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);
|
||||
let spread_ref = loc.as_ref().map(|l| l.spread).or(placement.map(|p| p.spread));
|
||||
let spread = spread_ref.and_then(|r| doc.spread(r));
|
||||
// Page items wrap the text of frames on the spread, not of anchored frames.
|
||||
if !tf.options.ignore_wrap
|
||||
&& loc.is_some()
|
||||
&& let Some(sp) = spread
|
||||
{
|
||||
let left = |x: f64| sp.page_at_x(x).and_then(|pi| sp.pages.get(pi)).is_some_and(|p| p.side == designcraft_doc::PageSide::Left);
|
||||
@@ -785,10 +803,23 @@ pub fn frame_specs(doc: &Document, sid: StoryId) -> Vec<FrameSpec> {
|
||||
|
||||
/// Compose a story of `doc` through its frames, with the text wrap of page items and of anchored
|
||||
/// objects ([`wrap`]).
|
||||
/// Anchored frames are composed where their host story places them (unless `opts` places them).
|
||||
pub fn compose_story(doc: &Document, sid: StoryId, opts: &ComposeOptions) -> ComposedStory {
|
||||
compose_story_at(doc, sid, opts, 0)
|
||||
}
|
||||
|
||||
/// [`compose_story`] for a story `depth` hosts down from the story asked for.
|
||||
fn compose_story_at(doc: &Document, sid: StoryId, opts: &ComposeOptions, depth: usize) -> ComposedStory {
|
||||
let others: Vec<ComposedStory> =
|
||||
wrap::neighbours(doc, sid).into_iter().map(|o| wrap::compose_among(doc, o, &ComposeOptions::default(), &[])).collect();
|
||||
wrap::compose_among(doc, sid, opts, &others.iter().collect::<Vec<_>>())
|
||||
let others: Vec<&ComposedStory> = others.iter().collect();
|
||||
if opts.placed.is_empty() && depth < placed::MAX_DEPTH {
|
||||
let placed = placed::placements(doc, sid, &mut |host| Arc::new(compose_story_at(doc, host, &ComposeOptions::default(), depth + 1)));
|
||||
if !placed.is_empty() {
|
||||
return wrap::compose_among(doc, sid, &ComposeOptions { placed, ..opts.clone() }, &others);
|
||||
}
|
||||
}
|
||||
wrap::compose_among(doc, sid, opts, &others)
|
||||
}
|
||||
|
||||
const DEFAULT_TAB: f64 = 36.0;
|
||||
|
||||
@@ -180,6 +180,7 @@ impl Notes {
|
||||
running: opts.running.clone(),
|
||||
label: Some(label),
|
||||
xrefs: opts.xrefs.clone(),
|
||||
placed: Vec::new(),
|
||||
};
|
||||
let cs = crate::compose(doc, &self.notes[k].text, std::slice::from_ref(&spec), &nopts);
|
||||
let lines = cs.frames.first().map(|ft| ft.lines.as_slice()).unwrap_or_default();
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//! Where anchored text frames sit on their spread.
|
||||
//!
|
||||
//! An anchored frame is not a spread item: its host story's composition places it on a line. Its
|
||||
//! own story is composed against that placement (page, page side, page rect, baseline grid), so
|
||||
//! the host story is composed first.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use designcraft_doc::{Document, Item, ItemId, SpreadRef, StoryId};
|
||||
use designcraft_geom::Affine;
|
||||
|
||||
use crate::{ComposedStory, FrameText};
|
||||
|
||||
/// Hosts nested deeper than this (a note anchored in a note anchored in …, or stories anchored in
|
||||
/// each other in a cycle) are composed where they are stored.
|
||||
pub(crate) const MAX_DEPTH: usize = 8;
|
||||
|
||||
/// An anchored item's parent space on its spread, where its host story's composition placed it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Placement {
|
||||
pub spread: SpreadRef,
|
||||
/// The item's parent space → spread space.
|
||||
pub xf: Affine,
|
||||
}
|
||||
|
||||
/// The placements of `sid`'s anchored frames, from their host stories composed by `host`.
|
||||
pub(crate) fn placements(doc: &Document, sid: StoryId, host: &mut dyn FnMut(StoryId) -> Arc<ComposedStory>) -> Vec<(ItemId, Placement)> {
|
||||
let Some(st) = doc.story(sid) else { return Vec::new() };
|
||||
let mut anchored = None;
|
||||
let mut out = Vec::new();
|
||||
for &fid in &st.frames {
|
||||
if doc.find(fid).is_some() {
|
||||
continue;
|
||||
}
|
||||
let items = anchored.get_or_insert_with(|| doc.anchored_items());
|
||||
// A frame anchored in its own story would need its own composition to place it.
|
||||
let Some(owner) = items.iter().find_map(|(owner, it)| (it.id == fid).then_some(*owner)).filter(|o| *o != sid) else { continue };
|
||||
if let Some(p) = locate(doc, &host(owner), fid) {
|
||||
out.push((fid, p));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Where `cs` (a host story's composition) places item `fid`: in its frames' anchored objects,
|
||||
/// footnotes and table cells.
|
||||
fn locate(doc: &Document, cs: &ComposedStory, fid: ItemId) -> Option<Placement> {
|
||||
cs.frames.iter().find_map(|ft| {
|
||||
let loc = doc.find(ft.frame)?;
|
||||
let frame = doc.item_at(&loc)?;
|
||||
let xf = in_frame(ft, doc.parent_xf(&loc) * doc.text_xf(frame), fid, 0)?;
|
||||
Some(Placement { spread: loc.spread, xf })
|
||||
})
|
||||
}
|
||||
|
||||
/// The parent space of `fid` among the objects of `ft`, whose text space maps to the spread by
|
||||
/// `xf`.
|
||||
fn in_frame(ft: &FrameText, xf: Affine, fid: ItemId, depth: usize) -> Option<Affine> {
|
||||
if depth > MAX_DEPTH {
|
||||
return None;
|
||||
}
|
||||
let objects = ft.objects.iter().find_map(|o| in_item(&o.object.item, xf * Affine::translate(o.origin.to_vec2()), fid, 0));
|
||||
objects
|
||||
.or_else(|| ft.notes.iter().find_map(|n| in_frame(n.text.frames.first()?, xf * Affine::translate(n.origin.to_vec2()), fid, depth + 1)))
|
||||
.or_else(|| {
|
||||
ft.tables
|
||||
.iter()
|
||||
.flat_map(|t| &t.cells)
|
||||
.find_map(|c| in_frame(c.text.frames.first()?, xf * Affine::translate(c.origin.to_vec2()), fid, depth + 1))
|
||||
})
|
||||
}
|
||||
|
||||
/// The parent space of `fid` in `it` (it or a group member), `it`'s parent space mapping to the
|
||||
/// spread by `parent`.
|
||||
fn in_item(it: &Item, parent: Affine, fid: ItemId, depth: usize) -> Option<Affine> {
|
||||
if it.id == fid {
|
||||
return Some(parent);
|
||||
}
|
||||
if depth > crate::MAX_WRAP_DEPTH {
|
||||
return None;
|
||||
}
|
||||
let xf = parent * it.xf;
|
||||
it.children().iter().find_map(|c| in_item(c, xf, fid, depth + 1))
|
||||
}
|
||||
@@ -7658,3 +7658,91 @@ fn an_rtl_first_line_indent_is_at_the_right_edge() {
|
||||
assert!((left_edge(lines[0]) - rect.x0).abs() < 1.0, "left to right, first line at the edge: {}", left_edge(lines[0]));
|
||||
assert!((left_edge(lines[1]) - (rect.x0 + 10.0 * MM)).abs() < 1.0, "left to right, wrapped line indented: {}", left_edge(lines[1]));
|
||||
}
|
||||
|
||||
/// A marginal note (an anchored text frame, Only Align First Line to Grid, 9/10) hangs 15 pt
|
||||
/// above its anchor's baseline on a right-hand page. It composes against the document grid where
|
||||
/// it is placed: its first line moves down onto the grid line, the second follows 10 pt below,
|
||||
/// and its page is the host's. Composed where it was stored, it snapped to a grid measured from
|
||||
/// its own top edge and sat on no page.
|
||||
#[test]
|
||||
fn anchored_text_frames_compose_against_the_grid_where_they_are_placed() {
|
||||
use designcraft_doc::anchored::AnchorRelative as R;
|
||||
let mut d = Document::new(&NewDocument { pages: 3, ..Default::default() });
|
||||
d.settings.baseline_grid.start = 36.0;
|
||||
d.settings.baseline_grid.increment = 10.0;
|
||||
let lid = d.default_layer();
|
||||
let page = d.spreads[1].pages[1].clone();
|
||||
assert_eq!(page.side, designcraft_doc::PageSide::Right);
|
||||
// Body lines at 46, 56, 66, … (on the grid) while the frame top is at 36.
|
||||
let body = "One.\nTwo.\nThree.\nFour.";
|
||||
let rect = Rect::new(page.x + 108.0, 36.0, page.x + page.width - 36.0, 400.0);
|
||||
let (bf, sid) = d.add_text_frame(SpreadRef::Doc(1), rect, lid, body, ParaFormat::default()).unwrap();
|
||||
d.item_mut(bf).unwrap().text_frame_mut().unwrap().options.first_baseline = FirstBaseline::Leading;
|
||||
let nine_ten = |f: &mut designcraft_doc::story::CharFormat| {
|
||||
f.over.size = Some(9.0);
|
||||
f.over.leading = Some(designcraft_doc::Leading::Points(10.0));
|
||||
};
|
||||
d.story_mut(sid).unwrap().format_chars(0..body.len(), nine_ten);
|
||||
// The note: its own story in a 60 × 40 frame anchored in the third line.
|
||||
let (nid, nsid) = (ItemId(d.alloc()), StoryId(d.alloc()));
|
||||
let text = "Note one\u{2028}note two";
|
||||
let para = ParaFormat { para: ParaAttrs { grid_align: Some(GridAlign::FirstLineOnly), ..Default::default() }, ..Default::default() };
|
||||
let mut story = Story::with_text(nsid, text, para);
|
||||
story.format_chars(0..text.len(), nine_ten);
|
||||
story.frames.push(nid);
|
||||
d.stories.insert(nsid, Arc::new(story));
|
||||
let mut note =
|
||||
designcraft_doc::Item::new(nid, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 60.0, 40.0)));
|
||||
note.content = Content::Text(designcraft_doc::TextFrame { story: nsid, options: Default::default() });
|
||||
let position = designcraft_doc::AnchorPosition::Custom {
|
||||
x_relative: R::TextFrame,
|
||||
y_relative: R::Anchor,
|
||||
x_offset: -72.0,
|
||||
y_offset: -15.0,
|
||||
object_point: 0,
|
||||
ref_point: 0,
|
||||
keep_within_column: false,
|
||||
spine_relative: false,
|
||||
};
|
||||
let at = body.find("Three").unwrap();
|
||||
d.story_mut(sid).unwrap().insert_object(at, designcraft_doc::AnchoredObject::new(note, position));
|
||||
|
||||
let cache = Cache::new();
|
||||
let on_grid = |y: f64| on_grid(y, (36.0, 10.0));
|
||||
// (anchor line baseline, note baselines), in spread space.
|
||||
let baselines = |d: &Document, cs: &ComposedStory| {
|
||||
let host = compose_story(d, sid, &ComposeOptions::default());
|
||||
let ft = host.frame(bf).unwrap();
|
||||
let o = &ft.objects[0];
|
||||
let fxf = d.item(bf).unwrap().xf;
|
||||
let anchor = (fxf * Point::new(0.0, ft.lines[o.line].baseline)).y;
|
||||
let nxf = fxf * Affine::translate(o.origin.to_vec2()) * d.anchored_item(nid).unwrap().xf;
|
||||
(anchor, all_lines(cs).iter().map(|l| (nxf * Point::new(0.0, l.baseline)).y).collect::<Vec<_>>())
|
||||
};
|
||||
let near = |a: f64, b: f64| (a - b).abs() < 1e-6;
|
||||
|
||||
let (anchor, lines) = baselines(&d, &cache.get(&d, nsid, None));
|
||||
assert!(near(anchor, 66.0), "anchor baseline {anchor}");
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert!(near(lines[0], anchor) && near(lines[1], anchor + 10.0), "note baselines {lines:?}, anchor {anchor}");
|
||||
assert_eq!(baselines(&d, &compose_story(&d, nsid, &ComposeOptions::default())).1, lines);
|
||||
// The note is on the host's right-hand page, with that page's rectangle.
|
||||
let placed = placed::placements(&d, nsid, &mut |h| Arc::new(compose_story(&d, h, &ComposeOptions::default())));
|
||||
let spec = &frame_specs_placed(&d, nsid, &placed)[0];
|
||||
assert_eq!((spec.page, spec.right_page, spec.left_page), (Some(2), true, false));
|
||||
assert!(spec.page_rect.is_some());
|
||||
|
||||
// Host moved down 5 pt: the anchor is off the grid, and the note's first line moves down from
|
||||
// 10 pt above it onto the grid line 5 pt above it. The cache composes the note again.
|
||||
let xf = d.item(bf).unwrap().xf;
|
||||
d.item_mut(bf).unwrap().xf = Affine::translate((0.0, 5.0)) * xf;
|
||||
let (anchor, lines) = baselines(&d, &cache.get(&d, nsid, None));
|
||||
assert!(near(anchor, 71.0), "anchor baseline {anchor}");
|
||||
assert!(on_grid(lines[0]) && near(lines[0], anchor - 5.0) && near(lines[1], lines[0] + 10.0), "note baselines {lines:?}, anchor {anchor}");
|
||||
assert_eq!(baselines(&d, &compose_story(&d, nsid, &ComposeOptions::default())).1, lines);
|
||||
|
||||
// The host's page turns into a left-hand page: the note is composed again for that side.
|
||||
let before = cache.get(&d, nsid, None);
|
||||
Arc::make_mut(&mut d.spreads[1]).pages[1].side = designcraft_doc::PageSide::Left;
|
||||
assert!(!Arc::ptr_eq(&before, &cache.get(&d, nsid, None)));
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ fn settled(a: &[Vec<Exclusion>], b: &[Vec<Exclusion>]) -> bool {
|
||||
/// [`neighbours`]) and of its own.
|
||||
pub(crate) fn compose_among(doc: &Document, sid: StoryId, opts: &ComposeOptions, others: &[&ComposedStory]) -> ComposedStory {
|
||||
let Some(st) = doc.story(sid) else { return ComposedStory { story: sid, ..Default::default() } };
|
||||
let mut specs = crate::frame_specs(doc, sid);
|
||||
let mut specs = crate::frame_specs_placed(doc, sid, &opts.placed);
|
||||
if !others.is_empty() {
|
||||
let mut obs = Vec::new();
|
||||
for cs in others {
|
||||
|
||||
Reference in New Issue
Block a user