Merge main into codex/graphic-attributes-cleanup

Resolve conflicts from main's rustfmt pass by taking main's versions of
command.rs, style.rs, view/mod.rs, scene/mod.rs, ui/window/mod.rs and
ui/properties.rs and re-applying this branch's Graphic Attributes changes.
Add the Graphic Attributes strings to the new lo-LA catalog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E1QnME6WJbD5iGFae1eeCb
This commit is contained in:
Claude committed 2026-10-11 19:09:37 +00:00
commit 44f8f76ab3
596 files changed
+69069 -34120

No files matched your search

+364 -205
View File
@@ -8,8 +8,9 @@ impl OpenCADStudio {
"PAN" => {
self.tabs[i].pan_mode = true;
self.clear_navigation_hover(i);
self.command_line
.push_output(crate::t!("PAN: drag with the left mouse button. Press Esc to exit.").as_ref());
self.command_line.push_output(
crate::t!("PAN: drag with the left mouse button. Press Esc to exit.").as_ref(),
);
}
// ── TABLE structure and cell editing ───────────────────────────────
@@ -33,12 +34,17 @@ impl OpenCADStudio {
}
let words: Vec<&str> = rest.split_whitespace().collect();
let integer = |index: usize| {
words.get(index).and_then(|value| value.parse::<usize>().ok())
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() {
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)) {
@@ -117,11 +123,7 @@ impl OpenCADStudio {
u64::from_str_radix(value.trim_start_matches("0x"), 16)
.ok()
.map(|handle| {
TableAction::Field(
row,
column,
codec::Handle::new(handle),
)
TableAction::Field(row, column, codec::Handle::new(handle))
})
}
_ => None,
@@ -158,16 +160,19 @@ impl OpenCADStudio {
None
};
if matches!(action, TableAction::Block(_, _, _)) && block_handle.is_none() {
self.command_line.push_error(
crate::t!("TABLE: block definition not found.").as_ref(),
);
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(),
);
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());
}
}
@@ -337,10 +342,10 @@ impl OpenCADStudio {
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());
self.command_line.push_error(
crate::t!("TABLE: row/column is out of range or the cell is locked.")
.as_ref(),
);
}
}
@@ -388,9 +393,10 @@ impl OpenCADStudio {
}
}
self.tabs[i].dirty = true;
self.command_line.push_output(crate::tf!(
"UCSICON {sub}: updated {count} viewport(s) + model space."
).as_ref());
self.command_line.push_output(
crate::tf!("UCSICON {sub}: updated {count} viewport(s) + model space.")
.as_ref(),
);
}
"" => {
// Bare UCSICON toggles visibility.
@@ -405,11 +411,13 @@ impl OpenCADStudio {
}
self.tabs[i].dirty = true;
let state = if visible { "ON" } else { "OFF" };
self.command_line.push_output(crate::tf!("UCSICON {state}").as_ref());
self.command_line
.push_output(crate::tf!("UCSICON {state}").as_ref());
}
_ => {
self.command_line
.push_info(crate::t!("Usage: UCSICON ON | OFF | NOORIGIN | ORIGIN").as_ref());
self.command_line.push_info(
crate::t!("Usage: UCSICON ON | OFF | NOORIGIN | ORIGIN").as_ref(),
);
}
}
}
@@ -437,40 +445,52 @@ impl OpenCADStudio {
self.tabs[i].scene.set_drawing_limit_check(enabled);
self.tabs[i].dirty = true;
}
self.command_line.push_output(crate::t!(if enabled {
"Limits checking ON."
} else {
"Limits checking OFF."
}).as_ref());
self.command_line.push_output(
crate::t!(if enabled {
"Limits checking ON."
} else {
"Limits checking OFF."
})
.as_ref(),
);
}
cmd if cmd.starts_with("LIMITS SET ") => {
let tokens: Vec<&str> = cmd["LIMITS SET ".len()..].split_whitespace().collect();
let values: Result<Vec<f64>, _> =
tokens.iter().map(|value| value.parse()).collect();
let Ok(values) = values else {
self.command_line
.push_error(crate::t!("LIMITS: four numeric coordinates required.").as_ref());
self.command_line.push_error(
crate::t!("LIMITS: four numeric coordinates required.").as_ref(),
);
return Some(Task::none());
};
if tokens.len() != 4 || !values.iter().all(|value| value.is_finite()) {
self.command_line
.push_error(crate::t!("LIMITS: four finite numeric coordinates required.").as_ref());
self.command_line.push_error(
crate::t!("LIMITS: four finite numeric coordinates required.").as_ref(),
);
} else {
let first = glam::DVec2::new(values[0], values[1]);
let opposite = glam::DVec2::new(values[2], values[3]);
let min = first.min(opposite);
let max = first.max(opposite);
if min.x == max.x || min.y == max.y {
self.command_line
.push_error(crate::t!("LIMITS: corners must define a non-zero area.").as_ref());
self.command_line.push_error(
crate::t!("LIMITS: corners must define a non-zero area.").as_ref(),
);
} else {
self.push_undo_snapshot(i, "LIMITS");
self.tabs[i].scene.set_current_drawing_limits(min, max);
self.tabs[i].dirty = true;
self.command_line.push_output(crate::tf!(
"Drawing limits: {:.4},{:.4} to {:.4},{:.4}.",
min.x, min.y, max.x, max.y
).as_ref());
self.command_line.push_output(
crate::tf!(
"Drawing limits: {:.4},{:.4} to {:.4},{:.4}.",
min.x,
min.y,
max.x,
max.y
)
.as_ref(),
);
}
}
}
@@ -504,15 +524,21 @@ impl OpenCADStudio {
// it only queues a per-viewport cache invalidation.
"REDRAW" => {
use crate::scene::ViewportRefreshScope;
self.tabs[i].scene.request_refresh(ViewportRefreshScope::Active);
self.command_line.push_output(crate::t!("REDRAW: viewport refreshed.").as_ref());
self.tabs[i]
.scene
.request_refresh(ViewportRefreshScope::Active);
self.command_line
.push_output(crate::t!("REDRAW: viewport refreshed.").as_ref());
return Some(Task::none());
}
// REDRAWALL — force re-rasterize of every generated viewport.
"REDRAWALL" => {
use crate::scene::ViewportRefreshScope;
self.tabs[i].scene.request_refresh(ViewportRefreshScope::All);
self.command_line.push_output(crate::t!("REDRAWALL: viewports refreshed.").as_ref());
self.tabs[i]
.scene
.request_refresh(ViewportRefreshScope::All);
self.command_line
.push_output(crate::t!("REDRAWALL: viewports refreshed.").as_ref());
return Some(Task::none());
}
// Rebuild tessellation caches without modifying the document.
@@ -520,7 +546,8 @@ impl OpenCADStudio {
// A regen is a field evaluation event (FIELDEVAL bit 16).
self.tabs[i].scene.update_fields(16, None);
self.tabs[i].scene.populate_meshes_from_document();
self.command_line.push_output(crate::t!("REGEN: regenerated model.").as_ref());
self.command_line
.push_output(crate::t!("REGEN: regenerated model.").as_ref());
return Some(Task::none());
}
@@ -598,8 +625,9 @@ impl OpenCADStudio {
// ── TOOLPALETTES — not yet implemented ───────────────────────────────
"TOOLPALETTES" => {
self.command_line
.push_info(crate::t!("TOOLPALETTES: Tool Palettes not yet implemented.").as_ref());
self.command_line.push_info(
crate::t!("TOOLPALETTES: Tool Palettes not yet implemented.").as_ref(),
);
}
// ── XDATA — read/write extended entity data ──────────────────────────
@@ -640,19 +668,24 @@ impl OpenCADStudio {
if let Some(entity) = self.tabs[i].scene.document.get_entity(*sh) {
let xd = &entity.common().extended_data;
if xd.is_empty() {
self.command_line
.push_output(crate::tf!(" {:x}: no xdata.", sh.value()).as_ref());
self.command_line.push_output(
crate::tf!(" {:x}: no xdata.", sh.value()).as_ref(),
);
} else {
for rec in xd.records() {
self.command_line.push_output(crate::tf!(
" {:x} [{}]: {} value(s)",
sh.value(),
rec.application_name,
rec.values.len()
).as_ref());
self.command_line.push_output(
crate::tf!(
" {:x} [{}]: {} value(s)",
sh.value(),
rec.application_name,
rec.values.len()
)
.as_ref(),
);
for v in &rec.values {
self.command_line
.push_output(crate::tf!(" {:?}", v).as_ref());
self.command_line.push_output(
crate::tf!(" {:?}", v).as_ref(),
);
}
}
}
@@ -685,10 +718,13 @@ impl OpenCADStudio {
}
}
self.tabs[i].dirty = true;
self.command_line.push_output(crate::tf!(
"XDATA: set [{app}] = \"{val}\" on {} entity/entities.",
editable.len()
).as_ref());
self.command_line.push_output(
crate::tf!(
"XDATA: set [{app}] = \"{val}\" on {} entity/entities.",
editable.len()
)
.as_ref(),
);
}
"CLEAR" => {
let app_filter = parts.get(1).copied();
@@ -728,11 +764,14 @@ impl OpenCADStudio {
}
}
self.tabs[i].dirty = true;
self.command_line.push_output(crate::t!("XDATA: cleared.").as_ref());
self.command_line
.push_output(crate::t!("XDATA: cleared.").as_ref());
}
_ => {
self.command_line
.push_info(crate::t!("Usage: XDATA LIST | SET <app> <value> | CLEAR [app]").as_ref());
self.command_line.push_info(
crate::t!("Usage: XDATA LIST | SET <app> <value> | CLEAR [app]")
.as_ref(),
);
}
}
}
@@ -791,7 +830,9 @@ impl OpenCADStudio {
use crate::modules::insert::solid3d_cmds::PresspullCommand;
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
let mut command = PresspullCommand::new(color);
command.set_isolines(crate::entities::solid3d::clamp_header_isolines(self.tabs[i].scene.document.header.isolines));
command.set_isolines(crate::entities::solid3d::clamp_header_isolines(
self.tabs[i].scene.document.header.isolines,
));
command.set_preselection(self.presspull_preselection());
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
@@ -802,7 +843,9 @@ impl OpenCADStudio {
use crate::modules::insert::solid3d_cmds::RevolveCommand;
let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect();
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
let isolines = crate::entities::solid3d::clamp_header_isolines(self.tabs[i].scene.document.header.isolines);
let isolines = crate::entities::solid3d::clamp_header_isolines(
self.tabs[i].scene.document.header.isolines,
);
let mut cmd = RevolveCommand::new(color, isolines);
if !selected.is_empty() {
cmd.set_preselection(
@@ -820,13 +863,23 @@ impl OpenCADStudio {
"SWEEP" => {
use crate::modules::insert::solid3d_cmds::SweepCommand;
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
let isolines = crate::entities::solid3d::clamp_header_isolines(self.tabs[i].scene.document.header.isolines);
let isolines = crate::entities::solid3d::clamp_header_isolines(
self.tabs[i].scene.document.header.isolines,
);
let mut cmd = SweepCommand::new(color, isolines);
cmd.set_parameters(self.tabs[i].scene.named_parameters().clone());
let selected = self.tabs[i].scene.selected_handles_in_order()
let selected = self.tabs[i]
.scene
.selected_handles_in_order()
.into_iter()
.filter_map(|handle| self.tabs[i].scene.document.get_entity(handle)
.cloned().map(|entity| (handle, entity)))
.filter_map(|handle| {
self.tabs[i]
.scene
.document
.get_entity(handle)
.cloned()
.map(|entity| (handle, entity))
})
.collect::<Vec<_>>();
if !selected.is_empty() {
cmd.set_preselection(selected);
@@ -839,13 +892,35 @@ impl OpenCADStudio {
"LOFT" => {
use crate::modules::insert::solid3d_cmds::LoftCommand;
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
let isolines = crate::entities::solid3d::clamp_header_isolines(self.tabs[i].scene.document.header.isolines);
let selected = self.tabs[i].scene.selected_handles_in_order().into_iter()
.filter_map(|handle| self.tabs[i].scene.document.get_entity(handle)
.cloned().map(|entity| (handle, entity))).collect();
let available = self.tabs[i].scene.document.entities()
.map(|entity| (entity.common().handle, entity.clone())).collect();
let cmd = LoftCommand::new(color, isolines, crate::command::ExtrudeMode::Solid, selected, available);
let isolines = crate::entities::solid3d::clamp_header_isolines(
self.tabs[i].scene.document.header.isolines,
);
let selected = self.tabs[i]
.scene
.selected_handles_in_order()
.into_iter()
.filter_map(|handle| {
self.tabs[i]
.scene
.document
.get_entity(handle)
.cloned()
.map(|entity| (handle, entity))
})
.collect();
let available = self.tabs[i]
.scene
.document
.entities()
.map(|entity| (entity.common().handle, entity.clone()))
.collect();
let cmd = LoftCommand::new(
color,
isolines,
crate::command::ExtrudeMode::Solid,
selected,
available,
);
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
@@ -928,14 +1003,18 @@ impl OpenCADStudio {
"Plot style: {} ({} color overrides)",
t.name,
t.aci_entries.iter().filter(|e| e.color.is_some()).count()
).into_owned()
)
.into_owned()
})
.unwrap_or_else(|| crate::t!("No plot style table loaded.").into_owned());
.unwrap_or_else(|| {
crate::t!("No plot style table loaded.").into_owned()
});
self.command_line.push_output(&msg);
}
_ => {
self.command_line
.push_error(crate::t!("Usage: PLOTSTYLE [LOAD | CLEAR | STATUS]").as_ref());
self.command_line.push_error(
crate::t!("Usage: PLOTSTYLE [LOAD | CLEAR | STATUS]").as_ref(),
);
}
}
}
@@ -953,17 +1032,21 @@ impl OpenCADStudio {
.selected_entities()
.iter()
.filter(|(handle, _)| !self.tabs[i].scene.is_layer_locked(*handle))
.filter_map(|(handle, _)| match self.tabs[i].scene.document.get_entity(*handle) {
Some(codec::EntityType::RasterImage(image)) => {
Some((*handle, image.flags.contains(ImageDisplayFlags::TRANSPARENCY_ON)))
.filter_map(|(handle, _)| {
match self.tabs[i].scene.document.get_entity(*handle) {
Some(codec::EntityType::RasterImage(image)) => Some((
*handle,
image.flags.contains(ImageDisplayFlags::TRANSPARENCY_ON),
)),
_ => None,
}
_ => None,
})
.collect();
let Some(&(_, first_on)) = images.first() else {
return Some(Task::none());
};
let handles: Vec<codec::Handle> = images.iter().map(|(handle, _)| *handle).collect();
let handles: Vec<codec::Handle> =
images.iter().map(|(handle, _)| *handle).collect();
if cmd == "TRANSPARENCY MODE" {
let c = crate::modules::insert::image_transparency::TransparencyCommand::mode(
handles, first_on,
@@ -1027,8 +1110,9 @@ impl OpenCADStudio {
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line
.push_error(crate::t!("UNDERLAY: select underlay entities first.").as_ref());
self.command_line.push_error(
crate::t!("UNDERLAY: select underlay entities first.").as_ref(),
);
} else {
let parts: Vec<&str> = sub.splitn(2, char::is_whitespace).collect();
let action = parts.first().copied().unwrap_or("");
@@ -1065,8 +1149,7 @@ impl OpenCADStudio {
}
"CLIP" => match arg {
"ON" => {
ul.flags |=
codec::entities::UnderlayDisplayFlags::CLIPPING;
ul.flags |= codec::entities::UnderlayDisplayFlags::CLIPPING;
changed += 1;
}
"OFF" => {
@@ -1145,8 +1228,10 @@ impl OpenCADStudio {
return Some(Task::none());
}
let Some(scale) = self.tabs[i].scene.creation_annotation_scale_handle() else {
self.command_line
.push_error(crate::t!("OBJECTSCALE: the active annotation scale is unavailable.").as_ref());
self.command_line.push_error(
crate::t!("OBJECTSCALE: the active annotation scale is unavailable.")
.as_ref(),
);
return Some(Task::none());
};
@@ -1161,11 +1246,9 @@ impl OpenCADStudio {
let member = memberships.iter().any(|(_, current)| *current == scale);
if adding {
!member
&& self.tabs[i]
.scene
.document
.get_entity(*handle)
.is_some_and(crate::scene::annotative::supports_annotation_context)
&& self.tabs[i].scene.document.get_entity(*handle).is_some_and(
crate::scene::annotative::supports_annotation_context,
)
} else {
member
}
@@ -1218,11 +1301,14 @@ impl OpenCADStudio {
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
self.command_line.push_output(crate::tf!(
"OBJECTSCALE: {} the current scale on {} object(s).",
if adding { "added" } else { "removed" },
changed.len()
).as_ref());
self.command_line.push_output(
crate::tf!(
"OBJECTSCALE: {} the current scale on {} object(s).",
if adding { "added" } else { "removed" },
changed.len()
)
.as_ref(),
);
return Some(Task::none());
}
@@ -1231,19 +1317,22 @@ impl OpenCADStudio {
"HYPERLINK" => {
use crate::command::SelectThenValueCommand;
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
let c = SelectThenValueCommand::new(
"HYPERLINK",
"HYPERLINK URL to attach:",
has_sel,
);
let c =
SelectThenValueCommand::new("HYPERLINK", "HYPERLINK URL to attach:", has_sel);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("HYPERLINK ") => {
use codec::xdata::XDataValue;
let url = cmd.strip_prefix("HYPERLINK").unwrap_or("").trim().to_string();
let url = cmd
.strip_prefix("HYPERLINK")
.unwrap_or("")
.trim()
.to_string();
if url.is_empty() {
self.command_line.push_info(crate::t!("Usage: HYPERLINK <url> (select objects first)").as_ref());
self.command_line.push_info(
crate::t!("Usage: HYPERLINK <url> (select objects first)").as_ref(),
);
return Some(Task::none());
}
let handles: Vec<codec::Handle> = self.tabs[i]
@@ -1254,7 +1343,8 @@ impl OpenCADStudio {
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line.push_error(crate::t!("HYPERLINK: select objects first.").as_ref());
self.command_line
.push_error(crate::t!("HYPERLINK: select objects first.").as_ref());
return Some(Task::none());
}
self.push_undo_snapshot(i, "HYPERLINK");
@@ -1287,7 +1377,11 @@ impl OpenCADStudio {
"ADJUST",
"ADJUST [Brightness / Contrast / Fade]:",
vec![
("Brightness", "BRIGHTNESS", Some("ADJUST brightness 0-100:")),
(
"Brightness",
"BRIGHTNESS",
Some("ADJUST brightness 0-100:"),
),
("Contrast", "CONTRAST", Some("ADJUST contrast 0-100:")),
("Fade", "FADE", Some("ADJUST fade 0-100:")),
],
@@ -1312,8 +1406,9 @@ impl OpenCADStudio {
self.command_line
.push_error(crate::t!("ADJUST: select raster image(s) first.").as_ref());
} else if action.is_empty() {
self.command_line
.push_info(crate::t!("Usage: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100>").as_ref());
self.command_line.push_info(
crate::t!("Usage: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100>").as_ref(),
);
} else if let Ok(v) = arg.parse::<u8>() {
let v = v.min(100);
self.push_undo_snapshot(i, "ADJUST");
@@ -1356,15 +1451,17 @@ impl OpenCADStudio {
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.command_line
.push_output(crate::tf!("ADJUST: {action} = {v} on {changed} image(s).").as_ref());
self.command_line.push_output(
crate::tf!("ADJUST: {action} = {v} on {changed} image(s).").as_ref(),
);
} else {
self.command_line.push_error(
crate::t!("ADJUST: no raster images selected, or unknown property (use BRIGHTNESS|CONTRAST|FADE).").as_ref(),
);
}
} else {
self.command_line.push_error(crate::t!("ADJUST: value must be 0-100.").as_ref());
self.command_line
.push_error(crate::t!("ADJUST: value must be 0-100.").as_ref());
}
}
@@ -1382,10 +1479,8 @@ impl OpenCADStudio {
}
"ANNOALLVISIBLE" => {
use crate::command::ValuePromptCommand;
let c = ValuePromptCommand::new(
"ANNOALLVISIBLE",
"ANNOALLVISIBLE new value [0/1]:",
);
let c =
ValuePromptCommand::new("ANNOALLVISIBLE", "ANNOALLVISIBLE new value [0/1]:");
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
@@ -1407,10 +1502,8 @@ impl OpenCADStudio {
}
"ANNOAUTOSCALE" => {
use crate::command::ValuePromptCommand;
let c = ValuePromptCommand::new(
"ANNOAUTOSCALE",
"ANNOAUTOSCALE new value [-4..4]:",
);
let c =
ValuePromptCommand::new("ANNOAUTOSCALE", "ANNOAUTOSCALE new value [-4..4]:");
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
@@ -1432,8 +1525,9 @@ impl OpenCADStudio {
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line
.push_error(crate::t!("ANNOUPDATE: select annotation objects first.").as_ref());
self.command_line.push_error(
crate::t!("ANNOUPDATE: select annotation objects first.").as_ref(),
);
return Some(Task::none());
}
self.push_undo_snapshot(i, "ANNOUPDATE");
@@ -1492,9 +1586,9 @@ impl OpenCADStudio {
self.command_line
.push_output(crate::tf!("Annotation scale: {arg}").as_ref());
}
None => self
.command_line
.push_error(crate::t!("Usage: ANNOSCALE <ratio> e.g. 1:50, 2:1, or a factor").as_ref()),
None => self.command_line.push_error(
crate::t!("Usage: ANNOSCALE <ratio> e.g. 1:50, 2:1, or a factor").as_ref(),
),
}
}
@@ -1508,8 +1602,16 @@ impl OpenCADStudio {
"SCALELISTEDIT",
"SCALELISTEDIT [Add / Delete]:",
vec![
("Add", "ADD", Some("SCALELISTEDIT ADD new scale ratio (e.g. 1:50):")),
("Delete", "DELETE", Some("SCALELISTEDIT DELETE scale ratio to remove:")),
(
"Add",
"ADD",
Some("SCALELISTEDIT ADD new scale ratio (e.g. 1:50):"),
),
(
"Delete",
"DELETE",
Some("SCALELISTEDIT DELETE scale ratio to remove:"),
),
],
);
self.command_line.push_info(&c.prompt());
@@ -1527,20 +1629,22 @@ impl OpenCADStudio {
self.push_undo_snapshot(i, "SCALELISTEDIT");
if self.tabs[i].scene.add_scale(arg, paper, drawing) {
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::tf!("Added annotation scale {arg}.").as_ref());
self.command_line.push_output(
crate::tf!("Added annotation scale {arg}.").as_ref(),
);
} else {
self.command_line
.push_info(crate::tf!("Scale {arg} already exists.").as_ref());
self.command_line.push_info(
crate::tf!("Scale {arg} already exists.").as_ref(),
);
}
}
_ => self
.command_line
.push_error(crate::t!("SCALELISTEDIT ADD: use a ratio like 1:50.").as_ref()),
_ => self.command_line.push_error(
crate::t!("SCALELISTEDIT ADD: use a ratio like 1:50.").as_ref(),
),
},
None => self
.command_line
.push_error(crate::t!("SCALELISTEDIT ADD: use a ratio like 1:50.").as_ref()),
None => self.command_line.push_error(
crate::t!("SCALELISTEDIT ADD: use a ratio like 1:50.").as_ref(),
),
},
"DELETE" | "REMOVE" => {
let current = self.tabs[i]
@@ -1550,20 +1654,25 @@ impl OpenCADStudio {
.current_annotation_scale
.clone();
if arg.is_empty() {
self.command_line.push_info(crate::t!("Usage: SCALELISTEDIT DELETE <name>").as_ref());
self.command_line.push_info(
crate::t!("Usage: SCALELISTEDIT DELETE <name>").as_ref(),
);
} else if arg.eq_ignore_ascii_case(&current) {
self.command_line.push_error(crate::tf!(
"Cannot delete the current annotation scale ({arg})."
).as_ref());
self.command_line.push_error(
crate::tf!("Cannot delete the current annotation scale ({arg}).")
.as_ref(),
);
} else {
self.push_undo_snapshot(i, "SCALELISTEDIT");
if self.tabs[i].scene.remove_scale(arg) {
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::tf!("Removed annotation scale {arg}.").as_ref());
self.command_line.push_output(
crate::tf!("Removed annotation scale {arg}.").as_ref(),
);
} else {
self.command_line
.push_info(crate::tf!("No annotation scale named {arg}.").as_ref());
self.command_line.push_info(
crate::tf!("No annotation scale named {arg}.").as_ref(),
);
}
}
}
@@ -1575,15 +1684,17 @@ impl OpenCADStudio {
.map(|(n, _, _)| n)
.collect();
if names.is_empty() {
self.command_line.push_info(crate::t!("No annotation scales defined.").as_ref());
} else {
self.command_line
.push_output(crate::tf!("Annotation scales: {}", names.join(", ")).as_ref());
.push_info(crate::t!("No annotation scales defined.").as_ref());
} else {
self.command_line.push_output(
crate::tf!("Annotation scales: {}", names.join(", ")).as_ref(),
);
}
}
_ => self
.command_line
.push_info(crate::t!("Usage: SCALELISTEDIT [ADD 1:50 | DELETE 1:50]").as_ref()),
_ => self.command_line.push_info(
crate::t!("Usage: SCALELISTEDIT [ADD 1:50 | DELETE 1:50]").as_ref(),
),
}
}
@@ -1629,8 +1740,10 @@ impl OpenCADStudio {
.collect();
if candidates.is_empty() {
self.command_line.push_info(
crate::t!("ANNORESET: no selected object has alternate scale representations.")
.as_ref(),
crate::t!(
"ANNORESET: no selected object has alternate scale representations."
)
.as_ref(),
);
return Some(Task::none());
}
@@ -1671,9 +1784,8 @@ impl OpenCADStudio {
cmd if cmd.starts_with("DATALINK ") => {
let path = cmd.trim_start_matches("DATALINK").trim();
if path.is_empty() {
self.command_line.push_info(
crate::t!("Usage: DATALINK <path-to-.csv>").as_ref(),
);
self.command_line
.push_info(crate::t!("Usage: DATALINK <path-to-.csv>").as_ref());
return Some(Task::none());
}
match std::fs::read_to_string(path) {
@@ -1700,29 +1812,32 @@ impl OpenCADStudio {
}
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 {
codec::objects::ObjectType::TableStyle(style)
if style.name.eq_ignore_ascii_case(&current_style) =>
{
Some(*handle)
}
_ => None,
}
});
table.table_style_handle =
doc.objects
.iter()
.find_map(|(handle, object)| match object {
codec::objects::ObjectType::TableStyle(style)
if style.name.eq_ignore_ascii_case(&current_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,
);
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
.push_error(crate::tf!("DATALINK: cannot read \"{path}\": {e}").as_ref());
self.command_line.push_error(
crate::tf!("DATALINK: cannot read \"{path}\": {e}").as_ref(),
);
}
}
}
@@ -1789,15 +1904,17 @@ impl OpenCADStudio {
&self.tabs[i].scene.document,
*link_handle,
)
.and_then(|path| std::fs::write(path, csv).map_err(|error| error.to_string()))
{
.and_then(|path| {
std::fs::write(path, csv).map_err(|error| error.to_string())
}) {
Ok(()) => written += 1,
Err(error) => errors.push(error),
}
}
if written > 0 {
self.command_line.push_output(
crate::tf!("DATALINKUPDATE: wrote {} linked source(s).", written).as_ref(),
crate::tf!("DATALINKUPDATE: wrote {} linked source(s).", written)
.as_ref(),
);
}
for error in errors {
@@ -1818,8 +1935,9 @@ impl OpenCADStudio {
.filter(|(_, _, rows)| !rows.is_empty())
.collect();
if updates.is_empty() {
self.command_line
.push_error(crate::t!("DATALINKUPDATE: linked sources could not be read.").as_ref());
self.command_line.push_error(
crate::t!("DATALINKUPDATE: linked sources could not be read.").as_ref(),
);
return Some(Task::none());
}
self.push_undo_snapshot(i, "DATALINKUPDATE");
@@ -1839,7 +1957,11 @@ impl OpenCADStudio {
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);
let width = table
.columns
.last()
.map(|column| column.width)
.unwrap_or(2.0);
table.add_column(width);
}
while table.column_count() > columns {
@@ -1869,8 +1991,11 @@ impl OpenCADStudio {
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(),
crate::tf!(
"DATALINKUPDATE: updated {} linked table(s).",
changed_handles.len()
)
.as_ref(),
);
}
@@ -1879,8 +2004,10 @@ impl OpenCADStudio {
// (northing easting elevation) → Point at (easting, northing, elev).
"LANDXMLIMPORT" => {
use crate::command::ValuePromptCommand;
let c =
ValuePromptCommand::new("LANDXMLIMPORT", "LANDXMLIMPORT path to the .xml file:");
let c = ValuePromptCommand::new(
"LANDXMLIMPORT",
"LANDXMLIMPORT path to the .xml file:",
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
@@ -1888,7 +2015,10 @@ impl OpenCADStudio {
let path = cmd.trim_start_matches("LANDXMLIMPORT").trim();
if path.is_empty() {
self.command_line.push_info(
crate::t!("Usage: LANDXMLIMPORT <path-to-.xml> (imports CgPoint survey points)").as_ref(),
crate::t!(
"Usage: LANDXMLIMPORT <path-to-.xml> (imports CgPoint survey points)"
)
.as_ref(),
);
return Some(Task::none());
}
@@ -1896,8 +2026,10 @@ impl OpenCADStudio {
Ok(xml) => {
let pts = parse_landxml_cgpoints(&xml);
if pts.is_empty() {
self.command_line
.push_info(crate::t!("LANDXMLIMPORT: no <CgPoint> survey points found.").as_ref());
self.command_line.push_info(
crate::t!("LANDXMLIMPORT: no <CgPoint> survey points found.")
.as_ref(),
);
return Some(Task::none());
}
self.push_undo_snapshot(i, "LANDXMLIMPORT");
@@ -1909,14 +2041,17 @@ impl OpenCADStudio {
.add_entity_clone(codec::EntityType::Point(p));
}
self.tabs[i].dirty = true;
self.command_line.push_output(crate::tf!(
self.command_line.push_output(
crate::tf!(
"LANDXMLIMPORT: imported {} survey point(s). Use ZOOM EXTENTS to view.",
pts.len()
).as_ref());
)
.as_ref(),
);
}
Err(e) => self
.command_line
.push_error(crate::tf!("LANDXMLIMPORT: cannot read \"{path}\": {e}").as_ref()),
Err(e) => self.command_line.push_error(
crate::tf!("LANDXMLIMPORT: cannot read \"{path}\": {e}").as_ref(),
),
}
}
@@ -2051,8 +2186,14 @@ mod tests {
app.tabs[i].scene.refresh_pending_any(),
"REDRAW must leave a pending force request"
);
assert_eq!(app.tabs[i].scene.geometry_epoch, geom_before, "REDRAW must not regen");
assert_eq!(app.tabs[i].scene.block_epoch, block_before, "REDRAW must not regen blocks");
assert_eq!(
app.tabs[i].scene.geometry_epoch, geom_before,
"REDRAW must not regen"
);
assert_eq!(
app.tabs[i].scene.block_epoch, block_before,
"REDRAW must not regen blocks"
);
}
#[test]
@@ -2062,7 +2203,10 @@ mod tests {
let _ = full.run_command_line("REDRAW");
let _ = short.run_command_line("R");
let i = full.active_tab;
assert!(short.tabs[i].scene.refresh_pending_any(), "'R' must trigger REDRAW");
assert!(
short.tabs[i].scene.refresh_pending_any(),
"'R' must trigger REDRAW"
);
assert_eq!(
short.tabs[i].scene.refresh_pending_any(),
full.tabs[i].scene.refresh_pending_any(),
@@ -2075,7 +2219,10 @@ mod tests {
let mut app = fresh_app();
let i = app.active_tab;
let _ = app.run_command_line("REDRAWALL");
assert!(app.tabs[i].scene.refresh_pending_any(), "REDRAWALL must leave a force request");
assert!(
app.tabs[i].scene.refresh_pending_any(),
"REDRAWALL must leave a force request"
);
}
#[test]
@@ -2086,11 +2233,23 @@ mod tests {
let block_before = app.tabs[i].scene.block_epoch;
app.tabs[i].dirty = false;
let _ = app.run_command_line("REGEN");
assert_ne!(app.tabs[i].scene.geometry_epoch, geom_before, "REGEN must regenerate geometry");
assert_ne!(app.tabs[i].scene.block_epoch, block_before, "REGEN must regenerate block epoch");
assert!(!app.tabs[i].dirty, "REGEN must NOT mark the document as modified (no DB change)");
assert_ne!(
app.tabs[i].scene.geometry_epoch, geom_before,
"REGEN must regenerate geometry"
);
assert_ne!(
app.tabs[i].scene.block_epoch, block_before,
"REGEN must regenerate block epoch"
);
assert!(
!app.tabs[i].dirty,
"REGEN must NOT mark the document as modified (no DB change)"
);
let _ = app.run_command_line("REGENALL");
assert!(!app.tabs[i].dirty, "REGENALL must not dirty the document either");
assert!(
!app.tabs[i].dirty,
"REGENALL must not dirty the document either"
);
}
#[test]