mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Fix #1756: Object → Expand expands repeats
Expand had nothing for live repeats, so a radial, grid or mirror repeat stayed live. Its Object option now turns every repeat in the selection (inside groups too, inner ones first) into a group of its instances, keeping the repeat's id and place, in the same undo step; the dialog enables Object for a repeat. Expand Repeat shares the code.
This commit is contained in:
1 parent
3024821c32
commit
2f4cc8a1cc
3 files changed
+95
-19
No files matched your search
@@ -1,5 +1,5 @@
|
||||
//! Object → Expand: live shapes, type and effects (Object), strokes (Stroke) and gradient fills
|
||||
//! (Fill) into plain art in one undo step. Placed documents (Object) become editable copies of the
|
||||
//! (Fill) into plain art in one undo step; blends and repeats become groups of their steps and instances. Placed documents (Object) become editable copies of the
|
||||
//! art they show, their resources joining the document. 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. Create Gradient Mesh shares the sampling.
|
||||
@@ -37,7 +37,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Expand…",
|
||||
["Object"],
|
||||
None,
|
||||
"{object?: true (type → outlines, live shapes → paths, effects baked, blends → groups of their steps, 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), stroke?: true (strokes → filled outlines), 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 (type → outlines, live shapes → paths, effects baked, blends → groups of their steps, repeats → groups of their instances, 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), stroke?: true (strokes → filled outlines), 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
|
||||
),
|
||||
@@ -106,6 +106,8 @@ struct Expandable {
|
||||
blends: bool,
|
||||
/// Placed documents (Object expands them into the art they show).
|
||||
placed: bool,
|
||||
/// Repeats (Object expands them into groups of their instances).
|
||||
repeats: bool,
|
||||
}
|
||||
|
||||
impl Expandable {
|
||||
@@ -120,6 +122,7 @@ impl Expandable {
|
||||
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.stroke |= shape && !c.appearance.stroke_paint().is_none();
|
||||
e.fill |= match &c.kind {
|
||||
NodeKind::Text(t) => t.runs.iter().any(|r| matches!(r.style.fill, Paint::Gradient(_))),
|
||||
@@ -131,7 +134,7 @@ impl Expandable {
|
||||
}
|
||||
|
||||
fn object(&self) -> bool {
|
||||
self.text || self.live || self.effects || self.envelope || self.blends || self.placed
|
||||
self.text || self.live || self.effects || self.envelope || self.blends || self.placed || self.repeats
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +167,11 @@ fn expand(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let roots = selected_roots(s)?;
|
||||
s.edit("Expand", |d, _| super::live::expand_blends(d, &roots).map(|_| ()))?;
|
||||
}
|
||||
// Repeats too: their instances are copies of art that expands below.
|
||||
if object && e.repeats {
|
||||
let roots = selected_roots(s)?;
|
||||
s.edit("Expand", |d, _| super::patterncmds::expand_repeats(d, &roots).map(|_| ()))?;
|
||||
}
|
||||
if object && e.effects {
|
||||
let _ = run_raw(s, "effect.expandAppearance", &json!({}));
|
||||
}
|
||||
|
||||
@@ -708,28 +708,50 @@ fn fix_ids(d: &mut Document, n: &mut Node) {
|
||||
fn repeat_expand(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let reps = selected_repeats(s);
|
||||
let ids = s.edit("Expand Repeat", |d, sel| {
|
||||
let mut out = vec![];
|
||||
for r in &reps {
|
||||
let Some(n) = d.node(*r).cloned() else { continue };
|
||||
let mut g = vectorcraft_doc::live::expanded_group(&n, None);
|
||||
|
||||
// The group keeps the repeat's id; every generated instance node gets a fresh one.
|
||||
if let Some(ch) = g.children_mut() {
|
||||
for c in ch.iter_mut() {
|
||||
fix_ids(d, Arc::make_mut(c));
|
||||
}
|
||||
}
|
||||
let (par, idx, _) = d.position(*r).ok_or(EngineError::NoNode(*r))?;
|
||||
d.remove(*r)?;
|
||||
d.insert(par, idx, g)?;
|
||||
out.push(*r);
|
||||
expand_repeat(d, *r)?;
|
||||
}
|
||||
sel.set(out.iter().copied());
|
||||
Ok(out)
|
||||
sel.set(reps.iter().copied());
|
||||
Ok(reps.clone())
|
||||
})?;
|
||||
Ok(ids_json(&ids))
|
||||
}
|
||||
|
||||
/// Repeat `r` replaced by a group of its instances, keeping its id and place; every generated
|
||||
/// instance node gets a fresh id.
|
||||
fn expand_repeat(d: &mut Document, r: NodeId) -> Result<()> {
|
||||
let Some(n) = d.node(r).cloned() else { return Ok(()) };
|
||||
let mut g = vectorcraft_doc::live::expanded_group(&n, None);
|
||||
if let Some(ch) = g.children_mut() {
|
||||
for c in ch.iter_mut() {
|
||||
fix_ids(d, Arc::make_mut(c));
|
||||
}
|
||||
}
|
||||
let (par, idx, _) = d.position(r).ok_or(EngineError::NoNode(r))?;
|
||||
d.remove(r)?;
|
||||
d.insert(par, idx, g)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Object › Expand: every repeat in the subtrees of `roots` becomes a group of its instances
|
||||
/// ([`expand_repeat`]), inner ones first so an outer repeat repeats plain art. Returns how many.
|
||||
pub(crate) fn expand_repeats(d: &mut Document, roots: &[NodeId]) -> Result<usize> {
|
||||
let mut reps = vec![];
|
||||
for r in roots {
|
||||
if let Some(n) = d.node(*r) {
|
||||
n.walk(&mut |c| {
|
||||
if pattern::is_repeat(c) {
|
||||
reps.push(c.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
for r in reps.iter().rev() {
|
||||
expand_repeat(d, *r)?;
|
||||
}
|
||||
Ok(reps.len())
|
||||
}
|
||||
|
||||
fn repeat_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "object.repeat.options";
|
||||
let reps = selected_repeats(s);
|
||||
|
||||
@@ -390,6 +390,52 @@ fn expand_yields_n_copies() {
|
||||
assert_eq!(u.len(), ids.len());
|
||||
}
|
||||
|
||||
/// #1756: Object → Expand turns radial, grid and mirror repeats into groups of their instances, in
|
||||
/// one undo step, as Expand Repeat does.
|
||||
#[test]
|
||||
fn object_expand_expands_repeats() {
|
||||
for (cmd, opts) in [("object.repeat.radial", json!({"instances": 5})), ("object.repeat.grid", json!({})), ("object.repeat.mirror", json!({}))] {
|
||||
let mut s = session();
|
||||
let r = rect(&mut s, 195.0, 95.0, 10.0, 10.0);
|
||||
sel(&mut s, &[r]);
|
||||
let id = id_of(&s.execute(cmd, &opts).unwrap());
|
||||
let NodeKind::Repeat(spec) = node(&s, id).kind else { panic!("{cmd}") };
|
||||
let n = spec.expand().len();
|
||||
assert_eq!(s.execute("object.expand.info", &json!({})).unwrap()["object"], json!(true), "{cmd}");
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
s.execute("object.expand", &json!({"fill": false, "stroke": false})).unwrap();
|
||||
let g = node(&s, id);
|
||||
assert!(matches!(g.kind, NodeKind::Group { .. }), "{cmd}");
|
||||
assert_eq!(g.children().unwrap().len(), n, "{cmd}");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "{cmd}");
|
||||
assert_eq!(s.doc().unwrap().selection.objects, vec![id], "{cmd}");
|
||||
// Nothing left to expand: the instances are plain rectangles.
|
||||
assert_eq!(s.execute("object.expand.info", &json!({})).unwrap()["object"], json!(false), "{cmd}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand reaches repeats inside a selected group, and with every option on also outlines the
|
||||
/// instances' strokes.
|
||||
#[test]
|
||||
fn object_expand_expands_repeats_in_groups() {
|
||||
let mut s = session();
|
||||
let r = rect(&mut s, 195.0, 95.0, 10.0, 10.0);
|
||||
sel(&mut s, &[r]);
|
||||
let rep = id_of(&s.execute("object.repeat.radial", &json!({"instances": 4})).unwrap());
|
||||
let other = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[rep, other]);
|
||||
s.execute("object.group", &json!({})).unwrap();
|
||||
s.execute("object.expand", &json!({})).unwrap();
|
||||
let mut repeats = 0;
|
||||
for l in &doc(&s).layers {
|
||||
l.walk(&mut |c| repeats += usize::from(matches!(c.kind, NodeKind::Repeat(_))));
|
||||
}
|
||||
assert_eq!(repeats, 0);
|
||||
let g = node(&s, rep);
|
||||
assert_eq!(g.children().unwrap().len(), 4);
|
||||
assert!(g.children().unwrap().iter().all(|c| c.appearance.stroke_paint().is_none()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grid_repeat_and_options() {
|
||||
let mut s = session();
|
||||
|
||||
Reference in new issue
Block a user