forked from 1b-agent/opencadstudio-upstream
feat(attribute): route ATTMAN/BATTMAN to the attribute editor (ATTEDIT)
ATTMAN and BATTMAN previously produced a command-line listing of block attributes. Route both to the attribute editor instead: extract the bare ATTEDIT behavior into a shared open_attedit_dialog helper (edit the selected block, or start the block-pick command) and call it from the ATTEDIT, ATTMAN, and BATTMAN arms. Kept as a match arm rather than an ocad.pgp alias so it works for existing users, whose config pgp is only seeded on first run. Replace the listing regression tests with one asserting ATTMAN and BATTMAN behave byte-for-byte like ATTEDIT. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
bd6a8845b4
commit
2a72227d5d
3 files changed
+53
-135
No files matched your search
+1
-1
@@ -194,7 +194,7 @@ Status of every standard CAD command in Open CAD Studio:
|
|||||||
| `ATTEXT` | — | Extract attributes (legacy) | ✅ |
|
| `ATTEXT` | — | Extract attributes (legacy) | ✅ |
|
||||||
| `ATTSYNC` | — | Synchronize attribute definitions | ✅ |
|
| `ATTSYNC` | — | Synchronize attribute definitions | ✅ |
|
||||||
| `BLOCKPALETTE` | — | Multi-view block palette (command-line list) | 🔶 |
|
| `BLOCKPALETTE` | — | Multi-view block palette (command-line list) | 🔶 |
|
||||||
| `ATTMAN` | `BATTMAN` | Attribute manager (command-line list) | 🔶 |
|
| `ATTMAN` | `BATTMAN` | Block attribute manager (opens the attribute editor) | ✅ |
|
||||||
| `XBIND` | XB | Bind xref elements to drawing | ❌ |
|
| `XBIND` | XB | Bind xref elements to drawing | ❌ |
|
||||||
| `XOPEN` | — | Open xref for editing | ✅ |
|
| `XOPEN` | — | Open xref for editing | ✅ |
|
||||||
| `BSAVE` | — | Save block in editor | ❌ |
|
| `BSAVE` | — | Save block in editor | ❌ |
|
||||||
|
|||||||
+20
-112
@@ -427,64 +427,15 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ATTMAN — list the attribute definitions of each block (or one named
|
// ATTMAN / BATTMAN — the Block Attribute Manager. Rather than a
|
||||||
// block). The full graphical manager is not built; this reports the
|
// command-line listing, both route to the attribute editor
|
||||||
// same information from the command line.
|
// (ATTEDIT): edit the selected block's attributes, or pick a block.
|
||||||
cmd if cmd == "ATTMAN"
|
cmd if cmd == "ATTMAN"
|
||||||
|| cmd == "BATTMAN"
|
|| cmd == "BATTMAN"
|
||||||
|| cmd.starts_with("ATTMAN ")
|
|| cmd.starts_with("ATTMAN ")
|
||||||
|| cmd.starts_with("BATTMAN ") =>
|
|| cmd.starts_with("BATTMAN ") =>
|
||||||
{
|
{
|
||||||
// BATTMAN is an alias for ATTMAN. Strip whichever keyword was
|
self.open_attedit_dialog();
|
||||||
// typed so the remainder is the optional block-name argument;
|
|
||||||
// check BATTMAN first since ATTMAN is a suffix of it.
|
|
||||||
let arg = cmd
|
|
||||||
.strip_prefix("BATTMAN")
|
|
||||||
.or_else(|| cmd.strip_prefix("ATTMAN"))
|
|
||||||
.unwrap_or("")
|
|
||||||
.trim();
|
|
||||||
let blocks = self.tabs[i].scene.custom_block_names();
|
|
||||||
let targets: Vec<String> = if arg.is_empty() {
|
|
||||||
blocks
|
|
||||||
} else {
|
|
||||||
blocks
|
|
||||||
.into_iter()
|
|
||||||
.filter(|b| b.eq_ignore_ascii_case(arg))
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
if targets.is_empty() {
|
|
||||||
self.command_line
|
|
||||||
.push_info("ATTMAN: no matching block. Usage: ATTMAN [block name]");
|
|
||||||
} else {
|
|
||||||
let doc = &self.tabs[i].scene.document;
|
|
||||||
let mut lines: Vec<String> = Vec::new();
|
|
||||||
for block in &targets {
|
|
||||||
let attdefs: Vec<String> = doc
|
|
||||||
.block_records
|
|
||||||
.get(block)
|
|
||||||
.map(|br| {
|
|
||||||
br.entity_handles
|
|
||||||
.iter()
|
|
||||||
.filter_map(|h| doc.get_entity(*h))
|
|
||||||
.filter_map(|e| match e {
|
|
||||||
acadrust::EntityType::AttributeDefinition(a) => {
|
|
||||||
Some(format!("{}={}", a.tag, a.default_value))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
lines.push(if attdefs.is_empty() {
|
|
||||||
format!(" {block}: no attributes.")
|
|
||||||
} else {
|
|
||||||
format!(" {block}: {}", attdefs.join(", "))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for l in lines {
|
|
||||||
self.command_line.push_output(&l);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"XATTACH" => {
|
"XATTACH" => {
|
||||||
@@ -653,30 +604,9 @@ impl OpenCADStudio {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use crate::app::OpenCADStudio;
|
use crate::app::OpenCADStudio;
|
||||||
|
|
||||||
/// Build a headless app with a single custom block "MYBLK" carrying one
|
fn fresh_app() -> OpenCADStudio {
|
||||||
/// attribute definition (COST=0), so ATTMAN/BATTMAN have something to list.
|
|
||||||
fn app_with_attributed_block() -> OpenCADStudio {
|
|
||||||
let mut app = OpenCADStudio::new_for_test();
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
app.automation_op(r#"{"op":"new"}"#);
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
|
||||||
let i = app.active_tab;
|
|
||||||
let doc = &mut app.tabs[i].scene.document;
|
|
||||||
let br_h = acadrust::Handle::new(doc.next_handle());
|
|
||||||
let mut br = acadrust::tables::BlockRecord::new("MYBLK");
|
|
||||||
br.handle = br_h;
|
|
||||||
doc.block_records.add(br).unwrap();
|
|
||||||
|
|
||||||
let mut ad = acadrust::entities::AttributeDefinition {
|
|
||||||
tag: "COST".into(),
|
|
||||||
prompt: "Cost".into(),
|
|
||||||
default_value: "0".into(),
|
|
||||||
insertion_point: acadrust::types::Vector3::new(0.0, 0.0, 0.0),
|
|
||||||
height: 1.0,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
ad.common.owner_handle = br_h;
|
|
||||||
doc.add_entity(acadrust::EntityType::AttributeDefinition(ad))
|
|
||||||
.unwrap();
|
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,49 +621,27 @@ mod tests {
|
|||||||
.join("\n")
|
.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regression for #295: BATTMAN triggered the ATTMAN handler but the argument
|
// ATTMAN and BATTMAN are the Block Attribute Manager; this build routes both
|
||||||
// was extracted with `trim_start_matches("ATTMAN")`, which leaves "BATTMAN"
|
// to the attribute editor (ATTEDIT) instead of the old command-line listing.
|
||||||
// as a bogus block-name argument — so BATTMAN always reported
|
// With nothing selected they start the same interactive pick as ATTEDIT,
|
||||||
// "ATTMAN: no matching block" instead of listing attributes like ATTMAN.
|
// byte for byte, and never emit the old "no matching block" listing text.
|
||||||
#[test]
|
#[test]
|
||||||
fn battman_is_a_working_alias_for_attman() {
|
fn attman_and_battman_route_to_attedit() {
|
||||||
let attman_out = run_capture(&mut app_with_attributed_block(), "ATTMAN");
|
let attedit = run_capture(&mut fresh_app(), "ATTEDIT");
|
||||||
let battman_out = run_capture(&mut app_with_attributed_block(), "BATTMAN");
|
let attman = run_capture(&mut fresh_app(), "ATTMAN");
|
||||||
|
let battman = run_capture(&mut fresh_app(), "BATTMAN");
|
||||||
|
|
||||||
// ATTMAN lists the block; the reported bug is that BATTMAN did not.
|
|
||||||
assert!(
|
|
||||||
attman_out.contains("MYBLK"),
|
|
||||||
"ATTMAN should list the block, got: {attman_out:?}"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
battman_out.contains("MYBLK"),
|
|
||||||
"BATTMAN should list the block like ATTMAN, got: {battman_out:?}"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!battman_out.contains("no matching block"),
|
|
||||||
"BATTMAN must not fall into the no-argument error path, got: {battman_out:?}"
|
|
||||||
);
|
|
||||||
// The alias must behave identically, attribute listing included.
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
attman_out, battman_out,
|
attman, attedit,
|
||||||
"BATTMAN output must match ATTMAN output exactly"
|
"ATTMAN must behave like ATTEDIT, got: {attman:?} vs {attedit:?}"
|
||||||
);
|
);
|
||||||
}
|
assert_eq!(
|
||||||
|
battman, attedit,
|
||||||
// The keyword-stripping fix must also honour an explicit block-name argument
|
"BATTMAN must behave like ATTEDIT, got: {battman:?} vs {attedit:?}"
|
||||||
// on BATTMAN (`BATTMAN <name>`), which the old code never matched at all.
|
|
||||||
#[test]
|
|
||||||
fn battman_honours_a_block_name_argument() {
|
|
||||||
let hit = run_capture(&mut app_with_attributed_block(), "BATTMAN MYBLK");
|
|
||||||
assert!(
|
|
||||||
hit.contains("MYBLK") && !hit.contains("no matching block"),
|
|
||||||
"BATTMAN MYBLK should list that block, got: {hit:?}"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let miss = run_capture(&mut app_with_attributed_block(), "BATTMAN NOPE");
|
|
||||||
assert!(
|
assert!(
|
||||||
miss.contains("no matching block"),
|
!attman.contains("no matching block"),
|
||||||
"BATTMAN NOPE should report no match, got: {miss:?}"
|
"ATTMAN must not run the old command-line listing, got: {attman:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+32
-22
@@ -258,28 +258,7 @@ impl OpenCADStudio {
|
|||||||
// that block; otherwise the pick command runs and the editor opens
|
// that block; otherwise the pick command runs and the editor opens
|
||||||
// once a block is chosen (see `command_driver`).
|
// once a block is chosen (see `command_driver`).
|
||||||
"ATTEDIT" => {
|
"ATTEDIT" => {
|
||||||
let selected_attr_insert = {
|
self.open_attedit_dialog();
|
||||||
let sel = self.tabs[i].scene.selected_entities();
|
|
||||||
if sel.len() == 1 {
|
|
||||||
let (h, e) = sel[0];
|
|
||||||
match e {
|
|
||||||
acadrust::EntityType::Insert(ins) if !ins.attributes.is_empty() => {
|
|
||||||
Some(h)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Some(handle) = selected_attr_insert {
|
|
||||||
self.open_attribute_editor(handle);
|
|
||||||
} else {
|
|
||||||
use crate::modules::draw::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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── REFEDIT — in-place block editing ─────────────────────────────
|
// ── REFEDIT — in-place block editing ─────────────────────────────
|
||||||
@@ -1250,6 +1229,37 @@ fn arith_eval(expr: &str) -> Result<f64, String> {
|
|||||||
Ok(v)
|
Ok(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OpenCADStudio {
|
||||||
|
/// Open the attribute editor. If a single block with attributes is already
|
||||||
|
/// selected it opens directly on that block; otherwise it starts the
|
||||||
|
/// block-pick command and opens once a block is chosen (see
|
||||||
|
/// `command_driver`). Shared by ATTEDIT and its command aliases
|
||||||
|
/// ATTMAN / BATTMAN.
|
||||||
|
pub(in crate::app) fn open_attedit_dialog(&mut self) {
|
||||||
|
let i = self.active_tab;
|
||||||
|
let selected_attr_insert = {
|
||||||
|
let sel = self.tabs[i].scene.selected_entities();
|
||||||
|
if sel.len() == 1 {
|
||||||
|
let (h, e) = sel[0];
|
||||||
|
match e {
|
||||||
|
acadrust::EntityType::Insert(ins) if !ins.attributes.is_empty() => Some(h),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(handle) = selected_attr_insert {
|
||||||
|
self.open_attribute_editor(handle);
|
||||||
|
} else {
|
||||||
|
use crate::modules::draw::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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ArithParser {
|
struct ArithParser {
|
||||||
chars: Vec<char>,
|
chars: Vec<char>,
|
||||||
pos: usize,
|
pos: usize,
|
||||||
|
|||||||
Reference in new issue
Block a user