forked from 1b-agent/opencadstudio-upstream
Only Previous and Last were on offer, so Window, Crossing, Fence, the two polygons and All could not be reached at all -- and with no keyboard to type them into, the buttons were the only way in. Window and Crossing fix the sense of the next box. Dragging derives that from the direction the corner travels and would overwrite it the moment the corner moved, so the derivation stands down while a keyword holds it. Add and Remove decide what a pick does for the rest of the round: Remove subtracts the way Shift already does, rather than through PICKADD, which decides whether a pick *replaces* the set and so would have wiped everything gathered so far. Both are dropped when the next command starts, since neither outlives the selection that asked for them. Fence and the polygons are picked point by point, which TRIM and EXTEND already ask for. That gesture -- the points, the rubber-banded dashed preview, whether the path closes -- moves into one place both bind to, and the two preview builders duplicated in trim.rs go with it. A fence is not the polygon test with crossing set: that closes the point list and counts whatever lies inside, so a line drawn past a row of objects would take everything behind it too. It gets a hit test that only counts a real intersection, and skips the area-based geometry entirely. The lasso gathered its hits from wires, hatches, block hatches, meshes and block meshes inline; the new modes need the same list, and a second copy would drift. Both now come through `Scene::path_hit_handles`. Closes #596 Co-Authored-By: Claude Opus 5 <[email protected]>
225 lines
8.7 KiB
Rust
225 lines
8.7 KiB
Rust
// SelectObjectsCommand — generic "select objects then run command" gather phase.
|
|
//
|
|
// Used when a modify command is invoked with nothing pre-selected.
|
|
// The user may single-click, box-select, or polygon-select any number of objects.
|
|
// Picks accumulate into the selection (Shift removes); the set is applied when
|
|
// the user presses Enter or right-clicks (standard "Select objects:" behaviour).
|
|
//
|
|
// Single-object commands (e.g. LAYMCUR) use `instant()` instead: the first
|
|
// completed selection action fires straight away, no Enter required.
|
|
|
|
use acadrust::Handle;
|
|
use glam::DVec3;
|
|
use crate::t;
|
|
|
|
use crate::command::{CadCommand, CmdOption, CmdResult};
|
|
use crate::modules::draw::fence::FencePick;
|
|
use crate::scene::model::wire_model::WireModel;
|
|
|
|
pub struct SelectObjectsCommand {
|
|
pending_cmd: String,
|
|
/// Selection accumulated so far (kept in sync with the scene selection by
|
|
/// each `on_selection_complete` call). Applied on Enter when `commit_on_enter`.
|
|
handles: Vec<Handle>,
|
|
/// When true, gathering continues until Enter / right-click commits the set.
|
|
/// When false, the first completed selection action fires immediately.
|
|
commit_on_enter: bool,
|
|
/// A Fence / WPolygon / CPolygon path being picked point by point. While
|
|
/// one is under way the host must send clicks here as points rather than
|
|
/// treating them as picks, which is what `is_selection_gathering` reports.
|
|
pick: Option<FencePick>,
|
|
/// Whether the polygon being picked takes what it merely touches. Ignored
|
|
/// by a fence, which has no inside to speak of.
|
|
pick_crossing: bool,
|
|
}
|
|
|
|
impl SelectObjectsCommand {
|
|
/// Standard selection set: accumulate picks, apply on Enter / right-click.
|
|
pub fn new(pending_cmd: &str) -> Self {
|
|
Self {
|
|
pending_cmd: pending_cmd.to_string(),
|
|
handles: Vec::new(),
|
|
commit_on_enter: true,
|
|
pick: None,
|
|
pick_crossing: true,
|
|
}
|
|
}
|
|
|
|
/// Single-object variant: the first completed selection action applies
|
|
/// immediately, with no Enter (used by commands that act on one object).
|
|
pub fn instant(pending_cmd: &str) -> Self {
|
|
Self {
|
|
pending_cmd: pending_cmd.to_string(),
|
|
handles: Vec::new(),
|
|
commit_on_enter: false,
|
|
pick: None,
|
|
pick_crossing: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CadCommand for SelectObjectsCommand {
|
|
fn name(&self) -> &'static str {
|
|
"SELECT"
|
|
}
|
|
|
|
fn prompt(&self) -> String {
|
|
if let Some(pick) = &self.pick {
|
|
let label = if !pick.closed {
|
|
t!("Fence")
|
|
} else if self.pick_crossing {
|
|
t!("Crossing polygon")
|
|
} else {
|
|
t!("Window polygon")
|
|
};
|
|
return t!(
|
|
"%{mode}: pick points (%{count} placed, Enter to apply):",
|
|
mode = label,
|
|
count = pick.len()
|
|
)
|
|
.into_owned();
|
|
}
|
|
if self.commit_on_enter && !self.handles.is_empty() {
|
|
t!(
|
|
"%{cmd} Select objects (%{count} selected, Enter to apply):",
|
|
cmd = self.pending_cmd,
|
|
count = self.handles.len()
|
|
)
|
|
.into_owned()
|
|
} else {
|
|
t!("%{cmd} Select objects:", cmd = self.pending_cmd).into_owned()
|
|
}
|
|
}
|
|
|
|
// Opt into the selection-gathering path; host routes clicks through
|
|
// the normal selection system and calls on_selection_complete after each action.
|
|
fn is_selection_gathering(&self) -> bool {
|
|
// A path being picked needs the clicks as points; handing them to the
|
|
// selection system instead would pick objects under each vertex.
|
|
self.pick.is_none()
|
|
}
|
|
|
|
// Clickable selection keywords (#426, #596). Window and Crossing fix the
|
|
// sense of the next box, which dragging would otherwise take from the
|
|
// direction the corner travels; All takes the whole space; Add and Remove
|
|
// decide what a pick does for the rest of the selection; Previous
|
|
// re-selects the set the last command worked on, Last the most recently
|
|
// created object. All are consumed centrally in `try_selection_keyword`, so
|
|
// they work typed as well — these buttons only surface them, which is what
|
|
// the on-screen keyboard-less case needs.
|
|
fn options(&self) -> Vec<CmdOption> {
|
|
if self.commit_on_enter {
|
|
vec![
|
|
CmdOption::new(t!("Window").as_ref(), "W"),
|
|
CmdOption::new(t!("Crossing").as_ref(), "C"),
|
|
CmdOption::new(t!("Fence").as_ref(), "F"),
|
|
CmdOption::new(t!("WPolygon").as_ref(), "WP"),
|
|
CmdOption::new(t!("CPolygon").as_ref(), "CP"),
|
|
CmdOption::new(t!("All").as_ref(), "ALL"),
|
|
CmdOption::new(t!("Add").as_ref(), "A"),
|
|
CmdOption::new(t!("Remove").as_ref(), "R"),
|
|
CmdOption::new(t!("Previous").as_ref(), "P"),
|
|
CmdOption::new(t!("Last").as_ref(), "L"),
|
|
]
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
|
if self.commit_on_enter {
|
|
// Keep gathering: remember the running set and update the prompt.
|
|
// The command is applied when the user presses Enter / right-clicks.
|
|
self.handles = handles;
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
// Single-object variant: apply on the first non-empty selection.
|
|
if handles.is_empty() {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
CmdResult::Relaunch(std::mem::take(&mut self.pending_cmd), handles)
|
|
}
|
|
|
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
|
if let Some(pick) = self.pick.as_mut() {
|
|
pick.push([pt.x, pt.y]);
|
|
}
|
|
CmdResult::NeedPoint
|
|
}
|
|
|
|
fn wants_text_input(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
// Fence and the two polygons are picked here rather than centrally: they
|
|
// need somewhere to accumulate points, and the command is what outlives the
|
|
// individual clicks. The rest of the keywords act at once and are consumed
|
|
// in `try_selection_keyword`, which sees the token first. (#596)
|
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
|
if !self.commit_on_enter {
|
|
return None;
|
|
}
|
|
match text.trim().to_uppercase().as_str() {
|
|
"F" | "FENCE" => {
|
|
self.pick = Some(FencePick::fence());
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
"CP" | "CPOLYGON" => {
|
|
self.pick = Some(FencePick::polygon());
|
|
self.pick_crossing = true;
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
"WP" | "WPOLYGON" => {
|
|
self.pick = Some(FencePick::polygon());
|
|
self.pick_crossing = false;
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
|
|
match &self.pick {
|
|
Some(pick) if !pick.is_empty() => {
|
|
vec![pick.preview([pt.x, pt.y], "select_fence")]
|
|
}
|
|
_ => vec![],
|
|
}
|
|
}
|
|
|
|
// Enter / right-click ends the gather phase and fires the pending command
|
|
// with the accumulated selection. Nothing selected → cancel.
|
|
fn on_enter(&mut self) -> CmdResult {
|
|
// Finishing a path applies it and returns to picking objects; the set
|
|
// it produced joins whatever was already gathered.
|
|
if let Some(pick) = self.pick.take() {
|
|
if pick.is_usable() {
|
|
return CmdResult::SelectByPath {
|
|
path: pick.path(),
|
|
closed: pick.closed,
|
|
crossing: self.pick_crossing,
|
|
};
|
|
}
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
if !self.commit_on_enter || self.handles.is_empty() {
|
|
return CmdResult::Cancel;
|
|
}
|
|
CmdResult::Relaunch(
|
|
std::mem::take(&mut self.pending_cmd),
|
|
std::mem::take(&mut self.handles),
|
|
)
|
|
}
|
|
|
|
fn on_escape(&mut self) -> CmdResult {
|
|
CmdResult::Cancel
|
|
}
|
|
fn on_hover_entity(&mut self, _handle: Handle, _pt: DVec3) -> Vec<WireModel> {
|
|
vec![]
|
|
}
|
|
}
|
|
|
|
|
|
// ── Autocomplete registry ─────────────────────────────────
|
|
inventory::submit!(crate::command::CommandRegistration { names: &["ARRAY", "ARRAYPATH", "ARRAYPOLAR", "ARRAYRECT", "BLOCK", "COPY", "COPYCLIP", "CUTCLIP", "ERASE", "EXPLODE", "GROUP", "LAYFRZ", "LAYLCK", "LAYMCUR", "LAYOFF", "LAYULK", "MIRROR", "MOVE", "ROTATE", "SCALE", "STRETCH"] }); // SelectObjectsCommand
|