mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 14:41:19 +00:00
Symbols: Break Link keeps the instance's opacity mask
Break Link to Symbol dropped the instance's opacity mask. The art now keeps it, or a group around the art holds it when the symbol's art has a mask of its own. The objects in the instance's mask art count against Break Link's limit of 1,000,000 objects.
This commit is contained in:
@@ -201,7 +201,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Break Link to Symbol",
|
||||
[],
|
||||
None,
|
||||
"{ids?|id?} expand selected instances into plain art (with their appearance changes to a dynamic symbol), placed through the symbol's 9-slice guides when it has them → {ids}",
|
||||
"{ids?|id?} expand selected instances into plain art (with their appearance changes to a dynamic symbol), placed through the symbol's 9-slice guides when it has them, with the instance's opacity mask → {ids}",
|
||||
has_selection,
|
||||
symbol_break_link
|
||||
),
|
||||
|
||||
@@ -341,7 +341,8 @@ pub(super) fn symbol_place(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// The plain art of instance `inst` in document space (fresh ids), with the instance's own
|
||||
/// appearance changes to a dynamic symbol's art when `own`, placed through its symbol's 9-slice
|
||||
/// guides when it has them.
|
||||
/// guides when it has them. The instance's opacity mask goes on the art, or on a group around it
|
||||
/// when the art has a mask of its own.
|
||||
fn expanded_instance(d: &mut Document, inst: &Node, own: bool) -> Option<Node> {
|
||||
let NodeKind::SymbolInstance { symbol, xf } = &inst.kind else { return None };
|
||||
let sym = d.symbols.iter().find(|s| &s.name == symbol)?;
|
||||
@@ -361,13 +362,21 @@ fn expanded_instance(d: &mut Document, inst: &Node, own: bool) -> Option<Node> {
|
||||
art.opacity *= inst.opacity;
|
||||
art.blend = inst.blend;
|
||||
}
|
||||
if let Some(mask) = &inst.extras().mask {
|
||||
if art.extras().mask.is_some() {
|
||||
let id = d.alloc_id();
|
||||
art = Node::group(id, vec![Arc::new(art)]);
|
||||
}
|
||||
art.extras_mut().mask = Some(mask.clone());
|
||||
}
|
||||
Some(art)
|
||||
}
|
||||
|
||||
/// The most objects [`expanded_instance`] makes of instance `inst` in `d` (`own` as there): its
|
||||
/// symbol's object count with the art of its opacity masks, or
|
||||
/// [`vectorcraft_doc::NineSliceMap::most_placed`] when it is placed through 9-slice guides, plus
|
||||
/// the group that opacity or a blending mode adds. 0 for any other object.
|
||||
/// the group that opacity, a blending mode or the instance's opacity mask adds, plus the art of
|
||||
/// the instance's opacity mask. 0 for any other object.
|
||||
fn expanded_bound(d: &Document, inst: &Node, own: bool) -> u64 {
|
||||
let NodeKind::SymbolInstance { symbol, xf } = &inst.kind else { return 0 };
|
||||
let Some(sym) = d.symbols.iter().find(|s| &s.name == symbol) else { return 0 };
|
||||
@@ -378,7 +387,8 @@ fn expanded_bound(d: &Document, inst: &Node, own: bool) -> u64 {
|
||||
None if dynamic => vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, inst).count_with_masks() as u64,
|
||||
None => sym.art.count_with_masks() as u64,
|
||||
};
|
||||
most.saturating_add(1)
|
||||
let mask = inst.extras().mask.as_ref().map_or(0, |m| m.art.count_with_masks() as u64);
|
||||
most.saturating_add(1).saturating_add(mask)
|
||||
}
|
||||
|
||||
/// Replace instances `ids` with their art ([`expanded_instance`], `own` as there). Returns the
|
||||
|
||||
@@ -1242,6 +1242,59 @@ fn break_link_then_update_redefines_all_instances() {
|
||||
assert!((bounds(&s, other).center().x - 300.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// A linked opacity mask of a 10 × 10 square at (100, 50).
|
||||
fn square_mask(s: &mut Session) -> Box<vectorcraft_doc::OpacityMask> {
|
||||
let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let art = Node::path(doc.alloc_id(), vectorcraft_geom::shapes::rectangle(Rect::new(100.0, 50.0, 110.0, 60.0)), Default::default());
|
||||
Box::new(vectorcraft_doc::OpacityMask::new(art, true))
|
||||
}
|
||||
|
||||
/// Break Link to Symbol keeps the instance's opacity mask on the art.
|
||||
#[test]
|
||||
fn break_link_keeps_the_instances_opacity_mask() {
|
||||
let mut s = session();
|
||||
let (inst, _, _) = make_symbol(&mut s);
|
||||
let m = square_mask(&mut s);
|
||||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).node_mut(inst).unwrap().extras_mut().mask = Some(m.clone());
|
||||
let r = s.execute("symbol.breakLink", &json!({"ids": [inst.0]})).unwrap();
|
||||
let art = node(&s, NodeId(r["ids"][0].as_u64().unwrap()));
|
||||
assert!(matches!(art.kind, NodeKind::Path { .. }), "{}", art.kind_label());
|
||||
assert_eq!(art.extras().mask, Some(m), "the art keeps the mask");
|
||||
}
|
||||
|
||||
/// When the symbol's art has an opacity mask of its own, Break Link puts the instance's mask on a
|
||||
/// group around the art, and the art keeps its own.
|
||||
#[test]
|
||||
fn break_link_puts_the_instances_mask_on_a_group_around_masked_art() {
|
||||
let mut s = session();
|
||||
let (inst, name, _) = make_symbol(&mut s);
|
||||
let (own, m) = (square_mask(&mut s), square_mask(&mut s));
|
||||
let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let sym = doc.symbols.iter_mut().find(|s| s.name == name).unwrap();
|
||||
std::sync::Arc::make_mut(&mut sym.art).extras_mut().mask = Some(own);
|
||||
doc.node_mut(inst).unwrap().extras_mut().mask = Some(m.clone());
|
||||
let r = s.execute("symbol.breakLink", &json!({"ids": [inst.0]})).unwrap();
|
||||
let art = node(&s, NodeId(r["ids"][0].as_u64().unwrap()));
|
||||
let NodeKind::Group { children, .. } = &art.kind else { panic!("a group around the art: {}", art.kind_label()) };
|
||||
assert_eq!(art.extras().mask, Some(m), "the group holds the instance's mask");
|
||||
assert!(children.len() == 1 && children[0].extras().mask.is_some(), "the art keeps its own mask");
|
||||
}
|
||||
|
||||
/// The art of the instance's opacity mask counts against Break Link's object limit: an instance
|
||||
/// whose mask holds more than a million objects stays an instance.
|
||||
#[test]
|
||||
fn break_link_counts_the_instances_mask_art() {
|
||||
let mut s = session();
|
||||
let (inst, _, _) = make_symbol(&mut s);
|
||||
let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let square =
|
||||
std::sync::Arc::new(Node::path(doc.alloc_id(), vectorcraft_geom::shapes::rectangle(Rect::new(100.0, 50.0, 110.0, 60.0)), Default::default()));
|
||||
let art = Node::group(doc.alloc_id(), vec![square; 1_000_000]);
|
||||
doc.node_mut(inst).unwrap().extras_mut().mask = Some(Box::new(vectorcraft_doc::OpacityMask::new(art, true)));
|
||||
assert!(s.execute("symbol.breakLink", &json!({"ids": [inst.0]})).is_err(), "too many objects");
|
||||
assert!(matches!(node(&s, inst).kind, NodeKind::SymbolInstance { .. }), "it stays an instance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_duplicate_and_replace_symbols() {
|
||||
let mut s = session();
|
||||
|
||||
+1
-1
@@ -1603,7 +1603,7 @@ already walks Image Trace. Short agent entry points:
|
||||
a symbol with guides keeps its corners at their size, and `symbol.breakLink` gives its art as it draws; dragging a
|
||||
guide on the canvas in symbol editing mode sends `nineSliceGuides` in one undo step; `type` is
|
||||
Symbol Type, `static` by default for the command and `dynamic` by default in the Symbol Options dialog),
|
||||
`symbol.place {name?, x?, y?}`, `symbol.breakLink {ids?}`, `symbol.replace`,
|
||||
`symbol.place {name?, x?, y?}`, `symbol.breakLink {ids?}` (the art keeps the instance's opacity mask), `symbol.replace`,
|
||||
`symbol.edit {id?, ids?, name?, x?, y?, artboard?, viewCenter?}` (symbol editing mode: the symbol's art on a temporary
|
||||
isolated layer, where the instance `id` shows it or centered on (x, y), by default on the center of artboard
|
||||
`artboard`, the active one the app passes, or in a document without artboards on `viewCenter`, the view's center the
|
||||
|
||||
Reference in New Issue
Block a user