forked from 1b-agent/opencadstudio-upstream
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:
1 parent
25ec6f66e5
commit
8078c4c9da
7 files changed
+187
-1
No files matched your search
+1
-1
@@ -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) | ✅ | — |
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user