diff --git a/crates/engine/src/cmd/modify.rs b/crates/engine/src/cmd/modify.rs
--- a/crates/engine/src/cmd/modify.rs
+++ b/crates/engine/src/cmd/modify.rs
@@ -12,8 +12,6 @@ use super::helpers::{arc, circle, line, lwpoly, v3};
 use super::machines::{SelOutcome, SelectPhase, number};
 use super::trimextend::{self, TrimM};
 use super::{CommandSpec, bad, bool_or, f64_or, f64_req, point_param, point_req, points_param, str_param, targets};
-
-use crate::select::{SelectKind, SelectMode};
 use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
 
 pub fn specs() -> Vec<CommandSpec> {
@@ -204,27 +202,10 @@ fn run_mirror(s: &mut Session, p: &Value) -> Result<Value> {
     Ok(json!({ "handles": r.iter().map(|h| h.hex()).collect::<Vec<_>>() }))
 }
 
-/// An area whose vertices STRETCH moves: a window's box, or a polygon closed back to its first
-/// point.
-#[derive(Clone)]
-enum StretchArea {
-    Box(cadcraft_geom::Bounds2),
-    Polygon(Vec<Vec2>),
-}
-
-impl StretchArea {
-    fn contains(&self, p: Vec2) -> bool {
-        match self {
-            StretchArea::Box(bx) => bx.contains(p),
-            StretchArea::Polygon(poly) => cadcraft_geom::point_in_polygon(poly, p),
-        }
-    }
-}
-
 /// Add to `hs` the visible multilines in `space` with a control vertex inside `area`. A crossing
 /// selection tests the drawn element lines, which sit away from the control vertices by the
 /// multiline's scale, so a small area around a vertex alone would miss them.
-fn add_mline_vertex_hits(d: &cadcraft_doc::Drawing, space: &cadcraft_doc::Space, area: &StretchArea, hs: &mut Vec<Handle>) {
+fn add_mline_vertex_hits(d: &cadcraft_doc::Drawing, space: &cadcraft_doc::Space, area: &crate::select::SelectArea, hs: &mut Vec<Handle>) {
     let Some(store) = d.space(space) else { return };
     for e in store.iter() {
         if let EntityKind::MLine(m) = &e.kind
@@ -238,7 +219,7 @@ fn add_mline_vertex_hits(d: &cadcraft_doc::Drawing, space: &cadcraft_doc::Space,
 }
 
 /// Move the vertices of `e` that lie inside any of the areas `wins` by `d` (STRETCH).
-fn stretch_entity(e: &mut Entity, wins: &[StretchArea], d: Vec2) {
+fn stretch_entity(e: &mut Entity, wins: &[crate::select::SelectArea], d: Vec2) {
     let inside = |p: Vec2| wins.iter().any(|a| a.contains(p));
     let mv = |p: &mut cadcraft_geom::Vec3| {
         if inside(p.xy()) {
@@ -299,12 +280,12 @@ fn run_stretch(s: &mut Session, p: &Value) -> Result<Value> {
         return Ok(json!({ "stretched": moved.len() }));
     }
     let (mut hs, area) = if let Some(poly) = listed("polygon", 3)? {
-        (crate::select::select_polygon(s.doc()?, &space, &poly, true), StretchArea::Polygon(poly))
+        (crate::select::select_polygon(s.doc()?, &space, &poly, true), crate::select::SelectArea::Polygon(poly))
     } else {
         let w = points_param(p, "window").ok_or_else(|| bad("stretch", "`window` [[x,y],[x,y]], `polygon` or `fence` is required"))?;
         let (Some(a), Some(b)) = (w.first(), w.get(1)) else { return Err(bad("stretch", "window needs 2 corners")) };
         let bx = cadcraft_geom::Bounds2::new(*a, *b);
-        (crate::select::select_window(s.doc()?, &space, bx, true), StretchArea::Box(bx))
+        (crate::select::select_window(s.doc()?, &space, bx, true), crate::select::SelectArea::Box(bx))
     };
     add_mline_vertex_hits(s.doc()?, &space, &area, &mut hs);
     hs.retain(|h| !super::curves::is_locked(s, *h));
@@ -325,10 +306,12 @@ enum StretchPhase {
     Second,
 }
 
-/// STRETCH: "Select objects" repeats until Enter. Crossing windows (implied right to left, or after
-/// `C`), windows (left to right, or after `W`) and polygons (`CP`, `WP`) select objects and mark
-/// the area whose vertices move; objects picked one at a time (or by a fence, `F`, or
-/// All/Last/Previous) move whole. Then the base point or Displacement, and the second point.
+/// STRETCH: "Select objects" repeats until Enter. It is the session's own selection prompt, as in
+/// every other command (#1116): crossing windows (implied right to left, or after `C`), windows
+/// (left to right, or after `W`) and polygons (`CP`, `WP`) select objects and mark the area whose
+/// vertices move ([`Session::take_pick_area`]); objects picked one at a time (or by a fence, `F`,
+/// or All/Last/Previous) move whole. `R`/`A` switch to removing and back. Then the base point or
+/// Displacement, and the second point.
 #[derive(Default)]
 struct StretchM {
     phase: StretchPhase,
@@ -336,12 +319,8 @@ struct StretchM {
     /// Objects selected without a window: they move whole.
     whole: Vec<Handle>,
     /// The selection windows and polygons: vertices inside any of them move.
-    windows: Vec<StretchArea>,
+    windows: Vec<crate::select::SelectArea>,
     removing: bool,
-    /// `C`, `W`, `BOX`, `CP`, `WP` or `F` was typed: the next points are that kind of selection.
-    mode: Option<SelectMode>,
-    /// First corner of the window being drawn.
-    corner: Option<Vec2>,
     base: Option<Vec2>,
 }
 
@@ -369,117 +348,26 @@ impl StretchM {
         s.set_selection(self.objs.clone());
     }
 
-    /// Select with a typed mode that has its points. With too few (a fence of one point, a polygon
-    /// of two) the mode is dropped.
-    fn finish_mode(&mut self, s: &mut Session, m: SelectMode) -> Result<()> {
-        let space = s.space();
-        let Some(mut hs) = m.select(s.doc()?, &space) else { return Ok(()) };
-        let crossing = match (m.kind, m.pts.first().zip(m.pts.get(1))) {
-            (SelectKind::Crossing | SelectKind::CPolygon, _) => true,
-            (SelectKind::Box, Some((a, b))) => b.x < a.x,
-            _ => false,
-        };
-        let area = match (m.kind, m.pts.first().zip(m.pts.get(1))) {
-            (SelectKind::WPolygon | SelectKind::CPolygon, _) => Some(StretchArea::Polygon(m.pts)),
-            (SelectKind::Window | SelectKind::Crossing | SelectKind::Box, Some((a, b))) => {
-                Some(StretchArea::Box(cadcraft_geom::Bounds2::new(*a, *b)))
-            }
-            // A fence marks no area: what it crosses moves whole.
-            _ => None,
-        };
-        let whole = area.is_none();
-        if let (Some(a), true, false) = (&area, crossing, self.removing) {
-            add_mline_vertex_hits(s.doc()?, &space, a, &mut hs);
-        }
-        if let (Some(a), false) = (area, self.removing) {
-            self.windows.push(a);
-        }
-        self.add(s, hs, whole);
-        Ok(())
-    }
-
     fn select(&mut self, s: &mut Session, i: Input) -> Result<Step> {
-        if let Some(mut m) = self.mode.take() {
-            match &i {
-                Input::Point(p) => {
-                    if m.push(*p) {
-                        self.finish_mode(s, m)?;
-                    } else {
-                        self.mode = Some(m);
-                    }
-                    return Ok(Step::Continue);
-                }
-                Input::Text(t) | Input::Keyword(t) if matches!(t.trim().to_ascii_lowercase().as_str(), "u" | "undo") => {
-                    m.pts.pop();
-                    self.mode = Some(m);
-                    return Ok(Step::Continue);
-                }
-                // Enter ends a fence or closes a polygon.
-                Input::Enter => {
-                    self.finish_mode(s, m)?;
-                    return Ok(Step::Continue);
-                }
-                // Anything else leaves the mode and is read as usual.
-                _ => {}
-            }
-        }
         match i {
-            Input::Point(p) => match self.corner.take() {
-                Some(c) => {
-                    let bx = cadcraft_geom::Bounds2::new(c, p);
-                    let space = s.space();
-                    let mut hs = crate::select::select_window(s.doc()?, &space, bx, p.x < c.x);
-                    if !self.removing {
-                        if p.x < c.x {
-                            add_mline_vertex_hits(s.doc()?, &space, &StretchArea::Box(bx), &mut hs);
-                        }
-                        self.windows.push(StretchArea::Box(bx));
-                    }
-                    self.add(s, hs, false);
-                }
-                None => {
-                    let aperture = s.pixel_size() * s.settings.pickbox.max(1.0) * 1.5;
-                    let space = s.space();
-                    match crate::select::pick(s.doc()?, &space, p, aperture) {
-                        Some(h) => self.add(s, vec![h], true),
-                        None => self.corner = Some(p),
-                    }
-                }
-            },
-            Input::Pick(hs) => self.add(s, hs, true),
-            Input::Text(t) | Input::Keyword(t) => {
-                self.corner = None;
-                let d = s.doc()?;
-                let store = d.space(&s.space());
-                let word = t.trim().to_ascii_lowercase();
-                // `_C`, `_L`: the English name, as scripts write it.
-                let word = word.strip_prefix('_').unwrap_or(&word).to_string();
-                if let Some(m) = SelectMode::from_keyword(&word).filter(|m| m.kind != SelectKind::Group) {
-                    self.mode = Some(m);
-                    return Ok(Step::Continue);
-                }
-                let picked = match word.as_str() {
-                    "r" | "remove" => {
-                        self.removing = true;
-                        None
-                    }
-                    "a" | "add" => {
-                        self.removing = false;
-                        None
+            // Whatever selected the objects; a window or polygon marks where vertices move.
+            Input::Pick(mut hs) => {
+                let area = s.take_pick_area();
+                let whole = area.is_none();
+                if let (Some(a), false) = (area, self.removing) {
+                    // A crossing tests a multiline's drawn lines; a control vertex inside counts too.
+                    if a.crossing {
+                        add_mline_vertex_hits(s.doc()?, &s.space(), &a.area, &mut hs);
                     }
-                    "all" => Some(store.map(|st| st.iter().filter(|e| d.is_visible(e)).map(|e| e.handle).collect()).unwrap_or_default()),
-                    "l" | "last" => Some(store.and_then(|st| st.last()).map(|e| vec![e.handle]).unwrap_or_default()),
-                    "p" | "previous" => Some(s.state()?.previous_selection.clone()),
-                    _ => {
-                        s.echo("*Invalid selection*");
-                        None
-                    }
-                };
-                if let Some(hs) = picked {
-                    self.add(s, hs, true);
+                    self.windows.push(a.area);
                 }
+                self.add(s, hs, whole);
             }
-            Input::Enter if self.corner.is_some() => self.corner = None,
+            Input::Text(t) | Input::Keyword(t) => match t.trim().to_ascii_lowercase().as_str() {
+                "r" | "remove" => self.removing = true,
+                "a" | "add" => self.removing = false,
+                _ => s.echo("*Invalid selection*"),
+            },
             Input::Enter if self.objs.is_empty() => return Ok(Step::Done),
             Input::Enter => {
                 s.remember_selection(&self.objs);
@@ -515,15 +403,11 @@ impl Interactive for StretchM {
         Ok(Step::Continue)
     }
     fn prompt(&self, _s: &Session) -> Prompt {
-        if let (StretchPhase::Select, Some(m)) = (self.phase, &self.mode) {
-            return m.prompt();
-        }
-        match (self.phase, self.corner) {
-            (StretchPhase::Select, Some(c)) => Prompt::new("Specify opposite corner", Accept::POINT).corner(c),
-            (StretchPhase::Select, None) => Prompt::new(if self.removing { "Remove objects" } else { "Select objects" }, Accept::POINT),
-            (StretchPhase::Base, _) => Prompt::new("Specify base point", Accept::POINT).kw(&["Displacement"]).default("Displacement"),
-            (StretchPhase::Displacement, _) => Prompt::new("Specify displacement", Accept::POINT).default("0,0"),
-            (StretchPhase::Second, _) => Prompt::new("Specify second point or <use first point as displacement>", Accept::POINT).base_opt(self.base),
+        match self.phase {
+            StretchPhase::Select => Prompt::new(if self.removing { "Remove objects" } else { "Select objects" }, Accept::SELECT),
+            StretchPhase::Base => Prompt::new("Specify base point", Accept::POINT).kw(&["Displacement"]).default("Displacement"),
+            StretchPhase::Displacement => Prompt::new("Specify displacement", Accept::POINT).default("0,0"),
+            StretchPhase::Second => Prompt::new("Specify second point or <use first point as displacement>", Accept::POINT).base_opt(self.base),
         }
     }
     fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
@@ -550,26 +434,8 @@ impl Interactive for StretchM {
         }
     }
     fn preview(&self, s: &Session, c: Vec2) -> Vec<EntityKind> {
-        let rect = |a: Vec2| {
-            let (b, d) = (Vec2::new(c.x, a.y), Vec2::new(a.x, c.y));
-            vec![line(a, b), line(b, c), line(c, d), line(d, a)]
-        };
-        if let (StretchPhase::Select, Some(m)) = (self.phase, &self.mode) {
-            let Some(first) = m.pts.first().copied() else { return Vec::new() };
-            if !matches!(m.kind, SelectKind::Fence | SelectKind::WPolygon | SelectKind::CPolygon) {
-                return rect(first);
-            }
-            // The fence or polygon so far, on to the cursor; a polygon closes back to its start.
-            let mut out: Vec<EntityKind> = m.pts.windows(2).filter_map(|w| Some(line(*w.first()?, *w.get(1)?))).collect();
-            out.extend(m.pts.last().map(|l| line(*l, c)));
-            if m.kind != SelectKind::Fence && m.pts.len() >= 2 {
-                out.push(line(c, first));
-            }
-            return out;
-        }
-        match (self.phase, self.corner, self.base) {
-            (StretchPhase::Select, Some(a), _) => rect(a),
-            (StretchPhase::Second, _, Some(base)) => {
+        match (self.phase, self.base) {
+            (StretchPhase::Second, Some(base)) => {
                 let Ok(doc) = s.doc() else { return Vec::new() };
                 let m = Mat3::translate(c - base);
                 let mut out: Vec<EntityKind> = self
diff --git a/crates/engine/src/cmd/options.rs b/crates/engine/src/cmd/options.rs
--- a/crates/engine/src/cmd/options.rs
+++ b/crates/engine/src/cmd/options.rs
@@ -63,7 +63,7 @@ pub fn specs() -> Vec<CommandSpec> {
         CommandSpec::new("options", "Options...", run_options)
             .menu(&["Tools", "Options..."])
             .alias(&["op", "preferences"])
-            .params("{reset?: bool, OSMODE?, PICKBOX?, APERTURE?, GRIPSIZE?, CURSORSIZE?, PICKFIRST?, PICKADD?, PICKDRAG?: 0 (two clicks) | 2 (clicks or drag), …: any profile variable}")
+            .params("{reset?: bool, OSMODE?, PICKBOX?, APERTURE?, GRIPSIZE?, CURSORSIZE?, PICKFIRST?, PICKADD?, PICKDRAG?: 0 (two clicks) | 1 (drag) | 2 (clicks or drag), …: any profile variable}")
             .enabled(always)
             .noundo(),
     ]
diff --git a/crates/engine/src/cmd/trimextend.rs b/crates/engine/src/cmd/trimextend.rs
--- a/crates/engine/src/cmd/trimextend.rs
+++ b/crates/engine/src/cmd/trimextend.rs
@@ -8,7 +8,7 @@ use cadcraft_doc::{Drawing, EntityKind, Handle};
 use cadcraft_geom::{Bounds2, Line, Vec2, line_line};
 use serde_json::{Value, json};
 
-use super::helpers::{line, lwpoly, rect_vertices};
+use super::helpers::line;
 use super::modify::{Edges, extend, trim};
 use super::{bad, point_param, points_param};
 use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
@@ -357,7 +357,8 @@ impl Interactive for TrimM {
                 Some(l) => Prompt::new("Specify next fence point", Accept::POINT).kw(&["Undo"]).base(*l),
             },
             Stage::Crossing(None) => Prompt::new("Specify first corner", Accept::POINT),
-            Stage::Crossing(Some(_)) => Prompt::new("Specify opposite corner", Accept::POINT),
+            // A selection window like any other: drawn as a crossing, closed by a drag (PICKDRAG).
+            Stage::Crossing(Some(a)) => Prompt::new("Specify opposite corner", Accept::POINT).window(*a, Some(true)),
             Stage::Mode => {
                 let msg = if self.extend { "Enter an extend mode option" } else { "Enter a trim mode option" };
                 Prompt::new(msg, Accept::TEXT).kw(&["Quick", "Standard"]).default(if quick_mode(s) { "Quick" } else { "Standard" })
@@ -525,7 +526,6 @@ impl Interactive for TrimM {
                 .windows(2)
                 .filter_map(|w| Some(line(*w.first()?, *w.get(1)?)))
                 .collect(),
-            Stage::Crossing(Some(a)) => vec![lwpoly(rect_vertices(*a, c), true)],
             _ => Vec::new(),
         }
     }
diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs
--- a/crates/engine/src/lib.rs
+++ b/crates/engine/src/lib.rs
@@ -47,7 +47,7 @@ pub use cadcraft_geom as geom;
 pub use cadcraft_render as render;
 pub use cmd::hatch::{HatchPicks, PatternChoice, PickMode, pattern_choices};
 pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command};
-pub use prompt::{Accept, Drag, Input, Interactive, Picker, Prompt, Step};
+pub use prompt::{Accept, Drag, Input, Interactive, Picker, Prompt, SelectWindow, Step};
 
 #[derive(Debug, thiserror::Error)]
 pub enum EngineError {
@@ -408,9 +408,10 @@ pub struct Settings {
     /// FILEDIA: kept for routines that switch file dialogs off around a script; no command reads it yet.
     pub filedia: bool,
     pub pickadd: bool,
-    /// PICKDRAG: false = two-click windows (0), true = clicks or dragging (2).
-    /// Drag-only mode (1) is not supported yet.
-    pub pickdrag: bool,
+    /// PICKDRAG: how a selection window's second corner is given: 0 by a second click, 1 where
+    /// the button that set the first corner comes up (drag), 2 either way (a drag held past the
+    /// press tolerance closes it on release, a click leaves it for the next click).
+    pub pickdrag: u8,
     /// PICKSTYLE: bit 1 picks whole selectable groups ([`cmd::groups`]), bit 2 associative hatches
     /// with their boundaries (stored, not used yet).
     pub pickstyle: u8,
@@ -502,7 +503,7 @@ impl Default for Settings {
             cmdecho: true,
             filedia: true,
             pickadd: true,
-            pickdrag: true,
+            pickdrag: 2,
             pickstyle: 1,
             gripsize: 5.0,
             cursorsize: 5.0,
@@ -692,6 +693,9 @@ pub struct Session {
     pub clipboard: Vec<Entity>,
     pub clipboard_base: Vec2,
     pub pending_window: Option<PendingWindow>,
+    /// The window or polygon the last pick at a command's "Select objects" was made with (`None`
+    /// for single picks, fences, All, Last…); see [`Session::take_pick_area`].
+    pick_area: Option<select::PickArea>,
     /// A selection mode typed at "Select objects" (W, C, F, WP, CP) collecting its points.
     pub select_mode: Option<select::SelectMode>,
     /// SIngle typed at "Select objects": the next selection ends the prompt.
@@ -751,6 +755,7 @@ impl Session {
             clipboard: Vec::new(),
             clipboard_base: Vec2::ZERO,
             pending_window: None,
+            pick_area: None,
             select_mode: None,
             select_single: false,
             untitled_counter: 0,
@@ -1148,6 +1153,7 @@ impl Session {
         if !prompt.accept.select {
             return Ok(Some(input));
         }
+        self.pick_area = None;
         if self.select_mode.is_some() {
             return self.select_mode_input(input);
         }
@@ -1183,7 +1189,9 @@ impl Session {
                 let ap = self.pixel_size() * self.settings.pickbox.max(1.0) * 1.5;
                 if let Some(pw) = self.pending_window.take() {
                     let crossing = p.x < pw.corner.x;
-                    let hs = select::select_window(self.doc()?, &space, Bounds2::new(pw.corner, p), crossing);
+                    let bx = Bounds2::new(pw.corner, p);
+                    let hs = select::select_window(self.doc()?, &space, bx, crossing);
+                    self.pick_area = Some(select::PickArea { area: select::SelectArea::Box(bx), crossing });
                     return Ok(Some(Input::Pick(cmd::groups::expand(self, hs))));
                 }
                 match select::pick(self.doc()?, &space, p, ap) {
@@ -1243,6 +1251,7 @@ impl Session {
         match input {
             Input::Point(p) => {
                 if mode.push(p) {
+                    self.pick_area = mode.area();
                     return Ok(mode.select(self.doc()?, &self.space()).map(Input::Pick));
                 }
             }
@@ -1250,7 +1259,10 @@ impl Session {
                 mode.pts.pop();
             }
             // Enter ends a fence or polygon; with too few points the mode is dropped.
-            Input::Enter => return Ok(mode.select(self.doc()?, &self.space()).map(Input::Pick)),
+            Input::Enter => {
+                self.pick_area = mode.area();
+                return Ok(mode.select(self.doc()?, &self.space()).map(Input::Pick));
+            }
             Input::Cancel | Input::Pick(_) => return Ok(Some(input)),
             _ => self.echo("Invalid point."),
         }
@@ -1476,6 +1488,8 @@ impl Session {
         let mut common = self.current_common();
         let (kinds, mut out) = match (self.lisp_preview(cursor), &self.running) {
             (Some(k), _) => (k, Vec::new()),
+            // A typed fence or polygon at any command's "Select objects" shows its lines so far.
+            (None, Some(_)) if let Some(m) = &self.select_mode => (m.preview(cursor), Vec::new()),
             (None, Some(r)) => (r.machine.preview(self, cursor), r.machine.preview_objects(self, cursor)),
             (None, None) => (Vec::new(), Vec::new()),
         };
@@ -1705,7 +1719,7 @@ impl Session {
     /// command running, or a command asking for objects. Returns whether a window was opened.
     pub fn begin_window(&mut self, corner: Vec2) -> bool {
         let during_command = self.running.is_some();
-        if !self.settings.pickdrag || !corner.is_finite() || (during_command && !self.current_prompt().is_some_and(|p| p.accept.select)) {
+        if self.settings.pickdrag == 0 || !corner.is_finite() || (during_command && !self.current_prompt().is_some_and(|p| p.accept.select)) {
             return false;
         }
         self.pending_window = Some(PendingWindow { corner, during_command });
@@ -1757,6 +1771,24 @@ impl Session {
 
     /// Whether a click at `p` selects objects by picking: no command running, or a command at
     /// "Select objects" with no selection window or typed mode under way.
+    /// The window or polygon the pick just handed to the running command was made with, taken
+    /// so it is read once: STRETCH moves the vertices inside it (#1116). `None` for a single
+    /// pick, a fence, All, Last or Previous.
+    pub fn take_pick_area(&mut self) -> Option<select::PickArea> {
+        self.pick_area.take()
+    }
+
+    /// The selection window waiting for its opposite corner: the one a click on empty space
+    /// opened (with or without a command), or one a command opened itself and asks for the corner
+    /// of ([`Prompt::window`]: TRIM's Crossing, a typed `W` or `C`). The UI draws them all alike
+    /// and lets a drag close them (PICKDRAG).
+    pub fn selection_window(&self) -> Option<SelectWindow> {
+        match self.pending_window {
+            Some(pw) => Some(SelectWindow { corner: pw.corner, crossing: None }),
+            None => self.current_prompt().and_then(|p| p.window),
+        }
+    }
+
     pub fn click_picks(&self) -> bool {
         self.pending_window.is_none()
             && match self.running.as_ref() {
diff --git a/crates/engine/src/prompt.rs b/crates/engine/src/prompt.rs
--- a/crates/engine/src/prompt.rs
+++ b/crates/engine/src/prompt.rs
@@ -66,6 +66,27 @@ pub struct Prompt {
     /// the routine writes, so it cannot be read for the word "Select" (#1598).
     #[serde(skip_serializing_if = "std::ops::Not::not")]
     pub picks_one: bool,
+    /// The prompt asks for the opposite corner of a selection window ([`Prompt::window`]).
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub window: Option<SelectWindow>,
+}
+
+/// A selection window waiting for its opposite corner ([`Prompt::window`],
+/// [`crate::Session::selection_window`]).
+#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SelectWindow {
+    pub corner: Vec2,
+    /// `Some(true)`: always a crossing (TRIM's Crossing, a typed `C`), `Some(false)`: always a
+    /// window (a typed `W`), `None`: a window to the right of the corner, a crossing to its left.
+    pub crossing: Option<bool>,
+}
+
+impl SelectWindow {
+    /// Whether the window from the corner to `p` selects as a crossing.
+    pub fn crossing_to(&self, p: Vec2) -> bool {
+        self.crossing.unwrap_or(p.x < self.corner.x)
+    }
 }
 
 /// Parse `@distance<angle` (relative polar), returning `(distance, angle in radians)`.
@@ -118,6 +139,15 @@ impl Prompt {
         self.corner = true;
         self
     }
+    /// Asks for the opposite corner of a selection window from `corner` (the `window` field):
+    /// the UI draws it as a selection area, and a drag that opened it closes it where the button
+    /// comes up (PICKDRAG). It picks objects, so object snaps and Dynamic Input stay off, and the
+    /// base is a window corner ([`Prompt::corner`]), so ortho never bends it onto an axis (#1556).
+    pub fn window(mut self, corner: Vec2, crossing: Option<bool>) -> Self {
+        self = self.corner(corner);
+        self.window = Some(SelectWindow { corner, crossing });
+        self
+    }
     /// Asks for a distance from the base (the `distance` field).
     pub fn distance(mut self) -> Self {
         self.distance = true;
@@ -133,13 +163,13 @@ impl Prompt {
         self
     }
     /// The prompt picks objects: a "Select objects" prompt, one that says it picks a single object
-    /// ([`Prompt::picks_one`]), or a point prompt whose click selects one ("Select object to
+    /// ([`Prompt::picks_one`]), a selection window's opposite corner ([`Prompt::window`]), or a point prompt whose click selects one ("Select object to
     /// offset", "Select first line"…, the commands that pick a single object themselves). The UI
     /// shows the pick box there. The wording is only read where the engine wrote it: a routine
     /// writes its own message (`raw`), which may open with "Select" and still ask for a point,
     /// and says through `picks_one` when it picks instead (#1635 review).
     pub fn picks_objects(&self) -> bool {
-        self.picks_one || self.accept.select || (self.accept.point && !self.raw && self.message.starts_with("Select"))
+        self.picks_one || self.accept.select || self.window.is_some() || (self.accept.point && !self.raw && self.message.starts_with("Select"))
     }
     /// "Specify next point or [Undo/Close] <default>:".
     pub fn display(&self) -> String {
diff --git a/crates/engine/src/select.rs b/crates/engine/src/select.rs
--- a/crates/engine/src/select.rs
+++ b/crates/engine/src/select.rs
@@ -376,6 +376,32 @@ pub fn select_group(d: &Drawing, space: &Space, name: &str) -> Option<Vec<Handle
     Some(g.members.iter().copied().filter(|h| store.get(*h).is_some_and(|e| d.is_visible(e))).collect())
 }
 
+/// The area a selection was made with: a window's box, or a polygon closed back to its first
+/// point. STRETCH moves the vertices inside it ([`crate::Session::take_pick_area`]).
+#[derive(Clone, Debug, PartialEq)]
+pub enum SelectArea {
+    Box(Bounds2),
+    Polygon(Vec<Vec2>),
+}
+
+impl SelectArea {
+    pub fn contains(&self, p: Vec2) -> bool {
+        match self {
+            SelectArea::Box(bx) => bx.contains(p),
+            SelectArea::Polygon(poly) => point_in_polygon(poly, p),
+        }
+    }
+}
+
+/// The area a pick at a command's "Select objects" was made with, and whether it selected as a
+/// crossing (objects inside or crossing it) or a window (objects inside it):
+/// [`crate::Session::take_pick_area`].
+#[derive(Clone, Debug, PartialEq)]
+pub struct PickArea {
+    pub area: SelectArea,
+    pub crossing: bool,
+}
+
 /// A selection mode typed at "Select objects", collecting its points before it selects.
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub enum SelectKind {
@@ -434,7 +460,14 @@ impl SelectMode {
         };
         match last {
             None => Prompt::new(first, Accept::POINT),
-            Some(p) if self.is_box() => Prompt::new(next, Accept::POINT).corner(p),
+            Some(p) if self.is_box() => Prompt::new(next, Accept::POINT).window(
+                p,
+                match self.kind {
+                    SelectKind::Window => Some(false),
+                    SelectKind::Crossing => Some(true),
+                    _ => None,
+                },
+            ),
             Some(p) => Prompt::new(next, Accept::POINT).kw(&["Undo"]).base(p),
         }
     }
@@ -445,6 +478,39 @@ impl SelectMode {
         }
         self.is_box() && self.pts.len() >= 2
     }
+    /// The fence or polygon so far, on to the cursor `c`; a polygon closes back to its start. A
+    /// window's corners are drawn by the UI as a selection area ([`Prompt::window`]).
+    pub fn preview(&self, c: Vec2) -> Vec<EntityKind> {
+        use crate::cmd::helpers::line;
+        let Some(first) = self.pts.first().copied() else { return Vec::new() };
+        if !matches!(self.kind, SelectKind::Fence | SelectKind::WPolygon | SelectKind::CPolygon) {
+            return Vec::new();
+        }
+        let mut out: Vec<EntityKind> = self.pts.windows(2).filter_map(|w| Some(line(*w.first()?, *w.get(1)?))).collect();
+        out.extend(self.pts.last().map(|l| line(*l, c)));
+        if self.kind != SelectKind::Fence && self.pts.len() >= 2 {
+            out.push(line(c, first));
+        }
+        out
+    }
+    /// The area the mode selects with once it has its points; a fence or a group marks none.
+    pub fn area(&self) -> Option<PickArea> {
+        let (area, crossing) = match (self.kind, self.pts.as_slice()) {
+            (SelectKind::Window | SelectKind::Crossing | SelectKind::Box, [a, b, ..]) => {
+                let crossing = match self.kind {
+                    SelectKind::Crossing => true,
+                    SelectKind::Box => b.x < a.x,
+                    _ => false,
+                };
+                (SelectArea::Box(Bounds2::new(*a, *b)), crossing)
+            }
+            (SelectKind::WPolygon | SelectKind::CPolygon, pts) if pts.len() >= 3 => {
+                (SelectArea::Polygon(pts.to_vec()), self.kind == SelectKind::CPolygon)
+            }
+            _ => return None,
+        };
+        Some(PickArea { area, crossing })
+    }
     /// The objects the mode selects, or `None` while it has too few points.
     pub fn select(&self, d: &Drawing, space: &Space) -> Option<Vec<Handle>> {
         match (self.kind, self.pts.as_slice()) {
diff --git a/crates/engine/src/sysvars.rs b/crates/engine/src/sysvars.rs
--- a/crates/engine/src/sysvars.rs
+++ b/crates/engine/src/sysvars.rs
@@ -135,7 +135,7 @@ pub fn get(s: &Session, name: &str) -> Option<Value> {
         "CMDECHO" => json!(i32::from(st.cmdecho)),
         "FILEDIA" => json!(i32::from(st.filedia)),
         "PICKADD" => json!(i32::from(st.pickadd)),
-        "PICKDRAG" => json!(i32::from(st.pickdrag) * 2),
+        "PICKDRAG" => json!(st.pickdrag),
         "PICKSTYLE" => json!(st.pickstyle),
         "GRIPSIZE" => json!(st.gripsize),
         "CURSORSIZE" => json!(st.cursorsize),
@@ -353,7 +353,7 @@ pub fn set(s: &mut Session, name: &str, v: &Value) -> Result<()> {
         "CMDECHO" => st.cmdecho = small(v, 1).ok_or_else(bad)? == 1,
         "FILEDIA" => st.filedia = small(v, 1).ok_or_else(bad)? == 1,
         "PICKADD" => st.pickadd = as_bool(v).ok_or_else(bad)?,
-        "PICKDRAG" => st.pickdrag = as_i(v).filter(|b| matches!(b, 0 | 2)).map(|b| b == 2).ok_or_else(bad)?,
+        "PICKDRAG" => st.pickdrag = small(v, 2).ok_or_else(bad)?,
         "PICKSTYLE" => st.pickstyle = as_i(v).filter(|b| (0..=3).contains(b)).and_then(|b| u8::try_from(b).ok()).ok_or_else(bad)?,
         "GRIPSIZE" => st.gripsize = as_f64(v).ok_or_else(bad)?.clamp(1.0, 255.0),
         "CURSORSIZE" => st.cursorsize = as_f64(v).ok_or_else(bad)?.clamp(1.0, 100.0),
diff --git a/crates/engine/tests/it/main.rs b/crates/engine/tests/it/main.rs
--- a/crates/engine/tests/it/main.rs
+++ b/crates/engine/tests/it/main.rs
@@ -110,6 +110,7 @@ mod select_similar;
 mod select_underscore_keywords;
 mod selection_cycling;
 mod selection_modes;
+mod selection_windows;
 mod setvar_cecolor;
 mod setvar_coverage;
 mod shapes;
diff --git a/crates/engine/tests/it/pickdrag.rs b/crates/engine/tests/it/pickdrag.rs
--- a/crates/engine/tests/it/pickdrag.rs
+++ b/crates/engine/tests/it/pickdrag.rs
@@ -11,15 +11,20 @@ fn pickdrag_is_saved_reset_and_validated() {
     let saved = options::profile(&s);
     let mut restored = Session::new();
     options::restore(&mut restored, &saved);
-    assert!(!restored.settings.pickdrag);
-    for value in [json!(1), json!(-1), json!(3), json!("bad"), json!(null), json!([0]), json!(1e300)] {
+    assert_eq!(restored.settings.pickdrag, 0);
+    for value in [json!(-1), json!(3), json!("bad"), json!(null), json!([0]), json!(1e300)] {
         assert!(s.execute("options", &json!({"PICKDRAG":value})).is_err());
         assert_eq!(options::profile(&s), saved);
     }
     s.execute("options", &json!({"reset":true})).unwrap();
-    assert!(s.settings.pickdrag);
-    options::restore(&mut restored, &json!({"PICKDRAG":1}));
-    assert!(!restored.settings.pickdrag, "unsupported saved modes are ignored");
+    assert_eq!(s.settings.pickdrag, 2);
+    // Drag-only (1) is a mode like the others.
+    s.execute("options", &json!({"PICKDRAG":1})).unwrap();
+    assert_eq!(options::profile(&s)["PICKDRAG"], 1);
+    options::restore(&mut restored, &options::profile(&s));
+    assert_eq!(restored.settings.pickdrag, 1);
+    options::restore(&mut restored, &json!({"PICKDRAG":7}));
+    assert_eq!(restored.settings.pickdrag, 1, "unsupported saved modes are ignored");
 }
 
 #[test]
diff --git a/crates/engine/tests/it/selection_windows.rs b/crates/engine/tests/it/selection_windows.rs
new file mode 100644
--- /dev/null
+++ b/crates/engine/tests/it/selection_windows.rs
@@ -0,0 +1,187 @@
+//! One selection for every command (#1116, #1001 follow-up): STRETCH selects through the
+//! session's "Select objects" like MOVE or ERASE, and every selection window waiting for its
+//! opposite corner, whoever opened it, is reported alike (`Session::selection_window`), so the UI
+//! draws it and lets a drag close it the same way everywhere.
+
+use cadcraft_engine::Session;
+use cadcraft_engine::doc::{EntityKind, Handle};
+use cadcraft_engine::geom::Vec2;
+use cadcraft_engine::prompt::Input;
+use cadcraft_engine::select::{PickArea, SelectArea};
+use serde_json::json;
+
+fn session() -> Session {
+    let mut s = Session::new();
+    s.viewport_px = (1000.0, 1000.0);
+    s.execute("view.set", &json!({"center": [10, 5], "height": 40})).unwrap();
+    s.settings.osmode = 0;
+    s
+}
+
+fn line(s: &mut Session, a: [f64; 2], b: [f64; 2]) -> Handle {
+    let h = s.execute("line", &json!({ "points": [a, b] })).unwrap()["handles"][0].as_str().unwrap().to_string();
+    Handle::parse_hex(&h).unwrap()
+}
+
+fn ends(s: &Session, h: Handle) -> (Vec2, Vec2) {
+    match &s.doc().unwrap().entity(h).unwrap().kind {
+        EntityKind::Line(l) => (l.a.xy(), l.b.xy()),
+        _ => panic!("not a line"),
+    }
+}
+
+fn point(s: &mut Session, x: f64, y: f64) {
+    s.input(Input::Point(Vec2::new(x, y))).unwrap();
+}
+
+#[test]
+fn stretch_selects_at_the_sessions_select_objects_prompt() {
+    let mut s = session();
+    let a = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
+    s.start("stretch").unwrap();
+    let p = s.current_prompt().unwrap();
+    assert!(p.accept.select, "STRETCH asks for objects like every other command: {p:?}");
+    // A click on empty space opens the session's window; right to left it is a crossing.
+    point(&mut s, 12.0, 2.0);
+    let w = s.selection_window().expect("a window waits for its opposite corner");
+    assert_eq!(w.corner, Vec2::new(12.0, 2.0));
+    assert!(w.crossing_to(Vec2::new(8.0, -2.0)));
+    point(&mut s, 8.0, -2.0);
+    assert!(s.selection_window().is_none());
+    assert_eq!(s.selection(), vec![a]);
+    s.input(Input::Enter).unwrap();
+    point(&mut s, 0.0, 0.0);
+    point(&mut s, 5.0, 0.0);
+    assert!(s.running.is_none(), "{}", s.prompt_text());
+    assert_eq!(ends(&s, a), (Vec2::new(0.0, 0.0), Vec2::new(15.0, 0.0)), "only the end inside the crossing moves");
+}
+
+#[test]
+fn stretch_moves_picked_objects_whole_and_removes_with_r() {
+    let mut s = session();
+    let a = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
+    let b = line(&mut s, [0.0, 5.0], [10.0, 5.0]);
+    s.start("stretch").unwrap();
+    // A pick, then a crossing over both right ends, then remove the second line with a pick.
+    point(&mut s, 5.0, 0.0);
+    point(&mut s, 12.0, 7.0);
+    point(&mut s, 8.0, -2.0);
+    s.input(Input::Text("R".into())).unwrap();
+    assert_eq!(s.current_prompt().unwrap().message, "Remove objects");
+    point(&mut s, 5.0, 5.0);
+    s.input(Input::Enter).unwrap();
+    point(&mut s, 0.0, 0.0);
+    point(&mut s, 0.0, 3.0);
+    assert!(s.running.is_none(), "{}", s.prompt_text());
+    // Picked first, so it moves whole even though the crossing also took it.
+    assert_eq!(ends(&s, a), (Vec2::new(0.0, 3.0), Vec2::new(10.0, 3.0)));
+    assert_eq!(ends(&s, b), (Vec2::new(0.0, 5.0), Vec2::new(10.0, 5.0)), "removed");
+}
+
+#[test]
+fn the_area_a_pick_was_made_with_is_read_once() {
+    let mut s = session();
+    line(&mut s, [0.0, 0.0], [10.0, 0.0]);
+    s.start("erase").unwrap();
+    point(&mut s, 5.0, 0.0);
+    assert_eq!(s.take_pick_area(), None, "a single pick marks no area");
+    point(&mut s, -2.0, -2.0);
+    point(&mut s, 12.0, 2.0);
+    let bx = match s.take_pick_area() {
+        Some(PickArea { area: SelectArea::Box(bx), crossing: false }) => bx,
+        other => panic!("left to right is a window: {other:?}"),
+    };
+    assert!(bx.contains(Vec2::new(0.0, 0.0)) && bx.contains(Vec2::new(10.0, 0.0)));
+    assert_eq!(s.take_pick_area(), None);
+    s.input(Input::Text("CP".into())).unwrap();
+    for (x, y) in [(-1.0, -1.0), (11.0, -1.0), (5.0, 3.0)] {
+        point(&mut s, x, y);
+    }
+    s.input(Input::Enter).unwrap();
+    assert!(matches!(s.take_pick_area(), Some(PickArea { area: SelectArea::Polygon(p), crossing: true }) if p.len() == 3));
+}
+
+#[test]
+fn typed_window_and_crossing_corners_are_selection_windows() {
+    let mut s = session();
+    s.start("erase").unwrap();
+    for (kw, crossing) in [("W", Some(false)), ("C", Some(true)), ("BOX", None)] {
+        s.input(Input::Text(kw.into())).unwrap();
+        assert!(s.selection_window().is_none(), "no corner yet");
+        point(&mut s, 0.0, 0.0);
+        let w = s.selection_window().expect("the opposite corner is a selection window");
+        assert_eq!((w.corner, w.crossing), (Vec2::ZERO, crossing), "{kw}");
+        let p = s.current_prompt().unwrap();
+        assert!(!cadcraft_engine::pointmod::accepts(&p), "{kw}: a window corner picks objects, no object snaps");
+        point(&mut s, 3.0, 3.0);
+        assert!(s.selection_window().is_none());
+    }
+}
+
+#[test]
+fn trims_crossing_window_is_a_selection_window() {
+    let mut s = session();
+    line(&mut s, [0.0, 0.0], [20.0, 0.0]);
+    line(&mut s, [10.0, -5.0], [10.0, 5.0]);
+    s.start("trim").unwrap();
+    // A click on empty space at "Select object to trim" opens TRIM's crossing window.
+    point(&mut s, 15.0, 2.0);
+    let w = s.selection_window().expect("TRIM's crossing waits for its opposite corner");
+    assert_eq!((w.corner, w.crossing), (Vec2::new(15.0, 2.0), Some(true)), "always a crossing");
+    assert!(!cadcraft_engine::pointmod::accepts(&s.current_prompt().unwrap()), "no object snaps at the corner");
+    point(&mut s, 18.0, -2.0);
+    assert!(s.selection_window().is_none());
+}
+
+#[test]
+fn a_typed_fence_shows_its_lines_at_any_select_objects() {
+    let mut s = session();
+    s.start("erase").unwrap();
+    s.input(Input::Text("F".into())).unwrap();
+    point(&mut s, 0.0, 0.0);
+    point(&mut s, 5.0, 0.0);
+    let pv = s.preview(Vec2::new(5.0, 5.0));
+    assert_eq!(pv.len(), 2, "the fence so far and on to the cursor: {pv:?}");
+}
+
+/// A crossing made at STRETCH's "Select objects" moves only the multiline vertex inside it (#1739),
+/// whether clicked or typed (`CP`).
+#[test]
+fn a_stretch_crossing_takes_a_multiline_by_its_vertex() {
+    let vertices = |s: &Session| -> Vec<(f64, f64)> {
+        s.doc()
+            .unwrap()
+            .model
+            .iter()
+            .find_map(|e| match &e.kind {
+                EntityKind::MLine(m) => Some(m.vertices.iter().map(|v| (v.p.x, v.p.y)).collect()),
+                _ => None,
+            })
+            .unwrap()
+    };
+    let mut s = Session::new();
+    s.viewport_px = (1000.0, 1000.0);
+    // Zoomed in so the clicks 5 from the vertex are on empty space, not a pick of the multiline.
+    s.execute("view.set", &json!({"center": [3000, 300], "height": 500})).unwrap();
+    s.settings.osmode = 0;
+    // The element lines run 100 away from the middle vertex at (3000, 0).
+    s.execute("mline", &json!({ "points": [[0, 0], [3000, 0], [3000, 2000]], "scale": 200 })).unwrap();
+    s.start("stretch").unwrap();
+    point(&mut s, 3005.0, 5.0);
+    point(&mut s, 2995.0, -5.0);
+    s.input(Input::Enter).unwrap();
+    point(&mut s, 0.0, 0.0);
+    point(&mut s, 0.0, 610.0);
+    assert!(s.running.is_none(), "{}", s.prompt_text());
+    assert_eq!(vertices(&s), vec![(0.0, 0.0), (3000.0, 610.0), (3000.0, 2000.0)]);
+    s.start("stretch").unwrap();
+    s.input(Input::Text("CP".into())).unwrap();
+    for (x, y) in [(2995.0, 605.0), (3005.0, 605.0), (3000.0, 615.0)] {
+        point(&mut s, x, y);
+    }
+    s.input(Input::Enter).unwrap();
+    s.input(Input::Enter).unwrap();
+    point(&mut s, 0.0, 0.0);
+    point(&mut s, 0.0, -610.0);
+    assert_eq!(vertices(&s), vec![(0.0, 0.0), (3000.0, 0.0), (3000.0, 2000.0)]);
+}
diff --git a/crates/engine/tests/stretch_window_ortho.rs b/crates/engine/tests/stretch_window_ortho.rs
--- a/crates/engine/tests/stretch_window_ortho.rs
+++ b/crates/engine/tests/stretch_window_ortho.rs
@@ -27,7 +27,8 @@ fn ortho_session() -> Session {
 
 /// Pick the window's opposite corner at `to`, stretch by 5,0 and return the extents.
 fn stretch_from_corner(mut s: Session, to: Vec2) -> (Vec2, Vec2) {
-    assert_eq!(s.current_prompt().unwrap().message, "Specify opposite corner");
+    // The session's window (implied) or the typed mode's (`C`) waits for its opposite corner.
+    assert!(s.selection_window().is_some(), "a selection window waits at {:?}", s.current_prompt().map(|p| p.message));
     let c = hover(&mut s, to);
     assert!(c.near(to, 1e-9), "the corner is where the cursor is: {c:?}");
     s.input(Input::Point(c)).unwrap();
diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs
--- a/crates/ui-egui/src/canvas.rs
+++ b/crates/ui-egui/src/canvas.rs
@@ -119,8 +119,8 @@ pub struct CanvasState {
     hover_at: Option<Pos2>,
     /// Constraint glyphs for the parametric overlay (cached per drawing revision).
     pub param: crate::parametric::Cache,
-    /// A selection window opened by a press-and-drag, to close where the button is let go.
-    drag_window: bool,
+    /// The primary-button press being held, when it was acted on (see [`press`]).
+    press: Option<press::Press>,
     /// The right-click that opened the shortcut menu held Shift or Ctrl: it is the object snap
     /// menu ([`crate::context_menu`]).
     pub snap_menu: bool,
@@ -1524,12 +1524,12 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
         if !in_active {
             app.canvas.hot_grips.clear();
             app.session.pending_window = None;
-            app.canvas.drag_window = false;
+            app.canvas.press = None;
             if app.run("mspace", serde_json::json!({"at": [pp.x, pp.y]})).is_err() && active_vp.is_some() {
                 let _ = app.run("pspace", serde_json::json!({}));
             }
             app.session.pending_window = None;
-            app.canvas.drag_window = false;
+            app.canvas.press = None;
             app.canvas.list = None;
             return;
         }
@@ -1558,77 +1558,89 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
         hatch_dialog = true;
     }
 
-    // While a command asks for a point, every click places one, however fast the drafting (#815).
-    // egui drops a click whose button comes up a few pixels from where it went down (a drag to it),
-    // and eframe hands a slow frame's clicks over together, which egui reports as one.
-    let point_prompt = app.session.current_prompt().is_some_and(|p| cadcraft_engine::pointmod::accepts(&p))
-        && app.session.pending_window.is_none()
-        && !app.session.realtime_zoom_active();
-    let pressed_primary = pressed_primary || (point_prompt && resp.drag_stopped_by(egui::PointerButton::Primary));
-    if point_prompt && inside && pressed_primary && !middle_down && app.cmd.options.is_none() && app.cmd.buffer.is_empty() {
-        let releases: Vec<egui::Pos2> = ui.input(|i| {
-            i.events
-                .iter()
-                .filter_map(|e| match e {
-                    egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, .. } => Some(*pos),
-                    _ => None,
-                })
-                .collect()
-        });
-        // The last one is placed below, at the cursor, like any click.
-        if let Some((_, earlier)) = releases.split_last() {
-            for pos in earlier.iter().filter(|p| rect.contains(**p)) {
-                let w = effective_point(app, xf.to_world(*pos), &xf, ui.ctx());
-                let result = match app.canvas.snap {
-                    Some(hit) => app.session.input_snapped(hit),
-                    None => app.session.input(Input::Point(w)),
+    // Clicks count when the button goes down, where it went down, however fast the mouse moves
+    // (#1001, #815): each press among this frame's events places its point, picks its object or
+    // sets a selection window's corner at once. What the pointer does while the button is held
+    // only decides whether the press becomes a drag ([`press`]): a drag closes the window its
+    // press opened where the button comes up; after a click the window waits for the next press.
+    // Presses on grips (idle) and while ZOOM Realtime runs are left to the clicks further down.
+    // Selection cycling's list closes when the prompt, the drawing or the view changes.
+    let key: CycleKey = (
+        app.session.running.as_ref().map(|r| r.id.clone()),
+        app.session.current_prompt().map(|p| p.message),
+        app.session.state().map_or(0, |st| st.uid),
+        app.session.state().map_or(0, |st| st.revision),
+        xf.center,
+        xf.scale,
+    );
+    if app.canvas.cycle_key.as_ref() != Some(&key) {
+        app.canvas.cycle = None;
+        app.canvas.hover_cycle = None;
+        app.canvas.cycle_key = Some(key);
+    }
+    let (steps, now, primary_down) = ui.input(|i| (press::steps(&i.events), i.time, i.pointer.primary_down()));
+    let mut click_used = false;
+    for step in steps {
+        match step {
+            press::Step::Down(pos, m) => {
+                app.canvas.press = None;
+                if !inside || !rect.contains(pos) || middle_down {
+                    continue;
+                }
+                // A press on the Dynamic Input options menu is the menu's (picked on release);
+                // elsewhere it closes the menu and counts as usual.
+                if app.cmd.options.is_some() {
+                    if app.cmd.option_at(pos).is_some() {
+                        continue;
+                    }
+                    app.cmd.options = None;
+                }
+                // Selection cycling: the press picks a row of the open list, or opens the list over
+                // overlapping objects (#1149). Its release must not count again below.
+                if cycle_click(app, &xf, Some(pos), Some(xf.to_world(pos)), m.shift) {
+                    app.canvas.press = Some(press::Press::new(pos, false));
+                    continue;
+                }
+                if press_takes(app, &xf, pos) {
+                    let opened = press_at(app, &xf, ui.ctx(), pos, m);
+                    app.canvas.press = Some(press::Press::new(pos, opened));
+                }
+            }
+            press::Step::Move(pos) => {
+                if let Some(p) = &mut app.canvas.press {
+                    p.moved(pos, now);
+                }
+            }
+            press::Step::Up(pos) => {
+                let Some(mut p) = app.canvas.press.take() else { continue };
+                click_used = true;
+                // The release closes the selection window its press opened: always with PICKDRAG 1,
+                // after a drag with 2; with 0 a window takes two clicks (#1032). Any selection
+                // window closes so: the session's, or a command's own (TRIM's Crossing, a typed
+                // W or C).
+                let closes = match app.session.settings.pickdrag {
+                    1 => true,
+                    2 => p.released_as_drag(pos, now),
+                    _ => false,
                 };
-                if let Err(e) = result {
-                    app.session.echo(e.to_string());
+                if closes && p.opened_window && app.session.selection_window().is_some() {
+                    let end = xf.to_world(pos);
+                    let done = if app.session.prompting() { app.session.input(Input::Point(end)) } else { app.session.idle_click(end, mods.shift) };
+                    if let Err(e) = done {
+                        app.session.echo(e.to_string());
+                    }
                 }
             }
         }
     }
+    if !primary_down {
+        // The button came up where we didn't see it (outside the window, say).
+        app.canvas.press = None;
+    }
 
     let raw_world = hover_pos.filter(|_| inside).map(|p| xf.to_world(p));
     let dyn_frame = crate::cmdline::dyn_frame(app);
-    if let Some(w) = raw_world {
-        let mut eff = effective_point(app, w, &xf, ui.ctx());
-        // A locked Dynamic Input value (`40,` or `40<` typed) holds the cursor and overrides snaps.
-        if let Some(f) = dyn_frame
-            && let Some(e) = crate::dyninput::parse(&app.cmd.buffer, &f)
-            && e.first_locked().is_some()
-        {
-            eff = crate::dyninput::constrain(&e, &f, eff, app.cmd.second_locked());
-            app.canvas.snap = None;
-            app.canvas.track = None;
-        }
-        // Grip dimensional input: typed lengths and angles hold the grip and override snaps.
-        let grip_held = crate::grip_dyn::constrain(app, eff);
-        if let Some(q) = grip_held {
-            eff = q;
-            app.canvas.snap = None;
-            app.canvas.track = None;
-        }
-        app.canvas.cursor = Some(eff);
-        app.session.cursor = eff;
-        app.session.cursor_deferred = app.canvas.snap.and_then(|h| h.deferred);
-        // Typed or menu snap overrides (END, NON…) and Tab cycling; a locked Dynamic Input value wins.
-        if dyn_frame.and_then(|f| crate::dyninput::parse(&app.cmd.buffer, &f)).is_none_or(|e| e.first_locked().is_none()) {
-            override_cursor(app, w, &xf);
-        }
-        // Typed grip dimensions win over snap overrides too.
-        if let Some(q) = grip_held {
-            app.canvas.cursor = Some(q);
-            app.session.cursor = q;
-            app.canvas.snap = None;
-            app.session.cursor_deferred = None;
-        }
-    } else {
-        app.canvas.cursor = None;
-        app.canvas.snap = None;
-        app.session.cursor_deferred = None;
-    }
+    track_cursor(app, raw_world, &xf, ui.ctx(), dyn_frame);
 
     // A click on the Dynamic Input options menu picks the keyword; elsewhere it closes the menu.
     let mut picked = false;
@@ -1646,26 +1658,14 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
     let idle_dbl = dbl_primary && app.session.running.is_none();
     if idle_dbl {
         app.session.pending_window = None;
-        app.canvas.drag_window = false;
+        app.canvas.press = None;
     }
     let pressed_primary = pressed_primary && !picked && !hatch_dialog && !idle_dbl;
-    let key: CycleKey = (
-        app.session.running.as_ref().map(|r| r.id.clone()),
-        app.session.current_prompt().map(|p| p.message),
-        app.session.state().map_or(0, |st| st.uid),
-        app.session.state().map_or(0, |st| st.revision),
-        xf.center,
-        xf.scale,
-    );
-    if app.canvas.cycle_key.as_ref() != Some(&key) {
-        app.canvas.cycle = None;
-        app.canvas.hover_cycle = None;
-        app.canvas.cycle_key = Some(key);
-    }
-    let cycled = pressed_primary && !middle_down && cycle_click(app, &xf, hover_pos, raw_world.filter(|_| inside), mods.shift);
-    // Clicks.
+    let cycled = pressed_primary && !click_used && !middle_down && cycle_click(app, &xf, hover_pos, raw_world.filter(|_| inside), mods.shift);
+    // Clicks the presses above left alone: on grips, and during ZOOM Realtime.
     if inside
         && pressed_primary
+        && !click_used
         && !middle_down
         && !cycled
         && let Some(p) = app.canvas.cursor
@@ -1697,36 +1697,6 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
             app.session.echo(e.to_string());
         }
     }
-    // Press-and-drag selection window (PICKDRAG = 2; click-click still works): a drag
-    // that starts while objects are being selected opens a window where the button went down,
-    // and letting go closes it. Drags on grips and drags while drawing are left alone.
-    if !app.session.settings.pickdrag {
-        app.canvas.drag_window = false;
-    }
-    if app.session.settings.pickdrag
-        && resp.drag_started_by(egui::PointerButton::Primary)
-        && !middle_down
-        && app.canvas.cycle.is_none()
-        && app.session.pending_window.is_none()
-        && let Some(origin) = ui.input(|i| i.pointer.press_origin())
-        && rect.contains(origin)
-        && grip_at(app, &xf, Some(origin)).is_none()
-        && crate::dyngrips::at(app, &xf, Some(origin)).is_none()
-    {
-        app.canvas.drag_window = app.session.begin_window(xf.to_world(origin));
-    }
-    if app.canvas.drag_window && resp.drag_stopped_by(egui::PointerButton::Primary) {
-        app.canvas.drag_window = false;
-        if app.session.pending_window.is_some()
-            && let Some(end) = ui.input(|i| i.pointer.interact_pos())
-        {
-            let end = xf.to_world(end);
-            let done = if app.session.prompting() { app.session.input(Input::Point(end)) } else { app.session.idle_click(end, mods.shift) };
-            if let Err(e) = done {
-                app.session.echo(e.to_string());
-            }
-        }
-    }
     // Right-click acts as Enter while a command runs (the classic CAD default; held, it opens the
     // command menu); otherwise it opens the shortcut menu (Repeat, Clipboard, …).
     // Shift/Ctrl+right-click opens the object snap menu.
@@ -1752,7 +1722,7 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
     // The cursor picks objects (pick box, rollover highlight): "Select objects" and single-object
     // prompts, or no command with PICKFIRST on; SELECTIONPREVIEW says in which of those it shows.
     let selecting = app.session.previews_selection();
-    if inside && selecting && app.session.pending_window.is_none() {
+    if inside && selecting && app.session.selection_window().is_none() {
         if hover_pos != app.canvas.hover_at
             && let (Some(w), Ok(st)) = (raw_world, app.session.state())
         {
@@ -1994,12 +1964,12 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
             draw_list(&painter, &xf, &list, bg, false);
         }
     }
-    // Pending selection window.
-    if let (Some(pw), Some(c)) = (app.session.pending_window, raw_world) {
-        let a = xf.to_screen(pw.corner);
+    // The selection window waiting for its opposite corner, whoever opened it.
+    if let (Some(w), Some(c)) = (app.session.selection_window(), raw_world) {
+        let a = xf.to_screen(w.corner);
         let b = xf.to_screen(c);
         let r = Rect::from_two_pos(a, b);
-        let crossing = c.x < pw.corner.x;
+        let crossing = w.crossing_to(c);
         if let Some(fill) = selection_area_fill(&app.session.settings, crossing) {
             painter.rect_filled(r, 0.0, fill);
         }
@@ -2066,6 +2036,95 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
     // spin the CPU with the low-latency present mode (`gpu::surface_config`).
 }
 
+/// Put the cursor at `raw` (world, `None` off the canvas): snaps, ortho and polar, a locked
+/// Dynamic Input value and typed grip dimensions applied (`app.canvas.cursor`, `.snap`, `.track`).
+fn track_cursor(app: &mut CadApp, raw: Option<Vec2>, xf: &Xf, ctx: &egui::Context, dyn_frame: Option<crate::dyninput::Frame>) {
+    if let Some(w) = raw {
+        let mut eff = effective_point(app, w, xf, ctx);
+        // A locked Dynamic Input value (`40,` or `40<` typed) holds the cursor and overrides snaps.
+        if let Some(f) = dyn_frame
+            && let Some(e) = crate::dyninput::parse(&app.cmd.buffer, &f)
+            && e.first_locked().is_some()
+        {
+            eff = crate::dyninput::constrain(&e, &f, eff, app.cmd.second_locked());
+            app.canvas.snap = None;
+            app.canvas.track = None;
+        }
+        // Grip dimensional input: typed lengths and angles hold the grip and override snaps.
+        let grip_held = crate::grip_dyn::constrain(app, eff);
+        if let Some(q) = grip_held {
+            eff = q;
+            app.canvas.snap = None;
+            app.canvas.track = None;
+        }
+        app.canvas.cursor = Some(eff);
+        app.session.cursor = eff;
+        app.session.cursor_deferred = app.canvas.snap.and_then(|h| h.deferred);
+        // Typed or menu snap overrides (END, NON…) and Tab cycling; a locked Dynamic Input value wins.
+        if dyn_frame.and_then(|f| crate::dyninput::parse(&app.cmd.buffer, &f)).is_none_or(|e| e.first_locked().is_none()) {
+            override_cursor(app, w, xf);
+        }
+        // Typed grip dimensions win over snap overrides too.
+        if let Some(q) = grip_held {
+            app.canvas.cursor = Some(q);
+            app.session.cursor = q;
+            app.canvas.snap = None;
+            app.session.cursor_deferred = None;
+        }
+    } else {
+        app.canvas.cursor = None;
+        app.canvas.snap = None;
+        app.session.cursor_deferred = None;
+    }
+}
+
+/// Whether the press at `pos` is acted on when the button goes down ([`press_at`]): at any prompt
+/// and on the drawing with no command running. Grips (with no command running) and ZOOM Realtime
+/// keep their clicks.
+fn press_takes(app: &mut CadApp, xf: &Xf, pos: Pos2) -> bool {
+    if app.session.realtime_zoom_active() {
+        return false;
+    }
+    if app.session.prompting() {
+        return true;
+    }
+    crate::dyngrips::at(app, xf, Some(pos)).is_none() && grip_at(app, xf, Some(pos)).is_none()
+}
+
+/// Act on a press at `pos`: at a prompt the point goes in (snapped as the cursor would be there);
+/// with no command running it picks the object there or sets a selection window's corner.
+/// Returns whether the press opened a selection window ([`cadcraft_engine::Session::selection_window`]).
+fn press_at(app: &mut CadApp, xf: &Xf, ctx: &egui::Context, pos: Pos2, mods: egui::Modifiers) -> bool {
+    let had_window = app.session.selection_window().is_some();
+    let raw = xf.to_world(pos);
+    let dyn_frame = crate::cmdline::dyn_frame(app);
+    track_cursor(app, Some(raw), xf, ctx, dyn_frame);
+    let Some(p) = app.canvas.cursor else { return false };
+    if app.session.prompting() {
+        // Ctrl held while picking the point of a grip edit makes copies (the Copy option).
+        if mods.ctrl && app.session.running.as_ref().is_some_and(|r| r.id == "grip") {
+            let _ = app.session.input(Input::Keyword("Copy".into()));
+        }
+        // Clicking places the point; a value typed into the Dynamic Input boxes is used up.
+        if let Some(f) = dyn_frame
+            && crate::dyninput::parse(&app.cmd.buffer, &f).is_some()
+        {
+            app.cmd.buffer.clear();
+        }
+        // Preserve a snap's viewport origin for DIST, and its deferred curve where needed.
+        let result = match app.canvas.snap {
+            Some(hit) => app.session.input_snapped(hit),
+            None => app.session.input(Input::Point(p)),
+        };
+        if let Err(e) = result {
+            app.session.echo(e.to_string());
+        }
+    } else if let Err(e) = app.session.idle_click(raw, mods.shift) {
+        app.session.echo(e.to_string());
+    }
+    !had_window && app.session.selection_window().is_some()
+}
+
 /// The model space tiles on screen (none without tiling): each tile's rectangle in `full`.
 /// Start TEXTEDIT on the text, mtext or attribute definition at `p` (the objects its shortcut menu
 /// offers Edit Text for); false when there is none.
@@ -2954,6 +3013,7 @@ mod blockedit;
 mod compare;
 
 /// Background rebuilds of the display list for a new view.
+mod press;
 mod rebuild;
 
 #[cfg(test)]
diff --git a/crates/ui-egui/src/canvas/press.rs b/crates/ui-egui/src/canvas/press.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/canvas/press.rs
@@ -0,0 +1,131 @@
+//! Press-first clicks (#1001): a click on the canvas counts when the primary button goes down, at
+//! the spot where it went down, however fast the mouse is moving. Where the pointer goes while the
+//! button is held only decides whether the press also becomes a drag:
+//!
+//! - holding still, or staying within [`TOLERANCE`] of the press, never makes a drag;
+//! - once the pointer leaves the tolerance a timer starts; a button still down [`DRAG_DELAY`]
+//!   later is a drag (a selection window the press opened closes where the button comes up);
+//! - a button that comes up sooner was a click (the window waits for the next press).
+//!
+//! The timer starts on leaving the tolerance, not on the press: pressing, holding still and then
+//! moving and letting go quickly is still a click, as in AutoCAD.
+
+use egui::{Modifiers, Pos2};
+
+/// How far (points) the pointer may wander from the press before the timer starts.
+pub(crate) const TOLERANCE: f32 = 6.0;
+/// How long (seconds) the button must stay down after the pointer left [`TOLERANCE`] for the
+/// press to be a drag.
+pub(crate) const DRAG_DELAY: f64 = 0.25;
+
+/// A primary-button press being held.
+#[derive(Clone, Copy, Debug)]
+pub(crate) struct Press {
+    origin: Pos2,
+    /// When the pointer first went further than [`TOLERANCE`] from `origin` (seconds).
+    left_at: Option<f64>,
+    /// The press opened a selection window (set its first corner).
+    pub(crate) opened_window: bool,
+}
+
+impl Press {
+    pub(crate) fn new(origin: Pos2, opened_window: bool) -> Self {
+        Self { origin, left_at: None, opened_window }
+    }
+
+    /// The pointer is at `pos` at time `now`.
+    pub(crate) fn moved(&mut self, pos: Pos2, now: f64) {
+        if self.left_at.is_none() && pos.distance(self.origin) > TOLERANCE {
+            self.left_at = Some(now);
+        }
+    }
+
+    /// Whether the button coming up at `pos` at time `now` ends a drag rather than a click.
+    pub(crate) fn released_as_drag(&mut self, pos: Pos2, now: f64) -> bool {
+        self.moved(pos, now);
+        self.left_at.is_some_and(|t| now - t >= DRAG_DELAY)
+    }
+}
+
+/// A primary-button event of the frame, in order.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub(crate) enum Step {
+    Down(Pos2, Modifiers),
+    Move(Pos2),
+    Up(Pos2),
+}
+
+/// The primary-button presses, pointer moves and releases among a frame's events, in order. A slow
+/// frame can hold several clicks; each press is its own step (#815).
+pub(crate) fn steps(events: &[egui::Event]) -> Vec<Step> {
+    events
+        .iter()
+        .filter_map(|e| match *e {
+            egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, modifiers } => Some(Step::Down(pos, modifiers)),
+            egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, .. } => Some(Step::Up(pos)),
+            egui::Event::PointerMoved(pos) => Some(Step::Move(pos)),
+            _ => None,
+        })
+        .collect()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use egui::pos2;
+
+    #[test]
+    fn staying_within_the_tolerance_is_a_click_however_long_the_button_is_held() {
+        let mut p = Press::new(pos2(100.0, 100.0), true);
+        p.moved(pos2(104.0, 103.0), 0.5);
+        assert!(!p.released_as_drag(pos2(105.0, 100.0), 5.0));
+    }
+
+    #[test]
+    fn letting_go_soon_after_leaving_the_tolerance_is_a_click() {
+        let mut p = Press::new(pos2(100.0, 100.0), true);
+        p.moved(pos2(112.0, 100.0), 1.0);
+        assert!(!p.released_as_drag(pos2(130.0, 100.0), 1.0 + DRAG_DELAY / 2.0));
+    }
+
+    #[test]
+    fn holding_still_then_moving_and_letting_go_quickly_is_a_click() {
+        // The timer starts when the pointer leaves the tolerance, not when the button goes down.
+        let mut p = Press::new(pos2(100.0, 100.0), true);
+        p.moved(pos2(101.0, 100.0), 2.0);
+        assert!(!p.released_as_drag(pos2(140.0, 100.0), 2.05));
+    }
+
+    #[test]
+    fn a_button_still_down_after_the_delay_is_a_drag() {
+        let mut p = Press::new(pos2(100.0, 100.0), true);
+        p.moved(pos2(110.0, 100.0), 1.0);
+        p.moved(pos2(200.0, 150.0), 1.1);
+        assert!(p.released_as_drag(pos2(250.0, 160.0), 1.0 + DRAG_DELAY));
+    }
+
+    #[test]
+    fn steps_keep_every_press_of_a_slow_frame_in_order() {
+        let at = |x| pos2(x, 10.0);
+        let button =
+            |x, pressed| egui::Event::PointerButton { pos: at(x), button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
+        let events = vec![
+            egui::Event::PointerMoved(at(1.0)),
+            button(1.0, true),
+            button(1.0, false),
+            egui::Event::PointerButton { pos: at(2.0), button: egui::PointerButton::Secondary, pressed: true, modifiers: Modifiers::NONE },
+            button(3.0, true),
+            button(3.0, false),
+        ];
+        assert_eq!(
+            steps(&events),
+            vec![
+                Step::Move(at(1.0)),
+                Step::Down(at(1.0), Modifiers::NONE),
+                Step::Up(at(1.0)),
+                Step::Down(at(3.0), Modifiers::NONE),
+                Step::Up(at(3.0))
+            ]
+        );
+    }
+}
diff --git a/crates/ui-egui/src/options/dialog.rs b/crates/ui-egui/src/options/dialog.rs
--- a/crates/ui-egui/src/options/dialog.rs
+++ b/crates/ui-egui/src/options/dialog.rs
@@ -370,7 +370,7 @@ fn selection(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>)
             changes.insert("PICKADD".into(), json!(i32::from(!shift_adds)));
         }
         let mut grouping = st.pickstyle & 1 != 0;
-        let mut two_clicks = !st.pickdrag;
+        let mut two_clicks = st.pickdrag == 0;
         if left
             .checkbox(&mut two_clicks, crate::tl!("Select using two clicks only"))
             .on_hover_text(crate::tl!("Define each corner of the selection window with a separate click."))
diff --git a/crates/ui-egui/tests/it/main.rs b/crates/ui-egui/tests/it/main.rs
--- a/crates/ui-egui/tests/it/main.rs
+++ b/crates/ui-egui/tests/it/main.rs
@@ -22,9 +22,11 @@ mod native_windows;
 mod options_dialog;
 mod parameters_rename;
 mod pickdrag;
+mod press_first_clicks;
 mod properties_units;
 mod rapid_clicks;
 mod selection_cycling;
+mod selection_windows;
 mod shared_controls;
 mod shortcut_menus;
 mod status_bar_toggles;
diff --git a/crates/ui-egui/tests/it/pickdrag.rs b/crates/ui-egui/tests/it/pickdrag.rs
--- a/crates/ui-egui/tests/it/pickdrag.rs
+++ b/crates/ui-egui/tests/it/pickdrag.rs
@@ -45,11 +45,11 @@ fn selection_tab_checkbox_updates_and_persists_the_preference() {
     h.run_steps(3);
     h.get_by_label("Select using two clicks only").click();
     h.run_steps(2);
-    assert!(!h.state().session.settings.pickdrag);
+    assert_eq!(h.state().session.settings.pickdrag, 0);
     let saved = h.state().profile_json();
     let mut restored = CadApp::new(Session::new(), Services::default());
     restored.load_profile(&saved);
-    assert!(!restored.session.settings.pickdrag);
+    assert_eq!(restored.session.settings.pickdrag, 0);
     if let Some(dir) = std::env::var_os("CADCRAFT_SELECTION_SHOTS") {
         let dir = std::path::PathBuf::from(dir);
         std::fs::create_dir_all(&dir).unwrap();
@@ -57,7 +57,7 @@ fn selection_tab_checkbox_updates_and_persists_the_preference() {
     }
     h.get_by_label("Select using two clicks only").click();
     h.run_steps(2);
-    assert!(h.state().session.settings.pickdrag);
+    assert_eq!(h.state().session.settings.pickdrag, 2);
 }
 
 #[test]
@@ -83,13 +83,11 @@ fn drag_is_disabled_but_two_clicks_still_complete_the_window() {
         button(&mut h, a, true);
         button(&mut h, a, false);
         assert!(h.state().session.pending_window.is_some());
-        // A held-button movement after the first click must not consume the second corner.
-        drag(&mut h, a, b);
-        assert!(h.state().session.pending_window.is_some());
-        assert!(h.state().session.selection().is_empty());
-        h.run_steps(30); // Separate deliberate clicks from egui's double-click interval.
-        button(&mut h, b, true);
-        button(&mut h, b, false);
+        // The second corner is where the button goes down (#1001): a drag on from there, let go
+        // elsewhere, changes nothing.
+        h.run_steps(30); // Separate deliberate clicks from the double-click interval.
+        let away = xf.to_screen(Vec2::new(30.0, 8.0));
+        drag(&mut h, b, away);
         assert!(h.state().session.pending_window.is_none());
         assert_eq!(h.state().session.selection().len(), 1);
     }
diff --git a/crates/ui-egui/tests/it/press_first_clicks.rs b/crates/ui-egui/tests/it/press_first_clicks.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/tests/it/press_first_clicks.rs
@@ -0,0 +1,189 @@
+//! Press-first clicks (#1001): a click counts when the button goes down, where it went down, even
+//! while the mouse is moving; holding the button after the pointer left a small tolerance makes a
+//! drag. The harness runs at 60 frames a second and each queued event is one frame, so the number
+//! of moves between press and release is how long the button was held.
+
+use cadcraft_engine::Session;
+use cadcraft_engine::doc::{Drawing, EntityKind, Space};
+use cadcraft_engine::geom::Vec2;
+use cadcraft_engine::prompt::Input;
+use cadcraft_ui_egui::{CadApp, Services};
+use egui::{Event, Modifiers, PointerButton, Pos2, vec2};
+use egui_kittest::Harness;
+use serde_json::json;
+
+fn harness() -> Harness<'static, CadApp> {
+    let mut session = Session::empty();
+    session.open_drawing(Drawing::new_imperial(), "Press first", None);
+    session.execute("view.set", &json!({"center":[50,50],"height":100})).unwrap();
+    session.settings.osmode = 0;
+    session.settings.snapmode = false;
+    session.settings.orthomode = false;
+    session.execute("line", &json!({"points": [[20, 20], [60, 20]]})).unwrap();
+    let mut app = CadApp::new(session, Services::default());
+    app.canvas.zoom_pending = false;
+    let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).with_step_dt(1.0 / 60.0).build_ui_state(
+        |ui, app: &mut CadApp| {
+            app.logic(ui.ctx());
+            app.ui(ui);
+        },
+        app,
+    );
+    h.run_steps(5);
+    h
+}
+
+fn screen(h: &Harness<'_, CadApp>, p: Vec2) -> Pos2 {
+    h.state().canvas.xf.unwrap().to_screen(p)
+}
+
+fn button(h: &mut Harness<'_, CadApp>, pos: Pos2, pressed: bool) {
+    h.event(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
+}
+
+/// Press at `at`, then move `dx` points to the right over `frames` frames and let go there.
+fn moving_click(h: &mut Harness<'_, CadApp>, at: Pos2, dx: f32, frames: usize) {
+    h.event(Event::PointerMoved(at));
+    button(h, at, true);
+    for i in 1..=frames {
+        h.event(Event::PointerMoved(at + vec2(dx * i as f32 / frames as f32, 0.0)));
+    }
+    button(h, at + vec2(dx, 0.0), false);
+    h.run_steps(2);
+}
+
+/// A click that moves 15 points while the button is down, let go within two frames.
+fn quick_moving_click(h: &mut Harness<'_, CadApp>, at: Vec2) {
+    let pos = screen(h, at);
+    moving_click(h, pos, 15.0, 2);
+}
+
+fn start(h: &mut Harness<'_, CadApp>, id: &str) {
+    h.state_mut().start(id);
+    h.run_steps(2);
+    assert_eq!(h.state().session.running.as_ref().map(|r| r.id.as_str()), Some(id));
+}
+
+fn enter(h: &mut Harness<'_, CadApp>) {
+    h.state_mut().session.input(Input::Enter).unwrap();
+    h.run_steps(2);
+}
+
+fn lines(h: &Harness<'_, CadApp>) -> Vec<(Vec2, Vec2)> {
+    let st = h.state().session.state().unwrap();
+    st.doc
+        .space(&Space::Model)
+        .unwrap()
+        .iter()
+        .filter_map(|e| match &e.kind {
+            EntityKind::Line(l) => Some((l.a.xy(), l.b.xy())),
+            _ => None,
+        })
+        .collect()
+}
+
+#[test]
+fn move_takes_the_base_and_second_point_where_the_button_went_down() {
+    let mut h = harness();
+    start(&mut h, "move");
+    quick_moving_click(&mut h, Vec2::new(40.0, 20.0));
+    enter(&mut h);
+    // Base point and second point, both clicked on the move: 15 points is 1.5 drawing units here.
+    quick_moving_click(&mut h, Vec2::new(30.0, 40.0));
+    quick_moving_click(&mut h, Vec2::new(50.0, 40.0));
+    let got = lines(&h);
+    assert_eq!(got.len(), 1, "{got:?}");
+    assert!(got[0].0.near(Vec2::new(40.0, 20.0), 1e-3) && got[0].1.near(Vec2::new(80.0, 20.0), 1e-3), "moved by (20, 0): {got:?}");
+}
+
+#[test]
+fn an_object_clicked_on_the_move_is_picked_with_no_command_running() {
+    let mut h = harness();
+    quick_moving_click(&mut h, Vec2::new(40.0, 20.0));
+    assert_eq!(h.state().session.selection().len(), 1);
+    assert!(h.state().session.pending_window.is_none());
+}
+
+#[test]
+fn an_object_clicked_on_the_move_is_picked_at_select_objects() {
+    let mut h = harness();
+    start(&mut h, "erase");
+    quick_moving_click(&mut h, Vec2::new(40.0, 20.0));
+    enter(&mut h);
+    assert!(lines(&h).is_empty(), "the line was picked and erased");
+}
+
+#[test]
+fn both_corners_of_a_click_click_window_count_while_the_mouse_moves() {
+    let mut h = harness();
+    // Lower left, then upper right: a window around the whole line.
+    quick_moving_click(&mut h, Vec2::new(10.0, 10.0));
+    assert!(h.state().session.pending_window.is_some(), "a quick click leaves the window open for the second corner");
+    quick_moving_click(&mut h, Vec2::new(70.0, 30.0));
+    assert!(h.state().session.pending_window.is_none());
+    assert_eq!(h.state().session.selection().len(), 1);
+}
+
+#[test]
+fn holding_the_button_while_moving_drags_a_window() {
+    let mut h = harness();
+    // Half a second of dragging from lower left to upper right of the line.
+    let from = screen(&h, Vec2::new(10.0, 10.0));
+    let to = screen(&h, Vec2::new(70.0, 30.0));
+    h.event(Event::PointerMoved(from));
+    button(&mut h, from, true);
+    for i in 1..=30 {
+        h.event(Event::PointerMoved(from + (to - from) * (i as f32 / 30.0)));
+    }
+    button(&mut h, to, false);
+    h.run_steps(2);
+    assert!(h.state().session.pending_window.is_none(), "letting go closes the dragged window");
+    assert_eq!(h.state().session.selection().len(), 1);
+}
+
+#[test]
+fn holding_still_then_moving_and_letting_go_quickly_is_a_click() {
+    let mut h = harness();
+    let at = screen(&h, Vec2::new(10.0, 10.0));
+    h.event(Event::PointerMoved(at));
+    button(&mut h, at, true);
+    // Held in place for half a second: no drag yet.
+    h.run_steps(30);
+    h.event(Event::PointerMoved(at + vec2(40.0, 0.0)));
+    button(&mut h, at + vec2(40.0, 0.0), false);
+    h.run_steps(2);
+    assert!(h.state().session.pending_window.is_some(), "still waiting for the second corner");
+}
+
+#[test]
+fn two_window_corners_clicked_within_one_frame_both_count() {
+    let mut h = harness();
+    let (a, b) = (screen(&h, Vec2::new(10.0, 10.0)), screen(&h, Vec2::new(70.0, 30.0)));
+    // A slow frame: both clicks arrive before the next one is drawn.
+    for pos in [a, b] {
+        h.input_mut().events.push(Event::PointerMoved(pos));
+        for pressed in [true, false] {
+            h.input_mut().events.push(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
+        }
+    }
+    h.run_steps(2);
+    assert!(h.state().session.pending_window.is_none());
+    assert_eq!(h.state().session.selection().len(), 1);
+}
+
+#[test]
+fn cpolygon_points_clicked_on_the_move_all_count() {
+    // #1546: a narrow crossing polygon over the line's right end (x 58.6–59.9, the line ends at
+    // 60), each point clicked while moving. Taken where the buttons came up (1.5 units further
+    // right) it would miss the line.
+    let mut h = harness();
+    start(&mut h, "erase");
+    h.state_mut().session.input(Input::Keyword("CP".into())).unwrap();
+    h.run_steps(2);
+    for p in [Vec2::new(58.6, 15.0), Vec2::new(59.9, 15.0), Vec2::new(59.9, 25.0), Vec2::new(58.6, 25.0)] {
+        quick_moving_click(&mut h, p);
+    }
+    enter(&mut h);
+    enter(&mut h);
+    assert!(lines(&h).is_empty(), "the polygon crossed the line and ERASE removed it");
+}
diff --git a/crates/ui-egui/tests/it/rapid_clicks.rs b/crates/ui-egui/tests/it/rapid_clicks.rs
--- a/crates/ui-egui/tests/it/rapid_clicks.rs
+++ b/crates/ui-egui/tests/it/rapid_clicks.rs
@@ -88,11 +88,12 @@ fn a_click_made_while_the_mouse_moves_still_places_the_point() {
     let down = screen(&h, Vec2::new(40.0, 10.0));
     button(&mut h, down, true);
     h.run_steps(1);
-    // The hand is already on its way to the next point when the button comes up.
+    // The hand is already on its way to the next point when the button comes up; the point is
+    // where the button went down (#1001), not where the hand got to.
     let up = down + vec2(12.0, 0.0);
     button(&mut h, up, false);
     h.run_steps(2);
     let got = lines(&h);
     assert_eq!(got.len(), 1, "{got:?}");
-    assert!(got[0].1.near(h.state().canvas.xf.unwrap().to_world(up), 1e-4), "{got:?}");
+    assert!(got[0].1.near(h.state().canvas.xf.unwrap().to_world(down), 1e-4), "{got:?}");
 }
diff --git a/crates/ui-egui/tests/it/selection_windows.rs b/crates/ui-egui/tests/it/selection_windows.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/tests/it/selection_windows.rs
@@ -0,0 +1,171 @@
+//! STRETCH and TRIM select like every other command (#1001 follow-up, #1116): a press counts at
+//! once, a click-click window waits for the next press, and a drag closes the window its press
+//! opened where the button comes up (PICKDRAG 2), whether the window is the session's or TRIM's
+//! own crossing. The harness runs at 60 frames a second; each queued event is one frame.
+
+use cadcraft_engine::Session;
+use cadcraft_engine::doc::{Drawing, EntityKind, Space};
+use cadcraft_engine::geom::Vec2;
+use cadcraft_engine::prompt::Input;
+use cadcraft_ui_egui::{CadApp, Services};
+use egui::{Event, Modifiers, PointerButton, Pos2, vec2};
+use egui_kittest::Harness;
+use serde_json::json;
+
+/// A horizontal line from (20, 20) to (60, 20) and a vertical one at x = 40 crossing it.
+fn session() -> Session {
+    let mut s = Session::empty();
+    s.open_drawing(Drawing::new_imperial(), "Selection windows", None);
+    s.execute("view.set", &json!({"center":[50,50],"height":100})).unwrap();
+    s.settings.osmode = 0;
+    s.settings.snapmode = false;
+    s.settings.orthomode = false;
+    s.execute("line", &json!({"points": [[20, 20], [60, 20]]})).unwrap();
+    s.execute("line", &json!({"points": [[40, 10], [40, 30]]})).unwrap();
+    s
+}
+
+fn harness() -> Harness<'static, CadApp> {
+    let mut app = CadApp::new(session(), Services::default());
+    app.canvas.zoom_pending = false;
+    let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).with_step_dt(1.0 / 60.0).build_ui_state(
+        |ui, app: &mut CadApp| {
+            app.logic(ui.ctx());
+            app.ui(ui);
+        },
+        app,
+    );
+    h.run_steps(5);
+    h
+}
+
+fn screen(h: &Harness<'_, CadApp>, p: Vec2) -> Pos2 {
+    h.state().canvas.xf.unwrap().to_screen(p)
+}
+
+fn button(h: &mut Harness<'_, CadApp>, pos: Pos2, pressed: bool) {
+    h.event(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
+}
+
+/// Press at `a`, move to `b` over `frames` frames and let go there (30 frames: half a second, a
+/// drag; 2 frames: a click made on the move).
+fn sweep(h: &mut Harness<'_, CadApp>, a: Vec2, b: Vec2, frames: usize) {
+    let (from, to) = (screen(h, a), screen(h, b));
+    h.event(Event::PointerMoved(from));
+    button(h, from, true);
+    for i in 1..=frames {
+        h.event(Event::PointerMoved(from + (to - from) * (i as f32 / frames as f32)));
+    }
+    button(h, to, false);
+    h.run_steps(2);
+}
+
+fn start(h: &mut Harness<'_, CadApp>, id: &str) {
+    h.state_mut().start(id);
+    h.run_steps(2);
+    assert_eq!(h.state().session.running.as_ref().map(|r| r.id.as_str()), Some(id));
+}
+
+fn input(h: &mut Harness<'_, CadApp>, i: Input) {
+    h.state_mut().session.input(i).unwrap();
+    h.run_steps(2);
+}
+
+fn lines(s: &Session) -> Vec<(Vec2, Vec2)> {
+    let mut v: Vec<(Vec2, Vec2)> = s
+        .state()
+        .unwrap()
+        .doc
+        .space(&Space::Model)
+        .unwrap()
+        .iter()
+        .filter_map(|e| match &e.kind {
+            EntityKind::Line(l) => Some((l.a.xy(), l.b.xy())),
+            _ => None,
+        })
+        .collect();
+    v.sort_by(|p, q| (p.0.x, p.0.y, p.1.x, p.1.y).partial_cmp(&(q.0.x, q.0.y, q.1.x, q.1.y)).unwrap());
+    v
+}
+
+/// STRETCH the right end of the horizontal line 5 to the right after selecting with `select`.
+fn stretch_with(select: impl Fn(&mut Harness<'_, CadApp>)) -> Vec<(Vec2, Vec2)> {
+    let mut h = harness();
+    start(&mut h, "stretch");
+    select(&mut h);
+    assert!(h.state().session.selection_window().is_none(), "the window closed");
+    input(&mut h, Input::Enter);
+    input(&mut h, Input::Point(Vec2::new(0.0, 0.0)));
+    input(&mut h, Input::Point(Vec2::new(5.0, 0.0)));
+    assert!(h.state().session.running.is_none(), "{}", h.state().session.prompt_text());
+    lines(&h.state().session)
+}
+
+#[test]
+fn a_stretch_window_closes_by_drag_or_by_two_clicks_on_the_move() {
+    let expected = vec![(Vec2::new(20.0, 20.0), Vec2::new(65.0, 20.0)), (Vec2::new(40.0, 10.0), Vec2::new(40.0, 30.0))];
+    // Right to left over the horizontal line's right end only: a crossing.
+    let dragged = stretch_with(|h| sweep(h, Vec2::new(70.0, 25.0), Vec2::new(55.0, 15.0), 30));
+    assert_eq!(dragged, expected, "a drag closes STRETCH's crossing where the button comes up");
+    let clicked = stretch_with(|h| {
+        sweep(h, Vec2::new(70.0, 25.0), Vec2::new(70.5, 25.0), 2);
+        assert!(h.state().session.selection_window().is_some(), "a click leaves the window open");
+        sweep(h, Vec2::new(55.0, 15.0), Vec2::new(57.0, 15.0), 2);
+    });
+    assert_eq!(clicked, expected, "the second corner is where the button went down");
+}
+
+/// TRIM's crossing on the right of the cutting line, made in a session by typed points.
+fn trimmed_by_typed_crossing() -> Vec<(Vec2, Vec2)> {
+    let mut s = session();
+    s.viewport_px = (1600.0, 1000.0);
+    s.start("trim").unwrap();
+    s.input(Input::Point(Vec2::new(55.0, 25.0))).unwrap();
+    s.input(Input::Point(Vec2::new(45.0, 15.0))).unwrap();
+    s.input(Input::Enter).unwrap();
+    lines(&s)
+}
+
+#[test]
+fn trims_crossing_closes_where_a_drag_lets_go() {
+    let mut h = harness();
+    start(&mut h, "trim");
+    sweep(&mut h, Vec2::new(55.0, 25.0), Vec2::new(45.0, 15.0), 30);
+    assert!(h.state().session.selection_window().is_none(), "the drag closed TRIM's crossing");
+    input(&mut h, Input::Enter);
+    let got = lines(&h.state().session);
+    assert_eq!(got, trimmed_by_typed_crossing());
+    assert!(got.iter().all(|(a, b)| a.x <= 40.0 + 1e-9 && b.x <= 40.0 + 1e-9), "the right part is trimmed off: {got:?}");
+}
+
+#[test]
+fn with_pickdrag_off_a_drag_leaves_trims_crossing_open() {
+    let mut h = harness();
+    h.state_mut().run("options", json!({"PICKDRAG": 0})).unwrap();
+    start(&mut h, "trim");
+    sweep(&mut h, Vec2::new(55.0, 25.0), Vec2::new(45.0, 15.0), 30);
+    let w = h.state().session.selection_window().expect("two clicks only: the window waits");
+    assert!(w.corner.dist(Vec2::new(55.0, 25.0)) < 0.5, "{w:?}");
+    sweep(&mut h, Vec2::new(45.0, 15.0), Vec2::new(45.0, 15.0), 1);
+    assert!(h.state().session.selection_window().is_none());
+    input(&mut h, Input::Enter);
+    assert_eq!(lines(&h.state().session), trimmed_by_typed_crossing());
+}
+
+#[test]
+fn with_pickdrag_1_the_release_always_closes_the_window() {
+    // Drag only: even a quick press and release on the move closes the window where the button
+    // comes up, at the session's "Select objects" and at TRIM's crossing alike.
+    let mut h = harness();
+    h.state_mut().run("options", json!({"PICKDRAG": 1})).unwrap();
+    start(&mut h, "stretch");
+    sweep(&mut h, Vec2::new(70.0, 25.0), Vec2::new(55.0, 15.0), 2);
+    assert!(h.state().session.selection_window().is_none());
+    assert_eq!(h.state().session.selection().len(), 1, "the crossing took the horizontal line");
+    h.state_mut().session.cancel();
+    start(&mut h, "trim");
+    sweep(&mut h, Vec2::new(55.0, 25.0), Vec2::new(45.0, 15.0), 2);
+    assert!(h.state().session.selection_window().is_none());
+    input(&mut h, Input::Enter);
+    assert_eq!(lines(&h.state().session), trimmed_by_typed_crossing());
+}
diff --git a/docs/gaps.md b/docs/gaps.md
--- a/docs/gaps.md
+++ b/docs/gaps.md
@@ -1,6 +1,6 @@
 # Where CADCraft falls short of AutoCAD
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (DIMTFILL limits in the `*D` block, EXPLODE and DXF: F7 updated) · **Target:** Autodesk AutoCAD 2027
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (PICKDRAG 1; STRETCH and TRIM share the selection windows: U6 narrowed) · **Target:** Autodesk AutoCAD 2027
 
 Every known shortfall, one row each, ranked within each section by user impact. This is the work
 list: agents pick from here (top of a section first), and remove or update a row in the same PR
@@ -55,7 +55,8 @@ Outlined fill highlights now keep deterministic contour starts and directions ac
 repaints and GPU rebuilds, preventing the dashed preview from appearing to flicker (U6).
 
 Options ▸ Selection can now require two separate clicks for a selection window (PICKDRAG 0)
-or allow clicks and drag (2, default), saved in the user profile. Drag-only mode (1) remains unavailable.
+or allow clicks and drag (2, default), or drag only (1), saved in the user profile. STRETCH and TRIM's
+crossing use the same selection windows as every other command.
 | # | Gap | User impact | Est. |
 |---|---|---|---|
 | U4 | Tab snap cycling offers the closest snap of each mode, not every candidate. Typed snap overrides (`END`, `MID` … `NON`), the Shift/Ctrl+right-click snap menu and Tab cycling landed (#393), the menu's Temporary track point in #441 | Low | 1 |
@@ -164,6 +165,7 @@ Detail: [localization-parity.md](localization-parity.md).
 | 2026-10-15 | minor | BREAKLINE (Tools ▸ Express Tools ▸ Break-line Symbol): one polyline between two points with a break symbol, Block/Size/Extension options, size times DIMSCALE: E3 narrowed |
 | 2026-10-15 | minor | TXT2MTXT (Tools ▸ Express Tools ▸ Convert Text to Mtext): selected TEXT, and MTEXT, combined into one MTEXT top-down or in selection order with optional word wrap, or one MTEXT per TEXT; SEttings remembered for the session: E3 narrowed |
 | 2026-10-13 | minor | Selection window gesture preference (PICKDRAG 0/2) added to Options ▸ Selection and saved in the profile (U6), with Portuguese and Ukrainian labels |
+| 2026-10-23 | minor | PICKDRAG 1 (drag only); STRETCH selects through the session's "Select objects", TRIM's crossing and typed W/C corners are selection windows like the session's (U6 narrowed) |
 | 2026-10-12 | minor | Object transparency (entity, ByLayer, ByBlock) is drawn: on screen (TRANSPARENCYDISPLAY, on by default), in PNG/SVG exports, and in plots when the page setup's "Plot transparency" is on (PDF alpha, PNG blending, SVG opacity; PLOTTRANSPARENCYOVERRIDE): F14 narrowed |
 | 2026-10-12 | minor | Hatch and Gradient dialog (HATCH, GRADIENT; HPDLGMODE) and Hatch Edit dialog (HATCHEDIT, right-click, double-click a hatch): F13 narrowed to Preview, Inherit Properties and boundary add/remove in edit mode |
 | 2026-10-12 | minor | A bare system variable name typed at the command line (ORTHOMODE, GRIDMODE…) runs SETVAR on that variable, as in AutoCAD; U10 narrowed |
diff --git a/docs/ui-parity.md b/docs/ui-parity.md
--- a/docs/ui-parity.md
+++ b/docs/ui-parity.md
@@ -1,6 +1,6 @@
 # UI parity
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (distinct polyline midpoint grips) · **Target:** Autodesk AutoCAD 2027 for Mac (26.0)
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (STRETCH and TRIM select like every command; PICKDRAG 1) · **Target:** Autodesk AutoCAD 2027 for Mac (26.0)
 
 How CADCraft's interaction compares with AutoCAD's: point entry, object snaps, tracking, dynamic
 input, grips, selection, the command line, chrome and navigation. A drafter's speed in AutoCAD
@@ -127,7 +127,7 @@ down-arrow options menu (DYNPROMPT), and DYNDIM.
 |---|---|---|
 | Pick, implied window (left→right) and crossing (right→left) | yes | yes, coloured and dashed rectangles (`lib.rs:576-589`, `canvas.rs:823-836`), filled as SELECTIONAREA, WINDOWAREACOLOR, CROSSINGAREACOLOR and SELECTIONAREAOPACITY say (Visual Effect Settings) |
 | Keywords W, C, WP, CP, F, BOX, AU, SI, M, G (group), CL (class) | yes | yes except CLass: W, C, F, WP, CP (#226); BOX, AUto, SIngle, Multiple, Group, Add, Remove, Undo (#428); ALL, Last, Previous |
-| Press-drag selection windows (PICKDRAG) | yes | clicks or drag (2, default), or two clicks only (0); Options ▸ Selection ▸ Select using two clicks only, saved in the profile; drag-only mode (1) is unavailable |
+| Press-drag selection windows (PICKDRAG) | yes | clicks or drag (2, default), two clicks only (0) or drag only (1, the release closes the window); Options ▸ Selection ▸ Select using two clicks only, saved in the profile. Every selection window waiting for its corner, the session's or a command's own (TRIM's Crossing, a typed W or C), is drawn and closed alike. Clicks count when the button goes down, even while the mouse moves, and a press held after the pointer leaves a 6-point tolerance for 0.25 s is a drag; a press on an object picks it, and a drag from it does not open a window (#1001) |
 | Lasso | yes | **no** |
 | Selection cycling (SELECTIONCYCLING) | yes | yes (#1143): SELECTIONCYCLING 2 lists the objects under a click (type, layer; the row under the pointer highlights its object), 1 the badge; Shift+Space steps the highlight through them (#1145); no Ctrl+W |
 | Selection preview, hover highlight | yes | selected and hovered objects redrawn in the selection colour, fills half see-through (SELECTIONEFFECT 1; 0 dashed), selection effect color; HIGHLIGHT (not saved), SELECTIONPREVIEW, PREVIEWEFFECT, PREVIEWFILTER (Visual Effect Settings) |
@@ -211,6 +211,8 @@ down-arrow options menu (DYNPROMPT), and DYNDIM.
 | 2026-10-16 | minor | ALIASEDIT opens the Command Aliases dialog (#1381) |
 | 2026-10-18 | minor | In-window menu bar: once a menu is open, pointing at another title closes it and opens that one (#1567) |
 | 2026-10-13 | minor | Options ▸ Selection adds Select using two clicks only; PICKDRAG 0 disables drag-window creation/completion, 2 preserves clicks or drag, and the preference survives restart; UI pointer and profile regressions |
+| 2026-10-23 | minor | Clicks on the drawing count when the button goes down, at that spot, even while the mouse moves: points (base points too), object picks, both corners of a click-click window and polygon points; holding the button after the pointer leaves a 6-point tolerance for 0.25 s drags a window (PICKDRAG 2); a press on an object picks it, and a drag from it no longer opens a window; selection cycling opens and picks on the press too (#1001) |
+| 2026-10-23 | minor | STRETCH selects at the session's "Select objects" like every other command (#1116): press-first picks, PICKDRAG drags, selection cycling, typed modes; the windows and polygons it selects with mark what stretches. TRIM's crossing and the typed W/C corners are selection windows like the session's: drawn with the selection area, closed by a drag, no object snaps at their corners. A typed fence or polygon shows its lines at any "Select objects". PICKDRAG 1 (drag only) is supported |
 | 2026-10-12 | minor | Selected and hovered objects redrawn in the selection colour (text and arrowheads included); Options ▸ Selection laid out as AutoCAD's: Object grouping, Show grips, grip object limit, selection preview, Visual Effect Settings (selection area, preview filter) |
 | 2026-10-12 | minor | Animation effects (dialog and menu fades, smoothed wheel scrolling and ⌘/Ctrl+wheel zoom) follow the operating system's setting; View ▸ Accessibility ▸ Animation Effects chooses System, On or Off |
 | 2026-10-12 | minor | Hatch and Gradient / Hatch Edit dialogs (HATCH, GRADIENT, HATCHEDIT, double-click a hatch; HPDLGMODE) |
