style: apply cargo fmt across workspace

This commit is contained in:
Karim Jerbi committed 2026-10-11 03:14:29 +01:00
1 parent 85710e5454
commit 9cf9ff2c5c
531 files changed
+57740 -33906

No files matched your search

+21 -8
View File
@@ -28,9 +28,16 @@ impl OpenCADStudio {
Some(Task::none())
}
"-ATTDEF" => {
let defaults =
crate::scene::creation_style::current_text_defaults(&self.tabs[i].scene.document);
let styles: Vec<_> = self.tabs[i].scene.document.text_styles.iter().cloned().collect();
let defaults = crate::scene::creation_style::current_text_defaults(
&self.tabs[i].scene.document,
);
let styles: Vec<_> = self.tabs[i]
.scene
.document
.text_styles
.iter()
.cloned()
.collect();
let command = AttdefCommand::new(
"-ATTDEF",
defaults.style_name,
@@ -97,7 +104,8 @@ impl OpenCADStudio {
Some(v) => match v.parse::<u8>().ok().filter(|v| *v <= 63) {
Some(v) => attdef::session().aflags = v,
None => {
self.command_line.push_error("Requires an integer between 0 and 63.");
self.command_line
.push_error("Requires an integer between 0 and 63.");
self.command_line
.push_output(&format!("Enter new value for AFLAGS <{current}>:"));
self.pending_setvar = Some("AFLAGS".to_string());
@@ -145,7 +153,8 @@ impl OpenCADStudio {
/// The Edit Attribute Definition dialog for `handle`.
pub(in crate::app) fn open_attdef_edit(&mut self, handle: codec::Handle) {
let i = self.active_tab;
let Some(codec::EntityType::AttributeDefinition(a)) = self.tabs[i].scene.document.get_entity(handle)
let Some(codec::EntityType::AttributeDefinition(a)) =
self.tabs[i].scene.document.get_entity(handle)
else {
return;
};
@@ -175,11 +184,15 @@ impl OpenCADStudio {
}
AttdefDialogMsg::EditInsertField => {
self.active_modal = None;
self.open_field_dialog(crate::ui::window::field_dialog::FieldTarget::AttdefEdit);
self.open_field_dialog(
crate::ui::window::field_dialog::FieldTarget::AttdefEdit,
);
}
AttdefDialogMsg::Help => self.command_line.push_info(
crate::t!("Changes the tag, prompt and default value of an attribute definition.")
.as_ref(),
crate::t!(
"Changes the tag, prompt and default value of an attribute definition."
)
.as_ref(),
),
AttdefDialogMsg::EditOk => return self.attdef_edit_ok(i),
AttdefDialogMsg::DismissError => edit.error = None,
+49 -46
View File
@@ -38,8 +38,10 @@ impl OpenCADStudio {
base: glam::DVec3,
) -> usize {
// Clone + dep-capture live in the shared kernel; storage stays here.
let (entities, deps) =
super::super::command_driver::copy_to_clipboard_kernel(&self.tabs[i].scene.document, handles);
let (entities, deps) = super::super::command_driver::copy_to_clipboard_kernel(
&self.tabs[i].scene.document,
handles,
);
let count = entities.len();
self.clipboard_base = base;
self.clipboard = entities;
@@ -492,7 +494,8 @@ impl OpenCADStudio {
// INSERT opens the Blocks palette on the drawing's blocks;
// -INSERT runs on the command line.
"INSERT" => {
self.command_line.push_output("*Insert a block from the Blocks palette");
self.command_line
.push_output("*Insert a block from the Blocks palette");
self.open_blocks_palette(Some(crate::ui::window::block_palette::Tab::Current));
}
@@ -519,7 +522,8 @@ impl OpenCADStudio {
cmd if cmd.starts_with("_-INSERTFILE ") => {
use crate::command::CadCommand;
use crate::modules::insert::insert_block::InsertBlockCommand;
let path = std::path::PathBuf::from(cmd["_-INSERTFILE ".len()..].trim().trim_matches('"'));
let path =
std::path::PathBuf::from(cmd["_-INSERTFILE ".len()..].trim().trim_matches('"'));
match self.import_file_as_block(path) {
Ok(name) => {
let units = self
@@ -645,11 +649,10 @@ impl OpenCADStudio {
.iter()
.any(|a| a.tag.eq_ignore_ascii_case(tag))
{
ins.attributes
.push(codec::entities::AttributeEntity::new(
tag.clone(),
default.clone(),
));
ins.attributes.push(codec::entities::AttributeEntity::new(
tag.clone(),
default.clone(),
));
}
}
synced += 1;
@@ -702,7 +705,12 @@ impl OpenCADStudio {
// A block picked in the ribbon gallery.
cmd if cmd.starts_with("_BLOCKINSERT ") => {
let name = cmd["_BLOCKINSERT ".len()..].trim().to_string();
return Some(self.palette_insert(crate::ui::window::block_palette::Item::Current(name), None));
return Some(
self.palette_insert(
crate::ui::window::block_palette::Item::Current(name),
None,
),
);
}
cmd if cmd.starts_with("_BLOCKSPALETTE ") => {
use crate::ui::window::block_palette::Tab;
@@ -737,10 +745,16 @@ impl OpenCADStudio {
}
// A file named after it skips the picker.
cmd if cmd.starts_with("_PDFIMPORTFILE ") => {
let path = cmd["_PDFIMPORTFILE ".len()..].trim().trim_matches('"').to_string();
let path = cmd["_PDFIMPORTFILE ".len()..]
.trim()
.trim_matches('"')
.to_string();
match std::fs::read(&path) {
Ok(bytes) => {
crate::scene::model::pdf_raster::register_source(&path, std::sync::Arc::new(bytes));
crate::scene::model::pdf_raster::register_source(
&path,
std::sync::Arc::new(bytes),
);
self.open_pdf_import_file(&path);
}
Err(_) => self.command_line.push_error(&format!("{path} not found.")),
@@ -760,8 +774,11 @@ impl OpenCADStudio {
"POINTCLOUDCROP" | "POINTCLOUDUNCROP" => {
use crate::command::CadCommand;
use crate::modules::insert::pc_crop::PointCloudCropCommand;
let command =
if cmd == "POINTCLOUDUNCROP" { PointCloudCropCommand::uncrop() } else { PointCloudCropCommand::new() };
let command = if cmd == "POINTCLOUDUNCROP" {
PointCloudCropCommand::uncrop()
} else {
PointCloudCropCommand::new()
};
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
@@ -810,7 +827,10 @@ impl OpenCADStudio {
// PDFATTACH picks the file; -PDFATTACH asks for it on the command
// line. A path after either names the file at once.
cmd if matches!(
cmd.split_whitespace().next().map(|v| v.to_ascii_uppercase()).as_deref(),
cmd.split_whitespace()
.next()
.map(|v| v.to_ascii_uppercase())
.as_deref(),
Some("PDFATTACH") | Some("-PDFATTACH")
) =>
{
@@ -859,14 +879,12 @@ impl OpenCADStudio {
} else {
format!("{}/new_block.dwg", default_folder.replace('\\', "/"))
};
self.wblock = Some(
crate::ui::window::wblock::WblockState::new(
existing_blocks,
handles,
default_unit,
default_path,
),
);
self.wblock = Some(crate::ui::window::wblock::WblockState::new(
existing_blocks,
handles,
default_unit,
default_path,
));
self.active_modal = Some(crate::app::ModalKind::WriteBlock);
}
@@ -1079,8 +1097,7 @@ impl OpenCADStudio {
self.tabs[i].xref_unloaded.remove(key);
self.tabs[i].xref_stat_cache.remove(key);
}
let handles: rustc_hash::FxHashSet<codec::types::Handle> = self.tabs
[i]
let handles: rustc_hash::FxHashSet<codec::types::Handle> = self.tabs[i]
.scene
.document
.block_records
@@ -1928,12 +1945,8 @@ mod tests {
.objects
.insert(h, ObjectType::ImageDefinition(def));
// Reference it so collect_entries lists it.
let mut img = codec::entities::RasterImage::new(
"img.png",
codec::types::Vector3::ZERO,
8.0,
8.0,
);
let mut img =
codec::entities::RasterImage::new("img.png", codec::types::Vector3::ZERO, 8.0, 8.0);
img.definition_handle = Some(h);
app.tabs[i]
.scene
@@ -1963,12 +1976,8 @@ mod tests {
.document
.objects
.insert(h, ObjectType::ImageDefinition(def));
let mut img = codec::entities::RasterImage::new(
"img.png",
codec::types::Vector3::ZERO,
8.0,
8.0,
);
let mut img =
codec::entities::RasterImage::new("img.png", codec::types::Vector3::ZERO, 8.0, 8.0);
img.definition_handle = Some(h);
app.tabs[i]
.scene
@@ -2069,9 +2078,7 @@ mod tests {
.unwrap();
let mut line = codec::entities::Line::new();
line.common.layer = "WALLS".to_string();
xref_doc
.add_entity(codec::EntityType::Line(line))
.unwrap();
xref_doc.add_entity(codec::EntityType::Line(line)).unwrap();
let bytes = crate::io::save_to_bytes(&xref_doc, "dwg", xref_doc.version).unwrap();
let xref_path = dir.join("plan.dwg");
std::fs::write(&xref_path, &bytes).unwrap();
@@ -2104,9 +2111,7 @@ mod tests {
let mut line = codec::entities::Line::new();
line.common.plotstyle_flags = 0b11;
line.common.plotstyle_handle = Some(xref_doc.allocate_handle());
xref_doc
.add_entity(codec::EntityType::Line(line))
.unwrap();
xref_doc.add_entity(codec::EntityType::Line(line)).unwrap();
let bytes = crate::io::save_to_bytes(&xref_doc, "dwg", xref_doc.version).unwrap();
let xref_path = dir.join("plan.dwg");
std::fs::write(&xref_path, &bytes).unwrap();
@@ -2347,9 +2352,7 @@ mod tests {
.unwrap();
let mut line = codec::entities::Line::new();
line.common.layer = "WALLS".to_string();
xref_doc
.add_entity(codec::EntityType::Line(line))
.unwrap();
xref_doc.add_entity(codec::EntityType::Line(line)).unwrap();
let bytes = crate::io::save_to_bytes(&xref_doc, "dwg", xref_doc.version).unwrap();
let xref_path = dir.join("plan.dwg");
std::fs::write(&xref_path, &bytes).unwrap();
+7 -2
View File
@@ -42,7 +42,8 @@ impl OpenCADStudio {
match name.as_str() {
"BLOCKSTATE" => {
let state = self.tabs[i].refedit_session.is_some() as u8;
self.command_line.push_output(&format!("BLOCKSTATE = {state} (read only)"));
self.command_line
.push_output(&format!("BLOCKSTATE = {state} (read only)"));
}
"INSNAME" | "BLOCKNAVIGATE" => {
let current = if name == "INSNAME" {
@@ -52,7 +53,11 @@ impl OpenCADStudio {
};
match value {
Some(v) => {
let v = if v == "." { String::new() } else { v.trim_matches('"').to_string() };
let v = if v == "." {
String::new()
} else {
v.trim_matches('"').to_string()
};
if name == "INSNAME" {
self.insname = v;
} else {
+209 -67
View File
@@ -6,14 +6,21 @@ use crate::app::{Message, OpenCADStudio};
use crate::command::CadCommand;
use crate::modules::annotate::count_cmd::{CountCommand, CountTableCommand, UpdateFieldCommand};
use crate::modules::annotate::field_cmd::FieldPlaceCommand;
use crate::ui::window::count_palette::{aci_rgba, CountMode, CountMsg, CountTarget, RowAction, AREA_LAYER};
use crate::ui::window::count_palette::{
aci_rgba, CountMode, CountMsg, CountTarget, RowAction, AREA_LAYER,
};
use codec::count::{area_json, block_json, single_json, CountKey};
use codec::{EntityType, Handle};
use iced::Task;
/// The variables this family answers for.
pub(super) const COUNT_SYSVARS: &[&str] =
&["COUNTNUMBER", "COUNTSERVICE", "COUNTPALETTESTATE", "COUNTCOLOR", "COUNTERRORCOLOR"];
pub(super) const COUNT_SYSVARS: &[&str] = &[
"COUNTNUMBER",
"COUNTSERVICE",
"COUNTPALETTESTATE",
"COUNTCOLOR",
"COUNTERRORCOLOR",
];
fn hex(h: Handle) -> String {
format!("{:X}", h.value())
@@ -56,18 +63,27 @@ impl OpenCADStudio {
match verb.as_str() {
"COUNT" => {
let preselected = self.tabs[i].scene.selected_handles_in_order();
let current = matches!(rest.trim_start_matches('_').to_ascii_uppercase().as_str(), "C");
let current = matches!(
rest.trim_start_matches('_').to_ascii_uppercase().as_str(),
"C"
);
if current && !preselected.is_empty() {
// Count Selection: the selection is the target.
let found = preselected.len();
self.command_line.push_output(&crate::tf!("{found} found"));
let handles: Vec<String> = preselected.iter().map(|h| hex(*h)).collect();
self.tabs[i].scene.deselect_all();
return Some(self.dispatch_command(&format!("_COUNTRUN C T {}", handles.join(","))));
return Some(
self.dispatch_command(&format!("_COUNTRUN C T {}", handles.join(","))),
);
}
// Plain COUNT asks for its targets itself.
self.tabs[i].scene.deselect_all();
let command = if current { CountCommand::with_area("C") } else { CountCommand::new(false) };
let command = if current {
CountCommand::with_area("C")
} else {
CountCommand::new(false)
};
Some(self.start_count_command(i, Box::new(command)))
}
"COUNTAREA" => Some(self.start_count_command(i, Box::new(CountCommand::new(true)))),
@@ -92,9 +108,9 @@ impl OpenCADStudio {
match self.count_field_code(i) {
Some(code) => return Some(self.start_count_field(i, code)),
None if self.tabs[i].count.is_some() => {}
None => self
.command_line
.push_error(&crate::t!("** COUNTFIELD command only available during Count. **")),
None => self.command_line.push_error(&crate::t!(
"** COUNTFIELD command only available during Count. **"
)),
}
Some(Task::none())
}
@@ -150,7 +166,8 @@ impl OpenCADStudio {
mode.cursor = None;
self.tabs[i].count = Some(mode);
self.apply_count_display(i);
self.command_line.push_output(&crate::t!("count area is active."));
self.command_line
.push_output(&crate::t!("count area is active."));
}
Some(Task::none())
}
@@ -158,7 +175,11 @@ impl OpenCADStudio {
let (point, names) = rest.split_once(' ').unwrap_or((rest, ""));
let p: Vec<f64> = point.split(',').filter_map(|v| v.parse().ok()).collect();
if p.len() == 3 {
let names: Vec<String> = names.split('|').filter(|n| !n.is_empty()).map(str::to_string).collect();
let names: Vec<String> = names
.split('|')
.filter(|n| !n.is_empty())
.map(str::to_string)
.collect();
self.place_count_table(i, [p[0], p[1], p[2]], names);
}
Some(Task::none())
@@ -193,7 +214,8 @@ impl OpenCADStudio {
_ => None,
};
if let Some(v) = read_only {
self.command_line.push_output(&crate::tf!("{name} = {v} (read only)"));
self.command_line
.push_output(&crate::tf!("{name} = {v} (read only)"));
return;
}
let (current, min, max) = match name {
@@ -202,7 +224,8 @@ impl OpenCADStudio {
_ => (self.count_palette.error_color as i32, 1, 255),
};
let ask = |app: &mut Self| {
app.command_line.push_output(&crate::tf!("Enter new value for {name} <{current}>:"));
app.command_line
.push_output(&crate::tf!("Enter new value for {name} <{current}>:"));
app.pending_setvar = Some(name.to_string());
};
let Some(value) = value else {
@@ -237,7 +260,8 @@ impl OpenCADStudio {
self.ribbon.set_count_palette(open);
if open {
if self.dock.location(id).is_none() {
self.dock.dock(id, crate::app::config::DockSide::Right, usize::MAX);
self.dock
.dock(id, crate::app::config::DockSide::Right, usize::MAX);
}
self.dock_expanded = Some(id);
} else if self.dock_expanded == Some(id) {
@@ -248,19 +272,26 @@ impl OpenCADStudio {
/// Recolour the drawing for count mode: counted references, duplicates,
/// the rest faded (nothing changes without a target).
pub(in crate::app) fn apply_count_display(&mut self, i: usize) {
let computed = self.tabs[i].count.as_ref().map(|m| m.compute(&self.tabs[i].scene.document));
let computed = self.tabs[i]
.count
.as_ref()
.map(|m| m.compute(&self.tabs[i].scene.document));
if let (Some(mode), Some(c)) = (self.tabs[i].count.as_mut(), computed) {
mode.cache = Some(c);
}
let display = self.tabs[i].count.as_ref().filter(|m| m.target.is_some()).map(|m| {
let r = m.result(&self.tabs[i].scene.document);
crate::scene::CountDisplay {
counted: r.counted.iter().copied().collect(),
errors: r.errors.iter().chain(&r.overlapped).copied().collect(),
color: aci_rgba(self.count_palette.color),
error_color: aci_rgba(self.count_palette.error_color),
}
});
let display = self.tabs[i]
.count
.as_ref()
.filter(|m| m.target.is_some())
.map(|m| {
let r = m.result(&self.tabs[i].scene.document);
crate::scene::CountDisplay {
counted: r.counted.iter().copied().collect(),
errors: r.errors.iter().chain(&r.overlapped).copied().collect(),
color: aci_rgba(self.count_palette.color),
error_color: aci_rgba(self.count_palette.error_color),
}
});
self.tabs[i].scene.set_count_display(display);
let epoch = self.tabs[i].scene.geometry_epoch;
if let Some(mode) = self.tabs[i].count.as_mut() {
@@ -271,7 +302,11 @@ impl OpenCADStudio {
/// After the drawing changes, count mode colours its references again.
pub(in crate::app) fn refresh_count_if_stale(&mut self) {
let i = self.active_tab;
let Some(mode) = self.tabs[i].count.as_ref().filter(|m| m.epoch != self.tabs[i].scene.geometry_epoch) else {
let Some(mode) = self.tabs[i]
.count
.as_ref()
.filter(|m| m.epoch != self.tabs[i].scene.geometry_epoch)
else {
return;
};
if let Some((h, _)) = mode.boundary {
@@ -294,7 +329,11 @@ impl OpenCADStudio {
}
// An edited boundary moves the area with it.
Some(EntityType::LwPolyline(pl)) => {
let ring: Vec<[f64; 2]> = pl.vertices.iter().map(|v| [v.location.x, v.location.y]).collect();
let ring: Vec<[f64; 2]> = pl
.vertices
.iter()
.map(|v| [v.location.x, v.location.y])
.collect();
if let Some(mode) = self.tabs[i].count.as_mut() {
mode.area = Some(ring);
}
@@ -388,12 +427,20 @@ impl OpenCADStudio {
tokens: &[&str],
) -> Option<(Option<Vec<[f64; 2]>>, Option<(Handle, bool)>)> {
let kind = tokens.first().copied().unwrap_or("E");
let points = || tokens[1..].iter().map_while(|t| parse_xy(t)).collect::<Vec<_>>();
let points = || {
tokens[1..]
.iter()
.map_while(|t| parse_xy(t))
.collect::<Vec<_>>()
};
match kind {
"E" => Some((None, None)),
"K" => {
let mode = self.tabs[i].count.as_ref();
Some((mode.and_then(|m| m.area.clone()), mode.and_then(|m| m.boundary)))
Some((
mode.and_then(|m| m.area.clone()),
mode.and_then(|m| m.boundary),
))
}
// Current area counts all of model space (the view and an area
// in use take no part).
@@ -450,7 +497,10 @@ impl OpenCADStudio {
/// counts the targets and enters count mode.
fn count_run(&mut self, i: usize, rest: &str) {
let tokens: Vec<&str> = rest.split_whitespace().collect();
let split = tokens.iter().position(|t| *t == "L" || *t == "T").unwrap_or(tokens.len());
let split = tokens
.iter()
.position(|t| *t == "L" || *t == "T")
.unwrap_or(tokens.len());
let (area_tokens, tail) = tokens.split_at(split);
let targets: Vec<Handle> = match tail {
["T", handles, ..] => handles
@@ -479,9 +529,21 @@ impl OpenCADStudio {
})
.collect();
let target = match blocks.first() {
Some(first) if blocks.len() == targets.len() && blocks.iter().all(|b| b.eq_ignore_ascii_case(first)) => {
let name = doc.block_records.get(first).map(|b| b.name.clone()).unwrap_or_else(|| first.to_string());
Some(CountTarget::Block { name, reference: targets.first().copied(), matching: [false; 3], picked: true })
Some(first)
if blocks.len() == targets.len()
&& blocks.iter().all(|b| b.eq_ignore_ascii_case(first)) =>
{
let name = doc
.block_records
.get(first)
.map(|b| b.name.clone())
.unwrap_or_else(|| first.to_string());
Some(CountTarget::Block {
name,
reference: targets.first().copied(),
matching: [false; 3],
picked: true,
})
}
None if targets.is_empty() => None,
_ => Some(CountTarget::Group(targets.clone())),
@@ -492,10 +554,16 @@ impl OpenCADStudio {
if let Some(old) = self.tabs[i].count.take().filter(|o| o.boundary != boundary) {
self.drop_count_boundary(i, old.boundary);
}
let mode = CountMode { area, boundary, target, ..CountMode::default() };
let mode = CountMode {
area,
boundary,
target,
..CountMode::default()
};
if let Some(name) = mode.target_name() {
let n = mode.count(&self.tabs[i].scene.document);
self.command_line.push_output(&crate::tf!("{name} ...... {n}"));
self.command_line
.push_output(&crate::tf!("{name} ...... {n}"));
}
self.tabs[i].count = Some(mode);
self.apply_count_display(i);
@@ -508,11 +576,12 @@ impl OpenCADStudio {
let mode = self.tabs[i].count.as_ref()?;
match mode.target.as_ref()? {
CountTarget::Group(handles) => Some(format!("\\AcCount {}", single_json(handles))),
CountTarget::Block { reference: Some(r), matching: [false, false, false], picked: true, .. }
if mode.boundary.is_none() =>
{
Some(format!("\\AcCount {}", single_json(&[*r])))
}
CountTarget::Block {
reference: Some(r),
matching: [false, false, false],
picked: true,
..
} if mode.boundary.is_none() => Some(format!("\\AcCount {}", single_json(&[*r]))),
CountTarget::Block { name, .. } => {
let instances = mode.instances(&self.tabs[i].scene.document);
Some(self.block_field_code(i, name, &mode.key(&instances)))
@@ -531,13 +600,21 @@ impl OpenCADStudio {
/// Place a count field as multi-line text (`Specify start point or [Height/Justify]:`).
pub(in crate::app) fn start_count_field(&mut self, i: usize, code: String) -> Task<Message> {
let doc = &self.tabs[i].scene.document;
let value = crate::entities::field::evaluate(doc, &code, &[], None).unwrap_or_else(|| "####".into());
let value = crate::entities::field::evaluate(doc, &code, &[], None)
.unwrap_or_else(|| "####".into());
let defaults = crate::scene::creation_style::current_text_defaults(doc);
self.command_line.push_output(&crate::tf!(
"MTEXT Current text style: \"{}\" Text height: {:.4}",
defaults.style_name, defaults.height
defaults.style_name,
defaults.height
));
let command = FieldPlaceCommand::new(value, (code, Vec::new()), defaults.style_name, defaults.height).named("MTEXT");
let command = FieldPlaceCommand::new(
value,
(code, Vec::new()),
defaults.style_name,
defaults.height,
)
.named("MTEXT");
self.start_count_command(i, Box::new(command))
}
@@ -563,7 +640,11 @@ impl OpenCADStudio {
Some(EntityType::Table(t)) => t
.block_record_handle
.and_then(|r| doc.block_records.iter().find(|b| b.handle == r))
.or_else(|| doc.block_records.get(&t.block_name).filter(|_| !t.block_name.is_empty())),
.or_else(|| {
doc.block_records
.get(&t.block_name)
.filter(|_| !t.block_name.is_empty())
}),
_ => None,
};
if let Some(record) = record {
@@ -573,7 +654,9 @@ impl OpenCADStudio {
let attributes: Vec<&codec::entities::EntityCommon> = handles
.iter()
.filter_map(|h| match doc.get_entity(*h) {
Some(EntityType::Insert(insert)) => Some(insert.attributes.iter().map(|a| &a.common)),
Some(EntityType::Insert(insert)) => {
Some(insert.attributes.iter().map(|a| &a.common))
}
_ => None,
})
.flatten()
@@ -585,8 +668,10 @@ impl OpenCADStudio {
}),
});
if !hosting {
self.command_line.push_output(&crate::tf!("{found} field(s) found.", found = 0));
self.command_line.push_output(&crate::tf!("{updated} field(s) updated.", updated = 0));
self.command_line
.push_output(&crate::tf!("{found} field(s) found.", found = 0));
self.command_line
.push_output(&crate::tf!("{updated} field(s) updated.", updated = 0));
return;
}
// The reference counts fields, not the texts holding them, and
@@ -595,8 +680,10 @@ impl OpenCADStudio {
if changed > 0 {
self.tabs[i].dirty = true;
}
self.command_line.push_output(&crate::tf!("{found} field(s) found."));
self.command_line.push_output(&crate::tf!("{updated} field(s) updated.", updated = found));
self.command_line
.push_output(&crate::tf!("{found} field(s) found."));
self.command_line
.push_output(&crate::tf!("{updated} field(s) updated.", updated = found));
}
/// A count table at `point`: `Item` | `Count`, one row per block (all
@@ -607,11 +694,19 @@ impl OpenCADStudio {
let area = self.tabs[i].count.as_ref().and_then(|m| m.area.clone());
let instances = codec::count::block_instances(doc, area.as_deref());
let mut names: Vec<String> = if names.is_empty() {
codec::count::block_counts(&instances).into_iter().map(|c| c.name).collect()
codec::count::block_counts(&instances)
.into_iter()
.map(|c| c.name)
.collect()
} else {
names
.iter()
.map(|n| doc.block_records.get(n).map(|b| b.name.clone()).unwrap_or_else(|| n.clone()))
.map(|n| {
doc.block_records
.get(n)
.map(|b| b.name.clone())
.unwrap_or_else(|| n.clone())
})
.collect()
};
names.sort_by_key(|n| n.to_ascii_uppercase());
@@ -659,8 +754,10 @@ impl OpenCADStudio {
}
}
}
let codes: Vec<String> =
names.iter().map(|n| self.block_field_code(i, n, &CountKey::default())).collect();
let codes: Vec<String> = names
.iter()
.map(|n| self.block_field_code(i, n, &CountKey::default()))
.collect();
self.push_undo_snapshot(i, "COUNTTABLE");
let Some(handle) = self.commit_entity_handle(EntityType::Table(Box::new(table))) else {
return;
@@ -672,19 +769,29 @@ impl OpenCADStudio {
.iter()
.enumerate()
.map(|(r, code)| {
let value = crate::entities::field::evaluate(doc, code, &[], None).unwrap_or_else(|| "####".into());
(r + 1, 1, r"%<\_FldIdx 0>%".to_string(), vec![codec::fields::NewField::new(code.clone(), value)])
let value = crate::entities::field::evaluate(doc, code, &[], None)
.unwrap_or_else(|| "####".into());
(
r + 1,
1,
r"%<\_FldIdx 0>%".to_string(),
vec![codec::fields::NewField::new(code.clone(), value)],
)
})
.collect();
doc.build_table_block(handle, fields);
self.tabs[i].scene.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
self.tabs[i]
.scene
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
self.tabs[i].dirty = true;
}
/// Zoom to a counted object (← / →, an error of the report).
fn zoom_to_insert(&mut self, i: usize, h: Handle) {
let doc = &self.tabs[i].scene.document;
let Some(entity) = doc.get_entity(h) else { return };
let Some(entity) = doc.get_entity(h) else {
return;
};
let outline = codec::count::entity_outline(doc, entity);
if outline.is_empty() {
return;
@@ -704,12 +811,20 @@ impl OpenCADStudio {
let instances = mode.instances(&self.tabs[i].scene.document);
let reference = instances
.iter()
.find(|inst| inst.duplicate_of.is_none() && inst.name.eq_ignore_ascii_case(&name) && key.matches(inst))
.find(|inst| {
inst.duplicate_of.is_none()
&& inst.name.eq_ignore_ascii_case(&name)
&& key.matches(inst)
})
.map(|inst| inst.handle);
mode.target = Some(CountTarget::Block {
name,
reference,
matching: [key.layer.is_some(), key.scale.is_some(), key.mirror_state.is_some()],
matching: [
key.layer.is_some(),
key.scale.is_some(),
key.mirror_state.is_some(),
],
picked: false,
});
mode.cursor = None;
@@ -721,9 +836,15 @@ impl OpenCADStudio {
/// expression counts.
pub(in crate::app) fn show_count_instances(&mut self, i: usize, json: &str) {
match codec::count::parse_query(json) {
Some(codec::count::CountQuery::Block { name, key, boundary }) => {
Some(codec::count::CountQuery::Block {
name,
key,
boundary,
}) => {
if let Some(b) = boundary {
if let Some(ring) = codec::count::boundary_polygon(&self.tabs[i].scene.document, b) {
if let Some(ring) =
codec::count::boundary_polygon(&self.tabs[i].scene.document, b)
{
let mut mode = self.tabs[i].count.take().unwrap_or_default();
if mode.boundary.map(|(h, _)| h) != Some(b) {
self.drop_count_boundary(i, mode.boundary);
@@ -737,7 +858,11 @@ impl OpenCADStudio {
}
Some(codec::count::CountQuery::Single(handles)) if !handles.is_empty() => {
let hexes: Vec<String> = handles.iter().map(|h| hex(*h)).collect();
let area = if self.tabs[i].count.is_some() { "K" } else { "E" };
let area = if self.tabs[i].count.is_some() {
"K"
} else {
"E"
};
self.count_run(i, &format!("{area} T {}", hexes.join(",")));
}
_ => {}
@@ -793,7 +918,10 @@ impl OpenCADStudio {
CountMsg::TableAll(on) => {
let names: Vec<String> = if on {
let area = self.tabs[i].count.as_ref().and_then(|m| m.area.clone());
let instances = codec::count::block_instances(&self.tabs[i].scene.document, area.as_deref());
let instances = codec::count::block_instances(
&self.tabs[i].scene.document,
area.as_deref(),
);
crate::ui::window::count_palette::rows(&instances, &self.count_palette)
.into_iter()
.map(|r| r.name.to_ascii_uppercase())
@@ -813,16 +941,26 @@ impl OpenCADStudio {
}
CountMsg::Back | CountMsg::Close => self.close_count(i),
CountMsg::Match(k, on) => {
if let Some(CountMode { target: Some(CountTarget::Block { matching, .. }), .. }) = self.tabs[i].count.as_mut() {
if let Some(CountMode {
target: Some(CountTarget::Block { matching, .. }),
..
}) = self.tabs[i].count.as_mut()
{
matching[k] = on;
}
self.apply_count_display(i);
}
CountMsg::ToggleDetails => self.count_palette.details_closed = !self.count_palette.details_closed,
CountMsg::ToggleErrors => self.count_palette.errors_closed = !self.count_palette.errors_closed,
CountMsg::ToggleDetails => {
self.count_palette.details_closed = !self.count_palette.details_closed
}
CountMsg::ToggleErrors => {
self.count_palette.errors_closed = !self.count_palette.errors_closed
}
CountMsg::ShowError(h) => self.zoom_to_insert(i, h),
CountMsg::Prev | CountMsg::Next => {
let Some(mode) = self.tabs[i].count.as_ref() else { return Task::none() };
let Some(mode) = self.tabs[i].count.as_ref() else {
return Task::none();
};
let result = mode.result(&self.tabs[i].scene.document);
let n = result.counted.len();
if n == 0 {
@@ -843,7 +981,11 @@ impl OpenCADStudio {
CountMsg::Area => return self.dispatch_command("COUNTAREA"),
CountMsg::Select => {
self.tabs[i].scene.deselect_all();
let command = CountCommand::with_area(if self.tabs[i].count.is_some() { "K" } else { "C" });
let command = CountCommand::with_area(if self.tabs[i].count.is_some() {
"K"
} else {
"C"
});
return self.start_count_command(i, Box::new(command));
}
CountMsg::Field => return self.dispatch_command("COUNTFIELD"),
+98 -95
View File
@@ -30,7 +30,7 @@ fn solid_edge_sources(
entity,
codec::EntityType::Solid3D(_) | codec::EntityType::Surface(_)
)
.then_some(entity.common().handle)
.then_some(entity.common().handle)
})
.collect::<Vec<_>>();
app.tabs[tab].scene.restore_solid_models(&handles);
@@ -199,8 +199,9 @@ impl OpenCADStudio {
_ => None,
};
if text_align.is_none() && mtext_ap.is_none() {
self.command_line
.push_error(crate::t!("JUSTIFYTEXT: unknown justification option.").as_ref());
self.command_line.push_error(
crate::t!("JUSTIFYTEXT: unknown justification option.").as_ref(),
);
return Some(Task::none());
}
self.push_undo_snapshot(i, "JUSTIFYTEXT");
@@ -358,10 +359,7 @@ impl OpenCADStudio {
.selected_entities()
.iter()
.filter(|(_, e)| {
matches!(
e,
codec::EntityType::Text(_) | codec::EntityType::MText(_)
)
matches!(e, codec::EntityType::Text(_) | codec::EntityType::MText(_))
})
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
@@ -421,8 +419,7 @@ impl OpenCADStudio {
"TEXTFIT" => {
use crate::command::SelectThenValueCommand;
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
let c =
SelectThenValueCommand::new("TEXTFIT", "TEXTFIT target width:", has_sel);
let c = SelectThenValueCommand::new("TEXTFIT", "TEXTFIT target width:", has_sel);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
@@ -485,9 +482,9 @@ impl OpenCADStudio {
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.command_line.push_output(crate::tf!(
"TEXTFIT: fitted {n} text object(s) to width {target}."
).as_ref());
self.command_line.push_output(
crate::tf!("TEXTFIT: fitted {n} text object(s) to width {target}.").as_ref(),
);
}
// (text sequential numbering)
@@ -505,20 +502,11 @@ impl OpenCADStudio {
cmd if cmd.starts_with("TCOUNT ") => {
let mut args = cmd.split_whitespace().skip(1);
let start: i64 = args
.next()
.and_then(|s| s.parse().ok())
.unwrap_or(1);
let start: i64 = args.next().and_then(|s| s.parse().ok()).unwrap_or(1);
let increment: i64 = args
.next()
.and_then(|s| s.parse().ok())
.unwrap_or(1);
let increment: i64 = args.next().and_then(|s| s.parse().ok()).unwrap_or(1);
let placement = args
.next()
.unwrap_or("O")
.to_uppercase();
let placement = args.next().unwrap_or("O").to_uppercase();
let placement = match placement.as_str() {
"O" | "OVERWRITE" => "O",
@@ -538,9 +526,7 @@ impl OpenCADStudio {
.selected_entities()
.iter()
.filter_map(|(h, e)| match e {
codec::EntityType::Text(t)
if !self.tabs[i].scene.is_layer_locked(*h) =>
{
codec::EntityType::Text(t) if !self.tabs[i].scene.is_layer_locked(*h) => {
Some((*h, t.insertion_point.x, t.insertion_point.y))
}
_ => None,
@@ -558,10 +544,7 @@ impl OpenCADStudio {
texts.sort_by(|a, b| {
b.2.partial_cmp(&a.2)
.unwrap_or(std::cmp::Ordering::Equal)
.then(
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal),
)
.then(a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
});
self.push_undo_snapshot(i, "TCOUNT");
@@ -589,9 +572,7 @@ impl OpenCADStudio {
let changes: Vec<_> = texts
.iter()
.map(|(handle, _, _)| {
(*handle, crate::scene::ChangeKind::Modified)
})
.map(|(handle, _, _)| (*handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
@@ -618,21 +599,22 @@ impl OpenCADStudio {
.header
.current_mleader_style_name
.clone();
let style = self.tabs[i]
.scene
.document
.objects
.iter()
.find_map(|(handle, object)| match object {
codec::objects::ObjectType::MultiLeaderStyle(style)
if style.name.eq_ignore_ascii_case(&name) =>
{
let mut style = style.clone();
style.handle = *handle;
Some(style)
}
_ => None,
});
let style =
self.tabs[i]
.scene
.document
.objects
.iter()
.find_map(|(handle, object)| match object {
codec::objects::ObjectType::MultiLeaderStyle(style)
if style.name.eq_ignore_ascii_case(&name) =>
{
let mut style = style.clone();
style.handle = *handle;
Some(style)
}
_ => None,
});
let multiplier = self.tabs[i].scene.creation_annotation_multiplier();
let block_sources = self.tabs[i]
.scene
@@ -656,9 +638,11 @@ impl OpenCADStudio {
.iter()
.map(|style| (style.name.clone(), style.handle))
.collect();
let new_cmd = style.map_or_else(MLeaderCommand::new, |style| {
MLeaderCommand::with_style(style, multiplier)
}).with_drawing_resources(block_sources, layers, text_styles);
let new_cmd = style
.map_or_else(MLeaderCommand::new, |style| {
MLeaderCommand::with_style(style, multiplier)
})
.with_drawing_resources(block_sources, layers, text_styles);
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
@@ -686,10 +670,7 @@ impl OpenCADStudio {
.filter(|handle| scene.entity_belongs_to_active_space(*handle))
.and_then(|handle| scene.document.get_entity(handle))
.cloned();
let cmd = DimContinueCommand::new(
recent,
self.dimension_continue_mode == 1,
);
let cmd = DimContinueCommand::new(recent, self.dimension_continue_mode == 1);
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
@@ -754,11 +735,8 @@ impl OpenCADStudio {
surface_area: None,
})
.collect();
let cmd = QdimCommand::new(
selection,
dim_spacing,
self.quick_dimension_snap_priority,
);
let cmd =
QdimCommand::new(selection, dim_spacing, self.quick_dimension_snap_priority);
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
@@ -829,13 +807,15 @@ impl OpenCADStudio {
"ZOOM EXTENTS ALL" | "ZOOM EXTENTS ALL VIEWPORTS" | "ZOOM EA" | "ZEA" => {
self.tabs[i].scene.remember_current_view();
self.tabs[i].scene.fit_all_model_viewports();
self.command_line.push_output(crate::t!("Zoom Extents — All Viewports").as_ref());
self.command_line
.push_output(crate::t!("Zoom Extents — All Viewports").as_ref());
}
"ZOOM EXTENTS" | "ZOOM E" | "ZOOMEXTENTS" | "ZE" => {
self.tabs[i].scene.remember_current_view();
self.tabs[i].scene.fit_all();
self.command_line.push_output(crate::t!("Zoom Extents").as_ref());
self.command_line
.push_output(crate::t!("Zoom Extents").as_ref());
}
"ZOOM IN" | "ZOOM I" | "ZI" => {
@@ -847,19 +827,22 @@ impl OpenCADStudio {
"ZOOM OUT" | "ZO" => {
self.tabs[i].scene.remember_current_view();
self.tabs[i].scene.zoom_camera(1.5);
self.command_line.push_output(crate::t!("Zoom Out").as_ref());
self.command_line
.push_output(crate::t!("Zoom Out").as_ref());
}
// ZOOM ALL — fit the configured drawing limits.
"ZOOM ALL" | "ZOOM A" | "ZA" => {
self.tabs[i].scene.remember_current_view();
self.tabs[i].scene.fit_all_with_limits();
self.command_line.push_output(crate::t!("Zoom All").as_ref());
self.command_line
.push_output(crate::t!("Zoom All").as_ref());
}
"ZOOM PREVIOUS" | "ZOOM P" | "ZP" => {
if self.tabs[i].scene.restore_previous_view() {
self.command_line.push_output(crate::t!("Zoom Previous").as_ref());
self.command_line
.push_output(crate::t!("Zoom Previous").as_ref());
} else {
self.command_line
.push_error(crate::t!("ZOOM: no previous view.").as_ref());
@@ -881,7 +864,8 @@ impl OpenCADStudio {
} else {
self.tabs[i].scene.remember_current_view();
if self.tabs[i].scene.zoom_to_entities(&handles) {
self.command_line.push_output(crate::t!("Zoom Object").as_ref());
self.command_line
.push_output(crate::t!("Zoom Object").as_ref());
} else {
self.command_line.push_error(
crate::t!("ZOOM: selected objects have no visible bounds.").as_ref(),
@@ -986,7 +970,11 @@ impl OpenCADStudio {
};
(!handles.is_empty()
&& handles.iter().all(|h| {
self.tabs[i].scene.wire_models_for(&[*h]).iter().any(touches)
self.tabs[i]
.scene
.wire_models_for(&[*h])
.iter()
.any(touches)
}))
.then(|| {
(
@@ -1257,25 +1245,30 @@ impl OpenCADStudio {
} else {
// EXPLMODE 0 keeps references that are not uniformly and
// positively scaled whole.
let selected: Vec<_> = if super::blockvars::explmode(&self.tabs[i].scene.document) == 0 {
selected
.into_iter()
.filter(|(_, entity)| match entity {
codec::EntityType::Insert(ins) => {
let (x, y, z) = (ins.x_scale(), ins.y_scale(), ins.z_scale());
let uniform = x > 0.0 && (x - y).abs() <= 1e-10 && (x - z).abs() <= 1e-10;
if !uniform {
self.command_line
.push_output("The object is not uniformly and positively scaled.");
let selected: Vec<_> =
if super::blockvars::explmode(&self.tabs[i].scene.document) == 0 {
selected
.into_iter()
.filter(|(_, entity)| match entity {
codec::EntityType::Insert(ins) => {
let (x, y, z) =
(ins.x_scale(), ins.y_scale(), ins.z_scale());
let uniform = x > 0.0
&& (x - y).abs() <= 1e-10
&& (x - z).abs() <= 1e-10;
if !uniform {
self.command_line.push_output(
"The object is not uniformly and positively scaled.",
);
}
uniform
}
uniform
}
_ => true,
})
.collect()
} else {
selected
};
_ => true,
})
.collect()
} else {
selected
};
let replacements = plan_explode(&selected, &self.tabs[i].scene.document);
let exploded = replacements.len();
if exploded > 0 {
@@ -1288,8 +1281,9 @@ impl OpenCADStudio {
self.command_line
.push_output(crate::tf!("{exploded} object(s) exploded.").as_ref());
} else {
self.command_line
.push_info(crate::t!("EXPLODE: no explodable objects selected.").as_ref());
self.command_line.push_info(
crate::t!("EXPLODE: no explodable objects selected.").as_ref(),
);
}
}
}
@@ -1350,8 +1344,7 @@ impl OpenCADStudio {
})
.collect();
let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect();
let new_cmd =
TrimCommand::with_cutting_edges(all_entities, initial_edges);
let new_cmd = TrimCommand::with_cutting_edges(all_entities, initial_edges);
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
@@ -1364,7 +1357,12 @@ impl OpenCADStudio {
.iter()
.filter_map(|w| {
let h = Scene::handle_from_wire_name(&w.name)?;
self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e))
self.tabs[i]
.scene
.document
.get_entity(h)
.cloned()
.map(|e| (h, e))
})
.collect();
let new_cmd = ExtrimCommand::new(all);
@@ -1413,7 +1411,10 @@ impl OpenCADStudio {
let text = cmd.strip_prefix("ARCTEXT").unwrap_or("").trim().to_string();
if text.is_empty() {
self.command_line.push_info(
crate::t!("Usage: ARCTEXT <text> (select an arc first; the text follows it)").as_ref(),
crate::t!(
"Usage: ARCTEXT <text> (select an arc first; the text follows it)"
)
.as_ref(),
);
return None;
}
@@ -1433,7 +1434,8 @@ impl OpenCADStudio {
};
let chars: Vec<char> = text.chars().filter(|c| !c.is_control()).collect();
if chars.is_empty() {
self.command_line.push_error(crate::t!("ARCTEXT: no printable text.").as_ref());
self.command_line
.push_error(crate::t!("ARCTEXT: no printable text.").as_ref());
return None;
}
let n = chars.len();
@@ -1456,8 +1458,9 @@ impl OpenCADStudio {
self.tabs[i].scene.add_entity(codec::EntityType::Text(t));
}
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::tf!("ARCTEXT: placed {n} character(s) along the arc.").as_ref());
self.command_line.push_output(
crate::tf!("ARCTEXT: placed {n} character(s) along the arc.").as_ref(),
);
}
_ => return None,
+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(),
);
}
}
}
@@ -499,15 +519,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.
@@ -515,7 +541,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());
}
@@ -593,8 +620,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 ──────────────────────────
@@ -635,19 +663,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(),
);
}
}
}
@@ -680,10 +713,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();
@@ -723,11 +759,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(),
);
}
}
}
@@ -786,7 +825,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));
@@ -797,7 +838,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(
@@ -815,13 +858,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);
@@ -834,13 +887,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));
}
@@ -923,14 +998,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(),
);
}
}
}
@@ -948,17 +1027,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,
@@ -1022,8 +1105,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("");
@@ -1060,8 +1144,7 @@ impl OpenCADStudio {
}
"CLIP" => match arg {
"ON" => {
ul.flags |=
codec::entities::UnderlayDisplayFlags::CLIPPING;
ul.flags |= codec::entities::UnderlayDisplayFlags::CLIPPING;
changed += 1;
}
"OFF" => {
@@ -1140,8 +1223,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());
};
@@ -1156,11 +1241,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
}
@@ -1213,11 +1296,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());
}
@@ -1226,19 +1312,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]
@@ -1249,7 +1338,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");
@@ -1282,7 +1372,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:")),
],
@@ -1307,8 +1401,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");
@@ -1351,15 +1446,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());
}
}
@@ -1377,10 +1474,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));
}
@@ -1402,10 +1497,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));
}
@@ -1427,8 +1520,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");
@@ -1487,9 +1581,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(),
),
}
}
@@ -1503,8 +1597,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());
@@ -1522,20 +1624,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]
@@ -1545,20 +1649,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(),
);
}
}
}
@@ -1570,15 +1679,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(),
),
}
}
@@ -1624,8 +1735,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());
}
@@ -1666,9 +1779,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) {
@@ -1695,29 +1807,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(),
);
}
}
}
@@ -1784,15 +1899,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 {
@@ -1813,8 +1930,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");
@@ -1834,7 +1952,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 {
@@ -1864,8 +1986,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(),
);
}
@@ -1874,8 +1999,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));
}
@@ -1883,7 +2010,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());
}
@@ -1891,8 +2021,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");
@@ -1904,14 +2036,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(),
),
}
}
@@ -2046,8 +2181,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]
@@ -2057,7 +2198,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(),
@@ -2070,7 +2214,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]
@@ -2081,11 +2228,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]
+67 -67
View File
@@ -438,9 +438,7 @@ impl OpenCADStudio {
.document
.entities()
.filter_map(|entity| match entity {
codec::EntityType::Face3D(face) => {
Some((face.common.handle, face.clone()))
}
codec::EntityType::Face3D(face) => Some((face.common.handle, face.clone())),
_ => None,
})
.collect();
@@ -1030,9 +1028,8 @@ impl OpenCADStudio {
false,
true,
);
self.command_line.push_output(
format!("{} constraint bar(s) reset.", count).as_str(),
);
self.command_line
.push_output(format!("{} constraint bar(s) reset.", count).as_str());
}
}
@@ -1145,20 +1142,15 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(sel));
} else {
let scope = self.tabs[i].current_parametric_scope();
let inferred = self.tabs[i]
.scene
.inferred_parametric_constraints(
scope,
&handles,
&self.auto_constrain_settings,
);
let inferred = self.tabs[i].scene.inferred_parametric_constraints(
scope,
&handles,
&self.auto_constrain_settings,
);
if inferred.is_empty() {
self.command_line.push_output(
format!(
"0 constraint(s) applied to {} object(s).",
handles.len()
)
.as_str(),
format!("0 constraint(s) applied to {} object(s).", handles.len())
.as_str(),
);
self.tabs[i].scene.deselect_all();
self.refresh_selected_grips();
@@ -1281,8 +1273,7 @@ impl OpenCADStudio {
use crate::scene::named_parameters::DrivingValue;
use crate::scene::parametric_constraints::{
dynamic_dimension_text, next_angular_parameter_name,
next_dimensional_parameter_name, next_radial_parameter_name,
ConstraintKind,
next_dimensional_parameter_name, next_radial_parameter_name, ConstraintKind,
};
let scope = self.tabs[i].current_parametric_scope();
@@ -1322,7 +1313,11 @@ impl OpenCADStudio {
};
if self.tabs[i]
.scene
.validate_parametric_constraint(kind, &refs, Some(&DrivingValue::Literal(measured)))
.validate_parametric_constraint(
kind,
&refs,
Some(&DrivingValue::Literal(measured)),
)
.is_err()
{
refused += 1;
@@ -1451,10 +1446,12 @@ impl OpenCADStudio {
self.tabs[i].scene.deselect_all();
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line
.push_output(crate::tf!("{converted} associative dimensions converted").as_ref());
self.command_line.push_output(
crate::tf!("{refused} associative dimension(s) could not be converted").as_ref(),
crate::tf!("{converted} associative dimensions converted").as_ref(),
);
self.command_line.push_output(
crate::tf!("{refused} associative dimension(s) could not be converted")
.as_ref(),
);
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
@@ -1472,7 +1469,11 @@ impl OpenCADStudio {
let (kind, axis, label) = if vertical {
(ConstraintKind::Vertical, "Vertical", "Vertical constraint")
} else {
(ConstraintKind::Horizontal, "Horizontal", "Horizontal constraint")
(
ConstraintKind::Horizontal,
"Horizontal",
"Horizontal constraint",
)
};
let new_command = || {
if vertical {
@@ -1491,13 +1492,14 @@ impl OpenCADStudio {
.push_error(&format!("{axis}: select exactly one compatible object."));
} else {
let handle = handles[0];
let reference = self.tabs[i]
.scene
.document
.get_entity(handle)
.and_then(|entity| {
HorizontalConstraintCommand::preselected_reference(entity, handle)
});
let reference =
self.tabs[i]
.scene
.document
.get_entity(handle)
.and_then(|entity| {
HorizontalConstraintCommand::preselected_reference(entity, handle)
});
if let Some(reference) = reference {
let plane = self.tabs[i].ucs_xform().working_plane();
let direction = if vertical { plane.y } else { plane.x };
@@ -1532,13 +1534,14 @@ impl OpenCADStudio {
// the reference's own point-or-object prompt.
let handles = self.tabs[i].scene.selected_handles_in_order();
if let [handle] = handles.as_slice() {
let reference = self.tabs[i]
.scene
.document
.get_entity(*handle)
.and_then(|entity| {
FixConstraintCommand::preselected_reference(entity, *handle)
});
let reference =
self.tabs[i]
.scene
.document
.get_entity(*handle)
.and_then(|entity| {
FixConstraintCommand::preselected_reference(entity, *handle)
});
if let Some(reference) = reference {
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
kind: ConstraintKind::Fixed,
@@ -1718,13 +1721,15 @@ impl OpenCADStudio {
})
.collect::<Vec<_>>();
if picks.len() == 2 && picks[0].reference != picks[1].reference {
return Some(self.apply_cmd_result(CmdResult::AddPerpendicularConstraint {
first: picks[0].reference,
second: picks[1].reference,
first_fixed: picks[0].fixed_reference,
second_start: picks[1].start_reference,
label: "Perpendicular constraint",
}));
return Some(self.apply_cmd_result(
CmdResult::AddPerpendicularConstraint {
first: picks[0].reference,
second: picks[1].reference,
first_fixed: picks[0].fixed_reference,
second_start: picks[1].start_reference,
label: "Perpendicular constraint",
},
));
}
self.tabs[i].scene.deselect_all();
self.command_line.push_error(
@@ -1860,21 +1865,21 @@ impl OpenCADStudio {
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
} else {
use crate::command::{
CmdResult, SymmetricConstraintSelection,
};
use crate::command::{CmdResult, SymmetricConstraintSelection};
use crate::modules::parametric::SymmetricConstraintCommand;
let refs = (handles.len() == 3).then(|| {
let first = self.tabs[i].scene.document.get_entity(handles[0])?;
let second = self.tabs[i].scene.document.get_entity(handles[1])?;
let axis = self.tabs[i].scene.document.get_entity(handles[2])?;
SymmetricConstraintCommand::preselected_refs(
(first, handles[0]),
(second, handles[1]),
(axis, handles[2]),
)
}).flatten();
let refs = (handles.len() == 3)
.then(|| {
let first = self.tabs[i].scene.document.get_entity(handles[0])?;
let second = self.tabs[i].scene.document.get_entity(handles[1])?;
let axis = self.tabs[i].scene.document.get_entity(handles[2])?;
SymmetricConstraintCommand::preselected_refs(
(first, handles[0]),
(second, handles[1]),
(axis, handles[2]),
)
})
.flatten();
if let Some([first, second, axis]) = refs {
return Some(self.apply_cmd_result(CmdResult::AddSymmetricConstraint {
selection: SymmetricConstraintSelection::Objects(first, second),
@@ -1962,10 +1967,8 @@ impl OpenCADStudio {
// asked for this command.
// The reference asks for the circle itself, whatever is
// selected, so a pick-first set is left alone.
let name = next_radial_parameter_name(
self.tabs[i].scene.named_parameters(),
diameter,
);
let name =
next_radial_parameter_name(self.tabs[i].scene.named_parameters(), diameter);
let command = DimConstraintCommand::new(
if diameter {
DimConstraintAxis::Diameter
@@ -2537,10 +2540,7 @@ impl OpenCADStudio {
.filter(|handle| {
matches!(
scene.document.get_entity(*handle),
Some(
codec::EntityType::Solid3D(_)
| codec::EntityType::Surface(_)
)
Some(codec::EntityType::Solid3D(_) | codec::EntityType::Surface(_))
)
})
.collect::<Vec<_>>();
+82 -24
View File
@@ -3,10 +3,12 @@
use crate::app::{Message, OpenCADStudio};
use crate::command::CadCommand;
use crate::modules::annotate::field_cmd::{FieldObjectPickCommand, FieldPlaceCommand, FieldTablePickCommand};
use crate::modules::annotate::field_cmd::{
FieldObjectPickCommand, FieldPlaceCommand, FieldTablePickCommand,
};
use crate::ui::window::field_dialog::{
fields_of, nav_props, object_properties, FieldDialogMsg, FieldDialogState, FieldTarget, DATE_FORMATS,
NAMED_TYPES,
fields_of, nav_props, object_properties, FieldDialogMsg, FieldDialogState, FieldTarget,
DATE_FORMATS, NAMED_TYPES,
};
use iced::Task;
@@ -18,7 +20,10 @@ impl OpenCADStudio {
Some(Task::none())
}
// FIELDEVAL — the events that update fields, kept in the drawing.
cmd if cmd == "FIELDEVAL" || cmd.starts_with("FIELDEVAL ") || cmd.starts_with("SETVAR FIELDEVAL") => {
cmd if cmd == "FIELDEVAL"
|| cmd.starts_with("FIELDEVAL ")
|| cmd.starts_with("SETVAR FIELDEVAL") =>
{
let value = cmd
.trim_start_matches("SETVAR ")
.trim_start_matches("FIELDEVAL")
@@ -44,8 +49,9 @@ impl OpenCADStudio {
}
_ => {
let (min, max) = (0, 31);
self.command_line
.push_error(&crate::tf!("Requires an integer between {min} and {max}."));
self.command_line.push_error(&crate::tf!(
"Requires an integer between {min} and {max}."
));
ask(self);
}
}
@@ -58,9 +64,14 @@ impl OpenCADStudio {
.and_then(|h| u64::from_str_radix(h, 16).ok())
.map(codec::Handle::new);
if let Some(state) = self.field_dialog.as_mut() {
if let Some(entity) = handle.and_then(|h| self.tabs[i].scene.document.get_entity(h)) {
if let Some(entity) =
handle.and_then(|h| self.tabs[i].scene.document.get_entity(h))
{
let props = object_properties(entity);
state.object = Some((entity.common().handle, crate::t!(crate::entities::names::ui_name(entity)).into_owned()));
state.object = Some((
entity.common().handle,
crate::t!(crate::entities::names::ui_name(entity)).into_owned(),
));
state.object_prop = props.first().copied();
state.object_props = props;
}
@@ -100,7 +111,11 @@ impl OpenCADStudio {
let function = *parts.first()?;
let cell = |at: &[&str]| -> Option<(codec::Handle, usize, usize)> {
let handle = codec::Handle::new(u64::from_str_radix(at.first()?, 16).ok()?);
let point = glam::DVec3::new(at.get(1)?.parse().ok()?, at.get(2)?.parse().ok()?, at.get(3)?.parse().ok()?);
let point = glam::DVec3::new(
at.get(1)?.parse().ok()?,
at.get(2)?.parse().ok()?,
at.get(3)?.parse().ok()?,
);
let document = &self.tabs[i].scene.document;
let codec::EntityType::Table(table) = document.get_entity(handle)? else {
return None;
@@ -111,7 +126,11 @@ impl OpenCADStudio {
let name = |row: usize, column: usize| format!("{}{}", column_letters(column), row + 1);
let (table, row, column) = cell(parts.get(1..5)?)?;
if function == "Cell" {
return Some(format!("Table(%<\\_ObjId {:X}>%).{}", table.value(), name(row, column)));
return Some(format!(
"Table(%<\\_ObjId {:X}>%).{}",
table.value(),
name(row, column)
));
}
let (second_table, row2, column2) = cell(parts.get(5..9)?)?;
if second_table != table {
@@ -139,7 +158,13 @@ impl OpenCADStudio {
.collect();
if let Some(db) = self.sheet_set.db() {
let custom = codec::sheet_set::custom_properties(db.sheet_set());
let names = |flag: i32| custom.iter().filter(|(_, _, f)| f & flag != 0).map(|(n, _, _)| n.clone()).collect();
let names = |flag: i32| {
custom
.iter()
.filter(|(_, _, f)| f & flag != 0)
.map(|(n, _, _)| n.clone())
.collect()
};
state.ss_sheet_custom = names(codec::sheet_set::CUSTOM_SHEET_PROP);
state.ss_set_custom = names(codec::sheet_set::CUSTOM_SHEET_SET_PROP);
state.ss_set_name = db.name().to_string();
@@ -153,8 +178,13 @@ impl OpenCADStudio {
}
fn field_value(&self, code: &str, objects: &[codec::Handle]) -> String {
crate::entities::field::evaluate(&self.tabs[self.active_tab].scene.document, code, objects, None)
.unwrap_or_else(|| "----".to_string())
crate::entities::field::evaluate(
&self.tabs[self.active_tab].scene.document,
code,
objects,
None,
)
.unwrap_or_else(|| "----".to_string())
}
fn refresh_field_preview(&mut self) {
@@ -165,7 +195,8 @@ impl OpenCADStudio {
let preview = if state.complete() {
if state.name == "BlockPlaceholder" {
// A placeholder shows its property name until a block reference resolves it.
let (label, _, _) = codec::fields::BLOCK_PLACEHOLDER_PROPERTIES[state.placeholder_property];
let (label, _, _) =
codec::fields::BLOCK_PLACEHOLDER_PROPERTIES[state.placeholder_property];
text_case(label, state.text_case)
} else {
self.field_value(&code, &objects)
@@ -191,16 +222,32 @@ impl OpenCADStudio {
.filter(|b| !b.name.starts_with('*'))
.map(|b| (b.name.clone(), b.handle))
.collect(),
"Dimstyle" => document.dim_styles.iter().map(|s| (s.name.clone(), s.handle)).collect(),
"Layer" => document.layers.iter().map(|l| (l.name.clone(), l.handle)).collect(),
"Linetype" => document.line_types.iter().map(|l| (l.name.clone(), l.handle)).collect(),
"Dimstyle" => document
.dim_styles
.iter()
.map(|s| (s.name.clone(), s.handle))
.collect(),
"Layer" => document
.layers
.iter()
.map(|l| (l.name.clone(), l.handle))
.collect(),
"Linetype" => document
.line_types
.iter()
.map(|l| (l.name.clone(), l.handle))
.collect(),
"Textstyle" => document
.text_styles
.iter()
.filter(|s| !s.name.is_empty())
.map(|s| (s.name.clone(), s.handle))
.collect(),
"View" => document.views.iter().map(|v| (v.name.clone(), v.handle)).collect(),
"View" => document
.views
.iter()
.map(|v| (v.name.clone(), v.handle))
.collect(),
_ => Vec::new(),
};
names.sort_by_key(|(n, _)| n.to_lowercase());
@@ -378,8 +425,9 @@ impl OpenCADStudio {
match state.target {
FieldTarget::NewText => {
self.close_active_modal();
let defaults =
crate::scene::creation_style::current_text_defaults(&self.tabs[i].scene.document);
let defaults = crate::scene::creation_style::current_text_defaults(
&self.tabs[i].scene.document,
);
let command = FieldPlaceCommand::new(
value,
field.expect("set above"),
@@ -416,8 +464,12 @@ impl OpenCADStudio {
let target = self.field_dialog.take().map(|s| s.target);
self.close_active_modal();
self.active_modal = match target {
Some(FieldTarget::AttdefDefault) if self.attdef_dialog.is_some() => Some(crate::app::ModalKind::AttDef),
Some(FieldTarget::AttdefEdit) if self.attdef_edit.is_some() => Some(crate::app::ModalKind::AttDefEdit),
Some(FieldTarget::AttdefDefault) if self.attdef_dialog.is_some() => {
Some(crate::app::ModalKind::AttDef)
}
Some(FieldTarget::AttdefEdit) if self.attdef_edit.is_some() => {
Some(crate::app::ModalKind::AttDefEdit)
}
_ => None,
};
}
@@ -445,14 +497,20 @@ fn text_case(value: &str, case: usize) -> String {
3 => {
let lower = value.to_lowercase();
let mut chars = lower.chars();
chars.next().map(|c| c.to_uppercase().chain(chars).collect()).unwrap_or_default()
chars
.next()
.map(|c| c.to_uppercase().chain(chars).collect())
.unwrap_or_default()
}
4 => value
.split(' ')
.map(|word| {
let lower = word.to_lowercase();
let mut chars = lower.chars();
chars.next().map(|c| c.to_uppercase().chain(chars).collect::<String>()).unwrap_or_default()
chars
.next()
.map(|c| c.to_uppercase().chain(chars).collect::<String>())
.unwrap_or_default()
})
.collect::<Vec<_>>()
.join(" "),
+148 -80
View File
@@ -28,8 +28,9 @@ impl OpenCADStudio {
// Tabs without a path (never saved) are skipped with a note.
"SAVEALL" => {
if self.read_only {
self.command_line
.push_error(crate::t!("Read-only session (--read-only): saving is disabled.").as_ref());
self.command_line.push_error(
crate::t!("Read-only session (--read-only): saving is disabled.").as_ref(),
);
return Some(Task::none());
}
#[cfg(not(target_arch = "wasm32"))]
@@ -41,47 +42,47 @@ impl OpenCADStudio {
continue;
}
if let Some(path) = self.tabs[t].current_path.clone() {
if self
.active_save_jobs
.contains_key(&self.tabs[t].id)
{
self.command_line.push_info(crate::tf!(
"SAVEALL: {} already has a save running",
path.display()
).as_ref());
if self.active_save_jobs.contains_key(&self.tabs[t].id) {
self.command_line.push_info(
crate::tf!(
"SAVEALL: {} already has a save running",
path.display()
)
.as_ref(),
);
skipped += 1;
continue;
}
match self.save_tab_synchronously_protected(
t,
path.clone(),
false,
) {
match self.save_tab_synchronously_protected(t, path.clone(), false) {
Ok(()) => {
saved += 1;
}
Err(e) => self.command_line.push_error(crate::tf!(
"SAVEALL: {} failed: {e}",
path.display()
).as_ref()),
Err(e) => self.command_line.push_error(
crate::tf!("SAVEALL: {} failed: {e}", path.display()).as_ref(),
),
}
} else {
skipped += 1;
}
}
self.command_line.push_output(crate::tf!(
"SAVEALL: saved {saved} drawing(s){}.",
if skipped > 0 {
crate::tf!("; {skipped} need SAVEAS (no file path yet)").into_owned()
} else {
String::new()
}
).as_ref());
self.command_line.push_output(
crate::tf!(
"SAVEALL: saved {saved} drawing(s){}.",
if skipped > 0 {
crate::tf!("; {skipped} need SAVEAS (no file path yet)")
.into_owned()
} else {
String::new()
}
)
.as_ref(),
);
}
#[cfg(target_arch = "wasm32")]
{
self.command_line
.push_info(crate::t!("SAVEALL: save each tab individually in the web build.").as_ref());
self.command_line.push_info(
crate::t!("SAVEALL: save each tab individually in the web build.").as_ref(),
);
}
return Some(Task::none());
}
@@ -105,7 +106,8 @@ impl OpenCADStudio {
// without undoing any work done since.
"OOPS" => {
if self.oops_cache.is_empty() {
self.command_line.push_info(crate::t!("OOPS: nothing to restore.").as_ref());
self.command_line
.push_info(crate::t!("OOPS: nothing to restore.").as_ref());
} else {
let restored = std::mem::take(&mut self.oops_cache);
let pending = self.begin_undo(i, "OOPS", restored.len(), true);
@@ -202,7 +204,11 @@ impl OpenCADStudio {
}
for d in deps {
let dp = PathBuf::from(&d);
let resolved = if dp.is_absolute() { dp } else { parent.join(&dp) };
let resolved = if dp.is_absolute() {
dp
} else {
parent.join(&dp)
};
if resolved.exists() {
if let Some(fname) = resolved.file_name() {
if std::fs::copy(&resolved, folder.join(fname)).is_ok() {
@@ -211,18 +217,23 @@ impl OpenCADStudio {
}
}
}
self.command_line.push_output(crate::tf!(
"{cmd}: packaged {copied} file(s) into {}",
folder.display()
).as_ref());
self.command_line.push_output(
crate::tf!(
"{cmd}: packaged {copied} file(s) into {}",
folder.display()
)
.as_ref(),
);
}
Err(e) => self
.command_line
.push_error(crate::tf!("{cmd}: cannot create folder ({e}).").as_ref()),
}
} else {
self.command_line
.push_error(crate::t!("ARCHIVE: save the drawing first (it has no file path yet).").as_ref());
self.command_line.push_error(
crate::t!("ARCHIVE: save the drawing first (it has no file path yet).")
.as_ref(),
);
}
}
@@ -241,11 +252,14 @@ impl OpenCADStudio {
"PERF" => {
self.perf_hud = !self.perf_hud;
crate::perf::set_ui_enabled(self.perf_hud);
self.command_line.push_info(crate::t!(if self.perf_hud {
"PERF panel on — tracing render and interaction costs"
} else {
"PERF panel off"
}).as_ref());
self.command_line.push_info(
crate::t!(if self.perf_hud {
"PERF panel on — tracing render and interaction costs"
} else {
"PERF panel off"
})
.as_ref(),
);
return Some(Task::none());
}
@@ -277,14 +291,16 @@ impl OpenCADStudio {
self.model_space.mode = crate::app::config::ModelSpaceMode::MatchTheme;
self.sync_model_space_theme(true);
self.persist_settings_if_changed();
self.command_line
.push_output(crate::t!("Model space background set to Match Theme.").as_ref());
self.command_line.push_output(
crate::t!("Model space background set to Match Theme.").as_ref(),
);
} else if first_arg == "CLASSIC" {
self.model_space.mode = crate::app::config::ModelSpaceMode::ClassicDark;
self.sync_model_space_theme(true);
self.persist_settings_if_changed();
self.command_line
.push_output(crate::t!("Model space background set to Classic CAD Dark.").as_ref());
self.command_line.push_output(
crate::t!("Model space background set to Classic CAD Dark.").as_ref(),
);
} else if first_arg == "DEFAULT" || first_arg == "RESET" {
if is_paper {
self.model_space.custom_paper_bg = None;
@@ -300,31 +316,48 @@ impl OpenCADStudio {
.push_output(crate::t!("Background reset to default.").as_ref());
} else if first_arg == "DESK" {
let sub_args = &args[1..];
let sub_first = sub_args.first().map(|s| s.to_uppercase()).unwrap_or_default();
let sub_first = sub_args
.first()
.map(|s| s.to_uppercase())
.unwrap_or_default();
if sub_first == "DEFAULT" || sub_first == "RESET" {
self.model_space.custom_desk_bg = None;
self.sync_model_space_theme(false);
self.persist_settings_if_changed();
self.command_line
.push_output(crate::t!("Paper desk surround background reset to default.").as_ref());
self.command_line.push_output(
crate::t!("Paper desk surround background reset to default.").as_ref(),
);
} else if let Some(rgba) = parse_background_color(sub_args) {
let [r, g, b, _] = rgba;
let rgb = [(r * 255.0).round() as u8, (g * 255.0).round() as u8, (b * 255.0).round() as u8];
let rgb = [
(r * 255.0).round() as u8,
(g * 255.0).round() as u8,
(b * 255.0).round() as u8,
];
self.model_space.custom_desk_bg = Some(rgb);
self.sync_model_space_theme(false);
self.persist_settings_if_changed();
self.command_line.push_output(crate::tf!(
"Desk surround background: rgb({}, {}, {})",
rgb[0],
rgb[1],
rgb[2]
).as_ref());
self.command_line.push_output(
crate::tf!(
"Desk surround background: rgb({}, {}, {})",
rgb[0],
rgb[1],
rgb[2]
)
.as_ref(),
);
} else {
self.command_line.push_info(crate::t!("Usage: BACKGROUND DESK <r> <g> <b> | DEFAULT").as_ref());
self.command_line.push_info(
crate::t!("Usage: BACKGROUND DESK <r> <g> <b> | DEFAULT").as_ref(),
);
}
} else if let Some(rgba) = parse_background_color(&args) {
let [r, g, b, _] = rgba;
let rgb = [(r * 255.0).round() as u8, (g * 255.0).round() as u8, (b * 255.0).round() as u8];
let rgb = [
(r * 255.0).round() as u8,
(g * 255.0).round() as u8,
(b * 255.0).round() as u8,
];
if is_paper {
self.model_space.custom_paper_bg = Some(rgb);
self.paper_bg_input = crate::app::config::rgb_to_hex(rgb);
@@ -335,12 +368,9 @@ impl OpenCADStudio {
}
self.sync_model_space_theme(true);
self.persist_settings_if_changed();
self.command_line.push_output(crate::tf!(
"Background: rgb({}, {}, {})",
rgb[0],
rgb[1],
rgb[2]
).as_ref());
self.command_line.push_output(
crate::tf!("Background: rgb({}, {}, {})", rgb[0], rgb[1], rgb[2]).as_ref(),
);
} else {
self.command_line.push_info(
crate::t!("Usage: BACKGROUND <r> <g> <b> (0–255) | THEME|CLASSIC|DEFAULT|DESK|BLACK|DARKGRAY|GRAY|LIGHTGRAY|WHITE").as_ref(),
@@ -397,15 +427,19 @@ impl OpenCADStudio {
.filter(|l| !l.starts_with('#') && !l.starts_with(';'))
.map(|l| Task::done(Message::ScriptLine(l.to_string())))
.collect();
self.command_line.push_output(crate::tf!(
"SCRIPT: running {} command(s) from {clean_path}.",
cmds.len()
).as_ref());
self.command_line.push_output(
crate::tf!(
"SCRIPT: running {} command(s) from {clean_path}.",
cmds.len()
)
.as_ref(),
);
return Some(Task::batch(cmds));
}
Err(e) => {
self.command_line
.push_error(crate::tf!("SCRIPT: cannot read {clean_path}: {e}").as_ref());
self.command_line.push_error(
crate::tf!("SCRIPT: cannot read {clean_path}: {e}").as_ref(),
);
}
}
}
@@ -480,17 +514,30 @@ mod tests {
// Feed standard multi-line CAD script sequence
let _ = app.update(Message::ScriptLine("LINE".into()));
assert!(app.tabs[i].active_cmd.is_some(), "LINE command should be active");
assert!(
app.tabs[i].active_cmd.is_some(),
"LINE command should be active"
);
let _ = app.update(Message::ScriptLine("0,0".into()));
assert!(app.tabs[i].active_cmd.is_some(), "LINE command should stay active after first point");
assert!(
app.tabs[i].active_cmd.is_some(),
"LINE command should stay active after first point"
);
let _ = app.update(Message::ScriptLine("10,10".into()));
let _ = app.update(Message::ScriptLine("".into())); // Enter to finish
let after_count = app.tabs[i].scene.document.entities().count();
assert_eq!(after_count, before_count + 1, "Line entity should have been created");
assert!(app.tabs[i].active_cmd.is_none(), "LINE command should be finished");
assert_eq!(
after_count,
before_count + 1,
"Line entity should have been created"
);
assert!(
app.tabs[i].active_cmd.is_none(),
"LINE command should be finished"
);
}
#[test]
@@ -502,8 +549,15 @@ mod tests {
let _ = app.update(Message::ScriptLine("CIRCLE 5,5 2".into()));
let after_count = app.tabs[i].scene.document.entities().count();
assert_eq!(after_count, before_count + 1, "Circle entity should have been created");
assert!(app.tabs[i].active_cmd.is_none(), "CIRCLE command should be finished");
assert_eq!(
after_count,
before_count + 1,
"Circle entity should have been created"
);
assert!(
app.tabs[i].active_cmd.is_none(),
"CIRCLE command should be finished"
);
}
#[test]
@@ -515,7 +569,11 @@ mod tests {
let _ = app.update(Message::ScriptLine("_CIRCLE 10,10 3".into()));
let after_count = app.tabs[i].scene.document.entities().count();
assert_eq!(after_count, before_count + 1, "_CIRCLE should resolve to CIRCLE and create entity");
assert_eq!(
after_count,
before_count + 1,
"_CIRCLE should resolve to CIRCLE and create entity"
);
}
#[test]
@@ -546,7 +604,11 @@ CIRCLE 50,50 15
ZOOM EXTENTS
"#;
for line in SCRIPT.lines().map(str::trim).filter(|l| !l.starts_with('#') && !l.starts_with(';')) {
for line in SCRIPT
.lines()
.map(str::trim)
.filter(|l| !l.starts_with('#') && !l.starts_with(';'))
{
let _ = app.update(Message::ScriptLine(line.to_string()));
}
// 4 square edges + 2 diagonal lines + 2 circles = 8 entities
@@ -572,7 +634,10 @@ ZOOM EXTENTS
// 2. Blank line when idle repeats LINE
let _ = app.update(Message::ScriptLine("".into()));
assert!(app.tabs[i].active_cmd.is_some(), "Blank line should repeat LINE");
assert!(
app.tabs[i].active_cmd.is_some(),
"Blank line should repeat LINE"
);
let _ = app.update(Message::ScriptLine("20,20".into()));
let _ = app.update(Message::ScriptLine("30,30".into()));
let _ = app.update(Message::ScriptLine("".into())); // finishes second LINE
@@ -586,7 +651,10 @@ ZOOM EXTENTS
// Blank line repeats CIRCLE
let _ = app.update(Message::ScriptLine("".into()));
assert!(app.tabs[i].active_cmd.is_some(), "Blank line should repeat CIRCLE");
assert!(
app.tabs[i].active_cmd.is_some(),
"Blank line should repeat CIRCLE"
);
let _ = app.update(Message::ScriptLine("15,15".into()));
let _ = app.update(Message::ScriptLine("3".into()));
assert_eq!(app.tabs[i].scene.document.entities().count(), 4);
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+110 -105
View File
@@ -37,7 +37,8 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.command_line.push_info(crate::t!("Layer(s) turned off.").as_ref());
self.command_line
.push_info(crate::t!("Layer(s) turned off.").as_ref());
}
}
@@ -74,7 +75,8 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.command_line.push_info(crate::t!("Layer(s) frozen.").as_ref());
self.command_line
.push_info(crate::t!("Layer(s) frozen.").as_ref());
}
}
@@ -91,7 +93,9 @@ impl OpenCADStudio {
.filter(|&(code, _)| code != 0)
.map(|(code, label)| (label, units::short(code), None))
.collect();
let current = crate::t!(units::label(self.tabs[i].scene.document.header.insertion_units));
let current = crate::t!(units::label(
self.tabs[i].scene.document.header.insertion_units
));
self.command_line
.push_info(crate::tf!("DWGUNITS current unit: {current}").as_ref());
let c = KeywordCommand::new("DWGUNITS", "DWGUNITS Convert drawing to:", choices);
@@ -124,7 +128,8 @@ impl OpenCADStudio {
cmd if cmd.starts_with("LAYDEL ") => {
let name = cmd.trim_start_matches("LAYDEL").trim();
if name.is_empty() {
self.command_line.push_info(crate::t!("Usage: LAYDEL <layer name>").as_ref());
self.command_line
.push_info(crate::t!("Usage: LAYDEL <layer name>").as_ref());
return Some(Task::none());
}
let resolved = self.tabs[i]
@@ -166,9 +171,9 @@ impl OpenCADStudio {
self.tabs[i].scene.bump_geometry();
self.tabs[i].dirty = true;
self.refresh_layer_panel();
self.command_line.push_output(crate::tf!(
"LAYDEL: deleted layer \"{layer}\" and {n} object(s)."
).as_ref());
self.command_line.push_output(
crate::tf!("LAYDEL: deleted layer \"{layer}\" and {n} object(s).").as_ref(),
);
}
// LAYMRG <source> <target> — move every object from <source> onto
@@ -187,8 +192,9 @@ impl OpenCADStudio {
let rest = cmd.trim_start_matches("LAYMRG").trim();
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() != 2 {
self.command_line
.push_info(crate::t!("Usage: LAYMRG <source layer> <target layer>").as_ref());
self.command_line.push_info(
crate::t!("Usage: LAYMRG <source layer> <target layer>").as_ref(),
);
return Some(Task::none());
}
let keys: Vec<String> = self.tabs[i]
@@ -207,18 +213,21 @@ impl OpenCADStudio {
.find(|k| k.eq_ignore_ascii_case(parts[1]))
.cloned();
let (Some(src), Some(dst)) = (src, dst) else {
self.command_line
.push_error(crate::t!("LAYMRG: source and target layers must both exist.").as_ref());
self.command_line.push_error(
crate::t!("LAYMRG: source and target layers must both exist.").as_ref(),
);
return Some(Task::none());
};
if src == dst {
self.command_line
.push_error(crate::t!("LAYMRG: source and target are the same layer.").as_ref());
self.command_line.push_error(
crate::t!("LAYMRG: source and target are the same layer.").as_ref(),
);
return Some(Task::none());
}
if src == "0" {
self.command_line
.push_error(crate::t!("LAYMRG: layer \"0\" cannot be merged away.").as_ref());
self.command_line.push_error(
crate::t!("LAYMRG: layer \"0\" cannot be merged away.").as_ref(),
);
return Some(Task::none());
}
if src.eq_ignore_ascii_case(&self.tabs[i].active_layer) {
@@ -241,9 +250,10 @@ impl OpenCADStudio {
.invalidate_layer_dependencies(std::slice::from_ref(&dst));
self.tabs[i].dirty = true;
self.refresh_layer_panel();
self.command_line.push_output(crate::tf!(
"LAYMRG: merged \"{src}\" into \"{dst}\" ({moved} object(s))."
).as_ref());
self.command_line.push_output(
crate::tf!("LAYMRG: merged \"{src}\" into \"{dst}\" ({moved} object(s)).")
.as_ref(),
);
}
// LAYERSTATE — save / restore named snapshots of all layer states
@@ -269,19 +279,24 @@ impl OpenCADStudio {
let states = self.tabs[i].scene.document.layer_states();
if states.is_empty() {
self.command_line.push_info(
crate::t!("LAYERSTATE: no saved states. Use LAYERSTATE SAVE <name>.").as_ref(),
crate::t!(
"LAYERSTATE: no saved states. Use LAYERSTATE SAVE <name>."
)
.as_ref(),
);
} else {
let mut names: Vec<&str> =
states.iter().map(|state| state.name.as_str()).collect();
names.sort_unstable();
self.command_line
.push_output(crate::tf!("Saved layer states: {}", names.join(", ")).as_ref());
self.command_line.push_output(
crate::tf!("Saved layer states: {}", names.join(", ")).as_ref(),
);
}
}
"SAVE" | "S" => {
if arg.is_empty() {
self.command_line.push_info(crate::t!("Usage: LAYERSTATE SAVE <name>").as_ref());
self.command_line
.push_info(crate::t!("Usage: LAYERSTATE SAVE <name>").as_ref());
} else {
let description = self.tabs[i]
.scene
@@ -304,9 +319,9 @@ impl OpenCADStudio {
self.command_line
.push_info(crate::t!("Usage: LAYERSTATE RESTORE <name>").as_ref());
} else if self.tabs[i].scene.document.layer_state(arg).is_none() {
self.command_line.push_error(crate::tf!(
"LAYERSTATE: no saved state named \"{arg}\"."
).as_ref());
self.command_line.push_error(
crate::tf!("LAYERSTATE: no saved state named \"{arg}\".").as_ref(),
);
} else {
let names: Vec<String> = self.tabs[i]
.scene
@@ -330,9 +345,10 @@ impl OpenCADStudio {
self.tabs[i].scene.invalidate_layer_dependencies(&names);
self.tabs[i].dirty = true;
self.refresh_layer_panel();
self.command_line.push_output(crate::tf!(
"LAYERSTATE: restored \"{arg}\" ({n} layer(s))."
).as_ref());
self.command_line.push_output(
crate::tf!("LAYERSTATE: restored \"{arg}\" ({n} layer(s)).")
.as_ref(),
);
}
}
"DELETE" | "D" => {
@@ -340,9 +356,9 @@ impl OpenCADStudio {
self.command_line
.push_info(crate::t!("Usage: LAYERSTATE DELETE <name>").as_ref());
} else if self.tabs[i].scene.document.layer_state(arg).is_none() {
self.command_line.push_error(crate::tf!(
"LAYERSTATE: no saved state named \"{arg}\"."
).as_ref());
self.command_line.push_error(
crate::tf!("LAYERSTATE: no saved state named \"{arg}\".").as_ref(),
);
} else {
self.push_undo_snapshot(i, "LAYERSTATE DELETE");
self.tabs[i].scene.document.delete_layer_state(arg);
@@ -352,8 +368,10 @@ impl OpenCADStudio {
}
}
_ => {
self.command_line
.push_info(crate::t!("Usage: LAYERSTATE SAVE|RESTORE|DELETE <name> | ? (list)").as_ref());
self.command_line.push_info(
crate::t!("Usage: LAYERSTATE SAVE|RESTORE|DELETE <name> | ? (list)")
.as_ref(),
);
}
}
}
@@ -389,7 +407,8 @@ impl OpenCADStudio {
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.refresh_properties();
self.command_line.push_info(crate::t!("Layer(s) locked.").as_ref());
self.command_line
.push_info(crate::t!("Layer(s) locked.").as_ref());
}
}
@@ -444,7 +463,8 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.command_line.push_info(crate::t!("All layers turned on.").as_ref());
self.command_line
.push_info(crate::t!("All layers turned on.").as_ref());
}
"LAYTHW" => {
@@ -465,7 +485,8 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.command_line.push_info(crate::t!("All layers thawed.").as_ref());
self.command_line
.push_info(crate::t!("All layers thawed.").as_ref());
}
"LAYULK" => {
@@ -499,7 +520,8 @@ impl OpenCADStudio {
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.refresh_properties();
self.command_line.push_info(crate::t!("Layer(s) unlocked.").as_ref());
self.command_line
.push_info(crate::t!("Layer(s) unlocked.").as_ref());
}
}
@@ -512,8 +534,10 @@ impl OpenCADStudio {
.map(|(_, e)| e.common().layer.clone())
.collect();
if sel_layers.is_empty() {
self.command_line
.push_error(crate::t!("LAYISO: select entities on the layers to isolate first.").as_ref());
self.command_line.push_error(
crate::t!("LAYISO: select entities on the layers to isolate first.")
.as_ref(),
);
} else {
let names: Vec<String> = self.tabs[i]
.scene
@@ -534,21 +558,22 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.command_line
.push_info(crate::tf!("LAYISO: isolated {} layer(s).", sel_layers.len()).as_ref());
self.command_line.push_info(
crate::tf!("LAYISO: isolated {} layer(s).", sel_layers.len()).as_ref(),
);
}
}
// ISOLATEOBJECTS — hide every object except the current selection
"ISOLATEOBJECTS" => {
if self.tabs[i].scene.selected.is_empty() {
self.command_line
.push_error(crate::t!("ISOLATEOBJECTS: select the objects to isolate first.").as_ref());
self.command_line.push_error(
crate::t!("ISOLATEOBJECTS: select the objects to isolate first.").as_ref(),
);
} else {
let n = self.tabs[i].scene.selected.len();
let before = self.tabs[i].scene.object_isolation.clone();
let selected_before =
self.tabs[i].scene.selected.iter().copied().collect();
let selected_before = self.tabs[i].scene.selected.iter().copied().collect();
self.tabs[i].scene.isolate_selected();
self.push_object_visibility_history(
i,
@@ -556,32 +581,28 @@ impl OpenCADStudio {
before,
selected_before,
);
self.command_line.push_info(crate::tf!(
"Isolated {n} object(s). UNISOLATEOBJECTS to restore."
).as_ref());
self.command_line.push_info(
crate::tf!("Isolated {n} object(s). UNISOLATEOBJECTS to restore.").as_ref(),
);
}
}
// HIDEOBJECTS — hide the current selection
"HIDEOBJECTS" => {
if self.tabs[i].scene.selected.is_empty() {
self.command_line
.push_error(crate::t!("HIDEOBJECTS: select the objects to hide first.").as_ref());
self.command_line.push_error(
crate::t!("HIDEOBJECTS: select the objects to hide first.").as_ref(),
);
} else {
let n = self.tabs[i].scene.selected.len();
let before = self.tabs[i].scene.object_isolation.clone();
let selected_before =
self.tabs[i].scene.selected.iter().copied().collect();
let selected_before = self.tabs[i].scene.selected.iter().copied().collect();
self.tabs[i].scene.hide_selected();
self.push_object_visibility_history(
i,
"HIDEOBJECTS",
before,
selected_before,
);
self.push_object_visibility_history(i, "HIDEOBJECTS", before, selected_before);
self.refresh_properties();
self.command_line
.push_info(crate::tf!("Hid {n} object(s). UNISOLATEOBJECTS to restore.").as_ref());
self.command_line.push_info(
crate::tf!("Hid {n} object(s). UNISOLATEOBJECTS to restore.").as_ref(),
);
}
}
@@ -589,8 +610,7 @@ impl OpenCADStudio {
"UNISOLATEOBJECTS" => {
if self.tabs[i].scene.is_isolation_active() {
let before = self.tabs[i].scene.object_isolation.clone();
let selected_before =
self.tabs[i].scene.selected.iter().copied().collect();
let selected_before = self.tabs[i].scene.selected.iter().copied().collect();
self.tabs[i].scene.end_isolation();
self.push_object_visibility_history(
i,
@@ -601,7 +621,8 @@ impl OpenCADStudio {
self.command_line
.push_info(crate::t!("Isolation ended — all objects shown.").as_ref());
} else {
self.command_line.push_info(crate::t!("No hidden objects.").as_ref());
self.command_line
.push_info(crate::t!("No hidden objects.").as_ref());
}
}
@@ -630,8 +651,7 @@ impl OpenCADStudio {
"QLAYER" => {
use crate::modules::draw::layers::quick_layer::{
QuickLayerCommand,
QuickLayerEntry,
QuickLayerCommand, QuickLayerEntry,
};
let layers: Vec<QuickLayerEntry> = self.tabs[i]
@@ -668,9 +688,8 @@ impl OpenCADStudio {
let encoded = cmd.trim_start_matches("QLAYER_APPLY_HEX").trim();
let Some(requested) = QuickLayerCommand::decode_layer_name(encoded) else {
self.command_line.push_error(
crate::t!("QLAYER: invalid encoded layer name.").as_ref(),
);
self.command_line
.push_error(crate::t!("QLAYER: invalid encoded layer name.").as_ref());
return Some(Task::none());
};
@@ -683,9 +702,8 @@ impl OpenCADStudio {
.map(|name| name.to_string());
let Some(layer) = resolved else {
self.command_line.push_error(
crate::tf!("QLAYER: no layer named \"{requested}\".").as_ref(),
);
self.command_line
.push_error(crate::tf!("QLAYER: no layer named \"{requested}\".").as_ref());
return Some(Task::none());
};
@@ -700,10 +718,7 @@ impl OpenCADStudio {
match self.set_current_layer_name(i, &layer) {
Ok(()) => {
self.command_line.push_info(
crate::tf!(
"QLAYER: current layer set to \"{layer}\"."
)
.as_ref(),
crate::tf!("QLAYER: current layer set to \"{layer}\".").as_ref(),
);
}
Err(error) => {
@@ -713,30 +728,19 @@ impl OpenCADStudio {
} else {
let count = handles.len();
self.apply_property_op(
i,
"QLAYER",
&handles,
|app, handle| {
if let Some(entity) =
app.tabs[i].scene.document.get_entity_mut(handle)
{
crate::scene::view::dispatch::apply_common_prop(
entity,
"layer",
&layer,
);
}
},
);
self.apply_property_op(i, "QLAYER", &handles, |app, handle| {
if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
crate::scene::view::dispatch::apply_common_prop(
entity, "layer", &layer,
);
}
});
self.ribbon.active_layer = layer.clone();
self.command_line.push_info(
crate::tf!(
"QLAYER: moved {count} object(s) to layer \"{layer}\"."
)
.as_ref(),
crate::tf!("QLAYER: moved {count} object(s) to layer \"{layer}\".")
.as_ref(),
);
}
}
@@ -840,8 +844,10 @@ impl OpenCADStudio {
let mappings = laytrans::map_same(&sources, &targets);
if mappings.is_empty() {
self.command_line.push_info(
crate::t!("LAYTRANS: no layer names in common — use LAYTRANS with no file to map them.")
.as_ref(),
crate::t!(
"LAYTRANS: no layer names in common — use LAYTRANS with no file to map them."
)
.as_ref(),
);
return Task::none();
}
@@ -968,11 +974,7 @@ impl OpenCADStudio {
.abs()
> 1.0e-12
{
self.tabs[i]
.scene
.document
.header
.annotation_scale_value /= factor;
self.tabs[i].scene.document.header.annotation_scale_value /= factor;
}
// Sizes the drawing keeps as settings rather than as geometry: dash
// lengths, default heights and widths, the radii the fillet and chamfer
@@ -1027,8 +1029,7 @@ impl OpenCADStudio {
&mut self,
preload: Option<std::path::PathBuf>,
) -> Task<Message> {
self.layer_translator
.get_or_insert_with(Default::default);
self.layer_translator.get_or_insert_with(Default::default);
self.active_modal = Some(crate::app::ModalKind::LayerTranslator);
match preload {
Some(path) => Task::done(Message::LayerTranslatorLoaded(path)),
@@ -1042,7 +1043,11 @@ impl OpenCADStudio {
/// there would mean inventing a place inside the DWG for something no other
/// application reads back. A small text file beside the drawing carries the
/// same information without touching the drawing's own structure.
pub(in crate::app) fn layer_translator_mappings_file(&mut self, path: &std::path::Path, save: bool) {
pub(in crate::app) fn layer_translator_mappings_file(
&mut self,
path: &std::path::Path,
save: bool,
) {
use crate::modules::draw::layers::laytrans::Mapping;
if save {
let Some(state) = self.layer_translator.as_ref() else {
+29 -26
View File
@@ -5,29 +5,29 @@ use iced::Task;
use std::path::PathBuf;
mod attdef;
mod field;
mod count;
mod blocks;
mod xref_attach;
mod blockvars;
mod count;
mod dim;
pub(crate) mod display;
mod draw;
mod field;
mod fileops;
mod inquiry;
mod layerprops;
mod layers;
pub(crate) mod pdf_import;
mod point_cloud;
mod pc_colormap;
mod pc_extract;
mod pdf_underlay;
mod pdf_dialogs;
pub(crate) mod sheet_set;
mod xclip;
mod blockvars;
pub(crate) mod pdf_import;
mod pdf_underlay;
mod plotvars;
mod point_cloud;
pub(crate) mod sheet_set;
mod styleprops;
mod view;
mod xclip;
mod xref_attach;
// `DrawOrderRefCommand` lives in the `view` family file but is referenced by
// path (`commands::DrawOrderRefCommand`) from `update.rs`, so re-export it at
@@ -80,7 +80,8 @@ impl OpenCADStudio {
.scene
.selection
.borrow_mut()
.input.right_click_entered = false;
.input
.right_click_entered = false;
// A fresh command starts at the polar/cartesian default — clear
// any `,`-driven reshape and locked dynamic-input values from a
// previous command. Otherwise a bare Enter on the first point prompt
@@ -212,8 +213,9 @@ impl OpenCADStudio {
// the single place that decides; `on_ribbon_tool_click` defers to it
// rather than keeping a second, blunter copy (#388, #389).
if self.tabs[i].is_start && !start_allowed(strip_command_prefixes(cmd).unwrap_or(cmd)) {
self.command_line
.push_info(crate::t!("No drawing open. Use NEW or OPEN to start a drawing.").as_ref());
self.command_line.push_info(
crate::t!("No drawing open. Use NEW or OPEN to start a drawing.").as_ref(),
);
return Task::none();
}
@@ -377,7 +379,13 @@ impl OpenCADStudio {
/// Commands that may run transparently (`'PAN`, `'ZOOM …`): the navigation
/// set, which never edits the drawing.
fn is_transparent_capable(cmd: &str) -> bool {
cmd == "PAN" || cmd == "ZOOM" || cmd.starts_with("ZOOM ") || matches!(cmd, "ZW" | "ZE" | "ZA" | "ZP" | "ZI" | "ZO" | "ZD" | "ZEA" | "ZOBJ")
cmd == "PAN"
|| cmd == "ZOOM"
|| cmd.starts_with("ZOOM ")
|| matches!(
cmd,
"ZW" | "ZE" | "ZA" | "ZP" | "ZI" | "ZO" | "ZD" | "ZEA" | "ZOBJ"
)
}
/// ZOOM forms that install an interactive prompt (window corners, object
@@ -414,9 +422,10 @@ fn is_spacemouse_command(cmd: &str) -> bool {
/// source of truth: the dispatch gate refuses everything else, and the ribbon
/// dims the tools this rejects.
pub fn start_allowed(cmd: &str) -> bool {
is_spacemouse_command(cmd) || matches!(
cmd,
"NEW"
is_spacemouse_command(cmd)
|| matches!(
cmd,
"NEW"
| "OPEN"
| "EXIT"
| "QUIT"
@@ -437,7 +446,7 @@ pub fn start_allowed(cmd: &str) -> bool {
// command that opens the same dialog belongs here too.
| "OPTIONS"
| "OP"
)
)
}
// ── Autocomplete registry — one-shot commands ──────────────────────────────
@@ -971,10 +980,7 @@ mod marquee_cancel_tests {
sel.input.left_press_time = Some(Instant::now());
sel.input.left_dragging = true;
sel.gesture.poly_active = true;
sel.gesture.poly_points = vec![
iced::Point::new(10.0, 10.0),
iced::Point::new(20.0, 30.0),
];
sel.gesture.poly_points = vec![iced::Point::new(10.0, 10.0), iced::Point::new(20.0, 30.0)];
sel.gesture.poly_crossing = true;
}
@@ -1058,11 +1064,8 @@ mod marquee_cancel_tests {
let mut app = fresh();
let i = app.active_tab;
let handle = Handle::new(42);
app.tabs[i].active_grip = Some(GripEdit::radius(
handle,
1,
glam::DVec3::new(2.0, 0.0, 0.0),
));
app.tabs[i].active_grip =
Some(GripEdit::radius(handle, 1, glam::DVec3::new(2.0, 0.0, 0.0)));
app.grip_pending = Some(GripPendingValue {
handle,
grip_id: 1,
+115 -34
View File
@@ -5,10 +5,15 @@
use super::*;
use codec::entities::{ExtendedEntityData, PointCloudExData};
use codec::objects::{ClassObject, ClassObjectData, Dictionary, ObjectType, PointCloudColorMap, PointCloudColorRamp, PointCloudRampColor};
use codec::objects::{
ClassObject, ClassObjectData, Dictionary, ObjectType, PointCloudColorMap, PointCloudColorRamp,
PointCloudRampColor,
};
use codec::types::Handle;
use crate::ui::window::pdf_dialogs::{short, MapRamp, MapSlot, OutOfRange, PdfDialogMsg, PointCloudColorMapState};
use crate::ui::window::pdf_dialogs::{
short, MapRamp, MapSlot, OutOfRange, PdfDialogMsg, PointCloudColorMapState,
};
const MAP_DICTIONARY: &str = "ACAD_POINTCLOUD_COLORMAP_DICT";
const MAP_ENTRY: &str = "ACAD_POINTCLOUD_COLORMAP";
@@ -28,10 +33,17 @@ fn cloud_data(document: &codec::CadDocument, handle: Handle) -> Option<&PointClo
/// The drawing's colour map object, created (with the built-in schemes)
/// under its dictionary when the drawing has none.
fn ensure_color_map(document: &mut codec::CadDocument) -> Option<Handle> {
let existing = document.objects.iter().find_map(|(handle, object)| match object {
ObjectType::ClassObject(object) if matches!(object.data, ClassObjectData::PointCloudColorMap(_)) => Some(*handle),
_ => None,
});
let existing = document
.objects
.iter()
.find_map(|(handle, object)| match object {
ObjectType::ClassObject(object)
if matches!(object.data, ClassObjectData::PointCloudColorMap(_)) =>
{
Some(*handle)
}
_ => None,
});
if existing.is_some() {
return existing;
}
@@ -40,13 +52,22 @@ fn ensure_color_map(document: &mut codec::CadDocument) -> Option<Handle> {
return None;
};
let dictionary = match root_dictionary.get(MAP_DICTIONARY) {
Some(handle) if matches!(document.objects.get(&handle), Some(ObjectType::Dictionary(_))) => handle,
Some(handle)
if matches!(
document.objects.get(&handle),
Some(ObjectType::Dictionary(_))
) =>
{
handle
}
_ => {
let handle = document.allocate_handle();
let mut dictionary = Dictionary::new();
dictionary.handle = handle;
dictionary.owner = root;
document.objects.insert(handle, ObjectType::Dictionary(dictionary));
document
.objects
.insert(handle, ObjectType::Dictionary(dictionary));
if let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get_mut(&root) {
root_dictionary.add_entry(MAP_DICTIONARY, handle);
}
@@ -60,15 +81,19 @@ fn ensure_color_map(document: &mut codec::CadDocument) -> Option<Handle> {
.collect();
let mut map = ClassObject::new(ClassObjectData::PointCloudColorMap(PointCloudColorMap {
class_version: 4,
default_intensity_scheme: crate::entities::extended::point_cloud_ramp_id("Spectrum").to_string(),
default_elevation_scheme: crate::entities::extended::point_cloud_ramp_id("Earth").to_string(),
default_intensity_scheme: crate::entities::extended::point_cloud_ramp_id("Spectrum")
.to_string(),
default_elevation_scheme: crate::entities::extended::point_cloud_ramp_id("Earth")
.to_string(),
default_classification_scheme: CLASSIFICATION_SCHEME.to_string(),
color_ramps: ramps,
classification_color_ramps: Vec::new(),
}));
map.handle = handle;
map.owner = dictionary;
document.objects.insert(handle, ObjectType::ClassObject(map));
document
.objects
.insert(handle, ObjectType::ClassObject(map));
if let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&dictionary) {
dictionary.add_entry(MAP_ENTRY, handle);
}
@@ -84,7 +109,8 @@ fn to_codec_ramp(ramp: &MapRamp) -> PointCloudColorRamp {
.colors
.iter()
.map(|[r, g, b]| PointCloudRampColor {
color: (0xC2u32 << 24 | u32::from(*r) << 16 | u32::from(*g) << 8 | u32::from(*b)) as i32,
color: (0xC2u32 << 24 | u32::from(*r) << 16 | u32::from(*g) << 8 | u32::from(*b))
as i32,
visible: true,
})
.collect(),
@@ -97,7 +123,14 @@ fn new_scheme_id() -> String {
let mut bytes = [0u8; 16];
getrandom::fill(&mut bytes).expect("OS random source");
let hex: String = bytes.iter().map(|b| format!("{b:02X}")).collect();
format!("{}-{}-{}-{}-{}", &hex[0..8], &hex[8..12], &hex[12..16], &hex[16..20], &hex[20..32])
format!(
"{}-{}-{}-{}-{}",
&hex[0..8],
&hex[8..12],
&hex[12..16],
&hex[16..20],
&hex[20..32]
)
}
/// `count` colours taken evenly along a scheme's blend.
@@ -127,13 +160,20 @@ impl OpenCADStudio {
(low, high)
})
.unwrap_or((0.0, 0.0));
let slot = |scheme: &str, fallback: &str, gradient: bool, max: String, min: String, out: i16| MapSlot {
scheme: if scheme.is_empty() { fallback.to_string() } else { scheme.to_string() },
gradient,
max,
min,
out_of_range: OutOfRange(out),
};
let slot =
|scheme: &str, fallback: &str, gradient: bool, max: String, min: String, out: i16| {
MapSlot {
scheme: if scheme.is_empty() {
fallback.to_string()
} else {
scheme.to_string()
},
gradient,
max,
min,
out_of_range: OutOfRange(out),
}
};
let (intensity, elevation, extents, elevation_tab) = match data {
Some(data) => (
slot(
@@ -148,8 +188,16 @@ impl OpenCADStudio {
&data.current_color_scheme,
&defaults.1,
data.elevation_as_gradient,
short(if data.elevation_apply_to_fixed_range { data.elevation_max } else { height.1 }),
short(if data.elevation_apply_to_fixed_range { data.elevation_min } else { height.0 }),
short(if data.elevation_apply_to_fixed_range {
data.elevation_max
} else {
height.1
}),
short(if data.elevation_apply_to_fixed_range {
data.elevation_min
} else {
height.0
}),
data.elevation_out_of_range_behavior,
),
!data.elevation_apply_to_fixed_range,
@@ -179,7 +227,10 @@ impl OpenCADStudio {
self.active_modal = Some(crate::app::ModalKind::PointCloudColorMap);
}
pub(in crate::app) fn update_point_cloud_color_map(&mut self, message: PdfDialogMsg) -> Task<Message> {
pub(in crate::app) fn update_point_cloud_color_map(
&mut self,
message: PdfDialogMsg,
) -> Task<Message> {
let i = self.active_tab;
let Some(state) = self.point_cloud_color_map.as_mut() else {
return Task::none();
@@ -219,7 +270,12 @@ impl OpenCADStudio {
// "Color Scheme 0", or the first number not taken.
let name = (0..)
.map(|n| format!("Color Scheme {n}"))
.find(|name| !state.ramps.iter().any(|r| r.name.eq_ignore_ascii_case(name)))
.find(|name| {
!state
.ramps
.iter()
.any(|r| r.name.eq_ignore_ascii_case(name))
})
.unwrap_or_default();
state.naming = Some((true, name));
}
@@ -238,17 +294,31 @@ impl OpenCADStudio {
return Task::none();
};
let name = name.trim().to_string();
let taken = state.ramps.iter().any(|r| r.name.eq_ignore_ascii_case(&name))
let taken = state
.ramps
.iter()
.any(|r| r.name.eq_ignore_ascii_case(&name))
&& !(!new && state.ramp().is_some_and(|r| r.name == name));
if name.is_empty() || taken {
self.command_line.push_error(crate::t!("A color scheme with this name already exists.").as_ref());
self.point_cloud_color_map.as_mut().map(|s| s.naming = Some((new, name)));
self.command_line.push_error(
crate::t!("A color scheme with this name already exists.").as_ref(),
);
self.point_cloud_color_map
.as_mut()
.map(|s| s.naming = Some((new, name)));
return Task::none();
}
if new {
let colors = state.ramp().map(|r| r.colors.clone()).unwrap_or_else(|| vec![[255; 3], [0; 3]]);
let colors = state
.ramp()
.map(|r| r.colors.clone())
.unwrap_or_else(|| vec![[255; 3], [0; 3]]);
let id = new_scheme_id();
state.ramps.push(MapRamp { id: id.clone(), name, colors });
state.ramps.push(MapRamp {
id: id.clone(),
name,
colors,
});
state.slot_mut().scheme = id;
} else if let Some(ramp) = state.ramp_mut() {
ramp.name = name;
@@ -279,7 +349,10 @@ impl OpenCADStudio {
// A band height sets the maximum from the minimum.
PdfDialogMsg::MapInterval(value) => {
let count = state.ramp().map_or(1, |r| r.colors.len()) as f64;
if let (Ok(step), Ok(min)) = (value.trim().parse::<f64>(), state.elevation.min.trim().parse::<f64>()) {
if let (Ok(step), Ok(min)) = (
value.trim().parse::<f64>(),
state.elevation.min.trim().parse::<f64>(),
) {
state.elevation.max = short(min + step * count);
}
state.interval = value;
@@ -314,20 +387,28 @@ impl OpenCADStudio {
let Some(state) = self.point_cloud_color_map.as_ref() else {
return false;
};
let intensity = (state.intensity.min.trim().parse::<i32>(), state.intensity.max.trim().parse::<i32>());
let elevation = (state.elevation.min.trim().parse::<f64>(), state.elevation.max.trim().parse::<f64>());
let intensity = (
state.intensity.min.trim().parse::<i32>(),
state.intensity.max.trim().parse::<i32>(),
);
let elevation = (
state.elevation.min.trim().parse::<f64>(),
state.elevation.max.trim().parse::<f64>(),
);
let cloud = state.cloud;
if cloud.is_some() {
let ok_intensity = matches!(intensity, (Ok(lo), Ok(hi)) if (0..=100).contains(&lo) && (0..=100).contains(&hi) && lo < hi);
let ok_elevation = state.extents || matches!(elevation, (Ok(lo), Ok(hi)) if lo < hi);
if !ok_intensity || !ok_elevation {
self.command_line.push_error(crate::t!("Invalid range of colorized points.").as_ref());
self.command_line
.push_error(crate::t!("Invalid range of colorized points.").as_ref());
return false;
}
}
let ramps: Vec<PointCloudColorRamp> = state.ramps.iter().map(to_codec_ramp).collect();
let (intensity_slot, elevation_slot) = (state.intensity.clone(), state.elevation.clone());
let (extents, make_current, elevation_tab) = (state.extents, state.make_current, state.elevation_tab);
let (extents, make_current, elevation_tab) =
(state.extents, state.make_current, state.elevation_tab);
self.push_undo_snapshot(i, "POINTCLOUDCOLORMAP");
let document = &mut self.tabs[i].scene.document;
if let Some(handle) = ensure_color_map(document) {
+48 -10
View File
@@ -30,7 +30,11 @@ impl OpenCADStudio {
let common = entity.common();
// The colour the renderer places with, so its cached points are reused.
let color = if matches!(common.color, codec::types::Color::ByLayer) {
document.layers.get(&common.layer).map(|layer| layer.color.clone()).unwrap_or(common.color.clone())
document
.layers
.get(&common.layer)
.map(|layer| layer.color.clone())
.unwrap_or(common.color.clone())
} else {
common.color.clone()
};
@@ -65,7 +69,12 @@ impl OpenCADStudio {
let vertical = dvec(data.vertical_direction).normalize_or(DVec3::Z);
let viewing = vertical.cross(tangent).normalize_or(-DVec3::Z)
* if data.flags & 4 != 0 { 1.0 } else { -1.0 };
Some(Section { origin: first, viewing, tangent, live: data.flags & 1 != 0 })
Some(Section {
origin: first,
viewing,
tangent,
live: data.flags & 1 != 0,
})
})
.collect()
}
@@ -104,7 +113,13 @@ impl OpenCADStudio {
let s = settings();
let current = crate::t!("Use Current").into_owned();
let mut layers = vec![current.clone()];
let mut names: Vec<String> = self.tabs[i].scene.document.layers.iter().map(|l| l.name.clone()).collect();
let mut names: Vec<String> = self.tabs[i]
.scene
.document
.layers
.iter()
.map(|l| l.name.clone())
.collect();
names.sort_by_key(|n| n.to_lowercase());
layers.extend(names);
let short = |v: f64| {
@@ -128,8 +143,10 @@ impl OpenCADStudio {
}
pub(in crate::app) fn update_pc_section(&mut self, message: PdfDialogMsg) -> Task<Message> {
use crate::modules::insert::pc_extract::{set_settings, settings, SectionCommand, SectionDistanceCommand};
use crate::command::CadCommand;
use crate::modules::insert::pc_extract::{
set_settings, settings, SectionCommand, SectionDistanceCommand,
};
let i = self.active_tab;
let Some(state) = self.pc_section.as_mut() else {
return Task::none();
@@ -155,18 +172,39 @@ impl OpenCADStudio {
}
PdfDialogMsg::SecCreate => {
let number = |t: &str| crate::entities::common::parse_f64(t.trim());
let (min_length, connect, width) = (number(&state.min_length), number(&state.connect), number(&state.width));
let (Some(min_length), Some(connect)) = (min_length.filter(|v| *v > 0.0), connect.filter(|v| *v > 0.0)) else {
self.command_line.push_error(crate::t!("Requires a positive number.").as_ref());
let (min_length, connect, width) = (
number(&state.min_length),
number(&state.connect),
number(&state.width),
);
let (Some(min_length), Some(connect)) = (
min_length.filter(|v| *v > 0.0),
connect.filter(|v| *v > 0.0),
) else {
self.command_line
.push_error(crate::t!("Requires a positive number.").as_ref());
return Task::none();
};
let Some(angle) = state.angle.trim().parse::<i64>().ok().filter(|v| (0..=10).contains(v)) else {
let Some(angle) = state
.angle
.trim()
.parse::<i64>()
.ok()
.filter(|v| (0..=10).contains(v))
else {
self.command_line
.push_error("Colinear angle tolerance only accepts integer from 0 to 10.");
return Task::none();
};
let Some(max_points) = state.max_points.trim().parse::<usize>().ok().filter(|v| *v > 0) else {
self.command_line.push_error(crate::t!("Requires a positive integer.").as_ref());
let Some(max_points) = state
.max_points
.trim()
.parse::<usize>()
.ok()
.filter(|v| *v > 0)
else {
self.command_line
.push_error(crate::t!("Requires a positive integer.").as_ref());
return Task::none();
};
let current = crate::t!("Use Current").into_owned();
+121 -31
View File
@@ -5,8 +5,8 @@ use super::*;
use crate::io::xref_model::Pathtype;
use crate::modules::insert::pdf_import::{self, PdfFileImport};
use crate::ui::window::pdf_dialogs::{
click_page, item_thumbs, page_thumbs, LayerTarget, PdfAttachState, PdfDialogMsg, PdfImportFileState,
PointCloudAttachState, RotationChoice, UnderlayLayersState,
click_page, item_thumbs, page_thumbs, LayerTarget, PdfAttachState, PdfDialogMsg,
PdfImportFileState, PointCloudAttachState, RotationChoice, UnderlayLayersState,
};
/// "7.8740 × 3.9369" (inches) for a page.
@@ -22,7 +22,13 @@ fn item_size_text(kind: codec::entities::UnderlayType, path: &str, item: &str) -
return page_size_text(path, item);
}
crate::scene::model::underlay_vector::sheet(kind, path, item)
.map(|s| format!("{:.4} × {:.4}", s.rect[2] - s.rect[0], s.rect[3] - s.rect[1]))
.map(|s| {
format!(
"{:.4} × {:.4}",
s.rect[2] - s.rect[0],
s.rect[3] - s.rect[1]
)
})
.unwrap_or_default()
}
@@ -66,7 +72,11 @@ impl OpenCADStudio {
}
/// PDFATTACH / DWFATTACH / DGNATTACH after the file is chosen.
pub(in crate::app) fn open_underlay_attach_dialog(&mut self, kind: codec::entities::UnderlayType, path: &str) {
pub(in crate::app) fn open_underlay_attach_dialog(
&mut self,
kind: codec::entities::UnderlayType,
path: &str,
) {
let i = self.active_tab;
let mut existing: Vec<(String, String)> = Vec::new();
for object in self.tabs[i].scene.document.objects.values() {
@@ -94,7 +104,11 @@ impl OpenCADStudio {
state.selected = vec![0];
state.anchor = 0;
state.found_in = crate::entities::underlay::display_path(path);
let first = state.page_labels().into_iter().next().unwrap_or_else(|| "1".to_string());
let first = state
.page_labels()
.into_iter()
.next()
.unwrap_or_else(|| "1".to_string());
state.page_size = item_size_text(state.kind, path, &first);
state.sub_per_master = sub_per_master(state.kind, path, &first);
state.sub_units = false;
@@ -108,7 +122,14 @@ impl OpenCADStudio {
let Some(cloud) = crate::scene::model::point_cloud::load(path) else {
return;
};
let yes_no = |on: bool| if on { crate::t!("Yes") } else { crate::t!("No") }.into_owned();
let yes_no = |on: bool| {
if on {
crate::t!("Yes")
} else {
crate::t!("No")
}
.into_owned()
};
let data = format!(
"RGB: {} · {}: {} · {}: {}",
yes_no(cloud.has_rgb),
@@ -124,14 +145,28 @@ impl OpenCADStudio {
crate::t!("Segmentation"),
yes_no(false),
);
let factor = crate::app::properties::insert_unit_scale(self.tabs[self.active_tab].scene.document.header.insertion_units, 6)
.unwrap_or(1.0);
let factor = crate::app::properties::insert_unit_scale(
self.tabs[self.active_tab]
.scene
.document
.header
.insertion_units,
6,
)
.unwrap_or(1.0);
let full = path.to_string_lossy().into_owned();
let memory = crate::ui::window::pdf_dialogs::ATTACH_MEMORY.lock().ok().and_then(|m| m.clone()).unwrap_or_default();
let memory = crate::ui::window::pdf_dialogs::ATTACH_MEMORY
.lock()
.ok()
.and_then(|m| m.clone())
.unwrap_or_default();
let mut state = PointCloudAttachState {
path: full.clone(),
name: file_stem(&full),
preview: cloud.preview.clone().map(iced::widget::image::Handle::from_bytes),
preview: cloud
.preview
.clone()
.map(iced::widget::image::Handle::from_bytes),
summary: format!(
"{} · {}",
crate::tf!("{count} scans", count = cloud.scans),
@@ -141,9 +176,21 @@ impl OpenCADStudio {
data_more,
size: cloud
.bounds
.map(|[lo, hi]| format!("{:.4} × {:.4} × {:.4}", hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]))
.map(|[lo, hi]| {
format!(
"{:.4} × {:.4} × {:.4}",
hi[0] - lo[0],
hi[1] - lo[1],
hi[2] - lo[2]
)
})
.unwrap_or_default(),
unit: format!("{} · {} {}", crate::t!("Meters"), crate::t!("Unit factor"), crate::app::properties::format_unit_factor(factor)),
unit: format!(
"{} · {} {}",
crate::t!("Meters"),
crate::t!("Unit factor"),
crate::app::properties::format_unit_factor(factor)
),
found_in: crate::entities::underlay::display_path(&full),
saved_path: String::new(),
path_type: memory.path_type,
@@ -156,7 +203,9 @@ impl OpenCADStudio {
lock: false,
zoom: true,
};
state.saved_path = crate::entities::underlay::display_path(&self.pdf_stored_path(&full, state.path_type.0));
state.saved_path = crate::entities::underlay::display_path(
&self.pdf_stored_path(&full, state.path_type.0),
);
self.point_cloud_attach = Some(state);
self.active_modal = Some(crate::app::ModalKind::PointCloudAttach);
}
@@ -172,7 +221,11 @@ impl OpenCADStudio {
let insertion = if state.insert_on_screen {
None
} else {
match (number(&state.insert[0]), number(&state.insert[1]), number(&state.insert[2])) {
match (
number(&state.insert[0]),
number(&state.insert[1]),
number(&state.insert[2]),
) {
(Some(x), Some(y), Some(z)) => Some(glam::DVec3::new(x, y, z)),
_ => {
self.command_line.push_error("Requires numeric value.");
@@ -186,7 +239,8 @@ impl OpenCADStudio {
match number(&state.scale).filter(|v| *v > 0.0) {
Some(v) => Some(v),
None => {
self.command_line.push_error("Value must be positive and nonzero.");
self.command_line
.push_error("Value must be positive and nonzero.");
return Task::none();
}
}
@@ -258,10 +312,12 @@ impl OpenCADStudio {
name = format!("{name} ({})", same + 1);
}
let mut layers: Vec<String> = match u.underlay_type {
codec::entities::UnderlayType::Pdf => crate::scene::model::pdf_layers::layers(&def.file_path)
.into_iter()
.map(|l| l.name)
.collect(),
codec::entities::UnderlayType::Pdf => {
crate::scene::model::pdf_layers::layers(&def.file_path)
.into_iter()
.map(|l| l.name)
.collect()
}
kind => crate::scene::model::underlay_vector::layer_names(kind, &def.file_path),
};
layers.sort_by_key(|n| n.to_lowercase());
@@ -313,7 +369,9 @@ impl OpenCADStudio {
/// Writes the layers each underlay turns off, as one undo step.
fn apply_underlay_layers(&mut self, i: usize, targets: Vec<LayerTarget>) {
use crate::scene::model::pdf_layers::{hidden_layers, DGN_OVERRIDE_PLACEHOLDER, LAYER_OVERRIDE_APP};
use crate::scene::model::pdf_layers::{
hidden_layers, DGN_OVERRIDE_PLACEHOLDER, LAYER_OVERRIDE_APP,
};
let changed: Vec<LayerTarget> = targets
.into_iter()
.filter(|t| match self.tabs[i].scene.document.get_entity(t.handle) {
@@ -345,7 +403,9 @@ impl OpenCADStudio {
let mut record = codec::xdata::ExtendedDataRecord::new(LAYER_OVERRIDE_APP);
// A DGN underlay's names follow a placeholder string.
if u.underlay_type == codec::entities::UnderlayType::Dgn {
record.add_value(codec::xdata::XDataValue::String(DGN_OVERRIDE_PLACEHOLDER.to_string()));
record.add_value(codec::xdata::XDataValue::String(
DGN_OVERRIDE_PLACEHOLDER.to_string(),
));
}
for name in &target.hidden {
record.add_value(codec::xdata::XDataValue::String(name.clone()));
@@ -394,7 +454,11 @@ impl OpenCADStudio {
// ── Attach ────────────────────────────────────────────────────
PdfDialogMsg::AttachBrowse => {
let kind = self.pdf_attach.as_ref().map(|s| s.kind).unwrap_or(codec::entities::UnderlayType::Pdf);
let kind = self
.pdf_attach
.as_ref()
.map(|s| s.kind)
.unwrap_or(codec::entities::UnderlayType::Pdf);
if let Some(state) = &self.pdf_attach {
state.remember();
}
@@ -407,7 +471,11 @@ impl OpenCADStudio {
if let Some(s) = self.pdf_attach.as_mut() {
s.sub_units = sub;
// The scale offered follows the units, as on the command line.
s.scale = if sub { format!("{}", 1.0 / s.sub_per_master) } else { "1.0000".to_string() };
s.scale = if sub {
format!("{}", 1.0 / s.sub_per_master)
} else {
"1.0000".to_string()
};
}
}
PdfDialogMsg::AttachName(name) => {
@@ -517,7 +585,10 @@ impl OpenCADStudio {
| PdfDialogMsg::SecPreview(_)
| PdfDialogMsg::SecCreate) => return self.update_pc_section(message),
PdfDialogMsg::CloudPathType(choice) => {
let stored = self.point_cloud_attach.as_ref().map(|s| self.pdf_stored_path(&s.path, choice.0));
let stored = self
.point_cloud_attach
.as_ref()
.map(|s| self.pdf_stored_path(&s.path, choice.0));
if let (Some(s), Some(stored)) = (self.point_cloud_attach.as_mut(), stored) {
s.path_type = choice;
s.saved_path = crate::entities::underlay::display_path(&stored);
@@ -648,7 +719,12 @@ impl OpenCADStudio {
}
PdfDialogMsg::ImportPageText(text) => {
if let Some(s) = self.pdf_import_file.as_mut() {
if let Some(n) = text.trim().parse::<usize>().ok().filter(|n| (1..=s.pages.len()).contains(n)) {
if let Some(n) = text
.trim()
.parse::<usize>()
.ok()
.filter(|n| (1..=s.pages.len()).contains(n))
{
s.selected = n - 1;
s.page_size = page_size_mm(&s.path, &n.to_string());
}
@@ -674,8 +750,10 @@ impl OpenCADStudio {
let Some(state) = self.pdf_import_file.as_ref() else {
return Task::none();
};
let Some(scale) = state.scale.trim().parse::<f64>().ok().filter(|v| *v > 0.0) else {
self.command_line.push_error("Value must be positive and nonzero.");
let Some(scale) = state.scale.trim().parse::<f64>().ok().filter(|v| *v > 0.0)
else {
self.command_line
.push_error("Value must be positive and nonzero.");
return Task::none();
};
let state = self.pdf_import_file.take().expect("checked above");
@@ -695,7 +773,10 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(command));
return self.focus_cmd_input();
}
self.run_pdf_import(i, crate::app::commands::pdf_import::PdfImportSource::File(import));
self.run_pdf_import(
i,
crate::app::commands::pdf_import::PdfImportSource::File(import),
);
}
}
Task::none()
@@ -710,7 +791,11 @@ impl OpenCADStudio {
let insertion = if state.insert_on_screen {
None
} else {
match (number(&state.insert[0]), number(&state.insert[1]), number(&state.insert[2])) {
match (
number(&state.insert[0]),
number(&state.insert[1]),
number(&state.insert[2]),
) {
(Some(x), Some(y), Some(z)) => Some(glam::DVec3::new(x, y, z)),
_ => {
self.command_line.push_error("Requires numeric value.");
@@ -724,7 +809,8 @@ impl OpenCADStudio {
match number(&state.scale).filter(|v| *v > 0.0) {
Some(v) => Some(v),
None => {
self.command_line.push_error("Value must be positive and nonzero.");
self.command_line
.push_error("Value must be positive and nonzero.");
return Task::none();
}
}
@@ -766,7 +852,11 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(command));
if matches!(result, crate::command::CmdResult::NeedPoint) {
self.command_line.push_info(
&self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()).unwrap_or_default(),
&self.tabs[i]
.active_cmd
.as_ref()
.map(|c| c.prompt())
.unwrap_or_default(),
);
return self.focus_cmd_input();
}
+19 -8
View File
@@ -86,7 +86,12 @@ impl OpenCADStudio {
self.push_undo_snapshot(i, "PDFIMPORT");
let scene = &mut self.tabs[i].scene;
if result.dashed && !scene.document.line_types.contains(pdf_import::DASH_LINETYPE) {
if result.dashed
&& !scene
.document
.line_types
.contains(pdf_import::DASH_LINETYPE)
{
let mut lt = codec::tables::LineType::new(pdf_import::DASH_LINETYPE);
lt.handle = scene.document.allocate_handle();
lt.add_element(codec::tables::LineTypeElement::dash(0.8));
@@ -106,8 +111,14 @@ impl OpenCADStudio {
break;
}
let file = dir.join(format!("{stem}{:08x}.png", image_file_id(&path, &page, n)));
if image::save_buffer(&file, &image.rgba, image.width, image.height, image::ColorType::Rgba8)
.is_err()
if image::save_buffer(
&file,
&image.rgba,
image.width,
image.height,
image::ColorType::Rgba8,
)
.is_err()
{
continue;
}
@@ -163,10 +174,8 @@ impl OpenCADStudio {
.define_block_from_owned_entities(result.entities, &name, glam::DVec3::ZERO)
.is_ok()
{
let mut insert = codec::entities::Insert::new(
name,
codec::types::Vector3::new(0.0, 0.0, 0.0),
);
let mut insert =
codec::entities::Insert::new(name, codec::types::Vector3::new(0.0, 0.0, 0.0));
insert.common.layer = current_layer;
scene.add_entity(codec::EntityType::Insert(insert));
}
@@ -198,7 +207,9 @@ impl OpenCADStudio {
scene.document.objects.remove(&definition);
for object in scene.document.objects.values_mut() {
if let ObjectType::Dictionary(dictionary) = object {
dictionary.entries.retain(|(_, target)| *target != definition);
dictionary
.entries
.retain(|(_, target)| *target != definition);
}
}
}
+117 -42
View File
@@ -2,8 +2,8 @@
//! selection, their buttons edit it.
use super::*;
use codec::entities::{Underlay, UnderlayDisplayFlags};
use crate::ui::ribbon::UnderlayContext;
use codec::entities::{Underlay, UnderlayDisplayFlags};
impl OpenCADStudio {
/// The selected underlays when they are all of one kind, or nothing
@@ -13,7 +13,9 @@ impl OpenCADStudio {
for (handle, entity) in self.tabs[i].scene.selected_entities() {
match entity {
codec::EntityType::Underlay(u)
if out.first().is_none_or(|(_, first)| first.underlay_type == u.underlay_type) =>
if out
.first()
.is_none_or(|(_, first)| first.underlay_type == u.underlay_type) =>
{
out.push((handle, u.clone()));
}
@@ -52,27 +54,37 @@ impl OpenCADStudio {
let context = if self.tabs[i].is_start {
None
} else {
self.selected_pdf_underlays(i).first().map(|(_, u)| {
UnderlayContext {
self.selected_pdf_underlays(i)
.first()
.map(|(_, u)| UnderlayContext {
kind: u.underlay_type,
monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME),
shown: u.flags.contains(UnderlayDisplayFlags::ON),
snap: crate::scene::model::pdf_vector::underlay_osnap(u.underlay_type),
}
})
})
};
self.ribbon.set_underlay_context(context, xref);
let clouds = if self.tabs[i].is_start { Vec::new() } else { self.selected_point_clouds(i) };
self.ribbon.set_point_cloud_context(clouds.first().map(|(_, data)| data.show_cropping));
let clouds = if self.tabs[i].is_start {
Vec::new()
} else {
self.selected_point_clouds(i)
};
self.ribbon
.set_point_cloud_context(clouds.first().map(|(_, data)| data.show_cropping));
}
/// The selected point clouds, or nothing when anything else is selected.
fn selected_point_clouds(&self, i: usize) -> Vec<(codec::Handle, codec::entities::PointCloudExData)> {
fn selected_point_clouds(
&self,
i: usize,
) -> Vec<(codec::Handle, codec::entities::PointCloudExData)> {
let mut out = Vec::new();
for (handle, entity) in self.tabs[i].scene.selected_entities() {
match entity {
codec::EntityType::Extended(extended) => match &extended.data {
codec::entities::ExtendedEntityData::PointCloudEx(data) => out.push((handle, data.clone())),
codec::entities::ExtendedEntityData::PointCloudEx(data) => {
out.push((handle, data.clone()))
}
_ => return Vec::new(),
},
_ => return Vec::new(),
@@ -118,18 +130,26 @@ impl OpenCADStudio {
if !done.is_empty() {
self.push_undo_snapshot(i, "POINTCLOUDSTYLIZE");
for handle in &done {
if let Some(codec::EntityType::Extended(extended)) = self.tabs[i].scene.document.get_entity_mut(*handle) {
if let codec::entities::ExtendedEntityData::PointCloudEx(data) = &mut extended.data {
if let Some(codec::EntityType::Extended(extended)) =
self.tabs[i].scene.document.get_entity_mut(*handle)
{
if let codec::entities::ExtendedEntityData::PointCloudEx(data) =
&mut extended.data
{
crate::entities::extended::set_point_cloud_stylization(data, stylization);
}
}
}
let changes: Vec<_> = done.iter().map(|h| (*h, crate::scene::ChangeKind::Modified)).collect();
let changes: Vec<_> = done
.iter()
.map(|h| (*h, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
self.refresh_properties();
}
self.command_line.push_output(&format!("{} point cloud(s) stylized", done.len()));
self.command_line
.push_output(&format!("{} point cloud(s) stylized", done.len()));
}
/// One undo step that edits every selected point cloud not locked.
@@ -150,13 +170,19 @@ impl OpenCADStudio {
}
self.push_undo_snapshot(i, label);
for handle in &handles {
if let Some(codec::EntityType::Extended(extended)) = self.tabs[i].scene.document.get_entity_mut(*handle) {
if let codec::entities::ExtendedEntityData::PointCloudEx(data) = &mut extended.data {
if let Some(codec::EntityType::Extended(extended)) =
self.tabs[i].scene.document.get_entity_mut(*handle)
{
if let codec::entities::ExtendedEntityData::PointCloudEx(data) = &mut extended.data
{
edit(data);
}
}
}
let changes: Vec<_> = handles.iter().map(|h| (*h, crate::scene::ChangeKind::Modified)).collect();
let changes: Vec<_> = handles
.iter()
.map(|h| (*h, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
self.refresh_properties();
@@ -164,14 +190,18 @@ impl OpenCADStudio {
/// A Point Cloud Manager click: a switch writes the cloud's hidden scans
/// and regions as one undo step; the rest is palette state.
pub(in crate::app) fn update_pc_manager(&mut self, message: crate::ui::window::pc_manager::PcManagerMsg) -> Task<Message> {
pub(in crate::app) fn update_pc_manager(
&mut self,
message: crate::ui::window::pc_manager::PcManagerMsg,
) -> Task<Message> {
use crate::scene::model::point_cloud::UNASSIGNED_OFF;
use crate::ui::window::pc_manager::{self, PcManagerMsg, Row};
let i = self.active_tab;
match message {
PcManagerMsg::Toggle(handle, row) => {
let Some(cloud) =
pc_manager::clouds(&self.tabs[i].scene.document, &[]).into_iter().find(|c| c.handle == handle)
let Some(cloud) = pc_manager::clouds(&self.tabs[i].scene.document, &[])
.into_iter()
.find(|c| c.handle == handle)
else {
return Task::none();
};
@@ -181,19 +211,34 @@ impl OpenCADStudio {
// A scan named by its file name (automation) is the scan with that name.
let row = match row {
Row::Scan(key) => Row::Scan(
cloud.scans.iter().find(|(name, _)| *name == key).map_or(key, |(_, id)| id.clone()),
cloud
.scans
.iter()
.find(|(name, _)| *name == key)
.map_or(key, |(_, id)| id.clone()),
),
row => row,
};
let hidden = pc_manager::toggled(&cloud.hidden, &cloud.scans, &row);
self.push_undo_snapshot(i, "POINTCLOUDMANAGER");
if let Some(codec::EntityType::Extended(extended)) = self.tabs[i].scene.document.get_entity_mut(handle) {
if let codec::entities::ExtendedEntityData::PointCloudEx(data) = &mut extended.data {
data.hidden_regions = if hidden.iter().any(|h| h == UNASSIGNED_OFF) { vec![0] } else { Vec::new() };
data.hidden_scans = hidden.into_iter().filter(|h| h != UNASSIGNED_OFF).collect();
if let Some(codec::EntityType::Extended(extended)) =
self.tabs[i].scene.document.get_entity_mut(handle)
{
if let codec::entities::ExtendedEntityData::PointCloudEx(data) =
&mut extended.data
{
data.hidden_regions = if hidden.iter().any(|h| h == UNASSIGNED_OFF) {
vec![0]
} else {
Vec::new()
};
data.hidden_scans =
hidden.into_iter().filter(|h| h != UNASSIGNED_OFF).collect();
}
}
self.tabs[i].scene.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
self.tabs[i]
.scene
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
self.tabs[i].dirty = true;
}
PcManagerMsg::Expand(key) => {
@@ -205,7 +250,9 @@ impl OpenCADStudio {
PcManagerMsg::Search(text) => self.pc_manager.search = text,
PcManagerMsg::CollapseAll => {
let clouds = pc_manager::clouds(&self.tabs[i].scene.document, &[]);
self.pc_manager.collapsed.extend(pc_manager::folding_keys(&clouds));
self.pc_manager
.collapsed
.extend(pc_manager::folding_keys(&clouds));
}
PcManagerMsg::ExpandAll => self.pc_manager.collapsed.clear(),
}
@@ -276,7 +323,11 @@ impl OpenCADStudio {
};
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.select_entity(handle, true);
let command = if cmd == "_XREFEDIT" { "REFEDIT" } else { "XOPEN" };
let command = if cmd == "_XREFEDIT" {
"REFEDIT"
} else {
"XOPEN"
};
return Some(self.dispatch_command(command));
}
"_XREFCLIP" => {
@@ -286,10 +337,13 @@ impl OpenCADStudio {
return Some(Task::none());
}
let clipped = inserts.iter().any(|h| {
crate::scene::pick::xclip::filter_handle(&self.tabs[i].scene.document, *h).is_some()
crate::scene::pick::xclip::filter_handle(&self.tabs[i].scene.document, *h)
.is_some()
});
let (command, first) =
crate::modules::insert::xclip::XclipCommand::start_new_boundary(inserts, clipped);
crate::modules::insert::xclip::XclipCommand::start_new_boundary(
inserts, clipped,
);
if let crate::command::CmdResult::ReportMeasurement(text) = first {
for line in text.lines() {
self.command_line.push_output(line);
@@ -301,7 +355,11 @@ impl OpenCADStudio {
"_XREFUNCLIP" => {
let inserts = self.selected_xrefs(i);
if !inserts.is_empty() {
self.apply_xclip(i, inserts, crate::modules::insert::xclip::XclipAction::Delete);
self.apply_xclip(
i,
inserts,
crate::modules::insert::xclip::XclipAction::Delete,
);
}
}
// Point cloud tools: a crop of the first selected cloud, or the
@@ -314,7 +372,9 @@ impl OpenCADStudio {
if self.tabs[i].scene.is_layer_locked(handle) {
return Some(Task::none());
}
let Some(codec::EntityType::Extended(cloud)) = self.tabs[i].scene.document.get_entity(handle).cloned() else {
let Some(codec::EntityType::Extended(cloud)) =
self.tabs[i].scene.document.get_entity(handle).cloned()
else {
return Some(Task::none());
};
let option = match cmd {
@@ -322,7 +382,9 @@ impl OpenCADStudio {
"_PCCROPCIRC" => "C",
_ => "",
};
let command = crate::modules::insert::pc_crop::PointCloudCropCommand::for_cloud(handle, *cloud, option);
let command = crate::modules::insert::pc_crop::PointCloudCropCommand::for_cloud(
handle, *cloud, option,
);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
@@ -349,20 +411,28 @@ impl OpenCADStudio {
if handles.is_empty() {
return Some(self.finish_dispatch(cmd));
}
let command = crate::modules::insert::pc_stylize::PointCloudStylizeCommand::new(handles);
let command =
crate::modules::insert::pc_stylize::PointCloudStylizeCommand::new(handles);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
c if c.starts_with("_PCSTYLIZEAPPLY ") => {
let mut parts = c.split_whitespace().skip(1);
let stylization: i16 = parts.next().and_then(|v| v.parse().ok()).unwrap_or(1);
let handles: Vec<codec::Handle> =
parts.filter_map(|h| u64::from_str_radix(h, 16).ok()).map(codec::Handle::new).collect();
let handles: Vec<codec::Handle> = parts
.filter_map(|h| u64::from_str_radix(h, 16).ok())
.map(codec::Handle::new)
.collect();
self.stylize_point_clouds(i, &handles, stylization);
}
"_PCCROPSHOW" => {
let on = !self.selected_point_clouds(i).first().is_some_and(|(_, data)| data.show_cropping);
self.edit_selected_point_clouds(i, "POINTCLOUDCROP", |data| data.show_cropping = on);
let on = !self
.selected_point_clouds(i)
.first()
.is_some_and(|(_, data)| data.show_cropping);
self.edit_selected_point_clouds(i, "POINTCLOUDCROP", |data| {
data.show_cropping = on
});
}
"_PCCROPINVERT" => self.edit_selected_point_clouds(i, "POINTCLOUDCROP", |data| {
for crop in &mut data.croppings {
@@ -375,7 +445,8 @@ impl OpenCADStudio {
let id = crate::ui::dock::PanelId::PointCloudManager;
self.pc_manager.show = true;
if self.dock.location(id).is_none() {
self.dock.dock(id, crate::app::config::DockSide::Right, usize::MAX);
self.dock
.dock(id, crate::app::config::DockSide::Right, usize::MAX);
}
self.dock_expanded = Some(id);
}
@@ -385,7 +456,8 @@ impl OpenCADStudio {
self.dock_expanded = None;
}
}
"_PCUNCROP" =>self.edit_selected_point_clouds(i, "POINTCLOUDUNCROP", |data| data.croppings.clear()),
"_PCUNCROP" => self
.edit_selected_point_clouds(i, "POINTCLOUDUNCROP", |data| data.croppings.clear()),
"_PDFULUNCLIP" => {
self.edit_selected_underlays(i, "PDFCLIP", |u| {
u.clip_boundary_vertices.clear();
@@ -400,7 +472,9 @@ impl OpenCADStudio {
return Some(Task::none());
};
let (command, first) =
crate::modules::insert::pdf_clip::PdfClipCommand::new_boundary(handle, underlay);
crate::modules::insert::pdf_clip::PdfClipCommand::new_boundary(
handle, underlay,
);
if let crate::command::CmdResult::ReportMeasurement(text) = first {
for line in text.lines() {
self.command_line.push_output(line);
@@ -414,7 +488,8 @@ impl OpenCADStudio {
let Some((handle, _)) = self.selected_pdf_underlays(i).into_iter().next() else {
return Some(Task::none());
};
let command = crate::modules::insert::pdf_import::PdfImportCommand::for_underlay(handle);
let command =
crate::modules::insert::pdf_import::PdfImportCommand::for_underlay(handle);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
+7 -4
View File
@@ -69,9 +69,8 @@ impl OpenCADStudio {
task
}
Ok(Outcome::Current(current)) => {
self.command_line.push_output(
crate::tf!("Enter new value for {name} <{current}>:").as_ref(),
);
self.command_line
.push_output(crate::tf!("Enter new value for {name} <{current}>:").as_ref());
self.pending_setvar = Some(name);
Task::none()
}
@@ -199,7 +198,11 @@ mod tests {
}
fn last_line(app: &OpenCADStudio) -> String {
app.command_line.history.last().map(|line| line.text.clone()).unwrap_or_default()
app.command_line
.history
.last()
.map(|line| line.text.clone())
.unwrap_or_default()
}
#[test]
+71 -29
View File
@@ -25,7 +25,10 @@ fn definitions_dictionary(document: &mut codec::CadDocument) -> Option<Handle> {
return None;
};
if let Some(existing) = root_dictionary.get(DEFINITIONS) {
if matches!(document.objects.get(&existing), Some(ObjectType::Dictionary(_))) {
if matches!(
document.objects.get(&existing),
Some(ObjectType::Dictionary(_))
) {
return Some(existing);
}
}
@@ -33,7 +36,9 @@ fn definitions_dictionary(document: &mut codec::CadDocument) -> Option<Handle> {
let mut dictionary = Dictionary::new();
dictionary.handle = handle;
dictionary.owner = root;
document.objects.insert(handle, ObjectType::Dictionary(dictionary));
document
.objects
.insert(handle, ObjectType::Dictionary(dictionary));
if let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get_mut(&root) {
root_dictionary.add_entry(DEFINITIONS, handle);
}
@@ -50,27 +55,41 @@ fn ensure_definition(
points: &crate::scene::model::point_cloud::CloudPoints,
) -> (Handle, Vector3, Vector3) {
let [lo, hi] = points.bounds.unwrap_or_default();
let (min, max) = (Vector3::new(lo[0], lo[1], lo[2]), Vector3::new(hi[0], hi[1], hi[2]));
let same = |stored: &str| stored.replace('/', "\\").eq_ignore_ascii_case(&path.replace('/', "\\"));
let existing = document.objects.iter().find_map(|(handle, object)| match object {
ObjectType::ClassObject(object) => match &object.data {
ClassObjectData::PointCloudDefinitionEx(def) if same(&def.source_filename) => Some(*handle),
let (min, max) = (
Vector3::new(lo[0], lo[1], lo[2]),
Vector3::new(hi[0], hi[1], hi[2]),
);
let same = |stored: &str| {
stored
.replace('/', "\\")
.eq_ignore_ascii_case(&path.replace('/', "\\"))
};
let existing = document
.objects
.iter()
.find_map(|(handle, object)| match object {
ObjectType::ClassObject(object) => match &object.data {
ClassObjectData::PointCloudDefinitionEx(def) if same(&def.source_filename) => {
Some(*handle)
}
_ => None,
},
_ => None,
},
_ => None,
});
});
if let Some(handle) = existing {
return (handle, min, max);
}
let handle = document.allocate_handle();
let mut definition = ClassObject::new(ClassObjectData::PointCloudDefinitionEx(PointCloudDefinition {
class_version: 1,
source_filename: path.to_string(),
is_loaded: true,
point_count: points.positions.len() as i64,
extents_min: min,
extents_max: max,
}));
let mut definition = ClassObject::new(ClassObjectData::PointCloudDefinitionEx(
PointCloudDefinition {
class_version: 1,
source_filename: path.to_string(),
is_loaded: true,
point_count: points.positions.len() as i64,
extents_min: min,
extents_max: max,
},
));
definition.handle = handle;
if let Some(dictionary) = definitions_dictionary(document) {
definition.owner = dictionary;
@@ -85,14 +104,22 @@ fn ensure_definition(
entries.add_entry(key, handle);
}
}
document.objects.insert(handle, ObjectType::ClassObject(definition));
document
.objects
.insert(handle, ObjectType::ClassObject(definition));
(handle, min, max)
}
impl OpenCADStudio {
pub(crate) fn attach_point_cloud(&mut self, i: usize, label: String, placement: PointCloudPlacement) {
pub(crate) fn attach_point_cloud(
&mut self,
i: usize,
label: String,
placement: PointCloudPlacement,
) {
let Some(points) = crate::scene::model::point_cloud::load(&placement.path) else {
self.command_line.push_error("All scans are not found or invalid.");
self.command_line
.push_error("All scans are not found or invalid.");
self.command_line.push_error("Attach point cloud failed");
return;
};
@@ -106,7 +133,12 @@ impl OpenCADStudio {
None if options.relative => host
.as_deref()
.and_then(|host| {
crate::io::xref_model::to_pathtype_result(&path, host, crate::io::xref_model::Pathtype::Relative).ok()
crate::io::xref_model::to_pathtype_result(
&path,
host,
crate::io::xref_model::Pathtype::Relative,
)
.ok()
})
.map(|relative| relative.replace('/', "\\"))
.unwrap_or_else(|| path.clone()),
@@ -125,10 +157,12 @@ impl OpenCADStudio {
let document = &mut self.tabs[i].scene.document;
// POINTCLOUDLOCK 1 attaches clouds locked.
let locked = options.locked.unwrap_or_else(|| {
crate::scene::model::point_cloud::setting("POINTCLOUDLOCK")
.is_some_and(|setting| crate::scene::model::point_cloud::setting_value(document, setting) == 1)
crate::scene::model::point_cloud::setting("POINTCLOUDLOCK").is_some_and(|setting| {
crate::scene::model::point_cloud::setting_value(document, setting) == 1
})
});
let (definition, extents_min, extents_max) = ensure_definition(document, &stored, &name, &points);
let (definition, extents_min, extents_max) =
ensure_definition(document, &stored, &name, &points);
let [x, y, z] = placement.axes;
// As the reference creates a cloud: scan colours, the intensity
// range 0–100 as a gradient, crops shown.
@@ -176,18 +210,26 @@ impl OpenCADStudio {
));
object.handle = reactor;
object.owner = cloud;
document.objects.insert(reactor, ObjectType::ClassObject(object));
if let Some(ObjectType::ClassObject(definition)) = document.objects.get_mut(&definition) {
document
.objects
.insert(reactor, ObjectType::ClassObject(object));
if let Some(ObjectType::ClassObject(definition)) = document.objects.get_mut(&definition)
{
definition.reactors.push(reactor);
}
if let Some(codec::EntityType::Extended(extended)) = document.get_entity_mut(cloud) {
if let codec::entities::ExtendedEntityData::PointCloudEx(data) = &mut extended.data {
if let codec::entities::ExtendedEntityData::PointCloudEx(data) = &mut extended.data
{
data.reactor_handle = reactor;
}
}
self.command_line.push_output("1 point cloud attached");
if relative_later {
self.tabs[i].xref_relative_on_save.insert(format!("{}{:X}", crate::io::xref::POINT_CLOUD_KEY, definition.value()));
self.tabs[i].xref_relative_on_save.insert(format!(
"{}{:X}",
crate::io::xref::POINT_CLOUD_KEY,
definition.value()
));
}
if options.zoom {
self.tabs[i].scene.zoom_to_entities(&[cloud]);
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+1071 -448
View File
File diff suppressed because it is too large. Load diff
+12 -4
View File
@@ -38,11 +38,17 @@ impl OpenCADStudio {
action: XclipAction,
) {
if let XclipAction::Depth { rejected: true, .. } = action {
self.command_line.push_error(crate::modules::insert::xclip::DEPTH_REJECTED);
self.command_line
.push_error(crate::modules::insert::xclip::DEPTH_REJECTED);
}
self.push_undo_snapshot(i, "XCLIP");
let extents: Vec<_> = inserts.iter().map(|h| self.insert_extents(i, *h)).collect();
let current_layer = self.tabs[i].scene.document.header.current_layer_name.clone();
let current_layer = self.tabs[i]
.scene
.document
.header
.current_layer_name
.clone();
let scene = &mut self.tabs[i].scene;
let mut polylines = Vec::new();
for (n, insert) in inserts.iter().enumerate() {
@@ -64,8 +70,10 @@ impl OpenCADStudio {
let Some(spatial) = clip::filter_handle(doc, *insert) else {
continue;
};
let (Some(codec::objects::ObjectType::SpatialFilter(filter)), Some(codec::EntityType::Insert(ins))) =
(doc.objects.get(&spatial), doc.get_entity(*insert))
let (
Some(codec::objects::ObjectType::SpatialFilter(filter)),
Some(codec::EntityType::Insert(ins)),
) = (doc.objects.get(&spatial), doc.get_entity(*insert))
else {
continue;
};
+46 -30
View File
@@ -5,8 +5,8 @@
use super::*;
use crate::io::xref_model::Pathtype;
use crate::modules::insert::xattach::{
existing_reference, path_to_block_name, prepare_xref_definition, stored_path,
XAttachCommand, XrefAttachRequest, XrefPlacement,
existing_reference, path_to_block_name, prepare_xref_definition, stored_path, XAttachCommand,
XrefAttachRequest, XrefPlacement,
};
use crate::modules::insert::xref_cmd::display_path;
use crate::ui::window::xref_attach::{XrefAttachMsg, XrefAttachState};
@@ -22,9 +22,12 @@ impl OpenCADStudio {
) {
use crate::command::CadCommand;
let insunits = self.tabs[i].scene.document.header.insertion_units;
let mut command = crate::modules::insert::pdf_attach::PdfAttachCommand::for_kind(kind, insunits);
let mut command =
crate::modules::insert::pdf_attach::PdfAttachCommand::for_kind(kind, insunits);
if !file.trim().is_empty() {
if let Some(crate::command::CmdResult::ReportError(message)) = command.on_text_input(file) {
if let Some(crate::command::CmdResult::ReportError(message)) =
command.on_text_input(file)
{
self.command_line.push_error(&message);
}
}
@@ -66,7 +69,9 @@ impl OpenCADStudio {
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
if !rest.is_empty() {
return Some(self.feed_command(crate::command::StepInput::Text(rest.to_string())));
return Some(
self.feed_command(crate::command::StepInput::Text(rest.to_string())),
);
}
Some(Task::none())
}
@@ -131,26 +136,31 @@ impl OpenCADStudio {
.block_records
.iter()
.filter(|br| br.flags.is_xref || br.flags.is_xref_overlay)
.filter(|br| {
pattern == "*" || crate::io::xref_model::wildcard_match(&br.name, pattern)
})
.filter(|br| pattern == "*" || crate::io::xref_model::wildcard_match(&br.name, pattern))
.map(|br| {
let kind = if br.flags.is_xref_overlay { "Overlay" } else { "Attach" };
let kind = if br.flags.is_xref_overlay {
"Overlay"
} else {
"Attach"
};
(br.name.clone(), kind, display_path(&br.xref_path))
})
.collect();
let out = &mut self.command_line;
out.push_output("");
out.push_output(&format!("{:<34}{:<13}{}", "Xref name", "Xref Type", "Path"));
out.push_output(&format!(
"{:<34}{:<13}{}",
"Xref name",
"Xref Type",
"Path"
"----------------------", "---------", "----------"
));
out.push_output(&format!("{:<34}{:<13}{}", "----------------------", "---------", "----------"));
out.push_output("");
for (name, kind, path) in &rows {
out.push_output(&format!("{:<34}{:<13}{}", format!("\"{name}\""), *kind, path));
out.push_output(&format!(
"{:<34}{:<13}{}",
format!("\"{name}\""),
*kind,
path
));
}
out.push_output("");
out.push_output(&format!("Total Xref(s): {}", rows.len()));
@@ -170,9 +180,8 @@ impl OpenCADStudio {
let host = self.tabs[i].current_path.clone();
if let Some(host) = host.as_deref() {
if crate::modules::insert::xattach::is_self_attach(host, &full, host.parent()) {
self.command_line.push_error(
"Error: Possible circular reference to current drawing. *Invalid*",
);
self.command_line
.push_error("Error: Possible circular reference to current drawing. *Invalid*");
return;
}
}
@@ -180,8 +189,7 @@ impl OpenCADStudio {
if let Some(existing) = existing.as_ref() {
self.command_line
.push_output(&format!("Xref \"{}\" has already been defined.", existing));
self.command_line
.push_output("Using existing definition.");
self.command_line.push_output("Using existing definition.");
} else {
let verb = if request.overlay {
format!("Overlay Xref \"{}\": {}", name, display_path(&full))
@@ -209,7 +217,8 @@ impl OpenCADStudio {
}
let (wires, source_units) = self.xref_preview(i, existing.as_deref(), &full);
let host_units = self.tabs[i].scene.document.header.insertion_units;
let unit = crate::app::properties::insert_unit_scale(host_units, source_units).unwrap_or(1.0);
let unit =
crate::app::properties::insert_unit_scale(host_units, source_units).unwrap_or(1.0);
let command = command.with_preview(wires, unit);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
@@ -326,8 +335,8 @@ impl OpenCADStudio {
let source_units = crate::io::load_file(std::path::Path::new(path))
.map(|doc| doc.header.insertion_units)
.unwrap_or(0);
state.unit_label = crate::t!(crate::app::properties::insunits_name(source_units))
.into_owned();
state.unit_label =
crate::t!(crate::app::properties::insunits_name(source_units)).into_owned();
let factor =
crate::app::properties::insert_unit_scale(host_units, source_units).unwrap_or(1.0);
state.unit_factor = crate::app::properties::format_unit_factor(factor);
@@ -434,9 +443,14 @@ impl OpenCADStudio {
let path = path.to_string_lossy().into_owned();
match std::fs::read(&path) {
Ok(bytes) => {
crate::scene::model::pdf_raster::register_source(&path, std::sync::Arc::new(bytes));
crate::scene::model::pdf_raster::register_source(
&path,
std::sync::Arc::new(bytes),
);
crate::scene::model::underlay_vector::forget(&path);
if crate::scene::model::underlay_vector::item_names(kind, &path).is_some_and(|n| !n.is_empty()) {
if crate::scene::model::underlay_vector::item_names(kind, &path)
.is_some_and(|n| !n.is_empty())
{
self.open_underlay_attach_dialog(kind, &path);
} else {
self.command_line.push_error(&format!(
@@ -455,7 +469,8 @@ impl OpenCADStudio {
// A point cloud goes on to the Attach Point Cloud dialog.
"rcp" | "rcs" => {
if crate::scene::model::point_cloud::load(&path).is_none() {
self.command_line.push_error("All scans are not found or invalid.");
self.command_line
.push_error("All scans are not found or invalid.");
self.command_line.push_error("Attach point cloud failed");
return Task::none();
}
@@ -509,7 +524,8 @@ impl OpenCADStudio {
{
let host = self.tabs[self.active_tab].current_path.clone();
let base = host.as_deref().and_then(|h| h.parent());
let full = crate::modules::insert::xattach::resolve_xref_store_path(&saved, base);
let full =
crate::modules::insert::xattach::resolve_xref_store_path(&saved, base);
self.load_xref_attach_file(&mut state, &full);
}
}
@@ -569,7 +585,9 @@ impl OpenCADStudio {
insert: (!state.insert_on_screen)
.then(|| state.insert_point())
.flatten(),
scale: (!state.scale_on_screen).then(|| state.scale_values()).flatten(),
scale: (!state.scale_on_screen)
.then(|| state.scale_values())
.flatten(),
rotation: (!state.rotation_on_screen)
.then(|| crate::entities::common::parse_typed_angle(&state.rotation))
.flatten()
@@ -577,9 +595,7 @@ impl OpenCADStudio {
};
let invalid = (!state.insert_on_screen && placement.insert.is_none())
|| (!state.scale_on_screen
&& placement
.scale
.is_none_or(|s| s.iter().any(|v| *v == 0.0)))
&& placement.scale.is_none_or(|s| s.iter().any(|v| *v == 0.0)))
|| (!state.rotation_on_screen && placement.rotation.is_none());
if invalid || state.path.is_empty() {
self.command_line