mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 12:24:19 +00:00
Merge pull request #7223 from dotCooCoo/fix/inline-graphics-in-symbol-art
Type: inline graphics in symbol, mask and pattern art follow their symbols
This commit is contained in:
@@ -1136,37 +1136,6 @@ fn font_list(_: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gives the inline graphics in the type of [`art_texts_of`] the current bounds of their symbols'
|
||||
/// art, and lays out again the type whose inline graphics changed. A symbol drawn inline in the
|
||||
/// art of another symbol takes one more round, so there is at most one round per symbol.
|
||||
fn resolve_art_inline(doc: &mut Document) {
|
||||
for _ in 0..=doc.symbols.len() {
|
||||
let names: BTreeSet<String> =
|
||||
art_texts_of(doc).iter().flat_map(|(_, t, _)| t.runs.iter().filter_map(|r| r.inline.as_ref().map(|a| a.symbol.clone()))).collect();
|
||||
let bounds: BTreeMap<String, Option<Rect>> = names
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
let b = doc.symbol_art_bounds(&name);
|
||||
(name, b)
|
||||
})
|
||||
.collect();
|
||||
let stale = |t: &TextObject| t.runs.iter().any(|r| r.inline.as_ref().is_some_and(|a| bounds.get(&a.symbol).is_some_and(|b| *b != a.bounds)));
|
||||
let mut changed = false;
|
||||
edit_art_texts_of(doc, &stale, &mut |t| {
|
||||
for a in t.runs.iter_mut().filter_map(|r| r.inline.as_mut()) {
|
||||
if let Some(b) = bounds.get(&a.symbol) {
|
||||
a.bounds = *b;
|
||||
}
|
||||
}
|
||||
refresh_bounds(t);
|
||||
changed = true;
|
||||
});
|
||||
if !changed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rescan(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -1188,8 +1157,7 @@ pub(crate) fn rescan(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
d.change_unsaved(|doc| {
|
||||
let doc = std::sync::Arc::make_mut(doc);
|
||||
edit_art_texts_of(doc, &|_| true, &mut refresh_bounds);
|
||||
resolve_art_inline(doc);
|
||||
doc.resolve_inline_art();
|
||||
doc.resolve_inline_art_with(&mut refresh_bounds);
|
||||
for id in texts {
|
||||
if let Some(NodeKind::Text(t)) = doc.node_mut(id).map(|n| &mut n.kind) {
|
||||
refresh_bounds(t);
|
||||
|
||||
@@ -22,16 +22,14 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
}
|
||||
|
||||
/// Inline graphics follow their symbols: after an edit of a document that has (or had) symbols,
|
||||
/// resolve the inline art of every text again (only texts whose art changed are touched).
|
||||
/// resolve the inline art of every text again, in the layers and in the art of symbols, graph
|
||||
/// designs, pattern tiles and opacity masks, and lay out again the texts whose art changed (a
|
||||
/// symbol's type before the type that shows that symbol inline).
|
||||
pub(crate) fn refresh(before: &Document, d: &mut Document) {
|
||||
if before.symbols.is_empty() && d.symbols.is_empty() {
|
||||
return;
|
||||
}
|
||||
for id in d.resolve_inline_art() {
|
||||
if let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) {
|
||||
refresh_bounds(t);
|
||||
}
|
||||
}
|
||||
d.resolve_inline_art_with(&mut refresh_bounds);
|
||||
}
|
||||
|
||||
fn insert_inline(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -85,7 +83,7 @@ fn insert_inline(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let caret = s.edit("Insert Inline Symbol", |d, _| {
|
||||
let mut art = InlineArt { scale, baseline_shift: shift, ..InlineArt::new(symbol.clone()) };
|
||||
art.bounds = d.symbol_art_bounds(&symbol);
|
||||
art.bounds = d.inline_art_bounds(&symbol);
|
||||
let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) else { return Err(EngineError::NoNode(id)) };
|
||||
let style = edit::insertion_style(&t.runs, a, b);
|
||||
let caret = edit::replace_range_styled(&mut t.runs, a, b, &[TextRun::inline(art, style)]);
|
||||
|
||||
@@ -270,6 +270,9 @@ impl Session {
|
||||
for tree in text_trees_mut(doc) {
|
||||
each_text_where(tree, &mut uncached, &mut cmd::typecmd::refresh_bounds_for_first_frame);
|
||||
}
|
||||
// The symbols' type is measured now: the inline graphics showing those symbols take their
|
||||
// art's measured size, a symbol's type before the type that shows that symbol inline.
|
||||
doc.resolve_inline_art_with(&mut cmd::typecmd::refresh_bounds_for_first_frame);
|
||||
}
|
||||
|
||||
/// Measure the type in open document `index` that `stale` picks (in its trees as
|
||||
@@ -287,12 +290,9 @@ impl Session {
|
||||
each_text_where(tree, &mut stale, &mut cmd::typecmd::refresh_bounds_for_first_frame);
|
||||
}
|
||||
// Inline graphics take their size from their symbols' art, whose type may just have
|
||||
// been measured again: the text holding one whose size changed is measured again too.
|
||||
if !doc.resolve_inline_art().is_empty() {
|
||||
for tree in text_trees_mut(doc) {
|
||||
each_text_where(tree, &mut |t| t.cached_bounds.is_none(), &mut cmd::typecmd::refresh_bounds_for_first_frame);
|
||||
}
|
||||
}
|
||||
// been measured again: the text holding one whose size changed is measured again too,
|
||||
// a symbol's type before the type that shows that symbol inline.
|
||||
doc.resolve_inline_art_with(&mut cmd::typecmd::refresh_bounds_for_first_frame);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::text::INLINE_CHAR;
|
||||
use vectorcraft_doc::{NodeId, NodeKind, TextObject};
|
||||
use vectorcraft_doc::{Node, NodeId, NodeKind, TextObject};
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
||||
|
||||
use crate::*;
|
||||
@@ -167,6 +167,164 @@ fn inline_graphics_follow_their_symbol() {
|
||||
let _ = s.execute("document.serialize", &json!({"format": "pdf"})).unwrap();
|
||||
}
|
||||
|
||||
/// The first text in tree `n`, its opacity mask art included.
|
||||
fn first_text(n: &Node) -> Option<TextObject> {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
return Some((**t).clone());
|
||||
}
|
||||
if let Some(t) = n.extras().mask.as_deref().and_then(|m| first_text(&m.art)) {
|
||||
return Some(t);
|
||||
}
|
||||
n.children().into_iter().flatten().find_map(|c| first_text(c))
|
||||
}
|
||||
|
||||
/// The first text in the art of symbol `name`.
|
||||
fn art_text(s: &Session, name: &str) -> TextObject {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
first_text(&d.symbols.iter().find(|x| x.name == name).unwrap().art).unwrap()
|
||||
}
|
||||
|
||||
/// The bounds of the first inline graphic in `t`.
|
||||
fn inline_bounds(t: &TextObject) -> Option<vectorcraft_geom::Rect> {
|
||||
t.runs.iter().find_map(|r| r.inline.as_ref()).unwrap().bounds
|
||||
}
|
||||
|
||||
/// Redefines symbol `name` as a 40 × 20 ellipse.
|
||||
fn redefine_as_wide_ellipse(s: &mut Session, name: &str) {
|
||||
let e = s.execute("shape.ellipse", &json!({"x": 300, "y": 300, "width": 40, "height": 20})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [e["id"]]})).unwrap();
|
||||
s.execute("symbol.update", &json!({"name": name})).unwrap();
|
||||
}
|
||||
|
||||
/// A symbol made from type with an inline graphic: when the symbol the graphic shows changes, the
|
||||
/// type in the symbol's art is laid out again, the symbol's art takes its new size, and an inline
|
||||
/// graphic of that symbol follows. Undo, redo and opening a saved copy keep it so.
|
||||
#[test]
|
||||
fn inline_graphics_in_symbol_art_follow_their_symbol() {
|
||||
let mut s = session();
|
||||
circle_symbol(&mut s, "Dot", 10.0);
|
||||
let id = text(&mut s, "ab");
|
||||
s.execute("text.insertInline", &json!({"id": id.0, "at": 1, "symbol": "Dot", "scale": 1})).unwrap();
|
||||
s.execute("symbol.new", &json!({"name": "Holder", "ids": [id.0]})).unwrap();
|
||||
let line = text(&mut s, "x");
|
||||
s.execute("text.insertInline", &json!({"id": line.0, "at": 0, "symbol": "Holder", "scale": 1})).unwrap();
|
||||
let held = art_text(&s, "Holder");
|
||||
let w0 = held.cached_bounds.unwrap().width();
|
||||
let holder0 = s.doc().unwrap().doc.symbol_art_bounds("Holder").unwrap();
|
||||
redefine_as_wide_ellipse(&mut s, "Dot");
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let held = art_text(&s, "Holder");
|
||||
let b = inline_bounds(&held).unwrap();
|
||||
assert!((b.width() / b.height() - 2.0).abs() < 0.1, "{b:?}");
|
||||
assert_eq!(Some(b), d.symbol_art_bounds("Dot"));
|
||||
let w1 = held.cached_bounds.unwrap().width();
|
||||
assert!((w1 - w0 - 20.0).abs() < 1.5, "{w0} → {w1}");
|
||||
assert!(held.cached_ink.is_some() && !held.cached_baselines.is_empty());
|
||||
let holder1 = d.symbol_art_bounds("Holder").unwrap();
|
||||
assert!(holder1.width() > holder0.width() + 1.0, "{holder0:?} → {holder1:?}");
|
||||
assert_eq!(inline_bounds(&obj(&s, line)), Some(holder1), "an inline graphic of the holder follows it");
|
||||
// Undo and redo.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!((art_text(&s, "Holder").cached_bounds.unwrap().width() - w0).abs() < 1e-6);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert!((art_text(&s, "Holder").cached_bounds.unwrap().width() - w1).abs() < 1e-6);
|
||||
// Opening a saved copy measures it the same way.
|
||||
let doc = (*s.doc().unwrap().doc).clone();
|
||||
let mut back = vectorcraft_format::load(&vectorcraft_format::save(&doc, false)).unwrap();
|
||||
back.title = "copy".into();
|
||||
let mut s2 = Session::new();
|
||||
s2.add_document(back, None);
|
||||
let held2 = art_text(&s2, "Holder");
|
||||
assert_eq!(inline_bounds(&held2), s2.doc().unwrap().doc.symbol_art_bounds("Dot"));
|
||||
assert!((held2.cached_bounds.unwrap().width() - w1).abs() < 1e-6);
|
||||
assert_eq!(inline_bounds(&obj(&s2, line)), s2.doc().unwrap().doc.symbol_art_bounds("Holder"));
|
||||
}
|
||||
|
||||
/// Type with an inline graphic in an opacity mask and in a pattern tile follows its symbol.
|
||||
#[test]
|
||||
fn inline_graphics_in_masks_and_pattern_tiles_follow_their_symbol() {
|
||||
let mut s = session();
|
||||
circle_symbol(&mut s, "Dot", 10.0);
|
||||
let masked = text(&mut s, "ab");
|
||||
s.execute("text.insertInline", &json!({"id": masked.0, "at": 1, "symbol": "Dot"})).unwrap();
|
||||
let owner = NodeId(s.execute("shape.rectangle", &json!({"x": 0, "y": 20, "width": 50, "height": 20})).unwrap()["id"].as_u64().unwrap());
|
||||
s.edit("Mask", |d, _| {
|
||||
let art = d.remove(masked)?;
|
||||
d.node_mut(owner).unwrap().extras_mut().mask = Some(Box::new(vectorcraft_doc::OpacityMask::new((*art).clone(), true)));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let tiled = text(&mut s, "cd");
|
||||
s.execute("text.insertInline", &json!({"id": tiled.0, "at": 1, "symbol": "Dot"})).unwrap();
|
||||
s.execute("object.pattern.make", &json!({"ids": [tiled.0], "name": "Tile", "edit": false})).unwrap();
|
||||
redefine_as_wide_ellipse(&mut s, "Dot");
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let want = d.symbol_art_bounds("Dot");
|
||||
let mask = first_text(&d.node(owner).unwrap().extras().mask.as_deref().unwrap().art).unwrap();
|
||||
assert_eq!(inline_bounds(&mask), want);
|
||||
let tile = d.patterns.iter().find(|p| p.name == "Tile").unwrap().art.iter().find_map(|n| first_text(n)).unwrap();
|
||||
assert_eq!(inline_bounds(&tile), want);
|
||||
}
|
||||
|
||||
/// Symbol C shows B inline and B shows A: with the symbols listed C, B, A, a change to A reaches C
|
||||
/// through B's new size.
|
||||
#[test]
|
||||
fn symbols_are_resolved_after_the_symbols_they_show_inline() {
|
||||
let mut s = session();
|
||||
circle_symbol(&mut s, "A", 10.0);
|
||||
for (name, shows) in [("B", "A"), ("C", "B")] {
|
||||
let t = text(&mut s, "z");
|
||||
s.execute("text.insertInline", &json!({"id": t.0, "at": 0, "symbol": shows})).unwrap();
|
||||
s.execute("symbol.new", &json!({"name": name, "ids": [t.0]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
}
|
||||
s.edit("Reverse the symbols", |d, _| {
|
||||
d.symbols.reverse();
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let b0 = s.doc().unwrap().doc.symbol_art_bounds("B");
|
||||
redefine_as_wide_ellipse(&mut s, "A");
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert_eq!(inline_bounds(&art_text(&s, "B")), d.symbol_art_bounds("A"));
|
||||
assert_ne!(d.symbol_art_bounds("B"), b0);
|
||||
assert_eq!(inline_bounds(&art_text(&s, "C")), d.symbol_art_bounds("B"));
|
||||
}
|
||||
|
||||
/// A symbol whose art shows the symbol itself inline, directly or through another symbol, is an
|
||||
/// empty slot, and stays one through later edits.
|
||||
#[test]
|
||||
fn a_symbol_that_shows_itself_inline_is_an_empty_slot() {
|
||||
let mut s = session();
|
||||
for name in ["Loop", "P", "Q"] {
|
||||
circle_symbol(&mut s, name, 10.0);
|
||||
}
|
||||
for (name, shows) in [("Loop", "Loop"), ("P", "Q"), ("Q", "P")] {
|
||||
let t = text(&mut s, "ab");
|
||||
s.execute("text.insertInline", &json!({"id": t.0, "at": 1, "symbol": shows})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
|
||||
s.execute("symbol.update", &json!({"name": name})).unwrap();
|
||||
}
|
||||
let line = text(&mut s, "x");
|
||||
s.execute("text.insertInline", &json!({"id": line.0, "at": 0, "symbol": "P"})).unwrap();
|
||||
for _ in 0..2 {
|
||||
for name in ["Loop", "P", "Q"] {
|
||||
assert_eq!(inline_bounds(&art_text(&s, name)), None, "{name}");
|
||||
assert_eq!(s.doc().unwrap().doc.inline_art_bounds(name), None, "{name}");
|
||||
}
|
||||
assert_eq!(inline_bounds(&obj(&s, line)), None);
|
||||
s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
}
|
||||
let _ = s.execute("document.serialize", &json!({"format": "png"})).unwrap();
|
||||
// Redefined without the inline graphic, Q is on no cycle, and neither is P.
|
||||
redefine_as_wide_ellipse(&mut s, "Q");
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert!(d.inline_art_bounds("P").is_some());
|
||||
assert_eq!(inline_bounds(&obj(&s, line)), d.symbol_art_bounds("P"));
|
||||
assert_eq!(inline_bounds(&art_text(&s, "P")), d.symbol_art_bounds("Q"));
|
||||
assert_eq!(d.inline_art_bounds("Loop"), None);
|
||||
}
|
||||
|
||||
/// A list paragraph that starts with an inline graphic draws its bullet before the graphic, and
|
||||
/// live SVG text writes the bullet.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user