feat: DDEDIT command and double-click text editing

Add DDEDIT (ED alias) command that edits Text/MText/Attribute entities
in-place via the command line. Double-clicking a text entity in model space
now auto-launches DDEDIT for that entity. Supports Text, MText,
AttributeDefinition, and AttributeEntity types.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Sonnet 4.6 committed 2026-04-07 11:05:24 +03:00
1 parent 25ec6f66e5
commit 8078c4c9da
7 files changed
+187 -1

No files matched your search

+1 -1
View File
@@ -128,7 +128,7 @@ Underlay (PDF/DWF/DGN)
| SPLINEDIT | ⬜ | — |
| HATCHEDIT | ⬜ | Var olan hatch'i düzenleme |
| ATTEDIT | ⬜ | Attribute değerlerini düzenleme |
| DDEDIT (çift tık metin) | ⬜ | In-place metin düzenleme |
| DDEDIT (çift tık metin) | ✅ | — |
| REFEDIT | ⬜ | Block in-place düzenleme |
| DIVIDE (DIV) | ✅ | — |
| MEASURE (ME) | ✅ | — |
+23
View File
@@ -619,6 +619,29 @@ impl H7CAD {
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
}
CmdResult::DdeditEntity { handle, new_text } => {
let mut updated = false;
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
match entity {
acadrust::EntityType::Text(t) => { t.value = new_text; updated = true; }
acadrust::EntityType::MText(t) => { t.value = new_text; updated = true; }
acadrust::EntityType::AttributeDefinition(a) => { a.default_value = new_text; updated = true; }
acadrust::EntityType::AttributeEntity(a) => { a.set_value(new_text); updated = true; }
_ => {}
}
}
if updated {
self.push_undo_snapshot(i, "DDEDIT");
self.tabs[i].dirty = true;
self.command_line.push_output("DDEDIT: text updated.");
} else {
self.command_line.push_error("DDEDIT: entity type not supported.");
}
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
}
}
// Focus the command-line input while a command is active; blur it when the command ends.
if self.tabs[i].active_cmd.is_some() {
+22
View File
@@ -906,6 +906,28 @@ impl H7CAD {
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"DDEDIT"|"ED" => {
use crate::modules::annotate::ddedit::{DdeditCommand, entity_text};
// If a single text/mtext entity is already selected, skip the pick step.
let sel = self.tabs[i].scene.selected_entities();
if sel.len() == 1 {
let (h, _) = sel[0];
if let Some(e) = self.tabs[i].scene.document.get_entity(h) {
if let Some(cur) = entity_text(e) {
let cmd = DdeditCommand::with_handle(h, cur.clone());
self.command_line.push_info(&format!("DDEDIT Enter new text <{cur}>:"));
self.tabs[i].active_cmd = Some(Box::new(cmd));
} else {
self.command_line.push_error("DDEDIT: selected entity is not text.");
}
}
} else {
let cmd = DdeditCommand::new();
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
}
"MTEXT"|"MT" => {
use crate::modules::annotate::mtext::MTextCommand;
let new_cmd = MTextCommand::new();
+43
View File
@@ -1215,6 +1215,49 @@ impl H7CAD {
}
}
// ── Double-click in Model Space: DDEDIT for Text/MText ────
if is_click
&& is_down
&& self.tabs[i].active_cmd.is_none()
&& self.tabs[i].scene.current_layout == "Model"
{
let now = Instant::now();
let is_double_model = self
.last_vp_click_time
.map(|t| {
let dt = now.duration_since(t).as_millis();
let last = self.last_vp_click_pos.unwrap_or(p);
let d = (p.x - last.x).hypot(p.y - last.y);
dt < 400 && d < 8.0
})
.unwrap_or(false);
self.last_vp_click_time = Some(now);
self.last_vp_click_pos = Some(p);
if is_double_model {
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
let all_wires = self.tabs[i].scene.hit_test_wires();
let hit = scene::hit_test::click_hit(p, &all_wires, vp_mat, bounds)
.and_then(|s| Scene::handle_from_wire_name(s));
if let Some(handle) = hit {
if let Some(entity) = self.tabs[i].scene.document.get_entity(handle) {
use crate::modules::annotate::ddedit::{DdeditCommand, entity_text};
if let Some(cur) = entity_text(entity) {
let cmd = DdeditCommand::with_handle(handle, cur.clone());
self.command_line.push_info(
&format!("DDEDIT Enter new text <{cur}>:")
);
self.tabs[i].active_cmd = Some(Box::new(cmd));
return self.focus_cmd_input();
}
}
}
}
}
// ── Double-click: enter/exit MSPACE ───────────────────────
// Only when no command is running, no drag, and we're in paper space.
if is_click
+2
View File
@@ -124,6 +124,8 @@ pub enum CmdResult {
},
/// Set the plot window on the active layout's PlotSettings.
SetPlotWindow { p1: Vec3, p2: Vec3 },
/// Replace the text content of a Text/MText entity in-place.
DdeditEntity { handle: Handle, new_text: String },
}
// ── Trait ─────────────────────────────────────────────────────────────────
+95
View File
@@ -0,0 +1,95 @@
// DDEDIT — edit text content of a Text or MText entity in-place.
//
// Workflow:
// 1. Pick a Text or MText entity (or fire from double-click with handle pre-set).
// 2. Enter new text. Press Enter to commit, Escape to cancel.
use acadrust::{EntityType, Handle};
use glam::Vec3;
use crate::command::{CadCommand, CmdResult};
enum DdeditStep {
PickEntity,
EnterText { handle: Handle, current: String },
}
pub struct DdeditCommand {
step: DdeditStep,
}
impl DdeditCommand {
pub fn new() -> Self {
Self { step: DdeditStep::PickEntity }
}
/// Start with a pre-picked entity (for double-click use).
pub fn with_handle(handle: Handle, current: String) -> Self {
Self { step: DdeditStep::EnterText { handle, current } }
}
}
impl CadCommand for DdeditCommand {
fn name(&self) -> &'static str { "DDEDIT" }
fn prompt(&self) -> String {
match &self.step {
DdeditStep::PickEntity => "DDEDIT Select text entity:".into(),
DdeditStep::EnterText { current, .. } => {
format!("DDEDIT Enter new text <{current}>:")
}
}
}
fn needs_entity_pick(&self) -> bool {
matches!(self.step, DdeditStep::PickEntity)
}
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
if handle.is_null() { return CmdResult::NeedPoint; }
// The current value will be filled in by the caller (commands.rs dispatch)
// via on_text_input once the entity is known. Store handle here.
self.step = DdeditStep::EnterText { handle, current: String::new() };
CmdResult::NeedPoint
}
fn wants_text_input(&self) -> bool {
matches!(self.step, DdeditStep::EnterText { .. })
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let (handle, current) = match &self.step {
DdeditStep::EnterText { handle, current } => (*handle, current.clone()),
_ => return None,
};
// Empty input → keep existing text
let new_text = if text.trim().is_empty() { current } else { text.to_string() };
Some(CmdResult::DdeditEntity { handle, new_text })
}
fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint }
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel }
}
/// Extract the text content from a Text or MText entity.
pub fn entity_text(entity: &EntityType) -> Option<String> {
match entity {
EntityType::Text(t) => Some(t.value.clone()),
EntityType::MText(t) => Some(t.value.clone()),
EntityType::AttributeDefinition(a) => Some(a.default_value.clone()),
EntityType::AttributeEntity(a) => Some(a.get_value().to_string()),
_ => None,
}
}
/// Apply new text content to a cloned entity, returning the updated entity.
pub fn set_entity_text(mut entity: EntityType, new_text: &str) -> Option<EntityType> {
match &mut entity {
EntityType::Text(t) => { t.value = new_text.to_string(); Some(entity) }
EntityType::MText(t) => { t.value = new_text.to_string(); Some(entity) }
EntityType::AttributeDefinition(a) => { a.default_value = new_text.to_string(); Some(entity) }
EntityType::AttributeEntity(a) => { a.set_value(new_text.to_string()); Some(entity) }
_ => None,
}
}
+1
View File
@@ -2,6 +2,7 @@
pub mod aligned_dim;
pub mod angular_dim;
pub mod ddedit;
pub mod diameter_dim;
pub mod dim_baseline;
pub mod dim_continue;