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

+70 -6
View File
@@ -35,6 +35,10 @@ pub struct CopyCommand {
wire_models: Vec<WireModel>,
step: Step,
count: usize,
/// Number of items for an Array copy (None = place copies one at a time).
array_count: Option<usize>,
/// True while the next typed value is captured as the array item count.
awaiting_count: bool,
}
impl CopyCommand {
@@ -44,6 +48,8 @@ impl CopyCommand {
wire_models,
step: Step::Base,
count: 0,
array_count: None,
awaiting_count: false,
}
}
}
@@ -54,15 +60,27 @@ impl CadCommand for CopyCommand {
}
fn prompt(&self) -> String {
if self.awaiting_count {
return "COPY Enter number of items to array:".to_string();
}
match &self.step {
Step::Base => format!(
"COPY Specify base point [{} objects]:",
self.handles.len()
),
Step::Placing(base) => format!(
"COPY Specify destination [{} copies so far | Enter=done | base {:.3},{:.3}]:",
self.count, base.x, base.y
),
Step::Placing(base) => {
if let Some(n) = self.array_count {
format!(
"COPY Specify second point for {n}-item array [base {:.3},{:.3}]:",
base.x, base.y
)
} else {
format!(
"COPY Specify destination [{} copies so far | Array | Enter=done | base {:.3},{:.3}]:",
self.count, base.x, base.y
)
}
}
}
}
@@ -74,19 +92,65 @@ impl CadCommand for CopyCommand {
}
Step::Placing(base) => {
let delta = pt - *base;
self.count += 1;
CmdResult::CopySelected(self.handles.clone(), EntityTransform::Translate(delta))
if let Some(n) = self.array_count {
// Array: place n-1 copies at delta, 2·delta, … so the result
// is n evenly spaced items including the original. Ends here.
let transforms: Vec<EntityTransform> = (1..n)
.map(|k| EntityTransform::Translate(delta * k as f64))
.collect();
CmdResult::BatchCopy(self.handles.clone(), transforms)
} else {
self.count += 1;
CmdResult::CopySelected(
self.handles.clone(),
EntityTransform::Translate(delta),
)
}
}
}
}
fn on_enter(&mut self) -> CmdResult {
// Cancel an in-progress Array count entry; otherwise finish.
if self.awaiting_count {
self.awaiting_count = false;
return CmdResult::NeedPoint;
}
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn wants_text_input(&self) -> bool {
matches!(self.step, Step::Placing(_))
}
fn point_step_accepts_keywords(&self) -> bool {
// Accept the Array keyword while placing, but route the typed item
// count straight through on_text_input.
matches!(self.step, Step::Placing(_)) && !self.awaiting_count
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if self.awaiting_count {
if let Ok(n) = text.trim().parse::<usize>() {
if n >= 2 {
self.array_count = Some(n);
}
}
self.awaiting_count = false;
return Some(CmdResult::NeedPoint);
}
match text.trim().to_uppercase().as_str() {
"A" | "ARRAY" => {
self.awaiting_count = true;
Some(CmdResult::NeedPoint)
}
_ => None,
}
}
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
let Step::Placing(base) = &self.step else {
return vec![];
+1
View File
@@ -12,6 +12,7 @@ pub mod mirror;
pub mod offset;
pub mod pedit;
pub mod refedit;
pub mod reverse;
pub mod rotate;
pub mod scale;
pub mod spline_ops;
+194
View File
@@ -0,0 +1,194 @@
// REVERSE command — reverse the direction (vertex / point order) of a single
// picked curve.
//
// The user picks one open or closed curve; the command builds a reversed copy
// and swaps it in via `CmdResult::ReplaceEntity`. Supported curve types:
//
// * Line — swap start / end.
// * LwPolyline — reverse the vertex list. The bulge stored at a vertex
// describes the arc on the segment *starting* at that vertex,
// so a plain `vertices.reverse()` would attach each bulge to
// the wrong segment (and with the wrong sense). We therefore
// recompute every bulge from the original segment bulges (see
// `reverse_lwpolyline`).
// * Polyline3D — reverse the vertex list (no bulges to reconcile).
// * Spline — reverse control points and fit points, then regenerate the
// clamped knot vector (mirrors the REVERSE branch of
// `splinedit::apply_spline_op`).
//
// Any other entity type is left untouched: the command returns
// `CmdResult::NeedPoint` and keeps prompting so nothing is corrupted.
use acadrust::entities::Spline;
use acadrust::{EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
// ── Ribbon definition ───────────────────────────────────────────────────────
#[allow(dead_code)] // ribbon definition ready for wiring; command works via the command line
pub fn tool() -> ToolDef {
ToolDef {
id: "REVERSE",
label: "Reverse",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/line.svg")),
event: ModuleEvent::Command("REVERSE".to_string()),
}
}
// ── Command implementation ──────────────────────────────────────────────────
pub struct ReverseCommand {
/// The picked entity, injected by the host before `on_entity_pick` runs.
/// `None` until the host injects it.
picked: Option<EntityType>,
}
impl ReverseCommand {
pub fn new() -> Self {
Self { picked: None }
}
/// Build a reversed copy of `entity`, or `None` for an unsupported type.
fn reversed(entity: &EntityType) -> Option<EntityType> {
match entity {
EntityType::Line(line) => {
let mut out = line.clone();
std::mem::swap(&mut out.start, &mut out.end);
Some(EntityType::Line(out))
}
EntityType::LwPolyline(pl) => Some(EntityType::LwPolyline(reverse_lwpolyline(pl))),
EntityType::Polyline3D(pl) => {
let mut out = pl.clone();
out.vertices.reverse();
Some(EntityType::Polyline3D(out))
}
EntityType::Spline(sp) => Some(EntityType::Spline(reverse_spline(sp))),
_ => None,
}
}
}
/// Reverse an LwPolyline, reconciling per-segment bulges.
///
/// Bulge semantics: `vertices[k].bulge` is the bulge of the segment that
/// *starts* at vertex `k`. For an open polyline with `n` vertices there are
/// `n - 1` segments (the trailing vertex's bulge is unused); for a closed
/// polyline there are `n` segments (the trailing vertex's bulge is the closing
/// segment back to vertex 0).
///
/// After reversal the new vertex `i` is the original vertex `n-1-i`. The new
/// segment from new-vertex `i` to new-vertex `i+1` is the original segment
/// between original vertices `n-1-i` and `n-2-i`, traversed backwards — i.e.
/// the original segment that *starts* at vertex `n-2-i`. Reversing the
/// traversal direction flips an arc's sense, so the new bulge is the negation
/// of that original segment's bulge:
///
/// new_vertices[i].bulge = -old_vertices[n-2-i].bulge (open / interior)
///
/// For a closed polyline the closing segment wraps, so the general modular form
/// is used: new seg `i` (from new vertex `i`) corresponds to original segment
/// starting at original vertex `(n-1-i-1).rem_euclid(n)`. The widths follow the
/// vertex they are attached to and are carried along with the reversed order;
/// they are not segment-relative, so they need no shift.
fn reverse_lwpolyline(pl: &acadrust::LwPolyline) -> acadrust::LwPolyline {
let mut out = pl.clone();
let n = pl.vertices.len();
if n < 2 {
return out;
}
// Reverse vertex positions and per-vertex widths by cloning in reverse.
for i in 0..n {
let src = &pl.vertices[n - 1 - i];
out.vertices[i].location = src.location;
out.vertices[i].start_width = src.start_width;
out.vertices[i].end_width = src.end_width;
}
// Recompute bulges from the original segment bulges.
if pl.is_closed {
// n segments; closing segment included.
for i in 0..n {
let src_seg_start = (n - 1 - i + n - 1) % n; // = (2n - 2 - i) % n
out.vertices[i].bulge = -pl.vertices[src_seg_start].bulge;
}
} else {
// n - 1 segments. New segment i (for i in 0..n-1) maps to original
// segment starting at vertex n-2-i. The trailing vertex's bulge is
// unused for an open polyline; clear it for cleanliness.
for i in 0..n - 1 {
out.vertices[i].bulge = -pl.vertices[n - 2 - i].bulge;
}
out.vertices[n - 1].bulge = 0.0;
}
out
}
/// Reverse a Spline: flip control points and fit points, regenerate the
/// clamped knot vector. Mirrors `splinedit::apply_spline_op`'s REVERSE branch.
fn reverse_spline(sp: &Spline) -> Spline {
let mut out = sp.clone();
out.control_points.reverse();
out.fit_points.reverse();
// Weights track control points one-to-one for rational splines.
if out.weights.len() == out.control_points.len() {
out.weights.reverse();
}
out.knots =
Spline::generate_clamped_knots(out.degree as usize, out.control_points.len());
out
}
impl CadCommand for ReverseCommand {
fn name(&self) -> &'static str {
"REVERSE"
}
fn prompt(&self) -> String {
"REVERSE Select line, polyline or spline to reverse:".to_string()
}
fn needs_entity_pick(&self) -> bool {
true
}
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::reversed) {
Some(reversed) => CmdResult::ReplaceEntity(handle, vec![reversed]),
// Unsupported entity type — keep prompting, do not corrupt it.
None => CmdResult::NeedPoint,
}
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
// ── Autocomplete registry ───────────────────────────────────────────────────
inventory::submit!(crate::command::CommandRegistration {
names: &["REVERSE"]
}); // ReverseCommand