mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Merge pull request #2622 from odgrim/fix/intertwine-expand-2540
Fix #2540: Object → Expand expands Intertwine groups
This commit is contained in:
@@ -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; blends and repeats become groups of their steps and instances. Placed documents (Object) become editable copies of the
|
||||
//! (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
|
||||
//! 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, 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}",
|
||||
"{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), 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
|
||||
),
|
||||
@@ -108,6 +108,8 @@ struct Expandable {
|
||||
placed: bool,
|
||||
/// Repeats (Object expands them into groups of their instances).
|
||||
repeats: bool,
|
||||
/// Intertwine groups (Object expands them into groups of what they draw).
|
||||
intertwines: bool,
|
||||
}
|
||||
|
||||
impl Expandable {
|
||||
@@ -123,6 +125,7 @@ impl Expandable {
|
||||
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 |= 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(_))),
|
||||
@@ -134,7 +137,7 @@ impl Expandable {
|
||||
}
|
||||
|
||||
fn object(&self) -> bool {
|
||||
self.text || self.live || self.effects || self.envelope || self.blends || self.placed || self.repeats
|
||||
self.text || self.live || self.effects || self.envelope || self.blends || self.placed || self.repeats || self.intertwines
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +165,12 @@ fn expand(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let roots = selected_roots(s)?;
|
||||
s.edit("Expand", |d, _| expand_placed_under(d, &roots))?;
|
||||
}
|
||||
// Intertwine groups before what they hold: their objects, and the copies of the parts drawn
|
||||
// over the others, expand below.
|
||||
if object && e.intertwines {
|
||||
let roots = selected_roots(s)?;
|
||||
s.edit("Expand", |d, _| super::intertwine::expand_intertwines(d, &roots).map(|_| ()))?;
|
||||
}
|
||||
// Blends first: what their steps hold (type, effects, strokes, gradients) expands below.
|
||||
if object && e.blends {
|
||||
let roots = selected_roots(s)?;
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
//! `object.intertwine.arrange` picks overlap areas, by a click (`at`) or by an enclosed path drawn
|
||||
//! around them (`area`), and changes which member is drawn on top there: each member keeps the
|
||||
//! area where it is drawn over the others (`NodeExtras::intertwine`, see
|
||||
//! `vectorcraft_doc::intertwine`).
|
||||
//! `vectorcraft_doc::intertwine`). Object › Expand turns the group into a plain group of what it
|
||||
//! draws ([`expand_intertwines`]), which Ungroup takes apart.
|
||||
//!
|
||||
//! An overlap area is a face of the planar arrangement of what the members paint (their fills and
|
||||
//! strokes, type outlined; other objects by their bounds): the part where two or more of them
|
||||
@@ -186,6 +187,41 @@ fn release(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() }))
|
||||
}
|
||||
|
||||
// ---------- Expand ----------
|
||||
|
||||
/// Replace Intertwine group `id` by a plain group of the art it draws
|
||||
/// ([`vectorcraft_doc::intertwine::expand`]: its objects, then each part drawn over the others as a
|
||||
/// clip group), keeping everything about the group itself; the generated art gets ids of its own.
|
||||
fn expand_group(d: &mut Document, id: NodeId) -> Result<()> {
|
||||
let Some(mut g) = d.node(id).cloned() else { return Ok(()) };
|
||||
let NodeKind::Intertwine { children } = &g.kind else { return Ok(()) };
|
||||
g.kind = NodeKind::Group { children: vectorcraft_doc::intertwine::expand(children).into_iter().map(Arc::new).collect(), clip: false };
|
||||
super::live::fix_ids(d, &mut g);
|
||||
*d.node_mut(id).ok_or(EngineError::NoNode(id))? = g;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Object › Expand: every Intertwine group in the subtrees of `roots` becomes a group of the art
|
||||
/// it draws ([`expand_group`]), inner ones first so an outer one copies plain art. Returns how
|
||||
/// many. A live Intertwine is expanded before it can be ungrouped:
|
||||
/// <https://community.adobe.com/questions-652/expand-ungroup-800281>.
|
||||
pub(crate) fn expand_intertwines(d: &mut Document, roots: &[NodeId]) -> Result<usize> {
|
||||
let mut groups = vec![];
|
||||
for r in roots {
|
||||
if let Some(n) = d.node(*r) {
|
||||
n.walk(&mut |c| {
|
||||
if is_intertwine(c) {
|
||||
groups.push(c.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
for g in groups.iter().rev() {
|
||||
expand_group(d, *g)?;
|
||||
}
|
||||
Ok(groups.len())
|
||||
}
|
||||
|
||||
// ---------- Arrange ----------
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
|
||||
@@ -383,7 +383,7 @@ fn replace_with(d: &mut Document, id: NodeId, nodes: Vec<Node>) -> Result<Vec<No
|
||||
}
|
||||
|
||||
/// Fresh ids for any id-0 (generated) node in `n`'s subtree; keeps real ids.
|
||||
fn fix_ids(d: &mut Document, n: &mut Node) {
|
||||
pub(super) fn fix_ids(d: &mut Document, n: &mut Node) {
|
||||
if n.id == NodeId(0) {
|
||||
n.id = d.alloc_id();
|
||||
}
|
||||
|
||||
@@ -151,6 +151,67 @@ fn release_gives_the_objects_back_as_they_were() {
|
||||
assert!(red(&render(&s), 50, 30));
|
||||
}
|
||||
|
||||
/// #2540: Object › Expand turns the group into a plain group of what it draws (the rings, then the
|
||||
/// part of A drawn over B as a clip group) in one undo step, looking the same; Ungroup then takes
|
||||
/// it apart.
|
||||
#[test]
|
||||
fn expand_turns_the_group_into_plain_art_that_ungroups() {
|
||||
let mut s = session();
|
||||
let (g, a, b) = intertwined(&mut s);
|
||||
arrange(&mut s, json!({"at": [50, 30]}));
|
||||
assert_eq!(s.execute("object.expand.info", &json!({})).unwrap()["object"], json!(true));
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
s.execute("object.expand", &json!({"fill": false, "stroke": false})).unwrap();
|
||||
let n = s.doc().unwrap().doc.node(g).unwrap().clone();
|
||||
let NodeKind::Group { children, clip: false } = &n.kind else { panic!("a group: {}", n.kind_label()) };
|
||||
assert_eq!(children.len(), 3, "the rings, then the part of A drawn over B");
|
||||
assert_eq!((children[0].id, children[1].id), (a, b), "the rings keep their ids and order");
|
||||
assert!(matches!(children[2].kind, NodeKind::Group { clip: true, .. }));
|
||||
assert!(children.iter().all(|c| c.extras().intertwine.is_none()), "no area is left over");
|
||||
let mut ids = vec![];
|
||||
n.walk(&mut |c| ids.push(c.id));
|
||||
let unique: std::collections::HashSet<_> = ids.iter().collect();
|
||||
assert!(unique.len() == ids.len() && !ids.contains(&NodeId(0)), "the generated art has ids of its own: {ids:?}");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
assert_eq!(s.doc().unwrap().selection.objects.to_vec(), [g]);
|
||||
assert_eq!(s.execute("object.expand.info", &json!({})).unwrap()["object"], json!(false), "nothing live is left");
|
||||
let r = render(&s);
|
||||
assert!(red(&r, 50, 30) && blue(&r, 50, 70), "it looks as it did");
|
||||
// Plain art: Ungroup takes it apart.
|
||||
s.execute("object.ungroup", &json!({})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.node(g).is_none());
|
||||
assert_eq!(s.doc().unwrap().selection.objects.len(), 3);
|
||||
let r = render(&s);
|
||||
assert!(red(&r, 50, 30) && blue(&r, 50, 70));
|
||||
// Undo brings the Intertwine group back.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(matches!(s.doc().unwrap().doc.node(g).unwrap().kind, NodeKind::Intertwine { .. }));
|
||||
}
|
||||
|
||||
/// Expand reaches an Intertwine group inside a selected group, and with every option on also
|
||||
/// outlines the strokes of the rings and of the part drawn over the other one.
|
||||
#[test]
|
||||
fn expand_reaches_intertwine_groups_in_groups_and_outlines_their_strokes() {
|
||||
let mut s = session();
|
||||
let (g, ..) = intertwined(&mut s);
|
||||
arrange(&mut s, json!({"at": [50, 30]}));
|
||||
let c = id_of(&s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 5, "height": 5})).unwrap());
|
||||
select(&mut s, &[g, c]);
|
||||
let outer = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
s.execute("object.expand", &json!({})).unwrap();
|
||||
let (mut intertwines, mut strokes) = (0, 0);
|
||||
s.doc().unwrap().doc.node(outer).unwrap().walk(&mut |n| {
|
||||
intertwines += usize::from(matches!(n.kind, NodeKind::Intertwine { .. }));
|
||||
strokes += usize::from(!n.appearance.stroke_paint().is_none());
|
||||
});
|
||||
assert_eq!((intertwines, strokes), (0, 0));
|
||||
let r = render(&s);
|
||||
assert!(red(&r, 50, 30) && blue(&r, 50, 70), "it looks as it did");
|
||||
let info = s.execute("object.expand.info", &json!({})).unwrap();
|
||||
assert_eq!((&info["object"], &info["stroke"]), (&json!(false), &json!(false)), "nothing is left to expand");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_arranged_areas_move_with_the_group() {
|
||||
let mut s = session();
|
||||
|
||||
Reference in New Issue
Block a user