diff --git a/ROADMAP.md b/ROADMAP.md index e493c9ca0..b46e1a505 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -127,7 +127,7 @@ Underlay (PDF/DWF/DGN) | STRETCH (SS) | ✅ | — | | SPLINEDIT | ✅ CLOSE/OPEN/REVERSE + grip editing | — | | HATCHEDIT | ✅ | — | -| ATTEDIT | ⬜ | Attribute değerlerini düzenleme | +| ATTEDIT | ✅ Interactive tag-by-tag value editing | — | | DDEDIT (çift tık metin) | ✅ | — | | REFEDIT | ⬜ | Block in-place düzenleme | | DIVIDE (DIV) | ✅ | — | diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index da64fcdc6..7735acb71 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -8,6 +8,36 @@ impl H7CAD { let i = self.active_tab; match result { CmdResult::NeedPoint => { + // If ATTEDIT just completed entity pick, inject attribute data. + let attedit_handle = self.tabs[i] + .active_cmd + .as_ref() + .and_then(|c| c.attedit_pending_handle()); + if let Some(ins_handle) = attedit_handle { + if let Some(acadrust::EntityType::Insert(ins)) = + self.tabs[i].scene.document.get_entity(ins_handle) + { + let attrs: Vec<(String, String)> = ins + .attributes + .iter() + .map(|a| (a.tag.clone(), a.get_value().to_string())) + .collect(); + if attrs.is_empty() { + self.command_line + .push_error("ATTEDIT This INSERT has no attributes."); + self.tabs[i].active_cmd = None; + return Task::none(); + } + if let Some(cmd) = &mut self.tabs[i].active_cmd { + cmd.attedit_set_attrs(attrs); + } + } else { + self.command_line + .push_error("ATTEDIT Please select an INSERT entity with attributes."); + self.tabs[i].active_cmd = None; + return Task::none(); + } + } let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { self.command_line.push_info(&p); @@ -170,6 +200,27 @@ impl H7CAD { self.refresh_properties(); } CmdResult::ReplaceEntity(handle, new_entities) => { + // Detect ATTEDIT sentinel. + if new_entities.len() == 1 { + if let acadrust::EntityType::XLine(ref xl) = new_entities[0] { + let layer = xl.common.layer.clone(); + if let Some(encoded) = layer.strip_prefix("__ATTEDIT__") { + let label = self.history_label_from_active_cmd(i, "ATTEDIT"); + self.push_undo_snapshot(i, label); + crate::modules::home::modify::attedit::apply_attedit( + &mut self.tabs[i].scene.document, + handle, + encoded, + ); + self.tabs[i].dirty = true; + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.command_line.push_output("ATTEDIT Attribute values updated."); + return Task::none(); + } + } + } + // Detect SPLINEDIT sentinel: a single XLine with a magic layer name. if new_entities.len() == 1 { if let acadrust::EntityType::XLine(ref xl) = new_entities[0] { diff --git a/src/app/commands.rs b/src/app/commands.rs index 57cecc3c9..a1cda167e 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -1442,6 +1442,13 @@ impl H7CAD { self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); } + "ATTEDIT"|"ATE"|"-ATTEDIT" => { + use crate::modules::home::modify::attedit::AtteditCommand; + let cmd_obj = AtteditCommand::new(); + self.command_line.push_info(&cmd_obj.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); + } + "ALIGN"|"AL" => { use crate::modules::home::modify::align::AlignCommand; let cmd = AlignCommand::new(); diff --git a/src/command/mod.rs b/src/command/mod.rs index 7bfb20c89..698281763 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -233,6 +233,16 @@ pub trait CadCommand: Send { None } + /// If this command needs attribute data injected (ATTEDIT), returns the + /// INSERT handle awaiting attr initialization; else None. + fn attedit_pending_handle(&self) -> Option { + None + } + + /// Inject attribute (tag, value) pairs into the command after entity pick. + fn attedit_set_attrs(&mut self, _attrs: Vec<(String, String)>) {} + + /// 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 { diff --git a/src/modules/home/modify/attedit.rs b/src/modules/home/modify/attedit.rs new file mode 100644 index 000000000..baff83d44 --- /dev/null +++ b/src/modules/home/modify/attedit.rs @@ -0,0 +1,159 @@ +// ATTEDIT command — edit attribute values of a selected INSERT entity. +// +// Workflow: +// Step 1: pick an INSERT entity that has attributes. +// Step 2+: for each attribute tag, show "TAG = " and accept new +// text via text input. Enter with empty string keeps the old value. +// After all attributes are processed, commit via ReplaceMany. + +use acadrust::EntityType; +use glam::Vec3; + +use crate::command::{CadCommand, CmdResult}; +use crate::scene::wire_model::WireModel; + +pub struct AtteditCommand { + step: Step, +} + +enum Step { + /// Waiting for the user to pick an INSERT entity. + SelectInsert, + /// Editing the attribute at index `idx` of the collected insert data. + EditAttr { + handle: acadrust::Handle, + /// (tag, current_value) pairs collected from the insert. + attrs: Vec<(String, String)>, + /// Index of the attribute currently being edited. + idx: usize, + }, +} + +impl AtteditCommand { + pub fn new() -> Self { + Self { step: Step::SelectInsert } + } +} + +impl CadCommand for AtteditCommand { + fn name(&self) -> &'static str { "ATTEDIT" } + + fn prompt(&self) -> String { + match &self.step { + Step::SelectInsert => "ATTEDIT Select block with attributes:".to_string(), + Step::EditAttr { attrs, idx, .. } => { + let (tag, val) = &attrs[*idx]; + format!("ATTEDIT {} = <{}> (Enter to keep, type new value):", tag, val) + } + } + } + + fn needs_entity_pick(&self) -> bool { + matches!(self.step, Step::SelectInsert) + } + + fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: Vec3) -> CmdResult { + if handle.is_null() { + return CmdResult::NeedPoint; + } + // We can't inspect the document here — store the handle and let the + // command host inject the attribute list via `init_with_attrs`. + // Instead, signal the host to call prepare_attedit(). + self.step = Step::EditAttr { + handle, + attrs: vec![], // will be filled by init_with_attrs() in cmd_result.rs + idx: 0, + }; + CmdResult::NeedPoint + } + + fn wants_text_input(&self) -> bool { + if let Step::EditAttr { ref attrs, idx, .. } = self.step { + !attrs.is_empty() && idx < attrs.len() + } else { + false + } + } + + fn on_text_input(&mut self, text: &str) -> Option { + let Step::EditAttr { handle, attrs, idx } = &mut self.step else { return None; }; + let handle = *handle; + + // Update the current attribute value if the user typed something. + if !text.trim().is_empty() { + attrs[*idx].1 = text.trim().to_string(); + } + *idx += 1; + + if *idx >= attrs.len() { + // All attributes done — build new Insert entity. + // We signal via a special CmdResult; the host will call apply_attedit(). + let pairs = attrs.clone(); + return Some(CmdResult::ReplaceEntity( + handle, + vec![make_attedit_sentinel(handle, &pairs)], + )); + } + + // More attributes to edit. + None + } + + fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint } + fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + fn on_preview_wires(&mut self, _pt: Vec3) -> Vec { vec![] } + + fn attedit_pending_handle(&self) -> Option { + if let Step::EditAttr { handle, attrs, .. } = &self.step { + if attrs.is_empty() { + return Some(*handle); + } + } + None + } + + fn attedit_set_attrs(&mut self, new_attrs: Vec<(String, String)>) { + if let Step::EditAttr { attrs, idx, .. } = &mut self.step { + *attrs = new_attrs; + *idx = 0; + } + } +} + +/// Make a sentinel entity carrying the edited attribute values. +/// Encodes all (tag=value) pairs in the layer field as "tag1\x01val1\x02tag2\x01val2...". +fn make_attedit_sentinel( + _handle: acadrust::Handle, + pairs: &[(String, String)], +) -> EntityType { + let encoded: String = pairs + .iter() + .map(|(t, v)| format!("{}\x01{}", t, v)) + .collect::>() + .join("\x02"); + let mut xl = acadrust::entities::XLine::new( + acadrust::types::Vector3::zero(), + acadrust::types::Vector3::new(1.0, 0.0, 0.0), + ); + xl.common.layer = format!("__ATTEDIT__{}", encoded); + EntityType::XLine(xl) +} + +/// Apply edited attribute values to an INSERT entity in the document. +/// Called from `cmd_result.rs` when the sentinel is detected. +pub fn apply_attedit( + doc: &mut acadrust::CadDocument, + handle: acadrust::Handle, + encoded: &str, +) { + let Some(EntityType::Insert(ins)) = doc.get_entity_mut(handle) else { return; }; + for pair in encoded.split('\x02') { + let mut parts = pair.splitn(2, '\x01'); + let Some(tag) = parts.next() else { continue; }; + let Some(val) = parts.next() else { continue; }; + if let Some(attrib) = ins.attributes.iter_mut().find(|a| a.tag == tag) { + attrib.set_value(val); + } + } +} + diff --git a/src/modules/home/modify/mod.rs b/src/modules/home/modify/mod.rs index f3cdd0c23..fe0ef897e 100644 --- a/src/modules/home/modify/mod.rs +++ b/src/modules/home/modify/mod.rs @@ -1,4 +1,5 @@ pub mod align; +pub mod attedit; pub mod array; pub mod break_cmd; pub mod copy;