M5: marquee selection follows Figma's measured rules (gap 4)

A plain marquee now selects each top-level layer it touches: a partly covered frame or section gives the layers in it the marquee touches, an enclosed one is selected itself, an empty frame on touch, a rotated shape by its outline. A drag from inside a frame never selects that frame and also takes the top-level layers it touches outside it. Once the marquee selects a frame whole, no frame opens and a partly covered frame comes whole. Measured in Figma's web app, 2026-11-11.

Flipped existing assertions: marquee_selects_what_it_touches, cmd_marquee_selects_nested_layers, a_box_selects_like_a_marquee. Tests: marquee_behaviour_tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 briefed; Sonnet 5.5 wrote at medium
This commit is contained in:
Trevor Brown
2026-11-11 17:13:26 -05:00
co-authored by Claude Sonnet 5.5
parent 3b9bf8c9a2
commit cb52621414
9 changed files with 357 additions and 40 deletions
+125 -18
View File
@@ -355,14 +355,15 @@ pub fn enter_target(doc: &Document, leaf: NodeId, selection: &[NodeId]) -> NodeI
inner.unwrap_or_else(|| select_target(doc, leaf, selection, false))
}
/// Nodes on `page` whose world bounds intersect `rect` (marquee): top-level children, or the
/// children of top-level frames when the marquee starts inside a frame (`within`).
/// Nodes a marquee over `rect` touches, one level only: the children of `within` (a focus view's
/// frame), or the top-level layers of `page`. Frames are not opened or enclosed here; see
/// [`marquee_select`] for a plain marquee.
pub fn marquee(doc: &Document, page: NodeId, rect: Rect, within: Option<NodeId>) -> Vec<NodeId> {
let parent = within.unwrap_or(page);
doc.node(parent).map(|p| marquee_children(doc, p, rect)).unwrap_or_default()
}
/// Nodes on `page` a Cmd/Ctrl marquee selects: the innermost layers whose bounds touch `rect`
/// Nodes on `page` a Cmd/Ctrl marquee selects: the innermost layers the marquee touches
/// (frames, groups and instances are looked into; a container none of whose children are touched
/// counts itself; boolean groups count as one shape).
pub fn marquee_deep(doc: &Document, page: NodeId, rect: Rect) -> Vec<NodeId> {
@@ -384,25 +385,131 @@ pub fn marquee_deep(doc: &Document, page: NodeId, rect: Rect) -> Vec<NodeId> {
out
}
/// The visible, unlocked children of `p` whose world bounds touch `rect` (on both axes: lines and
/// other flat layers too).
/// The visible, unlocked children of `p` the marquee touches (see [`marquee_touches`]).
fn marquee_children(doc: &Document, p: &Node, rect: Rect) -> Vec<NodeId> {
p.children
.iter()
.copied()
.filter(|c| doc.node(*c).is_some_and(|n| n.visible && !n.locked))
.filter(|c| doc.world_bounds(*c).is_some_and(|b| b.x0 <= rect.x1 && b.x1 >= rect.x0 && b.y0 <= rect.y1 && b.y1 >= rect.y0))
.collect()
p.children.iter().copied().filter(|c| doc.node(*c).is_some_and(|n| n.visible && !n.locked && marquee_touches(doc, *c, n, rect))).collect()
}
/// Figma marquee rule: a marquee that fully contains a top-level frame selects the frame; one
/// starting on empty canvas selects top-level objects it touches.
pub fn marquee_select(doc: &Document, page: NodeId, rect: Rect, start_inside: Option<NodeId>) -> Vec<NodeId> {
if let Some(frame) = start_inside {
let inside = marquee(doc, page, rect, Some(frame));
return inside;
/// The outline a rotated shape is touched by. One line: return the turned box instead
/// (`figcraft_geom::rounded_rect(n.width, n.height, [0.0; 4], 0.0)`) if Figma proves to test that.
fn marquee_outline(n: &Node) -> Option<BezPath> {
shape_path(n)
}
/// Whether the marquee touches layer `id`: its world box overlaps `rect` (inclusive, so lines and
/// zero-size marquees count) and, for a rotated shape, its outline in page space does too.
fn marquee_touches(doc: &Document, id: NodeId, n: &Node, rect: Rect) -> bool {
let box_hit = doc.world_bounds(id).is_some_and(|b| b.x0 <= rect.x1 && b.x1 >= rect.x0 && b.y0 <= rect.y1 && b.y1 >= rect.y0);
if !box_hit
|| !matches!(
n.kind,
NodeKind::Rectangle
| NodeKind::Ellipse { .. }
| NodeKind::Polygon { .. }
| NodeKind::Star { .. }
| NodeKind::Line
| NodeKind::Vector { .. }
)
{
return box_hit;
}
marquee(doc, page, rect, None)
let xf = doc.world_transform(id);
let [a, b, c, d, e, f] = xf.as_coeffs();
const EPS: f64 = 1e-9;
let rotated = !((b.abs() < EPS && c.abs() < EPS) || (a.abs() < EPS && d.abs() < EPS));
if !rotated {
return true;
}
let det = a * d - b * c;
if ![a, b, c, d, e, f, det].iter().all(|v| v.is_finite()) || det.abs() < EPS {
return true;
}
marquee_outline(n).is_none_or(|path| path_touches_rect(n, &path, xf, rect))
}
/// Whether the marquee and the shape drawn as local `path` under `xf` share a point: a path end
/// inside the marquee, a path segment crossing a marquee edge, or the marquee inside the filled shape.
fn path_touches_rect(n: &Node, path: &BezPath, xf: figcraft_geom::Affine, rect: Rect) -> bool {
use figcraft_geom::kurbo::{Line, ParamCurve, PathSeg};
let inside = |p: Point| p.x >= rect.x0 && p.x <= rect.x1 && p.y >= rect.y0 && p.y <= rect.y1;
let mut world = path.clone();
world.apply_affine(xf);
if world.segments().any(|s| inside(s.start()) || inside(s.end())) {
return true;
}
let (tl, tr, br, bl) = (Point::new(rect.x0, rect.y0), Point::new(rect.x1, rect.y0), Point::new(rect.x1, rect.y1), Point::new(rect.x0, rect.y1));
let edges = [(tl, tr), (tr, br), (br, bl), (bl, tl)];
for (p, q) in edges {
if (q - p).hypot() < 1e-9 {
continue;
}
let line = Line::new(p, q);
if world.segments().any(|s: PathSeg| s.intersect_line(line).iter().any(|h| (0.0..=1.0).contains(&h.line_t))) {
return true;
}
}
is_closed(n) && fills_point(n, path, xf.inverse() * tl)
}
/// Whether a marquee takes a touched top-level layer whole, opens it for the layers in it, or skips it.
enum Touch {
Whole,
/// The frame the drag started in: only the layers in it the marquee touches.
Open,
/// A partly covered frame or section: the layers in it, or the frame itself once the marquee is at the top level.
Partial,
Skip,
}
/// Figma's marquee rule (measured in the web app, 2026-11-11): each top-level layer the marquee
/// touches is selected whole, except a frame with children or a section with its contents shown:
/// enclosed by the marquee it is selected itself, otherwise the marquee takes the layers in it that
/// it touches. The frame the drag started in (`start_inside`) is never selected itself, only the
/// layers in it the marquee touches. Selecting a frame or section whole keeps the marquee at the
/// top level: no frame opens then, and a partly covered one is selected whole. Components, instances, board items and sections with hidden
/// contents are selected only when enclosed.
pub fn marquee_select(doc: &Document, page: NodeId, rect: Rect, start_inside: Option<NodeId>) -> Vec<NodeId> {
let Some(page_node) = doc.node(page) else { return Vec::new() };
let touched = marquee_children(doc, page_node, rect);
let classified: Vec<(NodeId, Touch, bool)> = touched
.iter()
.filter_map(|t| {
let n = doc.node(*t)?;
let enclosed = doc.world_bounds(*t).is_some_and(|b| rect.contains(b.origin()) && rect.contains(Point::new(b.x1, b.y1)));
let container = n.kind.is_frame_like() || matches!(n.kind, NodeKind::Section { .. });
let opens = match &n.kind {
NodeKind::Frame | NodeKind::Section { .. } => !n.is_board_atom() && !n.hides_contents() && !n.children.is_empty(),
_ => false,
};
let plain_empty_frame = matches!(n.kind, NodeKind::Frame) && n.children.is_empty() && !n.is_board_atom();
let act = if start_inside == Some(*t) {
Touch::Open
} else if opens {
if enclosed { Touch::Whole } else { Touch::Partial }
} else if plain_empty_frame || !(container || n.is_board_atom()) || enclosed {
Touch::Whole
} else {
Touch::Skip
};
let whole_container = container && matches!(act, Touch::Whole);
Some((*t, act, whole_container))
})
.collect();
let top_level_mode = classified.iter().any(|(_, _, whole_container)| *whole_container);
let mut out = Vec::new();
for (t, act, _) in &classified {
match act {
Touch::Whole => out.push(*t),
Touch::Partial if top_level_mode => out.push(*t),
Touch::Open | Touch::Partial if !top_level_mode => {
if let Some(n) = doc.node(*t) {
out.extend(marquee_children(doc, n, rect));
}
}
Touch::Open | Touch::Partial | Touch::Skip => {}
}
}
out
}
/// Bounds of a node's outline in page coordinates for snapping (the box corners and centre).
+14 -7
View File
@@ -47,16 +47,23 @@ impl Session {
/// the layers it touches (`deep`: nested ones too); `additive` (Shift) toggles them.
pub(crate) fn marquee_selection(&self, r: Rect, within: Option<NodeId>, deep: bool, base: &[NodeId], additive: bool) -> Result<Vec<NodeId>> {
let (doc, page) = (self.doc()?, self.page()?);
let mut hits =
if deep { figcraft_doc::geometry::marquee_deep(doc, page, r) } else { figcraft_doc::geometry::marquee_select(doc, page, r, within) };
if let Some(f) = self.ds()?.focused() {
let focused = self.ds()?.focused();
let mut hits = match (deep, focused) {
(true, _) => figcraft_doc::geometry::marquee_deep(doc, page, r),
(false, Some(f)) => figcraft_doc::geometry::marquee(doc, page, r, Some(f)),
(false, None) => figcraft_doc::geometry::marquee_select(doc, page, r, within),
};
if let Some(f) = focused {
hits.retain(|h| *h != f && doc.is_ancestor(f, *h));
}
// A marquee over a frame from outside selects the frame only when it covers it fully.
if within.is_none() && !deep {
// A widget's layers are part of the widget: a marquee from outside never opens one.
if !deep {
hits.retain(|h| {
let full = doc.world_bounds(*h).is_some_and(|b| r.contains(b.origin()) && r.contains(Point::new(b.x1, b.y1)));
full || !doc.node(*h).is_some_and(|n| n.kind.is_frame_like() || matches!(n.kind, NodeKind::Section { .. }))
doc.node(*h)
.and_then(|n| n.parent)
.filter(|p| Some(*p) != within)
.and_then(|p| doc.node(p))
.is_none_or(|p| !crate::widgets::is_widget(p))
});
}
// Shift toggles: what was selected and is touched again comes out.
+2
View File
@@ -421,6 +421,8 @@ mod live_tests;
#[cfg(test)]
mod lock_behaviour_tests;
#[cfg(test)]
mod marquee_behaviour_tests;
#[cfg(test)]
mod mask_tests;
#[cfg(test)]
mod masks_behaviour_tests;
@@ -0,0 +1,200 @@
//! Marquee selection, as measured in Figma's web app (2026-11-11, the "marquee rules" and "confirming
//! pass" readings) and described in the help-center article "Select layers and objects": a drag
//! selects each top-level layer it touches; a partly covered frame or section gives the layers in it
//! the marquee touches, an enclosed one is selected itself; a drag started inside a frame never selects
//! that frame; selecting a frame whole keeps the marquee at the top level; a rotated shape is touched by
//! its outline, an unrotated one by its box.
use serde_json::{Value, json};
use crate::Session;
use crate::params::id_of;
use crate::tools::{PointerEvent, PointerKind};
use figcraft_doc::NodeId;
use figcraft_geom::Point;
fn ex(s: &mut Session, cmd: &str, p: Value) -> Value {
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd}: {e}"))
}
fn id(v: &Value) -> NodeId {
id_of(&v["id"]).unwrap()
}
fn rect(s: &mut Session, x: f64, y: f64, w: f64, h: f64, parent: Option<NodeId>) -> NodeId {
let mut p = json!({"x": x, "y": y, "width": w, "height": h});
if let Some(parent) = parent {
p["parent"] = json!(parent);
}
id(&ex(s, "shape.rectangle", p))
}
fn ev(kind: PointerKind, x: f64, y: f64) -> PointerEvent {
PointerEvent::at(kind, x, y, 1.0)
}
fn drag(s: &mut Session, a: (f64, f64), b: (f64, f64)) {
s.pointer(&ev(PointerKind::Down, a.0, a.1)).unwrap();
s.pointer(&ev(PointerKind::Move, b.0, b.1)).unwrap();
s.pointer(&ev(PointerKind::Up, b.0, b.1)).unwrap();
}
fn sorted(mut v: Vec<NodeId>) -> Vec<NodeId> {
v.sort();
v
}
#[allow(dead_code)]
struct Scene {
f1: NodeId,
r1: NodeId,
r2: NodeId,
e: NodeId,
u: NodeId,
rr: NodeId,
re: NodeId,
f2: NodeId,
g: NodeId,
s: NodeId,
r5: NodeId,
}
/// The confirming pass's layout: `f1` (0,0 300x200) holding `r1` (20,20) and `r2` (220,120), both 60x60;
/// ellipse `e` (500,0 200x200); rectangle `u` (900,0 100x100); `rr` rectangle (0,400 200x100) and ellipse
/// `re` (500,400 200x100) turned 45 degrees about their centres; the empty frame `f2` (0,700 200x150);
/// the group `g` of two rectangles (300,700) and (360,760); the section `s` (600,700 300x200) holding `r5`.
fn scene(s: &mut Session) -> Scene {
let f1 = id(&ex(s, "frame.create", json!({"x": 0, "y": 0, "width": 300, "height": 200})));
let r1 = rect(s, 20.0, 20.0, 60.0, 60.0, Some(f1));
let r2 = rect(s, 220.0, 120.0, 60.0, 60.0, Some(f1));
let e = id(&ex(s, "shape.ellipse", json!({"x": 500, "y": 0, "width": 200, "height": 200})));
let u = rect(s, 900.0, 0.0, 100.0, 100.0, None);
let rr = rect(s, 0.0, 400.0, 200.0, 100.0, None);
ex(s, "node.set", json!({"id": rr, "props": {"rotation": 45}}));
let re = id(&ex(s, "shape.ellipse", json!({"x": 500, "y": 400, "width": 200, "height": 100})));
ex(s, "node.set", json!({"id": re, "props": {"rotation": 45}}));
let f2 = id(&ex(s, "frame.create", json!({"x": 0, "y": 700, "width": 200, "height": 150})));
let g1 = rect(s, 300.0, 700.0, 80.0, 80.0, None);
let g2 = rect(s, 360.0, 760.0, 80.0, 80.0, None);
let g = id(&ex(s, "object.group", json!({"ids": [g1, g2]})));
let sec = id(&ex(s, "node.create", json!({"type": "section", "x": 600, "y": 700, "width": 300, "height": 200})));
let r5 = rect(s, 20.0, 20.0, 60.0, 60.0, Some(sec));
ex(s, "select.none", json!({}));
Scene { f1, r1, r2, e, u, rr, re, f2, g, s: sec, r5 }
}
fn selected(s: &mut Session, a: (f64, f64), b: (f64, f64)) -> Vec<NodeId> {
ex(s, "select.none", json!({}));
drag(s, a, b);
let sel = sorted(s.selection());
ex(s, "select.none", json!({}));
sel
}
#[test]
fn a_partly_covered_frame_gives_the_layers_it_touches() {
let mut s = Session::new();
let k = scene(&mut s);
assert_eq!(selected(&mut s, (-50.0, -50.0), (60.0, 60.0)), vec![k.r1], "measured case 2: Rectangle 1, not F1");
}
#[test]
fn an_enclosed_frame_is_selected_itself() {
let mut s = Session::new();
let k = scene(&mut s);
assert_eq!(selected(&mut s, (-20.0, -20.0), (320.0, 220.0)), vec![k.f1], "measured b");
}
#[test]
fn an_empty_frame_is_selected_on_touch() {
let mut s = Session::new();
let k = scene(&mut s);
assert_eq!(selected(&mut s, (-20.0, 680.0), (50.0, 720.0)), vec![k.f2], "measured c: Frame 2");
}
#[test]
fn a_group_is_selected_whole_on_touch() {
let mut s = Session::new();
let k = scene(&mut s);
assert_eq!(selected(&mut s, (290.0, 690.0), (310.0, 710.0)), vec![k.g], "measured c: Group 1, not the rectangle");
}
#[test]
fn a_partly_covered_section_gives_the_layers_it_touches() {
let mut s = Session::new();
let k = scene(&mut s);
assert_eq!(selected(&mut s, (590.0, 690.0), (650.0, 750.0)), vec![k.r5], "measured c: Rectangle 5, not the section");
}
#[test]
fn an_unrotated_shape_is_touched_by_its_box() {
let mut s = Session::new();
let k = scene(&mut s);
assert_eq!(selected(&mut s, (490.0, -10.0), (505.0, 5.0)), vec![k.e], "measured case 3: the ellipse's box corner selects it");
assert_eq!(selected(&mut s, (890.0, -10.0), (905.0, 5.0)), vec![k.u], "control");
}
#[test]
fn a_rotated_rectangle_is_touched_by_its_outline() {
let mut s = Session::new();
let k = scene(&mut s);
assert_eq!(selected(&mut s, (-30.0, 320.0), (4.0, 354.0)), Vec::<NodeId>::new(), "measured case 3: the box corner selects nothing");
assert_eq!(selected(&mut s, (-30.0, 320.0), (100.0, 450.0)), vec![k.rr]);
}
#[test]
fn a_rotated_ellipse_is_not_touched_by_its_axis_aligned_box() {
let mut s = Session::new();
let k = scene(&mut s);
assert_eq!(selected(&mut s, (470.0, 320.0), (504.0, 354.0)), Vec::<NodeId>::new(), "measured case 3: the box corner selects nothing");
assert_eq!(selected(&mut s, (470.0, 320.0), (600.0, 450.0)), vec![k.re]);
}
#[test]
fn a_rotated_ellipse_is_not_touched_through_the_corner_of_its_turned_box() {
let mut s = Session::new();
let k = scene(&mut s);
// Not the measurement's literal numbers (see the brief, section 5): a point inside the turned box
// but outside the ellipse.
let p = Point::new(690.0, 420.0);
let doc = s.doc().unwrap();
let n = doc.node(k.re).unwrap();
assert!(n.local_rect().contains(doc.world_transform(k.re).inverse() * p), "the probe is inside the turned box");
assert_eq!(selected(&mut s, (727.0, 407.0), (690.0, 420.0)), Vec::<NodeId>::new());
}
#[test]
fn a_drag_from_inside_a_frame_never_selects_it() {
let mut s = Session::new();
let k = scene(&mut s);
let start_x = |s: &Session| s.doc().unwrap().world_bounds(k.f1).map(|b| (b.x0, b.y0));
assert_eq!(selected(&mut s, (150.0, 100.0), (180.0, 150.0)), Vec::<NodeId>::new(), "measured e1");
assert_eq!(start_x(&s), Some((0.0, 0.0)));
assert_eq!(selected(&mut s, (150.0, 100.0), (180.0, 250.0)), Vec::<NodeId>::new(), "measured e2");
assert_eq!(start_x(&s), Some((0.0, 0.0)));
assert_eq!(selected(&mut s, (150.0, 100.0), (600.0, 100.0)), vec![k.e], "measured e3: the ellipse only");
assert_eq!(start_x(&s), Some((0.0, 0.0)));
assert_eq!(selected(&mut s, (150.0, 100.0), (250.0, 150.0)), vec![k.r2], "today's rule, not measured");
assert_eq!(start_x(&s), Some((0.0, 0.0)));
}
#[test]
fn selecting_a_frame_whole_keeps_the_marquee_at_the_top_level() {
let mut s = Session::new();
let k = scene(&mut s);
// Encloses the empty f2, covers rr, and partly covers f1 over r1: no frame opens, f1 comes whole.
assert_eq!(
selected(&mut s, (-20.0, 50.0), (210.0, 860.0)),
sorted(vec![k.f2, k.f1, k.rr]),
"the article: selecting a top-level frame selects only top-level layers; measured: a partly covered frame comes whole"
);
}
#[test]
fn a_marquee_enclosing_an_empty_frame_takes_a_partly_covered_frame_whole() {
let mut s = Session::new();
let f1 = id(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 300, "height": 200})));
let _r1 = rect(&mut s, 20.0, 20.0, 60.0, 60.0, Some(f1));
let f2 = id(&ex(&mut s, "frame.create", json!({"x": 400, "y": 0, "width": 200, "height": 150})));
assert_eq!(selected(&mut s, (650.0, 170.0), (40.0, -10.0)), sorted(vec![f1, f2]), "measured re-measure: F2 and F1, not R1");
}
+4 -4
View File
@@ -1,6 +1,6 @@
//! `select.area`: what a marquee over a box selects, without a pointer (keyboard box selection in
//! the app, agents over MCP): top-level layers, a frame only when fully covered, the children of
//! the frame the box starts in, Shift-like toggling, Cmd/Ctrl-like nested layers; no box asks the
//! the app, agents over MCP): top-level layers, a frame only when fully covered (otherwise the
//! layers in it the box touches), the children of the frame the box starts in, Shift-like toggling, Cmd/Ctrl-like nested layers; no box asks the
//! app to draw one.
use serde_json::{Value, json};
@@ -40,8 +40,8 @@ fn a_box_selects_like_a_marquee() {
let inner = rect(&mut s, 20.0, 20.0, Some(frame));
let a = rect(&mut s, 0.0, 0.0, None);
let b = rect(&mut s, 100.0, 0.0, None);
// Top-level layers it touches; a frame only touched from outside isn't selected.
assert_eq!(area(&mut s, -10.0, -10.0, 340.0, 40.0, json!({})), sorted(vec![a, b]));
// Top-level layers it touches; a frame only touched from outside gives the layers in it the box touches.
assert_eq!(area(&mut s, -10.0, -10.0, 340.0, 40.0, json!({})), sorted(vec![a, b, inner]));
// A negative size draws the box the other way.
assert_eq!(area(&mut s, 130.0, 30.0, -140.0, -40.0, json!({})), sorted(vec![a, b]));
// Covering the frame selects it.
@@ -214,8 +214,8 @@ fn marquee_selects_what_it_touches() {
(450.0, 40.0),
NONE,
vec![],
vec![],
"from the canvas, a top-level frame is selected only when the marquee covers all of it",
vec![k.g, k.n],
"from the canvas, a partly covered top-level frame gives the layers in it the marquee touches (measured); the nested frame comes whole",
),
((-10.0, -10.0), (450.0, 310.0), NONE, vec![], vec![k.f], "a marquee covering a top-level frame selects the frame"),
(
+4 -4
View File
@@ -66,10 +66,10 @@ fn cmd_click_selects_the_nested_layer() {
#[test]
fn cmd_marquee_selects_nested_layers() {
let mut s = Session::new();
let (f, _, [a, b, c], out) = scene(&mut s);
// From empty canvas, a plain marquee across the frame only touches top-level layers (and the
// frame isn't fully covered).
assert_eq!(marquee(&mut s, (-10.0, 10.0), (500.0, 70.0), false, false), vec![out]);
let (f, g, [a, b, c], out) = scene(&mut s);
// From empty canvas, a plain marquee across the frame (not fully covered) gives the layers in it
// it touches (the group whole), plus the top-level layer.
assert_eq!(sorted(marquee(&mut s, (-10.0, 10.0), (500.0, 70.0), false, false)), sorted(vec![g, out]));
// Cmd/Ctrl: the innermost layers it touches, through the frame and the group.
assert_eq!(sorted(marquee(&mut s, (-10.0, 10.0), (500.0, 70.0), false, true)), sorted(vec![a, b, out]));
assert_eq!(sorted(marquee(&mut s, (-10.0, 10.0), (100.0, 190.0), false, true)), sorted(vec![a, b, c]));
+3 -3
View File
@@ -675,8 +675,8 @@ on; `key` resolves keys the way the canvas does. Commands behind keys:
panel has the keyboard (after F6 or a click on a row), else on the canvas below the selection; the
menu takes the keyboard and gives it back when it closes (no-op headless).
- `select.area {x, y, width, height, add?, deep?}`: select what a marquee from `x, y` over that box
(page units) would: top-level layers, a frame only when the box covers it, the children of the
top-level frame the box starts in; `add` toggles like a Shift marquee, `deep` takes nested layers
(page units) would: the top-level layers it touches (a rotated shape by its outline), a frame or section
only when the box covers it and otherwise the layers in it the box touches, never the frame the box starts in; `add` toggles like a Shift marquee, `deep` takes nested layers
like Cmd/Ctrl. Returns `{selection}`. Without a box (`F8` in the app) it draws one for the
keyboard around the selection (or in the middle of the view): the arrows move it, Shift+arrows
move its far corner (10 screen pixels a press), Enter selects (Shift+Enter adds, Cmd/Ctrl+Enter
@@ -2059,7 +2059,7 @@ turned off 0°.
/ Shift+Enter), `select.next` / `select.previous` (Tab / Shift+Tab). With `pointer`, `cmd: true`
clicks select the nested layer, `cmd` drags from empty canvas select the nested layers they touch,
and `shift` drags toggle what they touch. A press on the background of a top-level frame with
layers in it (the frame not selected) starts a marquee inside it, as on the canvas; an empty frame
layers in it (the frame not selected) starts a marquee, as on the canvas; an empty frame
is picked and dragged. Locked layers (selectable with `select.set`) get no handles and stay put
when the rest of the selection is dragged. `select.all` / `select.inverse` skip hidden and locked
layers.
+3 -2
View File
File diff suppressed because one or more lines are too long