mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 18:15:49 +00:00
Expand: selected symbol instances become their art, as Break Link places it
Object > Expand with Object on now turns each symbol instance in the selection, including one in a selected group or in a placed document's art, into its art, as Break Link to Symbol places it. It goes one level deep: the instances in that art stay instances. The art keeps the instance's id and place in the stacking order, and Expand stays one undo step. - Instances with effects stay instances, and Expand Appearance bakes them as before. Instances with an effect on a fill or stroke, instances in perspective, and those in blends, repeats and Intertwine groups stay instances. - The art keeps the instance's opacity mask, as Break Link gives it. - A hidden or locked instance's art stays hidden or locked. - A placed document in an instance's art is expanded too. - When the instances' art would make more than 1,000,000 objects, they stay instances, the limit Break Link uses. - The Fill and Stroke options (object.expand.info, and the Expand dialog) count the fills, strokes and gradients in the art of those instances, with a dynamic instance's own changes applied. The info reports under symbolArt which of them only that art holds, and the dialog enables those options only while Object is on.
This commit is contained in:
@@ -35,7 +35,9 @@ use brushes::{
|
||||
};
|
||||
use dynamic::{symbol_instance_reset, symbol_instance_set_appearance};
|
||||
use symbols::selected_instances;
|
||||
pub(crate) use symbols::{instance_in_place, natural_size, normalize, place_xf, set_natural_size, store_symbol};
|
||||
pub(crate) use symbols::{
|
||||
expand_instances_under, expandable_instances, instance_in_place, natural_size, normalize, place_xf, set_natural_size, store_symbol,
|
||||
};
|
||||
use symbols::{
|
||||
symbol_adjust, symbol_break_link, symbol_delete, symbol_duplicate, symbol_list, symbol_new, symbol_options, symbol_place, symbol_replace,
|
||||
symbol_select_instances, symbol_set_current, symbol_spray, symbol_unused, symbol_update,
|
||||
|
||||
@@ -412,7 +412,64 @@ fn break_links(d: &mut Document, ids: &[NodeId], own: bool) -> Result<Vec<NodeId
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// How many objects [`break_links`] may make at once.
|
||||
/// Object › Expand: the symbol instances under `roots` of `d` ([`expandable_instances`]) replaced
|
||||
/// by their art, as Break Link places it, each keeping the instance's id and place. The instances
|
||||
/// that art holds stay instances. A hidden or locked instance's art stays hidden or locked. When
|
||||
/// the instances together would make more than [`MAX_EXPAND_NODES`] objects, all of them stay
|
||||
/// instances. Returns whether any instance was expanded.
|
||||
pub(crate) fn expand_instances_under(d: &mut Document, roots: &[NodeId]) -> Result<bool> {
|
||||
let ids = expandable_instances(d, roots);
|
||||
let total = ids.iter().filter_map(|id| d.node(*id)).fold(0u64, |sum, n| sum.saturating_add(expanded_bound(d, n, true)));
|
||||
if total > MAX_EXPAND_NODES as u64 {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut any = false;
|
||||
for id in ids {
|
||||
let Some(inst) = d.node(id).cloned() else { continue };
|
||||
let Some(mut art) = expanded_instance(d, &inst, true) else { continue };
|
||||
let (par, idx, _) = d.position(id).ok_or(EngineError::NoNode(id))?;
|
||||
art.id = id;
|
||||
art.visible &= inst.visible;
|
||||
art.locked |= inst.locked;
|
||||
d.remove(id)?;
|
||||
d.insert(par, idx, art)?;
|
||||
any = true;
|
||||
}
|
||||
Ok(any)
|
||||
}
|
||||
|
||||
/// The symbol instances under `roots` of `d` that Object › Expand turns into their art: those
|
||||
/// without effects (Expand Appearance bakes the others), with no effects on their fills and
|
||||
/// strokes, not in perspective, and whose symbol `d` holds. The instances in blends, repeats and
|
||||
/// Intertwine groups are left to Expand, which turns those into plain groups.
|
||||
pub(crate) fn expandable_instances(d: &Document, roots: &[NodeId]) -> Vec<NodeId> {
|
||||
fn collect(d: &Document, n: &Node, out: &mut Vec<NodeId>) {
|
||||
match &n.kind {
|
||||
NodeKind::SymbolInstance { symbol, .. } => {
|
||||
if n.appearance.effects.is_empty()
|
||||
&& n.appearance.items.iter().all(|i| i.effects().is_empty())
|
||||
&& n.projection().is_none()
|
||||
&& d.symbols.iter().any(|s| &s.name == symbol)
|
||||
{
|
||||
out.push(n.id);
|
||||
}
|
||||
}
|
||||
NodeKind::Blend { .. } | NodeKind::Repeat(_) | NodeKind::Intertwine { .. } => {}
|
||||
_ => {
|
||||
for c in n.children().into_iter().flatten() {
|
||||
collect(d, c, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
for n in roots.iter().filter_map(|r| d.node(*r)) {
|
||||
collect(d, n, &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// How many objects [`break_links`] and [`expand_instances_under`] may make at once.
|
||||
const MAX_EXPAND_NODES: usize = 1_000_000;
|
||||
|
||||
pub(super) fn selected_instances(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Object → Expand: live shapes, type and effects (Object), strokes (Stroke) and gradient and pattern
|
||||
//! fills (Fill) into plain art in one undo step; blends, repeats and Intertwine groups become groups of their steps, of their instances and of what they draw. Placed documents (Object) become editable copies of the
|
||||
//! art they show, their resources joining the document. A linear or radial gradient fill becomes a
|
||||
//! art they show, their resources joining the document. Symbol instances (Object) become their art,
|
||||
//! as Break Link to Symbol places it. A linear or radial gradient fill becomes a
|
||||
//! gradient mesh, or solid strips / concentric ellipses, inside a clip group shaped like the object; a freeform
|
||||
//! gradient becomes a mesh shaped like the object. A pattern fill becomes the tiles it paints inside a clip
|
||||
//! group shaped like the object. Create Gradient Mesh shares the sampling.
|
||||
@@ -40,7 +41,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Expand…",
|
||||
["Object"],
|
||||
None,
|
||||
"{object?: true (type → outlines, live shapes → paths, effects baked, blends → groups of their steps, repeats → groups of their instances, Intertwine groups → groups of their objects and of the parts drawn over the others (clip groups), envelopes → their distorted content, placed documents → editable copies of their art, as placing the file without link gives, its symbols, patterns, swatches and images joining the document), fill?: true (gradient fills → `gradient` art, pattern fills → their tiles in a clip group shaped like the object), stroke?: true (strokes → filled outlines, the stroked paths that baking effects leaves included), gradient?: \"objects\" (default: `steps` solid strips, concentric ellipses for radial gradients) | \"mesh\" (a gradient mesh); both in a clip group shaped like the object (freeform gradients always become a mesh shaped like it), steps?: 1..1000 (255)} one undo step → {ids}",
|
||||
"{object?: true (symbol instances without effects (their fills' and strokes' included) and not in perspective → their art, as Break Link to Symbol places it (the instances that art holds stay instances), type → outlines, live shapes → paths, effects baked, blends → groups of their steps, repeats → groups of their instances, Intertwine groups → groups of their objects and of the parts drawn over the others (clip groups), envelopes → their distorted content, placed documents → editable copies of their art, as placing the file without link gives, its symbols, patterns, swatches and images joining the document), fill?: true (gradient fills → `gradient` art, pattern fills → their tiles in a clip group shaped like the object), stroke?: true (strokes → filled outlines, the stroked paths that baking effects leaves included), gradient?: \"objects\" (default: `steps` solid strips, concentric ellipses for radial gradients) | \"mesh\" (a gradient mesh); both in a clip group shaped like the object (freeform gradients always become a mesh shaped like it), steps?: 1..1000 (255)} one undo step → {ids}",
|
||||
has_selection,
|
||||
expand
|
||||
),
|
||||
@@ -49,11 +50,17 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Expand Options",
|
||||
[],
|
||||
None,
|
||||
"{} what each Expand option would change in the selection → {object, fill, stroke, gradient, appearance} (true: something to expand; gradient: a gradient fill that Fill expands, which Expand Gradient To applies to; appearance: appearance attributes Expand Appearance expands first, for which Object › Expand… is dimmed in the menus)",
|
||||
"{} what each Expand option would change in the selection → {object, fill, stroke, gradient, appearance, symbolArt?: {fill, stroke, gradient}} (true: something to expand; gradient: a gradient fill that Fill expands, which Expand Gradient To applies to; appearance: appearance attributes Expand Appearance expands first, for which Object › Expand… is dimmed in the menus; symbolArt, when the selection holds symbol instances that Object expands: true when only their art holds it, which Fill and Stroke reach only with Object on)",
|
||||
has_selection,
|
||||
|s, _| {
|
||||
let e = Expandable::of(&s.doc()?.doc, &selected_roots(s)?);
|
||||
Ok(json!({"object": e.object(), "fill": e.fill, "stroke": e.stroke, "gradient": e.gradient, "appearance": has_appearance(s)}))
|
||||
let mut info =
|
||||
json!({"object": e.object(), "fill": e.fill, "stroke": e.stroke, "gradient": e.gradient, "appearance": has_appearance(s)});
|
||||
if e.symbols {
|
||||
let [fill, stroke, gradient] = e.symbol_art;
|
||||
info["symbolArt"] = json!({"fill": fill, "stroke": stroke, "gradient": gradient});
|
||||
}
|
||||
Ok(info)
|
||||
}
|
||||
),
|
||||
]
|
||||
@@ -124,45 +131,74 @@ struct Expandable {
|
||||
repeats: bool,
|
||||
/// Intertwine groups (Object expands them into groups of what they draw).
|
||||
intertwines: bool,
|
||||
/// Symbol instances (Object expands them into their art, as Break Link to Symbol does).
|
||||
symbols: bool,
|
||||
/// Fill, stroke and gradient that only the art of those symbol instances holds: Fill and Stroke
|
||||
/// reach them only when Object expands the instances.
|
||||
symbol_art: [bool; 3],
|
||||
}
|
||||
|
||||
impl Expandable {
|
||||
fn of(d: &Document, roots: &[NodeId]) -> Self {
|
||||
let mut e = Self::default();
|
||||
for n in roots.iter().filter_map(|r| d.node(*r)) {
|
||||
n.walk(&mut |c| {
|
||||
let shape = matches!(c.kind, NodeKind::Path { .. } | NodeKind::Compound { .. });
|
||||
e.text |= matches!(c.kind, NodeKind::Text(_));
|
||||
e.live |= matches!(c.kind, NodeKind::Path { live: Some(_), .. });
|
||||
e.effects |= !c.appearance.effects.is_empty();
|
||||
e.envelope |= matches!(c.kind, NodeKind::Envelope { .. });
|
||||
e.blends |= matches!(c.kind, NodeKind::Blend { .. });
|
||||
e.placed |= matches!(c.kind, NodeKind::PlacedDocument(_));
|
||||
e.repeats |= matches!(c.kind, NodeKind::Repeat(_));
|
||||
e.intertwines |= matches!(c.kind, NodeKind::Intertwine { .. });
|
||||
e.stroke |= match &c.kind {
|
||||
// Type's outlines take its characters' stroke (see `type.createOutlines`).
|
||||
NodeKind::Text(t) => t.runs.iter().any(|r| r.style.has_stroke()),
|
||||
_ => shape && !c.appearance.stroke_paint().is_none(),
|
||||
};
|
||||
e.gradient |= match &c.kind {
|
||||
NodeKind::Text(t) => t.runs.iter().any(|r| matches!(r.style.fill, Paint::Gradient(_))),
|
||||
_ => shape && c.appearance.items.iter().any(|i| gradient_fill(i).is_some()),
|
||||
};
|
||||
e.fill |= e.gradient;
|
||||
e.fill |= match &c.kind {
|
||||
NodeKind::Text(t) => {
|
||||
t.runs.iter().any(|r| matches!(&r.style.fill, Paint::Pattern { pattern, .. } if d.pattern(pattern).is_some()))
|
||||
}
|
||||
_ => shape && c.appearance.items.iter().any(|i| pattern_fill(d, i).is_some()),
|
||||
};
|
||||
});
|
||||
e.add(d, n);
|
||||
}
|
||||
let own = [e.fill, e.stroke, e.gradient];
|
||||
// The Fill and Stroke options also reach the art of the symbol instances that Object
|
||||
// expands: a dynamic instance's art with its own changes, and each other symbol's art once.
|
||||
let instances = super::brushsym::expandable_instances(d, roots);
|
||||
e.symbols = !instances.is_empty();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for inst in instances.iter().filter_map(|id| d.node(*id)) {
|
||||
let NodeKind::SymbolInstance { symbol, .. } = &inst.kind else { continue };
|
||||
let Some(sym) = d.symbols.iter().find(|s| &s.name == symbol) else { continue };
|
||||
let mut a = Self::default();
|
||||
if !inst.extras().symbol_overrides.is_empty() {
|
||||
a.add(d, &vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, inst));
|
||||
} else if seen.insert(symbol.clone()) {
|
||||
a.add(d, &sym.art);
|
||||
}
|
||||
e.fill |= a.fill;
|
||||
e.gradient |= a.gradient;
|
||||
e.stroke |= a.stroke;
|
||||
}
|
||||
e.symbol_art = [e.fill && !own[0], e.stroke && !own[1], e.gradient && !own[2]];
|
||||
e
|
||||
}
|
||||
|
||||
/// Adds what `n` and the nodes under it hold.
|
||||
fn add(&mut self, d: &Document, n: &Node) {
|
||||
let e = self;
|
||||
n.walk(&mut |c| {
|
||||
let shape = matches!(c.kind, NodeKind::Path { .. } | NodeKind::Compound { .. });
|
||||
e.text |= matches!(c.kind, NodeKind::Text(_));
|
||||
e.live |= matches!(c.kind, NodeKind::Path { live: Some(_), .. });
|
||||
e.effects |= !c.appearance.effects.is_empty();
|
||||
e.envelope |= matches!(c.kind, NodeKind::Envelope { .. });
|
||||
e.blends |= matches!(c.kind, NodeKind::Blend { .. });
|
||||
e.placed |= matches!(c.kind, NodeKind::PlacedDocument(_));
|
||||
e.repeats |= matches!(c.kind, NodeKind::Repeat(_));
|
||||
e.intertwines |= matches!(c.kind, NodeKind::Intertwine { .. });
|
||||
e.stroke |= match &c.kind {
|
||||
// Type's outlines take its characters' stroke (see `type.createOutlines`).
|
||||
NodeKind::Text(t) => t.runs.iter().any(|r| r.style.has_stroke()),
|
||||
_ => shape && !c.appearance.stroke_paint().is_none(),
|
||||
};
|
||||
e.gradient |= match &c.kind {
|
||||
NodeKind::Text(t) => t.runs.iter().any(|r| matches!(r.style.fill, Paint::Gradient(_))),
|
||||
_ => shape && c.appearance.items.iter().any(|i| gradient_fill(i).is_some()),
|
||||
};
|
||||
e.fill |= e.gradient;
|
||||
e.fill |= match &c.kind {
|
||||
NodeKind::Text(t) => t.runs.iter().any(|r| matches!(&r.style.fill, Paint::Pattern { pattern, .. } if d.pattern(pattern).is_some())),
|
||||
_ => shape && c.appearance.items.iter().any(|i| pattern_fill(d, i).is_some()),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
fn object(&self) -> bool {
|
||||
self.text || self.live || self.effects || self.envelope || self.blends || self.placed || self.repeats || self.intertwines
|
||||
self.text || self.live || self.effects || self.envelope || self.blends || self.placed || self.repeats || self.intertwines || self.symbols
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +245,30 @@ fn expand_steps(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if object && e.placed {
|
||||
let roots = selected_roots(s)?;
|
||||
s.edit("Expand", |d, _| expand_placed_under(d, &roots))?;
|
||||
e = Expandable::of(&s.doc()?.doc, &selected_roots(s)?);
|
||||
}
|
||||
// Then the symbol instances, those in the placed documents' art included: their art expands
|
||||
// below, and the instances it holds stay instances.
|
||||
if object && e.symbols {
|
||||
let roots = selected_roots(s)?;
|
||||
// An edit that expands no instance (too many objects to make) is undone, and the
|
||||
// instances stay as they are.
|
||||
let mut expanded = None;
|
||||
let r = s.edit("Expand", |d, _| {
|
||||
let any = super::brushsym::expand_instances_under(d, &roots)?;
|
||||
expanded = Some(any);
|
||||
if any { Ok(()) } else { Err(EngineError::Other("Expand: nothing to expand".into())) }
|
||||
});
|
||||
if !matches!((&r, expanded), (Err(_), Some(false))) {
|
||||
r?;
|
||||
}
|
||||
e = Expandable::of(&s.doc()?.doc, &selected_roots(s)?);
|
||||
// The placed documents in the instances' art too.
|
||||
if e.placed {
|
||||
let roots = selected_roots(s)?;
|
||||
s.edit("Expand", |d, _| expand_placed_under(d, &roots))?;
|
||||
e = Expandable::of(&s.doc()?.doc, &selected_roots(s)?);
|
||||
}
|
||||
}
|
||||
// Intertwine groups before what they hold: their objects, and the copies of the parts drawn
|
||||
// over the others, expand below.
|
||||
|
||||
Reference in New Issue
Block a user