feat(blocks): author Block Properties Table from the block editor

Add the BPROPERTIESTABLE command, the Block Authoring palette row and
the grid dialog. The row needs an open block edit session with a Linear
or Polar parameter column; OK parses cells to finite numbers, re-checks
the table against the session it was opened from, writes it in one undo
step, and no-op edits close without an undo.
This commit is contained in:
Karim Jerbi committed 2026-10-18 08:11:41 +01:00
1 parent 2b0c1a6b58
commit a1de7e4ecd
13 files changed
+865 -4

No files matched your search

+127
View File
@@ -0,0 +1,127 @@
use super::{Message, ModalKind, OpenCADStudio};
use crate::scene::block_properties_table as table;
use crate::ui::window::block_properties_table::{Action, State};
use iced::Task;
impl OpenCADStudio {
pub(in crate::app) fn properties_table_error(&self) -> Option<&'static str> {
let tab = &self.tabs[self.active_tab];
let Some(session) = tab.active_block_edit_session() else {
return Some("Open a block in the Block Editor before editing its Block Properties Table.");
};
let definition = session.br_handle;
if table::columns(&tab.scene.document, definition).is_empty() {
return Some(
"Place a Linear or Polar parameter before editing the Block Properties Table.",
);
}
table::table(&tab.scene.document, definition).err()
}
pub(in crate::app) fn open_block_properties_table(&mut self) -> Task<Message> {
if let Some(error) = self.properties_table_error() {
self.command_line.push_error(crate::t!(error).as_ref());
return Task::none();
}
let tab = &self.tabs[self.active_tab];
let definition = tab.active_block_edit_session().unwrap().br_handle;
let original = match table::table(&tab.scene.document, definition) {
Ok(value) => value,
Err(error) => {
self.command_line.push_error(crate::t!(error).as_ref());
return Task::none();
}
};
let (_, columns, rows) = match table::grid_table(&tab.scene.document, definition) {
Ok(Some(value)) => value,
Ok(None) => (codec::Handle::NULL, vec![], vec![]),
Err(error) => {
self.command_line.push_error(crate::t!(error).as_ref());
return Task::none();
}
};
self.block_properties_table = Some(State {
document: tab.id,
definition,
epoch: tab.scene.geometry_epoch,
original,
columns,
properties: table::columns(&tab.scene.document, definition),
selected_property: None,
rows,
error: None,
notice: None,
});
self.active_modal = Some(ModalKind::BlockPropertiesTable);
Task::none()
}
pub(super) fn handle_block_properties_table(&mut self, action: Action) -> Task<Message> {
if !matches!(action, Action::Apply) {
if let Some(state) = self.block_properties_table.as_mut() {
state.edit(action);
}
return Task::none();
}
let Some(state) = self.block_properties_table.as_ref() else {
return Task::none();
};
let mut cells = Vec::with_capacity(state.rows.len());
for row in &state.rows {
let mut parsed = Vec::with_capacity(row.len());
for value in row {
match value.trim().parse::<f64>() {
Ok(value) if value.is_finite() => parsed.push(value),
_ => {
self.block_properties_table.as_mut().unwrap().error = Some(
crate::t!("Each cell must be a finite number.").into_owned(),
);
return Task::none();
}
}
}
cells.push(parsed);
}
let i = self.active_tab;
let tab = &self.tabs[i];
let stale = tab.id != state.document
|| tab.active_block_edit_session().map(|s| s.br_handle) != Some(state.definition)
|| tab.scene.geometry_epoch != state.epoch
|| table::table(&tab.scene.document, state.definition)
.ok()
.flatten()
.as_ref()
!= state.original.as_ref();
if stale {
self.block_properties_table.as_mut().unwrap().error = Some(
crate::t!("The block changed. Cancel and reopen the Block Properties Table.")
.into_owned(),
);
return Task::none();
}
let unchanged = state.original.as_ref().is_some_and(|view| {
view.columns == state.columns
&& view.rows.iter().eq(cells.iter())
});
if unchanged {
self.block_properties_table = None;
self.active_modal = None;
return Task::none();
}
let mut working = tab.scene.document.clone();
if let Err(error) =
table::apply(&mut working, state.definition, &state.columns, &cells)
{
self.block_properties_table.as_mut().unwrap().error =
Some(crate::t!(error).into_owned());
return Task::none();
}
self.push_undo_snapshot(i, "Block Properties Table");
self.tabs[i].scene.document = working;
self.tabs[i].scene.bump_geometry();
self.tabs[i].dirty = true;
self.finish_pending_history(i);
self.refresh_properties();
self.block_properties_table = None;
self.active_modal = None;
Task::none()
}
}
+3
View File
@@ -412,6 +412,9 @@ impl OpenCADStudio {
cmd if cmd == "BLOOKUPTABLE" || cmd.starts_with("BLOOKUPTABLE ") => {
Some(self.open_lookup_table(cmd.strip_prefix("BLOOKUPTABLE").unwrap().trim()))
}
cmd if cmd == "BPROPERTIESTABLE" || cmd.starts_with("BPROPERTIESTABLE ") => {
Some(self.open_block_properties_table())
}
cmd if cmd == "BPARAMETER" || cmd.starts_with("BPARAMETER ") => {
let Some(block) = self.visibility_block(i, "BPARAMETER") else {
return Some(Task::none());
+6
View File
@@ -218,6 +218,12 @@ impl OpenCADStudio {
return Task::none();
}
}
if cmd == "BPROPERTIESTABLE" || cmd.starts_with("BPROPERTIESTABLE ") {
if let Some(error) = self.properties_table_error() {
self.command_line.push_error(crate::t!(error).as_ref());
return Task::none();
}
}
if self.tabs[i].block_test.is_some() {
let name = cmd.split([' ', ':']).next().unwrap_or(cmd);
if matches!(
+77
View File
@@ -1456,6 +1456,83 @@ fn lookup_basepoint_palette_and_table_dialog_real_controls() {
});
}
#[test]
fn block_properties_table_palette_dialog_authors_rows() {
with_stack(|| {
use crate::ui::window::{authoring_palettes::Tab, block_properties_table::Action as A};
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 1,0 0,0.3");
let point = authoring_control(&app, Tab::Actions.widget_id());
authoring_click(&mut app, point);
let point = authoring_control(&app, "authoring-tool-properties-table");
authoring_click(&mut app, point);
assert_eq!(
app.active_modal,
Some(crate::app::ModalKind::BlockPropertiesTable)
);
let _ = app.update(Message::BlockPropertiesTable(A::Property(0)));
let point = find_in_view(&app, "Add Property");
authoring_click(&mut app, point);
let point = find_in_view(&app, "Add Row");
authoring_click(&mut app, point);
assert_eq!(
app.block_properties_table.as_ref().unwrap().rows.len(),
1
);
let _ = app.update(Message::BlockPropertiesTable(A::Cell(0, 0, "2.5".into())));
let point = find_in_view(&app, "OK");
authoring_click(&mut app, point);
assert!(app.block_properties_table.is_none());
assert!(app.active_modal.is_none());
let i = app.active_tab;
let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle;
let view =
crate::scene::block_properties_table::table(&app.tabs[i].scene.document, definition)
.unwrap()
.expect("the table was written");
assert_eq!(view.columns.len(), 1);
assert_eq!(view.rows, vec![vec![2.5]]);
});
}
#[test]
fn block_properties_table_apply_writes_undo_once_and_stale_edits_stay_open() {
with_stack(|| {
use crate::ui::window::block_properties_table::Action as A;
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 1,0 0,0.3");
let i = app.active_tab;
let history = app.tabs[i].history.undo_stack.len();
let _ = app.dispatch_command("BPROPERTIESTABLE");
assert_eq!(
app.active_modal,
Some(crate::app::ModalKind::BlockPropertiesTable)
);
let _ = app.update(Message::BlockPropertiesTable(A::Property(0)));
let _ = app.update(Message::BlockPropertiesTable(A::AddProperty));
let _ = app.update(Message::BlockPropertiesTable(A::AddRow));
let _ = app.update(Message::BlockPropertiesTable(A::Apply));
assert!(app.block_properties_table.is_none());
assert_eq!(app.tabs[i].history.undo_stack.len(), history + 1);
let _ = app.dispatch_command("BPROPERTIESTABLE");
assert!(app.block_properties_table.is_some());
let _ = app.update(Message::BlockPropertiesTable(A::Apply));
assert!(app.block_properties_table.is_none());
assert_eq!(app.tabs[i].history.undo_stack.len(), history + 1);
let _ = app.dispatch_command("BPROPERTIESTABLE");
app.tabs[i].scene.bump_geometry();
let _ = app.update(Message::BlockPropertiesTable(A::AddRow));
let _ = app.update(Message::BlockPropertiesTable(A::Apply));
assert!(app.block_properties_table.is_some());
assert!(app.block_properties_table.as_ref().unwrap().error.is_some());
let _ = app.update(Message::CloseModal);
assert!(app.block_properties_table.is_none());
assert!(app.active_modal.is_none());
});
}
#[test]
fn authoring_palettes_ribbon_toggle_and_dock_close_leave_document_and_history_unchanged() {
use crate::ui::dock::{DockMsg, PanelId};
+1
View File
@@ -411,6 +411,7 @@ impl OpenCADStudio {
self.point_parameter_placement_error(),
self.parameter_placement_error("FLIP"),
self.lookup_table_error(),
self.properties_table_error(),
self.action_error(Some(crate::scene::block_actions::Kind::Move)),
self.action_error(Some(crate::scene::block_actions::Kind::Scale)),
self.action_error(Some(crate::scene::block_actions::Kind::Rotate)),
+5
View File
@@ -14,6 +14,7 @@ mod block_editor_tests;
mod block_save_as;
mod block_visibility;
mod block_lookup;
mod block_properties_table;
mod block_actions;
mod block_authoring_edit;
mod block_properties;
@@ -597,6 +598,7 @@ pub(super) struct OpenCADStudio {
block_save_as: Option<crate::ui::window::block_save_as::State>,
block_visibility: Option<crate::ui::window::block_visibility::State>,
block_lookup: Option<crate::ui::window::block_lookup::State>,
block_properties_table: Option<crate::ui::window::block_properties_table::State>,
block_visibility_mode: bool,
/// PDF dialogs' working copies; `None` while closed.
pdf_attach: Option<crate::ui::window::pdf_dialogs::PdfAttachState>,
@@ -2300,6 +2302,7 @@ pub enum ModalKind {
BlockSaveAs,
BlockVisibility,
BlockLookup,
BlockPropertiesTable,
SaveDialog,
Recovery,
/// Fonts referenced by the drawing are missing on this machine.
@@ -2955,6 +2958,7 @@ pub enum Message {
BlockSaveAsApply,
BlockVisibility(crate::ui::window::block_visibility::Action),
BlockLookup(crate::ui::window::block_lookup::Action),
BlockPropertiesTable(crate::ui::window::block_properties_table::Action),
BlockEditPickerName(String),
BlockEditPickerSelect(String),
BlockEditPickerApply,
@@ -4739,6 +4743,7 @@ impl OpenCADStudio {
block_save_as: None,
block_visibility: None,
block_lookup: None,
block_properties_table: None,
block_visibility_mode: false,
pdf_attach: None,
point_cloud_attach: None,
+5
View File
@@ -238,6 +238,7 @@ impl OpenCADStudio {
Some(BlockSaveAs) => self.block_save_as = None,
Some(BlockVisibility) => self.block_visibility = None,
Some(BlockLookup) => self.block_lookup = None,
Some(BlockPropertiesTable) => self.block_properties_table = None,
Some(PdfAttach) => self.pdf_attach = None,
Some(UnderlayLayers) => self.underlay_layers = None,
Some(PdfImportSettings) => self.pdf_import_settings = None,
@@ -9739,6 +9740,9 @@ impl OpenCADStudio {
if self.active_modal == Some(super::ModalKind::BlockLookup) {
self.block_lookup = None;
}
if self.active_modal == Some(super::ModalKind::BlockPropertiesTable) {
self.block_properties_table = None;
}
if self.active_modal == Some(super::ModalKind::BlockVisibility) {
self.block_visibility = None;
}
@@ -10764,6 +10768,7 @@ impl OpenCADStudio {
Message::BlockSaveAsApply => self.apply_block_save_as_dialog(),
Message::BlockVisibility(action) => self.handle_block_visibility(action),
Message::BlockLookup(action) => self.handle_block_lookup(action),
Message::BlockPropertiesTable(action) => self.handle_block_properties_table(action),
Message::BlockEditPickerName(name) => {
if let Some(state) = self.block_edit_picker.as_mut() {
state.name = name.clone();
+7
View File
@@ -90,6 +90,7 @@ impl OpenCADStudio {
Some(K::BlockEditPicker) => crate::t!("Edit Block Definition").into_owned(),
Some(K::BlockVisibility) => crate::t!("Visibility States").into_owned(),
Some(K::BlockLookup) => crate::t!("Property Lookup Table").into_owned(),
Some(K::BlockPropertiesTable) => crate::t!("Block Properties Table").into_owned(),
Some(K::BlockSaveAs) => crate::t!("Save Block As").into_owned(),
Some(K::PointStyle) => crate::tr!("modal", "point-style"),
Some(K::AttributeEditor) => crate::tr!("modal", "attribute-editor"),
@@ -530,6 +531,12 @@ impl OpenCADStudio {
crate::ui::window::block_lookup::view(state, flow)
})
}
super::super::ModalKind::BlockPropertiesTable => {
let state = self.block_properties_table.as_ref()?;
sized_flow(ex, 940, 600, |flow| {
crate::ui::window::block_properties_table::view(state, flow)
})
}
super::super::ModalKind::BlockVisibility => {
let state = self.block_visibility.as_ref()?;
sized_flow(ex, 580, 470, |flow| {