mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 07:46:07 +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:
@@ -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