mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 21:57:12 -06:00
Fix #4928: Arrange Tools: Group Overlapping Objects
addon.arrange.groupOverlapping groups the selected top-level objects whose visual bounds overlap or touch, chains included (union-find over a sweep along x, at most 20000 objects), only within one layer or group. Each group takes its frontmost member's stacking place; one undo step, all groups made in one pass over the tree. Menu item, tests, sweep case, catalog rows and docs.
This commit is contained in:
@@ -2,14 +2,17 @@
|
||||
//! out in rows and columns, a row or a column; Swap Objects swaps the places (and sizes) of two;
|
||||
//! Replace with Top Object puts a copy of the topmost selected object in place of each of the
|
||||
//! others; and Set Size of Each gives every selected object an exact width and height. Places and
|
||||
//! sizes are measured and set as `object.setBounds` does. Its submenu (Object › Plug-ins › Arrange
|
||||
//! sizes are measured and set as `object.setBounds` does. Group Overlapping Objects groups the
|
||||
//! objects whose visual bounds overlap. Its submenu (Object › Plug-ins › Arrange
|
||||
//! Tools), its dialogs and Zoom to Selection (a view command) are the UI's.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{NodeId, OrientedBox};
|
||||
use vectorcraft_doc::{Node, NodeId, OrientedBox};
|
||||
use vectorcraft_geom::{Affine, Point, Rect};
|
||||
use vectorcraft_render::effects;
|
||||
|
||||
@@ -25,6 +28,8 @@ const LIMIT: f64 = 1e6;
|
||||
const LEAST: f64 = 0.001;
|
||||
/// The most columns Arrange in Grid takes.
|
||||
const MAX_COLUMNS: u64 = 10_000;
|
||||
/// The most objects Group Overlapping Objects looks at.
|
||||
const MAX_OBJECTS: usize = 20_000;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
@@ -64,6 +69,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_selection,
|
||||
size
|
||||
),
|
||||
cmd!(
|
||||
"addon.arrange.groupOverlapping",
|
||||
"Group Overlapping Objects",
|
||||
["Object", "Plug-ins", "Arrange Tools"],
|
||||
None,
|
||||
"{} group the selected objects (the top-level ones, at most 20000) wherever their visual bounds overlap or touch, chains included (when A overlaps B and B overlaps C, all three go in one group), as one undo step. Only objects in the same layer or group are grouped together, objects that overlap nothing stay as they are, and each new group goes where its frontmost member was in the stacking order; the new groups and the objects left alone stay selected → {groups: [id…], grouped (how many objects went into groups)}",
|
||||
has_multi,
|
||||
group_overlapping
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -270,3 +284,75 @@ fn size(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
transform_each(s, "Set Size of Each", strokes, &xfs)?;
|
||||
Ok(json!({ "count": xfs.len() }))
|
||||
}
|
||||
|
||||
/// The sets of two or more of `items` (each in the order given) whose rects overlap or touch,
|
||||
/// chains included, among items of the same key (parent). A sweep along x: only the rects still
|
||||
/// open where each one starts are compared with it.
|
||||
fn overlapping<K: PartialEq>(items: &[(K, Rect)]) -> Vec<Vec<usize>> {
|
||||
fn root(up: &mut [usize], mut i: usize) -> usize {
|
||||
while up[i] != i {
|
||||
up[i] = up[up[i]];
|
||||
i = up[i];
|
||||
}
|
||||
i
|
||||
}
|
||||
let mut up: Vec<usize> = (0..items.len()).collect();
|
||||
let mut order: Vec<usize> = (0..items.len()).collect();
|
||||
order.sort_by(|a, b| items[*a].1.x0.total_cmp(&items[*b].1.x0));
|
||||
let mut open: Vec<usize> = vec![];
|
||||
for i in order {
|
||||
let (key, r) = (&items[i].0, items[i].1);
|
||||
open.retain(|&j| items[j].1.x1 >= r.x0);
|
||||
for &j in &open {
|
||||
let (k, o) = (&items[j].0, items[j].1);
|
||||
if k == key && o.y0 <= r.y1 && r.y0 <= o.y1 {
|
||||
let (a, b) = (root(&mut up, i), root(&mut up, j));
|
||||
up[a] = b;
|
||||
}
|
||||
}
|
||||
open.push(i);
|
||||
}
|
||||
let mut sets: Vec<Vec<usize>> = vec![vec![]; items.len()];
|
||||
for i in 0..items.len() {
|
||||
let r = root(&mut up, i);
|
||||
sets[r].push(i);
|
||||
}
|
||||
sets.retain(|s| s.len() > 1);
|
||||
sets.sort_by_key(|s| s.first().copied());
|
||||
sets
|
||||
}
|
||||
|
||||
fn group_overlapping(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
const C: &str = "addon.arrange.groupOverlapping";
|
||||
let roots = selected_roots(s)?;
|
||||
if roots.len() > MAX_OBJECTS {
|
||||
return Err(bad(C, "too many objects selected (at most 20000)"));
|
||||
}
|
||||
let d = &s.doc()?.doc;
|
||||
let items: Vec<(NodeId, (Option<NodeId>, Rect))> =
|
||||
roots.iter().filter_map(|id| Some((*id, (d.parent_of(*id), effects::preview::bounds_of(d, &[*id], true)?)))).collect();
|
||||
let keyed: Vec<(Option<NodeId>, Rect)> = items.iter().map(|(_, k)| *k).collect();
|
||||
let sets: Vec<Vec<NodeId>> = overlapping(&keyed).into_iter().map(|set| set.iter().filter_map(|i| Some(items.get(*i)?.0)).collect()).collect();
|
||||
if sets.is_empty() {
|
||||
return Ok(json!({ "groups": [], "grouped": 0 }));
|
||||
}
|
||||
let grouped: std::collections::HashSet<NodeId> = sets.iter().flatten().copied().collect();
|
||||
let groups = s.edit("Group Overlapping Objects", |d, sel| {
|
||||
// Every group at once: the members behind each front one go, then each front one is
|
||||
// replaced by its group (one pass over the tree each, however many groups).
|
||||
let (mut places, mut behind, mut groups) = (vec![], vec![], vec![]);
|
||||
for set in &sets {
|
||||
let Some((&front, rest)) = set.split_last() else { continue };
|
||||
let children: Vec<Arc<Node>> = set.iter().filter_map(|id| d.node(*id).cloned().map(Arc::new)).collect();
|
||||
let gid = d.alloc_id();
|
||||
places.push((front, vec![Node::group(gid, children)]));
|
||||
behind.extend_from_slice(rest);
|
||||
groups.push(gid);
|
||||
}
|
||||
d.remove_many(&behind)?;
|
||||
d.insert_beside(places, true)?;
|
||||
sel.set(groups.iter().copied().chain(roots.iter().copied().filter(|id| !grouped.contains(id))));
|
||||
Ok(groups)
|
||||
})?;
|
||||
Ok(json!({ "groups": groups.iter().map(|i| i.0).collect::<Vec<_>>(), "grouped": grouped.len() }))
|
||||
}
|
||||
|
||||
@@ -256,8 +256,83 @@ fn off_without_a_selection() {
|
||||
let mut s = session();
|
||||
rect(&mut s, 0.0, 0.0, 20.0, 10.0);
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
for c in ["addon.arrange.grid", "addon.arrange.swap", "addon.arrange.replace", "addon.arrange.size"] {
|
||||
for c in ["addon.arrange.grid", "addon.arrange.swap", "addon.arrange.replace", "addon.arrange.size", "addon.arrange.groupOverlapping"] {
|
||||
assert!(!enabled(&s, c), "{c}");
|
||||
}
|
||||
assert!(!enabled(&Session::new(), "addon.arrange.swap"));
|
||||
}
|
||||
|
||||
fn ids_of(v: &Value) -> Vec<NodeId> {
|
||||
v.as_array().unwrap().iter().map(|i| NodeId(i.as_u64().unwrap())).collect()
|
||||
}
|
||||
|
||||
fn children(s: &Session, id: NodeId) -> Vec<NodeId> {
|
||||
s.doc().unwrap().doc.node(id).unwrap().children().unwrap().iter().map(|n| n.id).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_overlapping_groups_chains_and_leaves_loners() {
|
||||
let mut s = session();
|
||||
// a–b–c a chain (c only touches b), d alone, e over f.
|
||||
let a = rect(&mut s, 0.0, 0.0, 20.0, 20.0);
|
||||
let d = rect(&mut s, 200.0, 0.0, 10.0, 10.0);
|
||||
let b = rect(&mut s, 15.0, 15.0, 20.0, 20.0);
|
||||
let e = rect(&mut s, 100.0, 100.0, 30.0, 30.0);
|
||||
let c = rect(&mut s, 35.0, 0.0, 10.0, 20.0);
|
||||
let f = rect(&mut s, 110.0, 110.0, 5.0, 5.0);
|
||||
select(&mut s, &[a, b, c, d, e, f]);
|
||||
let n = steps(&s);
|
||||
let r = s.execute("addon.arrange.groupOverlapping", &json!({})).unwrap();
|
||||
assert_eq!(steps(&s), n + 1, "one undo step");
|
||||
assert_eq!(r["grouped"], json!(5));
|
||||
let groups = ids_of(&r["groups"]);
|
||||
assert_eq!(groups.len(), 2);
|
||||
assert_eq!(children(&s, groups[0]), [a, b, c], "members keep their stacking order");
|
||||
assert_eq!(children(&s, groups[1]), [e, f]);
|
||||
// Each group where its frontmost member was: a d [abc] [ef] → d, group(abc) above it, then (ef).
|
||||
let layer: Vec<NodeId> = s.doc().unwrap().doc.layers[0].children().unwrap().iter().map(|n| n.id).collect();
|
||||
assert_eq!(layer, [d, groups[0], groups[1]]);
|
||||
let sel = &s.doc().unwrap().selection.objects;
|
||||
assert!(sel.contains(&groups[0]) && sel.contains(&groups[1]) && sel.contains(&d) && sel.len() == 3, "{sel:?}");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.layers[0].children().unwrap().len(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_overlapping_keeps_layers_apart_and_measures_visual_bounds() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 0.0, 0.0, 20.0, 20.0);
|
||||
let gap = rect(&mut s, 22.0, 0.0, 20.0, 20.0);
|
||||
s.execute("layer.new", &json!({})).unwrap();
|
||||
let other = rect(&mut s, 10.0, 10.0, 20.0, 20.0);
|
||||
select(&mut s, &[a, gap, other]);
|
||||
// 2 pt apart: nothing overlaps within a layer, and a and `other` are in different layers.
|
||||
let n = steps(&s);
|
||||
let r = s.execute("addon.arrange.groupOverlapping", &json!({})).unwrap();
|
||||
assert_eq!((r["grouped"].clone(), steps(&s)), (json!(0), n), "no groups, no step");
|
||||
// A 4 pt stroke reaches 2 pt out: the two in the first layer now touch.
|
||||
select(&mut s, &[a, gap]);
|
||||
s.execute("paint.setStroke", &json!({"color": "#000000"})).unwrap();
|
||||
s.execute("stroke.set", &json!({"weight": 4})).unwrap();
|
||||
select(&mut s, &[a, gap, other]);
|
||||
let r = s.execute("addon.arrange.groupOverlapping", &json!({})).unwrap();
|
||||
let groups = ids_of(&r["groups"]);
|
||||
assert_eq!(groups.len(), 1);
|
||||
assert_eq!(children(&s, groups[0]), [a, gap]);
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
assert_ne!(doc.layer_of(other), doc.layer_of(a), "left in its own layer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_overlapping_is_off_with_one_object_and_scales_to_many() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 0.0, 0.0, 20.0, 20.0);
|
||||
select(&mut s, &[a]);
|
||||
assert!(!enabled(&s, "addon.arrange.groupOverlapping"));
|
||||
// A long row of pairs: each pair overlaps, neighbouring pairs don't.
|
||||
let rects: Vec<(usize, Rect)> =
|
||||
(0..4000).map(|i| (0, Rect::new(i as f64 * 10.0, 0.0, i as f64 * 10.0 + if i % 2 == 0 { 12.0 } else { 3.0 }, 5.0))).collect();
|
||||
let sets = super::overlapping(&rects);
|
||||
assert_eq!(sets.len(), 2000);
|
||||
assert!(sets.iter().all(|s| s.len() == 2 && s[1] == s[0] + 1));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user