forked from 1b-agent/opencadstudio-upstream
fix(count): invalid-area restore, honest UPDATEFIELD, i18n and SVG icons
- Invalid Area "Undo" reverts the step that lost the boundary (redo after an undo, undo otherwise), checks the boundary is back and otherwise puts the step back and continues without the area; no auto-undo loop. - COUNTCLOSE / area replacement remove the count polyline outside the history instead of as an undo step of their own. - UPDATEFIELD re-evaluates the field texts of a table's *T block and reports the number actually updated. - Zoom to a counted object goes through the f64 handle_zoom_to_window. - Palette: SVG icons instead of glyph text, list cache keyed by tab id, cached result borrowed instead of cloned, "Count" as the source text. - Command-line messages go through t!/tf! with entries in all locales. - Cleanups: \n escapes, area comment, unused parameter, comment order. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
8fb4a692c6
commit
1d78ab2574
33 files changed
+476
-151
No files matched your search
@@ -418,7 +418,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_zoom_to_window(&mut self, p1: glam::DVec3, p2: glam::DVec3) {
|
||||
pub(in crate::app) fn handle_zoom_to_window(&mut self, p1: glam::DVec3, p2: glam::DVec3) {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
|
||||
+92
-55
@@ -59,7 +59,8 @@ impl OpenCADStudio {
|
||||
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.
|
||||
self.command_line.push_output(&format!("{} found", preselected.len()));
|
||||
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(","))));
|
||||
@@ -67,9 +68,9 @@ impl OpenCADStudio {
|
||||
// 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) };
|
||||
Some(self.start_count_command(i, Box::new(command), "COUNT"))
|
||||
Some(self.start_count_command(i, Box::new(command)))
|
||||
}
|
||||
"COUNTAREA" => Some(self.start_count_command(i, Box::new(CountCommand::new(true)), "COUNTAREA")),
|
||||
"COUNTAREA" => Some(self.start_count_command(i, Box::new(CountCommand::new(true)))),
|
||||
"COUNTCLOSE" => {
|
||||
self.close_count(i);
|
||||
Some(Task::none())
|
||||
@@ -91,7 +92,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("** COUNTFIELD command only available during Count. **"),
|
||||
None => self
|
||||
.command_line
|
||||
.push_error(&crate::t!("** COUNTFIELD command only available during Count. **")),
|
||||
}
|
||||
Some(Task::none())
|
||||
}
|
||||
@@ -115,7 +118,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
blocks.sort_by_key(|b| b.to_ascii_uppercase());
|
||||
Some(self.start_count_command(i, Box::new(CountTableCommand::new(blocks, others)), "COUNTTABLE"))
|
||||
Some(self.start_count_command(i, Box::new(CountTableCommand::new(blocks, others))))
|
||||
}
|
||||
"UPDATEFIELD" => {
|
||||
let preselected = self.tabs[i].scene.selected_handles_in_order();
|
||||
@@ -123,7 +126,7 @@ impl OpenCADStudio {
|
||||
self.update_fields(i, &preselected);
|
||||
return Some(Task::none());
|
||||
}
|
||||
Some(self.start_count_command(i, Box::new(UpdateFieldCommand::new()), "UPDATEFIELD"))
|
||||
Some(self.start_count_command(i, Box::new(UpdateFieldCommand::new())))
|
||||
}
|
||||
"_UPDATEFIELDRUN" => {
|
||||
let handles: Vec<Handle> = rest.split(',').filter_map(parse_handle).collect();
|
||||
@@ -147,7 +150,7 @@ impl OpenCADStudio {
|
||||
mode.cursor = None;
|
||||
self.tabs[i].count = Some(mode);
|
||||
self.apply_count_display(i);
|
||||
self.command_line.push_output("count area is active.");
|
||||
self.command_line.push_output(&crate::t!("count area is active."));
|
||||
}
|
||||
Some(Task::none())
|
||||
}
|
||||
@@ -164,7 +167,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
fn start_count_command(&mut self, i: usize, command: Box<dyn CadCommand>, _name: &str) -> Task<Message> {
|
||||
fn start_count_command(&mut self, i: usize, command: Box<dyn CadCommand>) -> Task<Message> {
|
||||
self.reset_command_start_state(i);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(command);
|
||||
@@ -190,7 +193,7 @@ impl OpenCADStudio {
|
||||
_ => None,
|
||||
};
|
||||
if let Some(v) = read_only {
|
||||
self.command_line.push_output(&format!("{name} = {v} (read only)"));
|
||||
self.command_line.push_output(&crate::tf!("{name} = {v} (read only)"));
|
||||
return;
|
||||
}
|
||||
let (current, min, max) = match name {
|
||||
@@ -199,7 +202,7 @@ impl OpenCADStudio {
|
||||
_ => (self.count_palette.error_color as i32, 1, 255),
|
||||
};
|
||||
let ask = |app: &mut Self| {
|
||||
app.command_line.push_output(&format!("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 {
|
||||
@@ -218,9 +221,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
_ => {
|
||||
self.command_line.push_error(&if max == 1 {
|
||||
"Requires 0 or 1 only.".to_string()
|
||||
crate::t!("Requires 0 or 1 only.")
|
||||
} else {
|
||||
format!("Requires an integer between {min} and {max}.")
|
||||
crate::tf!("Requires an integer between {min} and {max}.")
|
||||
});
|
||||
ask(self);
|
||||
}
|
||||
@@ -252,8 +255,8 @@ impl OpenCADStudio {
|
||||
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.into_iter().collect(),
|
||||
errors: r.errors.into_iter().chain(r.overlapped).collect(),
|
||||
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),
|
||||
}
|
||||
@@ -303,10 +306,35 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
/// Invalid Area › Undo (also ✕ and Cancel): the change that took the
|
||||
/// boundary away is undone.
|
||||
/// boundary away is reverted — redone when an undo took it, undone
|
||||
/// otherwise. When that does not bring the boundary back, the history
|
||||
/// step is put back and the area is dropped as with Continue.
|
||||
fn count_invalid_undo(&mut self, i: usize) {
|
||||
self.undo_active_tab();
|
||||
self.apply_count_display(i);
|
||||
let Some((boundary, _)) = self.tabs[i].count.as_ref().and_then(|m| m.boundary) else {
|
||||
return;
|
||||
};
|
||||
// An open step (the edit that just erased the boundary) goes on the
|
||||
// undo stack first; after that, something to redo means the last
|
||||
// history step was an undo.
|
||||
self.finish_pending_history(i);
|
||||
let redo = !self.tabs[i].history.redo_stack.is_empty();
|
||||
if redo || !self.tabs[i].history.undo_stack.is_empty() {
|
||||
if redo {
|
||||
self.redo_active_tab();
|
||||
} else {
|
||||
self.undo_active_tab();
|
||||
}
|
||||
if self.tabs[i].scene.document.get_entity(boundary).is_some() {
|
||||
self.apply_count_display(i);
|
||||
return;
|
||||
}
|
||||
if redo {
|
||||
self.undo_active_tab();
|
||||
} else {
|
||||
self.redo_active_tab();
|
||||
}
|
||||
}
|
||||
self.count_invalid_continue(i);
|
||||
}
|
||||
|
||||
/// Invalid Area › Continue: the area goes, the count covers all of model space.
|
||||
@@ -341,8 +369,10 @@ impl OpenCADStudio {
|
||||
if let Some((h, true)) = boundary {
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
if doc.get_entity(h).is_some() && !boundary_in_use(doc, h) {
|
||||
self.push_undo_snapshot(i, "COUNT");
|
||||
// The area layer is locked; count mode removes its own polyline.
|
||||
// Count mode removes its own polyline (the area layer is
|
||||
// locked) outside the history: an open step must not take the
|
||||
// removal in, or undoing it would bring back an orphan area.
|
||||
self.finish_pending_history(i);
|
||||
self.tabs[i].scene.rollback_new_entities(&[h]);
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
@@ -465,7 +495,7 @@ impl OpenCADStudio {
|
||||
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(&format!("{name} ...... {n}"));
|
||||
self.command_line.push_output(&crate::tf!("{name} ...... {n}"));
|
||||
}
|
||||
self.tabs[i].count = Some(mode);
|
||||
self.apply_count_display(i);
|
||||
@@ -503,38 +533,44 @@ impl OpenCADStudio {
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
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(&format!(
|
||||
self.command_line.push_output(&crate::tf!(
|
||||
"MTEXT Current text style: \"{}\" Text height: {:.4}",
|
||||
defaults.style_name, defaults.height
|
||||
));
|
||||
let command = FieldPlaceCommand::new(value, (code, Vec::new()), defaults.style_name, defaults.height).named("MTEXT");
|
||||
self.start_count_command(i, Box::new(command), "COUNTFIELD")
|
||||
self.start_count_command(i, Box::new(command))
|
||||
}
|
||||
|
||||
/// UPDATEFIELD: every field the objects host is evaluated again and its
|
||||
/// text written back.
|
||||
/// text written back. A table's fields are hosted by the cell texts of its
|
||||
/// drawing (its `*T` block); fields a table keeps without such a text are
|
||||
/// evaluated when drawn and are not counted here.
|
||||
fn update_fields(&mut self, i: usize, handles: &[Handle]) {
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let mut updates: Vec<(Handle, String)> = Vec::new();
|
||||
let mut found = 0;
|
||||
let mut updates: Vec<(Handle, String)> = Vec::new();
|
||||
for h in handles {
|
||||
let Some(entity) = doc.get_entity(*h) else { continue };
|
||||
if matches!(entity, EntityType::Table(_)) {
|
||||
// Table cells evaluate their fields when drawn.
|
||||
if let EntityType::Table(t) = entity {
|
||||
found += t.field_handles.len();
|
||||
let hosts: Vec<Handle> = match entity {
|
||||
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()))
|
||||
.map(|record| record.entity_handles.clone())
|
||||
.unwrap_or_default(),
|
||||
_ => vec![*h],
|
||||
};
|
||||
for host in hosts {
|
||||
if !doc.get_entity(host).is_some_and(|e| crate::entities::field::hosts_field(doc, e)) {
|
||||
continue;
|
||||
}
|
||||
found += 1;
|
||||
if let Some(text) = crate::entities::field::resolve(doc, host) {
|
||||
updates.push((host, text));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if !crate::entities::field::hosts_field(doc, entity) {
|
||||
continue;
|
||||
}
|
||||
found += 1;
|
||||
if let Some(text) = crate::entities::field::resolve(doc, *h) {
|
||||
updates.push((*h, text));
|
||||
}
|
||||
}
|
||||
self.command_line.push_output(&format!("{found} field(s) found."));
|
||||
self.command_line.push_output(&crate::tf!("{found} field(s) found."));
|
||||
if found == 0 {
|
||||
return;
|
||||
}
|
||||
@@ -551,15 +587,16 @@ impl OpenCADStudio {
|
||||
changed.push((h, crate::scene::ChangeKind::Modified));
|
||||
}
|
||||
}
|
||||
let table_handles: Vec<(Handle, crate::scene::ChangeKind)> = handles
|
||||
.iter()
|
||||
.filter(|h| matches!(self.tabs[i].scene.document.get_entity(**h), Some(EntityType::Table(_))))
|
||||
.map(|h| (*h, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
changed.extend(table_handles);
|
||||
self.tabs[i].scene.bump_entities(&changed);
|
||||
let updated = changed.len();
|
||||
// Table cell texts live in a block definition.
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
if scene.changes_touch_block_definition(&changed) {
|
||||
scene.bump_geometry();
|
||||
} else {
|
||||
scene.bump_entities(&changed);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!("{found} field(s) updated."));
|
||||
self.command_line.push_output(&crate::tf!("{updated} field(s) updated."));
|
||||
}
|
||||
|
||||
/// A count table at `point`: `Item` | `Count`, one row per block (all
|
||||
@@ -652,15 +689,14 @@ impl OpenCADStudio {
|
||||
if outline.is_empty() {
|
||||
return;
|
||||
}
|
||||
let (mut lo, mut hi) = (glam::Vec3::splat(f32::MAX), glam::Vec3::splat(f32::MIN));
|
||||
let (mut lo, mut hi) = (glam::DVec3::splat(f64::MAX), glam::DVec3::splat(f64::MIN));
|
||||
for c in outline.iter().flat_map(codec::count::Piece::points) {
|
||||
let p = glam::Vec3::new(c.x as f32, c.y as f32, c.z as f32);
|
||||
let p = glam::DVec3::new(c.x, c.y, c.z);
|
||||
lo = lo.min(p);
|
||||
hi = hi.max(p);
|
||||
}
|
||||
let pad = (hi - lo) * 1.5;
|
||||
self.tabs[i].scene.remember_current_view();
|
||||
self.tabs[i].scene.zoom_to_window(lo - pad, hi + pad);
|
||||
self.handle_zoom_to_window(lo - pad, hi + pad);
|
||||
}
|
||||
|
||||
fn open_count_target(&mut self, i: usize, name: String, key: CountKey) {
|
||||
@@ -773,7 +809,7 @@ impl OpenCADStudio {
|
||||
if names.is_empty() {
|
||||
return Task::none();
|
||||
}
|
||||
return self.start_count_command(i, Box::new(CountTableCommand::placing(names)), "COUNTTABLE");
|
||||
return self.start_count_command(i, Box::new(CountTableCommand::placing(names)));
|
||||
}
|
||||
CountMsg::Back | CountMsg::Close => self.close_count(i),
|
||||
CountMsg::Match(k, on) => {
|
||||
@@ -787,27 +823,28 @@ impl OpenCADStudio {
|
||||
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 counted = mode.result(&self.tabs[i].scene.document).counted;
|
||||
if counted.is_empty() {
|
||||
let result = mode.result(&self.tabs[i].scene.document);
|
||||
let n = result.counted.len();
|
||||
if n == 0 {
|
||||
return Task::none();
|
||||
}
|
||||
let n = counted.len();
|
||||
let next = match (mode.cursor, matches!(m, CountMsg::Next)) {
|
||||
(None, true) => 0,
|
||||
(None, false) => n - 1,
|
||||
(Some(c), true) => (c + 1) % n,
|
||||
(Some(c), false) => (c + n - 1) % n,
|
||||
};
|
||||
let target = result.counted[next];
|
||||
if let Some(mode) = self.tabs[i].count.as_mut() {
|
||||
mode.cursor = Some(next);
|
||||
}
|
||||
self.zoom_to_insert(i, counted[next]);
|
||||
self.zoom_to_insert(i, target);
|
||||
}
|
||||
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" });
|
||||
return self.start_count_command(i, Box::new(command), "COUNT");
|
||||
return self.start_count_command(i, Box::new(command));
|
||||
}
|
||||
CountMsg::Field => return self.dispatch_command("COUNTFIELD"),
|
||||
CountMsg::InvalidAlways(v) => self.count_palette.invalid_always = v,
|
||||
|
||||
@@ -926,8 +926,6 @@ impl OpenCADStudio {
|
||||
};
|
||||
Message::PcManager(PcManagerMsg::Toggle(handle, row))
|
||||
}
|
||||
// Point Cloud Manager tree: "search=<text>", "collapse", "expand",
|
||||
// "toggle_node=<key>" or "select=<key>".
|
||||
// Count palette and count mode: "search=<text>", "sort=name|count",
|
||||
// "open=NAME", "child=NAME:<n>" (an expanded row), "menu=<review|field|
|
||||
// layer|scale|mirror>:NAME", "create_table", "check=NAME:1",
|
||||
@@ -1000,6 +998,8 @@ impl OpenCADStudio {
|
||||
_ => return Err(bad()),
|
||||
})
|
||||
}
|
||||
// Point Cloud Manager tree: "search=<text>", "collapse", "expand",
|
||||
// "toggle_node=<key>" or "select=<key>".
|
||||
"pc_manager" => {
|
||||
use crate::ui::window::pc_manager::PcManagerMsg as M;
|
||||
let value = string(req, "value")?;
|
||||
|
||||
@@ -3034,6 +3034,7 @@ impl OpenCADStudio {
|
||||
&self.count_palette,
|
||||
tab.count.as_ref(),
|
||||
&tab.scene.document,
|
||||
tab.id,
|
||||
tab.scene.geometry_epoch,
|
||||
width,
|
||||
auto_collapse,
|
||||
|
||||
+15
-2
@@ -5530,8 +5530,6 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"US Survey Feet" => Some(("common", "us-survey-feet")),
|
||||
"to layout" => Some(("common", "to-layout")),
|
||||
"{count} objects selected" => Some(("common", "count-objects-selected")),
|
||||
"Count palette" => Some(("count-feature", "palette")),
|
||||
"Count menu" => Some(("count-feature", "menu")),
|
||||
"Count Selection" => Some(("count-feature", "selection")),
|
||||
"Review Count Details" => Some(("count-feature", "review")),
|
||||
"Insert Count Field" => Some(("count-feature", "insert-field")),
|
||||
@@ -5554,6 +5552,21 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"Continue and count the entire model space" => Some(("count-feature", "invalid-continue")),
|
||||
"The count area is dropped; counting goes on in all of model space." => Some(("count-feature", "invalid-continue-detail")),
|
||||
"Always perform my current choice" => Some(("count-feature", "invalid-always")),
|
||||
"{found} found" => Some(("count-feature", "found")),
|
||||
"{found} found, {total} total" => Some(("count-feature", "found-total")),
|
||||
"** COUNTFIELD command only available during Count. **" => Some(("count-feature", "field-only-in-count")),
|
||||
"count area is active." => Some(("count-feature", "area-active")),
|
||||
"{name} = {v} (read only)" => Some(("count-feature", "read-only-value")),
|
||||
"Requires 0 or 1 only." => Some(("count-feature", "requires-0-or-1")),
|
||||
"Requires an integer between {min} and {max}." => Some(("count-feature", "requires-int-range")),
|
||||
"{name} ...... {n}" => Some(("count-feature", "target-count")),
|
||||
"MTEXT Current text style: \"{}\" Text height: {:.4}" => Some(("count-feature", "mtext-current-style")),
|
||||
"{found} field(s) found." => Some(("count-feature", "fields-found")),
|
||||
"{updated} field(s) updated." => Some(("count-feature", "fields-updated")),
|
||||
"Invalid option keyword." => Some(("count-feature", "invalid-keyword")),
|
||||
"Invalid count area boundary object. Select a closed polyline consisting of line segments and does not intersect itself." => Some(("count-feature", "invalid-boundary")),
|
||||
"Block \"{part}\" cannot be found." => Some(("count-feature", "block-not-found")),
|
||||
"Invalid point." => Some(("count-feature", "invalid-point")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@
|
||||
//
|
||||
// The command gathers its answers and hands them to the application as
|
||||
// `_COUNTRUN <area> L|T <handles>` (COUNTAREA: `_COUNTAREASET <area>`), the
|
||||
// area being `C` (current view), `E` (entire model space), `R x,y x,y`,
|
||||
// `P x,y x,y …` or `O <handle>`.
|
||||
// area being `C` (current area, which counts all of model space), `E`
|
||||
// (entire model space), `R x,y x,y`, `P x,y x,y …` or `O <handle>`.
|
||||
|
||||
use codec::types::Handle;
|
||||
use codec::EntityType;
|
||||
@@ -82,7 +82,7 @@ impl CountCommand {
|
||||
self.step = Step::PolyStart;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::ReportError("Invalid option keyword.".into()),
|
||||
_ => CmdResult::ReportError(crate::t!("Invalid option keyword.").into_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,10 +150,11 @@ impl CadCommand for CountCommand {
|
||||
let found = handles.iter().filter(|h| !self.targets.contains(h)).count();
|
||||
let first = self.targets.is_empty();
|
||||
self.targets = handles;
|
||||
let total = self.targets.len();
|
||||
CmdResult::ReportMeasurement(if first {
|
||||
format!("{found} found")
|
||||
crate::tf!("{found} found").into_owned()
|
||||
} else {
|
||||
format!("{found} found, {} total", self.targets.len())
|
||||
crate::tf!("{found} found, {total} total").into_owned()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -175,8 +176,10 @@ impl CadCommand for CountCommand {
|
||||
self.area_done(format!("O {:X}", handle.value()))
|
||||
}
|
||||
_ => CmdResult::CancelWithMessage(
|
||||
"Invalid count area boundary object. Select a closed polyline consisting of line segments and does not intersect itself."
|
||||
.into(),
|
||||
crate::t!(
|
||||
"Invalid count area boundary object. Select a closed polyline consisting of line segments and does not intersect itself."
|
||||
)
|
||||
.into_owned(),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -298,18 +301,11 @@ impl CountTableCommand {
|
||||
let page: Vec<String> = self.pending.drain(..self.pending.len().min(22)).collect();
|
||||
if !self.pending.is_empty() {
|
||||
self.step = TableStep::Page;
|
||||
return CmdResult::ReportMeasurement(page.join("
|
||||
"));
|
||||
return CmdResult::ReportMeasurement(page.join("\n"));
|
||||
}
|
||||
CmdResult::Measurement(format!(
|
||||
"{}
|
||||
|
||||
User External Dependent Unnamed
|
||||
Blocks References Blocks Blocks
|
||||
{:>5}{:>10}{:>12}{:>12}
|
||||
",
|
||||
page.join("
|
||||
"),
|
||||
"{}\n\nUser External Dependent Unnamed\nBlocks References Blocks Blocks\n{:>5}{:>10}{:>12}{:>12}\n",
|
||||
page.join("\n"),
|
||||
self.blocks.len(),
|
||||
self.others[0],
|
||||
self.others[1],
|
||||
@@ -371,7 +367,11 @@ impl CadCommand for CountTableCommand {
|
||||
for part in text.split(',').map(str::trim).filter(|p| !p.is_empty()) {
|
||||
match self.blocks.iter().find(|b| b.eq_ignore_ascii_case(part)) {
|
||||
Some(b) => names.push(b.clone()),
|
||||
None => return Some(CmdResult::CancelWithMessage(format!("Block \"{part}\" cannot be found."))),
|
||||
None => {
|
||||
return Some(CmdResult::CancelWithMessage(
|
||||
crate::tf!("Block \"{part}\" cannot be found.").into_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
self.names = names;
|
||||
@@ -393,7 +393,7 @@ impl CadCommand for CountTableCommand {
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step {
|
||||
TableStep::Point => CmdResult::ReportError("Invalid point.".into()),
|
||||
TableStep::Point => CmdResult::ReportError(crate::t!("Invalid point.").into_owned()),
|
||||
_ => self.on_text_input("").unwrap_or(CmdResult::NeedPoint),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/d
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "COUNTLIST",
|
||||
label: "Count palette",
|
||||
label: "Count",
|
||||
icon: ICON,
|
||||
event: ModuleEvent::Command("_COUNTLISTTOGGLE".to_string()),
|
||||
}
|
||||
|
||||
@@ -83,6 +83,8 @@ pub static GEAR: &[u8] = include_bytes!("../../assets/icons/ui/gear.svg");
|
||||
pub static DOT: &[u8] = include_bytes!("../../assets/icons/ui/dot.svg");
|
||||
pub static DIRTY_DOT: &[u8] = include_bytes!("../../assets/icons/ui/dirty_dot.svg");
|
||||
pub static ARROW_LONG_RIGHT: &[u8] = include_bytes!("../../assets/icons/ui/arrow_long_right.svg");
|
||||
pub static MORE: &[u8] = include_bytes!("../../assets/icons/ui/more.svg");
|
||||
pub static INFO: &[u8] = include_bytes!("../../assets/icons/ui/info.svg");
|
||||
|
||||
// ── Status-bar toggle icons (issue #216) ──────────────────────────────────
|
||||
pub static ST_ORTHO: &[u8] = include_bytes!("../../assets/icons/status/ortho.svg");
|
||||
|
||||
@@ -726,7 +726,7 @@ fn idle_rows(
|
||||
rows.push(MenuRow::Item(if has_selection {
|
||||
cmd(t!("Count Selection").into_owned(), "COUNT _C")
|
||||
} else {
|
||||
cmd(t!("Count menu").into_owned(), "COUNT")
|
||||
cmd(t!("Count").into_owned(), "COUNT")
|
||||
}));
|
||||
rows.push(MenuRow::Separator);
|
||||
|
||||
|
||||
@@ -82,10 +82,11 @@ impl CountMode {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn result(&self, doc: &CadDocument) -> CountResult {
|
||||
/// The cached result, or one worked out from the drawing when there is none.
|
||||
pub fn result(&self, doc: &CadDocument) -> std::borrow::Cow<'_, CountResult> {
|
||||
match &self.cache {
|
||||
Some((result, _)) => result.clone(),
|
||||
None => self.compute(doc).0,
|
||||
Some((result, _)) => std::borrow::Cow::Borrowed(result),
|
||||
None => std::borrow::Cow::Owned(self.compute(doc).0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,8 +159,8 @@ pub struct CountPalette {
|
||||
/// What a lost count boundary does without asking: 0 ask, 1 undo, 2 go
|
||||
/// on with all of model space (kept with the user settings).
|
||||
pub invalid_choice: u8,
|
||||
/// The list's references for (document, geometry epoch, area).
|
||||
pub list_cache: std::cell::RefCell<Option<(usize, u64, Option<Vec<[f64; 2]>>, std::rc::Rc<Vec<BlockInstance>>)>>,
|
||||
/// The list's references for (document tab id, geometry epoch, area).
|
||||
pub list_cache: std::cell::RefCell<Option<(u64, u64, Option<Vec<[f64; 2]>>, std::rc::Rc<Vec<BlockInstance>>)>>,
|
||||
}
|
||||
|
||||
impl Default for CountPalette {
|
||||
@@ -313,7 +314,7 @@ fn list_row<'a>(
|
||||
name: &str,
|
||||
key: CountKey,
|
||||
label: String,
|
||||
count: String,
|
||||
count: Option<usize>,
|
||||
errors: bool,
|
||||
depth: u16,
|
||||
expanded: Option<bool>,
|
||||
@@ -323,7 +324,7 @@ fn list_row<'a>(
|
||||
let n = name.to_string();
|
||||
checkbox(on).size(14).on_toggle(move |v| msg(CountMsg::TableCheck(n.clone(), v))).into()
|
||||
} else if expanded == Some(true) {
|
||||
container(text("−").size(13)).width(Length::Fixed(14.0)).into()
|
||||
container(crate::ui::icons::themed(crate::ui::icons::MINUS, 12.0)).width(Length::Fixed(14.0)).into()
|
||||
} else {
|
||||
Space::new().width(Length::Fixed(14.0)).into()
|
||||
};
|
||||
@@ -335,7 +336,11 @@ fn list_row<'a>(
|
||||
if errors {
|
||||
cells = cells.push(crate::ui::icons::themed_warning(WARNING_ICON, 12.0));
|
||||
}
|
||||
cells = cells.push(text(count).size(12));
|
||||
// An expanded row shows its counts on the rows below it.
|
||||
cells = cells.push(match count {
|
||||
Some(n) => Element::from(text(n.to_string()).size(12)),
|
||||
None => crate::ui::icons::themed(crate::ui::icons::MORE, 12.0),
|
||||
});
|
||||
let body = container(cells.spacing(6).align_y(iced::Center)).width(Fill).padding([4, 6]);
|
||||
let area = mouse_area(body)
|
||||
.on_press(msg(CountMsg::Open(name.to_string(), key.clone())))
|
||||
@@ -345,14 +350,11 @@ fn list_row<'a>(
|
||||
}
|
||||
|
||||
fn header_button<'a>(label: String, active: bool, descending: bool, m: CountMsg) -> Element<'a, Message> {
|
||||
let arrow = if !active {
|
||||
""
|
||||
} else if descending {
|
||||
" ▼"
|
||||
} else {
|
||||
" ▲"
|
||||
};
|
||||
button(text(format!("{label}{arrow}")).size(11))
|
||||
let mut content = row![text(label).size(11)].spacing(3).align_y(iced::Center);
|
||||
if active {
|
||||
content = content.push(crate::ui::icons::themed_arrow_toggle(!descending, 8.0));
|
||||
}
|
||||
button(content)
|
||||
.on_press(msg(m))
|
||||
.style(button::text)
|
||||
.padding([2, 4])
|
||||
@@ -385,7 +387,7 @@ fn list_view<'a>(palette: &'a CountPalette, instances: &[BlockInstance]) -> Elem
|
||||
let mut list = column![].spacing(1);
|
||||
for r in rows(instances, palette) {
|
||||
let expansion = palette.expansion(&r.name).filter(|e| e.iter().any(|v| *v));
|
||||
let count = if expansion.is_some() { "•••".to_string() } else { r.count.to_string() };
|
||||
let count = expansion.is_none().then_some(r.count);
|
||||
list = list.push(list_row(
|
||||
palette,
|
||||
&r.name,
|
||||
@@ -399,7 +401,7 @@ fn list_view<'a>(palette: &'a CountPalette, instances: &[BlockInstance]) -> Elem
|
||||
if let Some([l, s, m]) = expansion {
|
||||
for (key, n) in codec::count::expanded_counts(instances, &r.name, l, s, m) {
|
||||
let label = key.label();
|
||||
list = list.push(list_row(palette, &r.name, key, label, n.to_string(), false, 1, None));
|
||||
list = list.push(list_row(palette, &r.name, key, label, Some(n), false, 1, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -525,16 +527,17 @@ pub fn view<'a>(
|
||||
palette: &'a CountPalette,
|
||||
mode: Option<&'a CountMode>,
|
||||
doc: &'a CadDocument,
|
||||
doc_id: u64,
|
||||
epoch: u64,
|
||||
width: f32,
|
||||
auto_collapse: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let title_bar = crate::ui::dock::title_bar(PanelId::Count, crate::t!("Count palette").into_owned(), auto_collapse);
|
||||
let title_bar = crate::ui::dock::title_bar(PanelId::Count, crate::t!("Count").into_owned(), auto_collapse);
|
||||
let body = match mode {
|
||||
Some(mode) if mode.target.is_some() => mode_view(palette, mode, doc),
|
||||
_ => {
|
||||
let area = mode.and_then(|m| m.area.clone());
|
||||
let key = (doc as *const CadDocument as usize, epoch);
|
||||
let key = (doc_id, epoch);
|
||||
let mut cache = palette.list_cache.borrow_mut();
|
||||
let instances = match cache.as_ref() {
|
||||
Some((d, e, a, list)) if (*d, *e) == key && *a == area => list.clone(),
|
||||
@@ -567,13 +570,7 @@ pub fn toolbar<'a>(mode: &CountMode, doc: &CadDocument) -> Element<'a, Message>
|
||||
let status: Element<'a, Message> = if !result.errors.is_empty() {
|
||||
crate::ui::icons::themed_warning(WARNING_ICON, 14.0)
|
||||
} else if has_target {
|
||||
container(text("i").size(11).style(|theme: &Theme| text::Style { color: Some(theme.palette().primary.base.color) }))
|
||||
.padding([0, 4])
|
||||
.style(|theme: &Theme| container::Style {
|
||||
border: Border { color: theme.palette().primary.base.color, width: 1.0, radius: 8.0.into() },
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
crate::ui::icons::themed_primary(crate::ui::icons::INFO, 14.0)
|
||||
} else {
|
||||
crate::ui::icons::themed_disabled(WARNING_ICON, 14.0)
|
||||
};
|
||||
@@ -628,7 +625,7 @@ pub fn invalid_area_view<'a>(palette: &CountPalette, sizing: crate::ui::modal::M
|
||||
let action = |title: String, detail: String, m: CountMsg| -> Element<'a, Message> {
|
||||
button(
|
||||
row![
|
||||
text("→").size(16).style(|theme: &Theme| text::Style { color: Some(theme.palette().primary.base.color) }),
|
||||
crate::ui::icons::themed_primary(crate::ui::icons::ARROW_LONG_RIGHT, 16.0),
|
||||
column![text(title).size(13), text(detail).size(11).style(muted_style)].spacing(2),
|
||||
]
|
||||
.spacing(10)
|
||||
|
||||
Reference in new issue
Block a user