From e29b408bda5e7e24b5c1d516cb95bce50abc9150 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:52:13 +0300 Subject: [PATCH 01/13] WIP count feature --- Cargo.lock | 2 +- Cargo.toml | 3 + src/app/commands/count.rs | 728 ++++++++++++++++++++++++++++++ src/app/commands/field.rs | 9 + src/app/commands/inquiry.rs | 39 -- src/app/commands/mod.rs | 4 + src/app/document.rs | 3 + src/app/mod.rs | 3 + src/app/settings.rs | 20 + src/app/update/dialog.rs | 5 + src/app/update/file.rs | 8 +- src/app/update/mod.rs | 3 + src/app/view/mod.rs | 15 + src/modules/annotate/count_cmd.rs | 395 ++++++++++++++++ src/modules/annotate/field_cmd.rs | 11 +- src/modules/annotate/mod.rs | 1 + src/modules/view/count_palette.rs | 11 + src/modules/view/mod.rs | 2 + src/scene/mod.rs | 38 +- src/ui/dock.rs | 5 + src/ui/popup/context_menu.rs | 6 + src/ui/ribbon/mod.rs | 7 + src/ui/ribbon/widgets.rs | 2 + src/ui/window/count_palette.rs | 565 +++++++++++++++++++++++ src/ui/window/field_dialog.rs | 45 +- src/ui/window/mod.rs | 1 + 26 files changed, 1887 insertions(+), 44 deletions(-) create mode 100644 src/app/commands/count.rs create mode 100644 src/modules/annotate/count_cmd.rs create mode 100644 src/modules/view/count_palette.rs create mode 100644 src/ui/window/count_palette.rs diff --git a/Cargo.lock b/Cargo.lock index 51294fb85..cb79660da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4206,7 +4206,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.5.5" -source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=d96e3fa#d96e3fa2fe5acbeac966f1db4c01142618bf9c79" +source = "git+https://github.com/ramox81/cadcodec.git?rev=94095f7#94095f76fda8cd2596174b2ee9845d568e5da3fb" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 85cc56ce7..ea12cedbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,9 @@ objc2-app-kit = { version = "0.2", features = ["NSApplication", "NSResponder", " block2 = "0.5" objc2-foundation = { version = "0.2", features = ["NSArray", "NSURL", "NSString", "NSObject", "NSNotification", "NSThread", "NSEnumerator"] } +[patch."https://github.com/HakanSeven12/opencadcodec.git"] +opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "94095f7" } + [patch.crates-io] iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } diff --git a/src/app/commands/count.rs b/src/app/commands/count.rs new file mode 100644 index 000000000..4a30a0c10 --- /dev/null +++ b/src/app/commands/count.rs @@ -0,0 +1,728 @@ +//! COUNT and its family: count mode (COUNT, COUNTAREA, COUNTCLOSE), the +//! Count palette (COUNTLIST, COUNTLISTCLOSE), count fields (COUNTFIELD, +//! UPDATEFIELD), count tables (COUNTTABLE) and the COUNT* variables. + +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 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"]; + +fn hex(h: Handle) -> String { + format!("{:X}", h.value()) +} + +fn parse_handle(s: &str) -> Option { + u64::from_str_radix(s.trim(), 16).ok().map(Handle::new) +} + +fn parse_xy(s: &str) -> Option<[f64; 2]> { + let (x, y) = s.split_once(',')?; + Some([x.trim().parse().ok()?, y.trim().parse().ok()?]) +} + +/// A field referring to `boundary` keeps the count area's polyline. +fn boundary_in_use(doc: &codec::CadDocument, boundary: Handle) -> bool { + let needle = format!("\"boundaryObjectHandle\":\"{}\"", hex(boundary)); + doc.fields.values().any(|f| f.code.contains(&needle)) +} + +impl OpenCADStudio { + pub(super) fn dispatch_count(&mut self, cmd: &str, i: usize) -> Option> { + let cmd = cmd.trim(); + let (verb, rest) = cmd.split_once(char::is_whitespace).unwrap_or((cmd, "")); + let verb = verb.to_ascii_uppercase(); + let rest = rest.trim(); + if verb == "SETVAR" { + let (name, value) = rest.split_once(char::is_whitespace).unwrap_or((rest, "")); + let name = name.to_ascii_uppercase(); + if !COUNT_SYSVARS.contains(&name.as_str()) { + return None; + } + self.count_sysvar(i, &name, Some(value.trim()).filter(|v| !v.is_empty())); + return Some(Task::none()); + } + if COUNT_SYSVARS.contains(&verb.as_str()) { + self.count_sysvar(i, &verb, Some(rest).filter(|v| !v.is_empty())); + return Some(Task::none()); + } + 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"); + if current && !preselected.is_empty() { + // Count Selection: the selection is the target. + self.command_line.push_output(&format!("{} found", preselected.len())); + let handles: Vec = preselected.iter().map(|h| hex(*h)).collect(); + self.tabs[i].scene.deselect_all(); + 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) }; + Some(self.start_count_command(i, Box::new(command), "COUNT")) + } + "COUNTAREA" => Some(self.start_count_command(i, Box::new(CountCommand::new(true)), "COUNTAREA")), + "COUNTCLOSE" => { + self.close_count(i); + Some(Task::none()) + } + "COUNTLIST" => { + self.set_count_palette(true); + Some(Task::none()) + } + "COUNTLISTCLOSE" => { + self.set_count_palette(false); + Some(Task::none()) + } + "_COUNTLISTTOGGLE" => { + let open = !self.count_palette.show; + self.set_count_palette(open); + Some(Task::none()) + } + "COUNTFIELD" => { + 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. **"), + } + Some(Task::none()) + } + "COUNTTABLE" => { + let doc = &self.tabs[i].scene.document; + let mut blocks = Vec::new(); + let mut others = [0usize; 3]; + for br in doc.block_records.iter() { + let upper = br.name.to_ascii_uppercase(); + if upper == "*MODEL_SPACE" || upper.starts_with("*PAPER_SPACE") { + continue; + } + if br.flags.is_xref || br.flags.is_xref_overlay { + others[0] += 1; + } else if br.name.contains('|') { + others[1] += 1; + } else if br.name.starts_with('*') { + others[2] += 1; + } else { + blocks.push(br.name.clone()); + } + } + blocks.sort_by_key(|b| b.to_ascii_uppercase()); + Some(self.start_count_command(i, Box::new(CountTableCommand::new(blocks, others)), "COUNTTABLE")) + } + "UPDATEFIELD" => { + let preselected = self.tabs[i].scene.selected_handles_in_order(); + if !preselected.is_empty() { + self.update_fields(i, &preselected); + return Some(Task::none()); + } + Some(self.start_count_command(i, Box::new(UpdateFieldCommand::new()), "UPDATEFIELD")) + } + "_UPDATEFIELDRUN" => { + let handles: Vec = rest.split(',').filter_map(parse_handle).collect(); + self.tabs[i].scene.deselect_all(); + self.update_fields(i, &handles); + Some(Task::none()) + } + "_COUNTRUN" => { + self.count_run(i, rest); + Some(Task::none()) + } + "_COUNTAREASET" => { + let tokens: Vec<&str> = rest.split_whitespace().collect(); + if let Some((area, boundary)) = self.resolve_count_area(i, &tokens) { + let mut mode = self.tabs[i].count.take().unwrap_or_default(); + if mode.boundary != boundary { + self.drop_count_boundary(i, mode.boundary); + } + mode.area = area; + mode.boundary = boundary; + mode.cursor = None; + self.tabs[i].count = Some(mode); + self.apply_count_display(i); + self.command_line.push_output("count area is active."); + } + Some(Task::none()) + } + "_COUNTTABLEPLACE" => { + let (point, names) = rest.split_once(' ').unwrap_or((rest, "")); + let p: Vec = point.split(',').filter_map(|v| v.parse().ok()).collect(); + if p.len() == 3 { + let names: Vec = 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()) + } + _ => None, + } + } + + fn start_count_command(&mut self, i: usize, command: Box, _name: &str) -> Task { + self.reset_command_start_state(i); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(command); + self.push_ucs_to_cmd(i); + self.sync_dyn_fields(); + self.focus_cmd_input() + } + + /// COUNTNUMBER / COUNTPALETTESTATE are read-only; COUNTSERVICE, + /// COUNTCOLOR and COUNTERRORCOLOR take a value (asked for when none is + /// given). + fn count_sysvar(&mut self, i: usize, name: &str, value: Option<&str>) { + let read_only = match name { + "COUNTNUMBER" => Some( + self.tabs[i] + .count + .as_ref() + .filter(|m| m.target.is_some()) + .map(|m| m.result(&self.tabs[i].scene.document).counted.len()) + .unwrap_or(0), + ), + "COUNTPALETTESTATE" => Some(self.count_palette.show as usize), + _ => None, + }; + if let Some(v) = read_only { + self.command_line.push_output(&format!("{name} = {v} (read only)")); + return; + } + let (current, min, max) = match name { + "COUNTSERVICE" => (self.count_palette.service as i32, 0, 1), + "COUNTCOLOR" => (self.count_palette.color as i32, 1, 255), + _ => (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.pending_setvar = Some(name.to_string()); + }; + let Some(value) = value else { + ask(self); + return; + }; + match value.parse::() { + Ok(n) if (min..=max).contains(&n) => { + match name { + "COUNTSERVICE" => self.count_palette.service = n == 1, + "COUNTCOLOR" => self.count_palette.color = n as i16, + _ => self.count_palette.error_color = n as i16, + } + self.persist_settings_if_changed(); + self.apply_count_display(i); + } + Ok(_) => { + self.command_line.push_error(&if max == 1 { + "Requires 0 or 1 only.".to_string() + } else { + format!("Requires an integer between {min} and {max}.") + }); + ask(self); + } + Err(_) => { + self.command_line.push_error("Requires an integer value."); + ask(self); + } + } + } + + /// Open or close the Count palette (docked on the right, expanded). + pub(in crate::app) fn set_count_palette(&mut self, open: bool) { + let id = crate::ui::dock::PanelId::Count; + self.count_palette.show = open; + 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_expanded = Some(id); + } else if self.dock_expanded == Some(id) { + self.dock_expanded = None; + } + } + + /// 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 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().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() { + mode.epoch = epoch; + } + } + + /// 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; + if self.tabs[i].count.as_ref().is_some_and(|m| m.epoch != self.tabs[i].scene.geometry_epoch) { + self.apply_count_display(i); + } + } + + /// COUNTCLOSE: leave count mode; the area polyline count mode drew goes + /// unless a field counts in it. + pub(in crate::app) fn close_count(&mut self, i: usize) { + if let Some(mode) = self.tabs[i].count.take() { + self.drop_count_boundary(i, mode.boundary); + } + self.tabs[i].scene.set_count_display(None); + } + + fn drop_count_boundary(&mut self, i: usize, boundary: Option<(Handle, bool)>) { + 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"); + self.tabs[i].scene.erase_entities(&[h]); + self.tabs[i].dirty = true; + } + } + } + + /// The count area of `C`, `E`, `K`, `R a b`, `P a b c …` or `O h`, and + /// its boundary polyline (rectangles and polygons are drawn on the + /// `0-CountArea` layer). `None` when the area cannot be had. + fn resolve_count_area( + &mut self, + i: usize, + tokens: &[&str], + ) -> Option<(Option>, Option<(Handle, bool)>)> { + let kind = tokens.first().copied().unwrap_or("E"); + let points = || tokens[1..].iter().map_while(|t| parse_xy(t)).collect::>(); + 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))) + } + "C" => { + let (x0, y0, x1, y1) = self.display_plot_window()?; + Some((Some(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1]]), None)) + } + "R" => { + let p = points(); + let (a, b) = (p.first()?, p.get(1)?); + let ring = vec![[a[0], a[1]], [b[0], a[1]], [b[0], b[1]], [a[0], b[1]]]; + let h = self.draw_count_area(i, &ring)?; + Some((Some(ring), Some((h, true)))) + } + "P" => { + let ring = points(); + (ring.len() >= 3).then_some(())?; + let h = self.draw_count_area(i, &ring)?; + Some((Some(ring), Some((h, true)))) + } + "O" => { + let h = parse_handle(tokens.get(1)?)?; + let ring = codec::count::boundary_polygon(&self.tabs[i].scene.document, h)?; + Some((Some(ring), Some((h, false)))) + } + _ => None, + } + } + + /// A closed polyline on `0-CountArea` (colour 152, not plotted, + /// Continuous), whatever the current layer. + fn draw_count_area(&mut self, i: usize, ring: &[[f64; 2]]) -> Option { + self.push_undo_snapshot(i, "COUNT"); + let doc = &mut self.tabs[i].scene.document; + if !doc.layers.contains(AREA_LAYER) { + let mut layer = codec::tables::Layer::new(AREA_LAYER); + layer.handle = doc.allocate_handle(); + layer.color = codec::types::Color::from_index(152); + layer.is_plottable = false; + layer.line_type = "Continuous".into(); + let _ = doc.layers.add(layer); + } + let mut pl = codec::entities::LwPolyline::new(); + pl.vertices = ring + .iter() + .map(|p| codec::entities::LwVertex::new(codec::types::Vector2::new(p[0], p[1]))) + .collect(); + pl.is_closed = true; + pl.common.layer = AREA_LAYER.into(); + let h = self.commit_entity_handle_preserve_layer(EntityType::LwPolyline(pl)); + self.tabs[i].dirty = true; + h + } + + /// `_COUNTRUN L` opens the palette; `_COUNTRUN T ` + /// 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 (area_tokens, tail) = tokens.split_at(split); + let targets: Vec = match tail { + ["T", handles, ..] => handles + .split(',') + .filter_map(parse_handle) + .filter(|h| self.tabs[i].scene.document.get_entity(*h).is_some()) + .collect(), + _ => Vec::new(), + }; + if targets.is_empty() && matches!(area_tokens.first(), Some(&"C") | Some(&"E") | None) { + self.set_count_palette(true); + return; + } + let Some((area, boundary)) = self.resolve_count_area(i, area_tokens) else { + return; + }; + let doc = &self.tabs[i].scene.document; + let blocks: Vec<&str> = targets + .iter() + .filter_map(|h| match doc.get_entity(*h) { + Some(EntityType::Insert(ins)) => Some(ins.block_name.as_str()), + _ => None, + }) + .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 }) + } + None if targets.is_empty() => None, + _ => Some(CountTarget::Group(targets.clone())), + }; + let old = self.tabs[i].count.take(); + if let Some(old) = old.as_ref().filter(|o| o.boundary != boundary) { + self.drop_count_boundary(i, old.boundary); + } + let mode = CountMode { area, boundary, target, cursor: None, epoch: 0 }; + if let Some(name) = mode.target_name() { + let n = mode.result(&self.tabs[i].scene.document).counted.len(); + self.command_line.push_output(&format!("{name} ...... {n}")); + } + self.tabs[i].count = Some(mode); + self.apply_count_display(i); + } + + /// The field COUNTFIELD places for the current count: the picked + /// reference (`single`), or the block with the match options (in the + /// area's polyline when there is one). + fn count_field_code(&self, i: usize) -> Option { + 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 { name, .. } => { + let instances = mode.instances(&self.tabs[i].scene.document); + Some(self.block_field_code(i, name, &mode.key(&instances))) + } + } + } + + /// `\AcCount` of a block and key, `\AcCount2` inside count mode's area polyline. + fn block_field_code(&self, i: usize, name: &str, key: &CountKey) -> String { + match self.tabs[i].count.as_ref().and_then(|m| m.boundary) { + Some((boundary, _)) => format!("\\AcCount2 {}", area_json(name, key, boundary)), + None => format!("\\AcCount {}", block_json(name, key)), + } + } + + /// 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 { + 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!( + "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") + } + + /// UPDATEFIELD: every field the objects host is evaluated again and its + /// text written back. + 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; + 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(); + } + 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.")); + if found == 0 { + return; + } + self.push_undo_snapshot(i, "UPDATEFIELD"); + let mut changed = Vec::new(); + for (h, text) in updates { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(h) { + match entity { + EntityType::MText(m) => m.value = text, + EntityType::Text(t) => t.value = text, + EntityType::AttributeDefinition(a) => a.default_value = text, + _ => continue, + } + 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); + self.tabs[i].dirty = true; + self.command_line.push_output(&format!("{found} field(s) updated.")); + } + + /// A count table at `point`: `Item` | `Count`, one row per block (all + /// counted blocks when `names` is empty), sorted by name; the counts are + /// fields. + fn place_count_table(&mut self, i: usize, point: [f64; 3], names: Vec) { + let doc = &self.tabs[i].scene.document; + 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 = if names.is_empty() { + 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())) + .collect() + }; + names.sort_by_key(|n| n.to_ascii_uppercase()); + names.dedup_by_key(|n| n.to_ascii_uppercase()); + let mut table = codec::entities::TableBuilder::new(names.len() + 1, 2) + .at(codec::types::Vector3::new(point[0], point[1], point[2])) + .row_height(0.36) + .column_width(3.6) + .build(); + table.set_column_width(1, 1.08); + table.set_cell_text(0, 0, "Item"); + table.set_cell_text(0, 1, "Count"); + for (r, name) in names.iter().enumerate() { + table.set_cell_text(r + 1, 0, name); + } + let codes: Vec = + 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; + }; + let doc = &mut self.tabs[i].scene.document; + let mut fields = Vec::new(); + for code in &codes { + let value = crate::entities::field::evaluate(doc, code, &[], None).unwrap_or_else(|| "####".into()); + let field = doc.new_table_cell_field(handle, "%<\\_FldIdx 0>%", vec![codec::fields::NewField::new(code.clone(), value.clone())]); + fields.push((field, value)); + } + if let Some(EntityType::Table(table)) = doc.get_entity_mut(handle) { + for (r, (field, value)) in fields.into_iter().enumerate() { + let Some(field) = field else { continue }; + if let Some(cell) = table.cell_mut(r + 1, 1) { + let mut content = codec::entities::table::CellContent::text(&value); + content.field_handle = Some(field); + cell.contents.clear(); + cell.contents.push(content); + cell.cell_type = codec::entities::table::CellType::Text; + } + table.field_handles.push(field); + } + } + self.tabs[i].scene.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]); + self.tabs[i].dirty = true; + } + + /// Zoom to a reference (← / →, an error of the report). + fn zoom_to_insert(&mut self, i: usize, h: Handle) { + let doc = &self.tabs[i].scene.document; + let Some(EntityType::Insert(ins)) = doc.get_entity(h) else { return }; + let Some(corners) = codec::count::insert_corners(doc, ins, 0) else { return }; + let (mut lo, mut hi) = (glam::Vec3::splat(f32::MAX), glam::Vec3::splat(f32::MIN)); + for c in corners { + let p = glam::Vec3::new(c.x as f32, c.y as f32, c.z as f32); + 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); + } + + fn open_count_target(&mut self, i: usize, name: String, key: CountKey) { + let mut mode = self.tabs[i].count.take().unwrap_or_default(); + 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)) + .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()], + picked: false, + }); + mode.cursor = None; + self.tabs[i].count = Some(mode); + self.apply_count_display(i); + } + + /// The Field dialog's Show Count Instances: count mode on what the + /// 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 }) => { + if let Some(b) = boundary { + 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); + mode.boundary = Some((b, false)); + } + mode.area = Some(ring); + self.tabs[i].count = Some(mode); + } + } + self.open_count_target(i, name, key); + } + Some(codec::count::CountQuery::Single(handles)) if !handles.is_empty() => { + let hexes: Vec = handles.iter().map(|h| hex(*h)).collect(); + let area = if self.tabs[i].count.is_some() { "K" } else { "E" }; + self.count_run(i, &format!("{area} T {}", hexes.join(","))); + } + _ => {} + } + } + + pub(in crate::app) fn on_count(&mut self, m: CountMsg) -> Task { + let i = self.active_tab; + if self.tabs[i].is_start { + return Task::none(); + } + match m { + CountMsg::Search(s) => self.count_palette.search = s, + CountMsg::Sort(by_count) => { + if self.count_palette.by_count == by_count { + self.count_palette.descending = !self.count_palette.descending; + } else { + self.count_palette.by_count = by_count; + self.count_palette.descending = false; + } + } + CountMsg::Open(name, key) | CountMsg::Menu(name, key, RowAction::Review) => { + if self.count_palette.table.is_none() { + self.open_count_target(i, name, key); + } + } + CountMsg::Menu(name, key, RowAction::Field) => { + let code = self.block_field_code(i, &name, &key); + return self.start_count_field(i, code); + } + CountMsg::Menu(name, _, RowAction::Expand(k)) => { + let upper = name.to_ascii_uppercase(); + let list = &mut self.count_palette.expanded; + match list.iter_mut().find(|(n, _)| *n == upper) { + Some((_, e)) => e[k] = !e[k], + None => { + let mut e = [false; 3]; + e[k] = true; + list.push((upper, e)); + } + } + } + CountMsg::CreateTable => self.count_palette.table = Some(Vec::new()), + CountMsg::TableCheck(name, on) => { + if let Some(checked) = self.count_palette.table.as_mut() { + let upper = name.to_ascii_uppercase(); + checked.retain(|n| *n != upper); + if on { + checked.push(upper); + } + } + } + CountMsg::TableAll(on) => { + let names: Vec = 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()); + crate::ui::window::count_palette::rows(&instances, &self.count_palette) + .into_iter() + .map(|r| r.name.to_ascii_uppercase()) + .collect() + } else { + Vec::new() + }; + self.count_palette.table = Some(names); + } + CountMsg::TableCancel => self.count_palette.table = None, + CountMsg::TableInsert => { + let names = self.count_palette.table.take().unwrap_or_default(); + if names.is_empty() { + return Task::none(); + } + return self.start_count_command(i, Box::new(CountTableCommand::placing(names)), "COUNTTABLE"); + } + 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() { + 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::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() { + 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, + }; + if let Some(mode) = self.tabs[i].count.as_mut() { + mode.cursor = Some(next); + } + self.zoom_to_insert(i, counted[next]); + } + 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"); + } + CountMsg::Field => return self.dispatch_command("COUNTFIELD"), + } + Task::none() + } +} diff --git a/src/app/commands/field.rs b/src/app/commands/field.rs index 5c084fc78..123e23582 100644 --- a/src/app/commands/field.rs +++ b/src/app/commands/field.rs @@ -155,6 +155,15 @@ impl OpenCADStudio { ); } FieldDialogMsg::PlotScale(k) => state.plot_scale = k, + FieldDialogMsg::CountExpression(v) => state.count_expression = v, + FieldDialogMsg::ShowCountInstances => { + // The dialog closes and count mode shows what the expression counts. + let json = state.count_expression.clone(); + self.field_dialog = None; + self.close_active_modal(); + self.show_count_instances(i, &json); + return Task::none(); + } FieldDialogMsg::SelectObject => { // The dialog waits while one object is picked. self.active_modal = None; diff --git a/src/app/commands/inquiry.rs b/src/app/commands/inquiry.rs index e13a454d4..dda3f01b1 100644 --- a/src/app/commands/inquiry.rs +++ b/src/app/commands/inquiry.rs @@ -1216,45 +1216,6 @@ impl OpenCADStudio { } } - // ── COUNT — entity statistics ───────────────────────────────────── - "COUNT" => { - use crate::command::KeywordCommand; - let c = KeywordCommand::new( - "COUNT", - "COUNT tally [All (by type) / by Layer]:", - vec![("All", "TYPE", None), ("By layer", "LAYER", None)], - ); - self.command_line.push_info(&c.prompt()); - self.tabs[self.active_tab].active_cmd = Some(Box::new(c)); - } - cmd if cmd.starts_with("COUNT ") => { - let filter = cmd.split_once(' ').map(|(_, r)| r.trim().to_uppercase()); - let mut counts: std::collections::BTreeMap = Default::default(); - for e in self.tabs[i].scene.document.entities() { - let layer = &e.common().layer; - let type_name = crate::entities::names::dxf_name(e); - let key = match &filter { - Some(f) if f == "LAYER" => layer.clone(), - Some(f) if f == "TYPE" => type_name.to_string(), - Some(f) => { - // Filter by layer name - if layer.to_uppercase() != *f { - continue; - } - type_name.to_string() - } - None => type_name.to_string(), - }; - *counts.entry(key).or_default() += 1; - } - let total: usize = counts.values().sum(); - for (k, n) in &counts { - self.command_line.push_output(crate::tf!(" {k}: {n}").as_ref()); - } - self.command_line - .push_output(crate::tf!("COUNT: {total} entity(ies) total.").as_ref()); - } - "DATAEXTRACTION" | "EATTEXT" | "ATTEXT" => { self.open_data_extraction(); } diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index 7b9f22f2b..558a87546 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -6,6 +6,7 @@ use std::path::PathBuf; mod attdef; mod field; +mod count; mod blocks; mod xref_attach; mod dim; @@ -296,6 +297,9 @@ impl OpenCADStudio { if let Some(t) = self.dispatch_field(cmd, i) { return Some(t); } + if let Some(t) = self.dispatch_count(cmd, i) { + return Some(t); + } if let Some(t) = self.dispatch_draw(cmd, i) { return Some(t); } diff --git a/src/app/document.rs b/src/app/document.rs index 9e147db7e..605f13d8e 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -221,6 +221,8 @@ pub(super) struct DocumentTab { pub(super) paper_bg_color: Option<[f32; 4]>, /// Active REFEDIT session, if any. pub(super) refedit_session: Option, + /// COUNT mode, while it is on. + pub(crate) count: Option, /// Open BEDIT block tabs. Definitions are edited live; each tab owns its /// entry snapshot and camera so nested blocks can remain open independently. pub(super) block_edits: Vec, @@ -651,6 +653,7 @@ impl DocumentTab { bg_color: None, paper_bg_color: None, refedit_session: None, + count: None, block_edits: Vec::new(), active_block_edit: None, active_mleader_style: "Standard".to_string(), diff --git a/src/app/mod.rs b/src/app/mod.rs index 392e1bed1..03f541acd 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -816,6 +816,7 @@ pub(super) struct OpenCADStudio { pub(crate) show_browser: bool, /// Point Cloud Manager palette (POINTCLOUDMANAGER). pub(crate) pc_manager: crate::ui::window::pc_manager::PcManager, + pub(crate) count_palette: crate::ui::window::count_palette::CountPalette, /// Which viewport background the colour wheel is editing, or `None` when /// it is closed. One slot, because only one wheel can be open at a time. pub(crate) bg_picker: Option, @@ -3897,6 +3898,7 @@ pub enum Message { FieldDialog(crate::ui::window::field_dialog::FieldDialogMsg), /// A click or search in the Point Cloud Manager. PcManager(crate::ui::window::pc_manager::PcManagerMsg), + Count(crate::ui::window::count_palette::CountMsg), PdfImportPickResult(Result<(std::path::PathBuf, std::sync::Arc>), String>), /// Result of the PDFATTACH file picker. PdfAttachPickResult(Result<(std::path::PathBuf, std::sync::Arc>), String>), @@ -4222,6 +4224,7 @@ impl OpenCADStudio { show_external_references: false, show_browser: false, pc_manager: Default::default(), + count_palette: Default::default(), bg_picker: None, block_palette: Default::default(), xref_manager: Default::default(), diff --git a/src/app/settings.rs b/src/app/settings.rs index 690114e91..ee18517f7 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -612,6 +612,15 @@ pub struct UserSettings { /// BLOCKNAVIGATE (default "."). #[serde(default = "default_block_navigate")] pub block_navigate: String, + /// COUNTCOLOR (default 3). + #[serde(default = "default_count_color")] + pub count_color: i16, + /// COUNTERRORCOLOR (default 1). + #[serde(default = "default_count_error_color")] + pub count_error_color: i16, + /// COUNTSERVICE (default on). + #[serde(default = "default_true")] + pub count_service: bool, } /// A block the Blocks palette lists from outside the current drawing (a @@ -675,6 +684,14 @@ fn default_one_u8() -> u8 { 1 } +fn default_count_color() -> i16 { + 3 +} + +fn default_count_error_color() -> i16 { + 1 +} + fn default_block_navigate() -> String { ".".to_string() } @@ -851,6 +868,9 @@ impl Default for UserSettings { block_mru_list: 50, block_redefine_mode: 1, block_navigate: ".".to_string(), + count_color: 3, + count_error_color: 1, + count_service: true, } } } diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index 943ab3d4a..1f3d6ee60 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -606,6 +606,10 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven PanelId::PointCloudManager => { self.pc_manager.show = false; } + PanelId::Count => { + self.count_palette.show = false; + self.ribbon.set_count_palette(false); + } PanelId::Properties => { self.show_properties = false; self.ribbon.set_properties(false); @@ -711,6 +715,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven PanelId::Browser => self.show_browser, PanelId::NodeGraph => self.show_node_graph, PanelId::PointCloudManager => self.pc_manager.show, + PanelId::Count => self.count_palette.show, } } diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 2edd18786..91d8298db 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -744,6 +744,9 @@ impl OpenCADStudio { block_mru_list: self.block_mru_list, block_redefine_mode: self.block_redefine_mode, block_navigate: self.block_navigate.clone(), + count_color: self.count_palette.color, + count_error_color: self.count_palette.error_color, + count_service: self.count_palette.service, } } @@ -874,6 +877,9 @@ impl OpenCADStudio { self.block_mru_list = s.block_mru_list.min(100); self.block_redefine_mode = s.block_redefine_mode.min(2); self.block_navigate = s.block_navigate.clone(); + self.count_palette.color = s.count_color; + self.count_palette.error_color = s.count_error_color; + self.count_palette.service = s.count_service; self.block_palette.recent = s.block_recent.clone(); self.block_palette.favorites = s.block_favorites.clone(); self.block_palette.libraries = s.block_libraries.clone(); @@ -5520,7 +5526,7 @@ impl OpenCADStudio { /// Current visible rectangle in the active space. The result is deliberately /// not clamped to the paper sheet: Display and Window may include the grey /// canvas outside the sheet, matching Model-space plotting. - fn display_plot_window(&self) -> Option<(f64, f64, f64, f64)> { + pub(in crate::app) fn display_plot_window(&self) -> Option<(f64, f64, f64, f64)> { let scene = &self.tabs[self.active_tab].scene; let (canvas_w, canvas_h) = scene.selection.borrow().vp_size; let viewport = if scene.current_layout == "Model" { diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index fdcac3bea..c0bbd3c11 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -493,6 +493,8 @@ impl OpenCADStudio { // The Reference Manager watches the active drawing and re-scans when // the palette is open on another tab's entries. self.refresh_xref_manager_if_stale(); + // Count mode recolours its references after the drawing changes. + self.refresh_count_if_stale(); // Let V4 plugins observe selection changes that happened while handling // this message (picking, window select, QSELECT, SELECTALL, grip edits, // and plugin request draining). @@ -1819,6 +1821,7 @@ impl OpenCADStudio { Message::AttdefDialog(message) => self.on_attdef_dialog(message), Message::FieldDialog(message) => self.on_field_dialog(message), Message::PcManager(message) => self.update_pc_manager(message), + Message::Count(message) => self.on_count(message), Message::RibbonSelectTab(idx) => { self.ribbon.select(idx); Task::none() diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index ee8e0d7fc..0c2db547f 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1669,6 +1669,13 @@ bg={bg_ms:.1}ms n={view_count}" } } + // Count mode: a blue frame round the drawing and the count toolbar. + if let (Some(mode), false) = (tab.count.as_ref(), tab.is_start) { + viewport_stack = viewport_stack.push(crate::ui::window::count_palette::border()); + viewport_stack = viewport_stack + .push(crate::ui::window::count_palette::toolbar(mode, &tab.scene.document)); + } + // Reserve the overlaid command line when placing cursor-anchored panels. mark("chrome"); let command_line_inset = if self.command_line.history_open { @@ -1888,6 +1895,7 @@ bg={bg_ms:.1}ms n={view_count}" crate::ui::dock::PanelId::Browser => self.show_browser, crate::ui::dock::PanelId::NodeGraph => self.show_node_graph, crate::ui::dock::PanelId::PointCloudManager => self.pc_manager.show, + crate::ui::dock::PanelId::Count => self.count_palette.show, } }; let edge_stack = |side: crate::app::config::DockSide| -> Option> { @@ -3023,6 +3031,13 @@ impl OpenCADStudio { auto_collapse, ), crate::ui::dock::PanelId::NodeGraph => tab.graph.panel(width, auto_collapse), + crate::ui::dock::PanelId::Count => crate::ui::window::count_palette::view( + &self.count_palette, + tab.count.as_ref(), + &tab.scene.document, + width, + auto_collapse, + ), crate::ui::dock::PanelId::PointCloudManager => crate::ui::window::pc_manager::view( &self.pc_manager, &tab.scene.document, diff --git a/src/modules/annotate/count_cmd.rs b/src/modules/annotate/count_cmd.rs new file mode 100644 index 000000000..8e75a99ab --- /dev/null +++ b/src/modules/annotate/count_cmd.rs @@ -0,0 +1,395 @@ +// COUNT / COUNTAREA, COUNTTABLE and UPDATEFIELD. +// +// Specify first corner point of the count area or +// [Current area/Entire model space/Object/Polygonal] : +// Specify opposite corner: (Polygonal: Specify start point: / +// Specify next point:; Object: +// Select object as count area boundary:) +// Select target objects or [List all blocks] : (COUNT) +// +// The command gathers its answers and hands them to the application as +// `_COUNTRUN L|T ` (COUNTAREA: `_COUNTAREASET `), the +// area being `C` (current view), `E` (entire model space), `R x,y x,y`, +// `P x,y x,y …` or `O `. + +use codec::types::Handle; +use codec::EntityType; +use glam::DVec3; + +use crate::command::{CadCommand, CmdOption, CmdResult, InputKind}; +use crate::scene::model::wire_model::WireModel; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Step { + Area, + Corner, + PolyStart, + PolyNext, + Object, + Targets, +} + +pub struct CountCommand { + /// COUNTAREA: only the area is asked. + area_only: bool, + step: Step, + area: String, + points: Vec, + targets: Vec, + picked: Option, +} + +fn xy(p: DVec3) -> String { + format!("{:?},{:?}", p.x, p.y) +} + +impl CountCommand { + pub fn new(area_only: bool) -> Self { + Self { + area_only, + step: Step::Area, + area: String::new(), + points: Vec::new(), + targets: Vec::new(), + picked: None, + } + } + + /// COUNT whose area is already answered (`COUNT _C`). + pub fn with_area(area: &str) -> Self { + Self { area: area.to_string(), step: Step::Targets, ..Self::new(false) } + } + + fn area_done(&mut self, area: String) -> CmdResult { + self.area = area; + if self.area_only { + return CmdResult::Dispatch(format!("_COUNTAREASET {}", self.area)); + } + self.step = Step::Targets; + CmdResult::NeedPoint + } + + fn area_keyword(&mut self, text: &str) -> CmdResult { + match text.trim().trim_start_matches('_').to_ascii_uppercase().as_str() { + "" | "C" | "CURRENT" | "CURRENT AREA" => self.area_done("C".into()), + "E" | "ENTIRE" | "ENTIRE MODEL SPACE" => self.area_done("E".into()), + "O" | "OBJECT" => { + self.step = Step::Object; + CmdResult::NeedPoint + } + "P" | "POLYGONAL" => { + self.points.clear(); + self.step = Step::PolyStart; + CmdResult::NeedPoint + } + _ => CmdResult::ReportError("Invalid option keyword.".into()), + } + } + + fn finish_targets(&self) -> CmdResult { + if self.targets.is_empty() { + return CmdResult::Dispatch(format!("_COUNTRUN {} L", self.area)); + } + let handles: Vec = self.targets.iter().map(|h| format!("{:X}", h.value())).collect(); + CmdResult::Dispatch(format!("_COUNTRUN {} T {}", self.area, handles.join(","))) + } +} + +impl CadCommand for CountCommand { + fn name(&self) -> &'static str { + if self.area_only { + "COUNTAREA" + } else { + "COUNT" + } + } + + fn prompt(&self) -> String { + match self.step { + Step::Area => "Specify first corner point of the count area or [Current area/Entire model space/Object/Polygonal] :".into(), + Step::Corner => "Specify opposite corner:".into(), + Step::PolyStart => "Specify start point:".into(), + Step::PolyNext => "Specify next point:".into(), + Step::Object => "Select object as count area boundary:".into(), + Step::Targets => "Select target objects or [List all blocks] :".into(), + } + } + + fn options(&self) -> Vec { + match self.step { + Step::Area => vec![ + CmdOption::new("Current area", "C"), + CmdOption::new("Entire model space", "E"), + CmdOption::new("Object", "O"), + CmdOption::new("Polygonal", "P"), + ], + Step::Targets => vec![CmdOption::new("List all blocks", "L")], + _ => Vec::new(), + } + } + + fn input_kind(&self) -> InputKind { + match self.step { + Step::Targets => InputKind::SingleToken, + _ => InputKind::Point, + } + } + + fn point_step_accepts_keywords(&self) -> bool { + self.step == Step::Area + } + + fn is_selection_gathering(&self) -> bool { + self.step == Step::Targets + } + + fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { + self.targets = handles; + CmdResult::ReportMeasurement(format!("{} found", self.targets.len())) + } + + fn needs_entity_pick(&self) -> bool { + self.step == Step::Object + } + + fn inject_before_entity_pick(&self) -> bool { + true + } + + fn inject_picked_entity(&mut self, entity: EntityType) { + self.picked = Some(entity); + } + + fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult { + match self.picked.take() { + Some(EntityType::LwPolyline(pl)) if pl.is_closed && pl.vertices.len() >= 3 => { + self.area_done(format!("O {:X}", handle.value())) + } + // Anything else: the prompt again. + _ => CmdResult::NeedPoint, + } + } + + fn on_point(&mut self, pt: DVec3) -> CmdResult { + match self.step { + Step::Area => { + self.points = vec![pt]; + self.step = Step::Corner; + CmdResult::NeedPoint + } + Step::Corner => { + let area = format!("R {} {}", xy(self.points[0]), xy(pt)); + self.area_done(area) + } + Step::PolyStart | Step::PolyNext => { + self.points.push(pt); + self.step = Step::PolyNext; + CmdResult::NeedPoint + } + _ => CmdResult::NeedPoint, + } + } + + fn on_text_input(&mut self, text: &str) -> Option { + match self.step { + Step::Area => Some(self.area_keyword(text)), + Step::Targets => match text.trim().trim_start_matches('_').to_ascii_uppercase().as_str() { + "L" | "LIST" | "LIST ALL BLOCKS" => { + self.targets.clear(); + Some(self.finish_targets()) + } + _ => None, + }, + _ => None, + } + } + + fn on_enter(&mut self) -> CmdResult { + match self.step { + Step::Area => self.area_keyword(""), + Step::PolyNext if self.points.len() >= 3 => { + let ring: Vec = self.points.iter().map(|p| xy(*p)).collect(); + let area = format!("P {}", ring.join(" ")); + self.area_done(area) + } + Step::Targets => self.finish_targets(), + _ => CmdResult::NeedPoint, + } + } + + fn on_mouse_move(&mut self, pt: DVec3) -> Option { + let ring: Vec = match self.step { + Step::Corner => { + let a = self.points[0]; + vec![a, DVec3::new(pt.x, a.y, a.z), pt, DVec3::new(a.x, pt.y, a.z)] + } + Step::PolyNext => { + let mut ring = self.points.clone(); + ring.push(pt); + ring + } + _ => return None, + }; + let mut points: Vec<[f64; 3]> = ring.iter().map(|p| p.to_array()).collect(); + points.push(points[0]); + Some(WireModel::solid_f64("count_area".into(), points, WireModel::CYAN, false)) + } +} + +/// COUNTTABLE: the blocks to list, then where the table goes. +pub struct CountTableCommand { + step: TableStep, + /// Defined user blocks (sorted), and the listing's other counts: + /// external references, dependent and unnamed blocks. + blocks: Vec, + others: [usize; 3], + names: Vec, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum TableStep { + Names, + List, + Point, +} + +impl CountTableCommand { + pub fn new(blocks: Vec, others: [usize; 3]) -> Self { + Self { step: TableStep::Names, blocks, others, names: Vec::new() } + } + + /// The palette's Create Table: the blocks are chosen, only the point is asked. + pub fn placing(names: Vec) -> Self { + Self { step: TableStep::Point, blocks: Vec::new(), others: [0; 3], names } + } + + /// `?`: the defined blocks matching the pattern and the summary. + fn listing(&self, pattern: &str) -> String { + let pattern = if pattern.trim().is_empty() { "*" } else { pattern.trim() }; + let mut out = String::from("Defined blocks."); + for name in &self.blocks { + if pattern.split(',').any(|p| crate::io::xref_model::wildcard_match(name, p.trim())) { + out.push_str(&format!("\n \"{name}\"")); + } + } + out.push_str(&format!( + "\n\nUser External Dependent Unnamed\nBlocks References Blocks Blocks\n{:>5}{:>11}{:>13}{:>12}", + self.blocks.len(), + self.others[0], + self.others[1], + self.others[2] + )); + out + } +} + +impl CadCommand for CountTableCommand { + fn name(&self) -> &'static str { + "COUNTTABLE" + } + + fn prompt(&self) -> String { + match self.step { + TableStep::Names => "Enter block name(s) to include or [?] :".into(), + TableStep::List => "Enter block(s) to list <*>:".into(), + TableStep::Point => "Specify insertion point:".into(), + } + } + + fn input_kind(&self) -> InputKind { + match self.step { + TableStep::Point => InputKind::Point, + _ => InputKind::FreeText, + } + } + + fn on_text_input(&mut self, text: &str) -> Option { + let text = text.trim(); + Some(match self.step { + TableStep::Names if text == "?" => { + self.step = TableStep::List; + CmdResult::NeedPoint + } + TableStep::Names if text.is_empty() => { + self.names.clear(); + self.step = TableStep::Point; + CmdResult::NeedPoint + } + TableStep::Names => { + let mut names = Vec::new(); + 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."))), + } + } + self.names = names; + self.step = TableStep::Point; + CmdResult::NeedPoint + } + TableStep::List => CmdResult::Measurement(self.listing(text)), + TableStep::Point => return None, + }) + } + + fn on_point(&mut self, pt: DVec3) -> CmdResult { + if self.step != TableStep::Point { + return CmdResult::NeedPoint; + } + CmdResult::Dispatch(format!("_COUNTTABLEPLACE {:?},{:?},{:?} {}", pt.x, pt.y, pt.z, self.names.join("|"))) + } + + fn on_enter(&mut self) -> CmdResult { + match self.step { + TableStep::Point => CmdResult::ReportError("Invalid point.".into()), + _ => self.on_text_input("").unwrap_or(CmdResult::NeedPoint), + } + } +} + +/// UPDATEFIELD: `Select objects:`, then the fields of the selection are +/// evaluated again. +pub struct UpdateFieldCommand { + selected: Vec, +} + +impl UpdateFieldCommand { + pub fn new() -> Self { + Self { selected: Vec::new() } + } +} + +impl CadCommand for UpdateFieldCommand { + fn name(&self) -> &'static str { + "UPDATEFIELD" + } + + fn prompt(&self) -> String { + "Select objects:".into() + } + + fn is_selection_gathering(&self) -> bool { + true + } + + fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { + self.selected = handles; + CmdResult::NeedPoint + } + + fn on_point(&mut self, _pt: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + if self.selected.is_empty() { + return CmdResult::Cancel; + } + let handles: Vec = self.selected.iter().map(|h| format!("{:X}", h.value())).collect(); + CmdResult::Dispatch(format!("_UPDATEFIELDRUN {}", handles.join(","))) + } +} + +inventory::submit!(crate::command::CommandRegistration { + names: &["COUNT", "COUNTAREA", "COUNTCLOSE", "COUNTFIELD", "COUNTLIST", "COUNTLISTCLOSE", "COUNTTABLE", "UPDATEFIELD"] +}); diff --git a/src/modules/annotate/field_cmd.rs b/src/modules/annotate/field_cmd.rs index 53e62dfa4..f6cccfb9c 100644 --- a/src/modules/annotate/field_cmd.rs +++ b/src/modules/annotate/field_cmd.rs @@ -29,6 +29,7 @@ pub struct FieldPlaceCommand { height: f64, attachment: AttachmentPoint, plane: WorkingPlane, + name: &'static str, } impl FieldPlaceCommand { @@ -41,8 +42,16 @@ impl FieldPlaceCommand { height, attachment: AttachmentPoint::TopLeft, plane: WorkingPlane::default(), + name: "FIELD", } } + + /// The command name shown with the prompts (COUNTFIELD places its field + /// as MTEXT). + pub fn named(mut self, name: &'static str) -> Self { + self.name = name; + self + } } fn short(v: f64) -> String { @@ -52,7 +61,7 @@ fn short(v: f64) -> String { impl CadCommand for FieldPlaceCommand { fn name(&self) -> &'static str { - "FIELD" + self.name } fn set_working_plane(&mut self, plane: WorkingPlane) { diff --git a/src/modules/annotate/mod.rs b/src/modules/annotate/mod.rs index 5a882d148..8e9f8ba39 100644 --- a/src/modules/annotate/mod.rs +++ b/src/modules/annotate/mod.rs @@ -8,6 +8,7 @@ pub mod data_extract; pub mod data_link; pub mod ddedit; pub mod field_cmd; +pub mod count_cmd; pub mod diameter_dim; pub mod dim_baseline; pub mod dim_continue; diff --git a/src/modules/view/count_palette.rs b/src/modules/view/count_palette.rs new file mode 100644 index 000000000..97321536f --- /dev/null +++ b/src/modules/view/count_palette.rs @@ -0,0 +1,11 @@ +use crate::modules::{IconKind, ModuleEvent, ToolDef}; +pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/data_extract.svg")); +/// View › Palettes › Count: opens or closes the Count palette. +pub fn tool() -> ToolDef { + ToolDef { + id: "COUNTLIST", + label: "Count", + icon: ICON, + event: ModuleEvent::Command("_COUNTLISTTOGGLE".to_string()), + } +} diff --git a/src/modules/view/mod.rs b/src/modules/view/mod.rs index d29f1826d..e11bbd383 100644 --- a/src/modules/view/mod.rs +++ b/src/modules/view/mod.rs @@ -12,6 +12,7 @@ pub mod plot_window; pub mod quick_print; mod properties_palette; mod sheetset; +mod count_palette; mod tile_horiz; mod tile_vert; pub mod visual_style; @@ -120,6 +121,7 @@ impl CadModule for ViewModule { tools: vec![ RibbonItem::LargeTool(tool_palettes::tool()), RibbonItem::LargeTool(properties_palette::tool()), + RibbonItem::LargeTool(count_palette::tool()), RibbonItem::LargeTool(sheetset::tool()), ], }, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index a08c183b8..09a4dc793 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1930,6 +1930,15 @@ pub enum ViewportRefreshScope { /// Entity membership in document order, keyed by geometry epoch. type BlockMembers = (u64, HashMap>); +/// What count mode colours: see [`Scene::count_display`]. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct CountDisplay { + pub counted: HashSet, + pub errors: HashSet, + pub color: [f32; 4], + pub error_color: [f32; 4], +} + pub struct Scene { pub camera: Rc>, /// View saved immediately before the latest navigation operation. ZOOM @@ -2018,6 +2027,10 @@ pub struct Scene { /// edited geometry stands out while the surrounding drawing stays visible /// for context. `None` = not editing. (#136) pub refedit_keep: Option>, + /// COUNT mode colours: counted references in COUNTCOLOR, overlapping + /// duplicates in COUNTERRORCOLOR, everything else faded. `None` outside + /// count mode. + pub count_display: Option, /// Entity drawn with the selection-highlight colour without being part /// of the real selection — used to preview a row in the cycling list box. pub hover_highlight: Option, @@ -2612,6 +2625,7 @@ impl Scene { preview_hidden: HashSet::default(), command_preview_hidden: HashSet::default(), refedit_keep: None, + count_display: None, hover_highlight: None, constraint_hover_highlights: HashSet::default(), constraint_hover_refs: Vec::new(), @@ -4084,10 +4098,32 @@ impl Scene { self.bump_geometry_no_blocks(); } + /// Enter / leave the COUNT colouring; a no-op when nothing changed. + pub fn set_count_display(&mut self, display: Option) { + if self.count_display == display { + return; + } + self.count_display = display; + self.bump_geometry_no_blocks(); + } + /// Fade the colours of wires that belong to entities outside the REFEDIT /// keep set (no-op when not editing). The geometry is untouched, so - /// hit-testing still works on faded entities. + /// hit-testing still works on faded entities. In count mode the counted + /// references and the duplicates take the count colours instead. fn apply_refedit_fade(&self, wires: &mut [WireModel], bg: [f32; 4]) { + if let Some(count) = &self.count_display { + for w in wires.iter_mut() { + let h = Self::handle_from_wire_name(&w.name); + w.color = match h { + Some(h) if count.errors.contains(&h) => count.error_color, + Some(h) if count.counted.contains(&h) => count.color, + _ => crate::scene::cache::block_cache::fade_toward_bg(w.color, bg), + }; + // Shared instance geometry keeps its own colour; draw these wires on their own. + w.render_instance = None; + } + } let Some(keep) = &self.refedit_keep else { return; }; diff --git a/src/ui/dock.rs b/src/ui/dock.rs index 1e62a58cf..9cab6440c 100644 --- a/src/ui/dock.rs +++ b/src/ui/dock.rs @@ -48,6 +48,8 @@ pub enum PanelId { NodeGraph, /// Regions and scans of the drawing's point clouds. PointCloudManager, + /// Block counts (COUNTLIST). + Count, } impl PanelId { @@ -60,6 +62,7 @@ impl PanelId { PanelId::Browser => "Browser", PanelId::NodeGraph => "Node Graph", PanelId::PointCloudManager => "Point Cloud Manager", + PanelId::Count => "Count", } } @@ -72,6 +75,7 @@ impl PanelId { PanelId::Browser => 230.0, PanelId::NodeGraph => 220.0, PanelId::PointCloudManager => 280.0, + PanelId::Count => 280.0, } } @@ -162,6 +166,7 @@ impl DockState { PanelId::Browser, PanelId::NodeGraph, PanelId::PointCloudManager, + PanelId::Count, ] { self.panels.entry(id).or_insert_with(|| DockPanel::for_id(id)); } diff --git a/src/ui/popup/context_menu.rs b/src/ui/popup/context_menu.rs index 4c68f01ae..407aa66c2 100644 --- a/src/ui/popup/context_menu.rs +++ b/src/ui/popup/context_menu.rs @@ -722,6 +722,12 @@ fn idle_rows( t!("Quick Select...").into_owned(), MenuAction::QuickSelect, ))); + // Count the selected objects, or start a count. + rows.push(MenuRow::Item(if has_selection { + cmd(t!("Count Selection").into_owned(), "COUNT _C") + } else { + cmd(t!("Count menu").into_owned(), "COUNT") + })); rows.push(MenuRow::Separator); // ── Panels ───────────────────────────────────────────────────────── diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index 20989bd37..d59bac1c6 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -57,6 +57,8 @@ pub struct Ribbon { pub show_ucs_icon: bool, /// Properties panel (PROPERTIES) visibility — drives the Properties button highlight. pub show_properties: bool, + /// The Count palette is open (View › Palettes › Count). + pub show_count_palette: bool, /// File tabs (FILETAB) visibility — drives the File Tabs button highlight. pub show_file_tabs: bool, /// Layout tabs (LAYOUTTAB) visibility — drives the Layout Tabs button highlight. @@ -173,6 +175,7 @@ impl Ribbon { show_viewcube: true, show_ucs_icon: true, show_properties: true, + show_count_palette: false, show_file_tabs: true, show_layout_tabs: true, open_dropdown: None, @@ -330,6 +333,9 @@ impl Ribbon { pub fn set_properties(&mut self, on: bool) { self.show_properties = on; } + pub fn set_count_palette(&mut self, on: bool) { + self.show_count_palette = on; + } pub fn set_file_tabs(&mut self, on: bool) { self.show_file_tabs = on; } @@ -347,6 +353,7 @@ impl Ribbon { show_viewcube: self.show_viewcube, show_ucs_icon: self.show_ucs_icon, show_properties: self.show_properties, + show_count_palette: self.show_count_palette, show_block_palette, show_file_tabs: self.show_file_tabs, show_layout_tabs: self.show_layout_tabs, diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index 74e7d9393..89bb1de2f 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -37,6 +37,7 @@ pub(super) struct ToggleState { pub show_viewcube: bool, pub show_ucs_icon: bool, pub show_properties: bool, + pub show_count_palette: bool, pub show_block_palette: bool, pub show_file_tabs: bool, pub show_layout_tabs: bool, @@ -379,6 +380,7 @@ pub(super) fn is_active_tool( "NAVVCUBE" => state.show_viewcube, "UCSICON" => state.show_ucs_icon, "PROPERTIES" => state.show_properties, + "COUNTLIST" => state.show_count_palette, "BLOCKPALETTE" => state.show_block_palette, "FILETAB" => state.show_file_tabs, "LAYOUTTAB" => state.show_layout_tabs, diff --git a/src/ui/window/count_palette.rs b/src/ui/window/count_palette.rs new file mode 100644 index 000000000..cfd74c2bb --- /dev/null +++ b/src/ui/window/count_palette.rs @@ -0,0 +1,565 @@ +//! The Count palette (COUNTLIST) and count mode (COUNT): the drawing's +//! blocks with how many references of each are counted, and — once a block +//! is chosen — its counted references coloured in the drawing with a +//! floating toolbar above it. Counting itself is `codec::count`. + +use crate::app::Message; +use crate::ui::dock::PanelId; +use crate::ui::style::common::muted_style; +use crate::ui::style::form::{button_style, field_style}; +use codec::count::{BlockInstance, CountKey}; +use codec::{CadDocument, Handle}; +use iced::widget::{button, checkbox, column, container, mouse_area, row, scrollable, text, text_input, tooltip, Space}; +use iced::{Background, Border, Color, Element, Fill, Length, Theme}; + +const BLOCK_ICON: &[u8] = include_bytes!("../../../assets/icons/blocks/block.svg"); +const AREA_ICON: &[u8] = include_bytes!("../../../assets/icons/shapes/rect.svg"); +const SELECT_ICON: &[u8] = include_bytes!("../../../assets/icons/blocks/select_objects.svg"); +const TABLE_ICON: &[u8] = include_bytes!("../../../assets/icons/table.svg"); +const WARNING_ICON: &[u8] = include_bytes!("../../../assets/icons/ui/warning_triangle.svg"); +const CHECK_ICON: &[u8] = include_bytes!("../../../assets/icons/ui/check.svg"); + +/// The name of the layer count areas are drawn on. +pub const AREA_LAYER: &str = "0-CountArea"; + +/// What a count counts. +#[derive(Debug, Clone, PartialEq)] +pub enum CountTarget { + /// The references of a block. `reference` is the instance the match + /// options compare with; `picked` when it came from a COUNT selection. + Block { + name: String, + reference: Option, + matching: [bool; 3], + picked: bool, + }, + /// Several picked objects counted as one group. + Group(Vec), +} + +/// Count mode of a drawing. +#[derive(Debug, Clone, Default)] +pub struct CountMode { + /// The count area (a closed WCS polygon); `None` = all of model space. + pub area: Option>, + /// The closed polyline marking the area, and whether count mode drew it + /// (then COUNTCLOSE erases it unless a field refers to it). + pub boundary: Option<(Handle, bool)>, + pub target: Option, + /// The counted reference ← / → last showed. + pub cursor: Option, + /// The scene geometry epoch the colouring was computed at. + pub epoch: u64, +} + +/// A count mode's result: the counted references and the overlapping +/// duplicates (errors). +#[derive(Debug, Clone, Default, PartialEq)] +pub struct CountResult { + pub counted: Vec, + pub errors: Vec, +} + +impl CountMode { + pub fn instances(&self, doc: &CadDocument) -> Vec { + codec::count::block_instances(doc, self.area.as_deref()) + } + + /// The key the match options build from the reference instance. + pub fn key(&self, instances: &[BlockInstance]) -> CountKey { + match &self.target { + Some(CountTarget::Block { reference: Some(r), matching, .. }) => instances + .iter() + .find(|i| i.handle == *r) + .map(|i| CountKey::of(i, matching[0], matching[1], matching[2])) + .unwrap_or_default(), + _ => CountKey::default(), + } + } + + pub fn result(&self, doc: &CadDocument) -> CountResult { + match &self.target { + None => CountResult::default(), + Some(CountTarget::Group(handles)) => CountResult { + counted: handles.iter().copied().filter(|h| doc.get_entity(*h).is_some()).collect(), + errors: Vec::new(), + }, + Some(CountTarget::Block { name, .. }) => { + let instances = self.instances(doc); + let key = self.key(&instances); + let mine = |i: &&BlockInstance| i.name.eq_ignore_ascii_case(name) && key.matches(i); + CountResult { + counted: instances.iter().filter(mine).filter(|i| i.duplicate_of.is_none()).map(|i| i.handle).collect(), + errors: instances.iter().filter(mine).filter(|i| i.duplicate_of.is_some()).map(|i| i.handle).collect(), + } + } + } + } + + /// The name the toolbar and the command line show for the target. + pub fn target_name(&self) -> Option { + match &self.target { + Some(CountTarget::Block { name, .. }) => Some(name.clone()), + Some(CountTarget::Group(_)) => Some("group".into()), + None => None, + } + } +} + +/// Palette state that is not in the drawing, and the profile variables +/// COUNTCOLOR, COUNTERRORCOLOR and COUNTSERVICE. +#[derive(Debug)] +pub struct CountPalette { + pub show: bool, + pub search: String, + /// Sorted by the Count column (else by Name), and descending. + pub by_count: bool, + pub descending: bool, + /// Expanded rows: block name (upper case) → expand by layer, scale, mirror state. + pub expanded: Vec<(String, [bool; 3])>, + /// Create Table selection mode: the checked block names (upper case). + pub table: Option>, + pub details_closed: bool, + pub errors_closed: bool, + pub color: i16, + pub error_color: i16, + pub service: bool, +} + +impl Default for CountPalette { + fn default() -> Self { + Self { + show: false, + search: String::new(), + by_count: false, + descending: false, + expanded: Vec::new(), + table: None, + details_closed: false, + errors_closed: false, + color: 3, + error_color: 1, + service: true, + } + } +} + +impl CountPalette { + pub fn expansion(&self, name: &str) -> Option<[bool; 3]> { + let key = name.to_ascii_uppercase(); + self.expanded.iter().find(|(n, _)| *n == key).map(|(_, e)| *e) + } +} + +/// What a row's context menu does. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RowAction { + Review, + Field, + /// Expand by layer (0), scale (1) or mirror state (2). + Expand(usize), +} + +#[derive(Debug, Clone)] +pub enum CountMsg { + Search(String), + /// Sort by the Count column (`true`) or the Name column. + Sort(bool), + /// A row clicked: count mode on its block (and key, for an expanded row). + Open(String, CountKey), + Menu(String, CountKey, RowAction), + CreateTable, + TableCheck(String, bool), + TableAll(bool), + TableCancel, + TableInsert, + Back, + Match(usize, bool), + ToggleDetails, + ToggleErrors, + /// Show an overlapping duplicate. + ShowError(Handle), + Area, + Prev, + Next, + Select, + Field, + Close, +} + +fn msg(m: CountMsg) -> Message { + Message::Count(m) +} + +/// One block's row: counted references and duplicates. +pub struct Row { + pub name: String, + pub count: usize, + pub errors: usize, +} + +/// The rows of the list: blocks whose name contains the search text +/// (case-insensitively), sorted by the chosen column. +pub fn rows(instances: &[BlockInstance], palette: &CountPalette) -> Vec { + let query = palette.search.trim().to_lowercase(); + let mut rows: Vec = codec::count::block_counts(instances) + .into_iter() + .filter(|c| query.is_empty() || c.name.to_lowercase().contains(&query)) + .map(|c| Row { count: c.counted.len(), errors: c.duplicates.len(), name: c.name }) + .collect(); + if palette.by_count { + rows.sort_by(|a, b| a.count.cmp(&b.count).then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))); + } + if palette.descending { + rows.reverse(); + } + rows +} + +fn small_icon<'a>(bytes: &'static [u8], size: f32) -> Element<'a, Message> { + crate::ui::icons::semantic(bytes, size) +} + +fn row_menu(name: String, key: CountKey, expandable: bool) -> Element<'static, Message> { + let entry = |label: String, action: RowAction| -> Element<'static, Message> { + button(text(label).size(12)) + .width(Fill) + .padding([5, 12]) + .on_press(msg(CountMsg::Menu(name.clone(), key.clone(), action))) + .style(|theme: &Theme, status| { + let mut s = button::text(theme, status); + if matches!(status, button::Status::Hovered) { + s.background = Some(Background::Color(theme.palette().primary.base.color)); + s.text_color = theme.palette().primary.base.text; + } + s + }) + .into() + }; + let mut items = column![ + entry(crate::t!("Review Count Details").into_owned(), RowAction::Review), + entry(crate::t!("Insert Count Field").into_owned(), RowAction::Field), + ] + .spacing(1); + if expandable { + items = items + .push(container(Space::new()).height(1).width(Fill).style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.neutral.color)), + ..Default::default() + })) + .push(container(text(crate::t!("EXPAND BY")).size(10).style(muted_style)).padding([3, 12])) + .push(entry(crate::t!("Layer").into_owned(), RowAction::Expand(0))) + .push(entry(crate::t!("Scale").into_owned(), RowAction::Expand(1))) + .push(entry(crate::t!("Mirror State").into_owned(), RowAction::Expand(2))); + } + container(items) + .padding(4) + .width(Length::Fixed(200.0)) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.weak.color)), + border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() }, + ..Default::default() + }) + .into() +} + +fn list_row<'a>( + palette: &CountPalette, + name: &str, + key: CountKey, + label: String, + count: String, + errors: bool, + depth: u16, + expanded: Option, +) -> Element<'a, Message> { + let lead: Element<'a, Message> = if let Some(checked) = palette.table.as_ref().filter(|_| depth == 0) { + let on = checked.contains(&name.to_ascii_uppercase()); + 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() + } else { + Space::new().width(Length::Fixed(14.0)).into() + }; + let mut cells = row![Space::new().width(Length::Fixed(f32::from(depth) * 18.0)), lead]; + if depth == 0 { + cells = cells.push(small_icon(BLOCK_ICON, 14.0)); + } + cells = cells.push(text(label).size(12).width(Fill)); + if errors { + cells = cells.push(crate::ui::icons::themed_warning(WARNING_ICON, 12.0)); + } + cells = cells.push(text(count).size(12)); + 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()))) + .interaction(iced::mouse::Interaction::Pointer); + let (n, expandable) = (name.to_string(), depth == 0); + iced_aw::ContextMenu::new(area, move || row_menu(n.clone(), key.clone(), expandable)).into() +} + +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)) + .on_press(msg(m)) + .style(button::text) + .padding([2, 4]) + .into() +} + +/// The list: search, area button, Name / Count columns, rows, Create Table. +fn list_view<'a>(palette: &'a CountPalette, instances: &[BlockInstance]) -> Element<'a, Message> { + let search = text_input(&crate::t!("Search"), &palette.search) + .on_input(|v| msg(CountMsg::Search(v))) + .padding([4, 8]) + .size(12) + .style(field_style); + let top = row![ + search.width(Fill), + crate::ui::dock::tool_button( + small_icon(AREA_ICON, crate::ui::dock::TOOL_H), + crate::t!("Count in a specified area").into_owned(), + msg(CountMsg::Area), + ), + ] + .spacing(4) + .align_y(iced::Center); + let header = row![ + header_button(crate::t!("Name").into_owned(), !palette.by_count, palette.descending, CountMsg::Sort(false)), + Space::new().width(Fill), + header_button(crate::t!("Count column").into_owned(), palette.by_count, palette.descending, CountMsg::Sort(true)), + ] + .align_y(iced::Center); + 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() }; + list = list.push(list_row( + palette, + &r.name, + CountKey::default(), + r.name.clone(), + count, + r.errors > 0, + 0, + Some(expansion.is_some()), + )); + 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)); + } + } + } + let footer: Element<'a, Message> = match &palette.table { + None => row![ + Space::new().width(Fill), + button(text(crate::t!("Create Table")).size(12)) + .on_press(msg(CountMsg::CreateTable)) + .style(button_style(false)) + .padding([5, 12]), + ] + .into(), + Some(checked) => { + let all = rows(instances, palette); + let every = !all.is_empty() && all.iter().all(|r| checked.contains(&r.name.to_ascii_uppercase())); + let mut insert = button(text(crate::t!("Insert")).size(12)).style(button_style(true)).padding([5, 12]); + if !checked.is_empty() { + insert = insert.on_press(msg(CountMsg::TableInsert)); + } + row![ + checkbox(every).size(14).on_toggle(|v| msg(CountMsg::TableAll(v))), + Space::new().width(Fill), + button(text(crate::t!("Cancel")).size(12)) + .on_press(msg(CountMsg::TableCancel)) + .style(button_style(false)) + .padding([5, 12]), + insert, + ] + .spacing(6) + .align_y(iced::Center) + .into() + } + }; + column![ + top, + container(column![header, scrollable(list).height(Fill)].spacing(2)) + .padding(4) + .height(Fill) + .style(card), + footer, + ] + .spacing(6) + .into() +} + +fn card(theme: &Theme) -> container::Style { + container::Style { + background: Some(Background::Color(theme.palette().background.weak.color)), + border: Border { radius: 4.0.into(), ..Default::default() }, + ..Default::default() + } +} + +/// Count mode in the palette: back to the list, the counted block with the +/// match options, and the error report. +fn mode_view<'a>(palette: &'a CountPalette, mode: &'a CountMode, doc: &CadDocument) -> Element<'a, Message> { + let back = button(row![crate::ui::icons::themed_arrow_left(10.0), text(crate::t!("Back to List")).size(12)].spacing(4).align_y(iced::Center)) + .on_press(msg(CountMsg::Back)) + .style(button::text) + .padding([2, 2]); + let result = mode.result(doc); + let mut body = column![back].spacing(8); + if let Some(name) = mode.target_name() { + let mut details = column![button( + row![ + crate::ui::icons::themed_success(CHECK_ICON, 12.0), + text(format!("{name}: {}", result.counted.len())).size(12).width(Fill), + ] + .spacing(6) + .align_y(iced::Center), + ) + .on_press(msg(CountMsg::ToggleDetails)) + .style(button::text) + .padding(0)] + .spacing(6); + if let (false, Some(CountTarget::Block { matching, .. })) = (palette.details_closed, &mode.target) { + for (k, label) in ["Match Layer", "Match Scale", "Match Mirror State"].iter().enumerate() { + details = details.push( + checkbox(matching[k]) + .label(crate::t!(*label).into_owned()) + .text_size(12) + .size(14) + .on_toggle(move |v| msg(CountMsg::Match(k, v))), + ); + } + } + body = body.push(container(details).padding(8).width(Fill).style(card)); + } + if !result.errors.is_empty() { + let mut report = column![button( + row![ + crate::ui::icons::themed_warning(WARNING_ICON, 12.0), + text(crate::tf!("Count Error Report: {}", result.errors.len())).size(12).width(Fill), + ] + .spacing(6) + .align_y(iced::Center), + ) + .on_press(msg(CountMsg::ToggleErrors)) + .style(button::text) + .padding(0)] + .spacing(4); + if !palette.errors_closed { + for h in &result.errors { + report = report.push( + button(text(crate::t!("Overlapping object")).size(12)) + .on_press(msg(CountMsg::ShowError(*h))) + .style(button::text) + .padding([2, 18]), + ); + } + } + body = body.push(container(report).padding(8).width(Fill).style(card)); + } + body.into() +} + +pub fn view<'a>( + palette: &'a CountPalette, + mode: Option<&'a CountMode>, + doc: &'a CadDocument, + width: f32, + auto_collapse: bool, +) -> Element<'a, Message> { + let title_bar = crate::ui::dock::title_bar(PanelId::Count, crate::t!("Count").into_owned(), auto_collapse); + // ponytail: the list is recounted on every view; cache by geometry epoch + // if very large drawings make the palette slow. + let body = match mode { + Some(mode) if mode.target.is_some() => mode_view(palette, mode, doc), + _ => { + let instances = codec::count::block_instances(doc, mode.and_then(|m| m.area.as_deref())); + list_view(palette, &instances) + } + }; + crate::ui::dock::frame(column![title_bar, body].spacing(6), width) +} + +fn bar_button<'a>(icon: Element<'a, Message>, tip: String, m: Option) -> Element<'a, Message> { + let mut b = button(icon).style(button::text).padding([3, 5]); + if let Some(m) = m { + b = b.on_press(msg(m)); + } + tooltip(b, text(tip).size(10), tooltip::Position::Bottom).gap(4).into() +} + +/// The floating toolbar over the drawing in count mode: the count, the +/// status, previous / next reference, area, select, field and close. +pub fn toolbar<'a>(mode: &CountMode, doc: &CadDocument) -> Element<'a, Message> { + let has_target = mode.target.is_some(); + let result = mode.result(doc); + let count = if has_target { result.counted.len().to_string() } else { "-".to_string() }; + 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() + } else { + crate::ui::icons::themed_disabled(WARNING_ICON, 14.0) + }; + let on = |m: CountMsg| has_target.then_some(m); + let icon = |bytes: &'static [u8]| -> Element<'a, Message> { + if has_target { + crate::ui::icons::themed_success(bytes, 16.0) + } else { + crate::ui::icons::themed_disabled(bytes, 16.0) + } + }; + let bar = row![ + text(crate::tf!("Count: {}", count)).size(13), + status, + bar_button(crate::ui::icons::themed_arrow_left(12.0), crate::t!("Previous").into_owned(), on(CountMsg::Prev)), + bar_button(crate::ui::icons::themed_arrow_right(12.0), crate::t!("Next").into_owned(), on(CountMsg::Next)), + bar_button(crate::ui::icons::themed_success(AREA_ICON, 16.0), crate::t!("Count in a specified area").into_owned(), Some(CountMsg::Area)), + bar_button(crate::ui::icons::themed_success(SELECT_ICON, 16.0), crate::t!("Select objects").into_owned(), Some(CountMsg::Select)), + bar_button(icon(TABLE_ICON), crate::t!("Insert Count Field").into_owned(), on(CountMsg::Field)), + bar_button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0), crate::t!("Close").into_owned(), Some(CountMsg::Close)), + ] + .spacing(6) + .align_y(iced::Center); + let bar = container(bar).padding([4, 10]).style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.weak.color)), + border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() }, + ..Default::default() + }); + container(iced::widget::opaque(bar)).width(Fill).align_x(iced::Center).padding([10, 0]).into() +} + +/// The blue frame around the drawing area in count mode. +pub fn border<'a>() -> Element<'a, Message> { + container(Space::new().width(Fill).height(Fill)) + .width(Fill) + .height(Fill) + .style(|_: &Theme| container::Style { + border: Border { color: Color::from_rgb(0.18, 0.52, 0.95), width: 2.0, radius: 0.0.into() }, + ..Default::default() + }) + .into() +} + +/// A colour index as the display colour (`COUNTCOLOR` / `COUNTERRORCOLOR`). +pub fn aci_rgba(index: i16) -> [f32; 4] { + crate::scene::convert::tess_util::aci_to_rgba(&codec::types::Color::from_index(index)) +} + diff --git a/src/ui/window/field_dialog.rs b/src/ui/window/field_dialog.rs index 0d76b6816..54dbece83 100644 --- a/src/ui/window/field_dialog.rs +++ b/src/ui/window/field_dialog.rs @@ -32,7 +32,7 @@ pub const CATEGORIES: &[(&str, &[&str])] = &[ &["Author", "Comments", "Filename", "Filesize", "HyperlinkBase", "Keywords", "LastSavedBy", "Subject", "Title"], ), ("Linked", &["Hyperlink"]), - ("Objects", &["Formula", "NamedObject", "Object"]), + ("Objects", &["Count", "CountInArea", "Formula", "NamedObject", "Object"]), ("Other", &["DieselExpression", "SystemVariable"]), ( "Plot", @@ -77,6 +77,8 @@ pub enum FieldKind { Formula, Hyperlink, PlotScale, + /// Count / CountInArea: a JSON expression. + Count, } /// Formula precisions: the current one, then 0 to 8 decimals. @@ -95,6 +97,7 @@ pub fn kind_of(name: &str) -> FieldKind { "Formula" => FieldKind::Formula, "Hyperlink" => FieldKind::Hyperlink, "PlotScale" => FieldKind::PlotScale, + "Count" | "CountInArea" => FieldKind::Count, _ => FieldKind::Text, } } @@ -128,6 +131,8 @@ pub struct FieldDialogState { pub hyperlink_url: String, /// Index into codec PLOT_SCALE_FORMATS. pub plot_scale: usize, + /// The JSON expression of a Count / CountInArea field. + pub count_expression: String, pub preview: String, /// Today in each of DATE_FORMATS, for the Examples list. pub examples: Vec, @@ -158,6 +163,7 @@ impl FieldDialogState { hyperlink_text: String::new(), hyperlink_url: String::new(), plot_scale: 0, + count_expression: String::new(), preview: String::new(), examples: Vec::new(), } @@ -211,6 +217,13 @@ impl FieldDialogState { .map_or_else(|| "\\AcVar PlotScale".to_string(), |f| f.1.to_string()), vec![], ), + FieldKind::Count => { + let evaluator = if self.name == "CountInArea" { "AcCount2" } else { "AcCount" }; + match self.count_expression.trim() { + "" => (format!("\\{evaluator}"), vec![]), + json => (format!("\\{evaluator} {json}"), vec![]), + } + } FieldKind::SystemVariable => (format!("\\AcVar {}", self.sysvar), vec![]), FieldKind::Diesel => (format!("\\AcDiesel {}", self.diesel), vec![]), FieldKind::NamedObject => match self.named.and_then(|i| self.named_names.get(i)) { @@ -278,6 +291,8 @@ pub enum FieldDialogMsg { HyperlinkUrl(String), BrowseHyperlink, PlotScale(usize), + CountExpression(String), + ShowCountInstances, Help, Ok, } @@ -542,6 +557,7 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> { ] .spacing(8) .into(), + FieldKind::Count => column![].into(), FieldKind::Object => column![ row![ text(t!("Object type")).size(12).width(Length::Fixed(96.0)), @@ -629,6 +645,33 @@ pub fn view<'a>( ] .spacing(10), FieldKind::Hyperlink => column![card(t!("Hyperlink").into_owned(), format_panel(state)), preview].spacing(10), + FieldKind::Count => column![ + card( + t!("Expression").into_owned(), + column![ + text_input("", &state.count_expression) + .size(12) + .padding([8, 8]) + .font(iced::Font::MONOSPACE) + .style(field_style) + .on_input(|v| msg(FieldDialogMsg::CountExpression(v))), + row![ + button(text(t!("Evaluate")).size(12)) + .on_press(msg(FieldDialogMsg::Evaluate)) + .style(button_style(false)) + .padding([5, 14]), + button(text(t!("Show Count Instances")).size(12)) + .on_press(msg(FieldDialogMsg::ShowCountInstances)) + .style(button_style(false)) + .padding([5, 14]), + ] + .spacing(8), + ] + .spacing(8), + ), + preview, + ] + .spacing(10), _ => column![card(t!("Format").into_owned(), format_panel(state)), preview].spacing(10), }; let (code, _) = state.code(); diff --git a/src/ui/window/mod.rs b/src/ui/window/mod.rs index 3b3282d0f..3f3fafc09 100644 --- a/src/ui/window/mod.rs +++ b/src/ui/window/mod.rs @@ -27,6 +27,7 @@ pub mod alias_editor; pub mod find_replace; pub mod named_parameters; pub mod pc_manager; +pub mod count_palette; pub mod pdf_dialogs; pub mod xref_attach; pub mod xref_help; From f76b6cb3bb0d4b58c8d59e36761aefda93e28533 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:53:28 +0300 Subject: [PATCH 02/13] WIP count: automation, locales --- locales/ar-SA/opencadstudio.ftl | 17 ++++++++ locales/bg-BG/opencadstudio.ftl | 17 ++++++++ locales/cs-CZ/opencadstudio.ftl | 17 ++++++++ locales/de-DE/opencadstudio.ftl | 17 ++++++++ locales/el-GR/opencadstudio.ftl | 17 ++++++++ locales/en-US/opencadstudio.ftl | 17 ++++++++ locales/es-ES/opencadstudio.ftl | 17 ++++++++ locales/fi-FI/opencadstudio.ftl | 17 ++++++++ locales/fr-FR/opencadstudio.ftl | 17 ++++++++ locales/hi-IN/opencadstudio.ftl | 17 ++++++++ locales/hu-HU/opencadstudio.ftl | 17 ++++++++ locales/it-IT/opencadstudio.ftl | 17 ++++++++ locales/ja-JP/opencadstudio.ftl | 17 ++++++++ locales/ko-KR/opencadstudio.ftl | 17 ++++++++ locales/nl-NL/opencadstudio.ftl | 17 ++++++++ locales/pl-PL/opencadstudio.ftl | 17 ++++++++ locales/pt-BR/opencadstudio.ftl | 17 ++++++++ locales/ru-RU/opencadstudio.ftl | 17 ++++++++ locales/tr-TR/opencadstudio.ftl | 17 ++++++++ locales/zh-CN/opencadstudio.ftl | 17 ++++++++ locales/zh-TW/opencadstudio.ftl | 17 ++++++++ src/app/control/actions.rs | 69 +++++++++++++++++++++++++++++++ src/locale_catalog.rs | 16 +++++++ src/modules/view/count_palette.rs | 2 +- src/ui/window/count_palette.rs | 4 +- 25 files changed, 445 insertions(+), 3 deletions(-) diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 97fec3890..1a8715e9d 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -5930,3 +5930,20 @@ style-manager = .center-of-text-overline = مركز النص مع خط علوي .block-extents = نطاق الكتلة .base-point = نقطة الأساس +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 0fb1d43e9..814d0f4d4 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -5918,3 +5918,20 @@ style-manager = .center-of-text-overline = Център на надчертан текст .block-extents = Обхват на блок .base-point = Базова точка +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index d17ecfb1d..8613591b1 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -5915,3 +5915,20 @@ style-manager = .center-of-text-overline = Střed textu s přeškrtnutím .block-extents = Blokovat rozsahy .base-point = Základní bod +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 7fd9f0bd4..c05f97f7e 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -5916,3 +5916,20 @@ style-manager = .center-of-text-overline = Textzentrum mit Oberstrich .block-extents = Blockgrenzen .base-point = Basispunkt +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 53601f4be..d39f35443 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -5918,3 +5918,20 @@ style-manager = .center-of-text-overline = Κέντρο του κειμένου με επιγράμμιση .block-extents = Όρια μπλοκ .base-point = Σημείο βάσης +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index dcbbb7b69..d6ba4c9c1 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -5919,3 +5919,20 @@ style-manager = .center-of-text-overline = Center of text with overline .block-extents = Block extents .base-point = Base point +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index df1b19661..0ad5c4630 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -5918,3 +5918,20 @@ style-manager = .center-of-text-overline = Centro del texto con suprarrayado .block-extents = Extensión del bloque .base-point = Punto base +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 99f18d514..7f43aa3fc 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -5915,3 +5915,20 @@ style-manager = .center-of-text-overline = Tekstin keskikohta yliviivauksella .block-extents = Estä laajuudet .base-point = Peruspiste +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index bc00d9334..d634008d5 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -5917,3 +5917,20 @@ style-manager = .center-of-text-overline = Centre du texte avec surlignement .block-extents = Limites du bloc .base-point = Point de base +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 59440e35e..e0f26ae1e 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -5906,3 +5906,20 @@ style-manager = .center-of-text-overline = ऊपरी रेखा सहित टेक्स्ट का केंद्र .block-extents = ब्लॉक परिमिति .base-point = आधार बिंदु +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 2066c5156..9df1e87b6 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -5917,3 +5917,20 @@ style-manager = .center-of-text-overline = Szöveg közepe áthúzással .block-extents = Kiterjedések blokkolása .base-point = Alappont +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index bab01a81c..01c52fbb7 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -5917,3 +5917,20 @@ style-manager = .center-of-text-overline = Centro del testo con sopralineato .block-extents = Blocca estensioni .base-point = Punto base +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index f0df48710..88aaa76ca 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -5906,3 +5906,20 @@ style-manager = .center-of-text-overline = 上線付きテキストの中心 .block-extents = ブロック範囲 .base-point = 基点 +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 6ad109cc7..599abf44d 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -5917,3 +5917,20 @@ style-manager = .center-of-text-overline = 윗줄이 있는 텍스트 중앙 .block-extents = 블록 범위 .base-point = 기준점 +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 2f5e39e39..9694b0e11 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -5916,3 +5916,20 @@ style-manager = .center-of-text-overline = Tekstmiddelpunt met bovenstreep .block-extents = Symboolgrenzen .base-point = Basispunt +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index c31421220..dad85cf06 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -5917,3 +5917,20 @@ style-manager = .center-of-text-overline = Środek tekstu z podkreśleniem .block-extents = Zasięg bloku .base-point = Punkt bazowy +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index e96dea04d..820c37af5 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -5918,3 +5918,20 @@ style-manager = .center-of-text-overline = Centro do texto com sobrelinha .block-extents = Extensões do bloco .base-point = Ponto base +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index e3eb50fa6..f6fdb4c9c 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -5919,3 +5919,20 @@ style-manager = .center-of-text-overline = Центр текста с надчёркиванием .block-extents = Габариты блока .base-point = Базовая точка +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 3b01d7c2c..ab1501909 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -5841,3 +5841,20 @@ style-manager = .center-of-text-overline = Üst çizgili metin merkezi .block-extents = Blok sınırları .base-point = Taban noktası +count-feature = + .palette = Sayım + .menu = Say + .selection = Seçimi say + .review = Sayım ayrıntılarını incele + .insert-field = Sayım alanı ekle + .expand-by = GENİŞLET + .mirror-state = Ayna durumu + .area = Belirli bir alanda say + .create-table = Tablo Oluştur + .back = Listeye dön + .match-scale = Ölçeği Eşleştir + .match-mirror = Ayna Durumunu Eşleştir + .error-report = Sayım hata raporu: __ocs_fmt_0__ + .toolbar-count = Sayım: __ocs_fmt_0__ + .overlap = Üst üste binen nesne + .show-instances = Örnekleri göster diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 290f64be0..770c2600c 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -5901,3 +5901,20 @@ style-manager = .center-of-text-overline = 带上划线的文字中心 .block-extents = 块范围 .base-point = 基点 +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index c38cc4a04..b32682a8c 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -5913,3 +5913,20 @@ style-manager = .center-of-text-overline = 帶上划線的文字中心 .block-extents = 圖塊範圍 .base-point = 基點 +count-feature = + .palette = Count + .menu = Count + .selection = Count Selection + .review = Review Count Details + .insert-field = Insert Count Field + .expand-by = EXPAND BY + .mirror-state = Mirror State + .area = Count in a specified area + .create-table = Create Table + .back = Back to List + .match-scale = Match Scale + .match-mirror = Match Mirror State + .error-report = Count Error Report: __ocs_fmt_0__ + .toolbar-count = Count: __ocs_fmt_0__ + .overlap = Overlapping object + .show-instances = Show Count Instances diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index 671a08b77..e39367343 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -73,6 +73,7 @@ pub(super) const NAMES: &[&str] = &[ "pdf_layer_toggle", "pc_manager_toggle", "pc_manager", + "count_palette", "blocks_palette", "pdf_page_select", "pc_colormap", @@ -813,6 +814,74 @@ impl OpenCADStudio { } // Point Cloud Manager tree: "search=", "collapse", "expand", // "toggle_node=" or "select=". + // Count palette and count mode: "search=", "sort=name|count", + // "open=NAME", "child=NAME:" (an expanded row), "menu=:NAME", "create_table", "check=NAME:1", + // "all=1", "table_cancel", "table_insert", "back", "match=<0|1|2>:1", + // "details", "errors", "error=", "area", "prev", "next", "select", + // "field", "close". + "count_palette" => { + use crate::ui::window::count_palette::{CountMsg as M, RowAction}; + let value = string(req, "value")?; + let (key, arg) = value.split_once('=').unwrap_or((value, "")); + let flag = |s: &str| s == "1" || s.eq_ignore_ascii_case("true"); + let bad = || failure("invalid_value", "Unknown count palette edit"); + Message::Count(match key { + "search" => M::Search(arg.into()), + "sort" => M::Sort(arg == "count"), + "open" => M::Open(arg.into(), codec::count::CountKey::default()), + "child" => { + let (name, n) = arg.rsplit_once(':').ok_or_else(bad)?; + let n: usize = n.parse().map_err(|_| bad())?; + let [l, s, m] = self.count_palette.expansion(name).ok_or_else(bad)?; + let i = self.active_tab; + let area = self.tabs[i].count.as_ref().and_then(|c| c.area.clone()); + let instances = codec::count::block_instances(&self.tabs[i].scene.document, area.as_deref()); + let (k, _) = codec::count::expanded_counts(&instances, name, l, s, m).into_iter().nth(n).ok_or_else(bad)?; + M::Open(name.into(), k) + } + "menu" => { + let (action, name) = arg.split_once(':').ok_or_else(bad)?; + let action = match action { + "review" => RowAction::Review, + "field" => RowAction::Field, + "layer" => RowAction::Expand(0), + "scale" => RowAction::Expand(1), + "mirror" => RowAction::Expand(2), + _ => return Err(bad()), + }; + M::Menu(name.into(), codec::count::CountKey::default(), action) + } + "create_table" => M::CreateTable, + "check" => { + let (name, on) = arg.rsplit_once(':').unwrap_or((arg, "1")); + M::TableCheck(name.into(), flag(on)) + } + "all" => M::TableAll(flag(arg)), + "table_cancel" => M::TableCancel, + "table_insert" => M::TableInsert, + "back" => M::Back, + "match" => { + let (k, on) = arg.split_once(':').unwrap_or((arg, "1")); + M::Match(k.parse::().ok().filter(|k| *k < 3).ok_or_else(bad)?, flag(on)) + } + "details" => M::ToggleDetails, + "errors" => M::ToggleErrors, + "error" => { + let i = self.active_tab; + let n: usize = arg.parse().map_err(|_| bad())?; + let mode = self.tabs[i].count.as_ref().ok_or_else(bad)?; + M::ShowError(*mode.result(&self.tabs[i].scene.document).errors.get(n).ok_or_else(bad)?) + } + "area" => M::Area, + "prev" => M::Prev, + "next" => M::Next, + "select" => M::Select, + "field" => M::Field, + "close" => M::Close, + _ => return Err(bad()), + }) + } "pc_manager" => { use crate::ui::window::pc_manager::PcManagerMsg as M; let value = string(req, "value")?; diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 4ec7f0109..abf76c35e 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -5521,6 +5521,22 @@ 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")), + "EXPAND BY" => Some(("count-feature", "expand-by")), + "Mirror State" => Some(("count-feature", "mirror-state")), + "Count in a specified area" => Some(("count-feature", "area")), + "Create Table" => Some(("count-feature", "create-table")), + "Back to List" => Some(("count-feature", "back")), + "Match Scale" => Some(("count-feature", "match-scale")), + "Match Mirror State" => Some(("count-feature", "match-mirror")), + "Count Error Report: {}" => Some(("count-feature", "error-report")), + "Count: {}" => Some(("count-feature", "toolbar-count")), + "Overlapping object" => Some(("count-feature", "overlap")), + "Show Count Instances" => Some(("count-feature", "show-instances")), _ => None, } } diff --git a/src/modules/view/count_palette.rs b/src/modules/view/count_palette.rs index 97321536f..fd1d4e652 100644 --- a/src/modules/view/count_palette.rs +++ b/src/modules/view/count_palette.rs @@ -4,7 +4,7 @@ pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/d pub fn tool() -> ToolDef { ToolDef { id: "COUNTLIST", - label: "Count", + label: "Count palette", icon: ICON, event: ModuleEvent::Command("_COUNTLISTTOGGLE".to_string()), } diff --git a/src/ui/window/count_palette.rs b/src/ui/window/count_palette.rs index cfd74c2bb..7da66744c 100644 --- a/src/ui/window/count_palette.rs +++ b/src/ui/window/count_palette.rs @@ -334,7 +334,7 @@ fn list_view<'a>(palette: &'a CountPalette, instances: &[BlockInstance]) -> Elem let header = row![ header_button(crate::t!("Name").into_owned(), !palette.by_count, palette.descending, CountMsg::Sort(false)), Space::new().width(Fill), - header_button(crate::t!("Count column").into_owned(), palette.by_count, palette.descending, CountMsg::Sort(true)), + header_button(crate::t!("Count").into_owned(), palette.by_count, palette.descending, CountMsg::Sort(true)), ] .align_y(iced::Center); let mut list = column![].spacing(1); @@ -478,7 +478,7 @@ pub fn view<'a>( width: f32, auto_collapse: bool, ) -> Element<'a, Message> { - let title_bar = crate::ui::dock::title_bar(PanelId::Count, crate::t!("Count").into_owned(), auto_collapse); + let title_bar = crate::ui::dock::title_bar(PanelId::Count, crate::t!("Count palette").into_owned(), auto_collapse); // ponytail: the list is recounted on every view; cache by geometry epoch // if very large drawings make the palette slow. let body = match mode { From 547b89b5e75de2934fff7bde1dc88ac7974eb7f6 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:10:12 +0300 Subject: [PATCH 03/13] Count: group count, list with an area, field dialog automation --- src/app/commands/count.rs | 11 +++++++---- src/app/control/actions.rs | 2 ++ src/ui/window/count_palette.rs | 13 +++++++++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/app/commands/count.rs b/src/app/commands/count.rs index 4a30a0c10..754b6db80 100644 --- a/src/app/commands/count.rs +++ b/src/app/commands/count.rs @@ -183,7 +183,7 @@ impl OpenCADStudio { .count .as_ref() .filter(|m| m.target.is_some()) - .map(|m| m.result(&self.tabs[i].scene.document).counted.len()) + .map(|m| m.count(&self.tabs[i].scene.document)) .unwrap_or(0), ), "COUNTPALETTESTATE" => Some(self.count_palette.show as usize), @@ -374,9 +374,12 @@ impl OpenCADStudio { .collect(), _ => Vec::new(), }; - if targets.is_empty() && matches!(area_tokens.first(), Some(&"C") | Some(&"E") | None) { + // List all blocks: the palette, counting in the area when one was drawn. + if targets.is_empty() { self.set_count_palette(true); - return; + if matches!(area_tokens.first(), Some(&"C") | Some(&"E") | None) { + return; + } } let Some((area, boundary)) = self.resolve_count_area(i, area_tokens) else { return; @@ -403,7 +406,7 @@ impl OpenCADStudio { } let mode = CountMode { area, boundary, target, cursor: None, epoch: 0 }; if let Some(name) = mode.target_name() { - let n = mode.result(&self.tabs[i].scene.document).counted.len(); + let n = mode.count(&self.tabs[i].scene.document); self.command_line.push_output(&format!("{name} ...... {n}")); } self.tabs[i].count = Some(mode); diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index e39367343..e49f72008 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -1088,6 +1088,8 @@ impl OpenCADStudio { "link_text" => F::HyperlinkText(v.into()), "link_url" => F::HyperlinkUrl(v.into()), "plot_scale" => F::PlotScale(index(codec::fields::PLOT_SCALE_FORMATS.len())?), + "count" => F::CountExpression(v.into()), + "show_instances" => F::ShowCountInstances, _ => return Err(failure("invalid_value", "Unknown field dialog key")), }) } diff --git a/src/ui/window/count_palette.rs b/src/ui/window/count_palette.rs index 7da66744c..877ad92c0 100644 --- a/src/ui/window/count_palette.rs +++ b/src/ui/window/count_palette.rs @@ -96,6 +96,15 @@ impl CountMode { } } + /// What the count shows: the counted references, or 1 for a group whose + /// objects all exist. + pub fn count(&self, doc: &CadDocument) -> usize { + match &self.target { + Some(CountTarget::Group(handles)) => usize::from(handles.iter().all(|h| doc.get_entity(*h).is_some())), + _ => self.result(doc).counted.len(), + } + } + /// The name the toolbar and the command line show for the target. pub fn target_name(&self) -> Option { match &self.target { @@ -421,7 +430,7 @@ fn mode_view<'a>(palette: &'a CountPalette, mode: &'a CountMode, doc: &CadDocume let mut details = column![button( row![ crate::ui::icons::themed_success(CHECK_ICON, 12.0), - text(format!("{name}: {}", result.counted.len())).size(12).width(Fill), + text(format!("{name}: {}", mode.count(doc))).size(12).width(Fill), ] .spacing(6) .align_y(iced::Center), @@ -504,7 +513,7 @@ fn bar_button<'a>(icon: Element<'a, Message>, tip: String, m: Option) pub fn toolbar<'a>(mode: &CountMode, doc: &CadDocument) -> Element<'a, Message> { let has_target = mode.target.is_some(); let result = mode.result(doc); - let count = if has_target { result.counted.len().to_string() } else { "-".to_string() }; + let count = if has_target { mode.count(doc).to_string() } else { "-".to_string() }; let status: Element<'a, Message> = if !result.errors.is_empty() { crate::ui::icons::themed_warning(WARNING_ICON, 14.0) } else if has_target { From cfc762de7435b5e58f82d6ba2460f3c25ff99dc6 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:12:38 +0300 Subject: [PATCH 04/13] Cargo: codec 6d9c604 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ea12cedbc..f805f10db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ block2 = "0.5" objc2-foundation = { version = "0.2", features = ["NSArray", "NSURL", "NSString", "NSObject", "NSNotification", "NSThread", "NSEnumerator"] } [patch."https://github.com/HakanSeven12/opencadcodec.git"] -opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "94095f7" } +opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "6d9c604" } [patch.crates-io] iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } From d830e40bfa7ca180868afbf9261ae2e0726697ee Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:12:00 +0300 Subject: [PATCH 05/13] Count: overlapping pair in the error colour, targets deselected, locked area layer Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/app/commands/count.rs | 10 +++++++--- src/ui/window/count_palette.rs | 4 ++++ 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f58675e1..d77d98502 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4206,7 +4206,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.5.5" -source = "git+https://github.com/ramox81/cadcodec.git?rev=6d9c604#6d9c604dca78ed2f631e0f5b703415ea1f13b0db" +source = "git+https://github.com/ramox81/cadcodec.git?rev=48b86d0#48b86d08c895f07b53753888363d1d9a6b282305" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 361a526a2..51be8093f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ block2 = "0.5" objc2-foundation = { version = "0.2", features = ["NSArray", "NSURL", "NSString", "NSObject", "NSNotification", "NSThread", "NSEnumerator"] } [patch."https://github.com/HakanSeven12/opencadcodec.git"] -opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "6d9c604" } +opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "48b86d0" } [patch.crates-io] iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } diff --git a/src/app/commands/count.rs b/src/app/commands/count.rs index 754b6db80..101c21c65 100644 --- a/src/app/commands/count.rs +++ b/src/app/commands/count.rs @@ -253,7 +253,7 @@ impl OpenCADStudio { let r = m.result(&self.tabs[i].scene.document); crate::scene::CountDisplay { counted: r.counted.into_iter().collect(), - errors: r.errors.into_iter().collect(), + errors: r.errors.into_iter().chain(r.overlapped).collect(), color: aci_rgba(self.count_palette.color), error_color: aci_rgba(self.count_palette.error_color), } @@ -287,7 +287,8 @@ impl OpenCADStudio { 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"); - self.tabs[i].scene.erase_entities(&[h]); + // The area layer is locked; count mode removes its own polyline. + self.tabs[i].scene.rollback_new_entities(&[h]); self.tabs[i].dirty = true; } } @@ -335,7 +336,7 @@ impl OpenCADStudio { } } - /// A closed polyline on `0-CountArea` (colour 152, not plotted, + /// A closed polyline on `0-CountArea` (locked, colour 152, not plotted, /// Continuous), whatever the current layer. fn draw_count_area(&mut self, i: usize, ring: &[[f64; 2]]) -> Option { self.push_undo_snapshot(i, "COUNT"); @@ -345,6 +346,7 @@ impl OpenCADStudio { layer.handle = doc.allocate_handle(); layer.color = codec::types::Color::from_index(152); layer.is_plottable = false; + layer.flags.locked = true; layer.line_type = "Continuous".into(); let _ = doc.layers.add(layer); } @@ -400,6 +402,8 @@ impl OpenCADStudio { None if targets.is_empty() => None, _ => Some(CountTarget::Group(targets.clone())), }; + // The picked targets do not stay selected. + self.tabs[i].scene.deselect_all(); let old = self.tabs[i].count.take(); if let Some(old) = old.as_ref().filter(|o| o.boundary != boundary) { self.drop_count_boundary(i, old.boundary); diff --git a/src/ui/window/count_palette.rs b/src/ui/window/count_palette.rs index 877ad92c0..1c33dc886 100644 --- a/src/ui/window/count_palette.rs +++ b/src/ui/window/count_palette.rs @@ -58,6 +58,8 @@ pub struct CountMode { pub struct CountResult { pub counted: Vec, pub errors: Vec, + /// The counted references a duplicate overlaps (drawn in the error colour too). + pub overlapped: Vec, } impl CountMode { @@ -83,6 +85,7 @@ impl CountMode { Some(CountTarget::Group(handles)) => CountResult { counted: handles.iter().copied().filter(|h| doc.get_entity(*h).is_some()).collect(), errors: Vec::new(), + overlapped: Vec::new(), }, Some(CountTarget::Block { name, .. }) => { let instances = self.instances(doc); @@ -91,6 +94,7 @@ impl CountMode { CountResult { counted: instances.iter().filter(mine).filter(|i| i.duplicate_of.is_none()).map(|i| i.handle).collect(), errors: instances.iter().filter(mine).filter(|i| i.duplicate_of.is_some()).map(|i| i.handle).collect(), + overlapped: instances.iter().filter(mine).filter_map(|i| i.duplicate_of).collect(), } } } From 0a26709d139b629205520cd3958f74bf93db354a Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:36:24 +0300 Subject: [PATCH 06/13] Count: measured messages and listing, groups, Current area, boundary checks, fills and solids recoloured - COUNTCOLOR / COUNTERRORCOLOR / COUNTSERVICE reject any other value with the range message; COUNTTABLE ? lists in the reference order, padded, 22 lines a page with Press ENTER to continue:, then the totals. - Target picks report N found / N found, M total; several objects (or one that is not a block reference) count their copies, shown as Geometries in the palette. - Current area is the area in use (kept aside by Entire model space), otherwise all of model space; an invalid Object boundary ends with the reference message. - Count colours reach solids and fills; the palette list and the count are recomputed only when the drawing changes. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- locales/ar-SA/opencadstudio.ftl | 1 + locales/bg-BG/opencadstudio.ftl | 1 + locales/cs-CZ/opencadstudio.ftl | 1 + locales/de-DE/opencadstudio.ftl | 1 + locales/el-GR/opencadstudio.ftl | 1 + locales/en-US/opencadstudio.ftl | 1 + locales/es-ES/opencadstudio.ftl | 1 + locales/fi-FI/opencadstudio.ftl | 1 + locales/fr-FR/opencadstudio.ftl | 1 + locales/hi-IN/opencadstudio.ftl | 1 + locales/hu-HU/opencadstudio.ftl | 1 + locales/it-IT/opencadstudio.ftl | 1 + locales/ja-JP/opencadstudio.ftl | 1 + locales/ko-KR/opencadstudio.ftl | 1 + locales/nl-NL/opencadstudio.ftl | 1 + locales/pl-PL/opencadstudio.ftl | 1 + locales/pt-BR/opencadstudio.ftl | 1 + locales/ru-RU/opencadstudio.ftl | 1 + locales/tr-TR/opencadstudio.ftl | 1 + locales/zh-CN/opencadstudio.ftl | 1 + locales/zh-TW/opencadstudio.ftl | 1 + src/app/commands/count.rs | 48 +++++++++++------ src/app/update/file.rs | 2 +- src/app/view/mod.rs | 1 + src/locale_catalog.rs | 1 + src/modules/annotate/count_cmd.rs | 87 ++++++++++++++++++++++++------- src/scene/entity.rs | 2 + src/scene/mod.rs | 33 +++++++++++- src/ui/window/count_palette.rs | 75 +++++++++++++++++++------- 31 files changed, 214 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d77d98502..e869f168d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4206,7 +4206,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.5.5" -source = "git+https://github.com/ramox81/cadcodec.git?rev=48b86d0#48b86d08c895f07b53753888363d1d9a6b282305" +source = "git+https://github.com/ramox81/cadcodec.git?rev=178b596#178b596ba1606ce6d4ed719cd48551750a0f32a5" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 51be8093f..e5ef28bce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ block2 = "0.5" objc2-foundation = { version = "0.2", features = ["NSArray", "NSURL", "NSString", "NSObject", "NSNotification", "NSThread", "NSEnumerator"] } [patch."https://github.com/HakanSeven12/opencadcodec.git"] -opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "48b86d0" } +opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "178b596" } [patch.crates-io] iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index d8f9892f5..8a6956780 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -5955,3 +5955,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 4453f5bf9..331b7cba7 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -5943,3 +5943,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index d0a43de2c..4fafb4c8d 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -5940,3 +5940,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 96d321278..e2a2f8ae4 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -5941,3 +5941,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index db3cab564..95f7054d0 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -5943,3 +5943,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 51b717a8d..764b2f5c7 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -5944,3 +5944,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 3e7750281..5c228e77d 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -5943,3 +5943,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index a1d1ab170..b6b1cc717 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -5940,3 +5940,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 65bfde6e0..faaae44cd 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -5942,3 +5942,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 21035e9e7..a7efb14dc 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -5931,3 +5931,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 6906cabd4..fafcb7755 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -5942,3 +5942,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index f652cc7d6..5bb46d938 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -5942,3 +5942,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index d819a86ee..212923b58 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -5931,3 +5931,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 254a0f857..4c0f4307f 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -5942,3 +5942,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 105fe9086..62a0f0efc 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -5941,3 +5941,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 89854dd6f..73582eaf9 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -5942,3 +5942,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 395f9eef5..3c5d40863 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -5943,3 +5943,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 0c6c0735f..1a532105c 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -5944,3 +5944,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index faeb502e3..10943532b 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -5866,3 +5866,4 @@ count-feature = .toolbar-count = Sayım: __ocs_fmt_0__ .overlap = Üst üste binen nesne .show-instances = Örnekleri göster + .geometries = Geometriler: __ocs_fmt_0__ diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index ca77965ed..3fbd610ca 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -5926,3 +5926,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index d7013d976..fb702b25f 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -5938,3 +5938,4 @@ count-feature = .toolbar-count = Count: __ocs_fmt_0__ .overlap = Overlapping object .show-instances = Show Count Instances + .geometries = Geometries: __ocs_fmt_0__ diff --git a/src/app/commands/count.rs b/src/app/commands/count.rs index 101c21c65..94fd8a73e 100644 --- a/src/app/commands/count.rs +++ b/src/app/commands/count.rs @@ -216,7 +216,7 @@ impl OpenCADStudio { self.persist_settings_if_changed(); self.apply_count_display(i); } - Ok(_) => { + _ => { self.command_line.push_error(&if max == 1 { "Requires 0 or 1 only.".to_string() } else { @@ -224,10 +224,6 @@ impl OpenCADStudio { }); ask(self); } - Err(_) => { - self.command_line.push_error("Requires an integer value."); - ask(self); - } } } @@ -249,6 +245,10 @@ 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)); + 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 { @@ -278,6 +278,7 @@ impl OpenCADStudio { pub(in crate::app) fn close_count(&mut self, i: usize) { if let Some(mode) = self.tabs[i].count.take() { self.drop_count_boundary(i, mode.boundary); + self.drop_count_boundary(i, mode.remembered.and_then(|(_, b)| b)); } self.tabs[i].scene.set_count_display(None); } @@ -310,10 +311,13 @@ impl OpenCADStudio { let mode = self.tabs[i].count.as_ref(); Some((mode.and_then(|m| m.area.clone()), mode.and_then(|m| m.boundary))) } - "C" => { - let (x0, y0, x1, y1) = self.display_plot_window()?; - Some((Some(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1]]), None)) - } + // Current area: the count area in use (also one Entire model space + // set aside), else all of model space. + "C" => Some(match self.tabs[i].count.as_ref() { + Some(m) if m.area.is_some() => (m.area.clone(), m.boundary), + Some(m) => m.remembered.clone().map_or((None, None), |(a, b)| (Some(a), b)), + None => (None, None), + }), "R" => { let p = points(); let (a, b) = (p.first()?, p.get(1)?); @@ -404,11 +408,18 @@ impl OpenCADStudio { }; // The picked targets do not stay selected. self.tabs[i].scene.deselect_all(); - let old = self.tabs[i].count.take(); - if let Some(old) = old.as_ref().filter(|o| o.boundary != boundary) { - self.drop_count_boundary(i, old.boundary); + // Entire model space counts without the area but keeps it for a later + // Current area; a new area replaces it. + let mut remembered = None; + if let Some(old) = self.tabs[i].count.take() { + let old_area = old.area.map(|a| (a, old.boundary)).or(old.remembered); + if area.is_none() { + remembered = old_area; + } else if let Some((_, b)) = old_area.filter(|(_, b)| *b != boundary) { + self.drop_count_boundary(i, b); + } } - let mode = CountMode { area, boundary, target, cursor: None, epoch: 0 }; + let mode = CountMode { area, boundary, target, remembered, ..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}")); @@ -566,13 +577,16 @@ impl OpenCADStudio { self.tabs[i].dirty = true; } - /// Zoom to a reference (← / →, an error of the report). + /// 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(EntityType::Insert(ins)) = doc.get_entity(h) else { return }; - let Some(corners) = codec::count::insert_corners(doc, ins, 0) else { return }; + let Some(entity) = doc.get_entity(h) else { return }; + let outline = codec::count::entity_outline(doc, entity); + if outline.iter().all(|part| part.is_empty()) { + return; + } let (mut lo, mut hi) = (glam::Vec3::splat(f32::MAX), glam::Vec3::splat(f32::MIN)); - for c in corners { + for c in outline.into_iter().flatten() { let p = glam::Vec3::new(c.x as f32, c.y as f32, c.z as f32); lo = lo.min(p); hi = hi.max(p); diff --git a/src/app/update/file.rs b/src/app/update/file.rs index ecc041ff1..4fd0f783d 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -5544,7 +5544,7 @@ impl OpenCADStudio { /// Current visible rectangle in the active space. The result is deliberately /// not clamped to the paper sheet: Display and Window may include the grey /// canvas outside the sheet, matching Model-space plotting. - pub(in crate::app) fn display_plot_window(&self) -> Option<(f64, f64, f64, f64)> { + fn display_plot_window(&self) -> Option<(f64, f64, f64, f64)> { let scene = &self.tabs[self.active_tab].scene; let (canvas_w, canvas_h) = scene.selection.borrow().vp_size; let viewport = if scene.current_layout == "Model" { diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 0c2db547f..8528c1c9e 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -3035,6 +3035,7 @@ impl OpenCADStudio { &self.count_palette, tab.count.as_ref(), &tab.scene.document, + tab.scene.geometry_epoch, width, auto_collapse, ), diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 78d01a199..e1526f410 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -5545,6 +5545,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Count: {}" => Some(("count-feature", "toolbar-count")), "Overlapping object" => Some(("count-feature", "overlap")), "Show Count Instances" => Some(("count-feature", "show-instances")), + "Geometries: {}" => Some(("count-feature", "geometries")), _ => None, } } diff --git a/src/modules/annotate/count_cmd.rs b/src/modules/annotate/count_cmd.rs index 8e75a99ab..9b0517d6b 100644 --- a/src/modules/annotate/count_cmd.rs +++ b/src/modules/annotate/count_cmd.rs @@ -144,8 +144,16 @@ impl CadCommand for CountCommand { } fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { + // The selection arrives whole after every pick: `1 found`, then + // `1 found, 2 total`. + let found = handles.iter().filter(|h| !self.targets.contains(h)).count(); + let first = self.targets.is_empty(); self.targets = handles; - CmdResult::ReportMeasurement(format!("{} found", self.targets.len())) + CmdResult::ReportMeasurement(if first { + format!("{found} found") + } else { + format!("{found} found, {} total", self.targets.len()) + }) } fn needs_entity_pick(&self) -> bool { @@ -162,11 +170,13 @@ impl CadCommand for CountCommand { fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult { match self.picked.take() { - Some(EntityType::LwPolyline(pl)) if pl.is_closed && pl.vertices.len() >= 3 => { + Some(EntityType::LwPolyline(pl)) if codec::count::valid_boundary(&pl) => { self.area_done(format!("O {:X}", handle.value())) } - // Anything else: the prompt again. - _ => CmdResult::NeedPoint, + _ => CmdResult::CancelWithMessage( + "Invalid count area boundary object. Select a closed polyline consisting of line segments and does not intersect itself." + .into(), + ), } } @@ -244,6 +254,8 @@ pub struct CountTableCommand { blocks: Vec, others: [usize; 3], names: Vec, + /// Listing lines still to show after `Press ENTER to continue:`. + pending: Vec, } #[derive(Clone, Copy, PartialEq, Eq)] @@ -251,38 +263,75 @@ enum TableStep { Names, List, Point, + Page, } impl CountTableCommand { pub fn new(blocks: Vec, others: [usize; 3]) -> Self { - Self { step: TableStep::Names, blocks, others, names: Vec::new() } + Self { step: TableStep::Names, blocks, others, names: Vec::new(), pending: Vec::new() } } /// The palette's Create Table: the blocks are chosen, only the point is asked. pub fn placing(names: Vec) -> Self { - Self { step: TableStep::Point, blocks: Vec::new(), others: [0; 3], names } + Self { step: TableStep::Point, blocks: Vec::new(), others: [0; 3], names, pending: Vec::new() } } - /// `?`: the defined blocks matching the pattern and the summary. - fn listing(&self, pattern: &str) -> String { + /// `?`: the defined blocks matching the pattern, paged by 22 lines + /// (`Press ENTER to continue:` in between), then the summary. + fn listing(&mut self, pattern: &str) -> CmdResult { let pattern = if pattern.trim().is_empty() { "*" } else { pattern.trim() }; - let mut out = String::from("Defined blocks."); - for name in &self.blocks { - if pattern.split(',').any(|p| crate::io::xref_model::wildcard_match(name, p.trim())) { - out.push_str(&format!("\n \"{name}\"")); - } + let mut names: Vec<&String> = self + .blocks + .iter() + .filter(|name| pattern.split(',').any(|p| crate::io::xref_model::wildcard_match(name, p.trim()))) + .collect(); + names.sort_by_key(|name| list_order(name)); + self.pending = std::iter::once(String::new()) + .chain(std::iter::once("Defined blocks.".to_string())) + .chain(names.iter().map(|name| format!(" {:<31}", format!("\"{name}\"")))) + .collect(); + self.next_page() + } + + fn next_page(&mut self) -> CmdResult { + let page: Vec = self.pending.drain(..self.pending.len().min(22)).collect(); + if !self.pending.is_empty() { + self.step = TableStep::Page; + return CmdResult::ReportMeasurement(page.join(" +")); } - out.push_str(&format!( - "\n\nUser External Dependent Unnamed\nBlocks References Blocks Blocks\n{:>5}{:>11}{:>13}{:>12}", + CmdResult::Measurement(format!( + "{} + +User External Dependent Unnamed +Blocks References Blocks Blocks +{:>5}{:>10}{:>12}{:>12} +", + page.join(" +"), self.blocks.len(), self.others[0], self.others[1], self.others[2] - )); - out + )) } } +/// Block listing order: `_` first, then `-`, space and other punctuation, +/// digits, and letters without case. +fn list_order(name: &str) -> Vec<(u32, u32)> { + name.chars() + .map(|c| match c { + '_' => (0, 0), + '-' => (1, 0), + ' ' => (2, 0), + '0'..='9' => (4, c as u32), + c if c.is_alphabetic() => (5, c.to_lowercase().next().unwrap_or(c) as u32), + c => (3, c as u32), + }) + .collect() +} + impl CadCommand for CountTableCommand { fn name(&self) -> &'static str { "COUNTTABLE" @@ -293,6 +342,7 @@ impl CadCommand for CountTableCommand { TableStep::Names => "Enter block name(s) to include or [?] :".into(), TableStep::List => "Enter block(s) to list <*>:".into(), TableStep::Point => "Specify insertion point:".into(), + TableStep::Page => "Press ENTER to continue:".into(), } } @@ -327,7 +377,8 @@ impl CadCommand for CountTableCommand { self.step = TableStep::Point; CmdResult::NeedPoint } - TableStep::List => CmdResult::Measurement(self.listing(text)), + TableStep::List => self.listing(text), + TableStep::Page => self.next_page(), TableStep::Point => return None, }) } diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 9c22246bb..fcb98ef22 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1451,6 +1451,7 @@ impl Scene { } } } + m.color = self.count_color(handle, m.color, self.current_bg()); if tint_selected && self.selected.contains(&handle) { m.color = [0.15, 0.55, 1.00, m.color[3]]; } @@ -1752,6 +1753,7 @@ impl Scene { ) }); } + model.color = self.count_color(context.root_handle, model.color, hatch_bg); if tint_selected && self.selected.contains(&context.root_handle) { model.color = [0.15, 0.55, 1.00, model.color[3]]; } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 59e26575b..60d442f3f 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -4007,6 +4007,9 @@ impl Scene { ); } } + if self.meshes.contains_key(&h) { + material.diffuse = self.count_color(h, material.diffuse, bg); + } (h, material) }) }) @@ -4108,9 +4111,28 @@ impl Scene { return; } self.count_display = display; + self.hatch_cache.borrow_mut().clear(); + self.recolor_meshes(); self.bump_geometry_no_blocks(); } + /// The colour count mode gives an object drawn as part of `handle` (a + /// top-level entity): the count colours, else faded; unchanged outside + /// count mode. + pub(crate) fn count_color(&self, handle: Handle, color: [f32; 4], bg: [f32; 4]) -> [f32; 4] { + let Some(count) = &self.count_display else { + return color; + }; + let [r, g, b, _] = if count.errors.contains(&handle) { + count.error_color + } else if count.counted.contains(&handle) { + count.color + } else { + return crate::scene::cache::block_cache::fade_toward_bg(color, bg); + }; + [r, g, b, color[3]] + } + /// Fade the colours of wires that belong to entities outside the REFEDIT /// keep set (no-op when not editing). The geometry is untouched, so /// hit-testing still works on faded entities. In count mode the counted @@ -4118,8 +4140,7 @@ impl Scene { fn apply_refedit_fade(&self, wires: &mut [WireModel], bg: [f32; 4]) { if let Some(count) = &self.count_display { for w in wires.iter_mut() { - let h = Self::handle_from_wire_name(&w.name); - w.color = match h { + w.color = match Self::handle_from_wire_name(&w.name) { Some(h) if count.errors.contains(&h) => count.error_color, Some(h) if count.counted.contains(&h) => count.color, _ => crate::scene::cache::block_cache::fade_toward_bg(w.color, bg), @@ -8745,6 +8766,14 @@ impl Scene { self.material_base_dir.as_deref(), ); } + if self.count_display.is_some() { + let own = transformed.instance_color.or(set.display_color()).unwrap_or([1.0; 4]); + let color = self.count_color(context.root_handle, own, self.current_bg()); + transformed.instance_color = Some(color); + if let Some(material) = transformed.material.as_mut() { + material.diffuse = color; + } + } transformed.instance_handle = Some(context.root_handle); out.push(transformed); }, diff --git a/src/ui/window/count_palette.rs b/src/ui/window/count_palette.rs index 1c33dc886..8802d0ca5 100644 --- a/src/ui/window/count_palette.rs +++ b/src/ui/window/count_palette.rs @@ -33,7 +33,8 @@ pub enum CountTarget { matching: [bool; 3], picked: bool, }, - /// Several picked objects counted as one group. + /// Picked objects (several, or one that is not a reference): the copies + /// of the group are counted. Group(Vec), } @@ -50,6 +51,10 @@ pub struct CountMode { pub cursor: Option, /// The scene geometry epoch the colouring was computed at. pub epoch: u64, + /// The area Entire model space set aside, for a later Current area. + pub remembered: Option<(Vec<[f64; 2]>, Option<(Handle, bool)>)>, + /// The result and count at `epoch` (recomputed when the drawing changes). + pub cache: Option<(CountResult, usize)>, } /// A count mode's result: the counted references and the overlapping @@ -80,13 +85,35 @@ impl CountMode { } pub fn result(&self, doc: &CadDocument) -> CountResult { + match &self.cache { + Some((result, _)) => result.clone(), + None => self.compute(doc).0, + } + } + + /// What the count shows: the counted references, or the copies of a group. + pub fn count(&self, doc: &CadDocument) -> usize { + match &self.cache { + Some((_, n)) => *n, + None => self.compute(doc).1, + } + } + + /// The result and the count, worked out from the drawing. + pub fn compute(&self, doc: &CadDocument) -> (CountResult, usize) { + if let Some(CountTarget::Group(handles)) = &self.target { + let groups = codec::count::group_matches(doc, handles, self.area.as_deref()); + let result = CountResult { counted: groups.concat(), errors: Vec::new(), overlapped: Vec::new() }; + return (result, groups.len()); + } + let result = self.block_result(doc); + let n = result.counted.len(); + (result, n) + } + + fn block_result(&self, doc: &CadDocument) -> CountResult { match &self.target { - None => CountResult::default(), - Some(CountTarget::Group(handles)) => CountResult { - counted: handles.iter().copied().filter(|h| doc.get_entity(*h).is_some()).collect(), - errors: Vec::new(), - overlapped: Vec::new(), - }, + None | Some(CountTarget::Group(_)) => CountResult::default(), Some(CountTarget::Block { name, .. }) => { let instances = self.instances(doc); let key = self.key(&instances); @@ -100,15 +127,6 @@ impl CountMode { } } - /// What the count shows: the counted references, or 1 for a group whose - /// objects all exist. - pub fn count(&self, doc: &CadDocument) -> usize { - match &self.target { - Some(CountTarget::Group(handles)) => usize::from(handles.iter().all(|h| doc.get_entity(*h).is_some())), - _ => self.result(doc).counted.len(), - } - } - /// The name the toolbar and the command line show for the target. pub fn target_name(&self) -> Option { match &self.target { @@ -137,6 +155,8 @@ pub struct CountPalette { pub color: i16, pub error_color: i16, pub service: bool, + /// The list's references for (document, geometry epoch, area). + pub list_cache: std::cell::RefCell>, std::rc::Rc>)>>, } impl Default for CountPalette { @@ -153,6 +173,7 @@ impl Default for CountPalette { color: 3, error_color: 1, service: true, + list_cache: Default::default(), } } } @@ -434,7 +455,12 @@ fn mode_view<'a>(palette: &'a CountPalette, mode: &'a CountMode, doc: &CadDocume let mut details = column![button( row![ crate::ui::icons::themed_success(CHECK_ICON, 12.0), - text(format!("{name}: {}", mode.count(doc))).size(12).width(Fill), + text(match mode.target { + Some(CountTarget::Group(_)) => crate::tf!("Geometries: {}", mode.count(doc)).into_owned(), + _ => format!("{name}: {}", mode.count(doc)), + }) + .size(12) + .width(Fill), ] .spacing(6) .align_y(iced::Center), @@ -488,16 +514,25 @@ pub fn view<'a>( palette: &'a CountPalette, mode: Option<&'a CountMode>, doc: &'a CadDocument, + 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); - // ponytail: the list is recounted on every view; cache by geometry epoch - // if very large drawings make the palette slow. let body = match mode { Some(mode) if mode.target.is_some() => mode_view(palette, mode, doc), _ => { - let instances = codec::count::block_instances(doc, mode.and_then(|m| m.area.as_deref())); + let area = mode.and_then(|m| m.area.clone()); + let key = (doc as *const CadDocument as usize, 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(), + _ => { + let list = std::rc::Rc::new(codec::count::block_instances(doc, area.as_deref())); + *cache = Some((key.0, key.1, area, list.clone())); + list + } + }; list_view(palette, &instances) } }; From 789687659ba23a454c34f6e360f8b7f0b352def8 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:54:22 +0300 Subject: [PATCH 07/13] Count: Current area counts all of model space; Properties cleared after the pick Co-Authored-By: Claude Opus 5.5 --- src/app/commands/count.rs | 26 +++++++------------------- src/ui/window/count_palette.rs | 2 -- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/app/commands/count.rs b/src/app/commands/count.rs index 94fd8a73e..3dcbcd259 100644 --- a/src/app/commands/count.rs +++ b/src/app/commands/count.rs @@ -278,7 +278,6 @@ impl OpenCADStudio { pub(in crate::app) fn close_count(&mut self, i: usize) { if let Some(mode) = self.tabs[i].count.take() { self.drop_count_boundary(i, mode.boundary); - self.drop_count_boundary(i, mode.remembered.and_then(|(_, b)| b)); } self.tabs[i].scene.set_count_display(None); } @@ -311,13 +310,9 @@ impl OpenCADStudio { let mode = self.tabs[i].count.as_ref(); Some((mode.and_then(|m| m.area.clone()), mode.and_then(|m| m.boundary))) } - // Current area: the count area in use (also one Entire model space - // set aside), else all of model space. - "C" => Some(match self.tabs[i].count.as_ref() { - Some(m) if m.area.is_some() => (m.area.clone(), m.boundary), - Some(m) => m.remembered.clone().map_or((None, None), |(a, b)| (Some(a), b)), - None => (None, None), - }), + // Current area counts all of model space (the view and an area + // in use take no part). + "C" => Some((None, None)), "R" => { let p = points(); let (a, b) = (p.first()?, p.get(1)?); @@ -408,18 +403,11 @@ impl OpenCADStudio { }; // The picked targets do not stay selected. self.tabs[i].scene.deselect_all(); - // Entire model space counts without the area but keeps it for a later - // Current area; a new area replaces it. - let mut remembered = None; - if let Some(old) = self.tabs[i].count.take() { - let old_area = old.area.map(|a| (a, old.boundary)).or(old.remembered); - if area.is_none() { - remembered = old_area; - } else if let Some((_, b)) = old_area.filter(|(_, b)| *b != boundary) { - self.drop_count_boundary(i, b); - } + self.refresh_properties(); + 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, remembered, ..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(&format!("{name} ...... {n}")); diff --git a/src/ui/window/count_palette.rs b/src/ui/window/count_palette.rs index 8802d0ca5..f444a2266 100644 --- a/src/ui/window/count_palette.rs +++ b/src/ui/window/count_palette.rs @@ -51,8 +51,6 @@ pub struct CountMode { pub cursor: Option, /// The scene geometry epoch the colouring was computed at. pub epoch: u64, - /// The area Entire model space set aside, for a later Current area. - pub remembered: Option<(Vec<[f64; 2]>, Option<(Handle, bool)>)>, /// The result and count at `epoch` (recomputed when the drawing changes). pub cache: Option<(CountResult, usize)>, } From 4dba66bbac46f853124a89d8cc03899a53e2b3d0 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:06:58 +0300 Subject: [PATCH 08/13] Count: table drawn into its block with fields in the cell texts, polygon prompt, exact areas Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/app/commands/count.rs | 66 +++++++++++++++++++++---------- src/modules/annotate/count_cmd.rs | 5 ++- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e869f168d..6b96c417e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4206,7 +4206,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.5.5" -source = "git+https://github.com/ramox81/cadcodec.git?rev=178b596#178b596ba1606ce6d4ed719cd48551750a0f32a5" +source = "git+https://github.com/ramox81/cadcodec.git?rev=483c006#483c006df06edeb371af25a39f975007dd91f8ca" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index e5ef28bce..83e94433a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ block2 = "0.5" objc2-foundation = { version = "0.2", features = ["NSArray", "NSURL", "NSString", "NSObject", "NSNotification", "NSThread", "NSEnumerator"] } [patch."https://github.com/HakanSeven12/opencadcodec.git"] -opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "178b596" } +opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "483c006" } [patch.crates-io] iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } diff --git a/src/app/commands/count.rs b/src/app/commands/count.rs index 3dcbcd259..6fd6d0b92 100644 --- a/src/app/commands/count.rs +++ b/src/app/commands/count.rs @@ -530,10 +530,42 @@ impl OpenCADStudio { .column_width(3.6) .build(); table.set_column_width(1, 1.08); + // As the reference writes it: no title row, data rows centred + // (table and cell overrides), header cells without overrides. + table.value_flags = 22; + table.legacy_style_override = Some(codec::entities::table::LegacyTableStyleOverride { + flags: 0x10001, + title_suppressed: Some(true), + row_alignments: vec![5], + ..Default::default() + }); table.set_cell_text(0, 0, "Item"); table.set_cell_text(0, 1, "Count"); for (r, name) in names.iter().enumerate() { table.set_cell_text(r + 1, 0, name); + if let Some(cell) = table.cell_mut(r + 1, 1) { + // The count comes from the field; the cell keeps no text. + let mut content = codec::entities::table::CellContent::text(""); + content.value.flags = 1; + cell.contents = vec![content]; + cell.cell_type = codec::entities::table::CellType::Text; + } + } + for (r, row) in table.rows.iter_mut().enumerate() { + for cell in &mut row.cells { + if let Some(content) = cell.contents.first_mut().filter(|c| c.value.flags != 1) { + content.value.flags = 6; + } + if r == 0 { + cell.flag = 0x40000; + } else { + cell.flag = 0x40001; + let mut style = codec::entities::CellStyle::new(); + style.override_flags = 0x01; + style.alignment = 5; + cell.style = Some(style); + } + } } let codes: Vec = names.iter().map(|n| self.block_field_code(i, n, &CountKey::default())).collect(); @@ -541,26 +573,18 @@ impl OpenCADStudio { let Some(handle) = self.commit_entity_handle(EntityType::Table(Box::new(table))) else { return; }; + // The table's graphics block holds one MTEXT per cell; each count + // cell's MTEXT owns its field, which the cell refers to. let doc = &mut self.tabs[i].scene.document; - let mut fields = Vec::new(); - for code in &codes { - let value = crate::entities::field::evaluate(doc, code, &[], None).unwrap_or_else(|| "####".into()); - let field = doc.new_table_cell_field(handle, "%<\\_FldIdx 0>%", vec![codec::fields::NewField::new(code.clone(), value.clone())]); - fields.push((field, value)); - } - if let Some(EntityType::Table(table)) = doc.get_entity_mut(handle) { - for (r, (field, value)) in fields.into_iter().enumerate() { - let Some(field) = field else { continue }; - if let Some(cell) = table.cell_mut(r + 1, 1) { - let mut content = codec::entities::table::CellContent::text(&value); - content.field_handle = Some(field); - cell.contents.clear(); - cell.contents.push(content); - cell.cell_type = codec::entities::table::CellType::Text; - } - table.field_handles.push(field); - } - } + let fields = codes + .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)]) + }) + .collect(); + doc.build_table_block(handle, fields); self.tabs[i].scene.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]); self.tabs[i].dirty = true; } @@ -570,11 +594,11 @@ impl OpenCADStudio { let doc = &self.tabs[i].scene.document; let Some(entity) = doc.get_entity(h) else { return }; let outline = codec::count::entity_outline(doc, entity); - if outline.iter().all(|part| part.is_empty()) { + if outline.is_empty() { return; } let (mut lo, mut hi) = (glam::Vec3::splat(f32::MAX), glam::Vec3::splat(f32::MIN)); - for c in outline.into_iter().flatten() { + 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); lo = lo.min(p); hi = hi.max(p); diff --git a/src/modules/annotate/count_cmd.rs b/src/modules/annotate/count_cmd.rs index 9b0517d6b..f2be96126 100644 --- a/src/modules/annotate/count_cmd.rs +++ b/src/modules/annotate/count_cmd.rs @@ -3,7 +3,7 @@ // Specify first corner point of the count area or // [Current area/Entire model space/Object/Polygonal] : // Specify opposite corner: (Polygonal: Specify start point: / -// Specify next point:; Object: +// Specify next point; Object: // Select object as count area boundary:) // Select target objects or [List all blocks] : (COUNT) // @@ -109,7 +109,8 @@ impl CadCommand for CountCommand { Step::Area => "Specify first corner point of the count area or [Current area/Entire model space/Object/Polygonal] :".into(), Step::Corner => "Specify opposite corner:".into(), Step::PolyStart => "Specify start point:".into(), - Step::PolyNext => "Specify next point:".into(), + // The reference shows this one without a colon. + Step::PolyNext => "Specify next point".into(), Step::Object => "Select object as count area boundary:".into(), Step::Targets => "Select target objects or [List all blocks] :".into(), } From 7ad6e5b094a7f9feae8549df4b9fc2de68833da2 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:40:02 +0300 Subject: [PATCH 09/13] Count: Invalid Area dialog, table block kept with the table - When the count area's boundary is gone the approved Invalid Area dialog asks: undo the change (also its X and Cancel) or count all of model space; "always" keeps the choice in the settings. An edited boundary moves the area. - A table's *T block is brought up to date whenever the table changes. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- locales/ar-SA/opencadstudio.ftl | 7 ++++ locales/bg-BG/opencadstudio.ftl | 7 ++++ locales/cs-CZ/opencadstudio.ftl | 7 ++++ locales/de-DE/opencadstudio.ftl | 7 ++++ locales/el-GR/opencadstudio.ftl | 7 ++++ locales/en-US/opencadstudio.ftl | 7 ++++ locales/es-ES/opencadstudio.ftl | 7 ++++ locales/fi-FI/opencadstudio.ftl | 7 ++++ locales/fr-FR/opencadstudio.ftl | 7 ++++ locales/hi-IN/opencadstudio.ftl | 7 ++++ locales/hu-HU/opencadstudio.ftl | 7 ++++ locales/it-IT/opencadstudio.ftl | 7 ++++ locales/ja-JP/opencadstudio.ftl | 7 ++++ locales/ko-KR/opencadstudio.ftl | 7 ++++ locales/nl-NL/opencadstudio.ftl | 7 ++++ locales/pl-PL/opencadstudio.ftl | 7 ++++ locales/pt-BR/opencadstudio.ftl | 7 ++++ locales/ru-RU/opencadstudio.ftl | 7 ++++ locales/tr-TR/opencadstudio.ftl | 7 ++++ locales/zh-CN/opencadstudio.ftl | 7 ++++ locales/zh-TW/opencadstudio.ftl | 7 ++++ src/app/commands/count.rs | 68 ++++++++++++++++++++++++++++++++- src/app/control/actions.rs | 6 ++- src/app/mod.rs | 1 + src/app/settings.rs | 4 ++ src/app/update/file.rs | 2 + src/app/update/mod.rs | 4 ++ src/app/view/modal.rs | 4 ++ src/locale_catalog.rs | 7 ++++ src/scene/mod.rs | 9 +++++ src/ui/window/count_palette.rs | 67 ++++++++++++++++++++++++++++++++ 33 files changed, 318 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b96c417e..40b4fb9d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4206,7 +4206,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.5.5" -source = "git+https://github.com/ramox81/cadcodec.git?rev=483c006#483c006df06edeb371af25a39f975007dd91f8ca" +source = "git+https://github.com/ramox81/cadcodec.git?rev=115b753#115b7536c7c00bdd9684a80f2bb7642c11b048e9" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 83e94433a..a1dedd3f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ block2 = "0.5" objc2-foundation = { version = "0.2", features = ["NSArray", "NSURL", "NSString", "NSObject", "NSNotification", "NSThread", "NSEnumerator"] } [patch."https://github.com/HakanSeven12/opencadcodec.git"] -opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "483c006" } +opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "115b753" } [patch.crates-io] iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 8a6956780..72d8e4db4 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -5956,3 +5956,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 331b7cba7..c07082a2a 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -5944,3 +5944,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 4fafb4c8d..2d98d426d 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -5941,3 +5941,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index e2a2f8ae4..b37388bca 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -5942,3 +5942,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 95f7054d0..156d8d87c 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -5944,3 +5944,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 764b2f5c7..92e1f15c0 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -5945,3 +5945,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 5c228e77d..1ff70cdd5 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -5944,3 +5944,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index b6b1cc717..b6ad791b8 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -5941,3 +5941,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index faaae44cd..624cfcae7 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -5943,3 +5943,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index a7efb14dc..bcb1e45bf 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -5932,3 +5932,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index fafcb7755..71089bf43 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -5943,3 +5943,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 5bb46d938..87e46727e 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -5943,3 +5943,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 212923b58..4b26c5689 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -5932,3 +5932,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 4c0f4307f..f25a45ad6 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -5943,3 +5943,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 62a0f0efc..c488075de 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -5942,3 +5942,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 73582eaf9..a6d2dc415 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -5943,3 +5943,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 3c5d40863..6756aeac9 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -5944,3 +5944,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 1a532105c..2e370254d 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -5945,3 +5945,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 10943532b..25d2d556f 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -5867,3 +5867,10 @@ count-feature = .overlap = Üst üste binen nesne .show-instances = Örnekleri göster .geometries = Geometriler: __ocs_fmt_0__ + .invalid-title = Sayım - Geçersiz Alan + .invalid-message = Sayım sınırındaki değişiklikler geçersiz bir sayım alanıyla sonuçlandı. Ne yapmak istersiniz? + .invalid-undo = Sayım sınırındaki değişiklikleri geri al + .invalid-undo-detail = Son işlem geri alınır, sınır ve sayım alanı eski haline döner. + .invalid-continue = Devam et ve tüm model alanını say + .invalid-continue-detail = Sayım alanı kalkar; sayım tüm model alanında sürer. + .invalid-always = Geçerli seçimimi her zaman uygula diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 3fbd610ca..45e7c5f3c 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -5927,3 +5927,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index fb702b25f..19ede3dfd 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -5939,3 +5939,10 @@ count-feature = .overlap = Overlapping object .show-instances = Show Count Instances .geometries = Geometries: __ocs_fmt_0__ + .invalid-title = Count - Invalid Area + .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do? + .invalid-undo = Undo the changes to the count boundary + .invalid-undo-detail = The last change is undone; the boundary and the count area come back. + .invalid-continue = Continue and count the entire model space + .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. + .invalid-always = Always perform my current choice diff --git a/src/app/commands/count.rs b/src/app/commands/count.rs index 6fd6d0b92..d10041a43 100644 --- a/src/app/commands/count.rs +++ b/src/app/commands/count.rs @@ -268,8 +268,63 @@ 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; - if self.tabs[i].count.as_ref().is_some_and(|m| m.epoch != self.tabs[i].scene.geometry_epoch) { - self.apply_count_display(i); + 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 { + match self.tabs[i].scene.document.get_entity(h) { + // The boundary is gone (erased, undone): ask, or do what was + // chosen for good. + None => { + if self.active_modal == Some(crate::app::ModalKind::CountInvalidArea) { + return; + } + match self.count_palette.invalid_choice { + 1 => self.count_invalid_undo(i), + 2 => self.count_invalid_continue(i), + _ => { + self.count_palette.invalid_always = false; + self.active_modal = Some(crate::app::ModalKind::CountInvalidArea); + } + } + return; + } + // 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(); + if let Some(mode) = self.tabs[i].count.as_mut() { + mode.area = Some(ring); + } + } + Some(_) => {} + } + } + self.apply_count_display(i); + } + + /// Invalid Area › Undo (also ✕ and Cancel): the change that took the + /// boundary away is undone. + fn count_invalid_undo(&mut self, i: usize) { + self.undo_active_tab(); + self.apply_count_display(i); + } + + /// Invalid Area › Continue: the area goes, the count covers all of model space. + fn count_invalid_continue(&mut self, i: usize) { + if let Some(mode) = self.tabs[i].count.as_mut() { + mode.area = None; + mode.boundary = None; + } + self.apply_count_display(i); + } + + fn close_invalid_area(&mut self, choice: u8) { + if self.active_modal == Some(crate::app::ModalKind::CountInvalidArea) { + self.close_active_modal(); + } + if self.count_palette.invalid_always { + self.count_palette.invalid_choice = choice; + self.persist_settings_if_changed(); } } @@ -755,6 +810,15 @@ impl OpenCADStudio { return self.start_count_command(i, Box::new(command), "COUNT"); } CountMsg::Field => return self.dispatch_command("COUNTFIELD"), + CountMsg::InvalidAlways(v) => self.count_palette.invalid_always = v, + CountMsg::InvalidUndo => { + self.close_invalid_area(1); + self.count_invalid_undo(i); + } + CountMsg::InvalidContinue => { + self.close_invalid_area(2); + self.count_invalid_continue(i); + } } Task::none() } diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index d40ccbae7..9d6963605 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -933,7 +933,7 @@ impl OpenCADStudio { // layer|scale|mirror>:NAME", "create_table", "check=NAME:1", // "all=1", "table_cancel", "table_insert", "back", "match=<0|1|2>:1", // "details", "errors", "error=", "area", "prev", "next", "select", - // "field", "close". + // "field", "close", "invalid_undo", "invalid_continue", "invalid_always=1". "count_palette" => { use crate::ui::window::count_palette::{CountMsg as M, RowAction}; let value = string(req, "value")?; @@ -993,6 +993,10 @@ impl OpenCADStudio { "select" => M::Select, "field" => M::Field, "close" => M::Close, + // The Invalid Area dialog. + "invalid_undo" => M::InvalidUndo, + "invalid_continue" => M::InvalidContinue, + "invalid_always" => M::InvalidAlways(arg == "1"), _ => return Err(bad()), }) } diff --git a/src/app/mod.rs b/src/app/mod.rs index d1b4665cf..9183c3bd5 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1943,6 +1943,7 @@ pub enum ModalKind { GeometricTolerance, AttDef, AttDefEdit, + CountInvalidArea, Field, DraftingSettings, AutoConstrainSettings, diff --git a/src/app/settings.rs b/src/app/settings.rs index a8d302a39..89e1728ae 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -622,6 +622,9 @@ pub struct UserSettings { /// COUNTSERVICE (default on). #[serde(default = "default_true")] pub count_service: bool, + /// Count - Invalid Area: 0 ask, 1 undo, 2 count all of model space. + #[serde(default)] + pub count_invalid_area: u8, } /// A block the Blocks palette lists from outside the current drawing (a @@ -873,6 +876,7 @@ impl Default for UserSettings { count_color: 3, count_error_color: 1, count_service: true, + count_invalid_area: 0, } } } diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 4fd0f783d..42e997535 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -748,6 +748,7 @@ impl OpenCADStudio { count_color: self.count_palette.color, count_error_color: self.count_palette.error_color, count_service: self.count_palette.service, + count_invalid_area: self.count_palette.invalid_choice, } } @@ -882,6 +883,7 @@ impl OpenCADStudio { self.count_palette.color = s.count_color; self.count_palette.error_color = s.count_error_color; self.count_palette.service = s.count_service; + self.count_palette.invalid_choice = s.count_invalid_area.min(2); self.block_palette.recent = s.block_recent.clone(); self.block_palette.favorites = s.block_favorites.clone(); self.block_palette.libraries = s.block_libraries.clone(); diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 6408592db..6f1ab0b64 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -8524,6 +8524,10 @@ impl OpenCADStudio { } Message::CloseModal => { + // The Invalid Area dialog's ✕ undoes the boundary change, as its Cancel does. + if self.active_modal == Some(super::ModalKind::CountInvalidArea) { + return self.update(Message::Count(crate::ui::window::count_palette::CountMsg::InvalidUndo)); + } if self.active_modal == Some(super::ModalKind::Field) { self.close_field_dialog(); return Task::none(); diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index d5f81eae6..5f693e757 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -40,6 +40,7 @@ impl OpenCADStudio { Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(), Some(K::AttDef) => crate::t!("Attribute Definition").into_owned(), Some(K::AttDefEdit) => crate::t!("Edit Attribute Definition").into_owned(), + Some(K::CountInvalidArea) => crate::t!("Count - Invalid Area").into_owned(), Some(K::Field) => crate::t!("Field").into_owned(), Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(), Some(K::AutoConstrainSettings) => crate::t!("Constraint Settings").into_owned(), @@ -584,6 +585,9 @@ impl OpenCADStudio { let state = self.field_dialog.as_ref()?; sized_flow(ex, 760, 660, |flow| crate::ui::window::field_dialog::view(state, flow)) } + super::super::ModalKind::CountInvalidArea => { + sized_flow(ex, 520, 260, |flow| crate::ui::window::count_palette::invalid_area_view(&self.count_palette, flow)) + } super::super::ModalKind::AttDefEdit => { let state = self.attdef_edit.as_ref()?; sized_flow(ex, 460, 220, |flow| crate::ui::window::attdef_dialog::view_edit(state, flow)) diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index e1526f410..4d0f65539 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -5546,6 +5546,13 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Overlapping object" => Some(("count-feature", "overlap")), "Show Count Instances" => Some(("count-feature", "show-instances")), "Geometries: {}" => Some(("count-feature", "geometries")), + "Count - Invalid Area" => Some(("count-feature", "invalid-title")), + "The changes to count boundary have resulted to an invalid count area. What do you want to do?" => Some(("count-feature", "invalid-message")), + "Undo the changes to the count boundary" => Some(("count-feature", "invalid-undo")), + "The last change is undone; the boundary and the count area come back." => Some(("count-feature", "invalid-undo-detail")), + "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")), _ => None, } } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 60d442f3f..a63f22b54 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -3468,6 +3468,15 @@ impl Scene { self.associative_hatch_source_cache.borrow_mut().take(); } let mut changes = changes.to_vec(); + // A table's drawing (its *T block) follows the table's cells, sizes + // and styles; it is left alone while it already matches. + for (handle, kind) in &changes { + if !matches!(kind, ChangeKind::Removed) + && matches!(self.document.get_entity(*handle), Some(EntityType::Table(_))) + { + self.document.refresh_table_block(*handle); + } + } // A restyle (layer on/off, colour, text or dimension style) moves no // geometry, so nothing measured from it is re-resolved: an // association that disagrees with its dimension would otherwise diff --git a/src/ui/window/count_palette.rs b/src/ui/window/count_palette.rs index f444a2266..1f31eeb31 100644 --- a/src/ui/window/count_palette.rs +++ b/src/ui/window/count_palette.rs @@ -153,6 +153,11 @@ pub struct CountPalette { pub color: i16, pub error_color: i16, pub service: bool, + /// The Invalid Area dialog's "always" box while it is open. + pub invalid_always: bool, + /// 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>, std::rc::Rc>)>>, } @@ -171,6 +176,8 @@ impl Default for CountPalette { color: 3, error_color: 1, service: true, + invalid_always: false, + invalid_choice: 0, list_cache: Default::default(), } } @@ -217,6 +224,12 @@ pub enum CountMsg { Select, Field, Close, + /// The Invalid Area dialog: undo the boundary change (also its ✕ and + /// Cancel), or go on counting all of model space; `InvalidAlways` is its + /// "always perform my current choice" box. + InvalidUndo, + InvalidContinue, + InvalidAlways(bool), } fn msg(m: CountMsg) -> Message { @@ -609,3 +622,57 @@ pub fn aci_rgba(index: i16) -> [f32; 4] { crate::scene::convert::tess_util::aci_to_rgba(&codec::types::Color::from_index(index)) } + +/// The Invalid Area dialog, shown when the count area's boundary is gone. +pub fn invalid_area_view<'a>(palette: &CountPalette, sizing: crate::ui::modal::ModalSizing) -> Element<'a, Message> { + 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) }), + column![text(title).size(13), text(detail).size(11).style(muted_style)].spacing(2), + ] + .spacing(10) + .align_y(iced::Center), + ) + .on_press(msg(m)) + .width(Fill) + .padding([10, 12]) + .style(button_style(false)) + .into() + }; + column![ + row![ + crate::ui::icons::themed_warning(WARNING_ICON, 20.0), + text(crate::t!("The changes to count boundary have resulted to an invalid count area. What do you want to do?")) + .size(14) + .width(Fill), + ] + .spacing(12) + .align_y(iced::Center), + action( + crate::t!("Undo the changes to the count boundary").into_owned(), + crate::t!("The last change is undone; the boundary and the count area come back.").into_owned(), + CountMsg::InvalidUndo, + ), + action( + crate::t!("Continue and count the entire model space").into_owned(), + crate::t!("The count area is dropped; counting goes on in all of model space.").into_owned(), + CountMsg::InvalidContinue, + ), + row![ + checkbox(palette.invalid_always) + .label(crate::t!("Always perform my current choice").into_owned()) + .text_size(12) + .size(14) + .on_toggle(|v| msg(CountMsg::InvalidAlways(v))), + Space::new().width(Fill), + crate::ui::style::form::dialog_button(crate::t!("Cancel"), msg(CountMsg::InvalidUndo), false), + ] + .spacing(8) + .align_y(iced::Center), + ] + .spacing(10) + .padding([12, 14]) + .width(sizing.width) + .into() +} From 9b5b9dc707798fe55118268d3d69edd1bb0dd69a Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:47:29 +0300 Subject: [PATCH 10/13] Cargo: codec 6d3ecf4 (hatch copies along their curves, merged table cells) Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40b4fb9d5..db9e9d6c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4206,7 +4206,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.5.5" -source = "git+https://github.com/ramox81/cadcodec.git?rev=115b753#115b7536c7c00bdd9684a80f2bb7642c11b048e9" +source = "git+https://github.com/ramox81/cadcodec.git?rev=6d3ecf4#6d3ecf4962eb2e8a1ddcefb72329536d0efb9cc8" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index a1dedd3f3..e8cd768d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ block2 = "0.5" objc2-foundation = { version = "0.2", features = ["NSArray", "NSURL", "NSString", "NSObject", "NSNotification", "NSThread", "NSEnumerator"] } [patch."https://github.com/HakanSeven12/opencadcodec.git"] -opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "115b753" } +opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "6d3ecf4" } [patch.crates-io] iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } From 9ad569d33241820b1a64e2566872bb7807e2d197 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:10:39 +0300 Subject: [PATCH 11/13] Cargo: codec 196a5ae (mirrored pattern hatch is not a copy) Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db9e9d6c7..424ec3f19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4206,7 +4206,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.5.5" -source = "git+https://github.com/ramox81/cadcodec.git?rev=6d3ecf4#6d3ecf4962eb2e8a1ddcefb72329536d0efb9cc8" +source = "git+https://github.com/ramox81/cadcodec.git?rev=196a5ae#196a5aeabc1f22a526259d6788efcf0df9ad24e9" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index e8cd768d4..3f5b6ce85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ block2 = "0.5" objc2-foundation = { version = "0.2", features = ["NSArray", "NSURL", "NSString", "NSObject", "NSNotification", "NSThread", "NSEnumerator"] } [patch."https://github.com/HakanSeven12/opencadcodec.git"] -opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "6d3ecf4" } +opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "196a5ae" } [patch.crates-io] iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } From 8fb4a692c68acdbf0fa2a9ec3a67e5cf3cb21f88 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 6 Oct 2026 09:33:07 +0300 Subject: [PATCH 12/13] fix(count): redraw only app-edited table blocks; recolour count mode in place - refresh_table_block no longer runs for every table change: only tables whose picture reference this app dropped while editing them (and new tables without one), never on appearance-only bumps. Moved, restyled or freshly opened tables keep their authored *T block. TABLE edits now drop the picture reference like the other table edit paths. - set_count_display recolours just the handles whose count class changed (restyle path + recolor_meshes_for_handles); only entering or leaving count mode rebuilds the whole drawing. - Count colouring keeps insert instancing: counted and duplicate wires get their own instance groups instead of dropping render_instance. - The hatch backdrop takes the count colour too. Co-Authored-By: Claude Opus 5.5 --- src/app/commands/display.rs | 8 +++++ src/scene/entity.rs | 3 ++ src/scene/mod.rs | 72 ++++++++++++++++++++++++++++--------- 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index b979fec69..16925118b 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -179,6 +179,8 @@ impl OpenCADStudio { else { continue; }; + // `changed` tracks this table while the action runs. + let changed_before = std::mem::take(&mut changed); match &action { TableAction::Cell(row, column, text) => { if let Some(cell) = table.cell_mut(*row, *column) { @@ -321,6 +323,12 @@ impl OpenCADStudio { } _ => {} } + if changed { + // The stored picture no longer shows the cells: draw + // from them, and redraw the picture. + table.block_record_handle = None; + } + changed |= changed_before; } if changed { self.invalidate_property_targets(i, &selected_handles); diff --git a/src/scene/entity.rs b/src/scene/entity.rs index fcb98ef22..3a5cb2094 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1452,6 +1452,9 @@ impl Scene { } } m.color = self.count_color(handle, m.color, self.current_bg()); + if let Some(b) = &mut backdrop { + b.color = self.count_color(handle, b.color, self.current_bg()); + } if tint_selected && self.selected.contains(&handle) { m.color = [0.15, 0.55, 1.00, m.color[3]]; } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 2532218c6..6134a7955 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -3511,13 +3511,21 @@ impl Scene { self.associative_hatch_source_cache.borrow_mut().take(); } let mut changes = changes.to_vec(); - // A table's drawing (its *T block) follows the table's cells, sizes - // and styles; it is left alone while it already matches. - for (handle, kind) in &changes { - if !matches!(kind, ChangeKind::Removed) - && matches!(self.document.get_entity(*handle), Some(EntityType::Table(_))) - { - self.document.refresh_table_block(*handle); + // A table whose content this app changed (its picture reference + // dropped by the edit, or a new table without one) has its drawing + // (the *T block) redrawn from the cells. Tables keeping their picture + // (moved, restyled, opened from a file) are never redrawn: the block + // may hold authored content the redraw does not reproduce. + if !appearance_only { + for (handle, kind) in &changes { + if !matches!(kind, ChangeKind::Removed) + && matches!( + self.document.get_entity(*handle), + Some(EntityType::Table(t)) if t.block_record_handle.is_none() + ) + { + self.document.refresh_table_block(*handle); + } } } // A restyle (layer on/off, colour, text or dimension style) moves no @@ -4111,6 +4119,9 @@ impl Scene { material.diffuse = crate::scene::cache::block_cache::fade_toward_bg(material.diffuse, bg); } + if self.meshes.contains_key(&handle) { + material.diffuse = self.count_color(handle, material.diffuse, bg); + } (handle, material) }) }) @@ -4161,14 +4172,35 @@ impl Scene { } /// Enter / leave the COUNT colouring; a no-op when nothing changed. + /// Entering or leaving fades or restores the whole drawing; a change + /// inside count mode recolours only the objects whose colour changed. pub fn set_count_display(&mut self, display: Option) { if self.count_display == display { return; } - self.count_display = display; - self.hatch_cache.borrow_mut().clear(); - self.recolor_meshes(); - self.bump_geometry_no_blocks(); + let old = std::mem::replace(&mut self.count_display, display); + let (Some(old), Some(new)) = (old, self.count_display.as_ref()) else { + self.hatch_cache.borrow_mut().clear(); + self.recolor_meshes(); + self.bump_geometry_no_blocks(); + return; + }; + let recoloured = (old.color, old.error_color) != (new.color, new.error_color); + let class = |d: &CountDisplay, h: &Handle| (d.errors.contains(h), d.counted.contains(h)); + let changed: Vec = old + .counted + .iter() + .chain(&old.errors) + .chain(&new.counted) + .chain(&new.errors) + .copied() + .collect::>() + .into_iter() + .filter(|h| recoloured || class(&old, h) != class(new, h)) + .collect(); + self.recolor_meshes_for_handles(&changed); + let changes: Vec<(Handle, ChangeKind)> = changed.into_iter().map(|h| (h, ChangeKind::Modified)).collect(); + self.bump_entities_restyled(&changes); } /// The colour count mode gives an object drawn as part of `handle` (a @@ -4195,13 +4227,19 @@ impl Scene { fn apply_refedit_fade(&self, wires: &mut [WireModel], bg: [f32; 4]) { if let Some(count) = &self.count_display { for w in wires.iter_mut() { - w.color = match Self::handle_from_wire_name(&w.name) { - Some(h) if count.errors.contains(&h) => count.error_color, - Some(h) if count.counted.contains(&h) => count.color, - _ => crate::scene::cache::block_cache::fade_toward_bg(w.color, bg), + let (color, class) = match Self::handle_from_wire_name(&w.name) { + Some(h) if count.errors.contains(&h) => (count.error_color, 2u64), + Some(h) if count.counted.contains(&h) => (count.color, 1), + _ => (crate::scene::cache::block_cache::fade_toward_bg(w.color, bg), 0), }; - // Shared instance geometry keeps its own colour; draw these wires on their own. - w.render_instance = None; + w.color = color; + // Instances of one definition draw in one colour: counted and + // duplicate references get instance groups of their own. + // ponytail: tags the two top bits of the source id, which the + // id counter never reaches. + if let Some(instance) = w.render_instance.as_mut() { + instance.source_id |= class << 62; + } } } let Some(keep) = &self.refedit_keep else { From 1d78ab25746b5ee0d285cb198694366a80bba373 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 6 Oct 2026 09:33:08 +0300 Subject: [PATCH 13/13] 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 --- assets/icons/ui/info.svg | 1 + assets/icons/ui/more.svg | 1 + locales/ar-SA/opencadstudio.ftl | 17 +++- locales/bg-BG/opencadstudio.ftl | 17 +++- locales/cs-CZ/opencadstudio.ftl | 17 +++- locales/de-DE/opencadstudio.ftl | 17 +++- locales/el-GR/opencadstudio.ftl | 17 +++- locales/en-US/opencadstudio.ftl | 17 +++- locales/es-ES/opencadstudio.ftl | 17 +++- locales/fi-FI/opencadstudio.ftl | 17 +++- locales/fr-FR/opencadstudio.ftl | 17 +++- locales/hi-IN/opencadstudio.ftl | 17 +++- locales/hu-HU/opencadstudio.ftl | 17 +++- locales/it-IT/opencadstudio.ftl | 17 +++- locales/ja-JP/opencadstudio.ftl | 17 +++- locales/ko-KR/opencadstudio.ftl | 17 +++- locales/nl-NL/opencadstudio.ftl | 17 +++- locales/pl-PL/opencadstudio.ftl | 17 +++- locales/pt-BR/opencadstudio.ftl | 17 +++- locales/ru-RU/opencadstudio.ftl | 17 +++- locales/tr-TR/opencadstudio.ftl | 17 +++- locales/zh-CN/opencadstudio.ftl | 17 +++- locales/zh-TW/opencadstudio.ftl | 17 +++- src/app/command_driver/viewport.rs | 2 +- src/app/commands/count.rs | 147 ++++++++++++++++++----------- src/app/control/actions.rs | 4 +- src/app/view/mod.rs | 1 + src/locale_catalog.rs | 17 +++- src/modules/annotate/count_cmd.rs | 38 ++++---- src/modules/view/count_palette.rs | 2 +- src/ui/icons.rs | 2 + src/ui/popup/context_menu.rs | 2 +- src/ui/window/count_palette.rs | 53 +++++------ 33 files changed, 476 insertions(+), 151 deletions(-) create mode 100644 assets/icons/ui/info.svg create mode 100644 assets/icons/ui/more.svg diff --git a/assets/icons/ui/info.svg b/assets/icons/ui/info.svg new file mode 100644 index 000000000..212f70fce --- /dev/null +++ b/assets/icons/ui/info.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/ui/more.svg b/assets/icons/ui/more.svg new file mode 100644 index 000000000..50d01007a --- /dev/null +++ b/assets/icons/ui/more.svg @@ -0,0 +1 @@ + diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index a39edaf12..1c01174f9 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -5940,8 +5940,6 @@ style-manager = .block-extents = نطاق الكتلة .base-point = نقطة الأساس count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5964,3 +5962,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = تم العثور على __ocs_fmt_0__ + .found-total = تم العثور على __ocs_fmt_0__، الإجمالي __ocs_fmt_1__ + .field-only-in-count = ** الأمر COUNTFIELD متاح فقط أثناء العد. ** + .area-active = منطقة العد نشطة. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (للقراءة فقط) + .requires-0-or-1 = يتطلب 0 أو 1 فقط. + .requires-int-range = يتطلب عددًا صحيحًا بين __ocs_fmt_0__ و __ocs_fmt_1__. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT نمط النص الحالي: "__ocs_fmt_0__" ارتفاع النص: __ocs_fmt_1__ + .fields-found = تم العثور على __ocs_fmt_0__ من الحقول. + .fields-updated = تم تحديث __ocs_fmt_0__ من الحقول. + .invalid-keyword = كلمة مفتاحية للخيار غير صالحة. + .invalid-boundary = كائن حد منطقة العد غير صالح. حدد خطًا متعددًا مغلقًا يتكون من قطع مستقيمة ولا يتقاطع مع نفسه. + .block-not-found = تعذر العثور على الكتلة "__ocs_fmt_0__". + .invalid-point = نقطة غير صالحة. diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 294ebfdf2..fe2b0bf6c 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -5928,8 +5928,6 @@ style-manager = .block-extents = Обхват на блок .base-point = Базова точка count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5952,3 +5950,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = Намерени: __ocs_fmt_0__ + .found-total = Намерени: __ocs_fmt_0__, общо: __ocs_fmt_1__ + .field-only-in-count = ** Командата COUNTFIELD е достъпна само по време на преброяване. ** + .area-active = Областта за преброяване е активна. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (само за четене) + .requires-0-or-1 = Изисква се само 0 или 1. + .requires-int-range = Изисква се цяло число между __ocs_fmt_0__ и __ocs_fmt_1__. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT Текущ текстов стил: "__ocs_fmt_0__" Височина на текста: __ocs_fmt_1__ + .fields-found = Намерени полета: __ocs_fmt_0__. + .fields-updated = Обновени полета: __ocs_fmt_0__. + .invalid-keyword = Невалидна ключова дума на опция. + .invalid-boundary = Невалиден обект за граница на областта за преброяване. Изберете затворена полилиния от отсечки, която не се самопресича. + .block-not-found = Блокът "__ocs_fmt_0__" не може да бъде намерен. + .invalid-point = Невалидна точка. diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index b26dfe485..10cc0a116 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -5925,8 +5925,6 @@ style-manager = .block-extents = Blokovat rozsahy .base-point = Základní bod count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5949,3 +5947,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = Nalezeno: __ocs_fmt_0__ + .found-total = Nalezeno: __ocs_fmt_0__, celkem: __ocs_fmt_1__ + .field-only-in-count = ** Příkaz COUNTFIELD je dostupný pouze během počítání. ** + .area-active = Oblast počítání je aktivní. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (jen pro čtení) + .requires-0-or-1 = Vyžaduje pouze 0 nebo 1. + .requires-int-range = Vyžaduje celé číslo mezi __ocs_fmt_0__ a __ocs_fmt_1__. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT Aktuální styl textu: "__ocs_fmt_0__" Výška textu: __ocs_fmt_1__ + .fields-found = Nalezeno polí: __ocs_fmt_0__. + .fields-updated = Aktualizováno polí: __ocs_fmt_0__. + .invalid-keyword = Neplatné klíčové slovo volby. + .invalid-boundary = Neplatný objekt hranice oblasti počítání. Vyberte uzavřenou křivku složenou z úseček, která neprotíná sama sebe. + .block-not-found = Blok "__ocs_fmt_0__" nelze najít. + .invalid-point = Neplatný bod. diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 14e1ceaa3..605ccb174 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -5926,8 +5926,6 @@ style-manager = .block-extents = Blockgrenzen .base-point = Basispunkt count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5950,3 +5948,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = __ocs_fmt_0__ gefunden + .found-total = __ocs_fmt_0__ gefunden, __ocs_fmt_1__ gesamt + .field-only-in-count = ** Der Befehl COUNTFIELD ist nur während der Zählung verfügbar. ** + .area-active = Zählbereich ist aktiv. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (schreibgeschützt) + .requires-0-or-1 = Nur 0 oder 1 zulässig. + .requires-int-range = Erfordert eine ganze Zahl zwischen __ocs_fmt_0__ und __ocs_fmt_1__. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT Aktueller Textstil: "__ocs_fmt_0__" Texthöhe: __ocs_fmt_1__ + .fields-found = __ocs_fmt_0__ Feld(er) gefunden. + .fields-updated = __ocs_fmt_0__ Feld(er) aktualisiert. + .invalid-keyword = Ungültiges Optionsschlüsselwort. + .invalid-boundary = Ungültiges Begrenzungsobjekt für den Zählbereich. Wählen Sie eine geschlossene Polylinie aus Liniensegmenten, die sich nicht selbst schneidet. + .block-not-found = Block "__ocs_fmt_0__" wurde nicht gefunden. + .invalid-point = Ungültiger Punkt. diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 047ec281e..433624525 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -5928,8 +5928,6 @@ style-manager = .block-extents = Όρια μπλοκ .base-point = Σημείο βάσης count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5952,3 +5950,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = Βρέθηκαν: __ocs_fmt_0__ + .found-total = Βρέθηκαν: __ocs_fmt_0__, σύνολο: __ocs_fmt_1__ + .field-only-in-count = ** Η εντολή COUNTFIELD είναι διαθέσιμη μόνο κατά την καταμέτρηση. ** + .area-active = Η περιοχή καταμέτρησης είναι ενεργή. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (μόνο για ανάγνωση) + .requires-0-or-1 = Απαιτείται μόνο 0 ή 1. + .requires-int-range = Απαιτείται ακέραιος μεταξύ __ocs_fmt_0__ και __ocs_fmt_1__. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT Τρέχον στυλ κειμένου: "__ocs_fmt_0__" Ύψος κειμένου: __ocs_fmt_1__ + .fields-found = Βρέθηκαν πεδία: __ocs_fmt_0__. + .fields-updated = Ενημερώθηκαν πεδία: __ocs_fmt_0__. + .invalid-keyword = Μη έγκυρη λέξη-κλειδί επιλογής. + .invalid-boundary = Μη έγκυρο αντικείμενο ορίου περιοχής καταμέτρησης. Επιλέξτε μια κλειστή πολυγραμμή από ευθύγραμμα τμήματα που δεν τέμνει τον εαυτό της. + .block-not-found = Το μπλοκ "__ocs_fmt_0__" δεν βρέθηκε. + .invalid-point = Μη έγκυρο σημείο. diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 96e53825f..6f3f4730d 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -5929,8 +5929,6 @@ style-manager = .block-extents = Block extents .base-point = Base point count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5953,3 +5951,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = __ocs_fmt_0__ found + .found-total = __ocs_fmt_0__ found, __ocs_fmt_1__ total + .field-only-in-count = ** COUNTFIELD command only available during Count. ** + .area-active = count area is active. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (read only) + .requires-0-or-1 = Requires 0 or 1 only. + .requires-int-range = Requires an integer between __ocs_fmt_0__ and __ocs_fmt_1__. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT Current text style: "__ocs_fmt_0__" Text height: __ocs_fmt_1__ + .fields-found = __ocs_fmt_0__ field(s) found. + .fields-updated = __ocs_fmt_0__ field(s) updated. + .invalid-keyword = Invalid option keyword. + .invalid-boundary = Invalid count area boundary object. Select a closed polyline consisting of line segments and does not intersect itself. + .block-not-found = Block "__ocs_fmt_0__" cannot be found. + .invalid-point = Invalid point. diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 79a021096..4bb6669be 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -5928,8 +5928,6 @@ style-manager = .block-extents = Extensión del bloque .base-point = Punto base count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5952,3 +5950,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = __ocs_fmt_0__ encontrado(s) + .found-total = __ocs_fmt_0__ encontrado(s), __ocs_fmt_1__ en total + .field-only-in-count = ** El comando COUNTFIELD solo está disponible durante el recuento. ** + .area-active = El área de recuento está activa. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (solo lectura) + .requires-0-or-1 = Solo admite 0 o 1. + .requires-int-range = Se requiere un entero entre __ocs_fmt_0__ y __ocs_fmt_1__. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT Estilo de texto actual: "__ocs_fmt_0__" Altura de texto: __ocs_fmt_1__ + .fields-found = __ocs_fmt_0__ campo(s) encontrado(s). + .fields-updated = __ocs_fmt_0__ campo(s) actualizado(s). + .invalid-keyword = Palabra clave de opción no válida. + .invalid-boundary = Objeto de contorno de área de recuento no válido. Seleccione una polilínea cerrada formada por segmentos de línea que no se corte a sí misma. + .block-not-found = No se encuentra el bloque "__ocs_fmt_0__". + .invalid-point = Punto no válido. diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 72cbd4dcd..e01e8ffca 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -5925,8 +5925,6 @@ style-manager = .block-extents = Estä laajuudet .base-point = Peruspiste count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5949,3 +5947,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = Löytyi __ocs_fmt_0__ + .found-total = Löytyi __ocs_fmt_0__, yhteensä __ocs_fmt_1__ + .field-only-in-count = ** COUNTFIELD-komento on käytettävissä vain laskennan aikana. ** + .area-active = Laskenta-alue on aktiivinen. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (vain luku) + .requires-0-or-1 = Vain 0 tai 1 sallitaan. + .requires-int-range = Vaaditaan kokonaisluku väliltä __ocs_fmt_0__–__ocs_fmt_1__. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT Nykyinen tekstityyli: "__ocs_fmt_0__" Tekstin korkeus: __ocs_fmt_1__ + .fields-found = Kenttiä löytyi: __ocs_fmt_0__. + .fields-updated = Kenttiä päivitetty: __ocs_fmt_0__. + .invalid-keyword = Virheellinen asetuksen avainsana. + .invalid-boundary = Virheellinen laskenta-alueen rajaobjekti. Valitse suljettu, janoista koostuva moniviiva, joka ei leikkaa itseään. + .block-not-found = Lohkoa "__ocs_fmt_0__" ei löydy. + .invalid-point = Virheellinen piste. diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 702e42640..ca2bda963 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -5927,8 +5927,6 @@ style-manager = .block-extents = Limites du bloc .base-point = Point de base count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5951,3 +5949,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = __ocs_fmt_0__ trouvé(s) + .found-total = __ocs_fmt_0__ trouvé(s), __ocs_fmt_1__ au total + .field-only-in-count = ** La commande COUNTFIELD n'est disponible que pendant le comptage. ** + .area-active = La zone de comptage est active. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (lecture seule) + .requires-0-or-1 = Nécessite 0 ou 1 uniquement. + .requires-int-range = Nécessite un entier compris entre __ocs_fmt_0__ et __ocs_fmt_1__. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT Style de texte courant : "__ocs_fmt_0__" Hauteur du texte : __ocs_fmt_1__ + .fields-found = __ocs_fmt_0__ champ(s) trouvé(s). + .fields-updated = __ocs_fmt_0__ champ(s) mis à jour. + .invalid-keyword = Mot-clé d'option non valide. + .invalid-boundary = Objet de contour de zone de comptage non valide. Sélectionnez une polyligne fermée composée de segments de ligne et qui ne se recoupe pas. + .block-not-found = Le bloc "__ocs_fmt_0__" est introuvable. + .invalid-point = Point non valide. diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 0ab2cf7fa..3585bacf9 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -5916,8 +5916,6 @@ style-manager = .block-extents = ब्लॉक परिमिति .base-point = आधार बिंदु count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5940,3 +5938,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = __ocs_fmt_0__ मिले + .found-total = __ocs_fmt_0__ मिले, कुल __ocs_fmt_1__ + .field-only-in-count = ** COUNTFIELD कमांड केवल गणना के दौरान उपलब्ध है। ** + .area-active = गणना क्षेत्र सक्रिय है। + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (केवल पढ़ने के लिए) + .requires-0-or-1 = केवल 0 या 1 आवश्यक है। + .requires-int-range = __ocs_fmt_0__ और __ocs_fmt_1__ के बीच पूर्णांक आवश्यक है। + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT वर्तमान टेक्स्ट शैली: "__ocs_fmt_0__" टेक्स्ट ऊँचाई: __ocs_fmt_1__ + .fields-found = __ocs_fmt_0__ फ़ील्ड मिले। + .fields-updated = __ocs_fmt_0__ फ़ील्ड अपडेट किए गए। + .invalid-keyword = अमान्य विकल्प कीवर्ड। + .invalid-boundary = अमान्य गणना क्षेत्र सीमा ऑब्जेक्ट। रेखा खंडों से बनी ऐसी बंद पॉलीलाइन चुनें जो स्वयं को प्रतिच्छेद न करे। + .block-not-found = ब्लॉक "__ocs_fmt_0__" नहीं मिला। + .invalid-point = अमान्य बिंदु। diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index a92b034f2..665907146 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -5927,8 +5927,6 @@ style-manager = .block-extents = Kiterjedések blokkolása .base-point = Alappont count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5951,3 +5949,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = __ocs_fmt_0__ található + .found-total = __ocs_fmt_0__ található, összesen __ocs_fmt_1__ + .field-only-in-count = ** A COUNTFIELD parancs csak számlálás közben érhető el. ** + .area-active = A számlálási terület aktív. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (csak olvasható) + .requires-0-or-1 = Csak 0 vagy 1 adható meg. + .requires-int-range = __ocs_fmt_0__ és __ocs_fmt_1__ közötti egész szám szükséges. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT Aktuális szövegstílus: "__ocs_fmt_0__" Szövegmagasság: __ocs_fmt_1__ + .fields-found = __ocs_fmt_0__ mező található. + .fields-updated = __ocs_fmt_0__ mező frissítve. + .invalid-keyword = Érvénytelen opciókulcsszó. + .invalid-boundary = Érvénytelen számlálási terület határobjektum. Válasszon egy zárt, vonalszakaszokból álló, önmagát nem metsző vonalláncot. + .block-not-found = A(z) "__ocs_fmt_0__" blokk nem található. + .invalid-point = Érvénytelen pont. diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index d4f900ae2..134de2c2b 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -5927,8 +5927,6 @@ style-manager = .block-extents = Blocca estensioni .base-point = Punto base count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5951,3 +5949,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = __ocs_fmt_0__ trovati + .found-total = __ocs_fmt_0__ trovati, __ocs_fmt_1__ in totale + .field-only-in-count = ** Il comando COUNTFIELD è disponibile solo durante il conteggio. ** + .area-active = L'area di conteggio è attiva. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (sola lettura) + .requires-0-or-1 = Richiede solo 0 o 1. + .requires-int-range = Richiede un numero intero compreso tra __ocs_fmt_0__ e __ocs_fmt_1__. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT Stile di testo corrente: "__ocs_fmt_0__" Altezza testo: __ocs_fmt_1__ + .fields-found = __ocs_fmt_0__ campi trovati. + .fields-updated = __ocs_fmt_0__ campi aggiornati. + .invalid-keyword = Parola chiave dell'opzione non valida. + .invalid-boundary = Oggetto di contorno dell'area di conteggio non valido. Selezionare una polilinea chiusa composta da segmenti di linea che non si interseca. + .block-not-found = Impossibile trovare il blocco "__ocs_fmt_0__". + .invalid-point = Punto non valido. diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 8b1b1dbde..77c7b54d9 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -5916,8 +5916,6 @@ style-manager = .block-extents = ブロック範囲 .base-point = 基点 count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5940,3 +5938,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = __ocs_fmt_0__ 個を検出 + .found-total = __ocs_fmt_0__ 個を検出、合計 __ocs_fmt_1__ 個 + .field-only-in-count = ** COUNTFIELD コマンドはカウント中のみ使用できます。 ** + .area-active = カウント領域が有効です。 + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (読み取り専用) + .requires-0-or-1 = 0 または 1 のみ指定できます。 + .requires-int-range = __ocs_fmt_0__ から __ocs_fmt_1__ までの整数を指定してください。 + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT 現在の文字スタイル: "__ocs_fmt_0__" 文字の高さ: __ocs_fmt_1__ + .fields-found = __ocs_fmt_0__ 個のフィールドが見つかりました。 + .fields-updated = __ocs_fmt_0__ 個のフィールドを更新しました。 + .invalid-keyword = オプションのキーワードが無効です。 + .invalid-boundary = カウント領域の境界オブジェクトが無効です。線分で構成され、自己交差しない閉じたポリラインを選択してください。 + .block-not-found = ブロック "__ocs_fmt_0__" が見つかりません。 + .invalid-point = 点が無効です。 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 6c3081482..1d4e4ef29 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -5927,8 +5927,6 @@ style-manager = .block-extents = 블록 범위 .base-point = 기준점 count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5951,3 +5949,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = __ocs_fmt_0__개 찾음 + .found-total = __ocs_fmt_0__개 찾음, 총 __ocs_fmt_1__개 + .field-only-in-count = ** COUNTFIELD 명령은 개수 세기 중에만 사용할 수 있습니다. ** + .area-active = 개수 영역이 활성화되었습니다. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (읽기 전용) + .requires-0-or-1 = 0 또는 1만 입력할 수 있습니다. + .requires-int-range = __ocs_fmt_0__에서 __ocs_fmt_1__ 사이의 정수가 필요합니다. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT 현재 문자 스타일: "__ocs_fmt_0__" 문자 높이: __ocs_fmt_1__ + .fields-found = 필드 __ocs_fmt_0__개를 찾았습니다. + .fields-updated = 필드 __ocs_fmt_0__개를 업데이트했습니다. + .invalid-keyword = 옵션 키워드가 잘못되었습니다. + .invalid-boundary = 개수 영역 경계 객체가 잘못되었습니다. 선분으로 구성되고 자체 교차하지 않는 닫힌 폴리선을 선택하십시오. + .block-not-found = 블록 "__ocs_fmt_0__"을(를) 찾을 수 없습니다. + .invalid-point = 점이 잘못되었습니다. diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index c3fb6b01e..8a7b29a64 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -5926,8 +5926,6 @@ style-manager = .block-extents = Symboolgrenzen .base-point = Basispunt count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5950,3 +5948,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = __ocs_fmt_0__ gevonden + .found-total = __ocs_fmt_0__ gevonden, __ocs_fmt_1__ totaal + .field-only-in-count = ** De opdracht COUNTFIELD is alleen beschikbaar tijdens het tellen. ** + .area-active = Telgebied is actief. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (alleen-lezen) + .requires-0-or-1 = Alleen 0 of 1 toegestaan. + .requires-int-range = Vereist een geheel getal tussen __ocs_fmt_0__ en __ocs_fmt_1__. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT Huidige tekststijl: "__ocs_fmt_0__" Teksthoogte: __ocs_fmt_1__ + .fields-found = __ocs_fmt_0__ veld(en) gevonden. + .fields-updated = __ocs_fmt_0__ veld(en) bijgewerkt. + .invalid-keyword = Ongeldig optiesleutelwoord. + .invalid-boundary = Ongeldig grensobject voor het telgebied. Selecteer een gesloten polylijn die uit lijnsegmenten bestaat en zichzelf niet snijdt. + .block-not-found = Blok "__ocs_fmt_0__" kan niet worden gevonden. + .invalid-point = Ongeldig punt. diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 53ad2866a..b425dee41 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -5927,8 +5927,6 @@ style-manager = .block-extents = Zasięg bloku .base-point = Punkt bazowy count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5951,3 +5949,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = Znaleziono: __ocs_fmt_0__ + .found-total = Znaleziono: __ocs_fmt_0__, łącznie: __ocs_fmt_1__ + .field-only-in-count = ** Polecenie COUNTFIELD jest dostępne tylko podczas liczenia. ** + .area-active = Obszar liczenia jest aktywny. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (tylko do odczytu) + .requires-0-or-1 = Wymaga wyłącznie 0 lub 1. + .requires-int-range = Wymaga liczby całkowitej z zakresu od __ocs_fmt_0__ do __ocs_fmt_1__. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT Bieżący styl tekstu: "__ocs_fmt_0__" Wysokość tekstu: __ocs_fmt_1__ + .fields-found = Znaleziono pól: __ocs_fmt_0__. + .fields-updated = Zaktualizowano pól: __ocs_fmt_0__. + .invalid-keyword = Nieprawidłowe słowo kluczowe opcji. + .invalid-boundary = Nieprawidłowy obiekt obwiedni obszaru liczenia. Wybierz zamkniętą polilinię złożoną z odcinków, która nie przecina samej siebie. + .block-not-found = Nie można znaleźć bloku "__ocs_fmt_0__". + .invalid-point = Nieprawidłowy punkt. diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 6bfa83486..4f1c800b8 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -5928,8 +5928,6 @@ style-manager = .block-extents = Extensões do bloco .base-point = Ponto base count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5952,3 +5950,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = __ocs_fmt_0__ encontrado(s) + .found-total = __ocs_fmt_0__ encontrado(s), __ocs_fmt_1__ no total + .field-only-in-count = ** O comando COUNTFIELD só está disponível durante a contagem. ** + .area-active = A área de contagem está ativa. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (somente leitura) + .requires-0-or-1 = Requer apenas 0 ou 1. + .requires-int-range = Requer um número inteiro entre __ocs_fmt_0__ e __ocs_fmt_1__. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT Estilo de texto atual: "__ocs_fmt_0__" Altura do texto: __ocs_fmt_1__ + .fields-found = __ocs_fmt_0__ campo(s) encontrado(s). + .fields-updated = __ocs_fmt_0__ campo(s) atualizado(s). + .invalid-keyword = Palavra-chave de opção inválida. + .invalid-boundary = Objeto de contorno da área de contagem inválido. Selecione uma polilinha fechada composta de segmentos de linha e que não se cruze. + .block-not-found = O bloco "__ocs_fmt_0__" não foi encontrado. + .invalid-point = Ponto inválido. diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 6bffa6354..3c8c71a8e 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -5929,8 +5929,6 @@ style-manager = .block-extents = Габариты блока .base-point = Базовая точка count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5953,3 +5951,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = Найдено: __ocs_fmt_0__ + .found-total = Найдено: __ocs_fmt_0__, всего: __ocs_fmt_1__ + .field-only-in-count = ** Команда COUNTFIELD доступна только в режиме подсчёта. ** + .area-active = Область подсчёта активна. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (только чтение) + .requires-0-or-1 = Допускается только 0 или 1. + .requires-int-range = Требуется целое число от __ocs_fmt_0__ до __ocs_fmt_1__. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT Текущий текстовый стиль: "__ocs_fmt_0__" Высота текста: __ocs_fmt_1__ + .fields-found = Найдено полей: __ocs_fmt_0__. + .fields-updated = Обновлено полей: __ocs_fmt_0__. + .invalid-keyword = Недопустимое ключевое слово параметра. + .invalid-boundary = Недопустимый объект границы области подсчёта. Выберите замкнутую полилинию из отрезков, которая не пересекает саму себя. + .block-not-found = Блок "__ocs_fmt_0__" не найден. + .invalid-point = Недопустимая точка. diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 945dc938e..43908e04f 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -5851,8 +5851,6 @@ style-manager = .block-extents = Blok sınırları .base-point = Taban noktası count-feature = - .palette = Sayım - .menu = Say .selection = Seçimi say .review = Sayım ayrıntılarını incele .insert-field = Sayım alanı ekle @@ -5875,3 +5873,18 @@ count-feature = .invalid-continue = Devam et ve tüm model alanını say .invalid-continue-detail = Sayım alanı kalkar; sayım tüm model alanında sürer. .invalid-always = Geçerli seçimimi her zaman uygula + .found = __ocs_fmt_0__ bulundu + .found-total = __ocs_fmt_0__ bulundu, toplam __ocs_fmt_1__ + .field-only-in-count = ** COUNTFIELD komutu yalnızca sayım sırasında kullanılabilir. ** + .area-active = Sayım alanı etkin. + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (salt okunur) + .requires-0-or-1 = Yalnızca 0 veya 1 girilebilir. + .requires-int-range = __ocs_fmt_0__ ile __ocs_fmt_1__ arasında bir tamsayı gerekir. + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT Geçerli metin stili: "__ocs_fmt_0__" Metin yüksekliği: __ocs_fmt_1__ + .fields-found = __ocs_fmt_0__ alan bulundu. + .fields-updated = __ocs_fmt_0__ alan güncellendi. + .invalid-keyword = Geçersiz seçenek anahtar sözcüğü. + .invalid-boundary = Geçersiz sayım alanı sınır nesnesi. Doğru parçalarından oluşan, kendisiyle kesişmeyen kapalı bir çoklu çizgi seçin. + .block-not-found = "__ocs_fmt_0__" bloğu bulunamadı. + .invalid-point = Geçersiz nokta. diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index a9fdfa9bc..0249f7cfa 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -5911,8 +5911,6 @@ style-manager = .block-extents = 块范围 .base-point = 基点 count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5935,3 +5933,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = 找到 __ocs_fmt_0__ 个 + .found-total = 找到 __ocs_fmt_0__ 个,总计 __ocs_fmt_1__ 个 + .field-only-in-count = ** COUNTFIELD 命令仅在计数期间可用。 ** + .area-active = 计数区域已激活。 + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__(只读) + .requires-0-or-1 = 只能为 0 或 1。 + .requires-int-range = 需要介于 __ocs_fmt_0__ 和 __ocs_fmt_1__ 之间的整数。 + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT 当前文字样式: "__ocs_fmt_0__" 文字高度: __ocs_fmt_1__ + .fields-found = 找到 __ocs_fmt_0__ 个字段。 + .fields-updated = 已更新 __ocs_fmt_0__ 个字段。 + .invalid-keyword = 无效的选项关键字。 + .invalid-boundary = 无效的计数区域边界对象。请选择由直线段组成且不自交的闭合多段线。 + .block-not-found = 找不到块 "__ocs_fmt_0__"。 + .invalid-point = 无效的点。 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index ec8dffc8b..354f9442d 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -5923,8 +5923,6 @@ style-manager = .block-extents = 圖塊範圍 .base-point = 基點 count-feature = - .palette = Count - .menu = Count .selection = Count Selection .review = Review Count Details .insert-field = Insert Count Field @@ -5947,3 +5945,18 @@ count-feature = .invalid-continue = Continue and count the entire model space .invalid-continue-detail = The count area is dropped; counting goes on in all of model space. .invalid-always = Always perform my current choice + .found = 找到 __ocs_fmt_0__ 個 + .found-total = 找到 __ocs_fmt_0__ 個,總計 __ocs_fmt_1__ 個 + .field-only-in-count = ** COUNTFIELD 指令僅在計數期間可用。 ** + .area-active = 計數區域已啟用。 + .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__(唯讀) + .requires-0-or-1 = 只能為 0 或 1。 + .requires-int-range = 需要介於 __ocs_fmt_0__ 與 __ocs_fmt_1__ 之間的整數。 + .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__ + .mtext-current-style = MTEXT 目前文字型式: "__ocs_fmt_0__" 文字高度: __ocs_fmt_1__ + .fields-found = 找到 __ocs_fmt_0__ 個欄位。 + .fields-updated = 已更新 __ocs_fmt_0__ 個欄位。 + .invalid-keyword = 無效的選項關鍵字。 + .invalid-boundary = 無效的計數區域邊界物件。請選取由線段組成且不自我相交的封閉聚合線。 + .block-not-found = 找不到圖塊 "__ocs_fmt_0__"。 + .invalid-point = 無效的點。 diff --git a/src/app/command_driver/viewport.rs b/src/app/command_driver/viewport.rs index 35ceafa46..f8eadd485 100644 --- a/src/app/command_driver/viewport.rs +++ b/src/app/command_driver/viewport.rs @@ -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; diff --git a/src/app/commands/count.rs b/src/app/commands/count.rs index d10041a43..06e7f90ea 100644 --- a/src/app/commands/count.rs +++ b/src/app/commands/count.rs @@ -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 = 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 = 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, _name: &str) -> Task { + fn start_count_command(&mut self, i: usize, command: Box) -> Task { 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 = 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, diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index 8d63e0176..974939311 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -926,8 +926,6 @@ impl OpenCADStudio { }; Message::PcManager(PcManagerMsg::Toggle(handle, row)) } - // Point Cloud Manager tree: "search=", "collapse", "expand", - // "toggle_node=" or "select=". // Count palette and count mode: "search=", "sort=name|count", // "open=NAME", "child=NAME:" (an expanded row), "menu=:NAME", "create_table", "check=NAME:1", @@ -1000,6 +998,8 @@ impl OpenCADStudio { _ => return Err(bad()), }) } + // Point Cloud Manager tree: "search=", "collapse", "expand", + // "toggle_node=" or "select=". "pc_manager" => { use crate::ui::window::pc_manager::PcManagerMsg as M; let value = string(req, "value")?; diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 7a0af5777..021102307 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -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, diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 40758aea5..f657dec83 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -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, } } diff --git a/src/modules/annotate/count_cmd.rs b/src/modules/annotate/count_cmd.rs index f2be96126..9ac6c3d3f 100644 --- a/src/modules/annotate/count_cmd.rs +++ b/src/modules/annotate/count_cmd.rs @@ -9,8 +9,8 @@ // // The command gathers its answers and hands them to the application as // `_COUNTRUN L|T ` (COUNTAREA: `_COUNTAREASET `), the -// area being `C` (current view), `E` (entire model space), `R x,y x,y`, -// `P x,y x,y …` or `O `. +// 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 `. 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 = 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), } } diff --git a/src/modules/view/count_palette.rs b/src/modules/view/count_palette.rs index fd1d4e652..97321536f 100644 --- a/src/modules/view/count_palette.rs +++ b/src/modules/view/count_palette.rs @@ -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()), } diff --git a/src/ui/icons.rs b/src/ui/icons.rs index 7a2385fb4..a2b5af4e7 100644 --- a/src/ui/icons.rs +++ b/src/ui/icons.rs @@ -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"); diff --git a/src/ui/popup/context_menu.rs b/src/ui/popup/context_menu.rs index 407aa66c2..595184c8d 100644 --- a/src/ui/popup/context_menu.rs +++ b/src/ui/popup/context_menu.rs @@ -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); diff --git a/src/ui/window/count_palette.rs b/src/ui/window/count_palette.rs index 1f31eeb31..8f8e08d70 100644 --- a/src/ui/window/count_palette.rs +++ b/src/ui/window/count_palette.rs @@ -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>, std::rc::Rc>)>>, + /// The list's references for (document tab id, geometry epoch, area). + pub list_cache: std::cell::RefCell>, std::rc::Rc>)>>, } impl Default for CountPalette { @@ -313,7 +314,7 @@ fn list_row<'a>( name: &str, key: CountKey, label: String, - count: String, + count: Option, errors: bool, depth: u16, expanded: Option, @@ -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)