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:
Hakan Seven 2026-05-29 21:09:56 +03:00
commit 7d9ac435cb
4 changed files with 326 additions and 60 deletions

View file

@ -3392,22 +3392,23 @@ impl OpenCADStudio {
Message::QSelectOpen => {
let i = self.active_tab;
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
// Seed filters from the first selected entity (if any) so
// a right-click → Quick Select on a known object pre-fills
// the panel — same idea as Select Similar but exposes the
// pairing for editing.
// Seed the type filter from the first selected entity so a
// right-click → Quick Select on a known object opens the
// panel pre-tuned to that entity's type. Property defaults
// to "(Any property)" so the user immediately picks what
// they want to compare.
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(e) = self.tabs[i].scene.document.get_entity(h) {
use crate::entities::traits::entity_type_name;
type_filter = Some(entity_type_name(e).to_string());
layer_filter = Some(e.as_entity().layer().to_string());
}
}
self.qselect = Some(crate::app::QSelectState {
type_filter,
layer_filter,
property: None,
operator: crate::app::QSelectOp::Eq,
value: String::new(),
append: false,
});
Task::none()
@ -3420,14 +3421,41 @@ impl OpenCADStudio {
Message::QSelectSetType(t) => {
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.property = kept_property;
}
Task::none()
}
Message::QSelectSetLayer(l) => {
Message::QSelectSetProperty(p) => {
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()
}
@ -3446,7 +3474,9 @@ impl OpenCADStudio {
let i = self.active_tab;
let matched = self.tabs[i].scene.qselect(
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,
);
self.command_line