From 8078c4c9da2a1272b6c2eca8edb9f0bc34338b96 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 7 Apr 2026 11:05:24 +0300 Subject: [PATCH] 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 --- ROADMAP.md | 2 +- src/app/cmd_result.rs | 23 ++++++++ src/app/commands.rs | 22 ++++++++ src/app/update.rs | 43 +++++++++++++++ src/command/mod.rs | 2 + src/modules/annotate/ddedit.rs | 95 ++++++++++++++++++++++++++++++++++ src/modules/annotate/mod.rs | 1 + 7 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 src/modules/annotate/ddedit.rs diff --git a/ROADMAP.md b/ROADMAP.md index 74f599ae9..726f8b735 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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) | ✅ | — | diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 2a6688c4b..7f19548ee 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -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() { diff --git a/src/app/commands.rs b/src/app/commands.rs index 58d007a83..cea083416 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -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(); diff --git a/src/app/update.rs b/src/app/update.rs index cc05c1a8a..ca78bbc18 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -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 diff --git a/src/command/mod.rs b/src/command/mod.rs index 499e689a5..5a47abb9c 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -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 ───────────────────────────────────────────────────────────────── diff --git a/src/modules/annotate/ddedit.rs b/src/modules/annotate/ddedit.rs new file mode 100644 index 000000000..0989f1594 --- /dev/null +++ b/src/modules/annotate/ddedit.rs @@ -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 { + 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 { + 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 { + 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, + } +} diff --git a/src/modules/annotate/mod.rs b/src/modules/annotate/mod.rs index 354b77e1e..b1b8c9410 100644 --- a/src/modules/annotate/mod.rs +++ b/src/modules/annotate/mod.rs @@ -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;