feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic QSELECT model — pick a type, pick one of its properties, compare with a typed value via a logical operator — is much more general: a Line entity can now be filtered on Start X, Length, Angle, Delta Y, …, and any entity on the common Layer / Color / Linetype / Lineweight. Panel layout: Object type → Property → Operator → Value → Append. The Property dropdown is type-aware: it always lists the four common properties; picking a specific Object type appends that type's `geometry_properties` rows, sampled from the first entity of that type in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only appear when their entity type is selected). Changing the Object type drops the Property when the new type no longer offers it. Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`, `* Any value`. Eq/Neq do case-insensitive string compare against the canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse both sides as `f64` and reject anything non-numeric; Any skips the value test entirely. The Value text field is disabled when no Property is picked or the operator is Any. Append behaves the same as before. The new value reader is hand-rolled for the four common properties (layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`, linetype name, lineweight formatted as `0.30mm`) and falls back through `geometry_properties()` for type-specific fields — extracting the property's value out of the `PropValue` variant so EditText / Choice / ColorChoice all canonicalise the same way the panel will display them. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9db206f310
commit
7d9ac435cb
4 changed files with 326 additions and 60 deletions
|
|
@ -56,13 +56,70 @@ pub struct GripPendingValue {
|
||||||
pub label: &'static str,
|
pub label: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open Quick Select panel state. `type_filter` / `layer_filter` of
|
/// Operator the Quick Select filter applies between an entity's
|
||||||
/// `None` match every entity in that field; otherwise the filter is the
|
/// property value and the user-typed test value.
|
||||||
/// literal name to compare against.
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum QSelectOp {
|
||||||
|
/// `*Any value` — the entity matches as long as the type filter
|
||||||
|
/// passes; the value column is ignored.
|
||||||
|
Any,
|
||||||
|
Eq,
|
||||||
|
Neq,
|
||||||
|
Gt,
|
||||||
|
Lt,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for QSelectOp {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let s = match self {
|
||||||
|
QSelectOp::Any => "* Any value",
|
||||||
|
QSelectOp::Eq => "= Equals",
|
||||||
|
QSelectOp::Neq => "!= Not equal",
|
||||||
|
QSelectOp::Gt => "> Greater than",
|
||||||
|
QSelectOp::Lt => "< Less than",
|
||||||
|
};
|
||||||
|
f.write_str(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One row in the Quick Select Properties pick_list. `field` is the
|
||||||
|
/// stable identifier (`"layer"`, `"start_x"`, …) used to look up the
|
||||||
|
/// value on each candidate entity; `label` is the human label rendered
|
||||||
|
/// in the dropdown. Equality only compares `field` so the pick_list
|
||||||
|
/// round-trips selection correctly even when labels are duplicated.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct QSelectPropertyChoice {
|
||||||
|
pub field: String,
|
||||||
|
pub label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for QSelectPropertyChoice {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.field == other.field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for QSelectPropertyChoice {}
|
||||||
|
|
||||||
|
impl std::fmt::Display for QSelectPropertyChoice {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&self.label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open Quick Select panel state. The filter is one
|
||||||
|
/// `(type, property, op, value)` row plus an Append-to-current-selection
|
||||||
|
/// toggle, mirroring the classic QSELECT dialog: the panel filters
|
||||||
|
/// candidate entities (entire layout) by type, then by the chosen
|
||||||
|
/// property compared to the typed value using the operator.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct QSelectState {
|
pub struct QSelectState {
|
||||||
|
/// `None` = "(Any type)".
|
||||||
pub type_filter: Option<String>,
|
pub type_filter: Option<String>,
|
||||||
pub layer_filter: Option<String>,
|
/// `None` = no property filter; the type filter alone applies.
|
||||||
|
pub property: Option<QSelectPropertyChoice>,
|
||||||
|
pub operator: QSelectOp,
|
||||||
|
pub value: String,
|
||||||
pub append: bool,
|
pub append: bool,
|
||||||
}
|
}
|
||||||
use crate::snap::Snapper;
|
use crate::snap::Snapper;
|
||||||
|
|
@ -652,8 +709,14 @@ pub enum Message {
|
||||||
QSelectClose,
|
QSelectClose,
|
||||||
/// Type filter — `None` means "any type".
|
/// Type filter — `None` means "any type".
|
||||||
QSelectSetType(Option<String>),
|
QSelectSetType(Option<String>),
|
||||||
/// Layer filter — `None` means "any layer".
|
/// Property to compare. `None` means "no property filter — just type
|
||||||
QSelectSetLayer(Option<String>),
|
/// filter applies"; the operator and value fields are ignored in
|
||||||
|
/// that case.
|
||||||
|
QSelectSetProperty(Option<QSelectPropertyChoice>),
|
||||||
|
/// Comparison operator.
|
||||||
|
QSelectSetOperator(QSelectOp),
|
||||||
|
/// Compare-against value (free-text input).
|
||||||
|
QSelectSetValue(String),
|
||||||
/// Append-to-current-selection toggle.
|
/// Append-to-current-selection toggle.
|
||||||
QSelectSetAppend(bool),
|
QSelectSetAppend(bool),
|
||||||
/// Apply the current filter and close the panel.
|
/// Apply the current filter and close the panel.
|
||||||
|
|
|
||||||
|
|
@ -3392,22 +3392,23 @@ impl OpenCADStudio {
|
||||||
Message::QSelectOpen => {
|
Message::QSelectOpen => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
|
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
|
||||||
// Seed filters from the first selected entity (if any) so
|
// Seed the type filter from the first selected entity so a
|
||||||
// a right-click → Quick Select on a known object pre-fills
|
// right-click → Quick Select on a known object opens the
|
||||||
// the panel — same idea as Select Similar but exposes the
|
// panel pre-tuned to that entity's type. Property defaults
|
||||||
// pairing for editing.
|
// to "(Any property)" so the user immediately picks what
|
||||||
|
// they want to compare.
|
||||||
let mut type_filter: Option<String> = None;
|
let mut type_filter: Option<String> = None;
|
||||||
let mut layer_filter: Option<String> = None;
|
|
||||||
if let Some(&h) = self.tabs[i].scene.selected.iter().next() {
|
if let Some(&h) = self.tabs[i].scene.selected.iter().next() {
|
||||||
if let Some(e) = self.tabs[i].scene.document.get_entity(h) {
|
if let Some(e) = self.tabs[i].scene.document.get_entity(h) {
|
||||||
use crate::entities::traits::entity_type_name;
|
use crate::entities::traits::entity_type_name;
|
||||||
type_filter = Some(entity_type_name(e).to_string());
|
type_filter = Some(entity_type_name(e).to_string());
|
||||||
layer_filter = Some(e.as_entity().layer().to_string());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.qselect = Some(crate::app::QSelectState {
|
self.qselect = Some(crate::app::QSelectState {
|
||||||
type_filter,
|
type_filter,
|
||||||
layer_filter,
|
property: None,
|
||||||
|
operator: crate::app::QSelectOp::Eq,
|
||||||
|
value: String::new(),
|
||||||
append: false,
|
append: false,
|
||||||
});
|
});
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
@ -3420,14 +3421,41 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
Message::QSelectSetType(t) => {
|
Message::QSelectSetType(t) => {
|
||||||
if let Some(state) = self.qselect.as_mut() {
|
if let Some(state) = self.qselect.as_mut() {
|
||||||
|
// Drop the property when it no longer applies to the
|
||||||
|
// chosen type: type-specific fields like `start_x`
|
||||||
|
// would otherwise stay selected but never match.
|
||||||
|
let kept_property = state.property.clone().and_then(|p| {
|
||||||
|
let i = self.active_tab;
|
||||||
|
let props = self.tabs[i].scene.qselect_properties(t.as_deref());
|
||||||
|
if props.iter().any(|(f, _)| f == &p.field) {
|
||||||
|
Some(p)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
state.type_filter = t;
|
state.type_filter = t;
|
||||||
|
state.property = kept_property;
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::QSelectSetLayer(l) => {
|
Message::QSelectSetProperty(p) => {
|
||||||
if let Some(state) = self.qselect.as_mut() {
|
if let Some(state) = self.qselect.as_mut() {
|
||||||
state.layer_filter = l;
|
state.property = p;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::QSelectSetOperator(op) => {
|
||||||
|
if let Some(state) = self.qselect.as_mut() {
|
||||||
|
state.operator = op;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::QSelectSetValue(v) => {
|
||||||
|
if let Some(state) = self.qselect.as_mut() {
|
||||||
|
state.value = v;
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -3446,7 +3474,9 @@ impl OpenCADStudio {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let matched = self.tabs[i].scene.qselect(
|
let matched = self.tabs[i].scene.qselect(
|
||||||
state.type_filter.as_deref(),
|
state.type_filter.as_deref(),
|
||||||
state.layer_filter.as_deref(),
|
state.property.as_ref().map(|p| p.field.as_str()),
|
||||||
|
state.operator,
|
||||||
|
&state.value,
|
||||||
state.append,
|
state.append,
|
||||||
);
|
);
|
||||||
self.command_line
|
self.command_line
|
||||||
|
|
|
||||||
|
|
@ -912,8 +912,10 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
let qselect_layer: Element<'_, Message> = if let Some(state) = &self.qselect {
|
let qselect_layer: Element<'_, Message> = if let Some(state) = &self.qselect {
|
||||||
let types = tab.scene.entity_type_names_in_layout();
|
let types = tab.scene.entity_type_names_in_layout();
|
||||||
let layers = tab.scene.entity_layers_in_layout();
|
let properties = tab
|
||||||
qselect_overlay(state, &types, &layers)
|
.scene
|
||||||
|
.qselect_properties(state.type_filter.as_deref());
|
||||||
|
qselect_overlay(state, &types, &properties)
|
||||||
} else {
|
} else {
|
||||||
iced::widget::Space::new().width(0).height(0).into()
|
iced::widget::Space::new().width(0).height(0).into()
|
||||||
};
|
};
|
||||||
|
|
@ -1516,17 +1518,19 @@ fn layout_context_menu_overlay(name: &str) -> Element<'_, Message> {
|
||||||
|
|
||||||
// ── Quick Select panel ─────────────────────────────────────────────────────
|
// ── Quick Select panel ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
const QSELECT_ANY: &str = "(Any)";
|
const QSELECT_ANY_TYPE: &str = "(Any type)";
|
||||||
|
const QSELECT_ANY_PROP: &str = "(Any property)";
|
||||||
|
|
||||||
/// Floating panel for the Quick Select feature. Two pick_lists (object
|
/// Floating panel for the Quick Select feature. Single-row filter:
|
||||||
/// type, layer) filter the candidate set down to entities matching both;
|
/// object type → property → operator → value, plus an "Append to current
|
||||||
/// "(Any)" in either slot skips that filter. The panel returns to the
|
/// selection" checkbox. The property dropdown is type-aware — Common
|
||||||
/// stored state on every render, so editing one filter doesn't reset the
|
/// properties (Layer, Color, Linetype, Lineweight) are always shown;
|
||||||
/// other. Outside-click and Cancel both dismiss without applying.
|
/// picking a specific Object type adds that type's `geometry_properties`
|
||||||
|
/// fields (Start X, Length, Radius, …) so type-specific filtering works.
|
||||||
fn qselect_overlay<'a>(
|
fn qselect_overlay<'a>(
|
||||||
state: &'a crate::app::QSelectState,
|
state: &'a crate::app::QSelectState,
|
||||||
types: &[&'static str],
|
types: &[&'static str],
|
||||||
layers: &[String],
|
properties: &[(String, String)],
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
use iced::widget::{checkbox, pick_list};
|
use iced::widget::{checkbox, pick_list};
|
||||||
const BG: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 0.98 };
|
const BG: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 0.98 };
|
||||||
|
|
@ -1537,19 +1541,43 @@ fn qselect_overlay<'a>(
|
||||||
const BTN_BG: Color = Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 };
|
const BTN_BG: Color = Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 };
|
||||||
const BTN_HOV: Color = Color { r: 0.30, g: 0.30, b: 0.30, a: 1.0 };
|
const BTN_HOV: Color = Color { r: 0.30, g: 0.30, b: 0.30, a: 1.0 };
|
||||||
|
|
||||||
let mut type_options: Vec<String> = vec![QSELECT_ANY.to_string()];
|
let mut type_options: Vec<String> = vec![QSELECT_ANY_TYPE.to_string()];
|
||||||
type_options.extend(types.iter().map(|s| (*s).to_string()));
|
type_options.extend(types.iter().map(|s| (*s).to_string()));
|
||||||
let mut layer_options: Vec<String> = vec![QSELECT_ANY.to_string()];
|
|
||||||
layer_options.extend(layers.iter().cloned());
|
let mut prop_options: Vec<crate::app::QSelectPropertyChoice> =
|
||||||
|
vec![crate::app::QSelectPropertyChoice {
|
||||||
|
field: String::new(),
|
||||||
|
label: QSELECT_ANY_PROP.to_string(),
|
||||||
|
}];
|
||||||
|
prop_options.extend(properties.iter().map(|(field, label)| {
|
||||||
|
crate::app::QSelectPropertyChoice {
|
||||||
|
field: field.clone(),
|
||||||
|
label: label.clone(),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
let op_options: Vec<crate::app::QSelectOp> = vec![
|
||||||
|
crate::app::QSelectOp::Eq,
|
||||||
|
crate::app::QSelectOp::Neq,
|
||||||
|
crate::app::QSelectOp::Gt,
|
||||||
|
crate::app::QSelectOp::Lt,
|
||||||
|
crate::app::QSelectOp::Any,
|
||||||
|
];
|
||||||
|
|
||||||
let type_sel = state
|
let type_sel = state
|
||||||
.type_filter
|
.type_filter
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| QSELECT_ANY.to_string());
|
.unwrap_or_else(|| QSELECT_ANY_TYPE.to_string());
|
||||||
let layer_sel = state
|
let prop_sel = state.property.clone().unwrap_or(crate::app::QSelectPropertyChoice {
|
||||||
.layer_filter
|
field: String::new(),
|
||||||
.clone()
|
label: QSELECT_ANY_PROP.to_string(),
|
||||||
.unwrap_or_else(|| QSELECT_ANY.to_string());
|
});
|
||||||
|
|
||||||
|
// The value field is disabled (visually de-emphasised; we still
|
||||||
|
// render the same widget) when no property is picked or the
|
||||||
|
// operator is "*Any value" — both of those skip the value test.
|
||||||
|
let value_enabled = state.property.is_some()
|
||||||
|
&& !matches!(state.operator, crate::app::QSelectOp::Any);
|
||||||
|
|
||||||
let label = |s: &'static str| text(s).size(12).color(TEXT).width(iced::Length::Fixed(90.0));
|
let label = |s: &'static str| text(s).size(12).color(TEXT).width(iced::Length::Fixed(90.0));
|
||||||
|
|
||||||
|
|
@ -1575,13 +1603,18 @@ fn qselect_overlay<'a>(
|
||||||
.padding([4, 14])
|
.padding([4, 14])
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut value_input = text_input("", &state.value).size(12);
|
||||||
|
if value_enabled {
|
||||||
|
value_input = value_input.on_input(Message::QSelectSetValue);
|
||||||
|
}
|
||||||
|
|
||||||
let panel_body = column![
|
let panel_body = column![
|
||||||
text("Quick Select").size(14).color(TEXT),
|
text("Quick Select").size(14).color(TEXT),
|
||||||
Space::new().height(10),
|
Space::new().height(10),
|
||||||
row![
|
row![
|
||||||
label("Object type:"),
|
label("Object type:"),
|
||||||
pick_list(type_options, Some(type_sel), |s: String| {
|
pick_list(type_options, Some(type_sel), |s: String| {
|
||||||
if s == QSELECT_ANY {
|
if s == QSELECT_ANY_TYPE {
|
||||||
Message::QSelectSetType(None)
|
Message::QSelectSetType(None)
|
||||||
} else {
|
} else {
|
||||||
Message::QSelectSetType(Some(s))
|
Message::QSelectSetType(Some(s))
|
||||||
|
|
@ -1593,18 +1626,30 @@ fn qselect_overlay<'a>(
|
||||||
.spacing(8),
|
.spacing(8),
|
||||||
Space::new().height(6),
|
Space::new().height(6),
|
||||||
row![
|
row![
|
||||||
label("Layer:"),
|
label("Property:"),
|
||||||
pick_list(layer_options, Some(layer_sel), |s: String| {
|
pick_list(prop_options, Some(prop_sel), |p: crate::app::QSelectPropertyChoice| {
|
||||||
if s == QSELECT_ANY {
|
if p.field.is_empty() {
|
||||||
Message::QSelectSetLayer(None)
|
Message::QSelectSetProperty(None)
|
||||||
} else {
|
} else {
|
||||||
Message::QSelectSetLayer(Some(s))
|
Message::QSelectSetProperty(Some(p))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.width(Fill),
|
.width(Fill),
|
||||||
]
|
]
|
||||||
.align_y(iced::Alignment::Center)
|
.align_y(iced::Alignment::Center)
|
||||||
.spacing(8),
|
.spacing(8),
|
||||||
|
Space::new().height(6),
|
||||||
|
row![
|
||||||
|
label("Operator:"),
|
||||||
|
pick_list(op_options, Some(state.operator), Message::QSelectSetOperator)
|
||||||
|
.width(Fill),
|
||||||
|
]
|
||||||
|
.align_y(iced::Alignment::Center)
|
||||||
|
.spacing(8),
|
||||||
|
Space::new().height(6),
|
||||||
|
row![label("Value:"), value_input,]
|
||||||
|
.align_y(iced::Alignment::Center)
|
||||||
|
.spacing(8),
|
||||||
Space::new().height(10),
|
Space::new().height(10),
|
||||||
row![
|
row![
|
||||||
checkbox(state.append)
|
checkbox(state.append)
|
||||||
|
|
@ -1627,7 +1672,7 @@ fn qselect_overlay<'a>(
|
||||||
|
|
||||||
let panel = container(panel_body)
|
let panel = container(panel_body)
|
||||||
.padding(16)
|
.padding(16)
|
||||||
.width(iced::Length::Fixed(360.0))
|
.width(iced::Length::Fixed(400.0))
|
||||||
.style(|_: &Theme| container::Style {
|
.style(|_: &Theme| container::Style {
|
||||||
background: Some(Background::Color(BG)),
|
background: Some(Background::Color(BG)),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
|
|
||||||
168
src/scene/mod.rs
168
src/scene/mod.rs
|
|
@ -3763,18 +3763,24 @@ impl Scene {
|
||||||
added
|
added
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replaces (or extends, when `append` is true) the current selection
|
/// Replaces (or extends, when `append` is true) the current
|
||||||
/// with every entity in the active layout that matches the optional
|
/// selection with every entity in the active layout that matches
|
||||||
/// `type_name` and `layer` filters. A `None` filter means "any".
|
/// the filter. Returns the number of newly-matching entities.
|
||||||
/// Returns the number of matching entities (i.e. the post-call
|
///
|
||||||
/// selection size if `append` was false, or the new total if it was
|
/// `type_name` of `None` means "any type". `property_field` of
|
||||||
/// true and previously-selected matches are not double-counted).
|
/// `None` skips the property test (only the type filter applies).
|
||||||
|
/// The operator's `Any` variant also skips the property test.
|
||||||
|
/// Numeric operators (`Gt` / `Lt`) parse both sides as `f64` and
|
||||||
|
/// reject anything non-numeric.
|
||||||
pub fn qselect(
|
pub fn qselect(
|
||||||
&mut self,
|
&mut self,
|
||||||
type_name: Option<&str>,
|
type_name: Option<&str>,
|
||||||
layer: Option<&str>,
|
property_field: Option<&str>,
|
||||||
|
op: crate::app::QSelectOp,
|
||||||
|
value: &str,
|
||||||
append: bool,
|
append: bool,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
|
use crate::app::QSelectOp;
|
||||||
use crate::entities::traits::entity_type_name;
|
use crate::entities::traits::entity_type_name;
|
||||||
if !append {
|
if !append {
|
||||||
self.selected.clear();
|
self.selected.clear();
|
||||||
|
|
@ -3785,9 +3791,37 @@ impl Scene {
|
||||||
let Some(e) = self.document.get_entity(h) else {
|
let Some(e) = self.document.get_entity(h) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let type_ok = type_name.is_none_or(|t| entity_type_name(e) == t);
|
if let Some(t) = type_name {
|
||||||
let layer_ok = layer.is_none_or(|l| e.as_entity().layer() == l);
|
if entity_type_name(e) != t {
|
||||||
if type_ok && layer_ok {
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let prop_ok = match (property_field, op) {
|
||||||
|
(None, _) | (_, QSelectOp::Any) => true,
|
||||||
|
(Some(field), op) => {
|
||||||
|
let Some(actual) = self.entity_property_value(e, field) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
match op {
|
||||||
|
QSelectOp::Eq => actual.eq_ignore_ascii_case(value),
|
||||||
|
QSelectOp::Neq => !actual.eq_ignore_ascii_case(value),
|
||||||
|
QSelectOp::Gt | QSelectOp::Lt => {
|
||||||
|
let (Ok(a), Ok(b)) =
|
||||||
|
(actual.parse::<f64>(), value.parse::<f64>())
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if matches!(op, QSelectOp::Gt) {
|
||||||
|
a > b
|
||||||
|
} else {
|
||||||
|
a < b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QSelectOp::Any => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if prop_ok {
|
||||||
self.selected.insert(h);
|
self.selected.insert(h);
|
||||||
matched += 1;
|
matched += 1;
|
||||||
}
|
}
|
||||||
|
|
@ -3811,18 +3845,112 @@ impl Scene {
|
||||||
names.into_iter().collect()
|
names.into_iter().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the sorted set of layer names referenced by entities in
|
/// Returns the list of `(field, label)` pairs the Quick Select
|
||||||
/// the active layout. Used to populate the Quick Select "Layer"
|
/// "Properties" dropdown should show given the current type filter:
|
||||||
/// dropdown.
|
///
|
||||||
pub fn entity_layers_in_layout(&self) -> Vec<String> {
|
/// * Common properties (Layer, Color, Linetype, Lineweight) are
|
||||||
let mut names: std::collections::BTreeSet<String> =
|
/// always included.
|
||||||
std::collections::BTreeSet::new();
|
/// * When `type_name` names a specific entity type present in the
|
||||||
for h in self.current_layout_entity_handles() {
|
/// active layout, the first entity of that type contributes its
|
||||||
if let Some(e) = self.document.get_entity(h) {
|
/// `geometry_properties()` rows (Start X, Length, Radius, …) so
|
||||||
names.insert(e.as_entity().layer().to_string());
|
/// type-specific filtering works.
|
||||||
|
pub fn qselect_properties(
|
||||||
|
&self,
|
||||||
|
type_name: Option<&str>,
|
||||||
|
) -> Vec<(String, String)> {
|
||||||
|
use crate::entities::traits::{entity_type_name, EntityTypeOps};
|
||||||
|
let mut out: Vec<(String, String)> = vec![
|
||||||
|
("layer".to_string(), "Layer".to_string()),
|
||||||
|
("color".to_string(), "Color".to_string()),
|
||||||
|
("linetype".to_string(), "Linetype".to_string()),
|
||||||
|
("lineweight".to_string(), "Lineweight".to_string()),
|
||||||
|
];
|
||||||
|
if let Some(t) = type_name {
|
||||||
|
let text_style_names: Vec<String> = self
|
||||||
|
.document
|
||||||
|
.text_styles
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.name.clone())
|
||||||
|
.collect();
|
||||||
|
let sample = self
|
||||||
|
.current_layout_entity_handles()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|h| self.document.get_entity(h))
|
||||||
|
.find(|e| entity_type_name(e) == t);
|
||||||
|
if let Some(sample) = sample {
|
||||||
|
if let Some(section) = sample.geometry_properties(&text_style_names) {
|
||||||
|
for prop in section.props {
|
||||||
|
// Skip rows that don't sensibly compare via
|
||||||
|
// `entity_property_value` (read-only labels are
|
||||||
|
// fine — users can still match against them).
|
||||||
|
out.push((prop.field.to_string(), prop.label.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
names.into_iter().collect()
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a property value from an entity for QSELECT comparison.
|
||||||
|
/// Returns the canonical string used as the left-hand side of the
|
||||||
|
/// operator test. Common properties have hand-rolled formatting so
|
||||||
|
/// `"ByLayer"` / `"7"` / `"0.30mm"` are stable; everything else
|
||||||
|
/// goes through `geometry_properties()` and pulls the matching
|
||||||
|
/// row's value out.
|
||||||
|
pub fn entity_property_value(
|
||||||
|
&self,
|
||||||
|
entity: &acadrust::EntityType,
|
||||||
|
field: &str,
|
||||||
|
) -> Option<String> {
|
||||||
|
use crate::entities::traits::EntityTypeOps;
|
||||||
|
use crate::scene::object::PropValue;
|
||||||
|
match field {
|
||||||
|
"layer" => Some(entity.common().layer.clone()),
|
||||||
|
"color" => Some(Self::format_color(entity.common().color)),
|
||||||
|
"linetype" => Some(entity.common().linetype.clone()),
|
||||||
|
"lineweight" => Some(Self::format_lineweight(entity.common().line_weight)),
|
||||||
|
_ => {
|
||||||
|
let text_style_names: Vec<String> = self
|
||||||
|
.document
|
||||||
|
.text_styles
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.name.clone())
|
||||||
|
.collect();
|
||||||
|
let section = entity.geometry_properties(&text_style_names)?;
|
||||||
|
let prop = section.props.into_iter().find(|p| p.field == field)?;
|
||||||
|
Some(match prop.value {
|
||||||
|
PropValue::ReadOnly(s) | PropValue::EditText(s) => s,
|
||||||
|
PropValue::LayerChoice(s) => s,
|
||||||
|
PropValue::Choice { selected, .. } => selected,
|
||||||
|
PropValue::ColorChoice(c) => Self::format_color(c),
|
||||||
|
PropValue::LwChoice(lw) => Self::format_lineweight(lw),
|
||||||
|
PropValue::LinetypeChoice(s) => s,
|
||||||
|
PropValue::HatchPatternChoice(s) => s,
|
||||||
|
PropValue::BoolToggle { value, .. } => value.to_string(),
|
||||||
|
PropValue::ColorVaries | PropValue::LwVaries => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_color(c: acadrust::types::Color) -> String {
|
||||||
|
use acadrust::types::Color;
|
||||||
|
match c {
|
||||||
|
Color::ByLayer => "ByLayer".to_string(),
|
||||||
|
Color::ByBlock => "ByBlock".to_string(),
|
||||||
|
Color::Index(i) => i.to_string(),
|
||||||
|
Color::Rgb { r, g, b } => format!("{},{},{}", r, g, b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_lineweight(lw: acadrust::types::LineWeight) -> String {
|
||||||
|
use acadrust::types::LineWeight;
|
||||||
|
match lw {
|
||||||
|
LineWeight::ByLayer => "ByLayer".to_string(),
|
||||||
|
LineWeight::ByBlock => "ByBlock".to_string(),
|
||||||
|
LineWeight::Default => "Default".to_string(),
|
||||||
|
LineWeight::Value(v) => format!("{:.2}mm", v as f64 / 100.0),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Erase ─────────────────────────────────────────────────────────────
|
// ── Erase ─────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue