From a20558a0247ac8489b753d5366827631225dbf54 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 8 Apr 2026 00:33:56 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20ATTREQ=20=E2=80=94=20attribute=20prompt?= =?UTF-8?q?ing=20during=20INSERT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When inserting a block that contains AttributeDefinition entities, the INSERT command now enters an attribute-filling phase: each attdef is prompted tag-by-tag (showing its prompt text and default value). Pressing Enter accepts the default. The completed AttributeEntity list is embedded in the Insert before committing. Blocks without attdefs continue to insert immediately as before. Co-Authored-By: Claude Sonnet 4.6 --- ROADMAP.md | 4 +- src/app/cmd_result.rs | 44 +++++++++++ src/command/mod.rs | 15 ++++ src/modules/insert/insert_block.rs | 122 +++++++++++++++++++++++++---- 4 files changed, 168 insertions(+), 17 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 7f9ca4d16..4722587ac 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -154,7 +154,7 @@ Underlay (PDF/DWF/DGN) | MTEXT (T) | ✅ | | DIMSTYLE yöneticisi (DIMSTYLE/DDIM) | ✅ | | MLEADERSTYLE | ⬜ | -| DIMORDINATE | ⬜ | +| DIMORDINATE | ✅ | --- @@ -227,7 +227,7 @@ Underlay (PDF/DWF/DGN) | OBJ dosyası içe aktarma | ✅ | | REFEDIT (block yerinde düzenleme) | ⬜ | | WBLOCK (bloğu dış dosyaya yaz) | ✅ | -| Attributeli INSERT akışı (ATTREQ) | ⬜ | +| Attributeli INSERT akışı (ATTREQ) | ✅ | --- diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 7735acb71..ee0d5b08b 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -256,6 +256,50 @@ impl H7CAD { self.command_line.push_info(&p); } } + CmdResult::AttreqNeeded { block_name } => { + // Collect AttributeDefinitions owned by this block record. + let attdefs: Vec<(String, String, String)> = { + let doc = &self.tabs[i].scene.document; + if let Some(br) = doc.block_records.get(&block_name) { + br.entity_handles.iter().filter_map(|&h| { + if let Some(acadrust::EntityType::AttributeDefinition(ad)) = + doc.get_entity(h) + { + Some((ad.tag.clone(), ad.prompt.clone(), ad.default_value.clone())) + } else { + None + } + }).collect() + } else { + vec![] + } + }; + + if attdefs.is_empty() { + // No attribute definitions — commit the INSERT directly. + let entity = self.tabs[i].active_cmd.as_mut() + .and_then(|c| c.attreq_take_insert()); + if let Some(entity) = entity { + let label = self.history_label_from_active_cmd(i, "INSERT"); + self.push_undo_snapshot(i, label); + self.commit_entity(entity); + self.tabs[i].dirty = true; + self.tabs[i].scene.clear_preview_wire(); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.restore_pre_cmd_tangent(); + } + } else { + // Inject attdefs so the command enters attr-filling mode. + if let Some(cmd) = &mut self.tabs[i].active_cmd { + cmd.attreq_set_attdefs(attdefs); + } + let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); + if let Some(p) = prompt { + self.command_line.push_info(&p); + } + } + } CmdResult::Cancel => { self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; diff --git a/src/command/mod.rs b/src/command/mod.rs index 698281763..77b953c8b 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -138,6 +138,10 @@ pub enum CmdResult { /// Translation vector to apply to vertices inside the window. delta: Vec3, }, + /// 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 }, } // ── Trait ───────────────────────────────────────────────────────────────── @@ -242,6 +246,17 @@ pub trait CadCommand: Send { /// Inject attribute (tag, value) pairs into the command after entity pick. fn attedit_set_attrs(&mut self, _attrs: Vec<(String, String)>) {} + /// 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 { + None + } + /// Called instead of `on_point` when the command needs a tangent pick /// and the snap system found a tangent object. diff --git a/src/modules/insert/insert_block.rs b/src/modules/insert/insert_block.rs index c85b6bf7f..5df6788a2 100644 --- a/src/modules/insert/insert_block.rs +++ b/src/modules/insert/insert_block.rs @@ -1,4 +1,4 @@ -use acadrust::entities::Insert; +use acadrust::entities::{AttributeEntity, Insert}; use acadrust::types::Vector3; use acadrust::EntityType; use glam::Vec3; @@ -19,11 +19,22 @@ pub fn tool() -> ToolDef { enum Step { Name, Point { name: String }, + /// Attribute filling: prompt the user tag by tag. + FillAttr { + /// Attribute definitions: (tag, prompt_text, default_value). + attdefs: Vec<(String, String, String)>, + /// Index of the attdef currently being prompted. + idx: usize, + /// (tag, value) pairs collected so far. + values: Vec<(String, String)>, + }, } pub struct InsertBlockCommand { available: Vec, step: Step, + /// Pending Insert entity stored while attr-filling is in progress. + pending_insert: Option, } impl InsertBlockCommand { @@ -31,6 +42,7 @@ impl InsertBlockCommand { Self { available, step: Step::Name, + pending_insert: None, } } } @@ -51,6 +63,19 @@ impl CadCommand for InsertBlockCommand { format!("INSERT Enter block name:{hint}") } Step::Point { name } => format!("INSERT Specify insertion point for \"{}\":", name), + Step::FillAttr { attdefs, idx, .. } => { + if let Some((tag, prompt, default)) = attdefs.get(*idx) { + let default_hint = if default.is_empty() { + String::new() + } else { + format!(" <{default}>") + }; + let prompt_text = if prompt.is_empty() { tag.as_str() } else { prompt.as_str() }; + format!("INSERT {prompt_text}{default_hint}:") + } else { + "INSERT Filling attributes...".into() + } + } } } @@ -58,37 +83,104 @@ impl CadCommand for InsertBlockCommand { match &self.step { Step::Name => CmdResult::NeedPoint, Step::Point { name } => { - CmdResult::CommitAndExit(EntityType::Insert(Insert::new( + let ins = Insert::new( name.clone(), Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64), - ))) + ); + let block_name = name.clone(); + self.pending_insert = Some(ins); + // Signal the host to check for attdefs. + CmdResult::AttreqNeeded { block_name } } + Step::FillAttr { .. } => CmdResult::NeedPoint, } } fn on_enter(&mut self) -> CmdResult { - CmdResult::Cancel + match &self.step { + Step::Name | Step::Point { .. } => CmdResult::Cancel, + Step::FillAttr { .. } => { + // Treat Enter as accepting the default. + self.accept_attr_value("") + } + } } fn wants_text_input(&self) -> bool { - matches!(self.step, Step::Name) + matches!(self.step, Step::Name | Step::FillAttr { .. }) } fn on_text_input(&mut self, text: &str) -> Option { - if !matches!(self.step, Step::Name) { - return None; + match &self.step { + Step::Name => { + let name = text.trim(); + if !self.available.iter().any(|c| c.eq_ignore_ascii_case(name)) { + return None; + } + self.step = Step::Point { + name: name.to_string(), + }; + Some(CmdResult::NeedPoint) + } + Step::FillAttr { .. } => Some(self.accept_attr_value(text)), + Step::Point { .. } => None, } - let name = text.trim(); - if !self.available.iter().any(|candidate| candidate.eq_ignore_ascii_case(name)) { - return None; - } - self.step = Step::Point { - name: name.to_string(), - }; - Some(CmdResult::NeedPoint) } fn on_preview_wires(&mut self, _pt: Vec3) -> Vec { vec![] } + + fn attreq_set_attdefs(&mut self, attdefs: Vec<(String, String, String)>) { + self.step = Step::FillAttr { attdefs, idx: 0, values: vec![] }; + } + + fn attreq_take_insert(&mut self) -> Option { + self.pending_insert.take().map(|ins| EntityType::Insert(ins)) + } +} + +impl InsertBlockCommand { + /// Accept the current attribute value (empty = use default) and advance. + /// Returns CommitAndExit when all attdefs have been filled. + fn accept_attr_value(&mut self, text: &str) -> CmdResult { + let (tag, default, next_idx, total) = match &self.step { + Step::FillAttr { attdefs, idx, .. } => { + let (tag, _, default) = &attdefs[*idx]; + (tag.clone(), default.clone(), idx + 1, attdefs.len()) + } + _ => return CmdResult::Cancel, + }; + + let value = if text.trim().is_empty() { default } else { text.trim().to_string() }; + + if let Step::FillAttr { ref mut values, ref mut idx, .. } = self.step { + values.push((tag, value)); + *idx = next_idx; + } + + if next_idx >= total { + // All attdefs filled — build the INSERT with AttributeEntity list. + let values = match &self.step { + Step::FillAttr { values, .. } => values.clone(), + _ => vec![], + }; + let mut ins = match self.pending_insert.take() { + Some(i) => i, + None => return CmdResult::Cancel, + }; + for (tag, value) in values { + let mut attr = AttributeEntity { + tag, + value: value.clone(), + ..Default::default() + }; + attr.set_value(&value); + ins.attributes.push(attr); + } + CmdResult::CommitAndExit(EntityType::Insert(ins)) + } else { + CmdResult::NeedPoint + } + } }