Merge pull request #1806 from ramox81/codex/block-attribute-manager-window

Block Attribute Manager window (BATTMAN / ATTMAN)
This commit is contained in:
Hakan Seven committed 2026-10-15 14:51:47 +03:00
commit 7de32c91c2
36 files changed
+2089 -8

No files matched your search

+13
View File
@@ -6626,3 +6626,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6614,3 +6614,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6611,3 +6611,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6612,3 +6612,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6614,3 +6614,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6613,3 +6613,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6614,3 +6614,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6611,3 +6611,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6614,3 +6614,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6602,3 +6602,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6598,3 +6598,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6613,3 +6613,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6602,3 +6602,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6613,3 +6613,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6614,3 +6614,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6612,3 +6612,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6613,3 +6613,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6614,3 +6614,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6615,3 +6615,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6535,3 +6535,16 @@ block-def-dialog =
.remembered = Ekranda belirt, nesne seçeneği, dağıtmaya izin ve bu anahtar sonraki açılışta hatırlanır.
.no-objects = Blok için nesne seçilmedi.
.create-empty = Boş blok oluştur
attman =
.title = Öznitelik Yöneticisi
.modes = Kipler
.references = __ocs_fmt_0__ referans
.counts = Çizimde __ocs_fmt_0__ · model alanında __ocs_fmt_1__
.columns = Sütunlar:
.constant-note = Sabit kipi tanımda değiştirilemez.
.live = Değişiklikleri çizimde canlı göster
.apply-existing = Mevcut referanslara uygula
.emphasize = Yinelenen etiketleri vurgula
.sync = Eşitle
.help = BATTMAN bir bloğun öznitelik tanımlarını yönetir.
.no-attributed-block = Blok tablosunda __ocs_fmt_0__ adlı öznitelikli blok yok.
+13
View File
@@ -6597,3 +6597,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+13
View File
@@ -6609,3 +6609,16 @@ block-def-dialog =
.remembered = Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time.
.no-objects = No objects are selected for the block.
.create-empty = Create empty block
attman =
.title = Block Attribute Manager
.modes = Modes
.references = __ocs_fmt_0__ references
.counts = In drawing: __ocs_fmt_0__ · in model space: __ocs_fmt_1__
.columns = Columns:
.constant-note = The Constant mode cannot be changed in the definition.
.live = Show changes in the drawing as they are made
.apply-existing = Apply changes to existing references
.emphasize = Emphasize duplicate tags
.sync = Sync
.help = BATTMAN manages the attribute definitions of a block.
.no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__.
+565
View File
@@ -0,0 +1,565 @@
//! Block Attribute Manager (BATTMAN / ATTMAN): loading an attributed block's
//! definitions into the dialog and writing the edited list back to the
//! block definition and, when asked, to its existing references.
use codec::entities::AttributeDefinition;
use codec::{EntityType, Handle};
use iced::Task;
use crate::app::{Message, OpenCADStudio};
use crate::modules::insert::block_cmdline::BlockTable;
use crate::scene::view::dispatch::{apply_common_prop, apply_geom_prop};
use crate::ui::window::attman_dialog::{
self as dialog, AttManBlock, AttManMsg, AttManRow, AttManState, EditTab,
};
impl OpenCADStudio {
/// Open the dialog on the block of the first preselected attributed
/// reference, else on the first attributed block.
pub(in crate::app) fn open_attman(&mut self, i: usize, preselected: &[Handle]) -> Task<Message> {
let document = &self.tabs[i].scene.document;
let table = BlockTable::of(document);
let model = document
.block_records
.get("*Model_Space")
.map_or(Handle::NULL, |br| br.handle);
let blocks: Vec<AttManBlock> = table
.attributed
.iter()
.map(|name| {
let mut found = 0;
let mut in_space = 0;
for entity in document.entities() {
if let EntityType::Insert(insert) = entity {
if insert.block_name.eq_ignore_ascii_case(name) {
found += 1;
if insert.common.owner_handle == model {
in_space += 1;
}
}
}
}
AttManBlock {
name: name.clone(),
found,
in_space,
}
})
.collect();
let start = preselected
.iter()
.find_map(|h| match document.get_entity(*h) {
Some(EntityType::Insert(insert)) => blocks
.iter()
.position(|b| b.name.eq_ignore_ascii_case(&insert.block_name)),
_ => None,
})
.unwrap_or(0);
let mut styles: Vec<String> = document
.text_styles
.iter()
.filter(|s| !s.name.is_empty())
.map(|s| s.name.clone())
.collect();
styles.sort_by_key(|s| s.to_lowercase());
let layers: Vec<String> = document.layers.iter().map(|l| l.name.clone()).collect();
let linetypes: Vec<String> = document
.line_types
.iter()
.map(|lt| lt.name.clone())
.filter(|n| !n.eq_ignore_ascii_case("ByLayer") && !n.eq_ignore_ascii_case("ByBlock"))
.collect();
self.attman = Some(AttManState {
blocks,
block: start,
rows: Vec::new(),
removed: Vec::new(),
selected: None,
tab: EditTab::Attribute,
edit_open: true,
drafts: Default::default(),
dirty: false,
live_steps: 0,
more_columns: false,
styles,
layers,
linetypes,
error: None,
});
self.attman_load(i);
self.tabs[i].active_cmd = None;
self.active_modal = Some(crate::app::ModalKind::AttMan);
Task::none()
}
/// Read the shown block's attribute definitions, in block order.
fn attman_load(&mut self, i: usize) {
let document = &self.tabs[i].scene.document;
let Some(state) = self.attman.as_mut() else {
return;
};
let Some(name) = state.current_block().map(|b| b.name.clone()) else {
return;
};
state.rows = document
.block_records
.get(&name)
.map(|br| {
br.entity_handles
.iter()
.filter_map(|h| match document.get_entity(*h) {
Some(EntityType::AttributeDefinition(def)) => Some(AttManRow {
handle: *h,
original_tag: def.tag.clone(),
live_tag: def.tag.clone(),
def: def.clone(),
}),
_ => None,
})
.collect()
})
.unwrap_or_default();
state.removed.clear();
state.dirty = false;
state.selected = (!state.rows.is_empty()).then_some(0);
state.load_drafts();
}
pub(in crate::app) fn on_attman(&mut self, m: AttManMsg) -> Task<Message> {
let i = self.active_tab;
let Some(state) = self.attman.as_mut() else {
return Task::none();
};
// Edits of the selected row.
let mut edited = true;
let at = state.selected;
match m {
AttManMsg::Block(index) => {
if index == state.block || index >= state.blocks.len() {
return Task::none();
}
// Pending changes of the shown block are kept.
if state.dirty && !self.attman_commit(i, true, false) {
return Task::none();
}
if let Some(state) = self.attman.as_mut() {
state.block = index;
}
self.attman_load(i);
return Task::none();
}
AttManMsg::PickBlock => {
self.active_modal = None;
let command = crate::modules::insert::attman::AttmanPickCommand;
use crate::command::CadCommand as _;
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
return Task::none();
}
AttManMsg::Select(index) => {
if index < state.rows.len() {
state.selected = Some(index);
state.load_drafts();
}
return Task::none();
}
AttManMsg::MoveUp | AttManMsg::MoveDown => {
let Some(at) = at else {
return Task::none();
};
let to = if matches!(m, AttManMsg::MoveUp) {
at.checked_sub(1)
} else {
Some(at + 1).filter(|t| *t < state.rows.len())
};
let Some(to) = to else {
return Task::none();
};
state.rows.swap(at, to);
state.selected = Some(to);
}
AttManMsg::Remove => {
let Some(at) = at else {
return Task::none();
};
if state.rows.len() < 2 {
return Task::none();
}
let row = state.rows.remove(at);
state.removed.push(row.handle);
state.selected = Some(at.min(state.rows.len() - 1));
state.load_drafts();
}
AttManMsg::ToggleEdit => {
state.edit_open = !state.edit_open;
return Task::none();
}
AttManMsg::Tab(tab) => {
state.tab = tab;
return Task::none();
}
AttManMsg::MoreColumns => {
state.more_columns = !state.more_columns;
return Task::none();
}
AttManMsg::Column(column, on) => {
let bit = 1u32 << (column as u32);
let mut prefs = dialog::prefs();
prefs.columns = if on {
prefs.columns | bit
} else {
prefs.columns & !bit
};
return Task::none();
}
AttManMsg::ApplyExisting(on) => {
dialog::prefs().apply_existing = on;
return Task::none();
}
AttManMsg::Emphasize(on) => {
dialog::prefs().emphasize_duplicates = on;
return Task::none();
}
AttManMsg::Live(on) => {
dialog::prefs().live = on;
if on && state.dirty {
self.attman_commit(i, false, false);
}
return Task::none();
}
AttManMsg::DismissError => {
state.error = None;
return Task::none();
}
AttManMsg::Help => {
self.command_line.push_info(
crate::t!("BATTMAN manages the attribute definitions of a block.").as_ref(),
);
return Task::none();
}
AttManMsg::Sync => {
self.attman_commit(i, true, true);
return Task::none();
}
AttManMsg::Apply => {
self.attman_commit(i, true, false);
return Task::none();
}
AttManMsg::Ok => {
if self.attman_commit(i, true, false) {
self.attman = None;
self.close_active_modal();
}
return Task::none();
}
AttManMsg::Cancel => {
self.attman_withdraw_live();
self.attman = None;
self.close_active_modal();
return Task::none();
}
_ => {
edited = false;
}
}
if edited {
state.dirty = true;
return self.attman_after_edit(i);
}
// Field edits of the selected row.
let Some(row) = at.and_then(|at| state.rows.get_mut(at)) else {
return Task::none();
};
let def = &mut row.def;
let yes = |on: bool| if on { "Yes" } else { "No" };
match m {
AttManMsg::Tag(value) => {
state.drafts.tag = value.clone();
if crate::ui::window::attdef_dialog::tag_error(&value).is_none() {
geom(def, "att_tag", &value);
}
}
AttManMsg::Prompt(value) => geom(def, "att_prompt", &value),
AttManMsg::Default(value) => geom(def, "att_default", &value),
AttManMsg::Mode(key, on) => {
let field = match key {
"invisible" => "att_invisible",
"verify" => "att_verify",
"preset" => "att_preset",
"multiline" => "att_multiline",
_ => return Task::none(),
};
geom(def, field, yes(on));
}
AttManMsg::Style(value) => geom(def, "att_style", &value),
AttManMsg::Justify(justify) => geom(def, "att_justify", justify.label()),
AttManMsg::Height(value) => {
state.drafts.height = value.clone();
geom(def, "att_h", &value);
}
AttManMsg::Rotation(value) => {
state.drafts.rotation = value.clone();
geom(def, "att_rot", &value);
}
AttManMsg::Width(value) => {
state.drafts.width = value.clone();
geom(def, "att_wf", &value);
}
AttManMsg::Oblique(value) => {
state.drafts.oblique = value.clone();
geom(def, "att_ob", &value);
}
AttManMsg::Boundary(value) => {
state.drafts.boundary = value.clone();
geom(def, "att_bwidth", &value);
}
AttManMsg::Annotative(on) => geom(def, "att_annotative", yes(on)),
AttManMsg::Backwards(on) => geom(def, "att_backward", yes(on)),
AttManMsg::UpsideDown(on) => geom(def, "att_upside_down", yes(on)),
AttManMsg::Layer(value) => common(def, "layer", &value),
AttManMsg::Linetype(value) => common(def, "linetype", &value),
AttManMsg::Color(value) => {
if let Some(color) = dialog::parse_color(&value) {
def.common.color = color;
}
}
AttManMsg::Lineweight(value) => {
if let Some(lw) = dialog::parse_lineweight(&value) {
def.common.line_weight = lw;
}
}
_ => return Task::none(),
}
state.dirty = true;
self.attman_after_edit(i)
}
fn attman_after_edit(&mut self, i: usize) -> Task<Message> {
if dialog::prefs().live {
self.attman_commit(i, false, false);
}
Task::none()
}
/// The pick of `_ATTMAN_PICK <hex handle>`: show the picked reference's
/// block. A reference of a block without attributes is reported.
pub(in crate::app) fn attman_picked(&mut self, i: usize, handle: &str) -> Task<Message> {
self.active_modal = Some(crate::app::ModalKind::AttMan);
let Ok(value) = u64::from_str_radix(handle, 16) else {
return Task::none();
};
let name = match self.tabs[i].scene.document.get_entity(Handle::new(value)) {
Some(EntityType::Insert(insert)) => insert.block_name.clone(),
_ => return Task::none(),
};
let index = self.attman.as_ref().and_then(|state| {
state
.blocks
.iter()
.position(|b| b.name.eq_ignore_ascii_case(&name))
});
match index {
Some(index) => self.on_attman(AttManMsg::Block(index)),
None => {
if let Some(state) = self.attman.as_mut() {
state.error = Some(
crate::tf!("Block table contains no attributed block named {}.", name)
.into_owned(),
);
}
Task::none()
}
}
}
/// Take back the changes written live since the last Apply, quietly.
fn attman_withdraw_live(&mut self) {
let Some(state) = self.attman.as_mut() else {
return;
};
let steps = std::mem::take(&mut state.live_steps);
for row in &mut state.rows {
row.live_tag = row.original_tag.clone();
}
if steps > 0 {
self.undo_steps_reported(steps, false);
}
}
/// Write the working copy to the drawing. `finish` (Apply / OK / Sync)
/// leaves exactly one undo step: the live changes are taken back and the
/// whole list is written again in one step. Otherwise (a live edit) the
/// write joins the open live step. `sync` rebuilds the references'
/// attributes even when "apply to existing references" is off. Returns
/// false when a field is invalid.
fn attman_commit(&mut self, i: usize, finish: bool, sync: bool) -> bool {
let Some(state) = self.attman.as_mut() else {
return false;
};
if finish {
if let Some(error) = state
.rows
.iter()
.find_map(|row| crate::ui::window::attdef_dialog::tag_error(&row.def.tag))
.or_else(|| {
crate::ui::window::attdef_dialog::tag_error(&state.drafts.tag)
.filter(|_| state.selected.is_some())
})
{
state.error = Some(crate::t!(error).into_owned());
return false;
}
state.error = None;
self.attman_withdraw_live();
}
let Some(state) = self.attman.as_mut() else {
return false;
};
let Some(name) = state.current_block().map(|b| b.name.clone()) else {
return false;
};
let rows: Vec<(Handle, String, AttributeDefinition)> = state
.rows
.iter()
.map(|r| (r.handle, r.live_tag.clone(), r.def.clone()))
.collect();
let removed = state.removed.clone();
let write = state.dirty || sync;
let synced = sync || dialog::prefs().apply_existing;
// A live step something else closed starts a new one.
let open = !finish && state.live_steps > 0 && self.tabs[i].history.pending.is_some();
if write {
if !open {
self.push_undo_snapshot(i, "BATTMAN");
}
self.attman_write(i, &name, &rows, &removed, synced);
}
if finish && write {
self.finish_pending_history(i);
}
let Some(state) = self.attman.as_mut() else {
return false;
};
if finish {
state.dirty = false;
state.removed.clear();
for row in &mut state.rows {
row.original_tag = row.def.tag.clone();
row.live_tag = row.def.tag.clone();
}
} else if write {
if !open {
state.live_steps += 1;
}
// The references now hold the written tags.
if synced {
for row in &mut state.rows {
row.live_tag = row.def.tag.clone();
}
}
}
true
}
/// The definition list of `block` becomes `rows` (in that order) less
/// `removed`; with `sync`, every reference is rebuilt from it, keeping
/// each attribute's value through a tag rename.
fn attman_write(
&mut self,
i: usize,
block: &str,
rows: &[(Handle, String, AttributeDefinition)],
removed: &[Handle],
sync: bool,
) {
let document = &mut self.tabs[i].scene.document;
for handle in removed {
if document.get_entity(*handle).is_some() {
document.remove_entity(*handle);
}
}
// Each definition keeps its object; the definitions take the slots
// the block's definitions held, in the new order.
for (handle, _, def) in rows {
if let Some(EntityType::AttributeDefinition(slot)) = document.get_entity_mut(*handle) {
let mut def = def.clone();
def.common.handle = *handle;
def.common.owner_handle = slot.common.owner_handle;
*slot = def;
}
}
if let Some(br) = document.block_records.get_mut(block) {
br.entity_handles.retain(|h| !removed.contains(h));
let mut order = rows.iter().map(|(handle, _, _)| *handle);
for slot in br.entity_handles.iter_mut() {
if rows.iter().any(|(handle, _, _)| handle == slot) {
if let Some(next) = order.next() {
*slot = next;
}
}
}
}
// Annotative definitions carry the scale context data.
for (handle, _, def) in rows {
crate::scene::annotative::set_entity_annotative(
&mut self.tabs[i].scene.document,
*handle,
def.flags.annotative,
);
}
if sync {
// A renamed definition keeps the value each reference held.
let renames: Vec<(String, String)> = rows
.iter()
.filter(|(_, old, def)| !old.eq_ignore_ascii_case(&def.tag))
.map(|(_, old, def)| (old.clone(), def.tag.clone()))
.collect();
if !renames.is_empty() {
let inserts: Vec<Handle> = self.tabs[i]
.scene
.document
.entities()
.filter_map(|e| match e {
EntityType::Insert(ins) if ins.block_name.eq_ignore_ascii_case(block) => {
Some(ins.common.handle)
}
_ => None,
})
.collect();
for handle in inserts {
if let Some(EntityType::Insert(ins)) =
self.tabs[i].scene.document.get_entity_mut(handle)
{
for attribute in &mut ins.attributes {
if let Some((_, new)) = renames
.iter()
.find(|(old, _)| old.eq_ignore_ascii_case(&attribute.tag))
{
attribute.tag = new.clone();
}
}
}
}
}
self.attsync_block_with(i, block, false);
}
self.tabs[i].scene.bump_geometry();
self.tabs[i].dirty = true;
}
}
fn geom(def: &mut AttributeDefinition, field: &str, value: &str) {
let mut entity = EntityType::AttributeDefinition(def.clone());
apply_geom_prop(&mut entity, field, value);
if let EntityType::AttributeDefinition(updated) = entity {
*def = updated;
}
}
fn common(def: &mut AttributeDefinition, field: &str, value: &str) {
let mut entity = EntityType::AttributeDefinition(def.clone());
apply_common_prop(&mut entity, field, value);
if let EntityType::AttributeDefinition(updated) = entity {
*def = updated;
}
}
+14 -2
View File
@@ -135,7 +135,12 @@ impl OpenCADStudio {
.push_output("This drawing contains no attributed blocks.");
return Some(Task::none());
}
self.open_attedit_dialog();
let preselected = self.tabs[i].scene.selected_handles_in_order();
return Some(self.open_attman(i, &preselected));
}
cmd if cmd == "_ATTMAN_PICK" || cmd.starts_with("_ATTMAN_PICK ") => {
self.tabs[i].active_cmd = None;
return Some(self.attman_picked(i, cmd["_ATTMAN_PICK".len()..].trim()));
}
_ => return None,
}
@@ -166,6 +171,11 @@ impl OpenCADStudio {
/// attributes whose definition is gone are dropped. Constant definitions
/// carry no attribute.
pub(in crate::app) fn attsync_block(&mut self, i: usize, block: &str) {
self.attsync_block_with(i, block, true);
}
/// [`Self::attsync_block`], opening its own undo step only when `undo`.
pub(in crate::app) fn attsync_block_with(&mut self, i: usize, block: &str, undo: bool) {
use codec::Entity as _;
let doc = &self.tabs[i].scene.document;
let attdefs: Vec<codec::entities::AttributeDefinition> = doc
@@ -200,7 +210,9 @@ impl OpenCADStudio {
if inserts.is_empty() {
return;
}
self.push_undo_snapshot(i, "ATTSYNC");
if undo {
self.push_undo_snapshot(i, "ATTSYNC");
}
let mut changes = Vec::new();
for handle in inserts {
let document = &self.tabs[i].scene.document;
+3 -3
View File
@@ -1790,9 +1790,9 @@ mod tests {
.join("\n")
}
// ATTMAN and BATTMAN are the Block Attribute Manager; this build routes both
// to the attribute editor (ATTEDIT). A drawing without attributed blocks
// has nothing to manage, which both report instead of opening it.
// ATTMAN and BATTMAN open the Block Attribute Manager. A drawing without
// attributed blocks has nothing to manage, which both report instead of
// opening it.
#[test]
fn attman_and_battman_report_a_drawing_without_attributed_blocks() {
for command in ["ATTMAN", "BATTMAN"] {
+1
View File
@@ -5,6 +5,7 @@ use iced::Task;
use std::path::PathBuf;
mod attdef;
mod attman;
mod block_cmdline;
mod blocks;
mod blockvars;
+94
View File
@@ -123,6 +123,7 @@ pub(super) const NAMES: &[&str] = &[
"block_def_dialog",
"attdef_dialog",
"field_dialog",
"attman_dialog",
"sheet_set",
"close_document",
"toggle_properties",
@@ -1570,6 +1571,96 @@ impl OpenCADStudio {
_ => return Err(failure("invalid_value", "Unknown attdef dialog field")),
})
}
// The open Block Attribute Manager: `block=<name>`, `select=<row>`,
// `up`, `down`, `remove`, `edit`, `tab=attribute|text|properties`,
// `tag=…`, `prompt=…`, `default=…`, `mode=<invisible|verify|preset|multiline>:0|1`,
// `style=…`, `justify=MC`, `height=…`, `rotation=…`, `width=…`, `oblique=…`,
// `boundary=…`, `annotative=0|1`, `backwards=0|1`, `upside_down=0|1`,
// `layer=…`, `linetype=…`, `color=…`, `lineweight=…`, `column=<name>:0|1`,
// `more_columns`, `apply_existing=0|1`, `emphasize=0|1`, `live=0|1`,
// `pick`, `sync`, `apply`, `ok`, `cancel`.
"attman_dialog" => {
use crate::ui::window::attman_dialog::{AttManMsg as A, Column, EditTab};
let Some(state) = self.attman.as_ref() else {
return Err(failure("no_dialog", "The attribute manager is not open"));
};
let value = string(req, "value")?;
let (key, v) = value.split_once('=').unwrap_or((value, ""));
let on = v == "1";
Message::AttMan(match key {
"block" => A::Block(
state
.blocks
.iter()
.position(|b| b.name.eq_ignore_ascii_case(v))
.ok_or_else(|| failure("invalid_value", "No such attributed block"))?,
),
"select" => A::Select(
v.parse()
.ok()
.filter(|r: &usize| *r < state.rows.len())
.ok_or_else(|| failure("invalid_value", "No such row"))?,
),
"up" => A::MoveUp,
"down" => A::MoveDown,
"remove" => A::Remove,
"edit" => A::ToggleEdit,
"tab" => A::Tab(match v {
"text" => EditTab::Text,
"properties" => EditTab::Properties,
_ => EditTab::Attribute,
}),
"tag" => A::Tag(v.into()),
"prompt" => A::Prompt(v.into()),
"default" => A::Default(v.into()),
"mode" => {
let (mode, state) = v.split_once(':').unwrap_or((v, "1"));
let mode = match mode {
"invisible" => "invisible",
"verify" => "verify",
"preset" => "preset",
"multiline" => "multiline",
_ => return Err(failure("invalid_value", "Unknown mode")),
};
A::Mode(mode, state == "1")
}
"style" => A::Style(v.into()),
"justify" => A::Justify(
crate::modules::draw::draw::attdef::Justify::from_keyword(v)
.ok_or_else(|| failure("invalid_value", "Unknown justification"))?,
),
"height" => A::Height(v.into()),
"rotation" => A::Rotation(v.into()),
"width" => A::Width(v.into()),
"oblique" => A::Oblique(v.into()),
"boundary" => A::Boundary(v.into()),
"annotative" => A::Annotative(on),
"backwards" => A::Backwards(on),
"upside_down" => A::UpsideDown(on),
"layer" => A::Layer(v.into()),
"linetype" => A::Linetype(v.into()),
"color" => A::Color(v.into()),
"lineweight" => A::Lineweight(v.into()),
"column" => {
let (name, state) = v.split_once(':').unwrap_or((v, "1"));
A::Column(
Column::from_name(name)
.ok_or_else(|| failure("invalid_value", "Unknown column"))?,
state == "1",
)
}
"more_columns" => A::MoreColumns,
"apply_existing" => A::ApplyExisting(on),
"emphasize" => A::Emphasize(on),
"live" => A::Live(on),
"pick" => A::PickBlock,
"sync" => A::Sync,
"apply" => A::Apply,
"ok" => A::Ok,
"cancel" => A::Cancel,
_ => return Err(failure("invalid_value", "Unknown attribute manager field")),
})
}
// One field of the open Field dialog: `category=<index>`, `name=Date`,
// `date=yyyy-MM-dd`, `case=0..4`, `file=1|2|3`, `ext=0|1`, `size=0..2`,
// `sysvar=dimscale`, `diesel=…`, `named_type=<index>`, `named=<index>`,
@@ -2142,6 +2233,9 @@ impl OpenCADStudio {
Some(crate::app::ModalKind::Field) => {
Message::FieldDialog(crate::ui::window::field_dialog::FieldDialogMsg::Ok)
}
Some(crate::app::ModalKind::AttMan) => {
Message::AttMan(crate::ui::window::attman_dialog::AttManMsg::Ok)
}
_ => return Err(failure("no_dialog", "No dialog with an OK button is open")),
},
"close_document" => Message::TabClose(self.tabs[self.active_tab].id),
+10 -2
View File
@@ -1287,6 +1287,12 @@ impl OpenCADStudio {
}
pub(super) fn undo_steps(&mut self, steps: usize) {
self.undo_steps_reported(steps, true);
}
/// [`Self::undo_steps`]; `report` false takes the steps back without
/// the command-line notice (a dialog withdrawing its own live changes).
pub(super) fn undo_steps_reported(&mut self, steps: usize, report: bool) {
let i = self.active_tab;
self.finish_pending_history(i);
let available = self.tabs[i].history.undo_stack.len();
@@ -1353,8 +1359,10 @@ impl OpenCADStudio {
native_graph_changed,
&changes,
);
self.command_line
.push_output(crate::tf!("Undo: {last_label}").as_ref());
if report {
self.command_line
.push_output(crate::tf!("Undo: {last_label}").as_ref());
}
}
pub(super) fn redo_steps(&mut self, steps: usize) {
+7
View File
@@ -592,6 +592,8 @@ pub(super) struct OpenCADStudio {
/// ATTDEF dialog and Edit Attribute Definition dialog; `None` while closed.
attdef_dialog: Option<crate::ui::window::attdef_dialog::AttdefDialogState>,
attdef_edit: Option<crate::ui::window::attdef_dialog::AttdefEditState>,
/// Block Attribute Manager working copy; `None` while it is closed.
attman: Option<crate::ui::window::attman_dialog::AttManState>,
/// Field dialog working copy; `None` while closed.
field_dialog: Option<crate::ui::window::field_dialog::FieldDialogState>,
point_cloud_color_map: Option<crate::ui::window::pdf_dialogs::PointCloudColorMapState>,
@@ -2206,6 +2208,8 @@ pub enum ModalKind {
GeometricTolerance,
AttDef,
AttDefEdit,
/// Block Attribute Manager (BATTMAN / ATTMAN).
AttMan,
CountInvalidArea,
Field,
DraftingSettings,
@@ -4425,6 +4429,8 @@ pub enum Message {
/// An edit in one of the PDF dialogs.
PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg),
AttdefDialog(crate::ui::window::attdef_dialog::AttdefDialogMsg),
/// Block Attribute Manager edits.
AttMan(crate::ui::window::attman_dialog::AttManMsg),
FieldDialog(crate::ui::window::field_dialog::FieldDialogMsg),
/// An edit in the Edit Reference dialog.
RefEditDialog(crate::ui::window::refedit_dialog::RefEditDialogMsg),
@@ -4638,6 +4644,7 @@ impl OpenCADStudio {
pdf_attach: None,
point_cloud_attach: None,
attdef_dialog: None,
attman: None,
attdef_edit: None,
field_dialog: None,
point_cloud_color_map: None,
+9
View File
@@ -233,6 +233,7 @@ impl OpenCADStudio {
}
Some(GeometricTolerance) => self.geometric_tolerance = None,
Some(BlockDefinition) => self.block_definition = None,
Some(AttMan) => self.attman = None,
Some(BlockEditPicker) => self.block_edit_picker = None,
Some(BlockSaveAs) => self.block_save_as = None,
Some(PdfAttach) => self.pdf_attach = None,
@@ -2441,6 +2442,7 @@ impl OpenCADStudio {
Message::PdfDialog(message) => self.update_pdf_dialog(message),
Message::AttdefDialog(message) => self.on_attdef_dialog(message),
Message::AttMan(message) => self.on_attman(message),
Message::FieldDialog(message) => self.on_field_dialog(message),
Message::RefEditDialog(message) => self.update_refedit_dialog(message),
Message::PcManager(message) => self.update_pc_manager(message),
@@ -9666,6 +9668,13 @@ impl OpenCADStudio {
self.close_field_dialog();
return Task::none();
}
// Closing the attribute manager is its Cancel: live changes
// are taken back.
if self.active_modal == Some(super::ModalKind::AttMan) {
return self.on_attman(
crate::ui::window::attman_dialog::AttManMsg::Cancel,
);
}
if matches!(
self.active_modal,
Some(super::ModalKind::AttDef | super::ModalKind::AttDefEdit)
+7
View File
@@ -52,6 +52,7 @@ impl OpenCADStudio {
Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(),
Some(K::AttDef) => crate::t!("Attribute Definition").into_owned(),
Some(K::AttDefEdit) => crate::t!("Edit Attribute Definition").into_owned(),
Some(K::AttMan) => crate::t!("Block Attribute Manager").into_owned(),
Some(K::CountInvalidArea) => crate::t!("Count - Invalid Area").into_owned(),
Some(K::Field) => crate::t!("Field").into_owned(),
Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(),
@@ -630,6 +631,12 @@ impl OpenCADStudio {
super::super::ModalKind::CountInvalidArea => sized_flow(ex, 520, 260, |flow| {
crate::ui::window::count_palette::invalid_area_view(&self.count_palette, flow)
}),
super::super::ModalKind::AttMan => {
let state = self.attman.as_ref()?;
sized_flow(ex, 880, 660, |flow| {
crate::ui::window::attman_dialog::view(state, flow)
})
}
super::super::ModalKind::AttDefEdit => {
let state = self.attdef_edit.as_ref()?;
sized_flow(ex, 460, 220, |flow| {
+12
View File
@@ -7,6 +7,18 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Specify On-screen, the object option, Allow exploding and this switch are remembered for the next time." => Some(("block-def-dialog", "remembered")),
"No objects are selected for the block." => Some(("block-def-dialog", "no-objects")),
"Create empty block" => Some(("block-def-dialog", "create-empty")),
"Block Attribute Manager" => Some(("attman", "title")),
"Modes" => Some(("attman", "modes")),
"{} references" => Some(("attman", "references")),
"In drawing: {} · in model space: {}" => Some(("attman", "counts")),
"Columns:" => Some(("attman", "columns")),
"The Constant mode cannot be changed in the definition." => Some(("attman", "constant-note")),
"Show changes in the drawing as they are made" => Some(("attman", "live")),
"Apply changes to existing references" => Some(("attman", "apply-existing")),
"Emphasize duplicate tags" => Some(("attman", "emphasize")),
"Sync" => Some(("attman", "sync")),
"BATTMAN manages the attribute definitions of a block." => Some(("attman", "help")),
"Block table contains no attributed block named {}." => Some(("attman", "no-attributed-block")),
"{} entities added" => Some(("ref-edit", "entities-added")),
"Use REFCLOSE or the Refedit toolbar to end reference editing session." => Some(("ref-edit", "use-refclose")),
"Specify opposite corner:" => Some(("ref-edit", "opposite-corner")),
+41
View File
@@ -8,3 +8,44 @@ pub fn tool() -> ToolDef {
event: ModuleEvent::Command("ATTMAN".to_string()),
}
}
/// The Block Attribute Manager's "Select block" button: one block reference
/// picked in the drawing, handed back as `_ATTMAN_PICK <hex handle>`.
pub struct AttmanPickCommand;
impl crate::command::CadCommand for AttmanPickCommand {
fn name(&self) -> &'static str {
"BATTMAN"
}
fn prompt(&self) -> String {
"Select a block:".into()
}
fn needs_entity_pick(&self) -> bool {
true
}
fn entity_pick_highlights_hover(&self) -> bool {
true
}
fn on_entity_pick(&mut self, handle: codec::Handle, _pt: glam::DVec3) -> crate::command::CmdResult {
if handle.is_null() {
return crate::command::CmdResult::NeedPoint;
}
crate::command::CmdResult::Dispatch(format!("_ATTMAN_PICK {:X}", handle.value()))
}
fn on_point(&mut self, _pt: glam::DVec3) -> crate::command::CmdResult {
crate::command::CmdResult::NeedPoint
}
fn on_enter(&mut self) -> crate::command::CmdResult {
crate::command::CmdResult::Dispatch("_ATTMAN_PICK".into())
}
fn on_escape(&mut self) -> crate::command::CmdResult {
crate::command::CmdResult::Dispatch("_ATTMAN_PICK".into())
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
mod attdef;
pub(crate) mod attedit;
mod attman;
pub(crate) mod attman;
pub mod base_point;
pub(crate) mod block_cmdline;
mod content_browser;
File diff suppressed because it is too large. Load diff
+1
View File
@@ -2,6 +2,7 @@ pub mod about;
pub mod alias_editor;
pub mod annotation_data;
pub mod attdef_dialog;
pub mod attman_dialog;
pub mod attribute_editor;
pub mod authoring_palettes;
pub mod auto_constrain_settings;