Files
OneCAD-Draft/src/app/commands/field.rs
T
Claude bf1c4ddc74 style: apply cargo fmt across workspace (as on main)
Formats the branch the way main's 9cf9ff2 did, so merging main only
conflicts where the code itself differs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
2026-10-11 18:29:37 +00:00

520 lines
21 KiB
Rust

//! FIELD and the Field dialog behind the ƒ buttons of the attribute
//! definition dialogs.
use crate::app::{Message, OpenCADStudio};
use crate::command::CadCommand;
use crate::modules::annotate::field_cmd::{
FieldObjectPickCommand, FieldPlaceCommand, FieldTablePickCommand,
};
use crate::ui::window::field_dialog::{
fields_of, nav_props, object_properties, FieldDialogMsg, FieldDialogState, FieldTarget,
DATE_FORMATS, NAMED_TYPES,
};
use iced::Task;
impl OpenCADStudio {
pub(super) fn dispatch_field(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
match cmd {
"FIELD" => {
self.open_field_dialog(FieldTarget::NewText);
Some(Task::none())
}
// FIELDEVAL — the events that update fields, kept in the drawing.
cmd if cmd == "FIELDEVAL"
|| cmd.starts_with("FIELDEVAL ")
|| cmd.starts_with("SETVAR FIELDEVAL") =>
{
let value = cmd
.trim_start_matches("SETVAR ")
.trim_start_matches("FIELDEVAL")
.trim();
let current = crate::entities::field::fieldeval(&self.tabs[i].scene.document);
let ask = |app: &mut Self| {
app.command_line
.push_output(&format!("Enter new value for FIELDEVAL <{current}>:"));
app.pending_setvar = Some("FIELDEVAL".into());
};
match value.parse::<i32>() {
_ if value.is_empty() => ask(self),
Ok(n) if (0..=31).contains(&n) => {
if n != current {
self.push_undo_snapshot(i, "FIELDEVAL");
crate::io::set_drawing_variable(
&mut self.tabs[i].scene.document,
"FIELDEVAL",
&n.to_string(),
);
self.tabs[i].dirty = true;
}
}
_ => {
let (min, max) = (0, 31);
self.command_line.push_error(&crate::tf!(
"Requires an integer between {min} and {max}."
));
ask(self);
}
}
Some(Task::none())
}
cmd if cmd == "_FIELD_OBJECT" || cmd.starts_with("_FIELD_OBJECT ") => {
let handle = cmd
.split_whitespace()
.nth(1)
.and_then(|h| u64::from_str_radix(h, 16).ok())
.map(codec::Handle::new);
if let Some(state) = self.field_dialog.as_mut() {
if let Some(entity) =
handle.and_then(|h| self.tabs[i].scene.document.get_entity(h))
{
let props = object_properties(entity);
state.object = Some((
entity.common().handle,
crate::t!(crate::entities::names::ui_name(entity)).into_owned(),
));
state.object_prop = props.first().copied();
state.object_props = props;
}
self.active_modal = Some(crate::app::ModalKind::Field);
self.refresh_field_preview();
}
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}\""), &[]))
.collect();
if let Some(db) = self.sheet_set.db() {
let custom = codec::sheet_set::custom_properties(db.sheet_set());
let names = |flag: i32| {
custom
.iter()
.filter(|(_, _, f)| f & flag != 0)
.map(|(n, _, _)| n.clone())
.collect()
};
state.ss_sheet_custom = names(codec::sheet_set::CUSTOM_SHEET_PROP);
state.ss_set_custom = names(codec::sheet_set::CUSTOM_SHEET_SET_PROP);
state.ss_set_name = db.name().to_string();
}
state.ss_sets = self.sheet_set.sets.clone();
state.ss_set = self.sheet_set.current.unwrap_or(0);
self.field_dialog = Some(state);
self.fill_named_objects();
self.refresh_field_preview();
self.active_modal = Some(crate::app::ModalKind::Field);
}
fn field_value(&self, code: &str, objects: &[codec::Handle]) -> String {
crate::entities::field::evaluate(
&self.tabs[self.active_tab].scene.document,
code,
objects,
None,
)
.unwrap_or_else(|| "----".to_string())
}
fn refresh_field_preview(&mut self) {
let Some(state) = self.field_dialog.as_ref() else {
return;
};
let (code, objects) = state.code();
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;
}
}
/// The names of the chosen named-object type, sorted.
fn fill_named_objects(&mut self) {
let Some(state) = self.field_dialog.as_mut() else {
return;
};
let document = &self.tabs[self.active_tab].scene.document;
let mut names: Vec<(String, codec::Handle)> = match NAMED_TYPES[state.named_type] {
"Block" => document
.block_records
.iter()
.filter(|b| !b.name.starts_with('*'))
.map(|b| (b.name.clone(), b.handle))
.collect(),
"Dimstyle" => document
.dim_styles
.iter()
.map(|s| (s.name.clone(), s.handle))
.collect(),
"Layer" => document
.layers
.iter()
.map(|l| (l.name.clone(), l.handle))
.collect(),
"Linetype" => document
.line_types
.iter()
.map(|l| (l.name.clone(), l.handle))
.collect(),
"Textstyle" => document
.text_styles
.iter()
.filter(|s| !s.name.is_empty())
.map(|s| (s.name.clone(), s.handle))
.collect(),
"View" => document
.views
.iter()
.map(|v| (v.name.clone(), v.handle))
.collect(),
_ => Vec::new(),
};
names.sort_by_key(|(n, _)| n.to_lowercase());
state.named_names = names;
state.named = None;
}
pub(in crate::app) fn on_field_dialog(&mut self, m: FieldDialogMsg) -> Task<Message> {
let i = self.active_tab;
let Some(state) = self.field_dialog.as_mut() else {
return Task::none();
};
match m {
FieldDialogMsg::Category(c) => {
state.category = c;
let names = fields_of(c);
if !names.contains(&state.name) {
state.name = names.first().copied().unwrap_or("Date");
}
}
FieldDialogMsg::Name(n) => {
state.name = n;
// Sheet set fields start in Title case.
state.text_case = if crate::ui::window::field_dialog::SHEET_SET_FIELDS.contains(&n) { 4 } else { 0 };
state.ss_custom.clear();
state.ss_node.clear();
state.ss_prop.clear();
// SheetSet starts on the set and its first property; SheetView on nothing.
if n == "SheetSet" {
if let Some(db) = state.ss_sets.get(state.ss_set) {
state.ss_node = db.sheet_set().id().to_string();
state.ss_prop = nav_props(db, &state.ss_node).first().map(|p| p.0.clone()).unwrap_or_default();
}
}
}
FieldDialogMsg::SsCustom(name) => state.ss_custom = name,
FieldDialogMsg::SsSet(k) => {
state.ss_set = k;
state.ss_node.clear();
state.ss_prop.clear();
}
FieldDialogMsg::SsNode(id) => {
state.ss_prop = state
.ss_sets
.get(state.ss_set)
.and_then(|db| nav_props(db, &id).first().map(|p| p.0.clone()))
.unwrap_or_default();
state.ss_node = id;
}
FieldDialogMsg::SsProp(p) => state.ss_prop = p,
FieldDialogMsg::SsHref(v) => state.ss_href = v,
FieldDialogMsg::SsBrowse => {
return Task::perform(
async {
crate::sys::file_dialog()
.set_title(crate::t!("Open Sheet Set").as_ref())
.add_filter(crate::t!("Sheet Set (*.dst)").as_ref(), &["dst", "DST"])
.pick_file()
.await
.map(|h| crate::sys::handle_path(&h))
},
|p| Message::FieldDialog(FieldDialogMsg::SsPicked(p)),
);
}
FieldDialogMsg::SsPicked(None) => {}
FieldDialogMsg::SsPicked(Some(path)) => match crate::app::commands::sheet_set::read_set(&path.to_string_lossy()) {
Ok(db) => {
let key = codec::sheet_set::path_key(&path.to_string_lossy());
let open = state
.ss_sets
.iter()
.position(|d| d.path.as_deref().is_some_and(|p| codec::sheet_set::path_key(p) == key));
state.ss_set = match open {
Some(k) => k,
None => {
state.ss_sets.push(db);
state.ss_sets.len() - 1
}
};
state.ss_node.clear();
state.ss_prop.clear();
}
Err(e) => self.command_line.push_error(&format!("{}: {e}", path.display())),
},
FieldDialogMsg::SsPlaceholder(k) => {
state.ss_placeholder = k;
state.ss_custom.clear();
}
FieldDialogMsg::SsScaleFormat(k) => state.ss_scale_format = k,
FieldDialogMsg::DateFormat(f) => state.date_format = f,
FieldDialogMsg::DateExample(k) => {
if let Some(f) = DATE_FORMATS.get(k) {
state.date_format = f.to_string();
}
}
FieldDialogMsg::TextCase(k) => state.text_case = k,
FieldDialogMsg::FileParts(b) => state.file_parts = b,
FieldDialogMsg::FileExtension(v) => state.file_extension = v,
FieldDialogMsg::SizeUnit(k) => state.size_unit = k,
FieldDialogMsg::SysVar(v) => state.sysvar = v,
FieldDialogMsg::Diesel(v) => state.diesel = v,
FieldDialogMsg::NamedType(k) => {
state.named_type = k;
self.fill_named_objects();
}
FieldDialogMsg::Named(k) => state.named = Some(k),
FieldDialogMsg::ObjectProp(p) => state.object_prop = Some(p),
FieldDialogMsg::Formula(v) => state.formula = v,
FieldDialogMsg::FormulaFormat(k) => state.formula_format = k,
FieldDialogMsg::FormulaPrecision(k) => state.formula_precision = k,
FieldDialogMsg::Evaluate => {}
FieldDialogMsg::HyperlinkText(v) => state.hyperlink_text = v,
FieldDialogMsg::HyperlinkUrl(v) => state.hyperlink_url = v,
FieldDialogMsg::BrowseHyperlink => {
return Task::perform(
async {
rfd::AsyncFileDialog::new()
.set_title(crate::t!("Select a file to link").as_ref())
.pick_file()
.await
.map(|h| crate::sys::handle_path(&h))
},
|path| match path {
Some(path) => Message::FieldDialog(FieldDialogMsg::HyperlinkUrl(path.to_string_lossy().into_owned())),
None => Message::Noop,
},
);
}
FieldDialogMsg::PlotScale(k) => state.plot_scale = k,
FieldDialogMsg::CountExpression(v) => state.count_expression = v,
FieldDialogMsg::ShowCountInstances => {
// The dialog closes and count mode shows what the expression counts.
let json = state.count_expression.clone();
self.field_dialog = None;
self.close_active_modal();
self.show_count_instances(i, &json);
return Task::none();
}
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;
let command = FieldObjectPickCommand;
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
return Task::none();
}
FieldDialogMsg::Help => self.command_line.push_info(
crate::t!("Inserts a field: text that updates from the drawing, the date or another source.").as_ref(),
),
FieldDialogMsg::Ok => return self.field_dialog_ok(i),
}
self.refresh_field_preview();
Task::none()
}
fn field_dialog_ok(&mut self, i: usize) -> Task<Message> {
let Some(state) = self.field_dialog.take() else {
return Task::none();
};
let (code, objects) = state.code();
let value = state.preview.clone();
let field = Some((code, objects));
match state.target {
FieldTarget::NewText => {
self.close_active_modal();
let defaults = crate::scene::creation_style::current_text_defaults(
&self.tabs[i].scene.document,
);
let command = FieldPlaceCommand::new(
value,
field.expect("set above"),
defaults.style_name,
defaults.height,
);
self.reset_command_start_state(i);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
self.push_ucs_to_cmd(i);
return self.focus_cmd_input();
}
FieldTarget::AttdefDefault => {
if let Some(dialog) = self.attdef_dialog.as_mut() {
dialog.default = value;
dialog.field = field;
}
self.active_modal = Some(crate::app::ModalKind::AttDef);
}
FieldTarget::AttdefEdit => {
if let Some(edit) = self.attdef_edit.as_mut() {
edit.default = value;
edit.field = field;
edit.field_changed = true;
}
self.active_modal = Some(crate::app::ModalKind::AttDefEdit);
}
}
Task::none()
}
/// The dialogs a Field dialog was opened from come back when it closes.
pub(in crate::app) fn close_field_dialog(&mut self) {
let target = self.field_dialog.take().map(|s| s.target);
self.close_active_modal();
self.active_modal = match target {
Some(FieldTarget::AttdefDefault) if self.attdef_dialog.is_some() => {
Some(crate::app::ModalKind::AttDef)
}
Some(FieldTarget::AttdefEdit) if self.attdef_edit.is_some() => {
Some(crate::app::ModalKind::AttDefEdit)
}
_ => None,
};
}
}
/// 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(),
}
}