2026-03-22 14:56:27 +03:00
|
|
|
|
// Command system — trait for interactive CAD commands.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Each tool that requires user interaction (point picks, object selection,
|
|
|
|
|
|
// numeric input) implements `CadCommand`. The active command receives
|
|
|
|
|
|
// viewport events from main.rs and returns `CmdResult` tokens that tell
|
|
|
|
|
|
// the host what to do next.
|
|
|
|
|
|
|
|
|
|
|
|
use crate::scene::hatch_model::HatchModel;
|
|
|
|
|
|
use crate::scene::wire_model::WireModel;
|
2026-06-09 13:41:21 -04:00
|
|
|
|
use crate::scene::Scene;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
use acadrust::{EntityType, Handle};
|
|
|
|
|
|
use glam::Vec3;
|
|
|
|
|
|
|
2026-06-09 13:41:21 -04:00
|
|
|
|
/// Domain object resolved under the cursor for C3D-style ObjectPick snapping.
|
|
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
|
|
|
|
pub struct ObjectPickHit {
|
|
|
|
|
|
pub handle: Handle,
|
|
|
|
|
|
pub x: f64,
|
|
|
|
|
|
pub y: f64,
|
|
|
|
|
|
pub label: &'static str,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
// ── Transform ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// A geometric transformation applied to existing entities.
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
|
pub enum EntityTransform {
|
fix(commands): finish the world-XY coordinate migration (#54)
The drawing plane is world XY, but break, join, rectangular array, the
stretch/window move, the spline preview wire and several doc comments
still used the stale "Y-up XZ" mapping (world Z = DXF Y). That made
those commands operate against a constant-elevation axis: arc/circle/
line/spline break and join picked the wrong points, rectangular array
stepped rows along the Z (elevation) axis, window-stretch tested the
wrong window bound, and the spline wire drew in the wrong plane.
Map every drawing-plane coordinate through .y (elevation on .z),
matching the entity builders, the inquiry commands and the central
transform. Stale Y-up comments updated to avoid future confusion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:39:24 +03:00
|
|
|
|
/// Move every point by the given world-space delta (world XY plane).
|
2026-03-22 14:56:27 +03:00
|
|
|
|
Translate(Vec3),
|
fix(commands): finish the world-XY coordinate migration (#54)
The drawing plane is world XY, but break, join, rectangular array, the
stretch/window move, the spline preview wire and several doc comments
still used the stale "Y-up XZ" mapping (world Z = DXF Y). That made
those commands operate against a constant-elevation axis: arc/circle/
line/spline break and join picked the wrong points, rectangular array
stepped rows along the Z (elevation) axis, window-stretch tested the
wrong window bound, and the spline wire drew in the wrong plane.
Map every drawing-plane coordinate through .y (elevation on .z),
matching the entity builders, the inquiry commands and the central
transform. Stale Y-up comments updated to avoid future confusion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:39:24 +03:00
|
|
|
|
/// Rotate around `center` by `angle_rad` in the world XY plane.
|
2026-03-22 14:56:27 +03:00
|
|
|
|
Rotate { center: Vec3, angle_rad: f32 },
|
|
|
|
|
|
/// Uniform scale from `center` by `factor`.
|
|
|
|
|
|
Scale { center: Vec3, factor: f32 },
|
fix(commands): finish the world-XY coordinate migration (#54)
The drawing plane is world XY, but break, join, rectangular array, the
stretch/window move, the spline preview wire and several doc comments
still used the stale "Y-up XZ" mapping (world Z = DXF Y). That made
those commands operate against a constant-elevation axis: arc/circle/
line/spline break and join picked the wrong points, rectangular array
stepped rows along the Z (elevation) axis, window-stretch tested the
wrong window bound, and the spline wire drew in the wrong plane.
Map every drawing-plane coordinate through .y (elevation on .z),
matching the entity builders, the inquiry commands and the central
transform. Stale Y-up comments updated to avoid future confusion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:39:24 +03:00
|
|
|
|
/// Mirror across the line through `p1`→`p2` in the world XY plane.
|
2026-03-22 14:56:27 +03:00
|
|
|
|
Mirror { p1: Vec3, p2: Vec3 },
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 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: Vec3, p2: Vec3 },
|
fix(commands): finish the world-XY coordinate migration (#54)
The drawing plane is world XY, but break, join, rectangular array, the
stretch/window move, the spline preview wire and several doc comments
still used the stale "Y-up XZ" mapping (world Z = DXF Y). That made
those commands operate against a constant-elevation axis: arc/circle/
line/spline break and join picked the wrong points, rectangular array
stepped rows along the Z (elevation) axis, window-stretch tested the
wrong window bound, and the spline wire drew in the wrong plane.
Map every drawing-plane coordinate through .y (elevation on .z),
matching the entity builders, the inquiry commands and the central
transform. Stale Y-up comments updated to avoid future confusion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:39:24 +03:00
|
|
|
|
/// Circle in the world XY plane.
|
2026-03-22 14:56:27 +03:00
|
|
|
|
Circle { center: Vec3, radius: f32 },
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Result token ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// Returned by every `CadCommand` method to tell main.rs what to do.
|
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
|
pub enum CmdResult {
|
|
|
|
|
|
/// Command is still waiting for the next point; show updated prompt.
|
|
|
|
|
|
NeedPoint,
|
|
|
|
|
|
/// Update the committed-segment wire (normal colour) and keep collecting points.
|
|
|
|
|
|
InterimWire(WireModel),
|
|
|
|
|
|
/// Update the in-progress (cyan) preview wire in the viewport.
|
|
|
|
|
|
Preview(WireModel),
|
|
|
|
|
|
/// Commit an acadrust entity to the document; keep the command active.
|
|
|
|
|
|
CommitEntity(EntityType),
|
|
|
|
|
|
/// Commit an acadrust entity to the document and end the command.
|
|
|
|
|
|
CommitAndExit(EntityType),
|
2026-05-31 19:27:06 +03:00
|
|
|
|
/// Commit a Model-tab 3D solid: the acadrust entity (for selection /
|
|
|
|
|
|
/// persistence) plus its truck B-rep (cached for boolean ops + shaded
|
|
|
|
|
|
/// rendering). Ends the command.
|
|
|
|
|
|
CommitSolid {
|
|
|
|
|
|
entity: EntityType,
|
|
|
|
|
|
solid: Box<truck_modeling::Solid>,
|
|
|
|
|
|
},
|
2026-05-31 14:44:18 +03:00
|
|
|
|
/// Commit an acadrust entity, end the command, and open the in-place text
|
|
|
|
|
|
/// editor on it (used by MLEADER to type the annotation after placement).
|
|
|
|
|
|
CommitAndEditText(EntityType),
|
|
|
|
|
|
/// Commit several entities, end the command, and open the in-place text
|
|
|
|
|
|
/// editor on the one at `edit_index` (used by LEADER to place the leader
|
|
|
|
|
|
/// line plus an empty MText annotation, then type into the MText).
|
|
|
|
|
|
CommitManyAndEditText {
|
|
|
|
|
|
entities: Vec<EntityType>,
|
|
|
|
|
|
edit_index: usize,
|
|
|
|
|
|
},
|
2026-03-22 14:56:27 +03:00
|
|
|
|
/// Create a block definition from existing entities and insert one reference.
|
|
|
|
|
|
CreateBlock {
|
|
|
|
|
|
handles: Vec<Handle>,
|
|
|
|
|
|
name: String,
|
|
|
|
|
|
base: Vec3,
|
|
|
|
|
|
},
|
|
|
|
|
|
/// Apply a transform to selected entities and end the command.
|
|
|
|
|
|
TransformSelected(Vec<Handle>, EntityTransform),
|
|
|
|
|
|
/// Copy selected entities with a transform; command stays active for more copies.
|
|
|
|
|
|
CopySelected(Vec<Handle>, EntityTransform),
|
|
|
|
|
|
/// Commit a hatch fill (stored in Scene::hatches, not the DXF document).
|
|
|
|
|
|
CommitHatch(HatchModel),
|
|
|
|
|
|
/// Copy selected entities with multiple transforms (e.g. rectangular array); end command.
|
|
|
|
|
|
BatchCopy(Vec<Handle>, Vec<EntityTransform>),
|
|
|
|
|
|
/// Erase `handle` and replace with new entities; command stays active.
|
|
|
|
|
|
ReplaceEntity(Handle, Vec<EntityType>),
|
|
|
|
|
|
/// Replace / delete multiple entities and add new ones; command ends.
|
|
|
|
|
|
/// Each pair: (handle_to_erase, replacement_entities) — empty vec = delete only.
|
|
|
|
|
|
ReplaceMany(Vec<(Handle, Vec<EntityType>)>, Vec<EntityType>),
|
|
|
|
|
|
/// Cancel: discard any preview and end the command.
|
|
|
|
|
|
Cancel,
|
|
|
|
|
|
/// End the selection-gather phase and re-dispatch the named command
|
|
|
|
|
|
/// with the gathered handles installed as the active scene selection.
|
|
|
|
|
|
Relaunch(String, Vec<Handle>),
|
|
|
|
|
|
/// 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> },
|
Feat: viewport display lock, extended properties, VPLAYER and VPORTS commands
- Enforce status.locked in pan_active_viewport and zoom_active_viewport:
locked viewports now reject pan/zoom inputs entirely
- Add perspective, render_mode, shade_plot_mode, hide_plot, ucs_icon_visible
fields to the viewport property panel with appropriate pickers/toggles
- Add VPORTS command: lists all user viewports in the current layout
(id, size, position, scale, on/locked state) in the command line
- Add VPLAYER command: per-viewport layer freeze/thaw from the command line
(F <layer> to freeze, T <layer> to thaw, Enter to exit)
- Add CmdResult::VpLayerUpdate variant and its handler in apply_cmd_result
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 23:52:18 +03:00
|
|
|
|
/// Freeze or thaw layers by name in the given viewport; command stays active.
|
|
|
|
|
|
VpLayerUpdate {
|
|
|
|
|
|
vp_handle: Handle,
|
|
|
|
|
|
freeze: Vec<String>,
|
|
|
|
|
|
thaw: Vec<String>,
|
|
|
|
|
|
},
|
2026-03-22 14:56:27 +03:00
|
|
|
|
/// Paste clipboard entities translated so their centroid lands at `base_pt`; end command.
|
|
|
|
|
|
PasteClipboard { base_pt: Vec3 },
|
2026-04-02 23:28:00 +03:00
|
|
|
|
/// Zoom the model-space camera to fit the given corner points; end command.
|
|
|
|
|
|
ZoomToWindow { p1: Vec3, p2: Vec3 },
|
2026-04-02 23:36:53 +03:00
|
|
|
|
/// Print a measurement result to the command line and end the command.
|
|
|
|
|
|
Measurement(String),
|
2026-04-02 23:39:51 +03:00
|
|
|
|
/// Break `handle` at points `p1` and `p2`; replace with computed fragments.
|
|
|
|
|
|
BreakEntity { handle: Handle, p1: Vec3, p2: Vec3 },
|
2026-04-02 23:41:53 +03:00
|
|
|
|
/// Attempt to join the given entities into fewer merged entities.
|
|
|
|
|
|
JoinEntities(Vec<Handle>),
|
2026-04-03 09:19:36 +03:00
|
|
|
|
/// Apply a polyline-edit operation to one entity; keep command active.
|
|
|
|
|
|
PeditOp {
|
|
|
|
|
|
handle: Handle,
|
|
|
|
|
|
op: crate::modules::home::modify::pedit::PeditOp,
|
|
|
|
|
|
},
|
2026-04-02 23:43:33 +03:00
|
|
|
|
/// Place Point entities at N equal intervals along the entity.
|
|
|
|
|
|
DivideEntity { handle: Handle, n: usize },
|
|
|
|
|
|
/// Place Point entities at `segment_length` intervals along the entity.
|
|
|
|
|
|
MeasureEntity { handle: Handle, segment_length: f64 },
|
2026-04-02 23:45:06 +03:00
|
|
|
|
/// Extend/trim a Line or Arc by the given mode; end command.
|
|
|
|
|
|
LengthenEntity {
|
|
|
|
|
|
handle: Handle,
|
|
|
|
|
|
pick_pt: Vec3,
|
|
|
|
|
|
mode: crate::modules::home::modify::lengthen::LenMode,
|
|
|
|
|
|
},
|
2026-04-02 23:46:48 +03:00
|
|
|
|
/// Align selected entities: translate to dst1, rotate by angle_rad, optional scale.
|
|
|
|
|
|
AlignSelected {
|
|
|
|
|
|
handles: Vec<Handle>,
|
|
|
|
|
|
src1: Vec3,
|
|
|
|
|
|
dst1: Vec3,
|
|
|
|
|
|
angle_rad: f32,
|
|
|
|
|
|
scale: f32,
|
|
|
|
|
|
},
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
/// Set the plot window on the active layout's PlotSettings.
|
|
|
|
|
|
SetPlotWindow { p1: Vec3, p2: Vec3 },
|
2026-04-07 11:05:24 +03:00
|
|
|
|
/// Replace the text content of a Text/MText entity in-place.
|
|
|
|
|
|
DdeditEntity { handle: Handle, new_text: String },
|
2026-05-31 14:44:18 +03:00
|
|
|
|
/// 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 },
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
/// 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: Vec3,
|
|
|
|
|
|
handle: Option<Handle>,
|
|
|
|
|
|
initial: String,
|
|
|
|
|
|
height: f64,
|
|
|
|
|
|
},
|
2026-05-31 14:44:18 +03:00
|
|
|
|
/// 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: Vec3,
|
|
|
|
|
|
handle: Option<Handle>,
|
|
|
|
|
|
initial: String,
|
|
|
|
|
|
height: f64,
|
|
|
|
|
|
},
|
2026-04-07 11:08:51 +03:00
|
|
|
|
/// Apply new pattern/scale/angle to an existing hatch entity.
|
2026-05-12 10:36:15 +03:00
|
|
|
|
HatcheditApply {
|
|
|
|
|
|
handle: Handle,
|
|
|
|
|
|
name: String,
|
|
|
|
|
|
scale: f32,
|
|
|
|
|
|
angle: f32,
|
|
|
|
|
|
},
|
2026-04-07 11:25:39 +03:00
|
|
|
|
/// Stretch entities: move only vertices/endpoints inside the crossing window.
|
|
|
|
|
|
StretchEntities {
|
|
|
|
|
|
handles: Vec<Handle>,
|
|
|
|
|
|
/// Min corner of the crossing window in world XZ (= DXF XY).
|
|
|
|
|
|
win_min: Vec3,
|
|
|
|
|
|
/// Max corner of the crossing window in world XZ (= DXF XY).
|
|
|
|
|
|
win_max: Vec3,
|
|
|
|
|
|
/// Translation vector to apply to vertices inside the window.
|
|
|
|
|
|
delta: Vec3,
|
|
|
|
|
|
},
|
feat: BOX, SPHERE, CYLINDER primitives + EXTRUDE / REVOLVE commands
BOX, SPHERE, and CYLINDER build truck topology solids and tessellate
them into MeshModels stored alongside a Solid3D placeholder entity so
the GPU mesh pipeline renders them immediately.
EXTRUDE picks any closed 2D profile (Circle, LwPolyline, etc.),
attaches a planar face via try_attach_plane, and translational-sweeps
it (tsweep Face → Solid) along Z by the given height.
REVOLVE picks a profile, two axis points, and an angle (default 360°),
then rotational-sweeps the wire (rsweep) around the axis.
Three new CmdResult variants drive the operations:
CommitSolid3D, ExtrudeEntity, RevolveEntity.
Scene::layer_color() added to retrieve the active layer's RGBA color.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 12:55:28 +03:00
|
|
|
|
/// Create a Solid3D placeholder entity + associated MeshModel.
|
|
|
|
|
|
/// `mesh_fn` is called with the entity's handle string to build the mesh.
|
|
|
|
|
|
CommitSolid3D {
|
|
|
|
|
|
mesh_fn: Box<dyn FnOnce(String) -> Option<crate::scene::mesh_model::MeshModel> + Send>,
|
|
|
|
|
|
},
|
|
|
|
|
|
/// Extrude the profile entity `handle` by `height` along Z.
|
2026-05-12 10:36:15 +03:00
|
|
|
|
ExtrudeEntity {
|
|
|
|
|
|
handle: Handle,
|
|
|
|
|
|
height: f32,
|
|
|
|
|
|
color: [f32; 4],
|
|
|
|
|
|
},
|
feat: BOX, SPHERE, CYLINDER primitives + EXTRUDE / REVOLVE commands
BOX, SPHERE, and CYLINDER build truck topology solids and tessellate
them into MeshModels stored alongside a Solid3D placeholder entity so
the GPU mesh pipeline renders them immediately.
EXTRUDE picks any closed 2D profile (Circle, LwPolyline, etc.),
attaches a planar face via try_attach_plane, and translational-sweeps
it (tsweep Face → Solid) along Z by the given height.
REVOLVE picks a profile, two axis points, and an angle (default 360°),
then rotational-sweeps the wire (rsweep) around the axis.
Three new CmdResult variants drive the operations:
CommitSolid3D, ExtrudeEntity, RevolveEntity.
Scene::layer_color() added to retrieve the active layer's RGBA color.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 12:55:28 +03:00
|
|
|
|
/// Revolve the profile entity `handle` around the given axis by `angle_deg`.
|
|
|
|
|
|
RevolveEntity {
|
|
|
|
|
|
handle: Handle,
|
|
|
|
|
|
axis_start: glam::Vec3,
|
|
|
|
|
|
axis_end: glam::Vec3,
|
|
|
|
|
|
angle_deg: f32,
|
|
|
|
|
|
color: [f32; 4],
|
|
|
|
|
|
},
|
2026-04-08 18:07:00 +03:00
|
|
|
|
/// Sweep the profile entity `profile_handle` along `path_handle`.
|
|
|
|
|
|
SweepEntity {
|
|
|
|
|
|
profile_handle: Handle,
|
|
|
|
|
|
path_handle: Handle,
|
|
|
|
|
|
color: [f32; 4],
|
|
|
|
|
|
},
|
2026-04-08 18:08:53 +03:00
|
|
|
|
/// Loft through a series of profile entities.
|
|
|
|
|
|
LoftEntities {
|
|
|
|
|
|
handles: Vec<Handle>,
|
|
|
|
|
|
color: [f32; 4],
|
|
|
|
|
|
},
|
2026-04-08 00:33:56 +03:00
|
|
|
|
/// 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 },
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 13:41:54 +03:00
|
|
|
|
/// What kind of value the active command is currently asking for. Drives
|
|
|
|
|
|
/// the dynamic-input overlay so the tooltip shows the relevant quantity
|
|
|
|
|
|
/// (coordinates for a point pick, a single length for a radius/distance
|
|
|
|
|
|
/// prompt, degrees for an angle prompt).
|
|
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
|
|
|
|
|
pub enum DynField {
|
|
|
|
|
|
/// A position — X/Y coordinates (or distance+angle relative to the
|
|
|
|
|
|
/// last point). The default for every command step.
|
|
|
|
|
|
#[default]
|
|
|
|
|
|
Point,
|
|
|
|
|
|
/// A single linear distance (radius, length, offset) measured from
|
|
|
|
|
|
/// the last point.
|
|
|
|
|
|
Distance,
|
|
|
|
|
|
/// An angle, shown in degrees, measured from the last point.
|
|
|
|
|
|
Angle,
|
2026-06-09 18:32:03 +03:00
|
|
|
|
/// 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,
|
2026-05-27 13:41:54 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 17:12:08 +03:00
|
|
|
|
// ── 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,
|
2026-06-15 18:46:34 +03:00
|
|
|
|
/// Typed-only integer count. Reserved for upcoming command migrations.
|
|
|
|
|
|
#[allow(dead_code)]
|
2026-06-15 17:12:08 +03:00
|
|
|
|
Count,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 23:03:08 +03:00
|
|
|
|
/// 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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 17:12:08 +03:00
|
|
|
|
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,
|
2026-06-15 18:46:34 +03:00
|
|
|
|
/// 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,
|
2026-06-15 17:12:08 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Where a step's values are measured from.
|
|
|
|
|
|
#[derive(Clone, Copy, PartialEq, Debug)]
|
|
|
|
|
|
pub enum DynAnchor {
|
2026-06-15 18:46:34 +03:00
|
|
|
|
/// The previous committed point (`App::last_point`). Reserved — current
|
|
|
|
|
|
/// specs pass the anchor explicitly via `Point`.
|
|
|
|
|
|
#[allow(dead_code)]
|
2026-06-15 17:12:08 +03:00
|
|
|
|
LastPoint,
|
|
|
|
|
|
/// An explicit world point.
|
|
|
|
|
|
Point(Vec3),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// One value box in a [`DynSpec`].
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
|
pub struct DynFieldSpec {
|
|
|
|
|
|
pub role: DynRole,
|
|
|
|
|
|
/// Label override; `None` uses the role's default label.
|
|
|
|
|
|
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,
|
2026-06-15 18:46:34 +03:00
|
|
|
|
/// Far end of a reference line through `anchor` (only used by
|
|
|
|
|
|
/// [`DynGuide::Perp`]); `None` otherwise.
|
|
|
|
|
|
pub ref_point: Option<Vec3>,
|
2026-06-15 17:12:08 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
// ── Trait ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// An interactive CAD command that collects user input step-by-step.
|
|
|
|
|
|
pub trait CadCommand: Send {
|
|
|
|
|
|
/// Short name shown in the command line prompt, e.g. `"LINE"`.
|
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
|
fn name(&self) -> &'static str;
|
|
|
|
|
|
/// Current prompt string to display in the command line.
|
|
|
|
|
|
fn prompt(&self) -> String;
|
|
|
|
|
|
|
|
|
|
|
|
/// Called when the user left-clicks in the viewport (point pick).
|
|
|
|
|
|
fn on_point(&mut self, pt: Vec3) -> CmdResult;
|
|
|
|
|
|
|
|
|
|
|
|
/// Called when the user presses Enter (finalize / next option).
|
|
|
|
|
|
fn on_enter(&mut self) -> CmdResult;
|
|
|
|
|
|
|
|
|
|
|
|
/// Called when the user presses Escape (cancel).
|
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
|
fn on_escape(&mut self) -> CmdResult {
|
|
|
|
|
|
CmdResult::Cancel
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns `true` when the command needs entity picking (hit-test) instead of point picking.
|
|
|
|
|
|
fn needs_entity_pick(&self) -> bool {
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Called when the 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: Vec3) -> CmdResult {
|
|
|
|
|
|
CmdResult::Cancel
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 13:41:21 -04:00
|
|
|
|
/// 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: Vec3) -> 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: Vec3) -> 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 C3D-style object acquisition (e.g. "Inlet" under cursor).
|
|
|
|
|
|
fn set_acquisition_hint(&mut self, _hint: Option<&str>) {}
|
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
/// Called after `CmdResult::ReplaceEntity` is applied to the document.
|
|
|
|
|
|
/// `old` is the erased handle; `new_handles` are the handles assigned to the replacement entities.
|
|
|
|
|
|
/// Commands that stay active across replaces should update their internal snapshots here.
|
|
|
|
|
|
fn on_entity_replaced(&mut self, _old: Handle, _new_handles: &[Handle]) {}
|
|
|
|
|
|
|
|
|
|
|
|
/// Called on every mouse-move when `needs_entity_pick()` is true.
|
|
|
|
|
|
/// Return preview wires showing the operation result under the cursor.
|
|
|
|
|
|
/// Default: empty (no preview).
|
|
|
|
|
|
fn on_hover_entity(&mut self, _handle: Handle, _pt: Vec3) -> Vec<WireModel> {
|
|
|
|
|
|
vec![]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Called on every mouse-move in the viewport.
|
|
|
|
|
|
/// Return `Some(WireModel)` to update the rubber-band preview, `None` to skip.
|
|
|
|
|
|
fn on_mouse_move(&mut self, _pt: Vec3) -> 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: Vec3) -> Vec<WireModel> {
|
|
|
|
|
|
self.on_mouse_move(pt).into_iter().collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns `true` when the command is waiting for text typed in the command line.
|
|
|
|
|
|
fn wants_text_input(&self) -> bool {
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 17:12:08 +03:00
|
|
|
|
/// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 18:59:51 +03:00
|
|
|
|
/// Returns `true` when the active text prompt expects free-form prose
|
|
|
|
|
|
/// that can legitimately contain whitespace (the body of a TEXT /
|
|
|
|
|
|
/// MTEXT / DDEDIT entity, an attribute default value, etc.). For
|
|
|
|
|
|
/// these prompts the command-line input must let `Space` be typed as
|
|
|
|
|
|
/// a literal character; for every other prompt `Space` submits the
|
|
|
|
|
|
/// input the same way `Enter` does.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Default `false` — single-token prompts (option letters, numeric
|
|
|
|
|
|
/// radius, block name) do not embed spaces.
|
|
|
|
|
|
fn wants_text_with_spaces(&self) -> bool {
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
/// Called when the user submits text via the command line while `wants_text_input` is true.
|
|
|
|
|
|
fn on_text_input(&mut self, _text: &str) -> Option<CmdResult> {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns `true` when the command is in a selection-gathering phase.
|
|
|
|
|
|
/// While true, viewport clicks are routed through the normal selection
|
|
|
|
|
|
/// system (single / box / polygon) instead of the command's point-pick path.
|
|
|
|
|
|
/// After each completed selection action the host calls `on_selection_complete`.
|
|
|
|
|
|
fn is_selection_gathering(&self) -> bool {
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Called after a selection action completes while `is_selection_gathering` is true.
|
|
|
|
|
|
/// `handles` is the full set of currently selected entities.
|
|
|
|
|
|
/// Return `Relaunch` to fire the pending command, or `NeedPoint` to keep gathering.
|
|
|
|
|
|
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
|
|
|
|
|
|
CmdResult::Cancel
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns `true` when the command wants object picks via Tangent snap.
|
|
|
|
|
|
fn needs_tangent_pick(&self) -> bool {
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 00:02:38 +03:00
|
|
|
|
/// If this command is XATTACH, returns the file path to attach.
|
|
|
|
|
|
/// Default: None.
|
|
|
|
|
|
fn xattach_path(&self) -> Option<String> {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 00:19:43 +03:00
|
|
|
|
/// If this command needs attribute data injected (ATTEDIT), returns the
|
|
|
|
|
|
/// INSERT handle awaiting attr initialization; else None.
|
|
|
|
|
|
fn attedit_pending_handle(&self) -> Option<acadrust::Handle> {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Inject attribute (tag, value) pairs into the command after entity pick.
|
|
|
|
|
|
fn attedit_set_attrs(&mut self, _attrs: Vec<(String, String)>) {}
|
|
|
|
|
|
|
2026-04-08 00:33:56 +03:00
|
|
|
|
/// Inject attribute definitions (tag, prompt, default_value) for ATTREQ
|
|
|
|
|
|
/// attr-filling after INSERT point is picked.
|
|
|
|
|
|
fn attreq_set_attdefs(&mut self, _attdefs: Vec<(String, String, String)>) {}
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns the INSERT entity built so far (pending attr fill) if this is an
|
|
|
|
|
|
/// ATTREQ-aware INSERT command waiting for attdef injection.
|
|
|
|
|
|
/// Called by the host after `AttreqNeeded` to commit the completed Insert.
|
|
|
|
|
|
fn attreq_take_insert(&mut self) -> Option<acadrust::EntityType> {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
/// Called instead of `on_point` when the command needs a tangent pick
|
|
|
|
|
|
/// and the snap system found a tangent object.
|
|
|
|
|
|
fn on_tangent_point(&mut self, obj: TangentObject, hit: Vec3) -> CmdResult {
|
|
|
|
|
|
let _ = obj;
|
|
|
|
|
|
self.on_point(hit)
|
|
|
|
|
|
}
|
feat: add missing Annotate tab commands and icons
New dimension commands: QDIM, DIMEDIT, DIMTEDIT, DIMBREAK, DIMSPACE,
DIMJOGLINE with full command workflows and SVG icons.
New multileader commands: MLEADERADD, MLEADERREMOVE, MLEADERALIGN,
MLEADERCOLLECT with entity injection pattern for in-place editing.
Updated annotate ribbon: all tools exposed across Text, Dimensions,
Leaders, Tables, Markup, and Annotation Scaling groups. Added STYLE,
FIND, DIMSTYLE shortcut buttons.
Added `inject_picked_entity` to the CadCommand trait for commands that
need a cloned copy of the picked entity to modify (DIMTEDIT, MLEADERADD,
MLEADERREMOVE). Sentinel-based dispatch in cmd_result.rs handles
DIMBREAK, DIMSPACE, DIMJOGLINE, MLEADERALIGN, and MLEADERCOLLECT.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 03:26:47 +03:00
|
|
|
|
|
2026-06-09 13:41:21 -04:00
|
|
|
|
/// 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
|
feat: add missing Annotate tab commands and icons
New dimension commands: QDIM, DIMEDIT, DIMTEDIT, DIMBREAK, DIMSPACE,
DIMJOGLINE with full command workflows and SVG icons.
New multileader commands: MLEADERADD, MLEADERREMOVE, MLEADERALIGN,
MLEADERCOLLECT with entity injection pattern for in-place editing.
Updated annotate ribbon: all tools exposed across Text, Dimensions,
Leaders, Tables, Markup, and Annotation Scaling groups. Added STYLE,
FIND, DIMSTYLE shortcut buttons.
Added `inject_picked_entity` to the CadCommand trait for commands that
need a cloned copy of the picked entity to modify (DIMTEDIT, MLEADERADD,
MLEADERREMOVE). Sentinel-based dispatch in cmd_result.rs handles
DIMBREAK, DIMSPACE, DIMJOGLINE, MLEADERALIGN, and MLEADERCOLLECT.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 03:26:47 +03:00
|
|
|
|
/// that need to read/modify it (e.g. DIMTEDIT, MLEADERADD, MLEADERREMOVE).
|
|
|
|
|
|
/// Default: no-op.
|
|
|
|
|
|
fn inject_picked_entity(&mut self, _entity: acadrust::EntityType) {}
|
2026-05-27 13:41:54 +03:00
|
|
|
|
|
|
|
|
|
|
/// 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
|
|
|
|
|
|
}
|
2026-06-09 18:32:03 +03:00
|
|
|
|
|
2026-06-15 22:03:04 +03:00
|
|
|
|
/// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 17:12:08 +03:00
|
|
|
|
/// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 18:32:03 +03:00
|
|
|
|
/// 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: Vec3) -> Option<f64> {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
2026-05-26 22:45:54 +03:00
|
|
|
|
|
|
|
|
|
|
// ── Autocomplete registry ─────────────────────────────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// Every `impl CadCommand for Foo` module submits the names it answers to
|
|
|
|
|
|
// at compile time via `inventory::submit!`. The command-line autocomplete
|
|
|
|
|
|
// then iterates the resulting collection at runtime — no central list to
|
|
|
|
|
|
// keep in sync.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Non-interactive one-shot dispatch arms (NEW, OPEN, SAVE, …) live in
|
|
|
|
|
|
// `app/commands.rs` and don't have a `CadCommand` impl; they're absent
|
|
|
|
|
|
// from autocomplete by design. Add an explicit `inventory::submit!` next
|
|
|
|
|
|
// to their dispatch arm if you want them surfaced.
|
|
|
|
|
|
|
|
|
|
|
|
pub struct CommandRegistration {
|
|
|
|
|
|
pub names: &'static [&'static str],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
inventory::collect!(CommandRegistration);
|
|
|
|
|
|
|
|
|
|
|
|
/// All registered command names, including aliases.
|
|
|
|
|
|
pub fn all_registered_command_names() -> Vec<&'static str> {
|
|
|
|
|
|
inventory::iter::<CommandRegistration>
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.flat_map(|r| r.names.iter().copied())
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
}
|