Merge pull request #1614 from ramox81/codex/count-feature

Count: COUNT / COUNTAREA count mode, Count palette, count fields and tables
This commit is contained in:
Hakan Seven committed 2026-10-06 09:16:57 +03:00
commit 6da9675c55
51 files changed
+2828 -43

No files matched your search

+447
View File
@@ -0,0 +1,447 @@
// 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(),
// 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] <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 {
// 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(if first {
format!("{found} found")
} else {
format!("{found} found, {} total", 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 codec::count::valid_boundary(&pl) => {
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(),
),
}
}
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>,
/// Listing lines still to show after `Press ENTER to continue:`.
pending: Vec<String>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum TableStep {
Names,
List,
Point,
Page,
}
impl CountTableCommand {
pub fn new(blocks: Vec<String>, others: [usize; 3]) -> Self {
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<String>) -> Self {
Self { step: TableStep::Point, blocks: Vec::new(), others: [0; 3], names, pending: Vec::new() }
}
/// `?`: 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 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<String> = self.pending.drain(..self.pending.len().min(22)).collect();
if !self.pending.is_empty() {
self.step = TableStep::Page;
return CmdResult::ReportMeasurement(page.join("
"));
}
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]
))
}
}
/// 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"
}
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(),
TableStep::Page => "Press ENTER to continue:".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 => self.listing(text),
TableStep::Page => self.next_page(),
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 palette",
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()),
],
},