feat: ATTEDIT — edit block attribute values interactively
- attedit.rs: AtteditCommand picks an INSERT entity then iterates through
each attribute tag prompting for a new value (Enter keeps existing)
- attedit_pending_handle() / attedit_set_attrs() trait methods inject
attribute data into the command after entity-pick (avoids document access
in the command itself)
- cmd_result.rs: NeedPoint handler injects attr data; ReplaceEntity detects
the __ATTEDIT__ sentinel and calls apply_attedit() on the document
- apply_attedit() encodes tag/value pairs as __ATTEDIT__tag\x01val\x02...
- ROADMAP.md: ATTEDIT marked ✅
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0fc3766da7
commit
6f741a92e0
6 changed files with 229 additions and 1 deletions
|
|
@ -127,7 +127,7 @@ Underlay (PDF/DWF/DGN)
|
||||||
| STRETCH (SS) | ✅ | — |
|
| STRETCH (SS) | ✅ | — |
|
||||||
| SPLINEDIT | ✅ CLOSE/OPEN/REVERSE + grip editing | — |
|
| SPLINEDIT | ✅ CLOSE/OPEN/REVERSE + grip editing | — |
|
||||||
| HATCHEDIT | ✅ | — |
|
| HATCHEDIT | ✅ | — |
|
||||||
| ATTEDIT | ⬜ | Attribute değerlerini düzenleme |
|
| ATTEDIT | ✅ Interactive tag-by-tag value editing | — |
|
||||||
| DDEDIT (çift tık metin) | ✅ | — |
|
| DDEDIT (çift tık metin) | ✅ | — |
|
||||||
| REFEDIT | ⬜ | Block in-place düzenleme |
|
| REFEDIT | ⬜ | Block in-place düzenleme |
|
||||||
| DIVIDE (DIV) | ✅ | — |
|
| DIVIDE (DIV) | ✅ | — |
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,36 @@ impl H7CAD {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
match result {
|
match result {
|
||||||
CmdResult::NeedPoint => {
|
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());
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||||
if let Some(p) = prompt {
|
if let Some(p) = prompt {
|
||||||
self.command_line.push_info(&p);
|
self.command_line.push_info(&p);
|
||||||
|
|
@ -170,6 +200,27 @@ impl H7CAD {
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
}
|
}
|
||||||
CmdResult::ReplaceEntity(handle, new_entities) => {
|
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.
|
// Detect SPLINEDIT sentinel: a single XLine with a magic layer name.
|
||||||
if new_entities.len() == 1 {
|
if new_entities.len() == 1 {
|
||||||
if let acadrust::EntityType::XLine(ref xl) = new_entities[0] {
|
if let acadrust::EntityType::XLine(ref xl) = new_entities[0] {
|
||||||
|
|
|
||||||
|
|
@ -1442,6 +1442,13 @@ impl H7CAD {
|
||||||
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
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" => {
|
"ALIGN"|"AL" => {
|
||||||
use crate::modules::home::modify::align::AlignCommand;
|
use crate::modules::home::modify::align::AlignCommand;
|
||||||
let cmd = AlignCommand::new();
|
let cmd = AlignCommand::new();
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,16 @@ pub trait CadCommand: Send {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If this command needs attribute data injected (ATTEDIT), returns the
|
||||||
|
/// INSERT handle awaiting attr initialization; else None.
|
||||||
|
fn attedit_pending_handle(&self) -> Option<acadrust::Handle> {
|
||||||
|
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
|
/// Called instead of `on_point` when the command needs a tangent pick
|
||||||
/// and the snap system found a tangent object.
|
/// and the snap system found a tangent object.
|
||||||
fn on_tangent_point(&mut self, obj: TangentObject, hit: Vec3) -> CmdResult {
|
fn on_tangent_point(&mut self, obj: TangentObject, hit: Vec3) -> CmdResult {
|
||||||
|
|
|
||||||
159
src/modules/home/modify/attedit.rs
Normal file
159
src/modules/home/modify/attedit.rs
Normal file
|
|
@ -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 = <value>" 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<CmdResult> {
|
||||||
|
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<WireModel> { vec![] }
|
||||||
|
|
||||||
|
fn attedit_pending_handle(&self) -> Option<acadrust::Handle> {
|
||||||
|
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::<Vec<_>>()
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod align;
|
pub mod align;
|
||||||
|
pub mod attedit;
|
||||||
pub mod array;
|
pub mod array;
|
||||||
pub mod break_cmd;
|
pub mod break_cmd;
|
||||||
pub mod copy;
|
pub mod copy;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue