forked from 1b-agent/opencadstudio-upstream
Complete table creation and editing workflow
This commit is contained in:
1 parent
7215faae44
commit
4a08f7fe80
9 files changed
+1848
-279
No files matched your search
+539
-64
@@ -12,60 +12,320 @@ 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>",
|
||||
"Usage: TABLE CELL | INSERTROW | DELETEROW | INSERTCOL | DELETECOL | MERGE | UNMERGE | ROWHEIGHT | COLWIDTH | LOCK | BLOCK | FORMULA | FIELD",
|
||||
);
|
||||
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("TABLE: select a table first.");
|
||||
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("TABLE: block definition not found.");
|
||||
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("TABLE: field handle not found.");
|
||||
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("TABLE: edit applied.");
|
||||
} else {
|
||||
self.command_line
|
||||
.push_error("TABLE: row/column is out of range or the cell is locked.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1161,8 +1421,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 +1432,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 +1458,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 +1487,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 +1726,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,7 @@ impl OpenCADStudio {
|
||||
false
|
||||
};
|
||||
crate::scene::view::dispatch::set_prop_current_vertex(prop_vertex);
|
||||
crate::entities::table::set_prop_current_cell(prop_vertex);
|
||||
|
||||
let annotation_scale_handle = self.tabs[i].scene.displayed_annotation_scale_handle();
|
||||
let new_panel = {
|
||||
@@ -1294,6 +1298,43 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
acadrust::EntityType::Table(table) => {
|
||||
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 = table
|
||||
.table_style_handle
|
||||
.and_then(|handle| doc.objects.get(&handle))
|
||||
.and_then(|object| match object {
|
||||
acadrust::objects::ObjectType::TableStyle(style) => {
|
||||
Some(style.name.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.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,
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -1874,6 +1915,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,
|
||||
|
||||
@@ -2364,7 +2364,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.
|
||||
|
||||
@@ -4677,6 +4677,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 +4691,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()
|
||||
@@ -4864,6 +4868,49 @@ 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,
|
||||
) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 };
|
||||
|
||||
@@ -4391,6 +4391,95 @@ 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 flows_up = table
|
||||
.table_style_handle
|
||||
.and_then(|style_handle| {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.get(&style_handle)
|
||||
})
|
||||
.is_some_and(|object| {
|
||||
matches!(
|
||||
object,
|
||||
acadrust::objects::ObjectType::TableStyle(style)
|
||||
if matches!(
|
||||
style.flow_direction,
|
||||
acadrust::objects::TableFlowDirection::Up
|
||||
)
|
||||
)
|
||||
});
|
||||
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)?;
|
||||
Some((
|
||||
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();
|
||||
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.
|
||||
|
||||
+535
-90
@@ -13,6 +13,18 @@ use crate::scene::view::transform;
|
||||
use crate::scene::model::wire_model::SnapHint;
|
||||
use crate::t;
|
||||
|
||||
thread_local! {
|
||||
static PROPERTY_CELL: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
pub fn set_prop_current_cell(index: usize) {
|
||||
PROPERTY_CELL.with(|cell| cell.set(index));
|
||||
}
|
||||
|
||||
fn prop_current_cell() -> usize {
|
||||
PROPERTY_CELL.with(std::cell::Cell::get)
|
||||
}
|
||||
|
||||
fn v3(v: &acadrust::types::Vector3) -> Vec3 {
|
||||
Vec3::new(v.x as f32, v.y as f32, v.z as f32)
|
||||
}
|
||||
@@ -227,6 +239,71 @@ fn format_cell_value(value: &acadrust::entities::table::CellValue) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn table_cell_reference(reference: &str) -> Option<(usize, usize)> {
|
||||
let reference = reference.trim();
|
||||
let split = reference
|
||||
.char_indices()
|
||||
.find(|(_, ch)| ch.is_ascii_digit())?
|
||||
.0;
|
||||
let (letters, digits) = reference.split_at(split);
|
||||
if letters.is_empty() || digits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut column = 0usize;
|
||||
for ch in letters.chars() {
|
||||
if !ch.is_ascii_alphabetic() {
|
||||
return None;
|
||||
}
|
||||
column = column
|
||||
.checked_mul(26)?
|
||||
.checked_add(ch.to_ascii_uppercase() as usize - 'A' as usize + 1)?;
|
||||
}
|
||||
let row = digits.parse::<usize>().ok()?.checked_sub(1)?;
|
||||
Some((row, column.checked_sub(1)?))
|
||||
}
|
||||
|
||||
fn evaluate_table_formula(table: &Table, expression: &str) -> Option<String> {
|
||||
let body = expression.trim().strip_prefix('=')?.trim();
|
||||
if let Some((row, column)) = table_cell_reference(body) {
|
||||
return table.cell(row, column).map(|cell| cell.text_value().to_string());
|
||||
}
|
||||
let open = body.find('(')?;
|
||||
let close = body.rfind(')')?;
|
||||
let function = body[..open].trim().to_ascii_uppercase();
|
||||
let range = &body[open + 1..close];
|
||||
let (first, last) = range.split_once(':').unwrap_or((range, range));
|
||||
let (r1, c1) = table_cell_reference(first)?;
|
||||
let (r2, c2) = table_cell_reference(last)?;
|
||||
let mut values = Vec::new();
|
||||
for row in r1.min(r2)..=r1.max(r2) {
|
||||
for column in c1.min(c2)..=c1.max(c2) {
|
||||
if let Some(value) = table
|
||||
.cell(row, column)
|
||||
.and_then(|cell| cell.text_value().trim().parse::<f64>().ok())
|
||||
{
|
||||
values.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
match function.as_str() {
|
||||
"SUM" => Some(values.iter().sum::<f64>().to_string()),
|
||||
"AVERAGE" | "AVG" if !values.is_empty() => {
|
||||
Some((values.iter().sum::<f64>() / values.len() as f64).to_string())
|
||||
}
|
||||
"COUNT" => Some(values.len().to_string()),
|
||||
"MIN" if !values.is_empty() => {
|
||||
Some(values.into_iter().fold(f64::INFINITY, f64::min).to_string())
|
||||
}
|
||||
"MAX" if !values.is_empty() => Some(
|
||||
values
|
||||
.into_iter()
|
||||
.fold(f64::NEG_INFINITY, f64::max)
|
||||
.to_string(),
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback_content_centers(
|
||||
bounds: [f32; 4],
|
||||
sizes: &[(f32, f32)],
|
||||
@@ -312,12 +389,12 @@ fn fallback_content_centers(
|
||||
|
||||
fn content_display_value(
|
||||
document: &acadrust::CadDocument,
|
||||
table_handle: acadrust::Handle,
|
||||
table: &Table,
|
||||
content: &acadrust::entities::table::CellContent,
|
||||
) -> String {
|
||||
if let Some(handle) = content.field_handle {
|
||||
if let Some(value) =
|
||||
crate::entities::field::resolve_handle(document, handle, table_handle)
|
||||
crate::entities::field::resolve_handle(document, handle, table.common.handle)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
@@ -331,7 +408,8 @@ fn content_display_value(
|
||||
}
|
||||
}
|
||||
}
|
||||
format_cell_value(&content.value)
|
||||
let value = format_cell_value(&content.value);
|
||||
evaluate_table_formula(table, &value).unwrap_or(value)
|
||||
}
|
||||
|
||||
fn resolved_content_geometry(
|
||||
@@ -1326,8 +1404,7 @@ pub fn tessellate_table(
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, content)| {
|
||||
let text =
|
||||
content_display_value(document, tab.common.handle, content);
|
||||
let text = content_display_value(document, tab, content);
|
||||
(!text.is_empty()).then_some((index, content, text))
|
||||
})
|
||||
.collect();
|
||||
@@ -1740,30 +1817,120 @@ pub fn tessellate_table(
|
||||
|
||||
impl Grippable for Table {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
vec![square_grip(
|
||||
let origin = glam::DVec3::new(
|
||||
self.insertion_point.x,
|
||||
self.insertion_point.y,
|
||||
self.insertion_point.z,
|
||||
);
|
||||
let horizontal = glam::DVec3::new(
|
||||
self.horizontal_direction.x,
|
||||
self.horizontal_direction.y,
|
||||
self.horizontal_direction.z,
|
||||
)
|
||||
.normalize_or(glam::DVec3::X);
|
||||
let normal = glam::DVec3::new(self.normal.x, self.normal.y, self.normal.z)
|
||||
.normalize_or(glam::DVec3::Z);
|
||||
let down = horizontal.cross(normal).normalize_or(glam::DVec3::NEG_Y);
|
||||
let width = self.total_width();
|
||||
let height = self.total_height();
|
||||
let mut grips = vec![square_grip(
|
||||
0,
|
||||
glam::DVec3::new(
|
||||
self.insertion_point.x,
|
||||
self.insertion_point.y,
|
||||
self.insertion_point.z,
|
||||
),
|
||||
)]
|
||||
origin,
|
||||
)];
|
||||
grips.push(square_grip(1, origin + horizontal * width));
|
||||
grips.push(square_grip(2, origin + down * height));
|
||||
let mut offset = 0.0;
|
||||
for (column, definition) in self.columns.iter().enumerate() {
|
||||
offset += definition.width;
|
||||
if column + 1 < self.columns.len() {
|
||||
grips.push(square_grip(
|
||||
100 + column,
|
||||
origin + horizontal * offset + down * (height * 0.5),
|
||||
));
|
||||
}
|
||||
}
|
||||
offset = 0.0;
|
||||
for (row, definition) in self.rows.iter().enumerate() {
|
||||
offset += definition.height;
|
||||
if row + 1 < self.rows.len() {
|
||||
grips.push(square_grip(
|
||||
1000 + row,
|
||||
origin + horizontal * (width * 0.5) + down * offset,
|
||||
));
|
||||
}
|
||||
}
|
||||
for (index, data) in self.break_data.iter().enumerate() {
|
||||
grips.push(square_grip(
|
||||
2000 + index,
|
||||
origin + glam::DVec3::new(data.position.x, data.position.y, data.position.z),
|
||||
));
|
||||
}
|
||||
grips
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
if let GripApply::Translate(delta) = apply {
|
||||
if let Some(grip) = self.grips().into_iter().find(|grip| grip.id == grip_id) {
|
||||
self.apply_grip(grip_id, GripApply::Absolute(grip.world + delta));
|
||||
}
|
||||
return;
|
||||
}
|
||||
let GripApply::Absolute(point) = apply else {
|
||||
return;
|
||||
};
|
||||
if grip_id == 0 {
|
||||
match apply {
|
||||
GripApply::Translate(d) => {
|
||||
self.insertion_point.x += d.x as f64;
|
||||
self.insertion_point.y += d.y as f64;
|
||||
self.insertion_point.z += d.z as f64;
|
||||
self.insertion_point.x = point.x;
|
||||
self.insertion_point.y = point.y;
|
||||
self.insertion_point.z = point.z;
|
||||
return;
|
||||
}
|
||||
let origin = glam::DVec3::new(
|
||||
self.insertion_point.x,
|
||||
self.insertion_point.y,
|
||||
self.insertion_point.z,
|
||||
);
|
||||
let horizontal = glam::DVec3::new(
|
||||
self.horizontal_direction.x,
|
||||
self.horizontal_direction.y,
|
||||
self.horizontal_direction.z,
|
||||
)
|
||||
.normalize_or(glam::DVec3::X);
|
||||
let normal = glam::DVec3::new(self.normal.x, self.normal.y, self.normal.z)
|
||||
.normalize_or(glam::DVec3::Z);
|
||||
let down = horizontal.cross(normal).normalize_or(glam::DVec3::NEG_Y);
|
||||
if grip_id == 1 {
|
||||
let width = (point - origin).dot(horizontal).max(1.0e-6);
|
||||
let current = self.total_width();
|
||||
if current > 1.0e-12 {
|
||||
for column in &mut self.columns {
|
||||
column.width *= width / current;
|
||||
}
|
||||
GripApply::Absolute(p) => {
|
||||
self.insertion_point.x = p.x as f64;
|
||||
self.insertion_point.y = p.y as f64;
|
||||
self.insertion_point.z = p.z as f64;
|
||||
}
|
||||
} else if grip_id == 2 {
|
||||
let height = (point - origin).dot(down).max(1.0e-6);
|
||||
let current = self.total_height();
|
||||
if current > 1.0e-12 {
|
||||
for row in &mut self.rows {
|
||||
row.height *= height / current;
|
||||
}
|
||||
}
|
||||
} else if (100..1000).contains(&grip_id) {
|
||||
let column = grip_id - 100;
|
||||
let before: f64 = self.columns.iter().take(column).map(|item| item.width).sum();
|
||||
if let Some(definition) = self.columns.get_mut(column) {
|
||||
definition.width = ((point - origin).dot(horizontal) - before).max(1.0e-6);
|
||||
}
|
||||
} else if (1000..2000).contains(&grip_id) {
|
||||
let row = grip_id - 1000;
|
||||
let before: f64 = self.rows.iter().take(row).map(|item| item.height).sum();
|
||||
if let Some(definition) = self.rows.get_mut(row) {
|
||||
definition.height = ((point - origin).dot(down) - before).max(1.0e-6);
|
||||
}
|
||||
} else if grip_id >= 2000 {
|
||||
if let Some(data) = self.break_data.get_mut(grip_id - 2000) {
|
||||
let offset = point - origin;
|
||||
data.position = acadrust::types::Vector3::new(offset.x, offset.y, offset.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1772,13 +1939,6 @@ impl PropertyEditable for Table {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||
use crate::entities::common::edit_prop as edit;
|
||||
use acadrust::entities::table::BreakOptionFlags;
|
||||
|
||||
let fmt_h = |oh: &Option<acadrust::types::Handle>| -> String {
|
||||
match oh {
|
||||
Some(h) if !h.is_null() => format!("{:X}", h.value()),
|
||||
_ => "(none)".to_string(),
|
||||
}
|
||||
};
|
||||
let toggle = |label: &str, field: &'static str, b: bool| -> Property {
|
||||
Property {
|
||||
label: label.into(),
|
||||
@@ -1789,26 +1949,38 @@ impl PropertyEditable for Table {
|
||||
// Direction = angle of the horizontal direction vector in the XY plane.
|
||||
let direction_deg =
|
||||
(self.horizontal_direction.y.atan2(self.horizontal_direction.x)).to_degrees();
|
||||
let mut content_count = 0usize;
|
||||
let mut block_content_count = 0usize;
|
||||
let mut field_content_count = 0usize;
|
||||
let mut linked_cell_count = 0usize;
|
||||
for row in &self.rows {
|
||||
for cell in &row.cells {
|
||||
content_count += cell.contents.len();
|
||||
linked_cell_count += usize::from(cell.has_linked_data);
|
||||
for content in &cell.contents {
|
||||
block_content_count += usize::from(content.block_handle.is_some());
|
||||
field_content_count += usize::from(content.field_handle.is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
let break_heights = self
|
||||
.break_data
|
||||
.iter()
|
||||
.map(|data| format!("{:.4}", data.height))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let break_height = self.break_data.first().map(|data| data.height).unwrap_or(0.0);
|
||||
let cell_count = self.rows.len().saturating_mul(self.columns.len());
|
||||
let cell_index = prop_current_cell().min(cell_count.saturating_sub(1));
|
||||
let cell_row = if self.columns.is_empty() {
|
||||
0
|
||||
} else {
|
||||
cell_index / self.columns.len()
|
||||
};
|
||||
let cell_column = if self.columns.is_empty() {
|
||||
0
|
||||
} else {
|
||||
cell_index % self.columns.len()
|
||||
};
|
||||
let cell = self.cell(cell_row, cell_column);
|
||||
let cell_style = cell.and_then(|cell| cell.style.as_ref());
|
||||
let alignment = match cell_style.map(|style| style.alignment).unwrap_or(5) {
|
||||
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",
|
||||
};
|
||||
let cell_locked = cell.is_some_and(|cell| {
|
||||
use acadrust::entities::table::CellStateFlags;
|
||||
cell.state.intersects(
|
||||
CellStateFlags::CONTENT_LOCKED | CellStateFlags::CONTENT_READ_ONLY,
|
||||
)
|
||||
});
|
||||
|
||||
vec![
|
||||
PropSection {
|
||||
@@ -1817,42 +1989,125 @@ impl PropertyEditable for Table {
|
||||
ro(
|
||||
t!("Table style").as_ref(),
|
||||
"tbl_style_handle",
|
||||
fmt_h(&self.table_style_handle),
|
||||
"Standard",
|
||||
),
|
||||
ro(t!("Rows").as_ref(), "tbl_rows", self.rows.len().to_string()),
|
||||
ro(t!("Columns").as_ref(), "tbl_cols", self.columns.len().to_string()),
|
||||
ro(t!("Contents").as_ref(), "tbl_contents", content_count.to_string()),
|
||||
edit(t!("Insertion X").as_ref(), "tbl_insert_x", self.insertion_point.x),
|
||||
edit(t!("Insertion Y").as_ref(), "tbl_insert_y", self.insertion_point.y),
|
||||
edit(t!("Insertion Z").as_ref(), "tbl_insert_z", self.insertion_point.z),
|
||||
edit(t!("Direction").as_ref(), "tbl_direction", direction_deg),
|
||||
edit(t!("Rows").as_ref(), "tbl_rows", self.rows.len() as f64),
|
||||
edit(t!("Columns").as_ref(), "tbl_cols", self.columns.len() as f64),
|
||||
edit(t!("Table width").as_ref(), "tbl_width", self.total_width()),
|
||||
edit(t!("Table height").as_ref(), "tbl_height", self.total_height()),
|
||||
ro(t!("Normal X").as_ref(), "tbl_normal_x", format!("{:.4}", self.normal.x)),
|
||||
ro(t!("Normal Y").as_ref(), "tbl_normal_y", format!("{:.4}", self.normal.y)),
|
||||
ro(t!("Normal Z").as_ref(), "tbl_normal_z", format!("{:.4}", self.normal.z)),
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
title: t!("Cell").into_owned(),
|
||||
props: vec![
|
||||
Property {
|
||||
label: t!("Current cell").into_owned(),
|
||||
field: "tbl_current_cell",
|
||||
value: PropValue::Stepper {
|
||||
field: "tbl_current_cell",
|
||||
display: format!(
|
||||
"{},{} ({} / {})",
|
||||
cell_row,
|
||||
cell_column,
|
||||
cell_index.saturating_add(1),
|
||||
cell_count
|
||||
),
|
||||
},
|
||||
},
|
||||
ro(t!("Row").as_ref(), "tbl_cell_row", cell_row.to_string()),
|
||||
ro(
|
||||
t!("Block contents").as_ref(),
|
||||
"tbl_block_contents",
|
||||
block_content_count.to_string(),
|
||||
t!("Column").as_ref(),
|
||||
"tbl_cell_column",
|
||||
cell_column.to_string(),
|
||||
),
|
||||
ro(
|
||||
t!("Field contents").as_ref(),
|
||||
"tbl_field_contents",
|
||||
field_content_count.to_string(),
|
||||
Property {
|
||||
label: t!("Contents").into_owned(),
|
||||
field: "tbl_cell_text",
|
||||
value: PropValue::PlainText(
|
||||
cell.map(|cell| cell.text_value().to_string()).unwrap_or_default(),
|
||||
),
|
||||
},
|
||||
Property {
|
||||
label: t!("Alignment").into_owned(),
|
||||
field: "tbl_cell_alignment",
|
||||
value: PropValue::Choice {
|
||||
selected: alignment.to_string(),
|
||||
options: vec![
|
||||
"Top Left".into(),
|
||||
"Top Center".into(),
|
||||
"Top Right".into(),
|
||||
"Middle Left".into(),
|
||||
"Middle Center".into(),
|
||||
"Middle Right".into(),
|
||||
"Bottom Left".into(),
|
||||
"Bottom Center".into(),
|
||||
"Bottom Right".into(),
|
||||
],
|
||||
},
|
||||
},
|
||||
edit(
|
||||
t!("Text height").as_ref(),
|
||||
"tbl_cell_text_height",
|
||||
cell_style.map(|style| style.text_height).unwrap_or(0.18),
|
||||
),
|
||||
ro(
|
||||
t!("Merged ranges").as_ref(),
|
||||
"tbl_merged_ranges",
|
||||
self.merged_ranges.len().to_string(),
|
||||
Property {
|
||||
label: t!("Content color").into_owned(),
|
||||
field: "tbl_cell_content_color",
|
||||
value: PropValue::ColorChoice(
|
||||
cell_style
|
||||
.map(|style| style.content_color)
|
||||
.unwrap_or(acadrust::types::Color::ByBlock),
|
||||
),
|
||||
},
|
||||
Property {
|
||||
label: t!("Background color").into_owned(),
|
||||
field: "tbl_cell_background_color",
|
||||
value: PropValue::ColorChoice(
|
||||
cell_style
|
||||
.map(|style| style.background_color)
|
||||
.unwrap_or(acadrust::types::Color::ByBlock),
|
||||
),
|
||||
},
|
||||
Property {
|
||||
label: t!("Data format").into_owned(),
|
||||
field: "tbl_cell_format",
|
||||
value: PropValue::PlainText(
|
||||
cell_style.map(|style| style.value_format.clone()).unwrap_or_default(),
|
||||
),
|
||||
},
|
||||
toggle(
|
||||
t!("Background fill").as_ref(),
|
||||
"tbl_cell_fill",
|
||||
cell_style.is_some_and(|style| style.fill_enabled),
|
||||
),
|
||||
ro(
|
||||
t!("Linked cells").as_ref(),
|
||||
"tbl_linked_cells",
|
||||
linked_cell_count.to_string(),
|
||||
toggle(
|
||||
t!("Top border").as_ref(),
|
||||
"tbl_cell_border_top",
|
||||
cell_style.is_none_or(|style| !style.top_border.invisible),
|
||||
),
|
||||
ro(t!("Direction").as_ref(), "tbl_direction", format!("{:.4}", direction_deg)),
|
||||
ro(
|
||||
t!("Table width").as_ref(),
|
||||
"tbl_width",
|
||||
format!("{:.4}", self.total_width()),
|
||||
toggle(
|
||||
t!("Right border").as_ref(),
|
||||
"tbl_cell_border_right",
|
||||
cell_style.is_none_or(|style| !style.right_border.invisible),
|
||||
),
|
||||
ro(
|
||||
t!("Table height").as_ref(),
|
||||
"tbl_height",
|
||||
format!("{:.4}", self.total_height()),
|
||||
toggle(
|
||||
t!("Bottom border").as_ref(),
|
||||
"tbl_cell_border_bottom",
|
||||
cell_style.is_none_or(|style| !style.bottom_border.invisible),
|
||||
),
|
||||
toggle(
|
||||
t!("Left border").as_ref(),
|
||||
"tbl_cell_border_left",
|
||||
cell_style.is_none_or(|style| !style.left_border.invisible),
|
||||
),
|
||||
toggle(t!("Locked").as_ref(), "tbl_cell_locked", cell_locked),
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
@@ -1863,11 +2118,19 @@ impl PropertyEditable for Table {
|
||||
"tbl_break_enabled",
|
||||
self.break_options.contains(BreakOptionFlags::ENABLE_BREAKS),
|
||||
),
|
||||
ro(
|
||||
t!("Direction").as_ref(),
|
||||
"tbl_break_direction",
|
||||
format!("{:?}", self.break_flow_direction),
|
||||
),
|
||||
Property {
|
||||
label: t!("Direction").into_owned(),
|
||||
field: "tbl_break_direction",
|
||||
value: PropValue::Choice {
|
||||
selected: match self.break_flow_direction {
|
||||
acadrust::entities::table::BreakFlowDirection::Right => "Right",
|
||||
acadrust::entities::table::BreakFlowDirection::Left => "Left",
|
||||
acadrust::entities::table::BreakFlowDirection::Vertical => "Down",
|
||||
}
|
||||
.to_string(),
|
||||
options: vec!["Right".into(), "Left".into(), "Down".into()],
|
||||
},
|
||||
},
|
||||
toggle(
|
||||
t!("Repeat top labels").as_ref(),
|
||||
"tbl_break_repeat_top",
|
||||
@@ -1892,12 +2155,7 @@ impl PropertyEditable for Table {
|
||||
self.break_options
|
||||
.contains(BreakOptionFlags::ALLOW_MANUAL_HEIGHTS),
|
||||
),
|
||||
ro(
|
||||
t!("Segments").as_ref(),
|
||||
"tbl_break_segments",
|
||||
self.break_ranges.len().max(self.break_data.len()).to_string(),
|
||||
),
|
||||
ro(t!("Break heights").as_ref(), "tbl_break_height", break_heights),
|
||||
edit(t!("Maximum height").as_ref(), "tbl_break_height", break_height),
|
||||
edit(t!("Spacing").as_ref(), "tbl_break_spacing", self.break_spacing),
|
||||
],
|
||||
},
|
||||
@@ -1924,11 +2182,198 @@ impl PropertyEditable for Table {
|
||||
self.break_options.set(flag, on);
|
||||
return;
|
||||
}
|
||||
if field == "tbl_break_spacing" {
|
||||
if let Some(v) = parse_f64(value) {
|
||||
self.break_spacing = v;
|
||||
let cell_index = prop_current_cell();
|
||||
let columns = self.columns.len();
|
||||
let cell_position = (columns > 0).then_some((cell_index / columns, cell_index % columns));
|
||||
if let Some((row, column)) = cell_position {
|
||||
match field {
|
||||
"tbl_cell_text" => {
|
||||
if let Some(cell) = self.cell_mut(row, column) {
|
||||
use acadrust::entities::table::CellStateFlags;
|
||||
if !cell.state.intersects(
|
||||
CellStateFlags::CONTENT_LOCKED
|
||||
| CellStateFlags::CONTENT_READ_ONLY,
|
||||
) {
|
||||
cell.set_text(value);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
"tbl_cell_alignment" => {
|
||||
let alignment = match value.trim().to_ascii_uppercase().as_str() {
|
||||
"TOP LEFT" => 1,
|
||||
"TOP CENTER" => 2,
|
||||
"TOP RIGHT" => 3,
|
||||
"MIDDLE LEFT" => 4,
|
||||
"MIDDLE RIGHT" => 6,
|
||||
"BOTTOM LEFT" => 7,
|
||||
"BOTTOM CENTER" => 8,
|
||||
"BOTTOM RIGHT" => 9,
|
||||
_ => 5,
|
||||
};
|
||||
if let Some(cell) = self.cell_mut(row, column) {
|
||||
let style = cell.style.get_or_insert_with(Default::default);
|
||||
style.alignment = alignment;
|
||||
style.property_flags.insert(
|
||||
acadrust::entities::table::CellStylePropertyFlags::ALIGNMENT,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
"tbl_cell_format" => {
|
||||
if let Some(cell) = self.cell_mut(row, column) {
|
||||
let style = cell.style.get_or_insert_with(Default::default);
|
||||
style.value_format = value.to_string();
|
||||
style.property_flags.insert(
|
||||
acadrust::entities::table::CellStylePropertyFlags::DATA_FORMAT,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
"tbl_cell_fill" => {
|
||||
if let Some(cell) = self.cell_mut(row, column) {
|
||||
let style = cell.style.get_or_insert_with(Default::default);
|
||||
style.fill_enabled = if value == "toggle" {
|
||||
!style.fill_enabled
|
||||
} else {
|
||||
value == "true"
|
||||
};
|
||||
style.property_flags.insert(
|
||||
acadrust::entities::table::CellStylePropertyFlags::BACKGROUND_COLOR,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
"tbl_cell_border_top"
|
||||
| "tbl_cell_border_right"
|
||||
| "tbl_cell_border_bottom"
|
||||
| "tbl_cell_border_left" => {
|
||||
if let Some(cell) = self.cell_mut(row, column) {
|
||||
use acadrust::entities::table::{
|
||||
BorderPropertyFlags, CellEdgeFlags,
|
||||
};
|
||||
let style = cell.style.get_or_insert_with(Default::default);
|
||||
let (border, edge) = match field {
|
||||
"tbl_cell_border_top" => (&mut style.top_border, CellEdgeFlags::TOP),
|
||||
"tbl_cell_border_right" => {
|
||||
(&mut style.right_border, CellEdgeFlags::RIGHT)
|
||||
}
|
||||
"tbl_cell_border_bottom" => {
|
||||
(&mut style.bottom_border, CellEdgeFlags::BOTTOM)
|
||||
}
|
||||
_ => (&mut style.left_border, CellEdgeFlags::LEFT),
|
||||
};
|
||||
let visible = if value == "toggle" {
|
||||
border.invisible
|
||||
} else {
|
||||
value == "true"
|
||||
};
|
||||
border.invisible = !visible;
|
||||
border.override_flags.insert(BorderPropertyFlags::INVISIBILITY);
|
||||
style.applied_border_edges.insert(edge);
|
||||
}
|
||||
return;
|
||||
}
|
||||
"tbl_cell_locked" => {
|
||||
if let Some(cell) = self.cell_mut(row, column) {
|
||||
use acadrust::entities::table::CellStateFlags;
|
||||
let locked = if value == "toggle" {
|
||||
!cell.state.contains(CellStateFlags::CONTENT_LOCKED)
|
||||
} else {
|
||||
value == "true"
|
||||
};
|
||||
cell.state.set(CellStateFlags::CONTENT_LOCKED, locked);
|
||||
cell.state.set(CellStateFlags::FORMAT_LOCKED, locked);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let Some(number) = parse_f64(value) else {
|
||||
if field == "tbl_break_direction" {
|
||||
self.break_flow_direction = match value.trim().to_ascii_uppercase().as_str() {
|
||||
"LEFT" => acadrust::entities::table::BreakFlowDirection::Left,
|
||||
"DOWN" | "VERTICAL" => {
|
||||
acadrust::entities::table::BreakFlowDirection::Vertical
|
||||
}
|
||||
_ => acadrust::entities::table::BreakFlowDirection::Right,
|
||||
};
|
||||
}
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"tbl_insert_x" => self.insertion_point.x = number,
|
||||
"tbl_insert_y" => self.insertion_point.y = number,
|
||||
"tbl_insert_z" => self.insertion_point.z = number,
|
||||
"tbl_direction" => {
|
||||
let radians = number.to_radians();
|
||||
self.horizontal_direction.x = radians.cos();
|
||||
self.horizontal_direction.y = radians.sin();
|
||||
self.horizontal_direction.z = 0.0;
|
||||
}
|
||||
"tbl_rows" => {
|
||||
let requested = number.round().max(1.0) as usize;
|
||||
while self.rows.len() < requested {
|
||||
self.add_row();
|
||||
}
|
||||
while self.rows.len() > requested {
|
||||
self.remove_row(self.rows.len() - 1);
|
||||
}
|
||||
}
|
||||
"tbl_cols" => {
|
||||
let requested = number.round().max(1.0) as usize;
|
||||
let width = self.columns.last().map(|column| column.width).unwrap_or(2.0);
|
||||
while self.columns.len() < requested {
|
||||
self.add_column(width);
|
||||
}
|
||||
while self.columns.len() > requested {
|
||||
self.remove_column(self.columns.len() - 1);
|
||||
}
|
||||
}
|
||||
"tbl_width" if number > 0.0 => {
|
||||
let current = self.total_width();
|
||||
if current > 1.0e-12 {
|
||||
for column in &mut self.columns {
|
||||
column.width *= number / current;
|
||||
}
|
||||
}
|
||||
}
|
||||
"tbl_height" if number > 0.0 => {
|
||||
let current = self.total_height();
|
||||
if current > 1.0e-12 {
|
||||
for row in &mut self.rows {
|
||||
row.height *= number / current;
|
||||
}
|
||||
}
|
||||
}
|
||||
"tbl_cell_text_height" if number > 0.0 => {
|
||||
if let Some((row, column)) = cell_position {
|
||||
if let Some(cell) = self.cell_mut(row, column) {
|
||||
let style = cell.style.get_or_insert_with(Default::default);
|
||||
style.text_height = number;
|
||||
style.property_flags.insert(
|
||||
acadrust::entities::table::CellStylePropertyFlags::TEXT_HEIGHT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
"tbl_break_height" if number >= 0.0 => {
|
||||
if self.break_data.is_empty() {
|
||||
self.break_data.push(acadrust::entities::table::TableBreakData {
|
||||
position: acadrust::types::Vector3::ZERO,
|
||||
height: number,
|
||||
flags: 0,
|
||||
});
|
||||
} else {
|
||||
for data in &mut self.break_data {
|
||||
data.height = number;
|
||||
}
|
||||
}
|
||||
}
|
||||
"tbl_break_spacing" if number >= 0.0 => self.break_spacing = number,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// DATALINK command — manages data links to external spreadsheets.
|
||||
// DATALINK command — manages persistent links to external tabular data.
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use acadrust::entities::Table;
|
||||
use acadrust::EntityType;
|
||||
use glam::DVec3;
|
||||
|
||||
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/data_link.svg"));
|
||||
|
||||
@@ -12,3 +17,112 @@ pub fn tool() -> ToolDef {
|
||||
event: ModuleEvent::Command("DATALINK".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DataLinkPlaceCommand {
|
||||
table: Table,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl DataLinkPlaceCommand {
|
||||
pub fn new(mut table: Table, path: &str) -> Self {
|
||||
table.name = format!("__OPENCAD_LINK_PENDING__{path}");
|
||||
Self {
|
||||
table,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn preview(&self, point: DVec3) -> WireModel {
|
||||
let point = self.plane.to_local(point);
|
||||
let mut points = Vec::new();
|
||||
let mut x = 0.0;
|
||||
for column in 0..=self.table.column_count() {
|
||||
if column > 0 {
|
||||
x += self.table.columns[column - 1].width;
|
||||
}
|
||||
points.push(self.plane.to_world(point + DVec3::X * x).as_vec3().to_array());
|
||||
points.push(
|
||||
self.plane
|
||||
.to_world(point + DVec3::new(x, -self.table.total_height(), 0.0))
|
||||
.as_vec3()
|
||||
.to_array(),
|
||||
);
|
||||
}
|
||||
let mut y = 0.0;
|
||||
for row in 0..=self.table.row_count() {
|
||||
if row > 0 {
|
||||
y -= self.table.rows[row - 1].height;
|
||||
}
|
||||
points.push(self.plane.to_world(point + DVec3::Y * y).as_vec3().to_array());
|
||||
points.push(
|
||||
self.plane
|
||||
.to_world(point + DVec3::new(self.table.total_width(), y, 0.0))
|
||||
.as_vec3()
|
||||
.to_array(),
|
||||
);
|
||||
}
|
||||
WireModel {
|
||||
point_marker: None,
|
||||
taper_widths: Vec::new(),
|
||||
world_width: 0.0,
|
||||
depth_override: None,
|
||||
display_visible: true,
|
||||
plot_visible: true,
|
||||
fill_is_3d: false,
|
||||
fill_is_2d_solid: false,
|
||||
render_instance: None,
|
||||
pick_tris: Vec::new(),
|
||||
pick_tris_low: Vec::new(),
|
||||
dash_from_start: false,
|
||||
dash_align_end: None,
|
||||
text_verts: Vec::new(),
|
||||
name: "data_link_preview".into(),
|
||||
points,
|
||||
points_low: Vec::new(),
|
||||
color: WireModel::CYAN,
|
||||
selected: false,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: Vec::new(),
|
||||
tangent_geoms: Vec::new(),
|
||||
aci: 0,
|
||||
key_vertices: Vec::new(),
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
fill_tris: Vec::new(),
|
||||
fill_tris_low: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DataLinkPlaceCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"DATALINK"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
"DATALINK Specify insertion point:".to_string()
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
let point = self.plane.to_local(point);
|
||||
self.table.insertion_point = acadrust::types::Vector3::new(point.x, point.y, point.z);
|
||||
CmdResult::CommitAndExit(
|
||||
self.plane
|
||||
.place_entity(EntityType::Table(self.table.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||
Some(self.preview(point))
|
||||
}
|
||||
}
|
||||
+412
-123
@@ -1,16 +1,14 @@
|
||||
// TABLE command — create an empty table entity.
|
||||
// TABLE command — create a styled empty table.
|
||||
//
|
||||
// Workflow:
|
||||
// 1. Text: Enter number of columns (default 3)
|
||||
// 2. Text: Enter number of rows (default 4, includes header row)
|
||||
// 3. Point: Click insertion point
|
||||
//
|
||||
// Creates a Table entity with uniform row height (0.5) and column width (2.0).
|
||||
// Title/header rows come from the selected table style and are not counted as
|
||||
// data rows. The command remembers its previous settings and supports either a
|
||||
// fixed insertion point or a two-corner sizing window.
|
||||
|
||||
use acadrust::entities::TableBuilder;
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use glam::DVec3;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
@@ -29,22 +27,80 @@ pub fn tool() -> ToolDef {
|
||||
}
|
||||
|
||||
const DEFAULT_COLS: usize = 3;
|
||||
const DEFAULT_ROWS: usize = 4;
|
||||
const COL_WIDTH: f64 = 2.0;
|
||||
const ROW_HEIGHT: f64 = 0.5;
|
||||
const DEFAULT_DATA_ROWS: usize = 4;
|
||||
const DEFAULT_COL_WIDTH: f64 = 2.0;
|
||||
const DEFAULT_ROW_HEIGHT: f64 = 0.5;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum InsertionMode {
|
||||
Point,
|
||||
Window,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct TableDefaults {
|
||||
columns: usize,
|
||||
data_rows: usize,
|
||||
column_width: f64,
|
||||
row_height: f64,
|
||||
insertion_mode: InsertionMode,
|
||||
}
|
||||
|
||||
impl Default for TableDefaults {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
columns: DEFAULT_COLS,
|
||||
data_rows: DEFAULT_DATA_ROWS,
|
||||
column_width: DEFAULT_COL_WIDTH,
|
||||
row_height: DEFAULT_ROW_HEIGHT,
|
||||
insertion_mode: InsertionMode::Point,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn saved_defaults() -> &'static Mutex<TableDefaults> {
|
||||
static DEFAULTS: OnceLock<Mutex<TableDefaults>> = OnceLock::new();
|
||||
DEFAULTS.get_or_init(|| Mutex::new(TableDefaults::default()))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Step {
|
||||
Columns,
|
||||
Rows { cols: usize },
|
||||
Insertion { cols: usize, rows: usize },
|
||||
DataRows { columns: usize },
|
||||
ColumnWidth { columns: usize, data_rows: usize },
|
||||
RowHeight {
|
||||
columns: usize,
|
||||
data_rows: usize,
|
||||
column_width: f64,
|
||||
},
|
||||
InsertionMode {
|
||||
columns: usize,
|
||||
data_rows: usize,
|
||||
column_width: f64,
|
||||
row_height: f64,
|
||||
},
|
||||
Insertion {
|
||||
columns: usize,
|
||||
data_rows: usize,
|
||||
column_width: f64,
|
||||
row_height: f64,
|
||||
},
|
||||
WindowFirst { columns: usize, data_rows: usize },
|
||||
WindowSecond {
|
||||
columns: usize,
|
||||
data_rows: usize,
|
||||
first: DVec3,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct TableCommand {
|
||||
step: Step,
|
||||
style_handle: Option<acadrust::Handle>,
|
||||
column_width: f64,
|
||||
row_height: f64,
|
||||
suggested_column_width: f64,
|
||||
suggested_row_height: f64,
|
||||
preview_scale: f64,
|
||||
title_row: bool,
|
||||
header_row: bool,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
@@ -53,9 +109,11 @@ impl TableCommand {
|
||||
Self {
|
||||
step: Step::Columns,
|
||||
style_handle: None,
|
||||
column_width: COL_WIDTH,
|
||||
row_height: ROW_HEIGHT,
|
||||
suggested_column_width: DEFAULT_COL_WIDTH,
|
||||
suggested_row_height: DEFAULT_ROW_HEIGHT,
|
||||
preview_scale: 1.0,
|
||||
title_row: true,
|
||||
header_row: true,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
@@ -71,21 +129,120 @@ impl TableCommand {
|
||||
.max(style.header_row_style.text_height)
|
||||
.max(style.title_row_style.text_height)
|
||||
.max(1.0e-6);
|
||||
let row_height = text_height * 1.5 + style.vertical_margin * 2.0;
|
||||
let column_width = text_height * 8.0 + style.horizontal_margin * 2.0;
|
||||
Self {
|
||||
step: Step::Columns,
|
||||
style_handle: Some(style_handle),
|
||||
column_width,
|
||||
row_height,
|
||||
suggested_column_width: text_height * 8.0 + style.horizontal_margin * 2.0,
|
||||
suggested_row_height: text_height * 1.5 + style.vertical_margin * 2.0,
|
||||
preview_scale: if style.annotative {
|
||||
annotation_multiplier
|
||||
} else {
|
||||
1.0
|
||||
},
|
||||
title_row: !style.title_suppressed,
|
||||
header_row: !style.header_suppressed,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn defaults(&self) -> TableDefaults {
|
||||
let mut defaults = saved_defaults().lock().map(|value| *value).unwrap_or_default();
|
||||
if (defaults.column_width - DEFAULT_COL_WIDTH).abs() < 1.0e-9 {
|
||||
defaults.column_width = self.suggested_column_width;
|
||||
}
|
||||
if (defaults.row_height - DEFAULT_ROW_HEIGHT).abs() < 1.0e-9 {
|
||||
defaults.row_height = self.suggested_row_height;
|
||||
}
|
||||
defaults
|
||||
}
|
||||
|
||||
fn total_rows(&self, data_rows: usize) -> usize {
|
||||
data_rows + usize::from(self.title_row) + usize::from(self.header_row)
|
||||
}
|
||||
|
||||
fn build_table(
|
||||
&self,
|
||||
point: DVec3,
|
||||
rows: usize,
|
||||
columns: usize,
|
||||
column_width: f64,
|
||||
row_height: f64,
|
||||
) -> EntityType {
|
||||
let point = self.plane.to_local(point);
|
||||
let mut table = TableBuilder::new(rows, columns)
|
||||
.at(Vector3::new(point.x, point.y, point.z))
|
||||
.row_height(row_height)
|
||||
.column_width(column_width)
|
||||
.build();
|
||||
table.table_style_handle = self.style_handle;
|
||||
self.plane.place_entity(EntityType::Table(table))
|
||||
}
|
||||
|
||||
fn preview_grid(
|
||||
&self,
|
||||
point: DVec3,
|
||||
rows: usize,
|
||||
columns: usize,
|
||||
column_width: f64,
|
||||
row_height: f64,
|
||||
) -> WireModel {
|
||||
let point = self.plane.to_local(point);
|
||||
let width = columns as f64 * column_width * self.preview_scale;
|
||||
let height = rows as f64 * row_height * self.preview_scale;
|
||||
let mut points = Vec::with_capacity((rows + columns + 2) * 2);
|
||||
for column in 0..=columns {
|
||||
let x = column as f64 * column_width * self.preview_scale;
|
||||
points.push(self.plane.to_world(point + DVec3::X * x).as_vec3().to_array());
|
||||
points.push(
|
||||
self.plane
|
||||
.to_world(point + DVec3::new(x, -height, 0.0))
|
||||
.as_vec3()
|
||||
.to_array(),
|
||||
);
|
||||
}
|
||||
for row in 0..=rows {
|
||||
let y = -(row as f64 * row_height * self.preview_scale);
|
||||
points.push(self.plane.to_world(point + DVec3::Y * y).as_vec3().to_array());
|
||||
points.push(
|
||||
self.plane
|
||||
.to_world(point + DVec3::new(width, y, 0.0))
|
||||
.as_vec3()
|
||||
.to_array(),
|
||||
);
|
||||
}
|
||||
WireModel {
|
||||
point_marker: None,
|
||||
taper_widths: Vec::new(),
|
||||
world_width: 0.0,
|
||||
depth_override: None,
|
||||
display_visible: true,
|
||||
plot_visible: true,
|
||||
fill_is_3d: false,
|
||||
fill_is_2d_solid: false,
|
||||
render_instance: None,
|
||||
pick_tris: Vec::new(),
|
||||
pick_tris_low: Vec::new(),
|
||||
dash_from_start: false,
|
||||
dash_align_end: None,
|
||||
text_verts: Vec::new(),
|
||||
name: "table_preview".into(),
|
||||
points,
|
||||
points_low: Vec::new(),
|
||||
color: WireModel::CYAN,
|
||||
selected: false,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: Vec::new(),
|
||||
tangent_geoms: Vec::new(),
|
||||
aci: 0,
|
||||
key_vertices: Vec::new(),
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
fill_tris: Vec::new(),
|
||||
fill_tris_low: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for TableCommand {
|
||||
@@ -98,146 +255,278 @@ impl CadCommand for TableCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
let defaults = self.defaults();
|
||||
match &self.step {
|
||||
Step::Columns => t!(
|
||||
"TABLE Enter number of columns [%{cols}]:",
|
||||
cols = DEFAULT_COLS
|
||||
cols = defaults.columns
|
||||
)
|
||||
.into_owned(),
|
||||
Step::Rows { cols } => t!(
|
||||
"TABLE Enter number of rows (incl. header) [%{rows}] (%{cols} cols):",
|
||||
rows = DEFAULT_ROWS,
|
||||
cols = cols
|
||||
Step::DataRows { columns } => t!(
|
||||
"TABLE Enter number of data rows [%{rows}] (%{cols} cols):",
|
||||
rows = defaults.data_rows,
|
||||
cols = columns
|
||||
)
|
||||
.into_owned(),
|
||||
Step::Insertion { cols, rows } => t!(
|
||||
Step::ColumnWidth { .. } => {
|
||||
format!("TABLE Enter column width [{:.4}]:", defaults.column_width)
|
||||
}
|
||||
Step::RowHeight { .. } => {
|
||||
format!("TABLE Enter row height [{:.4}]:", defaults.row_height)
|
||||
}
|
||||
Step::InsertionMode { .. } => format!(
|
||||
"TABLE Insertion behavior [Point/Window] <{}>:",
|
||||
match defaults.insertion_mode {
|
||||
InsertionMode::Point => "Point",
|
||||
InsertionMode::Window => "Window",
|
||||
}
|
||||
),
|
||||
Step::Insertion { columns, data_rows, .. } => t!(
|
||||
"TABLE Specify insertion point [%{cols}×%{rows}]:",
|
||||
cols = cols,
|
||||
rows = rows
|
||||
cols = columns,
|
||||
rows = self.total_rows(*data_rows)
|
||||
)
|
||||
.into_owned(),
|
||||
Step::WindowFirst { .. } => "TABLE Specify first corner:".to_string(),
|
||||
Step::WindowSecond { .. } => "TABLE Specify opposite corner:".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
!matches!(self.step, Step::Insertion { .. })
|
||||
matches!(
|
||||
self.step,
|
||||
Step::Columns
|
||||
| Step::DataRows { .. }
|
||||
| Step::ColumnWidth { .. }
|
||||
| Step::RowHeight { .. }
|
||||
| Step::InsertionMode { .. }
|
||||
)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let t = text.trim();
|
||||
match &self.step {
|
||||
let input = text.trim();
|
||||
let defaults = self.defaults();
|
||||
match self.step {
|
||||
Step::Columns => {
|
||||
let cols = if t.is_empty() {
|
||||
DEFAULT_COLS
|
||||
let columns = if input.is_empty() {
|
||||
defaults.columns
|
||||
} else {
|
||||
t.parse::<usize>().ok().filter(|&n| n >= 1)?
|
||||
input.parse::<usize>().ok().filter(|value| *value > 0)?
|
||||
};
|
||||
self.step = Step::Rows { cols };
|
||||
Some(CmdResult::NeedPoint)
|
||||
self.step = Step::DataRows { columns };
|
||||
}
|
||||
Step::Rows { cols } => {
|
||||
let cols = *cols;
|
||||
let rows = if t.is_empty() {
|
||||
DEFAULT_ROWS
|
||||
Step::DataRows { columns } => {
|
||||
let data_rows = if input.is_empty() {
|
||||
defaults.data_rows
|
||||
} else {
|
||||
t.parse::<usize>().ok().filter(|&n| n >= 1)?
|
||||
input.parse::<usize>().ok().filter(|value| *value > 0)?
|
||||
};
|
||||
self.step = Step::Insertion { cols, rows };
|
||||
Some(CmdResult::NeedPoint)
|
||||
self.step = Step::ColumnWidth { columns, data_rows };
|
||||
}
|
||||
Step::ColumnWidth { columns, data_rows } => {
|
||||
let column_width = if input.is_empty() {
|
||||
defaults.column_width
|
||||
} else {
|
||||
input.parse::<f64>().ok().filter(|value| *value > 0.0)?
|
||||
};
|
||||
self.step = Step::RowHeight {
|
||||
columns,
|
||||
data_rows,
|
||||
column_width,
|
||||
};
|
||||
}
|
||||
Step::RowHeight { columns, data_rows, column_width } => {
|
||||
let row_height = if input.is_empty() {
|
||||
defaults.row_height
|
||||
} else {
|
||||
input.parse::<f64>().ok().filter(|value| *value > 0.0)?
|
||||
};
|
||||
self.step = Step::InsertionMode {
|
||||
columns,
|
||||
data_rows,
|
||||
column_width,
|
||||
row_height,
|
||||
};
|
||||
}
|
||||
Step::InsertionMode { columns, data_rows, column_width, row_height } => {
|
||||
let mode = if input.is_empty() {
|
||||
defaults.insertion_mode
|
||||
} else if "POINT".starts_with(&input.to_ascii_uppercase()) {
|
||||
InsertionMode::Point
|
||||
} else if "WINDOW".starts_with(&input.to_ascii_uppercase()) {
|
||||
InsertionMode::Window
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
if let Ok(mut saved) = saved_defaults().lock() {
|
||||
*saved = TableDefaults {
|
||||
columns,
|
||||
data_rows,
|
||||
column_width,
|
||||
row_height,
|
||||
insertion_mode: mode,
|
||||
};
|
||||
}
|
||||
self.step = match mode {
|
||||
InsertionMode::Point => Step::Insertion {
|
||||
columns,
|
||||
data_rows,
|
||||
column_width,
|
||||
row_height,
|
||||
},
|
||||
InsertionMode::Window => Step::WindowFirst { columns, data_rows },
|
||||
};
|
||||
}
|
||||
Step::Insertion { .. } | Step::WindowFirst { .. } | Step::WindowSecond { .. } => {
|
||||
return None;
|
||||
}
|
||||
Step::Insertion { .. } => None,
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match &self.step {
|
||||
Step::Columns => {
|
||||
// Accept default.
|
||||
self.step = Step::Rows { cols: DEFAULT_COLS };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Rows { cols } => {
|
||||
let cols = *cols;
|
||||
self.step = Step::Insertion {
|
||||
cols,
|
||||
rows: DEFAULT_ROWS,
|
||||
self.on_text_input("")
|
||||
.map_or(CmdResult::Cancel, |_| CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
Step::Insertion {
|
||||
columns,
|
||||
data_rows,
|
||||
column_width,
|
||||
row_height,
|
||||
} => CmdResult::CommitAndExit(self.build_table(
|
||||
point,
|
||||
self.total_rows(data_rows),
|
||||
columns,
|
||||
column_width,
|
||||
row_height,
|
||||
)),
|
||||
Step::WindowFirst { columns, data_rows } => {
|
||||
self.step = Step::WindowSecond {
|
||||
columns,
|
||||
data_rows,
|
||||
first: point,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Insertion { .. } => CmdResult::Cancel,
|
||||
Step::WindowSecond { columns, data_rows, first } => {
|
||||
let first = self.plane.to_local(first);
|
||||
let second = self.plane.to_local(point);
|
||||
let rows = self.total_rows(data_rows);
|
||||
let width = (second.x - first.x).abs().max(1.0e-6);
|
||||
let height = (second.y - first.y).abs().max(1.0e-6);
|
||||
let top_left = DVec3::new(first.x.min(second.x), first.y.max(second.y), first.z);
|
||||
CmdResult::CommitAndExit(self.build_table(
|
||||
self.plane.to_world(top_left),
|
||||
rows,
|
||||
columns,
|
||||
width / columns as f64,
|
||||
height / rows as f64,
|
||||
))
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if let Step::Insertion { cols, rows } = self.step {
|
||||
let point = self.plane.to_local(pt);
|
||||
let ins = Vector3::new(point.x, point.y, point.z);
|
||||
let mut table = TableBuilder::new(rows, cols)
|
||||
.at(ins)
|
||||
.row_height(self.row_height)
|
||||
.column_width(self.column_width)
|
||||
.build();
|
||||
table.table_style_handle = self.style_handle;
|
||||
CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Table(table)))
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
if let Step::Insertion { cols, rows } = self.step {
|
||||
// Preview: outline of the table bounding box.
|
||||
let w = (cols as f32) * (self.column_width * self.preview_scale) as f32;
|
||||
let h = (rows as f32) * (self.row_height * self.preview_scale) as f32;
|
||||
let point = self.plane.to_local(pt);
|
||||
let corners = [
|
||||
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||
match self.step {
|
||||
Step::Insertion {
|
||||
columns,
|
||||
data_rows,
|
||||
column_width,
|
||||
row_height,
|
||||
} => Some(self.preview_grid(
|
||||
point,
|
||||
point + DVec3::X * w as f64,
|
||||
point + DVec3::new(w as f64, -(h as f64), 0.0),
|
||||
point - DVec3::Y * h as f64,
|
||||
]
|
||||
.map(|corner| self.plane.to_world(corner).as_vec3().to_array());
|
||||
let [p0, p1, p2, p3] = corners;
|
||||
Some(WireModel {
|
||||
point_marker: None,
|
||||
taper_widths: Vec::new(),
|
||||
world_width: 0.0,
|
||||
depth_override: None,
|
||||
display_visible: true,
|
||||
plot_visible: true,
|
||||
fill_is_3d: false,
|
||||
fill_is_2d_solid: false,
|
||||
render_instance: None,
|
||||
pick_tris: Vec::new(),
|
||||
pick_tris_low: Vec::new(),
|
||||
dash_from_start: false,
|
||||
dash_align_end: None,
|
||||
text_verts: Vec::new(),
|
||||
name: "table_preview".into(),
|
||||
points: vec![
|
||||
p0, p1, p1, p2, p2, p3, p3, p0,
|
||||
],
|
||||
points_low: Vec::new(),
|
||||
color: WireModel::CYAN,
|
||||
selected: false,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
fill_tris: vec![],
|
||||
fill_tris_low: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
self.total_rows(data_rows),
|
||||
columns,
|
||||
column_width,
|
||||
row_height,
|
||||
)),
|
||||
Step::WindowSecond { columns, data_rows, first } => {
|
||||
let first = self.plane.to_local(first);
|
||||
let second = self.plane.to_local(point);
|
||||
let rows = self.total_rows(data_rows);
|
||||
let width = (second.x - first.x).abs().max(1.0e-6);
|
||||
let height = (second.y - first.y).abs().max(1.0e-6);
|
||||
let top_left = DVec3::new(first.x.min(second.x), first.y.max(second.y), first.z);
|
||||
Some(self.preview_grid(
|
||||
self.plane.to_world(top_left),
|
||||
rows,
|
||||
columns,
|
||||
width / columns as f64,
|
||||
height / rows as f64,
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["TABLE"] });
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["TABLE"] }); // TableCommand
|
||||
pub struct TableCellEditCommand {
|
||||
handle: acadrust::Handle,
|
||||
table: acadrust::entities::Table,
|
||||
row: usize,
|
||||
column: usize,
|
||||
}
|
||||
|
||||
impl TableCellEditCommand {
|
||||
pub fn new(
|
||||
handle: acadrust::Handle,
|
||||
table: &acadrust::entities::Table,
|
||||
row: usize,
|
||||
column: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
handle,
|
||||
table: table.clone(),
|
||||
row,
|
||||
column,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for TableCellEditCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"TABLE CELL"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
let current = self.table.cell_text(self.row, self.column).unwrap_or("");
|
||||
format!(
|
||||
"TABLE CELL Enter text for [{},{}] <{}>:",
|
||||
self.row, self.column, current
|
||||
)
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
use acadrust::entities::table::CellStateFlags;
|
||||
let cell = self.table.cell_mut(self.row, self.column)?;
|
||||
if cell
|
||||
.state
|
||||
.intersects(CellStateFlags::CONTENT_LOCKED | CellStateFlags::CONTENT_READ_ONLY)
|
||||
{
|
||||
return Some(CmdResult::Cancel);
|
||||
}
|
||||
cell.set_text(text);
|
||||
Some(CmdResult::ReplaceMany(
|
||||
vec![(self.handle, vec![EntityType::Table(self.table.clone())])],
|
||||
Vec::new(),
|
||||
))
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user