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:
parent
412e83c825
commit
3073f0ab03
|
|
@ -31,6 +31,10 @@ impl Hit {
|
|||
/// The object the Selection tool selects: the child of the layer (the outermost group).
|
||||
/// Inside isolation mode (`scope`), the child of the isolated container instead.
|
||||
pub fn top_object(&self, scope: Option<NodeId>) -> NodeId {
|
||||
// A single object isolated: it is the only object.
|
||||
if scope == Some(self.leaf) {
|
||||
return self.leaf;
|
||||
}
|
||||
// The innermost of the isolated container and an envelope whose contents are edited.
|
||||
let at = |s: Option<NodeId>| s.and_then(|s| self.ancestry.iter().position(|x| *x == s));
|
||||
if let Some(i) = at(scope).max(at(self.contents_of)) {
|
||||
|
|
@ -101,10 +105,16 @@ fn hit_test_skipping(doc: &Document, p: Point, opt: HitOptions, skip: &dyn Fn(No
|
|||
// locked, and clicks pass through it (#833).
|
||||
if editable_only
|
||||
&& let Some(scope) = opt.scope
|
||||
&& let Some(container) = doc.node(scope).filter(|n| n.is_container())
|
||||
&& let Some(isolated) = doc.node(scope)
|
||||
&& let Some(mut chain) = doc.ancestry(scope)
|
||||
{
|
||||
return hit_children(container, p, opt, skip, &mut chain, editable_only).map(finish);
|
||||
if isolated.is_container() {
|
||||
return hit_children(isolated, p, opt, skip, &mut chain, editable_only).map(finish);
|
||||
}
|
||||
// A single object isolated (a path, an image…): only it hits, among its siblings.
|
||||
chain.pop();
|
||||
let parent = chain.last().and_then(|id| doc.node(*id))?;
|
||||
return hit_children(parent, p, opt, &|id| id != scope || skip(id), &mut chain, editable_only).map(finish);
|
||||
}
|
||||
let mut chain = Vec::new();
|
||||
for layer in doc.layers.iter().rev() {
|
||||
|
|
@ -283,7 +293,11 @@ fn hit_any(n: &Node, p: Point, opt: HitOptions) -> Option<HitKind> {
|
|||
pub fn marquee(doc: &Document, r: Rect, scope: Option<NodeId>, leaves: bool) -> Vec<NodeId> {
|
||||
let mut out = Vec::new();
|
||||
let tops: Vec<&std::sync::Arc<Node>> = match scope.and_then(|s| doc.node(s)) {
|
||||
Some(s) => s.children().map(|c| c.iter().collect()).unwrap_or_default(),
|
||||
Some(s) => match s.children() {
|
||||
Some(c) => c.iter().collect(),
|
||||
// A single object isolated: only it.
|
||||
None => doc.parent_of(s.id).and_then(|p| doc.node(p)).and_then(Node::children).into_iter().flatten().filter(|c| c.id == s.id).collect(),
|
||||
},
|
||||
// The objects of the layers, looking through sublayers (hidden, locked and template ones
|
||||
// are left out).
|
||||
None => doc
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -632,7 +632,10 @@ mod tests {
|
|||
let cx2 = cx(&d, &whole, &p);
|
||||
let a = crate::create("selection").pointer(&cx2, &PointerEvent::new(PointerKind::DoubleClick, 190.0, 110.0));
|
||||
assert_eq!(a, vec![Action::Dialog(DIALOG.into(), json!({"id": id.0, "corners": [0, 1, 2, 3]}))]);
|
||||
assert!(crate::create("selection").pointer(&cx2, &PointerEvent::new(PointerKind::DoubleClick, 150.0, 150.0)).is_empty());
|
||||
// Off the widgets, the Selection tool's double-click isolates the shape (Double Click To
|
||||
// Isolate) instead.
|
||||
let a = crate::create("selection").pointer(&cx2, &PointerEvent::new(PointerKind::DoubleClick, 150.0, 150.0));
|
||||
assert_eq!(a, vec![Action::Exec("object.isolate".into(), json!({"id": id.0}))]);
|
||||
}
|
||||
|
||||
/// Hide Corner Widget for angles greater than (#394): a rectangle's right angles hide below
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
//! Guides, or with them off View › Snap to Point, snap it),
|
||||
//! bounding-box scale (Shift proportional, Alt from centre) and rotate (outside corners, Shift 45°),
|
||||
//! drag a live rectangle's or polygon's corner widget to round its corners (Alt-click cycles their kind,
|
||||
//! double-click opens the Corners dialog), double-click to enter isolation mode (Double Click To
|
||||
//! Isolate), Cmd/Ctrl-click to select the object behind (Command Click to Select Objects Behind),
|
||||
//! double-click opens the Corners dialog), double-click a group or an object to enter isolation
|
||||
//! mode (Double Click To Isolate), Cmd/Ctrl-click to select the object behind (Command Click to Select Objects Behind),
|
||||
//! click or drag a ruler guide ([`crate::rulerguide`]), drag the brackets of type on a path
|
||||
//! ([`crate::pathtype`]), double-click the type widget to convert point type to area type and back
|
||||
//! ([`crate::typewidget`]).
|
||||
|
|
@ -260,12 +260,19 @@ impl Tool for SelectionTool {
|
|||
}
|
||||
if let Some(h) = hit_test(cx.doc, p, cx.hit_options()) {
|
||||
let top = h.top_object(cx.isolation);
|
||||
if cx.double_click_isolate && cx.doc.node(top).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Group { .. })) {
|
||||
return vec![Action::Exec("object.isolate".into(), json!({ "id": top.0 }))];
|
||||
}
|
||||
if cx.doc.node(top).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Text(_))) {
|
||||
return vec![Action::SwitchTool("type".into())];
|
||||
}
|
||||
// A group, a compound path or a single path, image, mesh or placed file, not
|
||||
// the one already isolated. Blends, envelopes, repeats and symbol instances
|
||||
// aren't: they have editing modes of their own.
|
||||
use vectorcraft_doc::NodeKind as K;
|
||||
let isolates = cx.doc.node(top).is_some_and(|n| {
|
||||
matches!(n.kind, K::Group { .. } | K::Compound { .. } | K::Path { .. } | K::Image(_) | K::Mesh(_) | K::PlacedDocument(_))
|
||||
});
|
||||
if cx.double_click_isolate && isolates && cx.isolation != Some(top) {
|
||||
return vec![Action::Exec("object.isolate".into(), json!({ "id": top.0 }))];
|
||||
}
|
||||
} else if cx.isolation.is_some() {
|
||||
return vec![Action::Exec("object.exitIsolation".into(), json!({}))];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1159,7 +1159,7 @@ and `layerRows`, the rows highlighted in the panel.
|
|||
`layer.reverse {ids?}`, `layer.template {ids?, on?}`, `layer.hideOthers` / `layer.showAll`,
|
||||
`layer.outlineOthers` / `layer.previewAll`, `layer.lockOthers` / `layer.unlockAll` (Alt-clicking an eye or a lock
|
||||
runs Hide or Lock Others for the row's layer, or Show or Unlock All when the others already are), `layer.pasteRemembersLayers`,
|
||||
`object.isolate {id}` / `object.exitIsolation`.
|
||||
`object.isolate {id}` (a group, a layer or a single object; not type) / `object.exitIsolation`.
|
||||
- **Dialogs and panel state** (control channel and the app's MCP): `ui.layerOptions {ids?}` and `ui.newLayer
|
||||
{sublayer?}` open Layer Options (dialog `layerOptions`: name, color, template, locked, visible, printable, preview,
|
||||
dimImages, dimPercent; OK is one undo step), `ui.layersPanelOptions` opens Panel Options (`layersPanelOptions`:
|
||||
|
|
@ -1572,7 +1572,8 @@ The Selection & Anchor Display and General preferences apply to `pointer_gesture
|
|||
- `ctrlClickSelectsBehind` (on by default): a Selection tool click with `mods: {cmd: true}` (Command on macOS, Ctrl
|
||||
elsewhere) selects the object under the selected one there, the next such click the one under that, then the
|
||||
topmost again. Cmd held to borrow the selection tool from another tool clicks as usual.
|
||||
- `doubleClickToIsolate` (on by default): off, a `doubleclick` on a group with the Selection tool no longer isolates it.
|
||||
- `doubleClickToIsolate` (on by default): a `doubleclick` with the Selection tool isolates a group, a compound path or a
|
||||
single path, image, mesh or placed file (not type, blends, envelopes, repeats or symbol instances); off, it no longer does.
|
||||
- `usePreciseCursors`: the Pen, Eyedropper, Slice and Blend tools' pointers are a crosshair.
|
||||
- `snapToPointTolerance` (1–8 px, 2 by default): with View › Snap to Point on and Smart Guides off (desktop app), the
|
||||
point a selection is dragged by, a drawn point and a transform tool's reference point land on an anchor or a ruler
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ tools, never checked side by side) isn't captured row by row.
|
|||
| Cut path at selected anchors, Remove Anchor Points, Delete removes segments | done | #1278: segment Delete preserves neighboring edges and endpoint anchors; a second Delete removes the remainder; explicit anchor Delete retains its behavior |
|
||||
| Live Corners widgets (radius readout, Alt-click cycles kind, double-click the Corners dialog) | done | a Direct Selection click on a widget selects its corner (#1239); a press-and-drag on a widget of a path selected whole still rounds every corner (#908), and adding widgets to the selection by Shift-click (#1232) is missing |
|
||||
| Corners between curved sides (#1144), Relative rounding | partial | Relative rounding missing |
|
||||
| Isolation mode by double-click, breadcrumbs, Esc to exit | done | |
|
||||
| Isolation mode by double-click (groups and single objects), breadcrumbs, Esc to exit | done | Single paths, compound paths, images, meshes and placed files isolate too ([helpx: Isolate objects](https://helpx.adobe.com/africa/illustrator/desktop/manage-objects/select-objects/isolate-objects.html)) |
|
||||
| "Magnetic" moves of selected anchors with a modifier (#955 request) | missing | not an Illustrator feature as documented; under discussion |
|
||||
|
||||
## Smart guides and snapping
|
||||
|
|
@ -205,6 +205,7 @@ re-measure of the panels added since.
|
|||
| 2026-10-12 | minor | Attributes: directly selected compound members read and change the compound fill rule; path direction stays member-local (#1197); score unchanged |
|
||||
| 2026-10-12 | minor | Pathfinder Divide: open cutting lines split filled paths with one Undo (#1307); scores and hours unchanged |
|
||||
| 2026-10-12 | minor | Numeric entry: click-to-focus selection waits for release so text drags keep their start (#1326, in part); keyboard focus, double-click, arrows and wheel preserved; scores and hours unchanged; short movements below the click threshold remain open |
|
||||
| 2026-10-12 | minor | Double Click To Isolate: the Selection tool also isolates a single path, compound path, image, mesh or placed file; only it is hit, marqueed and selected inside; scores and hours unchanged |
|
||||
| 2026-10-11 | minor | A marquee inside a filled shape no longer selects it: only touching its path does (#1103); score unchanged (estimated) |
|
||||
| 2026-10-11 | minor | Live Corners: corners between curved sides round too, the curve trimmed to what the cut leaves (#1144); score unchanged (estimated) |
|
||||
| 2026-10-11 | minor | Artboards: Shift-click selects several, which move, copy and delete together (#1139); score unchanged (estimated) |
|
||||
|
|
|
|||
Loading…
Reference in New Issue