feat(commands): integrate standard CAD commands and fix behavior gaps

Broad pass over the command set: integrate missing commands, wire
previously-unreachable ribbon/menu buttons, and correct name/behaviour
mismatches. Every UI command token now resolves (real handler where the
infrastructure exists, an honest "not yet implemented" status otherwise).

New commands:
- Draw: 3DPOLY, 2D solid (SO/SOLID2D), HELIX, TRACE, SKETCH, DIMCENTER
  (DCE/CENTERMARK)
- Modify/inquiry: REVERSE, MEASUREGEOM (MEA), DBLIST
- Data/manage: BASE, OVERKILL (exact-duplicate removal), AUDIT (read-only
  integrity report), SETVAR (system-variable get/set), SCRIPT (.scr runner),
  FINDNONPURGEABLE

Behaviour fixes:
- LINE and SPLINE gain Close/Undo; AREA computes area/perimeter in f64
  (correct at large coordinates)
- ATTEDIT/ATE unified so the alias and the ribbon button open the
  interactive editor; -ATTEDIT is the command-line form
- Linear/aligned/radius/diameter dimensions gain a text override (Text) and
  text angle (Angle) option
- INSERT gains Scale/Rotate options; COPY gains an Array option
- LINETYPE SET sets the current entity linetype; SETBYLAYER resets overrides
- UNDO accepts a step count

Integrations: standard dimension/block aliases (DLI/DRA/DAN, BMAKE,
EXPORTPDF, DDIM), drafting-aid and screen toggles (GRID/SNAP/POLAR/OSNAP/
DSETTINGS/CLEANSCREEN/QUICKPROPERTIES), viewport tiling
(CASCADE/HORIZONTAL/VERTICAL/VPJOIN), and customization entry points
(CUI/ALIASEDIT).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-28 19:27:35 +03:00
1 parent e04b580a70
commit 0ca3954b5d
32 files changed
+2763 -67

No files matched your search

