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

+14 -4
View File
@@ -188,12 +188,14 @@ const PARAMETER_SETS: &[(&str, &[u8])] = &[
];
/// Catalog rows remain inert until their authoring commands exist.
#[allow(clippy::too_many_arguments)]
fn catalog(
tab: Tab,
visibility_error: Option<&'static str>,
point_error: Option<&'static str>,
flip_error: Option<&'static str>,
lookup_error: Option<&'static str>,
properties_table_error: Option<&'static str>,
move_error: Option<&'static str>,
scale_error: Option<&'static str>,
rotate_error: Option<&'static str>,
@@ -222,15 +224,16 @@ fn catalog(
let orientation_error = if *label=="Flip" {flip_error} else {point_error};
let single=tab==Tab::Parameters&&matches!(*label,"Lookup"|"Basepoint");
let lookup_action=tab==Tab::Actions&&*label=="Lookup";
let properties_table_action=tab==Tab::Actions&&*label=="Block Properties Table";
let geometry_action=tab==Tab::Actions&&matches!(*label,"Move"|"Scale"|"Stretch"|"Polar Stretch"|"Rotate"|"Flip"|"Array");
let action_error=match *label {"Scale"=>scale_error,"Stretch"=>stretch_action_error,"Polar Stretch"=>polar_stretch_action_error,"Rotate"=>rotate_error,"Flip"=>flip_action_error,"Array"=>array_action_error,_=>move_error};
let enabled = (geometry_action&&action_error.is_none())||(single&&point_error.is_none())||(lookup_action&&lookup_error.is_none())||(orientation && orientation_error.is_none()) || (visibility && visibility_error.is_none()) || ((point || distance) && point_error.is_none());
let enabled = (geometry_action&&action_error.is_none())||(single&&point_error.is_none())||(lookup_action&&lookup_error.is_none())||(properties_table_action&&properties_table_error.is_none())||(orientation && orientation_error.is_none()) || (visibility && visibility_error.is_none()) || ((point || distance) && point_error.is_none());
let base = if enabled {
icons::semantic(icon, 28.0)
} else {
icons::semantic_disabled(icon, 28.0)
};
let icon = if tab != Tab::Parameters && *label != "Block Properties Table" {
let icon = if tab != Tab::Parameters {
stack![
base,
container(if enabled {
@@ -248,8 +251,8 @@ fn catalog(
base
};
let message = enabled.then(|| Message::RibbonToolClick {
tool_id: if geometry_action {format!("BACTION_{}",label.to_ascii_uppercase())}else if lookup_action {"BLOOKUPTABLE".into()}else{format!("BPARAMETER_{}", label.to_ascii_uppercase())},
event: crate::modules::ModuleEvent::Command(if geometry_action {format!("BACTION {}",label.to_ascii_uppercase())}else if lookup_action {"BLOOKUPTABLE".into()}else{format!("BPARAMETER {}", label.to_ascii_uppercase())}),
tool_id: if geometry_action {format!("BACTION_{}",label.to_ascii_uppercase())}else if lookup_action {"BLOOKUPTABLE".into()}else if properties_table_action {"BPROPERTIESTABLE".into()}else{format!("BPARAMETER_{}", label.to_ascii_uppercase())},
event: crate::modules::ModuleEvent::Command(if geometry_action {format!("BACTION {}",label.to_ascii_uppercase())}else if lookup_action {"BLOOKUPTABLE".into()}else if properties_table_action {"BPROPERTIESTABLE".into()}else{format!("BPARAMETER {}", label.to_ascii_uppercase())}),
});
let description = if visibility {
crate::t!(visibility_error
@@ -272,6 +275,8 @@ fn catalog(
crate::t!(action_error.unwrap_or(if *label=="Stretch" {"Select a parameter endpoint, define the stretch frame, select its objects, then place the action marker."}else if *label=="Array" {"Select a parameter endpoint, select the objects to array, specify the column offset, then place the action marker."}else{"Select a parameter endpoint, select the objects it controls, then place the action marker. Use the reference parameter grip in the drawing or Test Block."})).into_owned()
} else if lookup_action {
crate::t!(lookup_error.unwrap_or("Create or edit property Lookup tables for this block.")).into_owned()
} else if properties_table_action {
crate::t!(properties_table_error.unwrap_or("Create or edit this block's properties table; rows bind Linear and Polar parameters.")).into_owned()
} else {
unavailable.to_string()
};
@@ -290,6 +295,8 @@ fn catalog(
container(entry).id(match *label {"Move"=>"authoring-tool-move-action","Scale"=>"authoring-tool-scale-action","Rotate"=>"authoring-tool-rotate-action","Stretch"=>"authoring-tool-stretch-action","Array"=>"authoring-tool-array-action",_=>"authoring-tool-flip-action"}).into()
} else if lookup_action {
container(entry).id("authoring-tool-lookup-table").into()
} else if properties_table_action {
container(entry).id("authoring-tool-properties-table").into()
} else {
entry
}
@@ -396,6 +403,7 @@ fn constraints() -> Element<'static, Message> {
.into()
}
#[allow(clippy::too_many_arguments)]
pub fn view(
selected_tab: Tab,
width: f32,
@@ -404,6 +412,7 @@ pub fn view(
point_error: Option<&'static str>,
flip_error: Option<&'static str>,
lookup_error: Option<&'static str>,
properties_table_error: Option<&'static str>,
move_error: Option<&'static str>,
scale_error: Option<&'static str>,
rotate_error: Option<&'static str>,
@@ -466,6 +475,7 @@ pub fn view(
point_error,
flip_error,
lookup_error,
properties_table_error,
move_error,
scale_error,
rotate_error,
+186
View File
@@ -0,0 +1,186 @@
use crate::app::Message;
use crate::scene::block_properties_table::{Column, TableView};
use codec::Handle;
use iced::{
widget::{button, column, container, pick_list, row, scrollable, text, text_input, Space},
Element, Fill,
};
#[derive(Debug, Clone)]
pub enum Action {
Property(usize),
AddProperty,
RemoveProperty(usize),
Cell(usize, usize, String),
AddRow,
DeleteRow(usize),
Apply,
}
pub(crate) struct State {
pub document: u64,
pub definition: Handle,
pub epoch: u64,
pub original: Option<TableView>,
pub columns: Vec<Column>,
pub properties: Vec<Column>,
pub selected_property: Option<usize>,
pub rows: Vec<Vec<String>>,
pub error: Option<String>,
pub notice: Option<String>,
}
impl State {
pub(crate) fn edit(&mut self, action: Action) {
self.error = None;
self.notice = None;
match action {
Action::Property(index) if index < self.properties.len() => {
self.selected_property = Some(index);
return;
}
_ => {}
}
match action {
Action::AddProperty => {
let Some(property) = self.selected_property.and_then(|i| self.properties.get(i))
else {
return;
};
if self.columns.len() >= 64
|| self.columns.iter().any(|c| {
c.node_id == property.node_id
&& c.connection_name == property.connection_name
})
{
return;
}
for row in &mut self.rows {
row.push("0".into());
}
self.columns.push(property.clone());
self.selected_property = None;
}
Action::RemoveProperty(index)
if self.original.is_none() && index < self.columns.len() =>
{
self.columns.remove(index);
for row in &mut self.rows {
if index < row.len() {
row.remove(index);
}
}
}
Action::Cell(row, col, value)
if row < self.rows.len() && col < self.columns.len() && col < self.rows[row].len() =>
{
self.rows[row][col] = value;
}
Action::AddRow if !self.columns.is_empty() && self.rows.len() < 1000 => {
self.rows.push(vec!["0".into(); self.columns.len()]);
}
Action::DeleteRow(row) if row < self.rows.len() => {
self.rows.remove(row);
}
_ => {}
}
}
}
pub(crate) fn view(state: &State, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
let send = |a| Message::BlockPropertiesTable(a);
let cols = state.columns.len();
let rows = state.rows.len();
let mut headings = row![text(" ").width(28)].spacing(6);
for (index, column_binding) in state.columns.iter().enumerate() {
let mut header = column![text(column_binding.connection_name.clone()).size(13)];
if state.original.is_none() {
header = header.push(
button(text(crate::t!("Remove").into_owned()).size(11))
.on_press(send(Action::RemoveProperty(index))),
);
}
headings = headings.push(container(header.spacing(4)).width(160));
}
let mut grid = column![headings].spacing(7);
for (row_index, row_values) in state.rows.iter().enumerate() {
let mut cells = row![text((row_index + 1).to_string()).width(28)].spacing(6);
for col_index in 0..cols {
cells = cells.push(
text_input("", row_values.get(col_index).map_or("", String::as_str))
.on_input(move |v| send(Action::Cell(row_index, col_index, v)))
.width(160)
.style(crate::ui::style::form::field_style),
);
}
cells = cells.push(
button(text(crate::t!("Delete").into_owned()))
.on_press(send(Action::DeleteRow(row_index))),
);
grid = grid.push(cells);
}
let available = state
.properties
.iter()
.enumerate()
.filter(|(_, p)| {
!state
.columns
.iter()
.any(|c| c.node_id == p.node_id && c.connection_name == p.connection_name)
})
.map(|(i, _)| i)
.collect::<Vec<_>>();
container(
column![
row![
pick_list(state.selected_property, available, |i: &usize| state
.properties[*i]
.connection_name
.clone())
.on_select(move |i| send(Action::Property(i)))
.placeholder(crate::t!("Choose input property").into_owned())
.width(Fill),
button(text(crate::t!("Add Property").into_owned()))
.on_press_maybe(state.selected_property.map(|_| send(Action::AddProperty)))
]
.spacing(8),
text(
crate::t!("Bind Linear or Polar parameters. Existing table columns are retained.")
.into_owned()
)
.size(12),
scrollable(grid)
.direction(scrollable::Direction::Both {
vertical: Default::default(),
horizontal: Default::default()
})
.height(Fill)
.width(Fill),
row![
button(text(crate::t!("Add Row").into_owned())).on_press_maybe(
(cols > 0 && rows < 1000).then_some(send(Action::AddRow))
),
Space::new().width(Fill)
]
.spacing(8),
text(state.error.clone().unwrap_or_default())
.size(12)
.style(text::danger),
text(state.notice.clone().unwrap_or_default()).size(12),
row![
Space::new().width(Fill),
button(text(crate::t!("OK").into_owned()))
.on_press(send(Action::Apply))
.style(button::primary),
button(text(crate::t!("Cancel").into_owned())).on_press(Message::CloseModal)
]
.spacing(8),
]
.spacing(12),
)
.padding(16)
.width(sizing.width)
.height(sizing.height)
.into()
}
+1
View File
@@ -12,6 +12,7 @@ pub mod block_palette;
pub mod block_save_as;
pub mod block_visibility;
pub mod block_lookup;
pub mod block_properties_table;
pub mod browser;
pub mod count_palette;
pub mod drafting_settings;