forked from 1b-agent/opencadstudio-upstream
feat(table): complete editing workflow
This commit is contained in:
commit
f40b172eb0
14 files changed
+3406
-389
No files matched your search
+546
-64
@@ -12,60 +12,327 @@ impl OpenCADStudio {
|
||||
.push_output(crate::t!("PAN: drag with the left mouse button. Press Esc to exit.").as_ref());
|
||||
}
|
||||
|
||||
// ── TABLE cell editing ─────────────────────────────────────────────
|
||||
// TABLE CELL <row> <col> <text> — set text for a cell in the selected Table
|
||||
// ── TABLE structure and cell editing ───────────────────────────────
|
||||
cmd if cmd.starts_with("TABLE ") => {
|
||||
let rest = cmd.trim_start_matches("TABLE").trim();
|
||||
let sub_up = rest.split_whitespace().next().unwrap_or("").to_uppercase();
|
||||
if sub_up == "CELL" {
|
||||
let parts: Vec<&str> = rest.splitn(4, char::is_whitespace).collect();
|
||||
// parts: ["CELL", "<row>", "<col>", "<text>"]
|
||||
let row_res = parts.get(1).and_then(|s| s.parse::<usize>().ok());
|
||||
let col_res = parts.get(2).and_then(|s| s.parse::<usize>().ok());
|
||||
let text = parts.get(3).copied().unwrap_or("");
|
||||
match (row_res, col_res) {
|
||||
(Some(row), Some(col)) => {
|
||||
let selected_handles: Vec<acadrust::Handle> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.iter()
|
||||
.map(|(h, _)| *h)
|
||||
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
||||
.collect();
|
||||
let mut found = false;
|
||||
for sh in &selected_handles {
|
||||
if let Some(acadrust::EntityType::Table(tbl)) = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities_mut()
|
||||
.find(|e| e.common().handle == *sh)
|
||||
{
|
||||
if tbl.set_cell_text(row, col, text) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if found {
|
||||
self.push_undo_snapshot(i, "TABLE CELL");
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"TABLE CELL: set [{row},{col}] = \"{text}\"."
|
||||
).as_ref());
|
||||
} else {
|
||||
self.command_line.push_error(
|
||||
crate::t!("TABLE CELL: select a Table entity first, or row/col out of range.").as_ref()
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.command_line
|
||||
.push_info(crate::t!("Usage: TABLE CELL <row> <col> <text>").as_ref());
|
||||
#[derive(Clone)]
|
||||
enum TableAction {
|
||||
Cell(usize, usize, String),
|
||||
InsertRow(usize),
|
||||
DeleteRow(usize),
|
||||
InsertColumn(usize),
|
||||
DeleteColumn(usize),
|
||||
Merge(usize, usize, usize, usize),
|
||||
Unmerge(usize, usize),
|
||||
RowHeight(usize, f64),
|
||||
ColumnWidth(usize, f64),
|
||||
Lock(usize, usize, bool),
|
||||
Block(usize, usize, String),
|
||||
Formula(usize, usize, String),
|
||||
Field(usize, usize, acadrust::Handle),
|
||||
}
|
||||
let words: Vec<&str> = rest.split_whitespace().collect();
|
||||
let integer = |index: usize| {
|
||||
words.get(index).and_then(|value| value.parse::<usize>().ok())
|
||||
};
|
||||
let real = |index: usize| {
|
||||
words.get(index).and_then(|value| value.parse::<f64>().ok())
|
||||
};
|
||||
let action = match words.first().map(|word| word.to_ascii_uppercase()).as_deref() {
|
||||
Some("CELL") => {
|
||||
let parts: Vec<&str> = rest.splitn(4, char::is_whitespace).collect();
|
||||
match (integer(1), integer(2)) {
|
||||
(Some(row), Some(column)) => Some(TableAction::Cell(
|
||||
row,
|
||||
column,
|
||||
parts.get(3).copied().unwrap_or("").to_string(),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Some("INSERTROW") => integer(1).map(TableAction::InsertRow),
|
||||
Some("DELETEROW") => integer(1).map(TableAction::DeleteRow),
|
||||
Some("INSERTCOLUMN") | Some("INSERTCOL") => {
|
||||
integer(1).map(TableAction::InsertColumn)
|
||||
}
|
||||
Some("DELETECOLUMN") | Some("DELETECOL") => {
|
||||
integer(1).map(TableAction::DeleteColumn)
|
||||
}
|
||||
Some("MERGE") => match (integer(1), integer(2), integer(3), integer(4)) {
|
||||
(Some(r1), Some(c1), Some(r2), Some(c2)) => {
|
||||
Some(TableAction::Merge(r1, c1, r2, c2))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
Some("UNMERGE") => match (integer(1), integer(2)) {
|
||||
(Some(row), Some(column)) => Some(TableAction::Unmerge(row, column)),
|
||||
_ => None,
|
||||
},
|
||||
Some("ROWHEIGHT") => match (integer(1), real(2)) {
|
||||
(Some(row), Some(height)) if height > 0.0 => {
|
||||
Some(TableAction::RowHeight(row, height))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
Some("COLUMNWIDTH") | Some("COLWIDTH") => match (integer(1), real(2)) {
|
||||
(Some(column), Some(width)) if width > 0.0 => {
|
||||
Some(TableAction::ColumnWidth(column, width))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
Some("LOCK") => match (integer(1), integer(2), words.get(3)) {
|
||||
(Some(row), Some(column), Some(value)) => Some(TableAction::Lock(
|
||||
row,
|
||||
column,
|
||||
!value.eq_ignore_ascii_case("OFF"),
|
||||
)),
|
||||
_ => None,
|
||||
},
|
||||
Some("BLOCK") => {
|
||||
let parts: Vec<&str> = rest.splitn(4, char::is_whitespace).collect();
|
||||
match (integer(1), integer(2), parts.get(3)) {
|
||||
(Some(row), Some(column), Some(name)) if !name.trim().is_empty() => {
|
||||
Some(TableAction::Block(row, column, name.trim().to_string()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
Some("FORMULA") => {
|
||||
let parts: Vec<&str> = rest.splitn(4, char::is_whitespace).collect();
|
||||
match (integer(1), integer(2), parts.get(3)) {
|
||||
(Some(row), Some(column), Some(expression))
|
||||
if !expression.trim().is_empty() =>
|
||||
{
|
||||
Some(TableAction::Formula(
|
||||
row,
|
||||
column,
|
||||
expression.trim().to_string(),
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
Some("FIELD") => match (integer(1), integer(2), words.get(3)) {
|
||||
(Some(row), Some(column), Some(value)) => {
|
||||
u64::from_str_radix(value.trim_start_matches("0x"), 16)
|
||||
.ok()
|
||||
.map(|handle| {
|
||||
TableAction::Field(
|
||||
row,
|
||||
column,
|
||||
acadrust::Handle::new(handle),
|
||||
)
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
let Some(action) = action else {
|
||||
self.command_line.push_info(
|
||||
"Usage: TABLE (creates new table) or TABLE CELL <row> <col> <text>",
|
||||
crate::t!("Usage: TABLE CELL | INSERTROW | DELETEROW | INSERTCOL | DELETECOL | MERGE | UNMERGE | ROWHEIGHT | COLWIDTH | LOCK | BLOCK | FORMULA | FIELD").as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
};
|
||||
let selected_handles: Vec<acadrust::Handle> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.iter()
|
||||
.map(|(handle, _)| *handle)
|
||||
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
||||
.collect();
|
||||
if selected_handles.is_empty() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("TABLE: select a table first.").as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
let block_handle = if let TableAction::Block(_, _, name) = &action {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|record| record.name.eq_ignore_ascii_case(name))
|
||||
.map(|record| record.handle)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if matches!(action, TableAction::Block(_, _, _)) && block_handle.is_none() {
|
||||
self.command_line.push_error(
|
||||
crate::t!("TABLE: block definition not found.").as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
if let TableAction::Field(_, _, field_handle) = &action {
|
||||
if !self.tabs[i].scene.document.fields.contains_key(field_handle) {
|
||||
self.command_line.push_error(
|
||||
crate::t!("TABLE: field handle not found.").as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
}
|
||||
self.push_undo_snapshot(i, "TABLE EDIT");
|
||||
let mut changed = false;
|
||||
for handle in &selected_handles {
|
||||
let Some(acadrust::EntityType::Table(table)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(*handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
match &action {
|
||||
TableAction::Cell(row, column, text) => {
|
||||
if let Some(cell) = table.cell_mut(*row, *column) {
|
||||
use acadrust::entities::table::CellStateFlags;
|
||||
if !cell.state.intersects(
|
||||
CellStateFlags::CONTENT_LOCKED
|
||||
| CellStateFlags::CONTENT_READ_ONLY,
|
||||
) {
|
||||
cell.set_text(text);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
TableAction::InsertRow(row) if *row <= table.row_count() => {
|
||||
let height = table
|
||||
.rows
|
||||
.get((*row).min(table.row_count().saturating_sub(1)))
|
||||
.map(|row| row.height)
|
||||
.unwrap_or(0.5);
|
||||
table.insert_row(*row);
|
||||
table.set_row_height(*row, height);
|
||||
changed = true;
|
||||
}
|
||||
TableAction::DeleteRow(row)
|
||||
if table.row_count() > 1 && *row < table.row_count() =>
|
||||
{
|
||||
table.remove_row(*row);
|
||||
changed = true;
|
||||
}
|
||||
TableAction::InsertColumn(column) if *column <= table.column_count() => {
|
||||
let width = table
|
||||
.columns
|
||||
.get((*column).min(table.column_count().saturating_sub(1)))
|
||||
.map(|column| column.width)
|
||||
.unwrap_or(2.0);
|
||||
table.insert_column(*column, width);
|
||||
changed = true;
|
||||
}
|
||||
TableAction::DeleteColumn(column)
|
||||
if table.column_count() > 1 && *column < table.column_count() =>
|
||||
{
|
||||
table.remove_column(*column);
|
||||
changed = true;
|
||||
}
|
||||
TableAction::Merge(r1, c1, r2, c2)
|
||||
if *r1 < table.row_count()
|
||||
&& *r2 < table.row_count()
|
||||
&& *c1 < table.column_count()
|
||||
&& *c2 < table.column_count() =>
|
||||
{
|
||||
table.merge_cells(acadrust::entities::table::CellRange::new(
|
||||
(*r1).min(*r2),
|
||||
(*c1).min(*c2),
|
||||
(*r1).max(*r2),
|
||||
(*c1).max(*c2),
|
||||
));
|
||||
changed = true;
|
||||
}
|
||||
TableAction::Unmerge(row, column) => {
|
||||
table.unmerge_cell(*row, *column);
|
||||
changed = true;
|
||||
}
|
||||
TableAction::RowHeight(row, height) if *row < table.row_count() => {
|
||||
table.set_row_height(*row, *height);
|
||||
changed = true;
|
||||
}
|
||||
TableAction::ColumnWidth(column, width)
|
||||
if *column < table.column_count() =>
|
||||
{
|
||||
table.set_column_width(*column, *width);
|
||||
changed = true;
|
||||
}
|
||||
TableAction::Lock(row, column, locked) => {
|
||||
if let Some(cell) = table.cell_mut(*row, *column) {
|
||||
use acadrust::entities::table::CellStateFlags;
|
||||
cell.state.set(CellStateFlags::CONTENT_LOCKED, *locked);
|
||||
cell.state.set(CellStateFlags::FORMAT_LOCKED, *locked);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
TableAction::Block(row, column, _) => {
|
||||
if let (Some(handle), Some(cell)) =
|
||||
(block_handle, table.cell_mut(*row, *column))
|
||||
{
|
||||
use acadrust::entities::table::{
|
||||
CellContent, CellStateFlags, CellType,
|
||||
};
|
||||
if !cell.state.intersects(
|
||||
CellStateFlags::CONTENT_LOCKED
|
||||
| CellStateFlags::CONTENT_READ_ONLY,
|
||||
) {
|
||||
cell.contents.clear();
|
||||
cell.contents.push(CellContent::block(handle));
|
||||
cell.cell_type = CellType::Block;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
TableAction::Formula(row, column, expression) => {
|
||||
if let Some(cell) = table.cell_mut(*row, *column) {
|
||||
use acadrust::entities::table::CellStateFlags;
|
||||
if !cell.state.intersects(
|
||||
CellStateFlags::CONTENT_LOCKED
|
||||
| CellStateFlags::CONTENT_READ_ONLY,
|
||||
) {
|
||||
let formula = if expression.starts_with('=') {
|
||||
expression.clone()
|
||||
} else {
|
||||
format!("={expression}")
|
||||
};
|
||||
cell.set_text(&formula);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
TableAction::Field(row, column, field_handle) => {
|
||||
let mut attached = false;
|
||||
if let Some(cell) = table.cell_mut(*row, *column) {
|
||||
use acadrust::entities::table::{
|
||||
CellContent, CellStateFlags, CellType,
|
||||
};
|
||||
if !cell.state.intersects(
|
||||
CellStateFlags::CONTENT_LOCKED
|
||||
| CellStateFlags::CONTENT_READ_ONLY,
|
||||
) {
|
||||
let mut content = CellContent::text("");
|
||||
content.field_handle = Some(*field_handle);
|
||||
cell.contents.clear();
|
||||
cell.contents.push(content);
|
||||
cell.cell_type = CellType::Text;
|
||||
attached = true;
|
||||
}
|
||||
}
|
||||
if attached {
|
||||
if !table.field_handles.contains(field_handle) {
|
||||
table.field_handles.push(*field_handle);
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
self.invalidate_property_targets(i, &selected_handles);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
self.command_line
|
||||
.push_output(crate::t!("TABLE: edit applied.").as_ref());
|
||||
} else {
|
||||
self.command_line
|
||||
.push_error(crate::t!(
|
||||
"TABLE: row/column is out of range or the cell is locked."
|
||||
).as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1161,8 +1428,7 @@ impl OpenCADStudio {
|
||||
return Some(Task::done(Message::AnnoObjectScaleOpen));
|
||||
}
|
||||
|
||||
// DATALINK <path.csv> — import a CSV file into a table placed at the
|
||||
// origin (one-time import; a live re-reading link is future work).
|
||||
// DATALINK <path.csv> — create a persistent linked table.
|
||||
"DATALINK" => {
|
||||
use crate::command::ValuePromptCommand;
|
||||
let c = ValuePromptCommand::new("DATALINK", "DATALINK path to the .csv file:");
|
||||
@@ -1173,17 +1439,13 @@ impl OpenCADStudio {
|
||||
let path = cmd.trim_start_matches("DATALINK").trim();
|
||||
if path.is_empty() {
|
||||
self.command_line.push_info(
|
||||
"Usage: DATALINK <path-to-.csv> — imports the CSV into a table at the origin.",
|
||||
"Usage: DATALINK <path-to-.csv>",
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(text) => {
|
||||
let rows_data: Vec<Vec<String>> = text
|
||||
.lines()
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.map(|line| line.split(',').map(|s| s.trim().to_string()).collect())
|
||||
.collect();
|
||||
let rows_data = parse_csv_table(&text);
|
||||
let nrows = rows_data.len();
|
||||
let ncols = rows_data.iter().map(|r| r.len()).max().unwrap_or(0);
|
||||
if nrows == 0 || ncols == 0 {
|
||||
@@ -1203,14 +1465,27 @@ impl OpenCADStudio {
|
||||
table.set_cell_text(r, c, cell);
|
||||
}
|
||||
}
|
||||
self.push_undo_snapshot(i, "DATALINK");
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.add_entity_clone(acadrust::EntityType::Table(table));
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"DATALINK: imported {nrows}×{ncols} cells into a table at the origin."
|
||||
).as_ref());
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let current_style = doc.header.current_table_style_name.clone();
|
||||
table.table_style_handle = doc.objects.iter().find_map(|(handle, object)| {
|
||||
match object {
|
||||
acadrust::objects::ObjectType::TableStyle(style)
|
||||
if style.name.eq_ignore_ascii_case(¤t_style) =>
|
||||
{
|
||||
Some(*handle)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
let link_path = std::fs::canonicalize(path)
|
||||
.map(|value| value.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|_| path.to_string());
|
||||
let command = crate::modules::annotate::data_link::DataLinkPlaceCommand::new(
|
||||
table, &link_path,
|
||||
);
|
||||
self.command_line
|
||||
.push_info(&crate::command::CadCommand::prompt(&command));
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
Err(e) => {
|
||||
self.command_line
|
||||
@@ -1219,6 +1494,151 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
cmd if cmd == "DATALINKUPDATE" || cmd.starts_with("DATALINKUPDATE ") => {
|
||||
let write_back = cmd
|
||||
.trim_start_matches("DATALINKUPDATE")
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("WRITE");
|
||||
let selected: Vec<acadrust::Handle> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.into_iter()
|
||||
.filter_map(|(handle, entity)| {
|
||||
matches!(entity, acadrust::EntityType::Table(_)).then_some(handle)
|
||||
})
|
||||
.collect();
|
||||
let table_handles = if selected.is_empty() {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter_map(|entity| {
|
||||
matches!(entity, acadrust::EntityType::Table(_))
|
||||
.then_some(entity.common().handle)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
selected
|
||||
};
|
||||
let mut jobs = Vec::new();
|
||||
for handle in &table_handles {
|
||||
let Some(acadrust::EntityType::Table(table)) =
|
||||
self.tabs[i].scene.document.get_entity(*handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let link_handle = table
|
||||
.rows
|
||||
.iter()
|
||||
.flat_map(|row| row.cells.iter())
|
||||
.find_map(|cell| cell.data_link_handle);
|
||||
let Some(link_handle) = link_handle else {
|
||||
continue;
|
||||
};
|
||||
let path = match self.tabs[i].scene.document.objects.get(&link_handle) {
|
||||
Some(acadrust::objects::ObjectType::ClassObject(object)) => {
|
||||
match &object.data {
|
||||
acadrust::objects::ClassObjectData::DataLink(link) => {
|
||||
Some(link.connection_string.clone())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(path) = path {
|
||||
jobs.push((*handle, link_handle, path));
|
||||
}
|
||||
}
|
||||
if jobs.is_empty() {
|
||||
self.command_line
|
||||
.push_error("DATALINKUPDATE: no linked tables found.");
|
||||
return Some(Task::none());
|
||||
}
|
||||
if write_back {
|
||||
let mut written = 0usize;
|
||||
for (table_handle, _, path) in &jobs {
|
||||
let Some(acadrust::EntityType::Table(table)) =
|
||||
self.tabs[i].scene.document.get_entity(*table_handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let csv = table_to_csv(table);
|
||||
if std::fs::write(path, csv).is_ok() {
|
||||
written += 1;
|
||||
}
|
||||
}
|
||||
self.command_line.push_output(
|
||||
crate::tf!("DATALINKUPDATE: wrote {} linked source(s).", written).as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
let updates: Vec<_> = jobs
|
||||
.into_iter()
|
||||
.filter_map(|(table_handle, link_handle, path)| {
|
||||
std::fs::read_to_string(&path)
|
||||
.ok()
|
||||
.map(|text| (table_handle, link_handle, parse_csv_table(&text)))
|
||||
})
|
||||
.filter(|(_, _, rows)| !rows.is_empty())
|
||||
.collect();
|
||||
if updates.is_empty() {
|
||||
self.command_line
|
||||
.push_error("DATALINKUPDATE: linked sources could not be read.");
|
||||
return Some(Task::none());
|
||||
}
|
||||
self.push_undo_snapshot(i, "DATALINKUPDATE");
|
||||
use acadrust::entities::table::CellStateFlags;
|
||||
let mut changed_handles = Vec::new();
|
||||
for (table_handle, link_handle, rows) in updates {
|
||||
let columns = rows.iter().map(Vec::len).max().unwrap_or(0);
|
||||
let Some(acadrust::EntityType::Table(table)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(table_handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
while table.row_count() < rows.len() {
|
||||
table.add_row();
|
||||
}
|
||||
while table.row_count() > rows.len() {
|
||||
table.remove_row(table.row_count() - 1);
|
||||
}
|
||||
while table.column_count() < columns {
|
||||
let width = table.columns.last().map(|column| column.width).unwrap_or(2.0);
|
||||
table.add_column(width);
|
||||
}
|
||||
while table.column_count() > columns {
|
||||
table.remove_column(table.column_count() - 1);
|
||||
}
|
||||
for row in 0..rows.len() {
|
||||
for column in 0..columns {
|
||||
if let Some(cell) = table.cell_mut(row, column) {
|
||||
cell.set_text(
|
||||
rows[row].get(column).map(String::as_str).unwrap_or(""),
|
||||
);
|
||||
cell.has_linked_data = true;
|
||||
cell.data_link_handle = Some(link_handle);
|
||||
cell.data_link_rows = rows.len() as i32;
|
||||
cell.data_link_columns = columns as i32;
|
||||
cell.state.insert(
|
||||
CellStateFlags::LINKED
|
||||
| CellStateFlags::CONTENT_LOCKED
|
||||
| CellStateFlags::FORMAT_LOCKED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
changed_handles.push(table_handle);
|
||||
}
|
||||
self.invalidate_property_targets(i, &changed_handles);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
self.command_line.push_output(
|
||||
crate::tf!("DATALINKUPDATE: updated {} linked table(s).", changed_handles.len())
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
// LANDXMLIMPORT <path> — import survey points (LandXML <CgPoint>
|
||||
// elements) as Point objects. Reads the coordinate text content
|
||||
// (northing easting elevation) → Point at (easting, northing, elev).
|
||||
@@ -1313,6 +1733,68 @@ fn parse_landxml_cgpoints(xml: &str) -> Vec<[f64; 3]> {
|
||||
out
|
||||
}
|
||||
|
||||
fn parse_csv_table(text: &str) -> Vec<Vec<String>> {
|
||||
let mut rows = Vec::new();
|
||||
let mut row = Vec::new();
|
||||
let mut field = String::new();
|
||||
let mut quoted = false;
|
||||
let mut chars = text.chars().peekable();
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'"' if quoted && chars.peek() == Some(&'"') => {
|
||||
field.push('"');
|
||||
chars.next();
|
||||
}
|
||||
'"' => quoted = !quoted,
|
||||
',' if !quoted => row.push(std::mem::take(&mut field)),
|
||||
'\n' if !quoted => {
|
||||
if field.ends_with('\r') {
|
||||
field.pop();
|
||||
}
|
||||
row.push(std::mem::take(&mut field));
|
||||
if row.iter().any(|value| !value.is_empty()) {
|
||||
rows.push(std::mem::take(&mut row));
|
||||
} else {
|
||||
row.clear();
|
||||
}
|
||||
}
|
||||
_ => field.push(ch),
|
||||
}
|
||||
}
|
||||
if field.ends_with('\r') {
|
||||
field.pop();
|
||||
}
|
||||
if !field.is_empty() || !row.is_empty() {
|
||||
row.push(field);
|
||||
if row.iter().any(|value| !value.is_empty()) {
|
||||
rows.push(row);
|
||||
}
|
||||
}
|
||||
rows
|
||||
}
|
||||
|
||||
fn table_to_csv(table: &acadrust::entities::Table) -> String {
|
||||
fn escape(value: &str) -> String {
|
||||
if value.contains([',', '"', '\r', '\n']) {
|
||||
format!("\"{}\"", value.replace('"', "\"\""))
|
||||
} else {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
table
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
row.cells
|
||||
.iter()
|
||||
.map(|cell| escape(cell.text_value()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\r\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::OpenCADStudio;
|
||||
|
||||
@@ -390,6 +390,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"OBJECTSCALE",
|
||||
// Import CSV into a table + LandXML survey points.
|
||||
"DATALINK",
|
||||
"DATALINKUPDATE",
|
||||
"LANDXMLIMPORT",
|
||||
// Keyboard-shortcut (CUI) export / import.
|
||||
"CUIEXPORT",
|
||||
|
||||
@@ -128,6 +128,9 @@ impl OpenCADStudio {
|
||||
crate::entities::spline::control_vertex_count(spline)
|
||||
})
|
||||
}
|
||||
acadrust::EntityType::Table(table) => {
|
||||
Some(table.row_count().saturating_mul(table.column_count()))
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
vertex_count.map_or(prop_vertex, |count| prop_vertex.min(count.saturating_sub(1)))
|
||||
@@ -140,6 +143,8 @@ impl OpenCADStudio {
|
||||
false
|
||||
};
|
||||
crate::scene::view::dispatch::set_prop_current_vertex(prop_vertex);
|
||||
crate::entities::table::set_prop_current_cell(prop_vertex);
|
||||
crate::entities::table::set_prop_current_cell_active(prop_vertex_indicator_active);
|
||||
|
||||
let annotation_scale_handle = self.tabs[i].scene.displayed_annotation_scale_handle();
|
||||
let new_panel = {
|
||||
@@ -1350,6 +1355,354 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
acadrust::EntityType::Table(table) => {
|
||||
use acadrust::entities::table::{
|
||||
CellEdgeFlags, CellStylePropertyFlags, CellValueType,
|
||||
};
|
||||
|
||||
let mut names: Vec<String> = doc
|
||||
.objects
|
||||
.values()
|
||||
.filter_map(|object| match object {
|
||||
acadrust::objects::ObjectType::TableStyle(style)
|
||||
if !style.name.trim().is_empty() =>
|
||||
{
|
||||
Some(style.name.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
names.sort_by_key(|name| name.to_ascii_lowercase());
|
||||
names.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
|
||||
let selected_style = table
|
||||
.table_style_handle
|
||||
.and_then(|handle| doc.objects.get(&handle))
|
||||
.and_then(|object| match object {
|
||||
acadrust::objects::ObjectType::TableStyle(style) => {
|
||||
Some(style)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
let selected = selected_style
|
||||
.map(|style| style.name.clone())
|
||||
.unwrap_or_else(|| "Standard".to_string());
|
||||
if !names.iter().any(|name| name.eq_ignore_ascii_case(&selected)) {
|
||||
names.insert(0, selected.clone());
|
||||
}
|
||||
set_row_value(
|
||||
&mut sections,
|
||||
"tbl_style_handle",
|
||||
crate::scene::model::object::PropValue::Choice {
|
||||
selected,
|
||||
options: names,
|
||||
},
|
||||
);
|
||||
|
||||
let title_suppressed =
|
||||
crate::entities::table::resolved_title_suppressed(
|
||||
table,
|
||||
selected_style,
|
||||
);
|
||||
let header_suppressed =
|
||||
crate::entities::table::resolved_header_suppressed(
|
||||
table,
|
||||
selected_style,
|
||||
);
|
||||
let flow_up = crate::entities::table::resolved_flow_up(
|
||||
table,
|
||||
selected_style,
|
||||
);
|
||||
let (horizontal_margin, vertical_margin) =
|
||||
crate::entities::table::resolved_table_margins(
|
||||
table,
|
||||
selected_style,
|
||||
);
|
||||
update_row_toggle(
|
||||
&mut sections,
|
||||
"tbl_title_suppressed",
|
||||
title_suppressed,
|
||||
);
|
||||
update_row_toggle(
|
||||
&mut sections,
|
||||
"tbl_header_suppressed",
|
||||
header_suppressed,
|
||||
);
|
||||
update_row_text(
|
||||
&mut sections,
|
||||
"tbl_flow_direction",
|
||||
if flow_up { "Up" } else { "Down" }.to_string(),
|
||||
);
|
||||
update_row_text(
|
||||
&mut sections,
|
||||
"tbl_horizontal_margin",
|
||||
crate::entities::common::format_length(horizontal_margin),
|
||||
);
|
||||
update_row_text(
|
||||
&mut sections,
|
||||
"tbl_vertical_margin",
|
||||
crate::entities::common::format_length(vertical_margin),
|
||||
);
|
||||
|
||||
let columns = table.column_count();
|
||||
if columns > 0 {
|
||||
let cell_index = prop_vertex.min(
|
||||
table.row_count()
|
||||
.saturating_mul(columns)
|
||||
.saturating_sub(1),
|
||||
);
|
||||
let row_index = cell_index / columns;
|
||||
let column_index = cell_index % columns;
|
||||
if let (Some(row), Some(cell)) = (
|
||||
table.rows.get(row_index),
|
||||
table.cell(row_index, column_index),
|
||||
) {
|
||||
let document_row_style = selected_style.map(|style| {
|
||||
let kind = match (
|
||||
title_suppressed,
|
||||
header_suppressed,
|
||||
row_index,
|
||||
) {
|
||||
(false, _, 0) => 0,
|
||||
(false, false, 1) | (true, false, 0) => 1,
|
||||
_ => 2,
|
||||
};
|
||||
match kind {
|
||||
0 => &style.title_row_style,
|
||||
1 => &style.header_row_style,
|
||||
_ => &style.data_row_style,
|
||||
}
|
||||
});
|
||||
let local = |property| {
|
||||
crate::entities::table::style_for_property(
|
||||
table,
|
||||
row,
|
||||
column_index,
|
||||
cell,
|
||||
property,
|
||||
)
|
||||
};
|
||||
let alignment = local(CellStylePropertyFlags::ALIGNMENT)
|
||||
.map(|style| style.alignment)
|
||||
.or_else(|| {
|
||||
document_row_style.map(|style| style.alignment as i32)
|
||||
})
|
||||
.unwrap_or(5);
|
||||
let alignment = match alignment {
|
||||
1 => "Top Left",
|
||||
2 => "Top Center",
|
||||
3 => "Top Right",
|
||||
4 => "Middle Left",
|
||||
6 => "Middle Right",
|
||||
7 => "Bottom Left",
|
||||
8 => "Bottom Center",
|
||||
9 => "Bottom Right",
|
||||
_ => "Middle Center",
|
||||
};
|
||||
update_row_text(
|
||||
&mut sections,
|
||||
"tbl_cell_alignment",
|
||||
alignment.to_string(),
|
||||
);
|
||||
|
||||
let text_style_name = local(CellStylePropertyFlags::TEXT_STYLE)
|
||||
.map(|style| style.text_style_name.clone())
|
||||
.filter(|name| !name.is_empty())
|
||||
.or_else(|| {
|
||||
document_row_style
|
||||
.map(|style| style.text_style_name.clone())
|
||||
.filter(|name| !name.is_empty())
|
||||
})
|
||||
.unwrap_or_else(|| "Standard".to_string());
|
||||
update_row_text(
|
||||
&mut sections,
|
||||
"tbl_cell_text_style",
|
||||
text_style_name,
|
||||
);
|
||||
let text_height = local(CellStylePropertyFlags::TEXT_HEIGHT)
|
||||
.map(|style| style.text_height)
|
||||
.or_else(|| {
|
||||
document_row_style.map(|style| style.text_height)
|
||||
})
|
||||
.unwrap_or(0.18);
|
||||
update_row_text(
|
||||
&mut sections,
|
||||
"tbl_cell_text_height",
|
||||
crate::entities::common::format_length(text_height),
|
||||
);
|
||||
let content_color = local(
|
||||
CellStylePropertyFlags::CONTENT_COLOR,
|
||||
)
|
||||
.map(|style| style.content_color)
|
||||
.or_else(|| {
|
||||
document_row_style.map(|style| style.text_color)
|
||||
})
|
||||
.unwrap_or(acadrust::types::Color::ByBlock);
|
||||
update_row_color(
|
||||
&mut sections,
|
||||
"tbl_cell_content_color",
|
||||
content_color,
|
||||
);
|
||||
let background_style =
|
||||
local(CellStylePropertyFlags::BACKGROUND_COLOR);
|
||||
let background_color = background_style
|
||||
.map(|style| style.background_color)
|
||||
.or_else(|| {
|
||||
document_row_style.map(|style| style.fill_color)
|
||||
})
|
||||
.unwrap_or(acadrust::types::Color::ByBlock);
|
||||
update_row_color(
|
||||
&mut sections,
|
||||
"tbl_cell_background_color",
|
||||
background_color,
|
||||
);
|
||||
update_row_toggle(
|
||||
&mut sections,
|
||||
"tbl_cell_fill",
|
||||
background_style
|
||||
.map(|style| style.fill_enabled)
|
||||
.or_else(|| {
|
||||
document_row_style
|
||||
.map(|style| style.fill_enabled)
|
||||
})
|
||||
.unwrap_or(false),
|
||||
);
|
||||
let format = local(CellStylePropertyFlags::DATA_FORMAT)
|
||||
.map(|style| style.value_format.clone())
|
||||
.or_else(|| {
|
||||
document_row_style
|
||||
.map(|style| style.format_string.clone())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
update_row_text(
|
||||
&mut sections,
|
||||
"tbl_cell_format",
|
||||
format,
|
||||
);
|
||||
let data_type = cell
|
||||
.contents
|
||||
.first()
|
||||
.map(|content| content.value.value_type)
|
||||
.filter(|kind| *kind != CellValueType::Unknown)
|
||||
.or_else(|| {
|
||||
local(CellStylePropertyFlags::DATA_TYPE)
|
||||
.map(|style| {
|
||||
CellValueType::from(
|
||||
style.value_data_type.max(0) as u32,
|
||||
)
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
document_row_style.map(|style| {
|
||||
CellValueType::from(style.data_type.max(0) as u32)
|
||||
})
|
||||
})
|
||||
.unwrap_or(CellValueType::String);
|
||||
let data_type = match data_type {
|
||||
CellValueType::Long => "Integer",
|
||||
CellValueType::Double => "Decimal",
|
||||
CellValueType::Date => "Date",
|
||||
CellValueType::Point2D => "Point 2D",
|
||||
CellValueType::Point3D => "Point 3D",
|
||||
CellValueType::Handle => "Handle",
|
||||
_ => "Text",
|
||||
};
|
||||
update_row_text(
|
||||
&mut sections,
|
||||
"tbl_cell_data_type",
|
||||
data_type.to_string(),
|
||||
);
|
||||
for (field, property, fallback) in [
|
||||
(
|
||||
"tbl_cell_margin_left",
|
||||
CellStylePropertyFlags::MARGIN_LEFT,
|
||||
horizontal_margin,
|
||||
),
|
||||
(
|
||||
"tbl_cell_margin_top",
|
||||
CellStylePropertyFlags::MARGIN_TOP,
|
||||
vertical_margin,
|
||||
),
|
||||
(
|
||||
"tbl_cell_margin_right",
|
||||
CellStylePropertyFlags::MARGIN_RIGHT,
|
||||
horizontal_margin,
|
||||
),
|
||||
(
|
||||
"tbl_cell_margin_bottom",
|
||||
CellStylePropertyFlags::MARGIN_BOTTOM,
|
||||
vertical_margin,
|
||||
),
|
||||
] {
|
||||
let value = local(property)
|
||||
.map(|style| match field {
|
||||
"tbl_cell_margin_left" => style.margin_left,
|
||||
"tbl_cell_margin_top" => style.margin_top,
|
||||
"tbl_cell_margin_right" => style.margin_right,
|
||||
_ => style.margin_bottom,
|
||||
})
|
||||
.unwrap_or(fallback);
|
||||
update_row_text(
|
||||
&mut sections,
|
||||
field,
|
||||
crate::entities::common::format_length(value),
|
||||
);
|
||||
}
|
||||
|
||||
let border_visible = |edge: CellEdgeFlags| {
|
||||
let local_style = [
|
||||
cell.style.as_ref(),
|
||||
row.style.as_ref(),
|
||||
table
|
||||
.columns
|
||||
.get(column_index)
|
||||
.and_then(|column| column.style.as_ref()),
|
||||
table.base_style.as_ref(),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find(|style| {
|
||||
style.applied_border_edges.contains(edge)
|
||||
});
|
||||
if let Some(style) = local_style {
|
||||
return if edge == CellEdgeFlags::TOP {
|
||||
!style.top_border.invisible
|
||||
} else if edge == CellEdgeFlags::RIGHT {
|
||||
!style.right_border.invisible
|
||||
} else if edge == CellEdgeFlags::BOTTOM {
|
||||
!style.bottom_border.invisible
|
||||
} else {
|
||||
!style.left_border.invisible
|
||||
};
|
||||
}
|
||||
document_row_style
|
||||
.map(|style| {
|
||||
if edge == CellEdgeFlags::TOP {
|
||||
!style.top_border.is_invisible
|
||||
} else if edge == CellEdgeFlags::RIGHT {
|
||||
!style.right_border.is_invisible
|
||||
} else if edge == CellEdgeFlags::BOTTOM {
|
||||
!style.bottom_border.is_invisible
|
||||
} else {
|
||||
!style.left_border.is_invisible
|
||||
}
|
||||
})
|
||||
.unwrap_or(true)
|
||||
};
|
||||
for (field, edge) in [
|
||||
("tbl_cell_border_top", CellEdgeFlags::TOP),
|
||||
("tbl_cell_border_right", CellEdgeFlags::RIGHT),
|
||||
("tbl_cell_border_bottom", CellEdgeFlags::BOTTOM),
|
||||
("tbl_cell_border_left", CellEdgeFlags::LEFT),
|
||||
] {
|
||||
update_row_toggle(
|
||||
&mut sections,
|
||||
field,
|
||||
border_visible(edge),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -1939,6 +2292,51 @@ impl OpenCADStudio {
|
||||
mut entity: acadrust::EntityType,
|
||||
) -> Option<Handle> {
|
||||
let i = self.active_tab;
|
||||
if let acadrust::EntityType::Table(table) = &mut entity {
|
||||
const PREFIX: &str = "__OPENCAD_LINK_PENDING__";
|
||||
if let Some(path) = table.name.strip_prefix(PREFIX).map(str::to_string) {
|
||||
use acadrust::entities::table::CellStateFlags;
|
||||
use acadrust::objects::{
|
||||
ClassObject, ClassObjectData, DataLink, ObjectType,
|
||||
};
|
||||
let document = &mut self.tabs[i].scene.document;
|
||||
let link_handle = document.allocate_handle();
|
||||
let mut object = ClassObject::new(ClassObjectData::DataLink(DataLink {
|
||||
data_adapter: "CSV".to_string(),
|
||||
description: path.clone(),
|
||||
tooltip: path.clone(),
|
||||
connection_string: path.clone(),
|
||||
status_flags: 1,
|
||||
update_status: "Linked".to_string(),
|
||||
..DataLink::default()
|
||||
}));
|
||||
object.handle = link_handle;
|
||||
document
|
||||
.objects
|
||||
.insert(link_handle, ObjectType::ClassObject(object));
|
||||
let rows = table.row_count() as i32;
|
||||
let columns = table.column_count() as i32;
|
||||
for row in &mut table.rows {
|
||||
for cell in &mut row.cells {
|
||||
cell.has_linked_data = true;
|
||||
cell.data_link_handle = Some(link_handle);
|
||||
cell.data_link_rows = rows;
|
||||
cell.data_link_columns = columns;
|
||||
cell.state.insert(
|
||||
CellStateFlags::LINKED
|
||||
| CellStateFlags::CONTENT_LOCKED
|
||||
| CellStateFlags::FORMAT_LOCKED,
|
||||
);
|
||||
}
|
||||
}
|
||||
table.name = std::path::Path::new(&path)
|
||||
.file_stem()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("Linked table")
|
||||
.to_string();
|
||||
table.description = path;
|
||||
}
|
||||
}
|
||||
let tracks_draw_anchor = self.tabs[i].active_cmd.is_some()
|
||||
&& matches!(
|
||||
&entity,
|
||||
@@ -2448,6 +2846,79 @@ fn set_row_value(
|
||||
}
|
||||
}
|
||||
|
||||
fn update_row_text(
|
||||
sections: &mut [crate::scene::model::object::PropSection],
|
||||
field: &str,
|
||||
value: String,
|
||||
) {
|
||||
use crate::scene::model::object::PropValue;
|
||||
for section in sections.iter_mut() {
|
||||
let Some(row) = section.props.iter_mut().find(|property| property.field == field) else {
|
||||
continue;
|
||||
};
|
||||
match &mut row.value {
|
||||
PropValue::ReadOnly(current)
|
||||
| PropValue::EditText(current)
|
||||
| PropValue::PlainText(current) => *current = value,
|
||||
PropValue::ReadOnlyWithTooltip { value: current, .. } => *current = value,
|
||||
PropValue::Choice { selected, .. } => *selected = value,
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_row_toggle(
|
||||
sections: &mut [crate::scene::model::object::PropSection],
|
||||
field: &str,
|
||||
value: bool,
|
||||
) {
|
||||
use crate::scene::model::object::PropValue;
|
||||
for section in sections.iter_mut() {
|
||||
let Some(row) = section.props.iter_mut().find(|property| property.field == field) else {
|
||||
continue;
|
||||
};
|
||||
match &mut row.value {
|
||||
PropValue::BoolToggle { value: current, .. } => *current = value,
|
||||
PropValue::ReadOnly(current) => {
|
||||
*current = if value { t!("Yes") } else { t!("No") }.into_owned()
|
||||
}
|
||||
PropValue::ReadOnlyWithTooltip { value: current, .. } => {
|
||||
*current = if value { t!("Yes") } else { t!("No") }.into_owned()
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_row_color(
|
||||
sections: &mut [crate::scene::model::object::PropSection],
|
||||
field: &str,
|
||||
color: acadrust::types::Color,
|
||||
) {
|
||||
use crate::scene::model::object::PropValue;
|
||||
let label = match color {
|
||||
acadrust::types::Color::None => "None".to_string(),
|
||||
acadrust::types::Color::ByLayer => "ByLayer".to_string(),
|
||||
acadrust::types::Color::ByBlock => "ByBlock".to_string(),
|
||||
acadrust::types::Color::Index(index) => index.to_string(),
|
||||
acadrust::types::Color::Rgb { r, g, b } => format!("{r},{g},{b}"),
|
||||
};
|
||||
for section in sections.iter_mut() {
|
||||
let Some(row) = section.props.iter_mut().find(|property| property.field == field) else {
|
||||
continue;
|
||||
};
|
||||
match &mut row.value {
|
||||
PropValue::ColorChoice(current) => *current = color,
|
||||
PropValue::ReadOnly(current) => *current = label,
|
||||
PropValue::ReadOnlyWithTooltip { value: current, .. } => *current = label,
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// The lineweight dropdown options (named defaults + the standard millimetre
|
||||
/// steps), matching the labels `dim_lineweight_label` produces.
|
||||
pub(crate) fn lineweight_options() -> Vec<String> {
|
||||
|
||||
@@ -2127,6 +2127,29 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
}
|
||||
}
|
||||
} else if field == "tbl_style_handle" {
|
||||
let style_handle = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.iter()
|
||||
.find_map(|(handle, object)| match object {
|
||||
acadrust::objects::ObjectType::TableStyle(style)
|
||||
if style.name.eq_ignore_ascii_case(value.trim()) =>
|
||||
{
|
||||
Some(*handle)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
if let Some(style_handle) = style_handle {
|
||||
for &handle in &handles {
|
||||
if let Some(acadrust::EntityType::Table(table)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
table.table_style_handle = Some(style_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if field == "transparency" {
|
||||
for &handle in &handles {
|
||||
if self.tabs[i].scene.is_layer_locked(handle) {
|
||||
@@ -2266,6 +2289,15 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if field.starts_with("tbl_") {
|
||||
for &handle in &handles {
|
||||
if let Some(acadrust::EntityType::Table(table)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
table.block_record_handle = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.invalidate_property_targets(i, &handles);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].properties.edit_choice_open = false;
|
||||
@@ -2382,7 +2414,30 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
return Task::none();
|
||||
}
|
||||
self.push_undo_snapshot(i, "CHPROP");
|
||||
if field == "block" {
|
||||
if field == "tbl_style_handle" {
|
||||
let style_handle = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.iter()
|
||||
.find_map(|(handle, object)| match object {
|
||||
acadrust::objects::ObjectType::TableStyle(style)
|
||||
if style.name.eq_ignore_ascii_case(val.trim()) =>
|
||||
{
|
||||
Some(*handle)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
if let Some(style_handle) = style_handle {
|
||||
for &handle in &handles {
|
||||
if let Some(acadrust::EntityType::Table(table)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
table.table_style_handle = Some(style_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if field == "block" {
|
||||
// Name row on a block reference: an existing name
|
||||
// re-points the selected inserts; a new one renames
|
||||
// the definition they share. Commit closes the list.
|
||||
@@ -2563,6 +2618,15 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if field.starts_with("tbl_") {
|
||||
for &handle in &handles {
|
||||
if let Some(acadrust::EntityType::Table(table)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
table.block_record_handle = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.invalidate_property_targets(i, &handles);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
|
||||
@@ -4651,6 +4651,47 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
"tbl_title_suppressed" | "tbl_header_suppressed" => {
|
||||
let next = {
|
||||
let document = &self.tabs[i].scene.document;
|
||||
let Some(acadrust::EntityType::Table(table)) =
|
||||
document.get_entity(handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let table_style = table.table_style_handle.and_then(|style_handle| {
|
||||
document.objects.get(&style_handle).and_then(|object| {
|
||||
match object {
|
||||
acadrust::objects::ObjectType::TableStyle(style) => {
|
||||
Some(style)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
});
|
||||
let current = if field == "tbl_title_suppressed" {
|
||||
crate::entities::table::resolved_title_suppressed(
|
||||
table,
|
||||
table_style,
|
||||
)
|
||||
} else {
|
||||
crate::entities::table::resolved_header_suppressed(
|
||||
table,
|
||||
table_style,
|
||||
)
|
||||
};
|
||||
!current
|
||||
};
|
||||
if let Some(entity) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
crate::scene::view::dispatch::apply_geom_prop(
|
||||
entity,
|
||||
field,
|
||||
if next { "true" } else { "false" },
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(entity) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
@@ -4662,6 +4703,15 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
if field.starts_with("tbl_") {
|
||||
for &handle in &handles {
|
||||
if let Some(acadrust::EntityType::Table(table)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
table.block_record_handle = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.invalidate_property_targets(i, &handles);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
@@ -4677,6 +4727,9 @@ impl OpenCADStudio {
|
||||
match self.tabs[i].scene.document.get_entity(handles[0]) {
|
||||
Some(acadrust::EntityType::LwPolyline(p)) => p.vertices.len(),
|
||||
Some(acadrust::EntityType::Polyline2D(p)) => p.vertices.len(),
|
||||
Some(acadrust::EntityType::Table(table)) => {
|
||||
table.row_count().saturating_mul(table.column_count())
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
} else {
|
||||
@@ -4688,6 +4741,7 @@ impl OpenCADStudio {
|
||||
let next = (cur + delta as i64).rem_euclid(n as i64) as usize;
|
||||
self.tabs[i].properties.prop_vertex = next;
|
||||
self.tabs[i].properties.prop_vertex_indicator_active = next != cur as usize;
|
||||
crate::entities::table::set_prop_current_cell(next);
|
||||
self.refresh_properties();
|
||||
}
|
||||
Task::none()
|
||||
@@ -4894,6 +4948,57 @@ impl OpenCADStudio {
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
if matches!(
|
||||
field.as_str(),
|
||||
"tbl_cell_content_color" | "tbl_cell_background_color"
|
||||
) {
|
||||
let cell_index = self.tabs[i].properties.prop_vertex;
|
||||
if !handles.is_empty() {
|
||||
self.push_undo_snapshot(i, "TABLE CELL COLOR");
|
||||
for &handle in &handles {
|
||||
let Some(acadrust::EntityType::Table(table)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let columns = table.column_count();
|
||||
if columns == 0 {
|
||||
continue;
|
||||
}
|
||||
if let Some(cell) = table.cell_mut(
|
||||
cell_index / columns,
|
||||
cell_index % columns,
|
||||
) {
|
||||
use acadrust::entities::table::CellStateFlags;
|
||||
if cell.state.intersects(
|
||||
CellStateFlags::FORMAT_LOCKED
|
||||
| CellStateFlags::FORMAT_READ_ONLY,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let style = cell.style.get_or_insert_with(Default::default);
|
||||
if field == "tbl_cell_content_color" {
|
||||
style.content_color = color;
|
||||
style.property_flags.insert(
|
||||
acadrust::entities::table::CellStylePropertyFlags::CONTENT_COLOR,
|
||||
);
|
||||
} else {
|
||||
style.background_color = color;
|
||||
style.fill_enabled = true;
|
||||
style.property_flags.insert(
|
||||
acadrust::entities::table::CellStylePropertyFlags::BACKGROUND_COLOR,
|
||||
);
|
||||
}
|
||||
}
|
||||
table.block_record_handle = None;
|
||||
}
|
||||
self.invalidate_property_targets(i, &handles);
|
||||
self.tabs[i].properties.open_color_field = None;
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
if !handles.is_empty() {
|
||||
self.push_undo_snapshot(i, "CHPROP");
|
||||
let idx = if field == "gradient_color_2" { 1 } else { 0 };
|
||||
|
||||
@@ -4392,6 +4392,104 @@ impl OpenCADStudio {
|
||||
).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
let table_editor = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.and_then(|entity| match entity {
|
||||
AcadEntityType::Table(table) => {
|
||||
let horizontal = glam::DVec3::new(
|
||||
table.horizontal_direction.x,
|
||||
table.horizontal_direction.y,
|
||||
table.horizontal_direction.z,
|
||||
)
|
||||
.normalize_or(glam::DVec3::X);
|
||||
let normal = glam::DVec3::new(
|
||||
table.normal.x,
|
||||
table.normal.y,
|
||||
table.normal.z,
|
||||
)
|
||||
.normalize_or(glam::DVec3::Z);
|
||||
let mut down = horizontal
|
||||
.cross(normal)
|
||||
.normalize_or(glam::DVec3::NEG_Y);
|
||||
let table_style = table.table_style_handle.and_then(|style_handle| {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.get(&style_handle)
|
||||
.and_then(|object| match object {
|
||||
acadrust::objects::ObjectType::TableStyle(style) => {
|
||||
Some(style)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
let flows_up = crate::entities::table::resolved_flow_up(
|
||||
table,
|
||||
table_style,
|
||||
);
|
||||
if flows_up {
|
||||
down = -down;
|
||||
}
|
||||
let origin = glam::DVec3::new(
|
||||
table.insertion_point.x,
|
||||
table.insertion_point.y,
|
||||
table.insertion_point.z,
|
||||
);
|
||||
let relative = click_world - origin;
|
||||
let x = relative.dot(horizontal);
|
||||
let y = relative.dot(down);
|
||||
if x < 0.0 || y < 0.0 {
|
||||
return None;
|
||||
}
|
||||
let column = table
|
||||
.columns
|
||||
.iter()
|
||||
.scan(0.0, |offset, column| {
|
||||
*offset += column.width;
|
||||
Some(*offset)
|
||||
})
|
||||
.position(|end| x <= end)?;
|
||||
let row = table
|
||||
.rows
|
||||
.iter()
|
||||
.scan(0.0, |offset, row| {
|
||||
*offset += row.height;
|
||||
Some(*offset)
|
||||
})
|
||||
.position(|end| y <= end)?;
|
||||
let locked = table.cell(row, column).is_some_and(|cell| {
|
||||
use acadrust::entities::table::CellStateFlags;
|
||||
cell.state.intersects(
|
||||
CellStateFlags::CONTENT_LOCKED
|
||||
| CellStateFlags::CONTENT_READ_ONLY,
|
||||
)
|
||||
});
|
||||
Some((
|
||||
(!locked).then(|| {
|
||||
crate::modules::annotate::table_cmd::TableCellEditCommand::new(
|
||||
handle, table, row, column,
|
||||
)
|
||||
}),
|
||||
row * table.column_count() + column,
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
if let Some((command, cell_index)) = table_editor {
|
||||
self.tabs[i].properties.prop_vertex = cell_index;
|
||||
self.tabs[i].properties.prop_vertex_indicator_active = true;
|
||||
crate::entities::table::set_prop_current_cell(cell_index);
|
||||
self.refresh_properties();
|
||||
if let Some(command) = command {
|
||||
self.command_line
|
||||
.push_info(&crate::command::CadCommand::prompt(&command));
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
// Any text-bearing entity opens its in-place editor
|
||||
// (plain box or rich MText editor, per type). A
|
||||
// Leader resolves to the entity it annotates.
|
||||
|
||||
Reference in new issue
Block a user