+18 -13
View File
@@ -1,13 +1,13 @@
// AREA command — compute area and perimeter of a polygon picked point by point.
// Press Enter to close and calculate.
use glam::{DVec3, Vec3};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::scene::model::wire_model::WireModel;
pub struct AreaCommand {
points: Vec<Vec3>,
points: Vec<DVec3>,
}
impl AreaCommand {
@@ -32,7 +32,7 @@ impl CadCommand for AreaCommand {
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3();
fn on_point(&mut self, pt: DVec3) -> CmdResult {
self.points.push(pt);
CmdResult::NeedPoint
}
@@ -41,28 +41,33 @@ impl CadCommand for AreaCommand {
if self.points.len() < 3 {
return CmdResult::Cancel;
}
// Shoelace formula in the world XY plane.
// Shoelace formula in the world XY plane, evaluated in f64 relative to
// the first vertex. Subtracting that origin keeps the cross-product
// terms small even at large (survey/UTM) coordinates, avoiding the
// catastrophic cancellation a raw f32/absolute evaluation suffers.
let n = self.points.len();
let mut area_sum = 0.0f32;
let mut perimeter = 0.0f32;
let origin = self.points[0];
let mut area_sum = 0.0f64;
let mut perimeter = 0.0f64;
for idx in 0..n {
let a = self.points[idx];
let b = self.points[(idx + 1) % n];
let a = self.points[idx] - origin;
let b = self.points[(idx + 1) % n] - origin;
area_sum += a.x * b.y - b.x * a.y;
perimeter += (b - a).length();
perimeter += (self.points[(idx + 1) % n] - self.points[idx]).length();
}
let area = (area_sum * 0.5).abs();
let msg = format!("Area = {area:.4}, Perimeter = {perimeter:.4}");
CmdResult::Measurement(msg)
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
if self.points.is_empty() {
return None;
}
let mut pts: Vec<[f32; 3]> = self.points.iter().map(|p| [p.x, p.y, p.z]).collect();
pts.push([pt.x, pt.y, pt.z]);
pts.push([self.points[0].x, self.points[0].y, self.points[0].z]);
let f = |p: DVec3| [p.x as f32, p.y as f32, p.z as f32];
let mut pts: Vec<[f32; 3]> = self.points.iter().map(|p| f(*p)).collect();
pts.push(f(pt));
pts.push(f(self.points[0]));
Some(WireModel {
name: "area_preview".into(),
points: pts,
+303
View File
@@ -0,0 +1,303 @@
// MEASUREGEOM command (alias MEA) — multi-mode geometry measurement.
//
// First step prompts for a mode keyword:
// Distance / Radius / Angle / ARea
// then collects the geometry the mode needs and prints a one-line readout
// via `CmdResult::Measurement`, ending the command.
//
// DISTANCE — two points → distance, delta X/Y/Z, angle in the XY plane.
// AREA — points until Enter → area (f64 shoelace relative to the first
// vertex, mirroring inquiry/area.rs for precision) + perimeter.
// ANGLE — three points (vertex + two ray endpoints) → angle in degrees.
// RADIUS — pick a Circle or Arc → radius + diameter.
//
// All arithmetic is kept in f64 (picked points stay full precision; downcasting
// to f32 loses several hundredths of a unit at survey-scale coordinates).
use acadrust::{EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
// ── Ribbon definition ─────────────────────────────────────────────────────
#[allow(dead_code)] // ribbon definition ready for wiring; command works via the command line
pub fn tool() -> ToolDef {
ToolDef {
id: "MEASUREGEOM",
label: "Measure Geometry",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/line.svg")),
event: ModuleEvent::Command("MEASUREGEOM".to_string()),
}
}
// ── Mode ───────────────────────────────────────────────────────────────────
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mode {
/// Awaiting the mode keyword.
Choose,
Distance,
Area,
Angle,
Radius,
}
// ── Command implementation ──────────────────────────────────────────────────
pub struct MeasureGeomCommand {
mode: Mode,
/// Picked points for the active point-pick mode.
points: Vec<DVec3>,
/// The picked entity for RADIUS, injected before `on_entity_pick`.
picked: Option<EntityType>,
}
impl MeasureGeomCommand {
pub fn new() -> Self {
Self {
mode: Mode::Choose,
points: vec![],
picked: None,
}
}
/// Build a cyan preview wire connecting the picked points and the cursor.
fn preview_wire(name: &str, pts: Vec<[f32; 3]>) -> WireModel {
WireModel {
name: name.to_string(),
points: pts,
points_low: Vec::new(),
color: WireModel::CYAN,
selected: false,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
}
/// Extract (radius) from a Circle or Arc; `None` for anything else.
fn radius_of(entity: &EntityType) -> Option<f64> {
match entity {
EntityType::Circle(c) => Some(c.radius),
EntityType::Arc(a) => Some(a.radius),
_ => None,
}
}
/// DISTANCE readout for the two collected points.
fn distance_msg(p1: DVec3, p2: DVec3) -> String {
let delta = p2 - p1;
let dist = delta.length();
let dx = delta.x;
let dy = delta.y; // drawing plane is world XY
let dz = delta.z; // elevation
let angle_xy = dy.atan2(dx).to_degrees();
format!(
"Distance = {dist:.4}, Angle in XY Plane = {angle_xy:.4}°\n Delta X = {dx:.4}, Delta Y = {dy:.4}, Delta Z = {dz:.4}",
)
}
/// AREA readout: shoelace area (f64, relative to first vertex) + perimeter.
fn area_msg(points: &[DVec3]) -> String {
let n = points.len();
let origin = points[0];
let mut area_sum = 0.0f64;
let mut perimeter = 0.0f64;
for idx in 0..n {
let a = points[idx] - origin;
let b = points[(idx + 1) % n] - origin;
area_sum += a.x * b.y - b.x * a.y;
perimeter += (points[(idx + 1) % n] - points[idx]).length();
}
let area = (area_sum * 0.5).abs();
format!("Area = {area:.4}, Perimeter = {perimeter:.4}")
}
/// ANGLE readout: angle at `vertex` between the rays to `a` and `b`.
fn angle_msg(vertex: DVec3, a: DVec3, b: DVec3) -> String {
let va = a - vertex;
let vb = b - vertex;
let la = va.length();
let lb = vb.length();
if la == 0.0 || lb == 0.0 {
return "Angle = 0.0000° (degenerate rays)".to_string();
}
let cos = (va.dot(vb) / (la * lb)).clamp(-1.0, 1.0);
let angle = cos.acos().to_degrees();
format!("Angle = {angle:.4}°")
}
}
impl CadCommand for MeasureGeomCommand {
fn name(&self) -> &'static str {
"MEASUREGEOM"
}
fn prompt(&self) -> String {
match self.mode {
Mode::Choose => {
"MEASUREGEOM Enter an option [Distance/Radius/Angle/ARea]:".into()
}
Mode::Distance => {
if self.points.is_empty() {
"MEASUREGEOM Specify first point:".into()
} else {
"MEASUREGEOM Specify second point:".into()
}
}
Mode::Area => {
if self.points.is_empty() {
"MEASUREGEOM Specify first corner point (Enter to cancel):".into()
} else {
format!(
"MEASUREGEOM Specify next point ({} picked, Enter to calculate):",
self.points.len()
)
}
}
Mode::Angle => match self.points.len() {
0 => "MEASUREGEOM Specify vertex point:".into(),
1 => "MEASUREGEOM Specify first ray point:".into(),
_ => "MEASUREGEOM Specify second ray point:".into(),
},
Mode::Radius => "MEASUREGEOM Select arc or circle:".into(),
}
}
fn wants_text_input(&self) -> bool {
// Only the opening mode-keyword step reads a typed token.
self.mode == Mode::Choose
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if self.mode != Mode::Choose {
return None;
}
let t = text.trim().to_uppercase();
self.mode = match t.as_str() {
"D" | "DISTANCE" => Mode::Distance,
"R" | "RADIUS" => Mode::Radius,
"A" | "ANGLE" => Mode::Angle,
"AR" | "AREA" => Mode::Area,
_ => return Some(CmdResult::NeedPoint), // re-prompt on unknown keyword
};
Some(CmdResult::NeedPoint)
}
fn needs_entity_pick(&self) -> bool {
self.mode == Mode::Radius
}
fn inject_before_entity_pick(&self) -> bool {
true
}
fn inject_picked_entity(&mut self, entity: EntityType) {
self.picked = Some(entity);
}
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
match self.picked.as_ref().and_then(Self::radius_of) {
Some(radius) => {
let diameter = radius * 2.0;
CmdResult::Measurement(format!(
"Radius = {radius:.4}, Diameter = {diameter:.4}"
))
}
// Picked something that is not a circle or arc — keep prompting.
None => CmdResult::NeedPoint,
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.mode {
Mode::Distance => {
self.points.push(pt);
if self.points.len() == 2 {
CmdResult::Measurement(Self::distance_msg(self.points[0], self.points[1]))
} else {
CmdResult::NeedPoint
}
}
Mode::Angle => {
self.points.push(pt);
if self.points.len() == 3 {
CmdResult::Measurement(Self::angle_msg(
self.points[0],
self.points[1],
self.points[2],
))
} else {
CmdResult::NeedPoint
}
}
Mode::Area => {
self.points.push(pt);
CmdResult::NeedPoint
}
// Choose / Radius do not take point picks.
_ => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
match self.mode {
Mode::Area => {
if self.points.len() < 3 {
CmdResult::Cancel
} else {
CmdResult::Measurement(Self::area_msg(&self.points))
}
}
_ => CmdResult::Cancel,
}
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let f = |p: DVec3| [p.x as f32, p.y as f32, p.z as f32];
match self.mode {
Mode::Distance | Mode::Angle => {
if self.points.is_empty() {
return None;
}
let mut pts: Vec<[f32; 3]> = self.points.iter().map(|p| f(*p)).collect();
pts.push(f(pt));
Some(Self::preview_wire("measuregeom_preview", pts))
}
Mode::Area => {
if self.points.is_empty() {
return None;
}
let mut pts: Vec<[f32; 3]> = self.points.iter().map(|p| f(*p)).collect();
pts.push(f(pt));
pts.push(f(self.points[0]));
Some(Self::preview_wire("measuregeom_preview", pts))
}
_ => None,
}
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration {
names: &["MEASUREGEOM", "MEA"]
}); // MeasureGeomCommand
+1
View File
@@ -2,3 +2,4 @@ pub mod area;
pub mod dist;
pub mod divide;
pub mod id;
pub mod measuregeom;