Merge PR #1590 from ramox81/codex/field-table-placeholders: table formula buttons, block placeholders, attribute fields on insert, plot-time fields

The PR pinned the codec to its fork through [patch]; codec #81 is merged
upstream and main already points at it, so the patch section is dropped.
This commit is contained in:
Hakan Seven committed 2026-10-02 20:09:27 +03:00
commit 29ed1cae9b
29 files changed
+524 -12

No files matched your search

+20 -2
View File
@@ -231,7 +231,15 @@ impl OpenCADStudio {
// objects are not part of the add delta, so the step is a snapshot.
let text_field = self.tabs[i].active_cmd.as_ref().and_then(|cmd| cmd.text_field());
let label = self.history_label_from_active_cmd(i, "ENTITY");
let delta_safe = text_field.is_none() && self.delta_add_safe(i, &entity);
// A block reference whose attribute definitions carry fields gets
// them copied onto its attributes (field objects: snapshot step).
let attribute_fields = match &entity {
codec::EntityType::Insert(insert) if !insert.attributes.is_empty() => {
crate::entities::field::block_has_attribute_fields(&self.tabs[i].scene.document, &insert.block_name)
}
_ => false,
};
let delta_safe = text_field.is_none() && !attribute_fields && self.delta_add_safe(i, &entity);
let field_value = match &entity {
codec::EntityType::MText(m) => m.value.clone(),
codec::EntityType::AttributeDefinition(a) => a.default_value.clone(),
@@ -264,6 +272,10 @@ impl OpenCADStudio {
&field_value,
);
}
if let (Some(handle), true) = (committed, attribute_fields) {
crate::entities::field::attach_attribute_fields(&mut self.tabs[i].scene.document, handle);
self.tabs[i].scene.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
}
if let Some(handle) = committed {
self.apply_continuous_constraints(i, &[handle]);
}
@@ -972,7 +984,7 @@ impl OpenCADStudio {
let i = self.active_tab;
// Collect the full AttributeDefinitions owned by this block
// record so each created attribute keeps its geometry (#255).
let attdefs: Vec<codec::entities::AttributeDefinition> = {
let mut attdefs: Vec<codec::entities::AttributeDefinition> = {
let doc = &self.tabs[i].scene.document;
if let Some(br) = doc.block_records.get(&block_name) {
br.entity_handles
@@ -992,6 +1004,12 @@ impl OpenCADStudio {
}
};
// ATTREQ 0: attributes take their defaults without prompting.
if !self.tabs[i].scene.document.header.attribute_request {
for attdef in &mut attdefs {
attdef.flags.preset = true;
}
}
if attdefs.is_empty() {
// No attribute definitions — commit the INSERT directly.
let entity = self.tabs[i]
+118 -2
View File
@@ -3,7 +3,7 @@
use crate::app::{Message, OpenCADStudio};
use crate::command::CadCommand;
use crate::modules::annotate::field_cmd::{FieldObjectPickCommand, FieldPlaceCommand};
use crate::modules::annotate::field_cmd::{FieldObjectPickCommand, FieldPlaceCommand, FieldTablePickCommand};
use crate::ui::window::field_dialog::{
fields_of, object_properties, FieldDialogMsg, FieldDialogState, FieldTarget, DATE_FORMATS,
NAMED_TYPES,
@@ -35,13 +35,70 @@ impl OpenCADStudio {
}
Some(Task::none())
}
cmd if cmd == "_FIELD_CELL" || cmd.starts_with("_FIELD_CELL ") => {
let parts: Vec<&str> = cmd.split_whitespace().skip(1).collect();
if let Some(expression) = self.table_cell_expression(i, &parts) {
if let Some(state) = self.field_dialog.as_mut() {
// The reference appends at the end, with no operator.
state.formula.push_str(&expression);
}
self.refresh_field_preview();
// A real result gets the Decimal format, as the reference picks it.
if let Some(state) = self.field_dialog.as_mut() {
if state.formula_format == 0 && state.preview.contains('.') {
state.formula_format = 2;
}
}
}
if self.field_dialog.is_some() {
self.active_modal = Some(crate::app::ModalKind::Field);
self.refresh_field_preview();
}
Some(Task::none())
}
_ => None,
}
}
/// The formula text for a picked table cell or cell range:
/// `Table(%<\_ObjId H>%).B4` or `Table(%<\_ObjId H>%).Evaluate(Sum(A3:B5))`.
fn table_cell_expression(&self, i: usize, parts: &[&str]) -> Option<String> {
let function = *parts.first()?;
let cell = |at: &[&str]| -> Option<(codec::Handle, usize, usize)> {
let handle = codec::Handle::new(u64::from_str_radix(at.first()?, 16).ok()?);
let point = glam::DVec3::new(at.get(1)?.parse().ok()?, at.get(2)?.parse().ok()?, at.get(3)?.parse().ok()?);
let document = &self.tabs[i].scene.document;
let codec::EntityType::Table(table) = document.get_entity(handle)? else {
return None;
};
let hit = crate::modules::annotate::table_cmd::table_cell_at(table, None, point)?;
Some((handle, hit.row, hit.column))
};
let name = |row: usize, column: usize| format!("{}{}", column_letters(column), row + 1);
let (table, row, column) = cell(parts.get(1..5)?)?;
if function == "Cell" {
return Some(format!("Table(%<\\_ObjId {:X}>%).{}", table.value(), name(row, column)));
}
let (second_table, row2, column2) = cell(parts.get(5..9)?)?;
if second_table != table {
return None;
}
Some(format!(
"Table(%<\\_ObjId {:X}>%).Evaluate({function}({}:{}))",
table.value(),
name(row.min(row2), column.min(column2)),
name(row.max(row2), column.max(column2)),
))
}
/// Open the Field dialog for `target`.
pub(in crate::app) fn open_field_dialog(&mut self, target: FieldTarget) {
let mut state = FieldDialogState::new(target);
if !matches!(target, FieldTarget::NewText) {
state.placeholder_block = self.tabs[self.active_tab]
.active_block_edit_session()
.map(|session| session.block_name.clone());
}
state.examples = DATE_FORMATS
.iter()
.map(|f| self.field_value(&format!("\\AcVar Date \\f \"{f}\""), &[]))
@@ -62,7 +119,17 @@ impl OpenCADStudio {
return;
};
let (code, objects) = state.code();
let preview = if state.complete() { self.field_value(&code, &objects) } else { String::new() };
let preview = if state.complete() {
if state.name == "BlockPlaceholder" {
// A placeholder shows its property name until a block reference resolves it.
let (label, _, _) = codec::fields::BLOCK_PLACEHOLDER_PROPERTIES[state.placeholder_property];
text_case(label, state.text_case)
} else {
self.field_value(&code, &objects)
}
} else {
String::new()
};
if let Some(state) = self.field_dialog.as_mut() {
state.preview = preview;
}
@@ -155,6 +222,18 @@ impl OpenCADStudio {
);
}
FieldDialogMsg::PlotScale(k) => state.plot_scale = k,
FieldDialogMsg::PlaceholderProperty(k) => {
state.placeholder_property = k;
state.text_case = 0;
}
FieldDialogMsg::TableFunction(function) => {
// The dialog waits while the cells are picked.
self.active_modal = None;
let command = FieldTablePickCommand::new(function);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
return Task::none();
}
FieldDialogMsg::SelectObject => {
// The dialog waits while one object is picked.
self.active_modal = None;
@@ -226,3 +305,40 @@ impl OpenCADStudio {
};
}
}
/// Spreadsheet column letters: A … Z, AA, AB …
fn column_letters(mut column: usize) -> String {
let mut letters = Vec::new();
loop {
letters.push(b'A' + (column % 26) as u8);
if column < 26 {
break;
}
column = column / 26 - 1;
}
letters.reverse();
String::from_utf8(letters).unwrap_or_default()
}
/// The Field dialog's text cases: (none), Uppercase, Lowercase, First capital, Title case.
fn text_case(value: &str, case: usize) -> String {
match case {
1 => value.to_uppercase(),
2 => value.to_lowercase(),
3 => {
let lower = value.to_lowercase();
let mut chars = lower.chars();
chars.next().map(|c| c.to_uppercase().chain(chars).collect()).unwrap_or_default()
}
4 => value
.split(' ')
.map(|word| {
let lower = word.to_lowercase();
let mut chars = lower.chars();
chars.next().map(|c| c.to_uppercase().chain(chars).collect::<String>()).unwrap_or_default()
})
.collect::<Vec<_>>()
.join(" "),
_ => value.to_string(),
}
}
+8
View File
@@ -475,6 +475,7 @@ impl OpenCADStudio {
if !path.to_ascii_lowercase().ends_with(".pdf") {
return Err(failure("invalid_path", "plot writes .pdf files"));
}
self.stamp_plot_fields();
let i = self.active_tab;
let names = self.tabs[i].scene.layout_names();
let requested = req["layout"].as_str().unwrap_or("Model").to_owned();
@@ -1132,6 +1133,13 @@ impl OpenCADStudio {
"link_text" => F::HyperlinkText(v.into()),
"link_url" => F::HyperlinkUrl(v.into()),
"plot_scale" => F::PlotScale(index(codec::fields::PLOT_SCALE_FORMATS.len())?),
"placeholder" => F::PlaceholderProperty(index(codec::fields::BLOCK_PLACEHOLDER_PROPERTIES.len())?),
"table_function" => F::TableFunction(
["Average", "Sum", "Count", "Cell"]
.into_iter()
.find(|f| f.eq_ignore_ascii_case(v))
.ok_or_else(|| failure("invalid_value", "Average|Sum|Count|Cell"))?,
),
_ => return Err(failure("invalid_value", "Unknown field dialog key")),
})
}
+16
View File
@@ -3764,7 +3764,20 @@ impl OpenCADStudio {
task
}
/// A plot gives its fields (PlotDate) the plot time, as the reference
/// stores it; the hosts are redrawn before the plot reads the scene.
pub(in crate::app) fn stamp_plot_fields(&mut self) {
let i = self.active_tab;
let hosts = crate::entities::field::stamp_plot_fields(&mut self.tabs[i].scene.document);
if !hosts.is_empty() {
let changes: Vec<_> = hosts.into_iter().map(|h| (h, crate::scene::ChangeKind::Modified)).collect();
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
}
}
fn print_all_pages(&mut self) -> Result<Vec<crate::io::pdf_export::PdfPageInput>, String> {
self.stamp_plot_fields();
let available = self.tabs[self.active_tab].scene.layout_names();
let selected: Vec<String> = self
.print_all_layouts
@@ -5337,6 +5350,9 @@ impl OpenCADStudio {
}
self.active_modal = None;
self.reset_modal_geometry();
if !preview {
self.stamp_plot_fields();
}
// Extents, Window and Display use one plot path in both spaces. Only
// Paper-space Layout is special: it uses the physical sheet bounds.