forked from 1b-agent/opencadstudio-upstream
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:
commit
6da9675c55
51 files changed
+2828
-43
No files matched your search
@@ -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"]
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
],
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user