Isolation mode: double-clicking a single object isolates it

With Double Click To Isolate on, the Selection tool isolated groups only. A
double-click on a single path, compound path, image, mesh or placed file now
isolates it too, as the public documentation describes. Type still switches to
the Type tool; blends, envelopes, repeats and symbol instances keep their own
editing modes.

While a single object is isolated only it is hit, marqueed and taken by Select
All, new art goes beside it in its group or layer, and a double-click on the
empty canvas leaves with it selected. object.isolate accepts any object except
type.
This commit is contained in:
Ayberk A.
2026-10-12 18:56:41 +07:00
committed by bflatastic
parent 412e83c825
commit 3073f0ab03
9 changed files with 104 additions and 15 deletions
+4 -2
View File
@@ -857,8 +857,10 @@ fn isolate(s: &mut Session, p: &Value) -> Result<Value> {
.or_else(|| s.active().and_then(|d| d.selection.objects.first().copied()))
.ok_or_else(|| bad("object.isolate", "missing id"))?;
let st = s.doc_mut()?;
if st.doc.node(id).is_none_or(|n| !n.is_container()) {
return Err(bad("object.isolate", "only groups and layers can be isolated"));
// Groups, layers and single objects (a path, a compound path, an image…); type isn't
// isolated: a double-click on it edits its text.
if st.doc.node(id).is_none_or(|n| matches!(n.kind, NodeKind::Text(_))) {
return Err(bad("object.isolate", "type objects can't be isolated"));
}
let isolation = st.doc.node(id).filter(|n| n.extras().shaper.is_some()).and_then(|n| n.children()).and_then(|c| c.first()).map_or(id, |n| n.id);
st.isolation = Some(isolation);
+2 -1
View File
@@ -193,7 +193,8 @@ pub fn specs() -> Vec<CommandSpec> {
fn selectable(d: &Document, iso: Option<NodeId>) -> Vec<NodeId> {
match iso.and_then(|i| d.node(i)) {
Some(c) if d.ancestry(c.id).is_some_and(|a| a.iter().all(|i| d.node(*i).is_some_and(|n| n.visible && !n.locked && !n.is_template()))) => {
c.layer_art(true)
// A single object isolated is all there is to select.
if c.is_container() { c.layer_art(true) } else { vec![c.id] }
}
Some(_) => vec![],
None => d.selectable_art(),
+6
View File
@@ -483,6 +483,12 @@ impl DocState {
{
return Some(i);
}
// A single object isolated: new art goes beside it, in its group or layer.
if let Some(p) = self.isolation.filter(|i| self.doc.node(*i).is_some_and(|n| !n.is_container())).and_then(|i| self.doc.parent_of(i))
&& self.doc.node(p).is_some_and(|n| matches!(n.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }))
{
return Some(p);
}
// A sublayer takes new art only while it and the layers around it are shown and unlocked.
self.active_layer.filter(|l| self.doc.node(*l).is_some_and(|n| n.is_layer()) && self.doc.is_editable(*l)).or_else(|| self.doc.default_layer())
}
+54
View File
@@ -298,6 +298,60 @@ fn selection_preferences_drive_the_selection_tool() {
assert!(double(&mut s).is_some(), "on: the group is isolated");
}
/// Double Click To Isolate: a double-click with the Selection tool isolates a single object too,
/// not only a group. Inside, only it is hit, marqueed and selected by Select All; new art goes
/// beside it; a double-click on the empty canvas leaves, with it selected. Type isn't isolated:
/// a double-click on it switches to the Type tool.
#[test]
fn double_clicking_a_single_object_isolates_it() {
use vectorcraft_tools::{PointerEvent, PointerKind};
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
let id = |v: Value| NodeId(v["id"].as_u64().unwrap());
let rect = |s: &mut Session, x: f64| id(s.execute("shape.rectangle", &json!({"x": x, "y": 100, "width": 50, "height": 50})).unwrap());
let (a, b) = (rect(&mut s, 100.0), rect(&mut s, 200.0));
s.select_tool("selection", ViewInfo::default()).unwrap();
let ev = |s: &mut Session, kinds: &[PointerKind], x: f64, y: f64| {
for k in kinds {
s.pointer(&PointerEvent::new(*k, x, y), ViewInfo::default()).unwrap();
}
};
let click = [PointerKind::Down, PointerKind::Up];
let double = [PointerKind::DoubleClick];
ev(&mut s, &double, 125.0, 125.0);
assert_eq!(s.doc().unwrap().isolation, Some(a));
// Only the isolated object is hit: a click on the other selects nothing.
ev(&mut s, &click, 225.0, 125.0);
assert!(s.doc().unwrap().selection.objects.is_empty());
ev(&mut s, &click, 125.0, 125.0);
assert_eq!(s.doc().unwrap().selection.objects, vec![a]);
// A marquee over both and Select All take it alone.
s.execute("select.none", &json!({})).unwrap();
ev(&mut s, &[PointerKind::Down], 90.0, 90.0);
ev(&mut s, &[PointerKind::Move, PointerKind::Up], 300.0, 300.0);
assert_eq!(s.doc().unwrap().selection.objects, vec![a]);
s.execute("select.none", &json!({})).unwrap();
s.execute("select.all", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().selection.objects, vec![a]);
// A second double-click on it stays in isolation; new art lands on its layer.
ev(&mut s, &double, 125.0, 125.0);
assert_eq!(s.doc().unwrap().isolation, Some(a));
let c = rect(&mut s, 300.0);
let st = s.doc().unwrap();
assert_eq!(st.doc.parent_of(c), st.doc.parent_of(a));
// A double-click on the empty canvas leaves isolation, the object selected.
ev(&mut s, &double, 50.0, 350.0);
let st = s.doc().unwrap();
assert_eq!((st.isolation, st.selection.objects.clone()), (None, vec![a]));
assert!(st.doc.node(b).is_some());
// Type is not isolated.
let t = id(s.execute("text.create", &json!({"x": 100, "y": 300, "text": "Type"})).unwrap());
assert!(s.execute("object.isolate", &json!({"id": t.0})).is_err());
s.execute("prefs.set", &json!({"key": "doubleClickToIsolate", "value": false})).unwrap();
ev(&mut s, &double, 225.0, 125.0);
assert_eq!(s.doc().unwrap().isolation, None, "off: no isolation");
}
/// General › Use Precise Cursors (#394): the Pen's pointer becomes a crosshair; the Selection
/// tool's arrow stays.
#[test]