forked from 1b-agent/opencadstudio-upstream
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:
commit
29ed1cae9b
29 files changed
+524
-12
No files matched your search
@@ -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
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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")),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user