Shaper: the Lasso, Smart Guides and the anchor tools skip a Shaper Group's original shapes

A Shaper Group keeps its original shapes as its first child, at no
opacity, and clicks do not pick them outside Construction mode. The
Lasso, the Add Anchor Point, Delete Anchor Point, Anchor Point and
Scissors tools still picked their anchor points and segments, also
where the group erased the face, and Smart Guides, Snap to Point and
the transform tools' reference point snapped to them. The Lasso and
the anchor tools also picked art outside the isolated group or object
in isolation mode.

The Lasso and the anchor tools now visit only the art a click can pick:
the isolated object in isolation mode, and a Shaper Group without its
original shapes unless they are isolated (Construction mode). Smart
Guides and Snap to Point leave the original shapes out of their
targets, and Spacing Guides leave out their bounds when a face moves.
The transform tools' reference point snaps to an original shape only
when that shape is selected, or under the pointer in Construction
mode. The Shaper Group test document moves into the shared test
helpers.

Docs: docs/shaper.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-18 12:34:37 +07:00
committed by bflatastic
parent 22ce516e2d
commit 7d56260b47
8 changed files with 149 additions and 37 deletions
+26 -2
View File
@@ -204,11 +204,35 @@ pub fn polyline(pts: &[Point]) -> BezPath {
bp bp
} }
/// Editable path nodes, selected ones first, then the rest top-most first. /// Visit `n` and everything inside it, depth first in paint order, but a Shaper Group's original
/// shapes (its first child), which are not shown.
pub(crate) fn walk_shown<'a>(n: &'a vectorcraft_doc::Node, f: &mut impl FnMut(&'a vectorcraft_doc::Node)) {
f(n);
for c in n.children().into_iter().flatten().skip(usize::from(n.extras().shaper.is_some())) {
walk_shown(c, f);
}
}
/// Visit the nodes the anchor tools and the Lasso may pick, bottom to top ([`walk_shown`]). In
/// isolation mode that is the isolated object and its contents, so in Construction mode, where a
/// Shaper Group's original shapes are isolated, it visits them.
pub(crate) fn walk_pickable<'a>(cx: &ToolContext<'a>, mut f: impl FnMut(&'a vectorcraft_doc::Node)) {
if let Some(id) = cx.isolation
&& let Some(n) = cx.doc.node(id)
{
walk_shown(n, &mut f);
} else if cx.isolation.is_none() {
for l in &cx.doc.layers {
walk_shown(l, &mut f);
}
}
}
/// Editable path nodes, selected ones first, then the rest top-most first ([`walk_pickable`]).
fn candidate_paths(cx: &ToolContext) -> Vec<NodeId> { fn candidate_paths(cx: &ToolContext) -> Vec<NodeId> {
let mut v: Vec<NodeId> = cx.selection.objects.iter().copied().filter(|id| cx.doc.node(*id).is_some_and(|n| n.path_data().is_some())).collect(); let mut v: Vec<NodeId> = cx.selection.objects.iter().copied().filter(|id| cx.doc.node(*id).is_some_and(|n| n.path_data().is_some())).collect();
let mut rest = vec![]; let mut rest = vec![];
cx.doc.walk(|n| { walk_pickable(cx, |n| {
if let NodeKind::Path { guide: false, .. } = &n.kind { if let NodeKind::Path { guide: false, .. } = &n.kind {
rest.push(n.id); rest.push(n.id);
} }
+32
View File
@@ -715,6 +715,38 @@ mod tests {
); );
} }
/// The Add Anchor Point, Delete Anchor Point, Anchor Point and Scissors tools pick anchors and
/// segments only on the art a click can pick: not on a Shaper Group's original shapes unless
/// they are isolated (Construction mode), and in isolation mode only on the isolated object.
#[test]
fn anchor_tools_leave_out_shaper_sources_and_art_outside_isolation() {
let (d, _, sources, face) = doc_with_shaper_group();
let rect = d.layers[0].children().unwrap()[0].id;
let b = d.node(sources).and_then(|n| n.children()).unwrap()[1].id;
let (s, p) = (Selection::default(), paint());
// The path the first command of a press with `tool` at (x, y) acts on.
let picked = |isolation: Option<NodeId>, tool: &str, x: f64, y: f64| {
let cx = ToolContext { isolation, ..cx(&d, &s, &p) };
let mut t = AnchorTool::new(tool);
let mut a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, x, y));
a.extend(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, x + 20.0, y - 10.0)));
a.iter().find_map(|a| match a {
Action::Exec(_, v) | Action::Preview(_, v) => v["id"].as_u64().map(NodeId),
_ => None,
})
};
// The second square's top right corner and its right side, which only its original shape
// has.
for (tool, x, y) in [("deleteAnchor", 150.0, 300.0), ("anchorPoint", 150.0, 300.0), ("addAnchor", 150.0, 350.0), ("scissors", 150.0, 350.0)] {
assert_eq!(picked(None, tool, x, y), None, "{tool}");
assert_eq!(picked(Some(sources), tool, x, y), Some(b), "{tool}");
}
// The face's top left corner, outside the isolated rectangle.
assert_eq!(picked(None, "deleteAnchor", 0.0, 300.0), Some(face));
assert_eq!(picked(Some(rect), "deleteAnchor", 0.0, 300.0), None);
assert_eq!(picked(Some(rect), "deleteAnchor", 100.0, 100.0), Some(rect));
}
#[test] #[test]
fn anchor_point_drag_pulls_handles_click_converts() { fn anchor_point_drag_pulls_handles_click_converts() {
let (d, id) = doc_with_rect(); let (d, id) = doc_with_rect();
+33 -5
View File
@@ -137,12 +137,13 @@ fn within<T>(sorted: &[T], key: impl Fn(&T) -> f64, v: f64, tol: f64) -> &[T] {
} }
/// Visit `nodes` and everything inside them, but the nodes in `skip` and everything inside those: /// Visit `nodes` and everything inside them, but the nodes in `skip` and everything inside those:
/// one pass, however many are skipped. /// one pass, however many are skipped. A Shaper Group's original shapes, its first child, are
/// not shown and are skipped too.
fn walk_but<'a>(nodes: &'a [Arc<Node>], skip: &HashSet<NodeId>, f: &mut impl FnMut(&'a Node)) { fn walk_but<'a>(nodes: &'a [Arc<Node>], skip: &HashSet<NodeId>, f: &mut impl FnMut(&'a Node)) {
for n in nodes.iter().filter(|n| !skip.contains(&n.id)) { for n in nodes.iter().filter(|n| !skip.contains(&n.id)) {
f(n); f(n);
if let Some(ch) = n.children() { if let Some(ch) = n.children() {
walk_but(ch, skip, f); walk_but(ch.get(usize::from(n.extras().shaper.is_some())..).unwrap_or_default(), skip, f);
} }
} }
} }
@@ -159,7 +160,7 @@ fn span(r: &Rect, x: bool) -> (f64, f64) {
/// The objects a moved selection (`moving`) spaces itself evenly with, by their bounds: each /// The objects a moved selection (`moving`) spaces itself evenly with, by their bounds: each
/// visible object on a layer as a whole (a group too), and inside a group holding some of the /// visible object on a layer as a whole (a group too), and inside a group holding some of the
/// moving objects its other objects. At most [`BUDGET`]. /// moving objects its other objects, but a Shaper Group's original shapes. At most [`BUDGET`].
fn object_boxes(doc: &Document, moving: &[NodeId]) -> Vec<Rect> { fn object_boxes(doc: &Document, moving: &[NodeId]) -> Vec<Rect> {
fn push(nodes: &[Arc<Node>], moving: &HashSet<NodeId>, holders: &HashSet<NodeId>, out: &mut Vec<Rect>) { fn push(nodes: &[Arc<Node>], moving: &HashSet<NodeId>, holders: &HashSet<NodeId>, out: &mut Vec<Rect>) {
for n in nodes.iter().filter(|n| n.visible && !moving.contains(&n.id)) { for n in nodes.iter().filter(|n| n.visible && !moving.contains(&n.id)) {
@@ -167,7 +168,9 @@ fn object_boxes(doc: &Document, moving: &[NodeId]) -> Vec<Rect> {
return; return;
} }
match n.children() { match n.children() {
Some(ch) if n.is_layer() || holders.contains(&n.id) => push(ch, moving, holders, out), Some(ch) if n.is_layer() || holders.contains(&n.id) => {
push(ch.get(usize::from(n.extras().shaper.is_some())..).unwrap_or_default(), moving, holders, out)
}
_ => out.extend(n.geometric_bounds()), _ => out.extend(n.geometric_bounds()),
} }
} }
@@ -445,7 +448,7 @@ impl Targets {
fn points_near(doc: &Document, roots: &[NodeId], near: Rect) -> Self { fn points_near(doc: &Document, roots: &[NodeId], near: Rect) -> Self {
let mut t = Targets::default(); let mut t = Targets::default();
for n in roots.iter().filter_map(|id| doc.node(*id)) { for n in roots.iter().filter_map(|id| doc.node(*id)) {
n.walk(&mut |c| { crate::draw2::walk_shown(n, &mut |c| {
if c.is_container() || !c.visible { if c.is_container() || !c.visible {
return; return;
} }
@@ -1479,6 +1482,31 @@ mod tests {
assert_eq!(t.snap_point(Point::new(101.0, 99.0), 4.0).0, Point::new(100.0, 100.0), "in line with its neighbours, not on itself"); assert_eq!(t.snap_point(Point::new(101.0, 99.0), 4.0).0, Point::new(100.0, 100.0), "in line with its neighbours, not on itself");
} }
/// A Shaper Group's original shapes, which are not shown, are no targets: not their
/// anchors, edges or segments, not their bounds as an object a moved face spaces itself evenly
/// with, and not the anchors a transform tool's reference point snaps to on the selected group.
/// Selected, or under the pointer in Construction mode, an original shape still gives its anchors.
#[test]
fn shaper_sources_are_no_targets() {
let (d, g, sources, face) = doc_with_shaper_group();
let b = d.node(sources).and_then(|n| n.children()).unwrap()[1].id;
// The top right corner of the second source square, which no shown art has.
let corner = Point::new(150.0, 300.0);
let gives_corner = |t: &Targets| {
t.points.iter().any(|(p, _)| *p == corner)
|| t.xs.iter().chain(&t.ys).any(|(_, p, _)| *p == corner)
|| t.segments.iter().any(|(_, s)| s.start() == corner || s.end() == corner)
};
assert!(!gives_corner(&Targets::collect(&d, &[], None)));
assert!(!gives_corner(&Targets::for_drawing(&d, &[], None)));
assert!(!gives_corner(&Targets::for_anchor_drag(&d, &Selection::default())));
assert!(!gives_corner(&Targets::for_handle(&d, face)));
assert!(!object_boxes(&d, &[face]).contains(&Rect::new(0.0, 300.0, 150.0, 400.0)), "the source shapes' bounds");
let near = Rect::new(146.0, 296.0, 154.0, 304.0);
assert!(!gives_corner(&Targets::points_near(&d, &[g], near)));
assert!(gives_corner(&Targets::points_near(&d, &[b], near)));
}
/// #2120: the dragged end of a straight segment at any angle lands on the segment's own /// #2120: the dragged end of a straight segment at any angle lands on the segment's own
/// extension (Line Extension), past its other end or short of it, the guide drawn from that /// extension (Line Extension), past its other end or short of it, the guide drawn from that
/// end; with two anchors moving there is none to keep to. /// end; with two anchors moving there is none to keep to.
+23
View File
@@ -867,6 +867,29 @@ pub(crate) mod testutil {
(d, id) (d, id)
} }
/// [`doc_with_rect`] plus a Shaper Group of two source squares (x from 0 to 100 and from 50 to
/// 150, y from 300 to 400) whose visible face is the first square: the document, the group, its
/// source shapes and the face.
pub fn doc_with_shaper_group() -> (Document, NodeId, NodeId, NodeId) {
use std::sync::Arc;
let (mut d, _) = doc_with_rect();
let l = d.layers[0].id;
let square = |x: f64| shapes::rectangle(Rect::new(x, 300.0, x + 100.0, 400.0));
let a = Node::path(d.alloc_id(), square(0.0), Appearance::default_art());
let b = Node::path(d.alloc_id(), square(50.0), Appearance::default_art());
let mut sources = Node::group(d.alloc_id(), vec![Arc::new(a), Arc::new(b)]);
sources.name = Some(vectorcraft_doc::shaper::SOURCES.into());
sources.opacity = 0.0;
let mut face = Node::path(d.alloc_id(), square(0.0), Appearance::default_art());
face.name = Some(format!("{}0", vectorcraft_doc::shaper::FILL_PREFIX));
let (sources_id, face_id) = (sources.id, face.id);
let mut group = Node::group(d.alloc_id(), vec![Arc::new(sources), Arc::new(face)]);
group.extras_mut().shaper = Some(Box::default());
let group_id = group.id;
d.insert(Some(l), 1, group).unwrap();
(d, group_id, sources_id, face_id)
}
pub fn paint() -> PaintDefaults { pub fn paint() -> PaintDefaults {
PaintDefaults::default() PaintDefaults::default()
} }
-28
View File
@@ -82,12 +82,6 @@ pub fn click(cx: &ToolContext, p: Point) -> Option<Action> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::sync::Arc;
use vectorcraft_doc::shaper::FILL_PREFIX;
use vectorcraft_doc::{Appearance, Node};
use vectorcraft_geom::{Rect, shapes};
use super::*; use super::*;
use crate::extra::ShaperTool; use crate::extra::ShaperTool;
use crate::select::SelectionTool; use crate::select::SelectionTool;
@@ -98,28 +92,6 @@ mod tests {
PointerEvent::new(kind, p.x, p.y) PointerEvent::new(kind, p.x, p.y)
} }
/// A Shaper Group of two source squares (x from 0 to 100 and from 50 to 150, y from 300 to 400)
/// whose visible face is the first square: the document, the group, its source shapes and the
/// face.
fn doc_with_shaper_group() -> (Document, NodeId, NodeId, NodeId) {
let (mut d, _) = doc_with_rect();
let l = d.layers[0].id;
let square = |x: f64| shapes::rectangle(Rect::new(x, 300.0, x + 100.0, 400.0));
let a = Node::path(d.alloc_id(), square(0.0), Appearance::default_art());
let b = Node::path(d.alloc_id(), square(50.0), Appearance::default_art());
let mut sources = Node::group(d.alloc_id(), vec![Arc::new(a), Arc::new(b)]);
sources.name = Some(SOURCES.into());
sources.opacity = 0.0;
let mut face = Node::path(d.alloc_id(), square(0.0), Appearance::default_art());
face.name = Some(format!("{FILL_PREFIX}0"));
let (sources_id, face_id) = (sources.id, face.id);
let mut group = Node::group(d.alloc_id(), vec![Arc::new(sources), Arc::new(face)]);
group.extras_mut().shaper = Some(Box::default());
let group_id = group.id;
d.insert(Some(l), 1, group).unwrap();
(d, group_id, sources_id, face_id)
}
#[test] #[test]
fn the_widget_sits_right_of_the_group_and_a_click_switches_the_mode() { fn the_widget_sits_right_of_the_group_and_a_click_switches_the_mode() {
let (d, g, sources, face) = doc_with_shaper_group(); let (d, g, sources, face) = doc_with_shaper_group();
+32 -2
View File
@@ -103,9 +103,9 @@ impl Tool for LassoTool {
if poly.len() < 3 { if poly.len() < 3 {
return if add || sub { vec![] } else { vec![Action::Exec("select.none".into(), json!({}))] }; return if add || sub { vec![] } else { vec![Action::Exec("select.none".into(), json!({}))] };
} }
// Anchors inside the loop for every editable path. // Anchors inside the loop for every editable path the Lasso may pick.
let mut hits: Vec<(NodeId, BTreeSet<AnchorRef>)> = vec![]; let mut hits: Vec<(NodeId, BTreeSet<AnchorRef>)> = vec![];
cx.doc.walk(|n| { crate::draw2::walk_pickable(cx, |n| {
if let NodeKind::Path { path, .. } = &n.kind { if let NodeKind::Path { path, .. } = &n.kind {
let v: BTreeSet<AnchorRef> = let v: BTreeSet<AnchorRef> =
path.anchors().filter(|(_, _, a)| point_in_polygon(&poly, a.p)).map(|(s, i, _)| (s, i)).collect(); path.anchors().filter(|(_, _, a)| point_in_polygon(&poly, a.p)).map(|(s, i, _)| (s, i)).collect();
@@ -171,6 +171,36 @@ mod tests {
assert_eq!(miss, vec![Action::Exec("select.none".into(), json!({}))]); assert_eq!(miss, vec![Action::Exec("select.none".into(), json!({}))]);
} }
/// The Lasso selects anchor points only on the art a click can pick: not on a Shaper Group's
/// original shapes unless they are isolated (Construction mode), and in isolation mode only on
/// the isolated object.
#[test]
fn lasso_leaves_out_shaper_sources_and_art_outside_isolation() {
let (d, _, sources, face) = doc_with_shaper_group();
let rect = d.layers[0].children().unwrap()[0].id;
let b = d.node(sources).and_then(|n| n.children()).unwrap()[1].id;
let (s, p) = (Selection::default(), paint());
let lasso = |isolation: Option<NodeId>, poly: [(f64, f64); 3]| {
let cx = ToolContext { isolation, ..cx(&d, &s, &p) };
let mut t = LassoTool::default();
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, poly[0].0, poly[0].1));
for (x, y) in &poly[1..] {
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, *x, *y));
}
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, poly[2].0, poly[2].1));
let Some(Action::Exec(_, v)) = a.first() else { panic!("a selection command: {a:?}") };
v["items"].as_array().unwrap().iter().map(|it| NodeId(it["id"].as_u64().unwrap())).collect::<Vec<_>>()
};
// Around the top right corner of the second square, which only its original shape has.
let corner = [(130.0, 280.0), (180.0, 280.0), (150.0, 320.0)];
assert_eq!(lasso(None, corner), vec![]);
assert_eq!(lasso(Some(sources), corner), vec![b]);
// Around the face's top left corner, where the first original shape has an anchor too.
let shared = [(-20.0, 280.0), (30.0, 280.0), (0.0, 320.0)];
assert_eq!(lasso(None, shared), vec![face]);
assert_eq!(lasso(Some(rect), shared), vec![]);
}
#[test] #[test]
fn lasso_selects_anchors_inside() { fn lasso_selects_anchors_inside() {
let (d, id) = doc_with_rect(); let (d, id) = doc_with_rect();
+2
View File
@@ -13,6 +13,8 @@ Click once to select the group; click again to select its visible fill and chang
While the group or one of its faces is selected (Face Selection mode) and the Shaper or Selection tool is active, an arrow widget sits beside the middle of the right side of the original shapes’ bounds. Click it to enter Construction mode: the original shapes are isolated and nothing is selected, and a click with Shaper selects the group’s original shape under the pointer. Click the widget again to leave Construction mode with the group selected. The arrow points right in Face Selection mode and left in Construction mode. While the group or one of its faces is selected (Face Selection mode) and the Shaper or Selection tool is active, an arrow widget sits beside the middle of the right side of the original shapes’ bounds. Click it to enter Construction mode: the original shapes are isolated and nothing is selected, and a click with Shaper selects the group’s original shape under the pointer. Click the widget again to leave Construction mode with the group selected. The arrow points right in Face Selection mode and left in Construction mode.
Smart Guides and Snap to Point do not snap to the original shapes. The reference point of the transform tools snaps to an original shape only when that shape is selected, or under the pointer in Construction mode. Outside Construction mode the Lasso does not pick the anchor points of the original shapes, and the Add Anchor Point, Delete Anchor Point, Anchor Point and Scissors tools do not pick the anchor points or segments of an original shape that is not selected.
Object › Shaper › Release restores the original paths. Expand keeps the visible result as ordinary paths. Each scribble and source edit records one undo step. Sources in a composition must share a parent layer/group; Shaper limits a composition to 64 source paths and 4,096 segments. Object › Shaper › Release restores the original paths. Expand keeps the visible result as ordinary paths. Each scribble and source edit records one undo step. Sources in a composition must share a parent layer/group; Shaper limits a composition to 64 source paths and 4,096 segments.
Finer gesture heuristics remain to be implemented. Finer gesture heuristics remain to be implemented.
+1
View File
@@ -501,6 +501,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Shaper:** uneven squares and rectangles become live rectangles even when drawing begins mid-edge. Recognition uses uniformly spaced samples and closed-loop corners, fits rectangle edges/angles rather than requiring a nearly perfect bounding-box fill, and includes the pointer-release position. Rectangles/squares and ellipses snap to 45° steps, hexagons to 90°, and triangles point up or down; lines keep their free angle even with Shift. Shaper Groups retain the live originals, merge regions with the gesture-origin fill, punch fills independently from strokes, remove boundaries on exiting scribbles and trim protruding stroke pieces. Source moves/resizes re-evaluate the result in the same undo step; double-click enters construction editing, and with the Shaper or Selection tool a click on the arrow widget beside a Shaper Group's original shapes switches the group between Face Selection and Construction mode. Native save/reopen, face recoloring, Release and Expand are covered. Finer gesture heuristics remain. - **Shaper:** uneven squares and rectangles become live rectangles even when drawing begins mid-edge. Recognition uses uniformly spaced samples and closed-loop corners, fits rectangle edges/angles rather than requiring a nearly perfect bounding-box fill, and includes the pointer-release position. Rectangles/squares and ellipses snap to 45° steps, hexagons to 90°, and triangles point up or down; lines keep their free angle even with Shift. Shaper Groups retain the live originals, merge regions with the gesture-origin fill, punch fills independently from strokes, remove boundaries on exiting scribbles and trim protruding stroke pieces. Source moves/resizes re-evaluate the result in the same undo step; double-click enters construction editing, and with the Shaper or Selection tool a click on the arrow widget beside a Shaper Group's original shapes switches the group between Face Selection and Construction mode. Native save/reopen, face recoloring, Release and Expand are covered. Finer gesture heuristics remain.
- **Selection:** Selection, Direct/Group Selection, Magic Wand, Lasso. A Shift-drag marquee toggles what it reaches: objects with the Selection tool, anchors with Direct and Group Selection (#483); the Lasso adds with Shift and subtracts with Alt. Dragging a path segment with Direct Selection bends it when it's curved (its anchors stay) and moves its two anchors when it's straight, one undo step; the fill, Shift or Alt still pick the whole path (Alt-drag copies it). - **Selection:** Selection, Direct/Group Selection, Magic Wand, Lasso. A Shift-drag marquee toggles what it reaches: objects with the Selection tool, anchors with Direct and Group Selection (#483); the Lasso adds with Shift and subtracts with Alt. Dragging a path segment with Direct Selection bends it when it's curved (its anchors stay) and moves its two anchors when it's straight, one undo step; the fill, Shift or Alt still pick the whole path (Alt-drag copies it).
With the Selection or Group Selection tool, pressing E while dragging a marquee makes the marquee select only the objects that lie entirely inside it. Pressing E again switches the marquee back to its default selection. With the Selection or Group Selection tool, pressing E while dragging a marquee makes the marquee select only the objects that lie entirely inside it. Pressing E again switches the marquee back to its default selection.
The Lasso picks anchor points only on art a click can pick, and the Add Anchor Point, Delete Anchor Point, Anchor Point and Scissors tools pick anchor points and segments only on the selected paths and on art a click can pick: not on a Shaper Group's original shapes outside Construction mode, and in isolation mode only inside the isolated group or object. Smart Guides and Snap to Point do not snap to a Shaper Group's original shapes, and the reference point of the transform tools snaps to one only when it is selected, or under the pointer in Construction mode.
A marquee selects a clip group, or an object on a layer with a clipping mask, only when it reaches the area inside the clipping path, and it measures the art only within that path's bounds. Within the bounds of a mask that is not a rectangle, it can still select art that the mask hides. Direct Selection's marquee still selects the hidden anchor points inside it (#2345). A marquee selects a clip group, or an object on a layer with a clipping mask, only when it reaches the area inside the clipping path, and it measures the art only within that path's bounds. Within the bounds of a mask that is not a rectangle, it can still select art that the mask hides. Direct Selection's marquee still selects the hidden anchor points inside it (#2345).
A marquee selects a Shaper Group only when it reaches the art the group shows, not the original shapes kept under it, and Direct Selection's clicks and marquee do not select those shapes' anchor points. In Construction mode a marquee selects the original shapes, and Direct Selection selects their anchor points. A marquee selects a Shaper Group only when it reaches the art the group shows, not the original shapes kept under it, and Direct Selection's clicks and marquee do not select those shapes' anchor points. In Construction mode a marquee selects the original shapes, and Direct Selection selects their anchor points.
A key object needs another selected object outside it. A Shift-click or any other change that leaves one object selected clears the key, and `select.key` sets none on one object or on a group selected with only its own members. With one object selected, the Align panel, the Control bar and the Properties panel align it to the artboard whatever Align To is set to. A key object needs another selected object outside it. A Shift-click or any other change that leaves one object selected clears the key, and `select.key` sets none on one object or on a group selected with only its own members. With one object selected, the Align panel, the Control bar and the Properties panel align it to the artboard whatever Align To is set to.