Files
OneCAD-Draft/src/command.rs
T
Christopher OverbeckandClaude Fable 5.1 49fe48f89c Add shared viewport frame, accepted snap, and measurement scale interfaces
Contract module for the viewport snapping, viewport dimension measurement,
and dimension association work that builds on it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 12:10:28 -05:00

2678 lines
95 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
use acadrust::{EntityType, Handle};
use glam::DVec3;
#[derive(Clone, Debug)]
pub enum HatchEditOperation {
Appearance {
color: Option<acadrust::types::Color>,
layer: Option<String>,
transparency: Option<acadrust::types::Transparency>,
},
Update {
origin: Option<(f64, f64)>,
store_origin: bool,
disassociate: bool,
style: Option<acadrust::entities::HatchStyleType>,
annotative: Option<bool>,
},
RecreateBoundary { associate: bool, region: bool },
BeginAssociate,
AssociatePaths(Vec<acadrust::entities::BoundaryPath>),
DrawOrderBoundary { above: bool },
Separate,
AddBoundaries(Vec<Handle>),
RemoveBoundaries(Vec<Handle>),
DrawOrderFront,
DrawOrderBack,
}
// ── Working plane ─────────────────────────────────────────────────────────
/// Full-precision coordinate frame used by interactive commands.
///
/// Points delivered to commands remain WCS points. Commands that construct
/// planar geometry use this frame for their local calculations, then convert
/// the result back to WCS for storage and preview.
#[derive(Clone, Copy, Debug)]
pub struct WorkingPlane {
pub origin: DVec3,
pub x: DVec3,
pub y: DVec3,
pub z: DVec3,
}
impl Default for WorkingPlane {
fn default() -> Self {
Self {
origin: DVec3::ZERO,
x: DVec3::X,
y: DVec3::Y,
z: DVec3::Z,
}
}
}
impl WorkingPlane {
pub fn is_identity(self) -> bool {
self.origin.abs_diff_eq(DVec3::ZERO, 1e-12)
&& self.x.abs_diff_eq(DVec3::X, 1e-12)
&& self.y.abs_diff_eq(DVec3::Y, 1e-12)
&& self.z.abs_diff_eq(DVec3::Z, 1e-12)
}
pub fn new(origin: DVec3, x: DVec3, y: DVec3) -> Self {
let x = x.normalize_or(DVec3::X);
let raw_y = y.normalize_or(DVec3::Y);
let fallback = if x.dot(DVec3::Z).abs() < 0.999 {
DVec3::Z
} else {
DVec3::Y
};
let z = x.cross(raw_y).normalize_or(x.cross(fallback).normalize());
let y = z.cross(x).normalize();
Self { origin, x, y, z }
}
pub fn to_world(self, point: DVec3) -> DVec3 {
self.origin + self.x * point.x + self.y * point.y + self.z * point.z
}
pub fn to_local(self, point: DVec3) -> DVec3 {
let delta = point - self.origin;
DVec3::new(delta.dot(self.x), delta.dot(self.y), delta.dot(self.z))
}
pub fn vector_to_world(self, vector: DVec3) -> DVec3 {
self.x * vector.x + self.y * vector.y + self.z * vector.z
}
pub fn vector_to_local(self, vector: DVec3) -> DVec3 {
DVec3::new(vector.dot(self.x), vector.dot(self.y), vector.dot(self.z))
}
pub fn angle(self, from: DVec3, to: DVec3) -> Option<f64> {
let direction = self.vector_to_local(to - from);
(direction.x.hypot(direction.y) > f64::EPSILON)
.then(|| direction.y.atan2(direction.x))
}
pub fn to_world_transform(self) -> acadrust::types::Transform {
use acadrust::types::{Matrix4, Transform};
Transform::from_matrix(Matrix4 {
m: [
[self.x.x, self.y.x, self.z.x, self.origin.x],
[self.x.y, self.y.y, self.z.y, self.origin.y],
[self.x.z, self.y.z, self.z.z, self.origin.z],
[0.0, 0.0, 0.0, 1.0],
],
})
}
pub fn to_local_transform(self) -> acadrust::types::Transform {
use acadrust::types::{Matrix4, Transform};
Transform::from_matrix(Matrix4 {
m: [
[self.x.x, self.x.y, self.x.z, -self.origin.dot(self.x)],
[self.y.x, self.y.y, self.y.z, -self.origin.dot(self.y)],
[self.z.x, self.z.y, self.z.z, -self.origin.dot(self.z)],
[0.0, 0.0, 0.0, 1.0],
],
})
}
pub fn place_entity(self, mut entity: EntityType) -> EntityType {
crate::scene::view::dispatch::apply_transform(
&mut entity,
&EntityTransform::Affine(self.to_world_transform()),
);
entity
}
}
#[cfg(test)]
mod working_plane_tests {
use super::*;
#[test]
fn translated_and_rotated_frame_round_trips_points_and_vectors() {
let plane = WorkingPlane::new(
DVec3::new(125_000.25, -42_000.5, 810.75),
DVec3::new(0.0, 1.0, 0.0),
DVec3::new(0.0, 0.0, 1.0),
);
let local = DVec3::new(12.5, -3.25, 7.75);
let vector = DVec3::new(-2.0, 5.0, 1.5);
assert!(plane
.to_local(plane.to_world(local))
.abs_diff_eq(local, 1e-9));
assert!(plane
.vector_to_local(plane.vector_to_world(vector))
.abs_diff_eq(vector, 1e-12));
}
}
/// 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,
}
#[derive(Clone)]
pub struct SelectionEntity {
pub handle: Handle,
pub entity: EntityType,
pub surface_area: Option<f64>,
}
/// Association source with an optional sub-entity marker.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DimensionAssociationSource {
pub handle: Handle,
pub marker: Option<i32>,
pub parameter: f64,
}
impl DimensionAssociationSource {
pub const fn inferred(handle: Handle) -> Self {
Self {
handle,
marker: None,
parameter: 0.0,
}
}
pub const fn explicit(handle: Handle, marker: i32, parameter: f64) -> Self {
Self {
handle,
marker: Some(marker),
parameter,
}
}
}
#[derive(Clone, Debug)]
pub enum DimensionAssociationInput {
Infer(Option<Handle>),
Explicit(Vec<Option<DimensionAssociationSource>>),
}
#[derive(Clone)]
pub enum AreaPreviewSource {
Handles(Vec<Handle>),
Boundary(Vec<[f64; 2]>),
}
#[derive(Clone)]
pub struct AreaPreviewRegion {
pub source: AreaPreviewSource,
pub subtract: bool,
}
// ── Transform ─────────────────────────────────────────────────────────────
/// A geometric transformation applied to existing entities.
#[derive(Clone)]
pub enum EntityTransform {
/// Move every point by the given world-space delta.
Translate(DVec3),
/// Rotate around the axis through `center`.
Rotate {
center: DVec3,
axis: DVec3,
angle_rad: f64,
},
/// Uniform scale from `center` by `factor`.
Scale { center: DVec3, factor: f64 },
/// Mirror through the plane containing `p1`→`p2` and the working normal.
Mirror {
p1: DVec3,
p2: DVec3,
working_normal: DVec3,
},
/// General affine transform. Used when a complete UCS basis must be baked
/// into block-local geometry instead of stored as drawing UCS state.
Affine(acadrust::types::Transform),
}
// ── 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 },
}
/// 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 {
crate::t!(self.prompt).into_owned()
}
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 => crate::t!("RENAME Select the object type to rename:").into_owned(),
RenameStep::Old { ty } => crate::t!(
"RENAME %{type} Enter the current name:",
type = ty
)
.into_owned(),
RenameStep::New { ty, old } => crate::t!(
"RENAME %{type} Rename \"%{old}\" to:",
type = ty,
old = old
)
.into_owned(),
}
}
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 => crate::t!("%{name} which register? [1-5]:", name = self.name)
.into_owned(),
Some(n) => crate::t!("%{name}%{slot} new value:", name = self.name, slot = n)
.into_owned(),
}
}
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)>,
/// Keyword a bare Enter dispatches at the verb step (`<Default>` prompts,
/// e.g. PLAN's `<Current>`). `None` = Enter cancels, as before.
default: Option<&'static str>,
}
impl KeywordCommand {
/// Set the verb a bare Enter dispatches (shown as `<Default>` in prompts).
pub fn with_default(mut self, kw: &'static str) -> Self {
self.default = Some(kw);
self
}
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,
default: None,
}
}
}
fn match_cmd_option<'a>(
options: &'a [(&'static str, &'static str, Option<&'static str>)],
text: &str,
) -> Option<&'a (&'static str, &'static str, Option<&'static str>)> {
let t = text.trim();
let up = t.to_uppercase();
if up.is_empty() {
return None;
}
// 1. Exact match on keyword or label (case-insensitive)
if let Some(opt) = options.iter().find(|(label, k, _)| {
k.eq_ignore_ascii_case(&up) || label.eq_ignore_ascii_case(t)
}) {
return Some(opt);
}
// 2. Unambiguous prefix match on keyword or label (e.g. "A" -> "ABOVE", "L" -> "LEFT")
let matches: Vec<_> = options
.iter()
.filter(|(label, k, _)| {
k.to_uppercase().starts_with(&up) || label.to_uppercase().starts_with(&up)
})
.collect();
if matches.len() == 1 {
return Some(matches[0]);
}
None
}
impl CadCommand for KeywordCommand {
fn name(&self) -> &'static str {
self.name
}
fn prompt(&self) -> String {
match self.pending {
Some((_, value_prompt)) => crate::t!(value_prompt).into_owned(),
None => crate::t!(self.prompt).into_owned(),
}
}
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 Some((_, keyword, value_prompt)) = match_cmd_option(&self.options, t)
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 {
// Bare Enter at the verb step runs the default verb when one is set.
if self.pending.is_none() {
if let Some(kw) = self.default {
return CmdResult::Dispatch(format!("{} {kw}", self.name));
}
}
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 => crate::t!(self.prompt1).into_owned(),
Some(_) => crate::t!(self.prompt2).into_owned(),
}
}
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,
}
}
}
// ── Mid between 2 points (MTP / M2P) ────────────────────────────────────────
/// Point-entry modifier that prompts for two points, draws a preview connecting line
/// with a midpoint marker, and returns the midpoint to the caller.
#[derive(Debug, Default)]
pub struct Mid2PointCommand {
pub first_point: Option<DVec3>,
}
impl Mid2PointCommand {
pub fn new() -> Self {
Self { first_point: None }
}
}
impl CadCommand for Mid2PointCommand {
fn name(&self) -> &'static str {
"MTP"
}
fn prompt(&self) -> String {
if self.first_point.is_none() {
crate::t!("_mtp Specify first point of mid:").into_owned()
} else {
crate::t!("_mtp Specify second point of mid:").into_owned()
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Some(first) = self.first_point {
let mid = (first + pt) * 0.5;
CmdResult::ReturnPoint(mid)
} else {
self.first_point = Some(pt);
CmdResult::NeedPoint
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
let mut wires = Vec::new();
if let Some(first) = self.first_point {
// Rubber-band line connecting the first point to the cursor
wires.push(WireModel::solid_f64(
"mtp_rubber_band".to_string(),
vec![[first.x, first.y, first.z], [pt.x, pt.y, pt.z]],
WireModel::CYAN,
false,
));
// Midpoint marker (AutoCAD style triangle glyph)
let mid = (first + pt) * 0.5;
let dist = (pt - first).length();
let s = (dist * 0.02).clamp(0.5, 10.0);
let h = s * 1.5;
let w = s * 1.0;
wires.push(WireModel::solid_f64(
"mtp_mid_triangle".to_string(),
vec![
[mid.x, mid.y + h * (2.0 / 3.0), mid.z],
[mid.x - w, mid.y - h * (1.0 / 3.0), mid.z],
[mid.x + w, mid.y - h * (1.0 / 3.0), mid.z],
],
WireModel::CYAN,
true,
));
}
wires
}
}
inventory::submit!(CommandRegistration {
names: &["MTP", "M2P"],
});
/// 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 crate::t!(
"%{name} select objects, then press Enter:",
name = self.name
)
.into_owned();
}
match self.pending {
Some((_, value_prompt)) => crate::t!(value_prompt).into_owned(),
None => crate::t!(self.prompt).into_owned(),
}
}
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, _)) => {
if self.selected.is_empty() {
Some(CmdResult::Dispatch(format!("{} {keyword} {t}", self.name)))
} else {
Some(CmdResult::Relaunch(
format!("{} {keyword} {t}", self.name),
std::mem::take(&mut self.selected),
))
}
}
None => {
let Some((_, keyword, value_prompt)) = match_cmd_option(&self.options, t)
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 => {
if self.selected.is_empty() {
Some(CmdResult::Dispatch(format!("{} {keyword}", self.name)))
} else {
Some(CmdResult::Relaunch(
format!("{} {keyword}", self.name),
std::mem::take(&mut self.selected),
))
}
}
}
}
}
}
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 {
crate::t!(
"%{name} select objects, then press Enter:",
name = self.name
)
.into_owned()
} else {
crate::t!(self.value_prompt).into_owned()
}
}
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))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TCountStep {
Start,
Increment,
Placement,
}
/// Interactive front-end for TCOUNT.
///
/// After gathering the text selection it asks for:
/// 1. starting number,
/// 2. increment,
/// 3. placement mode (Overwrite / Prefix / Suffix).
///
/// The actual entity modification remains in the inline TCOUNT handler.
pub struct TCountCommand {
gathering: bool,
selected: Vec<Handle>,
step: TCountStep,
start: i64,
increment: i64,
}
impl TCountCommand {
pub fn new(has_selection: bool) -> Self {
Self {
gathering: !has_selection,
selected: Vec::new(),
step: TCountStep::Start,
start: 1,
increment: 1,
}
}
fn dispatch(&self, placement: &str) -> CmdResult {
CmdResult::Dispatch(format!(
"TCOUNT {} {} {}",
self.start, self.increment, placement
))
}
}
impl CadCommand for TCountCommand {
fn name(&self) -> &'static str {
"TCOUNT"
}
fn prompt(&self) -> String {
if self.gathering {
return crate::t!("TCOUNT select text objects, then press Enter:").into_owned();
}
match self.step {
TCountStep::Start => {
let start = self.start;
crate::tf!("TCOUNT starting number <{start}>:").into_owned()
}
TCountStep::Increment => {
let increment = self.increment;
crate::tf!("TCOUNT increment <{increment}>:").into_owned()
}
TCountStep::Placement => {
crate::t!("TCOUNT placement [Overwrite/Prefix/Suffix] <Overwrite>:").into_owned()
}
}
}
fn options(&self) -> Vec<CmdOption> {
if self.gathering || self.step != TCountStep::Placement {
return Vec::new();
}
vec![
CmdOption::new("Overwrite", "O"),
CmdOption::new("Prefix", "P"),
CmdOption::new("Suffix", "S"),
]
}
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.step {
TCountStep::Start => {
if let Ok(value) = t.parse::<i64>() {
self.start = value;
self.step = TCountStep::Increment;
}
Some(CmdResult::NeedPoint)
}
TCountStep::Increment => {
if let Ok(value) = t.parse::<i64>() {
self.increment = value;
self.step = TCountStep::Placement;
}
Some(CmdResult::NeedPoint)
}
TCountStep::Placement => {
let placement = match t.to_uppercase().as_str() {
"O" | "OVERWRITE" => "O",
"P" | "PREFIX" => "P",
"S" | "SUFFIX" => "S",
_ => return Some(CmdResult::NeedPoint),
};
Some(self.dispatch(placement))
}
}
}
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;
}
match self.step {
TCountStep::Start => {
self.start = 1;
self.step = TCountStep::Increment;
CmdResult::NeedPoint
}
TCountStep::Increment => {
self.increment = 1;
self.step = TCountStep::Placement;
CmdResult::NeedPoint
}
TCountStep::Placement => self.dispatch("O"),
}
}
}
// ── Result token ──────────────────────────────────────────────────────────
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExtrudeMode {
Solid,
Surface,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ExtrudeExtent {
Height(f64),
Direction(DVec3),
Path(Handle),
}
/// Construction options shared by SWEEP creation and its live preview.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SweepOptions {
pub align: bool,
pub bank: bool,
pub base_point: Option<DVec3>,
pub scale: f64,
/// Total twist along the path, in radians.
pub twist_angle: f64,
}
impl Default for SweepOptions {
fn default() -> Self {
Self {
align: true,
bank: false,
base_point: None,
scale: 1.0,
twist_angle: 0.0,
}
}
}
/// Ordered cross-sections used by LOFT creation, validation and preview.
#[derive(Clone, Debug, PartialEq)]
pub enum LoftSectionSelection {
Entity(Handle),
Point(DVec3),
/// Connected source edges forming one exact cross-section.
Join(Vec<Handle>),
}
/// Construction parameters shared by the LOFT command and its history.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LoftOptions {
/// Ruled, Smooth, First normal, Last normal, Ends normal, All normal,
/// or Use draft angles, respectively.
pub normals: i32,
pub start_draft_angle: f64,
pub end_draft_angle: f64,
pub start_magnitude: f64,
pub end_magnitude: f64,
/// Point-end continuity: G0 (0) or G1 (1).
pub start_continuity: i32,
pub end_continuity: i32,
/// Positive point-end bulge factors; independent of draft magnitudes.
pub start_bulge: f64,
pub end_bulge: f64,
pub closed: bool,
pub periodic: bool,
pub align_direction: bool,
}
/// One face-selection edit made while collecting a solid shell operation.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ShellFaceAction {
Remove(DVec3),
Add(DVec3),
RemoveAll,
AddAll,
}
impl Default for LoftOptions {
fn default() -> Self {
Self {
normals: 1,
start_draft_angle: std::f64::consts::FRAC_PI_2,
end_draft_angle: std::f64::consts::FRAC_PI_2,
start_magnitude: 0.0,
end_magnitude: 0.0,
start_continuity: 1,
end_continuity: 1,
start_bulge: 0.5,
end_bulge: 0.5,
closed: false,
periodic: true,
align_direction: true,
}
}
}
#[derive(Clone, Debug)]
pub enum DimensionEditOperation {
Home,
NewText(String),
Rotate(f64),
Oblique(f64),
}
#[derive(Clone, Copy)]
pub enum DimensionBreakOperation {
Auto,
Object(Handle),
Manual(DVec3, DVec3),
Remove,
}
/// 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 several acadrust entities in one undo step; keep the command active.
CommitEntities(Vec<EntityType>),
/// Commit several entities in one undo step and end the command.
CommitEntitiesAndExit(Vec<EntityType>),
/// Commit an acadrust entity to the document and end the command.
CommitAndExit(EntityType),
/// Commit a dimension using the drawing's association mode.
CommitDimension {
entity: EntityType,
association: DimensionAssociationInput,
/// Retain the source dimension's layer and style instead of replacing
/// them with the current creation defaults (DIMCONTINUEMODE=1).
preserve_base_style: bool,
/// Keep collecting points after the dimension is committed.
continue_command: bool,
},
/// Commit several dimensions with independent association sources in one
/// undo step, then end the command.
CommitDimensionsAndExit(Vec<(EntityType, DimensionAssociationInput)>),
/// Persist the quick-dimension extension-origin priority while keeping the
/// active command at its current prompt (0 = endpoints, 1 = intersections).
SetQuickDimensionSnapPriority(u8),
/// Align multileader content points along a picked infinite line.
AlignMLeaders {
handles: Vec<Handle>,
from: DVec3,
to: DVec3,
},
/// Merge compatible block multileaders at a picked content point.
CollectMLeaders {
handles: Vec<Handle>,
point: DVec3,
},
/// Commit a Model-tab 3D solid: the acadrust entity (for selection /
/// persistence) plus its B-rep (cached for boolean ops + shaded
/// rendering). Ends the command.
CommitSolid {
entity: EntityType,
solid: Box<cadkernel::brep::Body>,
history: acadrust::objects::SolidHistoryOperation,
erase_source: Option<Handle>,
},
/// 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,
open_editor: bool,
},
/// Create a block definition from existing entities and insert one reference.
CreateBlock {
handles: Vec<Handle>,
name: String,
base: DVec3,
},
/// 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),
/// Store selected entities in the shared clipboard; command stays active.
CopyToClipboard { handles: Vec<Handle>, base: DVec3 },
/// Commit a hatch fill (stored in Scene::hatches, not the DXF document).
CommitHatch(HatchModel),
/// Commit a hatch with the selected hatch's entity colour and transparency.
CommitStyledHatch {
hatch: HatchModel,
color: acadrust::types::Color,
transparency: acadrust::types::Transparency,
},
/// Commit a hatch and retain each boundary ring as an entity.
CommitHatchWithBoundaries {
hatch: HatchModel,
boundaries: Vec<EntityType>,
entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>,
},
/// Commit independently editable hatch entities for every selected region.
CommitHatches {
hatches: Vec<HatchModel>,
entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>,
},
/// 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>),
/// Update one entity in place, preserve its handle, and end the command.
UpdateEntityAndFinish { handle: Handle, entity: 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>),
/// Replace several entities as one undo step while keeping the command active.
ReplaceManyContinue(Vec<(Handle, Vec<EntityType>)>),
/// Add a persistent parametric constraint to the current scope's
/// `SketchConstraintSet` and trigger its
/// first solve; end the command. Unlike `ReplaceMany`/`CommitEntity`,
/// nothing here is geometry to add or replace directly — the host adds
/// the constraint record, then re-solves through `Scene::bump_entities`'s
/// existing chain (`refresh_sketch_constraints`) the same way any later
/// edit to these entities will.
AddSketchConstraint {
kind: crate::scene::sketch_constraints::ConstraintKind,
refs: Vec<crate::scene::sketch_constraints::SketchRef>,
/// The typed target for a dimensional kind (Distance/Angle/Radius) —
/// a literal number, or a named-parameter reference recognized by
/// `DistanceConstraintCommand`/`AngleConstraintCommand::
/// on_text_input` when the typed token matches a known parameter
/// name instead of parsing as a number); `None` for a purely
/// geometric kind.
driving_param: Option<crate::scene::named_parameters::DrivingValue>,
/// Undo-history label, e.g. `"Horizontal constraint"`.
label: &'static str,
},
/// Adds a Coincident constraint between the sub-entity points nearest
/// the two picked positions. The host resolves the raw points because
/// `CadCommand` has no document access.
AddCoincidentConstraint {
point_a: DVec3,
point_b: DVec3,
label: &'static str,
},
/// Add a persistent `CenterPoint`/`Midpoint`/`PointOnCurve` constraint
/// (`crate::modules::parametric::point_on_entity`) between one
/// picked point and `target`, a whole entity selected before the tool
/// ran. Like `AddCoincidentConstraint`, the host resolves `point` via
/// `sketch_constraints::nearest_sketch_point` (a `CadCommand` has no
/// document access) and, for `CenterPoint` specifically, addresses
/// `target` via its center marker rather than as a whole entity — see
/// `ConstraintKind::CenterPoint`'s own doc comment for why it's the
/// same solve as `Coincident`/`Concentric` under a different DWG-native
/// class name.
AddPointOnEntityConstraint {
point: DVec3,
target: Handle,
kind: crate::scene::sketch_constraints::ConstraintKind,
label: &'static str,
},
/// Add a persistent `EqualDistance` constraint
/// (`crate::modules::parametric::equal_distance`): the distance
/// between `points[0]`/`points[1]` equals the distance between
/// `points[2]`/`points[3]`. The host resolves each point via
/// `sketch_constraints::nearest_sketch_point`, same reasoning as
/// `AddCoincidentConstraint`.
AddEqualDistanceConstraint {
points: [DVec3; 4],
label: &'static str,
},
/// Attach one smart centre mark to a newly selected circular source.
ReassociateCenterMark {
target: Handle,
source: Handle,
point: DVec3,
},
EditDimensionBreak {
dimensions: Vec<Handle>,
operation: DimensionBreakOperation,
},
EditDimensionJog {
dimension: Handle,
point: Option<DVec3>,
},
SpaceDimensions {
base: Handle,
others: Vec<Handle>,
spacing: Option<f64>,
},
/// Cancel: discard any preview and end the command.
Cancel,
/// End the command and begin in-place editing of the table cell under
/// `point` on table `handle` (TABLEDIT). The host resolves the cell —
/// it owns the document needed for the table-style lookup — honors
/// content locks, and launches the cell editor, re-prompting the
/// command when the pick misses a cell.
EditTableCell { handle: Handle, point: DVec3 },
/// Cancel because the active drawing space changed. Cleanup is identical
/// to `Cancel`, but the host reports the context change explicitly.
CancelForSpaceChange,
/// End the selection-gather phase and re-dispatch the named command
/// with the gathered handles installed as the active scene selection.
/// Select by a path the user picked point by point. `closed` polygons take
/// what they enclose, or merely touch when `crossing`; an open fence takes
/// only what it actually cuts. The host owns the hit test, so the command
/// hands over the geometry rather than the answer. (#596)
SelectByPath {
path: Vec<[f64; 2]>,
closed: bool,
crossing: bool,
},
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),
/// 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>,
},
/// 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),
/// Print a measurement result and keep the command active.
ReportMeasurement(String),
/// Print an input error and keep the command active.
ReportError(String),
/// Print a measurement result, clear the current selection, and keep the command active.
ReportMeasurementAndDeselect(String),
/// Clear the current selection and keep the command active at its updated step.
DeselectAndContinue,
/// 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>),
/// Join candidates into an explicitly selected source.
JoinToSource { source: Handle, handles: 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, marker: Option<CurveMarker> },
/// Place Point entities at `segment_length` intervals along the entity.
MeasureEntity { handle: Handle, segment_length: f64, pick_point: DVec3, marker: Option<CurveMarker> },
/// 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 },
/// Create a paper-space viewport. `preserve_view` keeps an explicitly
/// selected/defined view instead of applying the normal model-extents fit.
MviewCreate {
viewport: acadrust::entities::Viewport,
preserve_view: bool,
},
/// Create a viewport clipped by either a new polygon boundary or an
/// existing closed paper-space entity.
MviewCreateClipped {
boundary: Option<EntityType>,
boundary_handle: Handle,
},
/// Create a wipeout from an existing closed polyline in the active space.
/// `erase_source` controls whether the source boundary is consumed.
WipeoutFromPolyline {
handle: Handle,
erase_source: bool,
},
/// Temporarily switch between paper and Model while MVIEW defines a new
/// model-space window, keeping the command active.
MviewSwitchLayout(String),
/// Cancel MVIEW's temporary Model-space step and return to its layout.
MviewCancelToLayout(String),
/// 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 },
/// Apply one DIMEDIT operation to every selected dimension.
EditDimensions {
handles: Vec<Handle>,
operation: DimensionEditOperation,
},
/// 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,
/// Optional entity defaults collected by the interactive MTEXT
/// command (boundary, rotation, attachment, spacing and columns).
/// Existing-entity edits leave this as `None` and load the document
/// entity instead.
template: Option<Box<acadrust::MText>>,
},
/// Collect rich text without creating an MText entity.
SuspendForMTextInput {
pos: DVec3,
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,
},
/// Suspend the active TEXT command while the in-place editor collects one
/// independent line. The prepared entity carries the chosen style,
/// justification, rotation and two-point geometry. When the editor closes,
/// the command resumes so another line can be placed directly below it.
SuspendForTextInput {
pos: DVec3,
entity: acadrust::entities::Text,
},
/// Apply new pattern/scale/angle to an existing hatch entity.
HatcheditApply {
handle: Handle,
name: String,
scale: f32,
angle: f32,
operation: HatchEditOperation,
},
/// STRETCH crossing-window selection. The command can accumulate several
/// independent crossing windows before Enter ends the selection stage.
StretchWindow {
/// Handles already gathered by previous crossing windows / preselection.
handles: Vec<Handle>,
/// Every crossing window gathered so far.
windows: Vec<(DVec3, DVec3)>,
},
/// Stretch entities: move only vertices/endpoints inside any gathered
/// crossing window.
StretchEntities {
handles: Vec<Handle>,
/// Independent crossing windows that define the points to move.
windows: Vec<(DVec3, DVec3)>,
/// Translation vector applied once to every selected point.
delta: DVec3,
},
/// Extrude one or more profiles with the requested construction mode.
ExtrudeEntities {
handles: Vec<Handle>,
extent: ExtrudeExtent,
mode: ExtrudeMode,
taper_angle: f64,
color: [f32; 4],
},
/// Thicken one or more persistent surface bodies without consuming them.
ThickenEntities {
handles: Vec<Handle>,
distance: f64,
},
/// Resolve a profile, bounded area, or solid face for PRESSPULL.
PresspullPick {
handle: Option<Handle>,
point: DVec3,
offset: bool,
multiple: bool,
},
/// Apply a signed distance to the resolved PRESSPULL selection.
PresspullApply {
targets: Vec<crate::scene::model::presspull_model::PresspullTarget>,
distance: f64,
color: [f32; 4],
},
/// Revolve the profile entities around the given axis.
RevolveEntities {
handles: Vec<Handle>,
axis_start: glam::DVec3,
axis_end: glam::DVec3,
angle: f64,
start_angle: f64,
mode: ExtrudeMode,
color: [f32; 4],
},
/// Sweep the selected profiles along one path in a single undo step.
SweepEntities {
handles: Vec<Handle>,
path_handle: Handle,
mode: ExtrudeMode,
options: SweepOptions,
color: [f32; 4],
},
/// Loft through ordered cross-sections, with optional guides or a path.
LoftEntities {
sections: Vec<LoftSectionSelection>,
guides: Vec<Handle>,
path: Option<Handle>,
mode: ExtrudeMode,
options: LoftOptions,
color: [f32; 4],
},
/// Round or bevel one or more resolved B-rep edges on a solid.
SolidEdgeBlend {
handle: Handle,
edges: Vec<cadkernel::brep::EdgeKey>,
base_face: Option<cadkernel::brep::FaceKey>,
value: f64,
other_value: f64,
fillet: bool,
},
/// Offset a solid and optionally open selected faces.
SolidShell {
handle: Handle,
actions: Vec<ShellFaceAction>,
distance: f64,
},
SolidSubtract {
bases: Vec<Handle>,
cutters: Vec<Handle>,
convert_meshes: bool,
},
/// Split every selected solid or surface with one arbitrary plane.
/// `keep_point == None` retains both sides; otherwise it selects the side
/// containing that WCS point.
SliceEntities {
targets: Vec<Handle>,
plane: cadkernel::space::Plane,
keep_point: Option<DVec3>,
},
/// Split selected solids or surfaces with one selected analytic sheet.
SliceSurfaceEntities {
targets: Vec<Handle>,
cutter: Box<cadkernel::brep::Body>,
keep_point: Option<DVec3>,
},
/// 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,
},
/// End a command-owned live entity without replacing its already-current
/// document geometry. PLINE uses this for Enter/Escape after the latest
/// vertex was published, avoiding one redundant geometry epoch/GPU patch.
FinalizeLiveEntity(Handle),
/// 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),
/// Return a resolved point back to a suspended parent command (e.g. MTP / M2P).
ReturnPoint(DVec3),
}
/// 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>,
}
// ── 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: crate::t!(label).into_owned(),
keyword: keyword.to_string(),
}
}
/// A "finish" button that submits the step like Enter.
pub fn enter(label: &str) -> Self {
Self {
label: crate::t!(label).into_owned(),
keyword: String::new(),
}
}
}
/// What kind of input the command's current step expects from the command line.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum InputKind {
/// Normal point-picking / viewport interaction or keyword options.
/// The command line does not await dedicated text input.
#[default]
Point,
/// Single-token command-line text input (option letters, numeric radius,
/// layer name, block name, numeric angle). Space acts as a submit delimiter,
/// characters are automatically uppercased, and expressions like `5*2`
/// are evaluated.
SingleToken,
/// Free-form text prose (table cell contents, TEXT / MTEXT bodies,
/// dimension text overrides, attribute prompt defaults). Space is a
/// literal character, typed letter casing is preserved, math expression
/// evaluation is bypassed, Enter finishes the edit, and Shift+Enter
/// inserts a line break.
FreeText,
}
impl InputKind {
/// Returns `true` when the command is waiting for typed text input (`SingleToken` or `FreeText`).
pub fn wants_text(self) -> bool {
matches!(self, InputKind::SingleToken | InputKind::FreeText)
}
/// Returns `true` when the current prompt collects free-form prose.
pub fn is_free_text(self) -> bool {
self == InputKind::FreeText
}
}
#[derive(Clone, Debug)]
pub struct CurveMarker {
pub block: String,
pub align: bool,
pub plane: WorkingPlane,
}
/// Current screen projection and configured aperture for point-feature picking.
#[derive(Clone, Copy)]
pub struct PointPickContext {
pub view: glam::Mat4,
pub eye: DVec3,
pub bounds: iced::Rectangle,
pub aperture_px: f32,
}
pub trait CadCommand: Send {
/// Preserve source appearance for commands that extract existing entities.
fn preserve_commit_style(&self) -> bool { false }
fn nested_copy_bind_setting(&self) -> Option<bool> { None }
/// Symbol localization is committed together with the extracted entities.
fn nested_copy_symbol_names(&self) -> Option<&acadrust::nested_copy::NestedCopySymbolNames> { None }
/// Keep the layer already carried by entities committed by this command
/// instead of replacing it with the current drawing layer.
fn preserve_commit_layer(&self) -> bool {
false
}
/// 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()
}
/// Live search: called on each keystroke in the command line while the
/// command is active. Return true if the input updated internal filter
/// and the UI should refresh (prompt/options). Used for INSERT/MINSERT
/// incremental block name search without requiring Enter. Performance
/// critical — implementations must use precomputed caches and partial
/// sorting.
fn on_live_input(&mut self, _input: &str) -> bool {
false
}
/// Push the active coordinate frame in full precision. Geometry commands
/// use it for plane-local construction; inquiry and modify commands use it
/// for local deltas, angles and transformation axes.
fn set_working_plane(&mut self, _plane: WorkingPlane) {}
/// 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) {}
/// 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) {}
/// Constrain the cursor to a construction axis for this step.
fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
None
}
/// Override the model-space plane onto which viewport cursor rays are
/// projected for this command step.
fn cursor_plane(&self) -> Option<(DVec3, DVec3)> {
None
}
/// 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
}
/// Called when the user left-clicks in the viewport (point pick).
fn on_point(&mut self, pt: DVec3) -> CmdResult;
/// Opt in only while a point input selects an existing point feature.
fn wants_point_pick_context(&self) -> bool { false }
/// Refreshed for each point input so zoom and viewport changes are reflected.
fn set_point_pick_context(&mut self, _context: Option<PointPickContext>) {}
/// Called when the user presses Enter (finalize / next option).
fn on_enter(&mut self) -> CmdResult;
/// Whether a bare Enter should supply the drawing's continuation point as
/// this command's first point instead of calling [`Self::on_enter`]. Draw
/// commands opt in only while their first point is still unset; later
/// Enter presses retain their normal finish/cancel meaning.
fn enter_accepts_default_start(&self) -> bool {
false
}
/// Called when the user presses Escape (cancel).
#[allow(dead_code)]
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
/// Abort because the active drawing coordinate space is about to change.
/// Unlike `on_escape`, the default never interprets cancellation as
/// "finish with the points collected so far" (SPLINE does that for a
/// deliberate Escape). Commands owning a live document entity can
/// override this to close that entity's deferred history safely.
fn on_space_change(&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
}
/// Accept typed coordinates while object picking.
fn entity_pick_accepts_points(&self) -> bool {
false
}
/// Include filled hatch / DXF SOLID regions in the entity hit-test.
///
/// Most entity-pick commands operate on curve geometry and intentionally
/// keep the cheaper wire-only path. Commands that accept fill entities
/// override this so clicking inside a fill resolves its entity handle.
fn entity_pick_includes_fills(&self) -> bool {
false
}
/// Supply the mesh surface hit instead of the working-plane projection.
fn entity_pick_uses_surface_point(&self) -> bool {
false
}
/// Supply the picked surface or profile direction when available.
fn set_entity_pick_direction(&mut self, _direction: Option<DVec3>) {}
/// Render the entity under the cursor through the normal rollover
/// highlight while this command is waiting for an entity pick.
fn entity_pick_highlights_hover(&self) -> bool {
false
}
/// Expensive bounded-area acquisition runs once after cursor dwell, not
/// from every pointer event. The host supplies the same WCS surface pick
/// used for clicks, and clears the overlay when movement resumes.
fn entity_pick_deferred_hover(&self) -> bool {
false
}
fn on_deferred_entity_hover(
&mut self, _scene: &Scene, _handle: Option<Handle>, _point: DVec3,
) -> Vec<WireModel> {
Vec::new()
}
/// 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
}
/// Resume the command with collected rich text.
fn on_editor_text(&mut self, _value: String) {}
fn on_editor_display_height(&mut self, _height: f64) {}
/// 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 {
CmdResult::Cancel
}
/// Supply a resolved PRESSPULL target after an object or bounded-area pick.
fn on_presspull_target(
&mut self,
_target: crate::scene::model::presspull_model::PresspullTarget,
) {
}
/// Resume PRESSPULL after an atomic apply attempt. A failed operation keeps
/// the current targets available for another distance or selection undo.
fn on_presspull_applied(&mut self, _success: bool) {}
/// 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>) {}
/// 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 after a PEDIT operation changed its target.
fn on_pedit_applied(&mut self) {}
/// Consume a lasso or drag-box gesture while the command is active.
/// `fence` is the gesture boundary in drawing coordinates; `window` is
/// present for rectangular gestures. Returning `Some` prevents the normal
/// selection system from selecting the crossed entities.
fn on_drag_selection(
&mut self,
_fence: &[[f64; 2]],
_window: Option<([f64; 2], [f64; 2])>,
) -> Option<CmdResult> {
None
}
/// Whether an empty click may start a two-corner selection box for this
/// command instead of being reported as a missed entity pick.
fn accepts_drag_selection(&self) -> bool {
false
}
/// 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> {
vec![]
}
/// Request a source snapshot only when the hovered entity changes.
/// Commands can cache geometry across mouse moves without holding a scene.
fn wants_hover_entity(&self, _handle: Handle) -> bool {
false
}
fn inject_hover_entity(&mut self, _handle: Handle, _entity: EntityType) {}
/// 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> {
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> {
self.on_mouse_move(pt).into_iter().collect()
}
/// Source entities replaced by the current live preview. The host removes
/// these from the resident render until the command commits or cancels.
/// Commands such as COPY keep their sources visible and use the default.
fn preview_hidden_handles(&self) -> &[Handle] {
&[]
}
/// What kind of input the current step expects from the command line.
/// Default is [`InputKind::Point`].
fn input_kind(&self) -> InputKind {
#[allow(deprecated)]
if self.wants_text_with_spaces() {
InputKind::FreeText
} else if self.wants_text_input() {
InputKind::SingleToken
} else {
InputKind::Point
}
}
/// Returns `true` when the command is waiting for text typed in the command line.
#[deprecated(note = "Use `input_kind().wants_text()` instead")]
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
}
/// Current drawing-persisted SKETCH settings.
fn sketch_settings(&self) -> Option<(i16, f64, f64)> {
None
}
/// Current drawing-persisted multiline creation settings.
fn mline_settings(&self) -> Option<(f64, i16, String, Option<Handle>)> {
None
}
/// Returns `true` when the active text prompt expects free-form prose
/// that can legitimately contain whitespace (the body of a TEXT /
/// MTEXT / DDEDIT entity, a table cell, an attribute default value).
#[deprecated(note = "Use `input_kind().is_free_text()` instead")]
fn wants_text_with_spaces(&self) -> bool {
false
}
/// The current step collects free-form prose from the command line:
/// Space is a literal character, typed case is preserved, Enter
/// finishes the edit and Shift+Enter inserts a line break.
fn is_free_text_step(&self) -> bool {
self.input_kind().is_free_text()
}
/// 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
}
fn selection_forces_add(&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
}
/// Exclude locked-layer entities from injected selection geometry.
fn selection_entities_exclude_locked(&self) -> bool { false }
fn inject_selection_entities(&mut self, _entities: Vec<SelectionEntity>) {}
fn area_preview_regions(&self) -> Option<Vec<AreaPreviewRegion>> {
None
}
fn hatch_preview_models(
&self,
) -> Option<Vec<crate::scene::model::hatch_model::HatchModel>> {
None
}
/// 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
}
/// 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 block attribute definitions after the INSERT point is picked.
fn attreq_set_attdefs(
&mut self,
_attdefs: Vec<acadrust::entities::AttributeDefinition>,
) -> Option<acadrust::EntityType> {
None
}
/// 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
}
/// 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 {
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) {}
/// Supply the tessellated surface area associated with the picked entity.
/// Commands that measure mesh-backed objects can opt in without owning the
/// scene's render cache.
fn inject_picked_surface_area(&mut self, _area: f64) {}
/// 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
}
/// Whether a bare dynamic angle inherits its sign from the world-XY
/// cursor side. Commands with their own 3D axis semantics opt out.
fn dyn_auto_sign_angle(&self) -> bool {
true
}
/// 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
}
/// Optional world-space point where the dynamic value box should be
/// centred. The view projects it with the active camera.
fn dyn_label_point(&self, _cursor: DVec3) -> Option<DVec3> {
None
}
}
// ── 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()
}
#[cfg(test)]
mod constraint_registry_tests {
use super::*;
/// Ensures every constraint command remains discoverable by autocomplete
/// and command listings.
#[test]
fn every_constraint_command_is_in_the_autocomplete_registry() {
let names = all_registered_command_names();
for id in [
"CCONSTRAINT",
"EDCONSTRAINT",
"CPCONSTRAINT",
"MPCONSTRAINT",
"OCCONSTRAINT",
"HCONSTRAINT",
"VCONSTRAINT",
"PCONSTRAINT",
"QCONSTRAINT",
"ECONSTRAINT",
"TCONSTRAINT",
"NCONSTRAINT",
"NRCONSTRAINT",
"LCONSTRAINT",
"FXCONSTRAINT",
"SYCONSTRAINT",
"DCONSTRAINT",
"ACONSTRAINT",
] {
assert!(names.contains(&id), "{id} is missing from the command registry");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mid2point_command() {
let mut cmd = Mid2PointCommand::new();
assert_eq!(cmd.name(), "MTP");
assert_eq!(cmd.prompt(), "_mtp Specify first point of mid:");
// First point
let res = cmd.on_point(DVec3::new(10.0, 20.0, 0.0));
assert!(matches!(res, CmdResult::NeedPoint));
assert_eq!(cmd.prompt(), "_mtp Specify second point of mid:");
// Preview wires
let wires = cmd.on_preview_wires(DVec3::new(30.0, 40.0, 0.0));
assert!(!wires.is_empty());
// Second point
let res = cmd.on_point(DVec3::new(30.0, 40.0, 0.0));
assert!(matches!(res, CmdResult::ReturnPoint(mid) if mid == DVec3::new(20.0, 30.0, 0.0)));
}
#[test]
fn test_mid2point_cancel() {
let mut cmd = Mid2PointCommand::new();
assert!(matches!(cmd.on_escape(), CmdResult::Cancel));
assert!(matches!(cmd.on_enter(), CmdResult::Cancel));
}
#[test]
fn test_mtp_registered() {
let names = all_registered_command_names();
assert!(names.contains(&"MTP"));
assert!(names.contains(&"M2P"));
}
}