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 + } + } }