forked from 1b-agent/opencadstudio-upstream
feat: Add advanced selection filter
This commit is contained in:
1 parent
7c61583ac2
commit
09cf99f85c
8 files changed
+621
-80
No files matched your search
+174
-30
@@ -426,6 +426,27 @@ impl Scene {
|
||||
value: &str,
|
||||
mode: crate::app::QSelectMode,
|
||||
append: bool,
|
||||
) -> usize {
|
||||
let condition = crate::app::QSelectCondition {
|
||||
type_filter: type_name.map(str::to_owned),
|
||||
property: property_field.map(|field| crate::app::QSelectPropertyChoice {
|
||||
field: field.to_owned(),
|
||||
label: field.to_owned(),
|
||||
editor: crate::app::QSelectValueEditor::Text,
|
||||
}),
|
||||
operator: op,
|
||||
value: value.to_owned(),
|
||||
};
|
||||
self.qselect_conditions(scope, &[condition], mode, append)
|
||||
}
|
||||
|
||||
/// Evaluate all conditions against the same candidate set, then apply result behavior once.
|
||||
pub fn qselect_conditions(
|
||||
&mut self,
|
||||
scope: crate::app::QSelectScope,
|
||||
conditions: &[crate::app::QSelectCondition],
|
||||
mode: crate::app::QSelectMode,
|
||||
append: bool,
|
||||
) -> usize {
|
||||
use crate::app::{QSelectMode, QSelectOp};
|
||||
use crate::entities::traits::entity_type_name;
|
||||
@@ -436,33 +457,44 @@ impl Scene {
|
||||
let Some(e) = self.document.get_entity(h) else {
|
||||
continue;
|
||||
};
|
||||
let type_ok = type_name.is_none_or(|t| entity_type_name(e) == t);
|
||||
let prop_ok = if !type_ok {
|
||||
true
|
||||
} else {
|
||||
match (property_field, op) {
|
||||
(None, _) | (_, QSelectOp::Any) => true,
|
||||
(Some(field), op) => match self.entity_property_value(e, field) {
|
||||
Some(actual) => match op {
|
||||
QSelectOp::Eq => actual.eq_ignore_ascii_case(value),
|
||||
QSelectOp::Neq => !actual.eq_ignore_ascii_case(value),
|
||||
QSelectOp::Gt | QSelectOp::Lt => {
|
||||
match (
|
||||
crate::entities::common::parse_f64(&actual),
|
||||
crate::entities::common::parse_f64(value),
|
||||
) {
|
||||
(Some(a), Some(b)) if matches!(op, QSelectOp::Gt) => a > b,
|
||||
(Some(a), Some(b)) => a < b,
|
||||
_ => false,
|
||||
let matches_filter = conditions.iter().all(|condition| {
|
||||
let type_ok = condition
|
||||
.type_filter
|
||||
.as_deref()
|
||||
.is_none_or(|t| entity_type_name(e) == t);
|
||||
let prop_ok = if !type_ok {
|
||||
true
|
||||
} else {
|
||||
match (
|
||||
condition
|
||||
.property
|
||||
.as_ref()
|
||||
.map(|property| property.field.as_str()),
|
||||
condition.operator,
|
||||
) {
|
||||
(None, _) | (_, QSelectOp::Any) => true,
|
||||
(Some(field), op) => match self.entity_property_value(e, field) {
|
||||
Some(actual) => match op {
|
||||
QSelectOp::Eq => actual.eq_ignore_ascii_case(&condition.value),
|
||||
QSelectOp::Neq => !actual.eq_ignore_ascii_case(&condition.value),
|
||||
QSelectOp::Gt | QSelectOp::Lt => {
|
||||
match (
|
||||
crate::entities::common::parse_f64(&actual),
|
||||
crate::entities::common::parse_f64(&condition.value),
|
||||
) {
|
||||
(Some(a), Some(b)) if matches!(op, QSelectOp::Gt) => a > b,
|
||||
(Some(a), Some(b)) => a < b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
QSelectOp::Any => true,
|
||||
QSelectOp::Any => true,
|
||||
},
|
||||
None => false,
|
||||
},
|
||||
None => false,
|
||||
},
|
||||
}
|
||||
};
|
||||
let matches_filter = type_ok && prop_ok;
|
||||
}
|
||||
};
|
||||
type_ok && prop_ok
|
||||
});
|
||||
let keep = match mode {
|
||||
QSelectMode::Include => matches_filter,
|
||||
QSelectMode::Exclude => !matches_filter,
|
||||
@@ -582,6 +614,17 @@ impl Scene {
|
||||
&self,
|
||||
type_name: Option<&str>,
|
||||
scope: crate::app::QSelectScope,
|
||||
) -> Vec<crate::app::QSelectPropertyChoice> {
|
||||
self.qselect_properties_with_sample(type_name, scope, None)
|
||||
}
|
||||
|
||||
/// Use the picked entity's own property schema, including optional fields
|
||||
/// that may not be present on the first entity of this type in the drawing.
|
||||
fn qselect_properties_with_sample(
|
||||
&self,
|
||||
type_name: Option<&str>,
|
||||
scope: crate::app::QSelectScope,
|
||||
sample_handle: Option<Handle>,
|
||||
) -> Vec<crate::app::QSelectPropertyChoice> {
|
||||
use crate::app::{QSelectPropertyChoice, QSelectValueEditor};
|
||||
use crate::entities::traits::{entity_type_name, EntityTypeOps};
|
||||
@@ -680,11 +723,16 @@ impl Scene {
|
||||
.iter()
|
||||
.map(|s| s.name.clone())
|
||||
.collect();
|
||||
let sample = candidate_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.filter_map(|h| self.document.get_entity(h))
|
||||
.find(|e| entity_type_name(e) == t);
|
||||
let sample = sample_handle
|
||||
.and_then(|handle| self.document.get_entity(handle))
|
||||
.filter(|entity| entity_type_name(entity) == t)
|
||||
.or_else(|| {
|
||||
candidate_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.filter_map(|h| self.document.get_entity(h))
|
||||
.find(|e| entity_type_name(e) == t)
|
||||
});
|
||||
if let Some(sample) = sample {
|
||||
let mut sections = vec![crate::scene::cache::properties::general_section(sample)];
|
||||
if let Some(section) =
|
||||
@@ -836,6 +884,58 @@ impl Scene {
|
||||
out
|
||||
}
|
||||
|
||||
/// Build editable equality conditions without changing the current selection.
|
||||
/// The unique handle and empty values are deliberately not sampled.
|
||||
pub fn qselect_conditions_from_entity(
|
||||
&self,
|
||||
handle: Handle,
|
||||
) -> Vec<crate::app::QSelectCondition> {
|
||||
use crate::app::{QSelectCondition, QSelectOp, QSelectScope, QSelectValueEditor};
|
||||
let Some(entity) = self.document.get_entity(handle) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let type_name = crate::entities::traits::entity_type_name(entity).to_owned();
|
||||
let mut conditions = vec![QSelectCondition {
|
||||
type_filter: Some(type_name.clone()),
|
||||
property: None,
|
||||
operator: QSelectOp::Eq,
|
||||
value: String::new(),
|
||||
}];
|
||||
let properties = self.qselect_properties_with_sample(
|
||||
Some(&type_name),
|
||||
QSelectScope::CurrentSpace,
|
||||
Some(handle),
|
||||
);
|
||||
for mut property in properties {
|
||||
if property.field == "handle" {
|
||||
continue;
|
||||
}
|
||||
let Some(value) = self.entity_property_value(entity, &property.field) else {
|
||||
continue;
|
||||
};
|
||||
if value.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Ensure a sampled choice can also be edited, even outside the
|
||||
// filter's current-selection scope.
|
||||
if let QSelectValueEditor::Choice(options) = &mut property.editor {
|
||||
if !options.contains(&value) {
|
||||
options.push(value.clone());
|
||||
}
|
||||
}
|
||||
let condition = QSelectCondition {
|
||||
type_filter: Some(type_name.clone()),
|
||||
property: Some(property),
|
||||
operator: QSelectOp::Eq,
|
||||
value,
|
||||
};
|
||||
if condition.validation_error().is_none() {
|
||||
conditions.push(condition);
|
||||
}
|
||||
}
|
||||
conditions
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -1172,6 +1272,50 @@ impl Scene {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn qselect_sample_matches_identical_entities_without_unique_handles() {
|
||||
use crate::app::{QSelectMode, QSelectScope};
|
||||
use codec::entities::Line;
|
||||
use codec::types::Vector3;
|
||||
let mut scene = Scene::new();
|
||||
let line = || {
|
||||
EntityType::Line(Line::from_points(
|
||||
Vector3::ZERO,
|
||||
Vector3::new(10.0, 0.0, 0.0),
|
||||
))
|
||||
};
|
||||
let first = scene.add_entity(line());
|
||||
let second = scene.add_entity(line());
|
||||
scene.select_entity(second, false);
|
||||
let before = scene.selected.clone();
|
||||
let conditions = scene.qselect_conditions_from_entity(first);
|
||||
assert_eq!(
|
||||
scene.selected, before,
|
||||
"sampling must not change the selection"
|
||||
);
|
||||
assert!(conditions.iter().any(|condition| condition
|
||||
.property
|
||||
.as_ref()
|
||||
.is_some_and(|property| property.field == "layer")));
|
||||
assert!(conditions.iter().all(|condition| condition
|
||||
.property
|
||||
.as_ref()
|
||||
.is_none_or(|property| property.field != "handle")));
|
||||
assert!(conditions
|
||||
.iter()
|
||||
.all(|condition| condition.validation_error().is_none()));
|
||||
assert_eq!(
|
||||
scene.qselect_conditions(
|
||||
QSelectScope::CurrentSpace,
|
||||
&conditions,
|
||||
QSelectMode::Include,
|
||||
false,
|
||||
),
|
||||
2
|
||||
);
|
||||
assert!(scene.selected.contains(&first) && scene.selected.contains(&second));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pe_url_of_reads_standard_hyperlink_xdata() {
|
||||
use codec::entities::Point;
|
||||
|
||||
Reference in new issue
Block a user