Files
OneCAD-Draft/src/modules/draw/select.rs
T

279 lines
11 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 {
prompt_cmd: String,
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,
/// Whether the generic selection methods are shown as command buttons.
show_options: 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,
/// Add the command-specific Settings keyword used by Auto Constrain.
auto_constrain_settings: bool,
}
impl SelectObjectsCommand {
/// Standard selection set: accumulate picks, apply on Enter / right-click.
pub fn new(pending_cmd: &str) -> Self {
Self {
prompt_cmd: pending_cmd.to_string(),
pending_cmd: pending_cmd.to_string(),
handles: Vec::new(),
commit_on_enter: true,
show_options: true,
pick: None,
pick_crossing: true,
auto_constrain_settings: false,
}
}
/// Plain gather prompt with no generic selection-method buttons. The
/// visible command name may differ from the private apply command.
pub fn plain(prompt_cmd: &str, pending_cmd: &str) -> Self {
Self {
prompt_cmd: prompt_cmd.to_string(),
pending_cmd: pending_cmd.to_string(),
handles: Vec::new(),
commit_on_enter: true,
show_options: false,
pick: None,
pick_crossing: true,
auto_constrain_settings: false,
}
}
/// 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 {
prompt_cmd: pending_cmd.to_string(),
pending_cmd: pending_cmd.to_string(),
handles: Vec::new(),
commit_on_enter: false,
show_options: false,
pick: None,
pick_crossing: true,
auto_constrain_settings: false,
}
}
pub fn auto_constrain(pending_cmd: &str) -> Self {
let mut command = Self::new(pending_cmd);
command.auto_constrain_settings = true;
command
}
}
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() {
if self.auto_constrain_settings {
return t!(
"%{cmd} Select objects or [Settings] (%{count} selected, Enter to apply):",
cmd = self.prompt_cmd,
count = self.handles.len()
)
.into_owned();
}
t!(
"%{cmd} Select objects (%{count} selected, Enter to apply):",
cmd = self.prompt_cmd,
count = self.handles.len()
)
.into_owned()
} else {
if self.auto_constrain_settings {
return t!(
"%{cmd} Select objects or [Settings]:",
cmd = self.prompt_cmd
)
.into_owned();
}
t!("%{cmd} Select objects:", cmd = self.prompt_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 && self.show_options {
if self.auto_constrain_settings {
return vec![CmdOption::new(t!("Settings").as_ref(), "S")];
}
let options = 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"),
];
options
} 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() {
"S" | "SETTINGS" if self.auto_constrain_settings => {
Some(CmdResult::OpenAutoConstrainSettings)
}
"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