WIP count feature

This commit is contained in:
ramox81 committed 2026-10-02 17:52:13 +03:00
1 parent b8def31145
commit e29b408bda
26 files changed
+1887 -44

No files matched your search

Generated
+1 -1
View File
@@ -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",
+3
View File
@@ -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" }
+728
View File
@@ -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<Handle> {
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<Task<Message>> {
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<String> = preselected.iter().map(|h| hex(*h)).collect();
self.tabs[i].scene.deselect_all();
return Some(self.dispatch_command(&format!("_COUNTRUN C T {}", handles.join(","))));
}
// 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<Handle> = 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<f64> = point.split(',').filter_map(|v| v.parse().ok()).collect();
if p.len() == 3 {
let names: Vec<String> = names.split('|').filter(|n| !n.is_empty()).map(str::to_string).collect();
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<dyn CadCommand>, _name: &str) -> Task<Message> {
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::<i32>() {
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<Vec<[f64; 2]>>, Option<(Handle, bool)>)> {
let kind = tokens.first().copied().unwrap_or("E");
let points = || tokens[1..].iter().map_while(|t| parse_xy(t)).collect::<Vec<_>>();
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<Handle> {
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 <area> L` opens the palette; `_COUNTRUN <area> T <handles>`
/// 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<Handle> = 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<String> {
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<Message> {
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<String>) {
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<String> = 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<String> =
names.iter().map(|n| self.block_field_code(i, n, &CountKey::default())).collect();
self.push_undo_snapshot(i, "COUNTTABLE");
let Some(handle) = self.commit_entity_handle(EntityType::Table(Box::new(table))) else {
return;
};
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<String> = 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<Message> {
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<String> = if on {
let area = self.tabs[i].count.as_ref().and_then(|m| m.area.clone());
let instances = codec::count::block_instances(&self.tabs[i].scene.document, area.as_deref());
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()
}
}
+9
View File
@@ -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;
-39
View File
@@ -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<String, usize> = 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();
}
+4
View File
@@ -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);
}
+3
View File
@@ -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<RefEditSession>,
/// COUNT mode, while it is on.
pub(crate) count: Option<crate::ui::window::count_palette::CountMode>,
/// 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<BlockEditSession>,
@@ -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(),
+3
View File
@@ -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<BgTarget>,
@@ -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<Vec<u8>>), String>),
/// Result of the PDFATTACH file picker.
PdfAttachPickResult(Result<(std::path::PathBuf, std::sync::Arc<Vec<u8>>), 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(),
+20
View File
@@ -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,
}
}
}
+5
View File
@@ -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,
}
}
+7 -1
View File
@@ -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" {
+3
View File
@@ -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()
+15
View File
@@ -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<Element<'_, Message>> {
@@ -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,
+395
View File
@@ -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] <Current area>:
// Specify opposite corner: (Polygonal: Specify start point: /
// Specify next point:; Object:
// Select object as count area boundary:)
// Select target objects or [List all blocks] <List all blocks>: (COUNT)
//
// The command gathers its answers and hands them to the application as
// `_COUNTRUN <area> L|T <handles>` (COUNTAREA: `_COUNTAREASET <area>`), the
// area being `C` (current view), `E` (entire model space), `R x,y x,y`,
// `P x,y x,y …` or `O <handle>`.
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<DVec3>,
targets: Vec<Handle>,
picked: Option<EntityType>,
}
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<String> = 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] <Current area>:".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] <List all blocks>:".into(),
}
}
fn options(&self) -> Vec<CmdOption> {
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<Handle>) -> 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<CmdResult> {
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<String> = 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<WireModel> {
let ring: Vec<DVec3> = 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<String>,
others: [usize; 3],
names: Vec<String>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum TableStep {
Names,
List,
Point,
}
impl CountTableCommand {
pub fn new(blocks: Vec<String>, 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<String>) -> 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 [?] <all blocks>:".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<CmdResult> {
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<Handle>,
}
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<Handle>) -> 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<String> = 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"]
});
+10 -1
View File
@@ -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) {
+1
View File
@@ -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;
+11
View File
@@ -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()),
}
}
+2
View File
@@ -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()),
],
},
+37 -1
View File
@@ -1930,6 +1930,15 @@ pub enum ViewportRefreshScope {
/// Entity membership in document order, keyed by geometry epoch.
type BlockMembers = (u64, HashMap<Handle, Vec<Handle>>);
/// What count mode colours: see [`Scene::count_display`].
#[derive(Debug, Clone, PartialEq, Default)]
pub struct CountDisplay {
pub counted: HashSet<Handle>,
pub errors: HashSet<Handle>,
pub color: [f32; 4],
pub error_color: [f32; 4],
}
pub struct Scene {
pub camera: Rc<RefCell<Camera>>,
/// 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<HashSet<Handle>>,
/// COUNT mode colours: counted references in COUNTCOLOR, overlapping
/// duplicates in COUNTERRORCOLOR, everything else faded. `None` outside
/// count mode.
pub count_display: Option<CountDisplay>,
/// 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<Handle>,
@@ -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<CountDisplay>) {
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;
};
+5
View File
@@ -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));
}
+6
View File
@@ -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 ─────────────────────────────────────────────────────────
+7
View File
@@ -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,
+2
View File
@@ -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,
+565
View File
@@ -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<Handle>,
matching: [bool; 3],
picked: bool,
},
/// Several picked objects counted as one group.
Group(Vec<Handle>),
}
/// 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<Vec<[f64; 2]>>,
/// 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<CountTarget>,
/// The counted reference ← / → last showed.
pub cursor: Option<usize>,
/// 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<Handle>,
pub errors: Vec<Handle>,
}
impl CountMode {
pub fn instances(&self, doc: &CadDocument) -> Vec<BlockInstance> {
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<String> {
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<Vec<String>>,
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<Row> {
let query = palette.search.trim().to_lowercase();
let mut rows: Vec<Row> = 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<bool>,
) -> 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<CountMsg>) -> 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))
}
+44 -1
View File
@@ -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<String>,
@@ -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();
+1
View File
@@ -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;