cad-editor/src/command.rs

1522 lines
56 KiB
Rust
Raw Normal View History

2026-03-22 14:56:27 +03:00
// Command system — trait for interactive CAD commands.
//
// Each tool that requires user interaction (point picks, object selection,
// numeric input) implements `CadCommand`. The active command receives
// viewport events from main.rs and returns `CmdResult` tokens that tell
// the host what to do next.
use crate::scene::model::hatch_model::HatchModel;
use crate::scene::model::wire_model::WireModel;
use crate::scene::Scene;
2026-03-22 14:56:27 +03:00
use acadrust::{EntityType, Handle};
use glam::DVec3;
2026-03-22 14:56:27 +03:00
/// Domain object resolved under the cursor for ObjectPick snapping.
#[derive(Clone, Copy, Debug)]
pub struct ObjectPickHit {
pub handle: Handle,
pub x: f64,
pub y: f64,
pub label: &'static str,
}
2026-03-22 14:56:27 +03:00
// ── Transform ─────────────────────────────────────────────────────────────
/// A geometric transformation applied to existing entities.
#[derive(Clone)]
pub enum EntityTransform {
/// Move every point by the given world-space delta (world XY plane).
Translate(DVec3),
/// Rotate around `center` by `angle_rad` in the world XY plane.
Rotate { center: DVec3, angle_rad: f64 },
2026-03-22 14:56:27 +03:00
/// Uniform scale from `center` by `factor`.
Scale { center: DVec3, factor: f64 },
/// Mirror across the line through `p1`→`p2` in the world XY plane.
Mirror { p1: DVec3, p2: DVec3 },
2026-03-22 14:56:27 +03:00
}
// ── Tangent object ─────────────────────────────────────────────────────────
/// Geometric representation of a tangent-snap target.
#[derive(Clone, Copy, Debug)]
pub enum TangentObject {
/// Infinite line through two world-space XZ-plane points.
Line { p1: DVec3, p2: DVec3 },
/// Circle in the world XY plane.
Circle { center: DVec3, radius: f64 },
2026-03-22 14:56:27 +03:00
}
/// One unit of input to the active command's step machine.
///
/// Every input source — the GUI command line, the headless automation feeder,
/// dynamic input, the plugin API, and the viewport (clicks / picks / selection
/// / tangent) — translates its raw input into one of these and routes it
/// through `OpenCADStudio::feed_command`, so a single place drives the command
/// regardless of where the step came from.
///
/// Variants are wired up incrementally as each source is migrated onto
/// `feed_command`; `#[allow(dead_code)]` covers those not yet constructed.
#[allow(dead_code)]
pub enum StepInput {
/// A coordinate (`on_point`).
Point(DVec3),
/// A typed token: keyword, option letter, distance, or value
/// (`on_text_input`).
Text(String),
/// An object pick (`on_entity_pick`).
EntityPick(Handle, DVec3),
/// A sub-structure pick — vertex / edge / face (`on_structure_pick`).
StructurePick(Handle, DVec3),
/// A completed selection set (`on_selection_complete`).
SelectionComplete(Vec<Handle>),
/// A tangent-snap target (`on_tangent_point`).
Tangent(TangentObject, DVec3),
/// The text / MText editor closed, with its commit flag (`on_editor_closed`).
EditorClosed(bool),
/// Advance / finish the current step — Enter, or Space-as-Enter (`on_enter`).
Enter,
/// Cancel the command (`on_escape`).
Escape,
}
/// Generic interactive front-end for a single-value setting command (PDMODE,
/// PDSIZE, LTSCALE, CELTSCALE, …). Bare `<name>` enters this command, which
/// prompts for one value and then delegates to the existing inline
/// `<name> <value>` handler via [`CmdResult::Dispatch`]. A bare Enter delegates
/// to `<name> ` (trailing space) so the inline handler reports the current
/// value. Keeps the value math + persistence in the one inline place while
/// making the command prompt for its argument step-by-step.
pub struct ValuePromptCommand {
name: &'static str,
prompt: &'static str,
}
impl ValuePromptCommand {
pub fn new(name: &'static str, prompt: &'static str) -> Self {
Self { name, prompt }
}
}
impl CadCommand for ValuePromptCommand {
fn name(&self) -> &'static str {
self.name
}
fn prompt(&self) -> String {
self.prompt.to_string()
}
fn wants_text_input(&self) -> bool {
true
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let t = text.trim();
if t.is_empty() {
Some(CmdResult::Dispatch(format!("{} ", self.name)))
} else {
Some(CmdResult::Dispatch(format!("{} {t}", self.name)))
}
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
// A value command takes no point; ignore stray clicks, keep prompting.
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
// Bare Enter → report the current value via the inline handler.
CmdResult::Dispatch(format!("{} ", self.name))
}
}
/// Interactive front-end for RENAME. Prompts for the object type (as clickable
/// buttons), then the current name, then the new name, and delegates to the
/// inline `RENAME <type> <old> <new>` handler via [`CmdResult::Dispatch`] — the
/// rename logic stays in one place while the command prompts step by step like
/// every other command instead of only working with all three arguments typed
/// on one line.
pub struct RenameCommand {
step: RenameStep,
}
enum RenameStep {
Type,
Old { ty: String },
New { ty: String, old: String },
}
impl RenameCommand {
/// `(button label, keyword)` for the object types the inline handler can
/// actually rename.
const TYPES: [(&'static str, &'static str); 7] = [
("Layer", "LAYER"),
("Block", "BLOCK"),
("Text style", "STYLE"),
("Dim style", "DIMSTYLE"),
("Linetype", "LINETYPE"),
("UCS", "UCS"),
("View", "VIEW"),
];
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
step: RenameStep::Type,
}
}
}
impl CadCommand for RenameCommand {
fn name(&self) -> &'static str {
"RENAME"
}
fn prompt(&self) -> String {
match &self.step {
RenameStep::Type => "RENAME Select the object type to rename:".to_string(),
RenameStep::Old { ty } => format!("RENAME {ty} Enter the current name:"),
RenameStep::New { ty, old } => format!("RENAME {ty} Rename \"{old}\" to:"),
}
}
fn options(&self) -> Vec<CmdOption> {
match self.step {
RenameStep::Type => Self::TYPES
.iter()
.map(|(label, keyword)| CmdOption::new(label, keyword))
.collect(),
_ => Vec::new(),
}
}
fn wants_text_input(&self) -> bool {
true
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let t = text.trim();
if t.is_empty() {
// Keep prompting for the current step.
return None;
}
// NOTE: a consumed-but-still-prompting input must return
// `Some(NeedPoint)` (reprint the prompt), NOT `None` — the driver
// treats `None` as "not consumed" and offers the same text to the
// command a second time, which would advance two steps at once.
match &self.step {
RenameStep::Type => {
// Accept a known type (or a common alias); re-prompt otherwise so
// a typo doesn't advance to a rename that can't happen.
let canonical = match t.to_uppercase().as_str() {
"LAYER" | "LA" => "LAYER",
"BLOCK" | "B" => "BLOCK",
"STYLE" | "TEXTSTYLE" | "ST" => "STYLE",
"DIMSTYLE" | "D" => "DIMSTYLE",
"LINETYPE" | "LT" => "LINETYPE",
"UCS" => "UCS",
"VIEW" | "V" => "VIEW",
_ => return Some(CmdResult::NeedPoint),
};
self.step = RenameStep::Old {
ty: canonical.to_string(),
};
Some(CmdResult::NeedPoint)
}
RenameStep::Old { ty } => {
self.step = RenameStep::New {
ty: ty.clone(),
old: t.to_string(),
};
Some(CmdResult::NeedPoint)
}
RenameStep::New { ty, old } => {
Some(CmdResult::Dispatch(format!("RENAME {ty} {old} {t}")))
}
}
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
// A rename takes no point; ignore stray clicks and keep prompting.
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
// Bare Enter with nothing typed cancels, like the other prompt commands.
CmdResult::Cancel
}
}
/// Interactive front-end for USERI / USERR — the five integer / real user
/// registers in the drawing header. They take two inputs (which register, then
/// the value), so they prompt for the register as clickable `1`–`5` buttons and
/// then the value, and delegate to the inline `USERI <n> <value>` handler.
pub struct UserRegCommand {
/// `"USERI"` or `"USERR"`.
name: &'static str,
/// Chosen register 1–5 once the first step is answered.
slot: Option<u8>,
}
impl UserRegCommand {
pub fn new(name: &'static str) -> Self {
Self { name, slot: None }
}
}
impl CadCommand for UserRegCommand {
fn name(&self) -> &'static str {
self.name
}
fn prompt(&self) -> String {
match self.slot {
None => format!("{} which register? [1-5]:", self.name),
Some(n) => format!("{}{n} new value:", self.name),
}
}
fn options(&self) -> Vec<CmdOption> {
match self.slot {
None => (1..=5).map(|n| CmdOption::new(&n.to_string(), &n.to_string())).collect(),
Some(_) => Vec::new(),
}
}
fn wants_text_input(&self) -> bool {
true
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let t = text.trim();
if t.is_empty() {
return None;
}
match self.slot {
None => {
// Accept a register 1–5; re-prompt on anything else. Consumed
// either way (`Some(NeedPoint)`) — `None` would feed the same
// text to the command a second time.
if let Ok(n @ 1..=5) = t.parse::<u8>() {
self.slot = Some(n);
}
Some(CmdResult::NeedPoint)
}
// The inline handler validates the value (int vs real) and reports
// usage if it doesn't parse, so just hand the whole line over.
Some(n) => Some(CmdResult::Dispatch(format!("{} {n} {t}", self.name))),
}
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
/// Generic interactive front-end for a keyword sub-verb command (ATTDISP,
/// LAYERSTATE, SCALELISTEDIT, VIEW…). Bare `<name>` enters this command, which
/// shows the sub-verbs as clickable buttons; picking one either dispatches
/// `<name> <keyword>` straight away (an action verb) or, when the verb needs an
/// argument, prompts for it and then dispatches `<name> <keyword> <value>`.
/// Delegates to the existing inline `<name> …` handler so the logic stays in
/// one place, the way [`ValuePromptCommand`] does for single values.
pub struct KeywordCommand {
name: &'static str,
prompt: &'static str,
/// `(button label, keyword, value prompt)`. A `Some` value prompt means the
/// verb takes one argument collected in a second step; `None` acts alone.
options: Vec<(&'static str, &'static str, Option<&'static str>)>,
/// Set once a value-taking verb is chosen: `(keyword, value prompt)`.
pending: Option<(&'static str, &'static str)>,
}
impl KeywordCommand {
pub fn new(
name: &'static str,
prompt: &'static str,
options: Vec<(&'static str, &'static str, Option<&'static str>)>,
) -> Self {
Self {
name,
prompt,
options,
pending: None,
}
}
}
impl CadCommand for KeywordCommand {
fn name(&self) -> &'static str {
self.name
}
fn prompt(&self) -> String {
match self.pending {
Some((_, value_prompt)) => value_prompt.to_string(),
None => self.prompt.to_string(),
}
}
fn options(&self) -> Vec<CmdOption> {
match self.pending {
Some(_) => Vec::new(),
None => self
.options
.iter()
.map(|(label, keyword, _)| CmdOption::new(label, keyword))
.collect(),
}
}
fn wants_text_input(&self) -> bool {
true
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let t = text.trim();
if t.is_empty() {
return None;
}
match self.pending {
// Second step: the argument for the already-chosen verb.
Some((keyword, _)) => Some(CmdResult::Dispatch(format!("{} {keyword} {t}", self.name))),
// First step: match the typed / clicked token to a sub-verb.
// Consumed inputs that keep prompting return `Some(NeedPoint)` —
// `None` would hand the same text to the command a second time.
None => {
let up = t.to_uppercase();
let Some((_, keyword, value_prompt)) =
self.options.iter().find(|(_, k, _)| k.eq_ignore_ascii_case(&up))
else {
// Unknown verb — keep prompting rather than dispatch garbage.
return Some(CmdResult::NeedPoint);
};
match value_prompt {
Some(vp) => {
self.pending = Some((keyword, vp));
Some(CmdResult::NeedPoint)
}
None => Some(CmdResult::Dispatch(format!("{} {keyword}", self.name))),
}
}
}
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
/// Generic interactive front-end for a two-argument command (LAYMRG source +
/// target, SETVAR variable + value…). Prompts for the two values in turn and
/// dispatches `<name> <first> <second>` to the existing inline handler. A bare
/// Enter on the second value dispatches `<name> <first>` (no second token) so a
/// getter-style command (SETVAR reading a variable) still works.
pub struct TwoValuePromptCommand {
name: &'static str,
prompt1: &'static str,
prompt2: &'static str,
first: Option<String>,
}
impl TwoValuePromptCommand {
pub fn new(name: &'static str, prompt1: &'static str, prompt2: &'static str) -> Self {
Self {
name,
prompt1,
prompt2,
first: None,
}
}
}
impl CadCommand for TwoValuePromptCommand {
fn name(&self) -> &'static str {
self.name
}
fn prompt(&self) -> String {
match &self.first {
None => self.prompt1.to_string(),
Some(_) => self.prompt2.to_string(),
}
}
fn wants_text_input(&self) -> bool {
true
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let t = text.trim();
match &self.first {
None => {
if t.is_empty() {
return None;
}
self.first = Some(t.to_string());
// Consumed; `None` here would feed the same text back as the
// second value and dispatch `<name> <x> <x>` in one step.
Some(CmdResult::NeedPoint)
}
Some(first) => Some(CmdResult::Dispatch(if t.is_empty() {
format!("{} {first}", self.name)
} else {
format!("{} {first} {t}", self.name)
})),
}
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
match &self.first {
// Enter on the second step with nothing typed = report / no-op form.
Some(first) => CmdResult::Dispatch(format!("{} {first}", self.name)),
None => CmdResult::Cancel,
}
}
}
/// Generic interactive front-end for a keyword command that operates on the
/// current selection (CHPROP, ADJUST, XDATA, UNDERLAY, DRAWORDER…). If nothing
/// is selected when it starts it first gathers a selection (Enter confirms),
/// then shows the sub-verbs as buttons exactly like [`KeywordCommand`] and
/// dispatches `<name> <verb> [value]` to the inline handler, which reads the
/// (still-selected) set. Verbs the generic form can't express — a second value
/// (XDATA SET) or a reference pick (DRAWORDER ABOVE) — stay available by typing
/// the full argument line.
pub struct SelectThenKeywordCommand {
name: &'static str,
prompt: &'static str,
options: Vec<(&'static str, &'static str, Option<&'static str>)>,
gathering: bool,
selected: Vec<Handle>,
pending: Option<(&'static str, &'static str)>,
}
impl SelectThenKeywordCommand {
pub fn new(
name: &'static str,
prompt: &'static str,
options: Vec<(&'static str, &'static str, Option<&'static str>)>,
has_selection: bool,
) -> Self {
Self {
name,
prompt,
options,
gathering: !has_selection,
selected: Vec::new(),
pending: None,
}
}
}
impl CadCommand for SelectThenKeywordCommand {
fn name(&self) -> &'static str {
self.name
}
fn prompt(&self) -> String {
if self.gathering {
return format!("{} select objects, then press Enter:", self.name);
}
match self.pending {
Some((_, value_prompt)) => value_prompt.to_string(),
None => self.prompt.to_string(),
}
}
fn options(&self) -> Vec<CmdOption> {
if self.gathering || self.pending.is_some() {
return Vec::new();
}
self.options
.iter()
.map(|(label, keyword, _)| CmdOption::new(label, keyword))
.collect()
}
fn wants_text_input(&self) -> bool {
!self.gathering
}
fn is_selection_gathering(&self) -> bool {
self.gathering
}
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
// The normal selection system has set the scene selection; remember the
// set so Enter knows whether anything was picked, and keep gathering.
self.selected = handles;
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
if self.gathering {
if self.selected.is_empty() {
return CmdResult::Cancel;
}
self.gathering = false;
return CmdResult::NeedPoint;
}
CmdResult::Cancel
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let t = text.trim();
if t.is_empty() {
return None;
}
match self.pending {
Some((keyword, _)) => Some(CmdResult::Dispatch(format!("{} {keyword} {t}", self.name))),
None => {
let up = t.to_uppercase();
let Some((_, keyword, value_prompt)) =
self.options.iter().find(|(_, k, _)| k.eq_ignore_ascii_case(&up))
else {
// Unknown verb — consumed, keep prompting (`None` would
// feed the same text to the command a second time).
return Some(CmdResult::NeedPoint);
};
match value_prompt {
Some(vp) => {
self.pending = Some((keyword, vp));
Some(CmdResult::NeedPoint)
}
None => Some(CmdResult::Dispatch(format!("{} {keyword}", self.name))),
}
}
}
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
}
/// Generic interactive front-end for a single-value command that operates on
/// the current selection (HYPERLINK url, ARCTEXT text, TEXTFIT width, TCASE
/// aside…). Gathers a selection first when none is set (Enter confirms), then
/// prompts for one value and dispatches `<name> <value>` to the inline handler,
/// which reads the still-selected set. A bare Enter on the value step dispatches
/// `<name>` alone — for commands whose value is optional (TCOUNT, TEXTMASK).
pub struct SelectThenValueCommand {
name: &'static str,
value_prompt: &'static str,
gathering: bool,
selected: Vec<Handle>,
}
impl SelectThenValueCommand {
pub fn new(name: &'static str, value_prompt: &'static str, has_selection: bool) -> Self {
Self {
name,
value_prompt,
gathering: !has_selection,
selected: Vec::new(),
}
}
}
impl CadCommand for SelectThenValueCommand {
fn name(&self) -> &'static str {
self.name
}
fn prompt(&self) -> String {
if self.gathering {
format!("{} select objects, then press Enter:", self.name)
} else {
self.value_prompt.to_string()
}
}
fn wants_text_input(&self) -> bool {
!self.gathering
}
fn is_selection_gathering(&self) -> bool {
self.gathering
}
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
self.selected = handles;
CmdResult::NeedPoint
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if self.gathering {
return None;
}
let t = text.trim();
if t.is_empty() {
return None;
}
Some(CmdResult::Dispatch(format!("{} {t}", self.name)))
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
if self.gathering {
if self.selected.is_empty() {
return CmdResult::Cancel;
}
self.gathering = false;
return CmdResult::NeedPoint;
}
// Value step, nothing typed: the no-argument form (optional value).
CmdResult::Dispatch(format!("{} ", self.name))
}
}
/// Generic front-end for a two-value command that operates on the current
/// selection (POLYSOLID width + height on a selected polyline). Gathers a
/// selection first when none is set, prompts for two values, and dispatches
/// `<name> <first> <second>` to the inline handler.
pub struct SelectThenTwoValueCommand {
name: &'static str,
prompt1: &'static str,
prompt2: &'static str,
gathering: bool,
selected: Vec<Handle>,
first: Option<String>,
}
impl SelectThenTwoValueCommand {
pub fn new(
name: &'static str,
prompt1: &'static str,
prompt2: &'static str,
has_selection: bool,
) -> Self {
Self {
name,
prompt1,
prompt2,
gathering: !has_selection,
selected: Vec::new(),
first: None,
}
}
}
impl CadCommand for SelectThenTwoValueCommand {
fn name(&self) -> &'static str {
self.name
}
fn prompt(&self) -> String {
if self.gathering {
format!("{} select objects, then press Enter:", self.name)
} else if self.first.is_none() {
self.prompt1.to_string()
} else {
self.prompt2.to_string()
}
}
fn wants_text_input(&self) -> bool {
!self.gathering
}
fn is_selection_gathering(&self) -> bool {
self.gathering
}
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
self.selected = handles;
CmdResult::NeedPoint
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if self.gathering {
return None;
}
let t = text.trim();
if t.is_empty() {
return None;
}
match &self.first {
None => {
self.first = Some(t.to_string());
// Consumed; `None` would feed the same text back as the second
// value and dispatch `<name> <x> <x>` in one step.
Some(CmdResult::NeedPoint)
}
Some(first) => Some(CmdResult::Dispatch(format!("{} {first} {t}", self.name))),
}
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
if self.gathering {
if self.selected.is_empty() {
return CmdResult::Cancel;
}
self.gathering = false;
return CmdResult::NeedPoint;
}
CmdResult::Cancel
}
}
2026-03-22 14:56:27 +03:00
// ── Result token ──────────────────────────────────────────────────────────
/// Returned by every `CadCommand` method to tell main.rs what to do.
#[allow(dead_code)]
pub enum CmdResult {
/// Command is still waiting for the next point; show updated prompt.
NeedPoint,
/// Update the committed-segment wire (normal colour) and keep collecting points.
InterimWire(WireModel),
/// Update the in-progress (cyan) preview wire in the viewport.
Preview(WireModel),
/// Commit an acadrust entity to the document; keep the command active.
CommitEntity(EntityType),
/// Commit an acadrust entity to the document and end the command.
CommitAndExit(EntityType),
/// Commit a Model-tab 3D solid: the acadrust entity (for selection /
/// persistence) plus its truck B-rep (cached for boolean ops + shaded
/// rendering). Ends the command.
CommitSolid {
entity: EntityType,
solid: Box<truck_modeling::Solid>,
},
/// Commit an acadrust entity, end the command, and open the in-place text
/// editor on it (used by MLEADER to type the annotation after placement).
CommitAndEditText(EntityType),
/// Commit several entities, end the command, and open the in-place text
/// editor on the one at `edit_index` (used by LEADER to place the leader
/// line plus an empty MText annotation, then type into the MText).
CommitManyAndEditText {
entities: Vec<EntityType>,
edit_index: usize,
},
2026-03-22 14:56:27 +03:00
/// Create a block definition from existing entities and insert one reference.
CreateBlock {
handles: Vec<Handle>,
name: String,
base: DVec3,
2026-03-22 14:56:27 +03:00
},
/// Apply a transform to selected entities and end the command.
TransformSelected(Vec<Handle>, EntityTransform),
/// Copy selected entities with a transform; command stays active for more copies.
CopySelected(Vec<Handle>, EntityTransform),
/// Commit a hatch fill (stored in Scene::hatches, not the DXF document).
CommitHatch(HatchModel),
/// Copy selected entities with multiple transforms (e.g. rectangular array); end command.
BatchCopy(Vec<Handle>, Vec<EntityTransform>),
/// Erase `handle` and replace with new entities; command stays active.
ReplaceEntity(Handle, Vec<EntityType>),
/// Replace / delete multiple entities and add new ones; command ends.
/// Each pair: (handle_to_erase, replacement_entities) — empty vec = delete only.
ReplaceMany(Vec<(Handle, Vec<EntityType>)>, Vec<EntityType>),
/// Cancel: discard any preview and end the command.
Cancel,
/// End the selection-gather phase and re-dispatch the named command
/// with the gathered handles installed as the active scene selection.
Relaunch(String, Vec<Handle>),
/// End the command and dispatch the given command string. Used by an
/// interactive front-end that gathered its arguments step-by-step and
/// delegates execution to an existing inline command handler (e.g. UCS
/// collecting an option + value, then running `UCS Z 90`). Unlike
/// `Relaunch` it does not touch the selection.
Dispatch(String),
2026-03-22 14:56:27 +03:00
/// Move `dest` entities to the layer of the `src` entity; end command.
MatchEntityLayer { dest: Vec<Handle>, src: Handle },
/// Copy all visual properties (layer/color/linetype/lineweight) from `src` to `dest`; end command.
MatchProperties { dest: Vec<Handle>, src: Handle },
/// Create a named group from the given entity handles; end command.
CreateGroup { handles: Vec<Handle>, name: String },
/// Dissolve all groups that contain any of the given handles; end command.
DeleteGroups { handles: Vec<Handle> },
/// Freeze or thaw layers by name in the given viewport; command stays active.
VpLayerUpdate {
vp_handle: Handle,
freeze: Vec<String>,
thaw: Vec<String>,
},
2026-03-22 14:56:27 +03:00
/// Paste clipboard entities translated so their centroid lands at `base_pt`; end command.
PasteClipboard { base_pt: DVec3 },
/// Zoom the model-space camera to fit the given corner points; end command.
ZoomToWindow { p1: DVec3, p2: DVec3 },
/// Print a measurement result to the command line and end the command.
Measurement(String),
/// Break `handle` at points `p1` and `p2`; replace with computed fragments.
BreakEntity { handle: Handle, p1: DVec3, p2: DVec3 },
/// Attempt to join the given entities into fewer merged entities.
JoinEntities(Vec<Handle>),
/// Apply a polyline-edit operation to one entity; keep command active.
PeditOp {
handle: Handle,
op: crate::modules::draw::modify::pedit::PeditOp,
},
/// Place Point entities at N equal intervals along the entity.
DivideEntity { handle: Handle, n: usize },
/// Place Point entities at `segment_length` intervals along the entity.
MeasureEntity { handle: Handle, segment_length: f64 },
/// Extend/trim a Line or Arc by the given mode; end command.
LengthenEntity {
handle: Handle,
pick_pt: DVec3,
mode: crate::modules::draw::modify::lengthen::LenMode,
},
/// Align selected entities: translate to dst1, rotate by angle_rad, optional scale.
AlignSelected {
handles: Vec<Handle>,
src1: DVec3,
dst1: DVec3,
angle_rad: f64,
scale: f64,
},
/// Set the plot window on the active layout's PlotSettings.
SetPlotWindow { p1: DVec3, p2: DVec3 },
/// Quick-print the bounding box of the given selected entities to a PDF.
QuickPrint(Vec<Handle>),
/// Replace the text content of a Text/MText entity in-place.
DdeditEntity { handle: Handle, new_text: String },
/// Open the in-place editor (plain box or rich MText editor, per type) for
/// a text-bearing entity picked by a command such as DDEDIT.
EditTextEntity { handle: Handle },
/// Open the in-place MText editor (formatting toolbar + multi-line text
/// area with live viewport preview). `handle` is `Some` when editing an
/// existing MText, `None` when creating a new one at `pos`.
OpenMTextEditor {
pos: DVec3,
handle: Option<Handle>,
initial: String,
height: f64,
},
/// Open the in-place single-line TEXT editor (a plain text-entry box, no
/// formatting toolbar). `handle` is `Some` when editing an existing Text,
/// `None` when creating a new one at `pos`.
OpenTextEditor {
pos: DVec3,
handle: Option<Handle>,
initial: String,
height: f64,
},
/// Apply new pattern/scale/angle to an existing hatch entity.
2026-05-12 10:36:15 +03:00
HatcheditApply {
handle: Handle,
name: String,
scale: f32,
angle: f32,
},
/// Implicit STRETCH selection: the crossing window was drawn with no prior
/// selection, so the host resolves which entities it touches and restarts
/// the command at the base-point step with them. (#338)
StretchWindow { win_min: DVec3, win_max: DVec3 },
/// Stretch entities: move only vertices/endpoints inside the crossing window.
StretchEntities {
handles: Vec<Handle>,
/// Min corner of the crossing window in world XZ (= DXF XY).
win_min: DVec3,
/// Max corner of the crossing window in world XZ (= DXF XY).
win_max: DVec3,
/// Translation vector to apply to vertices inside the window.
delta: DVec3,
},
/// Create a Solid3D placeholder entity + associated MeshModel.
/// `mesh_fn` is called with the entity's handle string to build the mesh.
CommitSolid3D {
mesh_fn: Box<dyn FnOnce(String) -> Option<crate::scene::model::mesh_model::MeshModel> + Send>,
},
/// Extrude the profile entity `handle` by `height` along Z.
2026-05-12 10:36:15 +03:00
ExtrudeEntity {
handle: Handle,
height: f64,
2026-05-12 10:36:15 +03:00
color: [f32; 4],
},
/// Revolve the profile entity `handle` around the given axis by `angle_deg`.
RevolveEntity {
handle: Handle,
axis_start: glam::DVec3,
axis_end: glam::DVec3,
angle_deg: f32,
color: [f32; 4],
},
/// Sweep the profile entity `profile_handle` along `path_handle`.
SweepEntity {
profile_handle: Handle,
path_handle: Handle,
color: [f32; 4],
},
/// Loft through a series of profile entities.
LoftEntities {
handles: Vec<Handle>,
color: [f32; 4],
},
/// INSERT landed on a block that has AttributeDefinitions.
/// The host should look up the attdefs for `block_name` from the document
/// and call `attreq_set_attdefs()` on the command, then loop on text input.
AttreqNeeded { block_name: String },
/// Add a command-owned "live" entity to the document mid-command and hand
/// its assigned handle back to the active command via `set_live_handle()`.
/// One undo snapshot is pushed here, so the whole in-progress object reverts
/// as a single unit. The command stays active. Used by PLINE so the partial
/// polyline is a real, snappable entity while later vertices are placed.
CommitLiveEntity(EntityType),
/// Replace the geometry of the live entity `handle` in place — preserving
/// its layer — without pushing a new undo snapshot. When `finish` is true
/// the command also exits (the entity is already committed, so no separate
/// commit is needed).
UpdateLiveEntity {
handle: Handle,
entity: EntityType,
finish: bool,
},
/// Remove the live entity from the document but keep the command running —
/// PLINE's Undo popping back below the two vertices an entity needs.
RemoveLiveEntity(Handle),
/// Suspends command execution, moves it to suspended_cmd, and opens the text editor for the given handle.
SuspendForTextEdit { handle: Handle },
/// Requests a standard document-level undo while keeping the command active.
UndoDocument,
/// Sets the TEXTEDITMODE system variable and ends the command.
SetTexteditMode(bool),
2026-03-22 14:56:27 +03:00
}
/// What kind of value the active command is currently asking for. Drives
/// the dynamic-input overlay so the tooltip shows the relevant quantity
/// (coordinates for a point pick, a single length for a radius/distance
/// prompt, degrees for an angle prompt).
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum DynField {
/// A position — X/Y coordinates (or distance+angle relative to the
/// last point). The default for every command step.
#[default]
Point,
/// A single linear distance (radius, length, offset) measured from
/// the last point.
Distance,
/// An angle, shown in degrees, measured from the last point.
Angle,
/// A typed scalar with no geometric meaning at the cursor — a count
/// (number of sides / segments) or any value the command reads purely
/// from the keyboard. Shown as a single typed box.
Scalar,
}
// ── Per-step dynamic-input specification ───────────────────────────────────
//
// `DynField` only says "this step wants a point / distance / angle". `DynSpec`
// lets a command describe its step precisely: which value boxes to show (with
// roles + labels), what guide geometry to draw, and where it is measured from.
// A command returns `Some(DynSpec)` from `dyn_spec()` to take explicit control;
// returning `None` (the default) keeps the legacy `dyn_field()` behaviour.
/// Semantic role of a dynamic-input box. Resolution maps each role to a base
/// ordinate/distance/angle; the role additionally drives the label and any
/// value scaling (e.g. a diameter shows/accepts twice the geometric radius).
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DynRole {
X,
Y,
Z,
/// Linear distance from the anchor.
Distance,
/// Angle from the anchor, degrees.
Angle,
/// Distance shown labelled `R` (circle/arc radius).
Radius,
/// Distance shown labelled `⌀`; displayed/typed value is twice the radius.
Diameter,
/// Cartesian X-delta shown labelled `W` (rectangle width).
Width,
/// Cartesian Y-delta shown labelled `H` (rectangle height).
Height,
/// Typed-only scale factor.
Factor,
/// Typed-only integer count. Reserved for upcoming command migrations.
#[allow(dead_code)]
Count,
}
/// Shared rule for how an angle reads in the dynamic-input box: the unsigned
/// magnitude of the short signed angle, so a clockwise angle (cursor below the
/// reference axis) shows as a positive value rather than a negative or a
/// CCW 300-something. Callers keep the *signed* radian for the actual
/// computation/commit; this is display-only. `signed_rad` is the angle from
/// the reference to the cursor.
pub fn dyn_display_angle_deg(signed_rad: f32) -> f32 {
let mut a = signed_rad % std::f32::consts::TAU;
if a > std::f32::consts::PI {
a -= std::f32::consts::TAU;
}
if a <= -std::f32::consts::PI {
a += std::f32::consts::TAU;
}
a.to_degrees().abs()
}
impl DynRole {
/// Default label shown before the value (empty = value only).
pub fn label(self) -> &'static str {
match self {
DynRole::X => "X",
DynRole::Y => "Y",
DynRole::Z => "Z",
DynRole::Distance | DynRole::Angle | DynRole::Factor => "",
DynRole::Radius => "R",
DynRole::Diameter => "\u{2300}",
DynRole::Width => "W",
DynRole::Height => "H",
DynRole::Count => "#",
}
}
/// Multiplier between the geometric value and the displayed/typed value.
/// A diameter box shows and accepts twice the underlying radius.
pub fn value_scale(self) -> f32 {
match self {
DynRole::Diameter => 2.0,
_ => 1.0,
}
}
}
/// Guide geometry the overlay draws for a step, anchored at the step's base.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DynGuide {
/// No guide lines.
None,
/// +X reference line and the angle arc (polar point entry).
Polar,
/// Dotted projections from the cursor down to the anchor's X and Y axes.
AxisDelta,
/// A line from the anchor to the cursor (radius / single distance).
Radius,
/// The two rectangle sides (width × height) from the anchor corner.
RectSides,
/// A line from the anchor, perpendicular to the reference line (anchor →
/// `DynSpec::ref_point`), reaching the cursor's perpendicular offset — the
/// measured semi-axis (ellipse minor). The value is that offset.
Perp,
/// Like `Perp` but drawn as a dimension: the measured segment is offset off
/// the edge with extension lines back to its endpoints (rectangle height).
PerpDim,
}
/// Where a step's values are measured from.
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum DynAnchor {
/// The previous committed point (`App::last_point`). Reserved — current
/// specs pass the anchor explicitly via `Point`.
#[allow(dead_code)]
LastPoint,
/// An explicit world point.
Point(DVec3),
}
/// One value box in a [`DynSpec`].
#[derive(Clone, Debug)]
pub struct DynFieldSpec {
pub role: DynRole,
/// Label override; `None` uses the role's default label.
#[allow(dead_code)] // dyn-spec framework field; not yet consumed
pub label: Option<&'static str>,
}
impl DynFieldSpec {
pub fn new(role: DynRole) -> Self {
Self { role, label: None }
}
}
/// A full per-step dynamic-input description.
#[derive(Clone, Debug)]
pub struct DynSpec {
pub anchor: DynAnchor,
pub fields: Vec<DynFieldSpec>,
pub guide: DynGuide,
/// Far end of a reference line through `anchor` (only used by
/// [`DynGuide::Perp`]); `None` otherwise.
pub ref_point: Option<DVec3>,
}
2026-03-22 14:56:27 +03:00
// ── Trait ─────────────────────────────────────────────────────────────────
/// An interactive CAD command that collects user input step-by-step.
/// One clickable option a command step offers. Rendered as a button next to
/// the command-line prompt; clicking it feeds `keyword` to the running command
/// exactly as if the user had typed it (routed through `on_text_input`). An
/// empty `keyword` submits the step like pressing Enter (the finish / default
/// action). Lets every bracketed `[A=arc L=line …]` prompt become buttons so
/// the option need not be typed. (#304)
#[derive(Clone, Debug)]
pub struct CmdOption {
/// Text shown on the button, e.g. `"3P"` or `"Close"`.
pub label: String,
/// Token fed to the command when clicked, e.g. `"3P"`. Empty = Enter.
pub keyword: String,
}
impl CmdOption {
/// Button whose keyword is typed on click, e.g. `("Ttr", "TTR")`.
pub fn new(label: &str, keyword: &str) -> Self {
Self {
label: label.to_string(),
keyword: keyword.to_string(),
}
}
/// A "finish" button that submits the step like Enter.
pub fn enter(label: &str) -> Self {
Self {
label: label.to_string(),
keyword: String::new(),
}
}
}
2026-03-22 14:56:27 +03:00
pub trait CadCommand: Send {
/// Short name shown in the command line prompt, e.g. `"LINE"`.
#[allow(dead_code)]
fn name(&self) -> &'static str;
/// Current prompt string to display in the command line.
fn prompt(&self) -> String;
/// Clickable keyword options for the current step, rendered as buttons in
/// the command line next to the prompt. Default: none. A command returns
/// different options per step; clicking a button feeds its keyword through
/// the same path as typed command-line text (`on_text_input`). (#304)
fn options(&self) -> Vec<CmdOption> {
Vec::new()
}
/// Push the active UCS into the command as a UCS→render(wire)-space affine
/// (identity = plain WCS). Commands that build axis-aligned geometry (RECT,
/// rectangular ARRAY, …) override this to store it and rotate their implicit
/// axes into the UCS; most commands work purely from picked points and
/// ignore it. Called before each point / preview dispatch.
fn set_ucs(&mut self, _ucs: glam::Mat4) {}
/// Push the live Ctrl-key state into the command before each preview/commit
/// dispatch. Commands that offer a Ctrl toggle (e.g. arc-direction flip on
/// `ARC_CONT`) store it; most commands ignore it. Default no-op.
fn set_ctrl(&mut self, _ctrl: bool) {}
feat(trim): TRIM/EXTEND options — edges, fence, crossing, edge, erase (#336) Both commands gain a shared option mode machine: - Cutting/Boundary edges (T/B): pick which objects act as the boundary (picked edges highlight yellow, the hovered candidate half-strength); Enter rebuilds the boundary set, empty = everything (quick mode). - Fence (F): draw a dashed fence; every object crossing it is trimmed / extended at each crossing, re-applied to the surviving pieces so a fence across both overhanging ends clears both. Live red/cyan result preview while placing points. - Crossing (C): crossing-selection semantics — the dashed green window picks objects crossing its edges AND wholly inside; runs through the fence machinery with a synthesized inside click. - Edge (E): boundaries extrapolate (lines run past their ends, arcs close to full circles). Cuts that land on an implied extension draw a dashed yellow guide from the boundary's drawn end to the cut point, so the user sees which edge causes the cut. Extend previews also show the added tail as a bright dashed slice of the true curve. - Erase (R, TRIM): picks delete without needing an intersection, with a red hover preview. - Shift+click swaps trim<->extend per pick (new set_shift plumbing into the entity-pick and hover paths); the hover preview follows the held state. Project is out of scope for the 2D pipeline. The per-type pick bodies moved to shared free fns (pick_trim_at / pick_extend_at) used by direct picks, hover previews and the fence pass; EXTEND's snapshot bookkeeping unified with TRIM's placeholder flow. Option keywords now take precedence over the hex-handle reading in feed_active_cmd ("F"/"C"/"E" are valid hex), and fence geos carry sentinel handles — probing with NULL against NULL-handle geos excluded the whole fence. Regression test: fence_trims_x_arm. Closes #336 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 14:53:50 +03:00
/// Push the live Shift-key state into the command before each dispatch.
/// TRIM/EXTEND use it for the shift-select swap (Shift+click extends
/// during TRIM and trims during EXTEND, #336). Default no-op.
fn set_shift(&mut self, _shift: bool) {}
/// Mid-command Ctrl+Z: a multi-point drawing command can take the undo
/// itself (PLINE pops its last vertex) instead of the document undo
/// swallowing the whole in-progress object. `None` (default) lets the
/// normal document undo run.
fn on_undo_step(&mut self) -> Option<CmdResult> {
None
}
2026-03-22 14:56:27 +03:00
/// Called when the user left-clicks in the viewport (point pick).
fn on_point(&mut self, pt: DVec3) -> CmdResult;
2026-03-22 14:56:27 +03:00
/// Called when the user presses Enter (finalize / next option).
fn on_enter(&mut self) -> CmdResult;
/// Called when the user presses Escape (cancel).
#[allow(dead_code)]
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
/// Returns `true` when the command needs entity picking (hit-test) instead of point picking.
fn needs_entity_pick(&self) -> bool {
false
}
/// Called when the text editor closes, either because the user committed or cancelled the edit.
fn on_editor_closed(&mut self, _committed: bool) -> CmdResult {
CmdResult::Cancel
}
2026-03-22 14:56:27 +03:00
/// Called when the user clicks and `needs_entity_pick()` is true.
/// `handle` is the nearest wire's entity handle (Handle::NULL if nothing found).
fn on_entity_pick(&mut self, _handle: Handle, _pt: DVec3) -> CmdResult {
2026-03-22 14:56:27 +03:00
CmdResult::Cancel
}
/// Host callback after `CmdResult::CommitLiveEntity`: records the handle the
/// new live entity was assigned so later `UpdateLiveEntity` results can
/// target it.
fn set_live_handle(&mut self, _handle: Handle) {}
/// Point-click pick of domain objects (wire hit-test often misses small markers).
fn needs_structure_point_pick(&self) -> bool {
false
}
/// Resolve a domain object near `(x, y)` while `needs_structure_point_pick()` is active.
fn resolve_object_pick(&self, _scene: &Scene, _x: f64, _y: f64) -> Option<ObjectPickHit> {
None
}
/// Preview wires while hovering during object-point pick.
fn object_pick_hover_previews(&self, _scene: &Scene, _cursor: DVec3) -> Vec<WireModel> {
vec![]
}
/// Message when `resolve_object_pick` returns none on click.
fn object_pick_miss_message(&self) -> &'static str {
"No object near click."
}
/// Called when `needs_structure_point_pick()` is true and a structure is found near the click.
fn on_structure_pick(&mut self, _handle: Handle, _pt: DVec3) -> CmdResult {
CmdResult::Cancel
}
/// Extra acquisition previews during entity pick (besides `on_hover_entity`).
fn entity_pick_acquire_previews(&self, _scene: &Scene, _handle: Handle) -> Vec<WireModel> {
vec![]
}
/// Acquisition hint label during entity pick hover.
fn entity_pick_acquire_hint(&self, _handle: Handle) -> Option<&'static str> {
None
}
/// Hover label for object acquisition (e.g. "Inlet" under cursor).
fn set_acquisition_hint(&mut self, _hint: Option<&str>) {}
2026-03-22 14:56:27 +03:00
/// Called after `CmdResult::ReplaceEntity` is applied to the document.
/// `old` is the erased handle; `new_handles` are the handles assigned to the replacement entities.
/// Commands that stay active across replaces should update their internal snapshots here.
fn on_entity_replaced(&mut self, _old: Handle, _new_handles: &[Handle]) {}
/// Called on every mouse-move when `needs_entity_pick()` is true.
/// Return preview wires showing the operation result under the cursor.
/// Default: empty (no preview).
fn on_hover_entity(&mut self, _handle: Handle, _pt: DVec3) -> Vec<WireModel> {
2026-03-22 14:56:27 +03:00
vec![]
}
/// Called on every mouse-move in the viewport.
/// Return `Some(WireModel)` to update the rubber-band preview, `None` to skip.
fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
2026-03-22 14:56:27 +03:00
None
}
/// Called on every mouse-move; return all preview wires to show (object ghosts + rubber-band).
/// Default: forwards to `on_mouse_move` for backwards compatibility.
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
2026-03-22 14:56:27 +03:00
self.on_mouse_move(pt).into_iter().collect()
}
/// Returns `true` when the command is waiting for text typed in the command line.
fn wants_text_input(&self) -> bool {
false
}
/// Returns `true` when the current step is a point pick that *also* accepts
/// optional keyword letters (e.g. PLINE's A/L/C/U). Such a step keeps the
/// polar dynamic-input boxes: typed digits become coordinates while letters
/// still reach the command line as keywords. Without this, a command that
/// returns `wants_text_input() == true` for its keywords would suppress the
/// dynamic-input distance/angle entirely. Default `false`.
fn point_step_accepts_keywords(&self) -> bool {
false
}
/// Returns `true` when the active text prompt expects free-form prose
/// that can legitimately contain whitespace (the body of a TEXT /
/// MTEXT / DDEDIT entity, an attribute default value, etc.). For
/// these prompts the command-line input must let `Space` be typed as
/// a literal character; for every other prompt `Space` submits the
/// input the same way `Enter` does.
///
/// Default `false` — single-token prompts (option letters, numeric
/// radius, block name) do not embed spaces.
fn wants_text_with_spaces(&self) -> bool {
false
}
2026-03-22 14:56:27 +03:00
/// Called when the user submits text via the command line while `wants_text_input` is true.
fn on_text_input(&mut self, _text: &str) -> Option<CmdResult> {
None
}
/// Returns `true` when the command is in a selection-gathering phase.
/// While true, viewport clicks are routed through the normal selection
/// system (single / box / polygon) instead of the command's point-pick path.
/// After each completed selection action the host calls `on_selection_complete`.
fn is_selection_gathering(&self) -> bool {
false
}
/// Called after a selection action completes while `is_selection_gathering` is true.
/// `handles` is the full set of currently selected entities.
/// Return `Relaunch` to fire the pending command, or `NeedPoint` to keep gathering.
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
CmdResult::Cancel
}
/// Returns `true` when the current step picks a corner of a selection
/// *window* by point (e.g. STRETCH's crossing window). Such a pick must be a
/// free point: applying the Ortho/Polar lock would pin the opposite corner to
/// an axis through the first corner, collapsing the rectangle to a line and
/// making the window unusable. The host skips the ortho/polar constraint for
/// these steps. Default `false`. (#291)
fn window_corner_pick(&self) -> bool {
false
}
/// The already-picked first corner of the selection window (world space)
/// while `window_corner_pick()` is true and the opposite corner is being
/// dragged. The host projects it and draws a filled selection marquee to the
/// cursor, so a point-picked window (STRETCH) reads like a normal box
/// selection. `None` before the first corner is set. (#291)
fn window_first_corner(&self) -> Option<glam::DVec3> {
None
}
2026-03-22 14:56:27 +03:00
/// Returns `true` when the command wants object picks via Tangent snap.
fn needs_tangent_pick(&self) -> bool {
false
}
/// If this command is XATTACH, returns the file path to attach.
/// Default: None.
fn xattach_path(&self) -> Option<String> {
None
}
/// The block reference an ATTEDIT pick has resolved to, awaiting the
/// attribute editor dialog; else None.
fn attedit_pending_handle(&self) -> Option<acadrust::Handle> {
None
}
/// Inject the block's attribute definitions for ATTREQ attr-filling after
/// the INSERT point is picked. Carries the full definitions so the created
/// attributes inherit their geometry, not just tag / prompt / default (#255).
fn attreq_set_attdefs(&mut self, _attdefs: Vec<acadrust::entities::AttributeDefinition>) {}
/// Returns the INSERT entity built so far (pending attr fill) if this is an
/// ATTREQ-aware INSERT command waiting for attdef injection.
/// Called by the host after `AttreqNeeded` to commit the completed Insert.
fn attreq_take_insert(&mut self) -> Option<acadrust::EntityType> {
None
}
2026-03-22 14:56:27 +03:00
/// Called instead of `on_point` when the command needs a tangent pick
/// and the snap system found a tangent object.
fn on_tangent_point(&mut self, obj: TangentObject, hit: DVec3) -> CmdResult {
2026-03-22 14:56:27 +03:00
let _ = obj;
self.on_point(hit)
}
/// Commit a picked point that may carry a running Tangent-snap reference
/// (the object under the cursor when Tangent won). The default ignores the
/// tangent, so only commands that opt in see it — LINE uses it to resolve a
/// deferred tangent-to-tangent line between two circles, which needs both
/// objects and so can't be computed at a single pick. Returning `None`
/// means "not handled — fall back to `on_point`". (#274)
fn on_point_with_tangent(
&mut self,
_pt: DVec3,
_tangent: Option<TangentObject>,
) -> Option<CmdResult> {
None
}
/// The command's current anchor (rubber-band origin) after a commit, so the
/// host can sync `last_point` when the command replaced the picked
/// coordinate — e.g. LINE resolving a deferred tangent to the true tangent
/// point. `None` keeps the picked point. Only consulted after
/// `on_point_with_tangent` handled the pick. (#274)
fn resolved_anchor(&self) -> Option<DVec3> {
None
}
/// When true, `update.rs` injects the picked entity before calling
/// `on_entity_pick` (required when the pick handler reads injected state).
fn inject_before_entity_pick(&self) -> bool {
false
}
/// Called by update.rs to inject the cloned entity into commands
/// that need to read/modify it (e.g. DIMTEDIT, MLEADERADD, MLEADERREMOVE).
/// Default: no-op.
fn inject_picked_entity(&mut self, _entity: acadrust::EntityType) {}
/// What the command is asking for at this step, used to label the
/// dynamic-input overlay. Default is a point pick; commands waiting
/// on a radius/length return `Distance` and angle prompts return
/// `Angle`.
fn dyn_field(&self) -> DynField {
DynField::Point
}
/// When true, a value typed into the dynamic-input box for this step is
/// committed via `on_text_input` (as a string the command parses) rather
/// than resolved into a point. Used by steps whose typed value is a span /
/// included angle / length the command interprets itself (e.g. ARC angle
/// modes), while the box still previews a live value from the cursor.
fn dyn_commit_as_text(&self) -> bool {
false
}
/// Explicit per-step dynamic-input description. `Some(spec)` takes full
/// control of the boxes, guide geometry and anchor for this step; `None`
/// (the default) falls back to the legacy `dyn_field()` behaviour so
/// commands that haven't migrated keep working unchanged.
fn dyn_spec(&self) -> Option<DynSpec> {
None
}
/// Live value for the dynamic-input scalar box, derived from the cursor
/// world position. Lets a command drive a typed prompt by mouse — e.g.
/// OFFSET returns the perpendicular distance from the cursor to the
/// object being offset, so moving the cursor fills in the distance.
/// Returns `None` when the value can only be typed (a count, or a
/// distance with no reference yet). The string the host commits is this
/// value formatted; the command's own `on_text_input` parses it back.
fn dyn_live_value(&self, _cursor: DVec3) -> Option<f64> {
None
}
2026-03-22 14:56:27 +03:00
}
// ── Autocomplete registry ─────────────────────────────────────────────────
//
// Every `impl CadCommand for Foo` module submits the names it answers to
// at compile time via `inventory::submit!`. The command-line autocomplete
// then iterates the resulting collection at runtime — no central list to
// keep in sync.
//
// Non-interactive one-shot dispatch arms (NEW, OPEN, SAVE, …) live in
// `app/commands.rs` and don't have a `CadCommand` impl; they're absent
// from autocomplete by design. Add an explicit `inventory::submit!` next
// to their dispatch arm if you want them surfaced.
pub struct CommandRegistration {
pub names: &'static [&'static str],
}
inventory::collect!(CommandRegistration);
/// All registered command names, including aliases.
pub fn all_registered_command_names() -> Vec<&'static str> {
inventory::iter::<CommandRegistration>
.into_iter()
.flat_map(|r| r.names.iter().copied())
.collect()
}