forked from 1b-agent/opencadstudio-upstream
1 parent
e07f4f959b
commit
afc94dce13
6 files changed
+722
-163
No files matched your search
+44
-4
@@ -109,6 +109,38 @@ pub enum QSelectOp {
|
||||
Lt,
|
||||
}
|
||||
|
||||
/// Candidate set searched by Quick Select.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum QSelectScope {
|
||||
CurrentSpace,
|
||||
CurrentSelection,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for QSelectScope {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let source = match self {
|
||||
QSelectScope::CurrentSpace => "Current space",
|
||||
QSelectScope::CurrentSelection => "Current selection",
|
||||
};
|
||||
f.write_str(crate::t!(source).as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether matching candidates are kept or removed from the result.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum QSelectMode {
|
||||
Include,
|
||||
Exclude,
|
||||
}
|
||||
|
||||
/// Editor used by the Quick Select value field.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum QSelectValueEditor {
|
||||
Text,
|
||||
Number,
|
||||
Choice(Vec<String>),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for QSelectOp {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
@@ -131,6 +163,7 @@ impl std::fmt::Display for QSelectOp {
|
||||
pub struct QSelectPropertyChoice {
|
||||
pub field: String,
|
||||
pub label: String,
|
||||
pub editor: QSelectValueEditor,
|
||||
}
|
||||
|
||||
impl PartialEq for QSelectPropertyChoice {
|
||||
@@ -148,19 +181,22 @@ impl std::fmt::Display for QSelectPropertyChoice {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// `(scope, type, property, op, value)` row plus result behavior.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct QSelectState {
|
||||
pub scope: QSelectScope,
|
||||
pub available_types: Vec<String>,
|
||||
pub available_properties: Vec<QSelectPropertyChoice>,
|
||||
pub candidate_count: usize,
|
||||
/// `None` = "(Any type)".
|
||||
pub type_filter: Option<String>,
|
||||
/// `None` = no property filter; the type filter alone applies.
|
||||
pub property: Option<QSelectPropertyChoice>,
|
||||
pub operator: QSelectOp,
|
||||
pub value: String,
|
||||
pub mode: QSelectMode,
|
||||
pub append: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -2493,6 +2529,8 @@ pub enum Message {
|
||||
QSelectOpen,
|
||||
/// Close the Quick Select panel without applying.
|
||||
QSelectClose,
|
||||
/// Candidate scope: active space or the current selection.
|
||||
QSelectSetScope(QSelectScope),
|
||||
/// Type filter — `None` means "any type".
|
||||
QSelectSetType(Option<String>),
|
||||
/// Property to compare. `None` means "no property filter — just type
|
||||
@@ -2503,6 +2541,8 @@ pub enum Message {
|
||||
QSelectSetOperator(QSelectOp),
|
||||
/// Compare-against value (free-text input).
|
||||
QSelectSetValue(String),
|
||||
/// Include or exclude objects matching the filter.
|
||||
QSelectSetMode(QSelectMode),
|
||||
/// Append-to-current-selection toggle.
|
||||
QSelectSetAppend(bool),
|
||||
/// Apply the current filter and close the panel.
|
||||
|
||||
@@ -1313,12 +1313,24 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
type_filter = Some(entity_type_name(e).to_string());
|
||||
}
|
||||
}
|
||||
let scope = crate::app::QSelectScope::CurrentSpace;
|
||||
let available_types = self.tabs[i].scene.qselect_entity_type_names(scope);
|
||||
let available_properties = self.tabs[i]
|
||||
.scene
|
||||
.qselect_properties(type_filter.as_deref(), scope);
|
||||
let candidate_count = self.tabs[i].scene.qselect_candidate_count(scope);
|
||||
self.qselect = Some(crate::app::QSelectState {
|
||||
scope,
|
||||
available_types,
|
||||
available_properties,
|
||||
candidate_count,
|
||||
type_filter,
|
||||
property: None,
|
||||
operator: crate::app::QSelectOp::Eq,
|
||||
value: String::new(),
|
||||
mode: crate::app::QSelectMode::Include,
|
||||
append: false,
|
||||
error: None,
|
||||
});
|
||||
self.reset_modal_geometry();
|
||||
Task::none()
|
||||
|
||||
+141
-14
@@ -3845,22 +3845,77 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::QSelectSetType(t) => {
|
||||
Message::QSelectSetScope(scope) => {
|
||||
let i = self.active_tab;
|
||||
let available_types = self.tabs[i].scene.qselect_entity_type_names(scope);
|
||||
let type_filter = self.qselect.as_ref().and_then(|state| {
|
||||
state
|
||||
.type_filter
|
||||
.as_ref()
|
||||
.filter(|selected| available_types.iter().any(|item| item == *selected))
|
||||
.cloned()
|
||||
});
|
||||
let available_properties = self.tabs[i]
|
||||
.scene
|
||||
.qselect_properties(type_filter.as_deref(), scope);
|
||||
let candidate_count = self.tabs[i].scene.qselect_candidate_count(scope);
|
||||
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.scope = scope;
|
||||
state.available_types = available_types;
|
||||
state.available_properties = available_properties;
|
||||
state.candidate_count = candidate_count;
|
||||
state.type_filter = type_filter;
|
||||
state.property = state.property.as_ref().and_then(|selected| {
|
||||
state
|
||||
.available_properties
|
||||
.iter()
|
||||
.find(|available| available.field == selected.field)
|
||||
.cloned()
|
||||
});
|
||||
state.value.clear();
|
||||
state.error = None;
|
||||
if matches!(scope, crate::app::QSelectScope::CurrentSelection) {
|
||||
state.append = false;
|
||||
}
|
||||
if matches!(state.operator, crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt)
|
||||
&& !state.property.as_ref().is_some_and(|property| {
|
||||
matches!(property.editor, crate::app::QSelectValueEditor::Number)
|
||||
})
|
||||
{
|
||||
state.operator = crate::app::QSelectOp::Eq;
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::QSelectSetType(t) => {
|
||||
let i = self.active_tab;
|
||||
let scope = self
|
||||
.qselect
|
||||
.as_ref()
|
||||
.map_or(crate::app::QSelectScope::CurrentSpace, |state| state.scope);
|
||||
let properties = self.tabs[i]
|
||||
.scene
|
||||
.qselect_properties(t.as_deref(), scope);
|
||||
if let Some(state) = self.qselect.as_mut() {
|
||||
let kept_property = state.property.as_ref().and_then(|selected| {
|
||||
properties
|
||||
.iter()
|
||||
.find(|available| available.field == selected.field)
|
||||
.cloned()
|
||||
});
|
||||
state.type_filter = t;
|
||||
state.available_properties = properties;
|
||||
state.property = kept_property;
|
||||
state.value.clear();
|
||||
state.error = None;
|
||||
if matches!(state.operator, crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt)
|
||||
&& !state.property.as_ref().is_some_and(|property| {
|
||||
matches!(property.editor, crate::app::QSelectValueEditor::Number)
|
||||
})
|
||||
{
|
||||
state.operator = crate::app::QSelectOp::Eq;
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@@ -3868,6 +3923,15 @@ impl OpenCADStudio {
|
||||
Message::QSelectSetProperty(p) => {
|
||||
if let Some(state) = self.qselect.as_mut() {
|
||||
state.property = p;
|
||||
state.value.clear();
|
||||
state.error = None;
|
||||
if matches!(state.operator, crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt)
|
||||
&& !state.property.as_ref().is_some_and(|property| {
|
||||
matches!(property.editor, crate::app::QSelectValueEditor::Number)
|
||||
})
|
||||
{
|
||||
state.operator = crate::app::QSelectOp::Eq;
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@@ -3875,6 +3939,7 @@ impl OpenCADStudio {
|
||||
Message::QSelectSetOperator(op) => {
|
||||
if let Some(state) = self.qselect.as_mut() {
|
||||
state.operator = op;
|
||||
state.error = None;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@@ -3882,29 +3947,91 @@ impl OpenCADStudio {
|
||||
Message::QSelectSetValue(v) => {
|
||||
if let Some(state) = self.qselect.as_mut() {
|
||||
state.value = v;
|
||||
state.error = None;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::QSelectSetMode(mode) => {
|
||||
if let Some(state) = self.qselect.as_mut() {
|
||||
state.mode = mode;
|
||||
state.error = None;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::QSelectSetAppend(b) => {
|
||||
if let Some(state) = self.qselect.as_mut() {
|
||||
state.append = b;
|
||||
if matches!(state.scope, crate::app::QSelectScope::CurrentSpace) {
|
||||
state.append = b;
|
||||
}
|
||||
state.error = None;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::QSelectApply => {
|
||||
let validation_error = self.qselect.as_ref().and_then(|state| {
|
||||
let candidate_count = self.tabs[self.active_tab]
|
||||
.scene
|
||||
.qselect_candidate_count(state.scope);
|
||||
if candidate_count == 0 {
|
||||
Some(crate::t!("No objects are available in this scope.").into_owned())
|
||||
} else if let Some(property) = state.property.as_ref() {
|
||||
if matches!(state.operator, crate::app::QSelectOp::Any) {
|
||||
None
|
||||
} else if matches!(
|
||||
state.operator,
|
||||
crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt
|
||||
) && !matches!(
|
||||
property.editor,
|
||||
crate::app::QSelectValueEditor::Number
|
||||
) {
|
||||
Some(
|
||||
crate::t!("This operator requires a numeric property.")
|
||||
.into_owned(),
|
||||
)
|
||||
} else {
|
||||
match &property.editor {
|
||||
crate::app::QSelectValueEditor::Number
|
||||
if crate::entities::common::parse_f64(&state.value).is_none() =>
|
||||
{
|
||||
Some(crate::t!("Enter a valid number.").into_owned())
|
||||
}
|
||||
crate::app::QSelectValueEditor::Choice(_)
|
||||
if state.value.is_empty() =>
|
||||
{
|
||||
Some(crate::t!("Choose a value.").into_owned())
|
||||
}
|
||||
crate::app::QSelectValueEditor::Text
|
||||
| crate::app::QSelectValueEditor::Number
|
||||
| crate::app::QSelectValueEditor::Choice(_) => None,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(error) = validation_error {
|
||||
if let Some(state) = self.qselect.as_mut() {
|
||||
state.error = Some(error);
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
let Some(state) = self.qselect.take() else {
|
||||
return Task::none();
|
||||
};
|
||||
self.reset_modal_geometry();
|
||||
let i = self.active_tab;
|
||||
let matched = self.tabs[i].scene.qselect(
|
||||
state.scope,
|
||||
state.type_filter.as_deref(),
|
||||
state.property.as_ref().map(|p| p.field.as_str()),
|
||||
state.operator,
|
||||
&state.value,
|
||||
state.append,
|
||||
state.mode,
|
||||
state.append
|
||||
&& matches!(state.scope, crate::app::QSelectScope::CurrentSpace),
|
||||
);
|
||||
self.command_line
|
||||
.push_output(crate::tf!("QSELECT: {} object(s) selected.", matched).as_ref());
|
||||
|
||||
+3
-4
@@ -1720,12 +1720,11 @@ impl OpenCADStudio {
|
||||
};
|
||||
|
||||
let qselect_layer: Element<'_, Message> = if let Some(state) = &self.qselect {
|
||||
let types = tab.scene.entity_type_names_in_layout();
|
||||
let properties = tab.scene.qselect_properties(state.type_filter.as_deref());
|
||||
qselect_overlay(
|
||||
state,
|
||||
&types,
|
||||
&properties,
|
||||
&state.available_types,
|
||||
&state.available_properties,
|
||||
state.candidate_count,
|
||||
self.modal_offset,
|
||||
self.modal_resize,
|
||||
)
|
||||
|
||||
+179
-75
@@ -1199,16 +1199,14 @@ pub(super) fn snap_override_overlay(pos: iced::Point) -> Element<'static, Messag
|
||||
const QSELECT_ANY_TYPE: &str = "(Any type)";
|
||||
const QSELECT_ANY_PROP: &str = "(Any property)";
|
||||
|
||||
/// Floating panel for the Quick Select feature. Single-row filter:
|
||||
/// object type → property → operator → value, plus an "Append to current
|
||||
/// selection" checkbox. The property dropdown is type-aware — Common
|
||||
/// properties (Layer, Color, Linetype, Lineweight) are always shown;
|
||||
/// picking a specific Object type adds that type's `geometry_properties`
|
||||
/// fields (Start X, Length, Radius, …) so type-specific filtering works.
|
||||
/// Quick Select uses the application's shared movable modal frame. Its form
|
||||
/// groups scope, filter, and result behavior while reusing the same compact
|
||||
/// field and button styles as the other workspace dialogs.
|
||||
pub(super) fn qselect_overlay<'a>(
|
||||
state: &'a crate::app::QSelectState,
|
||||
types: &[String],
|
||||
properties: &[(String, String)],
|
||||
properties: &[crate::app::QSelectPropertyChoice],
|
||||
candidate_count: usize,
|
||||
modal_offset: iced::Vector,
|
||||
modal_resize: iced::Vector,
|
||||
) -> Element<'a, Message> {
|
||||
@@ -1216,12 +1214,14 @@ pub(super) fn qselect_overlay<'a>(
|
||||
state,
|
||||
types,
|
||||
properties,
|
||||
candidate_count,
|
||||
crate::ui::modal::ModalSizing::INTRINSIC,
|
||||
);
|
||||
let content = qselect_content(
|
||||
state,
|
||||
types,
|
||||
properties,
|
||||
candidate_count,
|
||||
crate::ui::modal::ModalSizing::FILL,
|
||||
);
|
||||
let content = crate::ui::modal::intrinsic(
|
||||
@@ -1244,10 +1244,11 @@ pub(super) fn qselect_overlay<'a>(
|
||||
fn qselect_content<'a>(
|
||||
state: &'a crate::app::QSelectState,
|
||||
types: &[String],
|
||||
properties: &[(String, String)],
|
||||
properties: &[crate::app::QSelectPropertyChoice],
|
||||
candidate_count: usize,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
use iced::widget::{checkbox};
|
||||
use iced::widget::{checkbox, radio, rule};
|
||||
let mut type_options: Vec<String> = vec![QSELECT_ANY_TYPE.to_string()];
|
||||
type_options.extend(types.iter().cloned());
|
||||
|
||||
@@ -1255,21 +1256,22 @@ fn qselect_content<'a>(
|
||||
vec![crate::app::QSelectPropertyChoice {
|
||||
field: String::new(),
|
||||
label: QSELECT_ANY_PROP.to_string(),
|
||||
editor: crate::app::QSelectValueEditor::Text,
|
||||
}];
|
||||
prop_options.extend(properties.iter().map(|(field, label)| {
|
||||
crate::app::QSelectPropertyChoice {
|
||||
field: field.clone(),
|
||||
label: label.clone(),
|
||||
}
|
||||
}));
|
||||
prop_options.extend(properties.iter().cloned());
|
||||
|
||||
let op_options: Vec<crate::app::QSelectOp> = vec![
|
||||
let number_property = state.property.as_ref().is_some_and(|property| {
|
||||
matches!(property.editor, crate::app::QSelectValueEditor::Number)
|
||||
});
|
||||
let mut 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,
|
||||
];
|
||||
if number_property {
|
||||
op_options.push(crate::app::QSelectOp::Gt);
|
||||
op_options.push(crate::app::QSelectOp::Lt);
|
||||
}
|
||||
op_options.push(crate::app::QSelectOp::Any);
|
||||
|
||||
let type_sel = state
|
||||
.type_filter
|
||||
@@ -1281,11 +1283,9 @@ fn qselect_content<'a>(
|
||||
.unwrap_or(crate::app::QSelectPropertyChoice {
|
||||
field: String::new(),
|
||||
label: QSELECT_ANY_PROP.to_string(),
|
||||
editor: crate::app::QSelectValueEditor::Text,
|
||||
});
|
||||
|
||||
// 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 field_width = if matches!(sizing.width, iced::Length::Fill) {
|
||||
@@ -1293,49 +1293,121 @@ fn qselect_content<'a>(
|
||||
} else {
|
||||
iced::Length::Shrink
|
||||
};
|
||||
let flex_width = if matches!(sizing.width, iced::Length::Fill) {
|
||||
Fill
|
||||
} else {
|
||||
iced::Length::Shrink
|
||||
};
|
||||
|
||||
let label = |s: std::borrow::Cow<'static, str>| {
|
||||
text(s)
|
||||
.size(12)
|
||||
.width(iced::Length::Fixed(90.0))
|
||||
.width(iced::Length::Fixed(112.0))
|
||||
};
|
||||
let section_label = |s: std::borrow::Cow<'static, str>| {
|
||||
text(s).size(11).style(|theme: &Theme| iced::widget::text::Style {
|
||||
color: Some(theme.palette().background.base.text.scale_alpha(0.65)),
|
||||
})
|
||||
};
|
||||
|
||||
let btn = |lbl: std::borrow::Cow<'static, str>, msg: Message, primary: bool| {
|
||||
button(text(lbl).size(12))
|
||||
.on_press(msg)
|
||||
.style(move |theme: &Theme, st| {
|
||||
let palette = theme.palette();
|
||||
let pair = match (
|
||||
primary,
|
||||
matches!(st, button::Status::Hovered | button::Status::Pressed),
|
||||
) {
|
||||
(true, true) => palette.primary.strong,
|
||||
(true, false) => palette.primary.base,
|
||||
(false, true) => palette.background.strong,
|
||||
(false, false) => palette.background.weak,
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(pair.color)),
|
||||
text_color: pair.text,
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
let value_editor: Element<'a, Message> = match state.property.as_ref() {
|
||||
Some(property) => match &property.editor {
|
||||
crate::app::QSelectValueEditor::Choice(options) => {
|
||||
let selected = (!state.value.is_empty()).then(|| state.value.clone());
|
||||
let picker = iced::widget::pick_list(
|
||||
selected,
|
||||
options.clone(),
|
||||
|value| value.to_string(),
|
||||
)
|
||||
.width(field_width);
|
||||
if value_enabled {
|
||||
picker.on_select(Message::QSelectSetValue).into()
|
||||
} else {
|
||||
picker.into()
|
||||
}
|
||||
})
|
||||
.padding([4, 14])
|
||||
}
|
||||
crate::app::QSelectValueEditor::Text
|
||||
| crate::app::QSelectValueEditor::Number => {
|
||||
let mut input = text_input("", &state.value).size(12).width(field_width);
|
||||
if value_enabled {
|
||||
input = input.on_input(Message::QSelectSetValue);
|
||||
}
|
||||
input.into()
|
||||
}
|
||||
},
|
||||
None => text_input("", "").size(12).width(field_width).into(),
|
||||
};
|
||||
|
||||
let mut value_input = text_input("", &state.value)
|
||||
.size(12)
|
||||
.width(field_width);
|
||||
if value_enabled {
|
||||
value_input = value_input.on_input(Message::QSelectSetValue);
|
||||
let derived_error = if candidate_count == 0 {
|
||||
Some(crate::t!("No objects are available in this scope.").into_owned())
|
||||
} else if value_enabled && number_property
|
||||
&& crate::entities::common::parse_f64(&state.value).is_none()
|
||||
{
|
||||
Some(crate::t!("Enter a valid number.").into_owned())
|
||||
} else if value_enabled
|
||||
&& state.property.as_ref().is_some_and(|property| {
|
||||
matches!(property.editor, crate::app::QSelectValueEditor::Choice(_))
|
||||
})
|
||||
&& state.value.is_empty()
|
||||
{
|
||||
Some(crate::t!("Choose a value.").into_owned())
|
||||
} else if matches!(state.operator, crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt)
|
||||
&& !number_property
|
||||
{
|
||||
Some(crate::t!("This operator requires a numeric property.").into_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let error = state.error.clone().or(derived_error);
|
||||
|
||||
let scope_options = vec![
|
||||
crate::app::QSelectScope::CurrentSpace,
|
||||
crate::app::QSelectScope::CurrentSelection,
|
||||
];
|
||||
let scope_picker = iced::widget::pick_list(
|
||||
Some(state.scope),
|
||||
scope_options,
|
||||
|value| value.to_string(),
|
||||
)
|
||||
.on_select(Message::QSelectSetScope)
|
||||
.width(field_width);
|
||||
|
||||
let mut append = checkbox(state.append).size(14);
|
||||
if matches!(state.scope, crate::app::QSelectScope::CurrentSpace) {
|
||||
append = append.on_toggle(Message::QSelectSetAppend);
|
||||
}
|
||||
|
||||
let cancel = button(text(t!("Cancel")).size(12))
|
||||
.on_press(Message::QSelectClose)
|
||||
.style(button::subtle)
|
||||
.padding([5, 16]);
|
||||
let apply = button(text(t!("Apply")).size(12))
|
||||
.style(button::primary)
|
||||
.padding([5, 18]);
|
||||
let apply = if error.is_none() {
|
||||
apply.on_press(Message::QSelectApply)
|
||||
} else {
|
||||
apply
|
||||
};
|
||||
|
||||
let panel_body = column![
|
||||
section_label(t!("Scope")),
|
||||
Space::new().height(5),
|
||||
row![label(t!("Apply to:")), scope_picker]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(8)
|
||||
.width(sizing.width),
|
||||
Space::new().height(4),
|
||||
text(format!("{} candidate object(s)", candidate_count))
|
||||
.size(11)
|
||||
.style(|theme: &Theme| iced::widget::text::Style {
|
||||
color: Some(theme.palette().background.base.text.scale_alpha(0.65)),
|
||||
}),
|
||||
Space::new().height(10),
|
||||
container(rule::horizontal(1)).width(flex_width),
|
||||
Space::new().height(10),
|
||||
section_label(t!("Filter")),
|
||||
Space::new().height(5),
|
||||
row![
|
||||
label(t!("Object type:")),
|
||||
iced::widget::pick_list(
|
||||
@@ -1364,12 +1436,12 @@ fn qselect_content<'a>(
|
||||
|value| value.to_string(),
|
||||
)
|
||||
.on_select(|p: crate::app::QSelectPropertyChoice| {
|
||||
if p.field.is_empty() {
|
||||
Message::QSelectSetProperty(None)
|
||||
} else {
|
||||
Message::QSelectSetProperty(Some(p))
|
||||
}
|
||||
})
|
||||
if p.field.is_empty() {
|
||||
Message::QSelectSetProperty(None)
|
||||
} else {
|
||||
Message::QSelectSetProperty(Some(p))
|
||||
}
|
||||
})
|
||||
.width(field_width),
|
||||
]
|
||||
.align_y(iced::Alignment::Center)
|
||||
@@ -1390,25 +1462,57 @@ fn qselect_content<'a>(
|
||||
.spacing(8)
|
||||
.width(sizing.width),
|
||||
Space::new().height(6),
|
||||
row![label(t!("Value:")), value_input,]
|
||||
row![label(t!("Value:")), value_editor]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(8)
|
||||
.width(sizing.width),
|
||||
Space::new().height(10),
|
||||
container(rule::horizontal(1)).width(flex_width),
|
||||
Space::new().height(10),
|
||||
section_label(t!("Result")),
|
||||
Space::new().height(5),
|
||||
column![
|
||||
radio(
|
||||
t!("Include matching objects"),
|
||||
crate::app::QSelectMode::Include,
|
||||
Some(state.mode),
|
||||
Message::QSelectSetMode,
|
||||
)
|
||||
.size(14)
|
||||
.text_size(12),
|
||||
radio(
|
||||
t!("Exclude matching objects"),
|
||||
crate::app::QSelectMode::Exclude,
|
||||
Some(state.mode),
|
||||
Message::QSelectSetMode,
|
||||
)
|
||||
.size(14)
|
||||
.text_size(12),
|
||||
]
|
||||
.spacing(5),
|
||||
Space::new().height(8),
|
||||
row![
|
||||
checkbox(state.append)
|
||||
.on_toggle(Message::QSelectSetAppend)
|
||||
.size(14),
|
||||
append,
|
||||
Space::new().width(6),
|
||||
text(t!("Append to current selection")).size(12),
|
||||
text(t!("Append result to current selection")).size(12),
|
||||
]
|
||||
.align_y(iced::Alignment::Center),
|
||||
Space::new().height(14),
|
||||
Space::new().height(10),
|
||||
if let Some(message) = error {
|
||||
text(message)
|
||||
.size(11)
|
||||
.style(|theme: &Theme| iced::widget::text::Style {
|
||||
color: Some(theme.palette().danger.base.color),
|
||||
})
|
||||
} else {
|
||||
text("").size(11)
|
||||
},
|
||||
Space::new().height(8),
|
||||
row![
|
||||
Space::new().width(field_width),
|
||||
btn(t!("Cancel"), Message::QSelectClose, false),
|
||||
Space::new().width(flex_width),
|
||||
cancel,
|
||||
Space::new().width(8),
|
||||
btn(t!("Apply"), Message::QSelectApply, true),
|
||||
apply,
|
||||
]
|
||||
.align_y(iced::Alignment::Center),
|
||||
]
|
||||
@@ -1423,13 +1527,13 @@ fn qselect_content<'a>(
|
||||
.style(|theme: &Theme| {
|
||||
let palette = theme.palette();
|
||||
container::Style {
|
||||
background: Some(Background::Color(palette.background.weak.color)),
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 6.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
background: Some(Background::Color(palette.background.weak.color)),
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 6.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+343
-66
@@ -56,6 +56,30 @@ impl Scene {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn qselect_candidate_handles(&self, scope: crate::app::QSelectScope) -> Vec<Handle> {
|
||||
match scope {
|
||||
crate::app::QSelectScope::CurrentSpace => self.current_layout_entity_handles(),
|
||||
crate::app::QSelectScope::CurrentSelection => {
|
||||
self.selected.iter().copied().collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn qselect_candidate_count(&self, scope: crate::app::QSelectScope) -> usize {
|
||||
self.qselect_candidate_handles(scope).len()
|
||||
}
|
||||
|
||||
pub fn qselect_entity_type_names(&self, scope: crate::app::QSelectScope) -> Vec<String> {
|
||||
use crate::entities::traits::entity_type_name;
|
||||
let mut names = std::collections::BTreeSet::new();
|
||||
for h in self.qselect_candidate_handles(scope) {
|
||||
if let Some(entity) = self.document.get_entity(h) {
|
||||
names.insert(entity_type_name(entity).to_string());
|
||||
}
|
||||
}
|
||||
names.into_iter().collect()
|
||||
}
|
||||
|
||||
/// Extends the current selection with every entity in the active
|
||||
/// layout that matches one of the selected entities by `(variant,
|
||||
/// layer)`. The seed selection stays selected. No-op when nothing is
|
||||
@@ -112,9 +136,9 @@ impl Scene {
|
||||
self.selected.len()
|
||||
}
|
||||
|
||||
/// Replaces (or extends, when `append` is true) the current
|
||||
/// selection with every entity in the active layout that matches
|
||||
/// the filter. Returns the number of newly-matching entities.
|
||||
/// Builds a selection from the chosen scope and filter. Exclude mode
|
||||
/// keeps candidates that do not match. Append mode unions the result
|
||||
/// with the selection that existed before the query.
|
||||
///
|
||||
/// `type_name` of `None` means "any type". `property_field` of
|
||||
/// `None` skips the property test (only the type filter applies).
|
||||
@@ -123,19 +147,19 @@ impl Scene {
|
||||
/// reject anything non-numeric.
|
||||
pub fn qselect(
|
||||
&mut self,
|
||||
scope: crate::app::QSelectScope,
|
||||
type_name: Option<&str>,
|
||||
property_field: Option<&str>,
|
||||
op: crate::app::QSelectOp,
|
||||
value: &str,
|
||||
mode: crate::app::QSelectMode,
|
||||
append: bool,
|
||||
) -> usize {
|
||||
use crate::app::QSelectOp;
|
||||
use crate::app::{QSelectMode, QSelectOp};
|
||||
use crate::entities::traits::entity_type_name;
|
||||
if !append {
|
||||
self.selected.clear();
|
||||
}
|
||||
let handles = self.current_layout_entity_handles();
|
||||
let mut matched = 0;
|
||||
let previous = self.selected.clone();
|
||||
let handles = self.qselect_candidate_handles(scope);
|
||||
let mut result = HashSet::default();
|
||||
for h in handles {
|
||||
let Some(e) = self.document.get_entity(h) else {
|
||||
continue;
|
||||
@@ -150,43 +174,48 @@ impl Scene {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(t) = type_name {
|
||||
if entity_type_name(e) != t {
|
||||
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
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
QSelectOp::Any => true,
|
||||
},
|
||||
None => false,
|
||||
}
|
||||
QSelectOp::Any => true,
|
||||
}
|
||||
}
|
||||
};
|
||||
if prop_ok {
|
||||
self.selected.insert(h);
|
||||
matched += 1;
|
||||
let matches_filter = type_ok && prop_ok;
|
||||
let keep = match mode {
|
||||
QSelectMode::Include => matches_filter,
|
||||
QSelectMode::Exclude => !matches_filter,
|
||||
};
|
||||
if keep {
|
||||
result.insert(h);
|
||||
}
|
||||
}
|
||||
self.bump_selection();
|
||||
matched
|
||||
if append {
|
||||
result.extend(previous);
|
||||
}
|
||||
self.replace_selection(result);
|
||||
self.selected.len()
|
||||
}
|
||||
|
||||
/// Returns the sorted set of entity-type names present in the active
|
||||
@@ -223,25 +252,100 @@ impl Scene {
|
||||
!self.selection_filter.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the list of `(field, label)` pairs the Quick Select
|
||||
/// "Properties" dropdown should show given the current type filter:
|
||||
///
|
||||
/// * Common properties (Layer, Color, Linetype, Lineweight) are
|
||||
/// always included.
|
||||
/// * When `type_name` names a specific entity type present in the
|
||||
/// active layout, the first entity of that type contributes its
|
||||
/// `geometry_properties()` rows (Start X, Length, Radius, …) so
|
||||
/// type-specific filtering works.
|
||||
/// Returns common and type-specific filter properties with the editor
|
||||
/// each value needs. Choice editors are enriched from both document
|
||||
/// tables and values present in the selected candidate scope.
|
||||
pub fn qselect_properties(
|
||||
&self,
|
||||
type_name: Option<&str>,
|
||||
) -> Vec<(String, String)> {
|
||||
scope: crate::app::QSelectScope,
|
||||
) -> Vec<crate::app::QSelectPropertyChoice> {
|
||||
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()),
|
||||
use crate::scene::model::object::PropValue;
|
||||
use crate::app::{QSelectPropertyChoice, QSelectValueEditor};
|
||||
|
||||
let candidate_handles: Vec<Handle> = self
|
||||
.qselect_candidate_handles(scope)
|
||||
.into_iter()
|
||||
.filter(|h| {
|
||||
self.document.get_entity(*h).is_some_and(|entity| {
|
||||
type_name.is_none_or(|t| entity_type_name(entity) == t)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut layer_options: Vec<String> = self
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.map(|layer| layer.name.clone())
|
||||
.collect();
|
||||
layer_options.sort_by_key(|value| value.to_lowercase());
|
||||
let mut linetype_options = vec!["ByLayer".to_string(), "ByBlock".to_string()];
|
||||
linetype_options.extend(
|
||||
self.document
|
||||
.line_types
|
||||
.iter()
|
||||
.map(|linetype| linetype.name.clone()),
|
||||
);
|
||||
|
||||
let choice = |field: &str, label: String, editor: QSelectValueEditor| {
|
||||
QSelectPropertyChoice {
|
||||
field: field.to_string(),
|
||||
label,
|
||||
editor,
|
||||
}
|
||||
};
|
||||
let mut out = vec![
|
||||
choice("handle", crate::t!("Handle").into_owned(), QSelectValueEditor::Text),
|
||||
choice(
|
||||
"color",
|
||||
crate::t!("Color").into_owned(),
|
||||
QSelectValueEditor::Choice(vec!["ByLayer".into(), "ByBlock".into()]),
|
||||
),
|
||||
choice(
|
||||
"layer",
|
||||
crate::t!("Layer").into_owned(),
|
||||
QSelectValueEditor::Choice(layer_options.clone()),
|
||||
),
|
||||
choice(
|
||||
"linetype",
|
||||
crate::t!("Linetype").into_owned(),
|
||||
QSelectValueEditor::Choice(linetype_options.clone()),
|
||||
),
|
||||
choice(
|
||||
"linetype_scale",
|
||||
crate::t!("Linetype scale").into_owned(),
|
||||
QSelectValueEditor::Number,
|
||||
),
|
||||
choice(
|
||||
"plot_style",
|
||||
crate::t!("Plot style").into_owned(),
|
||||
QSelectValueEditor::Choice(vec![
|
||||
"ByLayer".into(),
|
||||
"ByBlock".into(),
|
||||
"ByColor".into(),
|
||||
]),
|
||||
),
|
||||
choice(
|
||||
"lineweight",
|
||||
crate::t!("Lineweight").into_owned(),
|
||||
QSelectValueEditor::Choice(vec![
|
||||
"ByLayer".into(),
|
||||
"ByBlock".into(),
|
||||
"Default".into(),
|
||||
]),
|
||||
),
|
||||
choice(
|
||||
"transparency",
|
||||
crate::t!("Transparency").into_owned(),
|
||||
QSelectValueEditor::Choice(vec!["ByLayer".into()]),
|
||||
),
|
||||
choice(
|
||||
"hyperlink",
|
||||
crate::t!("Hyperlink").into_owned(),
|
||||
QSelectValueEditor::Text,
|
||||
),
|
||||
];
|
||||
if let Some(t) = type_name {
|
||||
let text_style_names: Vec<String> = self
|
||||
@@ -250,22 +354,146 @@ impl Scene {
|
||||
.iter()
|
||||
.map(|s| s.name.clone())
|
||||
.collect();
|
||||
let sample = self
|
||||
.current_layout_entity_handles()
|
||||
.into_iter()
|
||||
let sample = candidate_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.filter_map(|h| self.document.get_entity(h))
|
||||
.find(|e| entity_type_name(e) == t);
|
||||
if let Some(sample) = sample {
|
||||
for section in sample.geometry_properties(&text_style_names) {
|
||||
let mut sections = vec![crate::scene::cache::properties::general_section(sample)];
|
||||
if let Some(section) =
|
||||
crate::scene::cache::properties::visualization_section(sample)
|
||||
{
|
||||
sections.push(section);
|
||||
}
|
||||
sections.extend(sample.geometry_properties(&text_style_names));
|
||||
for section in sections {
|
||||
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()));
|
||||
if out.iter().any(|item| item.field == prop.field) {
|
||||
continue;
|
||||
}
|
||||
let editor = if prop.field == "material" {
|
||||
QSelectValueEditor::Choice(vec![
|
||||
"ByLayer".into(),
|
||||
"ByBlock".into(),
|
||||
"Custom".into(),
|
||||
])
|
||||
} else {
|
||||
match prop.value {
|
||||
PropValue::ReadOnly(ref value)
|
||||
| PropValue::EditText(ref value) => {
|
||||
let field = prop.field.to_ascii_lowercase();
|
||||
let textual = [
|
||||
"name",
|
||||
"text",
|
||||
"content",
|
||||
"style",
|
||||
"tag",
|
||||
"prompt",
|
||||
"value",
|
||||
"description",
|
||||
"format",
|
||||
"font",
|
||||
"path",
|
||||
"file",
|
||||
"url",
|
||||
]
|
||||
.iter()
|
||||
.any(|part| field.contains(part));
|
||||
if !textual
|
||||
&& crate::entities::common::parse_f64(value).is_some()
|
||||
{
|
||||
QSelectValueEditor::Number
|
||||
} else {
|
||||
QSelectValueEditor::Text
|
||||
}
|
||||
}
|
||||
PropValue::LayerChoice(_) => {
|
||||
QSelectValueEditor::Choice(layer_options.clone())
|
||||
}
|
||||
PropValue::Choice { ref options, .. } => {
|
||||
QSelectValueEditor::Choice(options.clone())
|
||||
}
|
||||
PropValue::EditChoice {
|
||||
ref value,
|
||||
ref options,
|
||||
} => {
|
||||
let mut values = options.clone();
|
||||
if !values.iter().any(|item| item == value) {
|
||||
values.push(value.clone());
|
||||
}
|
||||
QSelectValueEditor::Choice(values)
|
||||
}
|
||||
PropValue::ColorChoice(_) => QSelectValueEditor::Choice(vec![
|
||||
"ByLayer".into(),
|
||||
"ByBlock".into(),
|
||||
]),
|
||||
PropValue::LwChoice(_) => QSelectValueEditor::Choice(vec![
|
||||
"ByLayer".into(),
|
||||
"ByBlock".into(),
|
||||
"Default".into(),
|
||||
]),
|
||||
PropValue::LinetypeChoice(_) => {
|
||||
QSelectValueEditor::Choice(linetype_options.clone())
|
||||
}
|
||||
PropValue::HatchPatternChoice(value) => {
|
||||
let mut patterns: Vec<String> =
|
||||
crate::scene::model::hatch_patterns::catalog()
|
||||
.iter()
|
||||
.map(|entry| entry.name.clone())
|
||||
.collect();
|
||||
if !patterns.iter().any(|item| item == &value) {
|
||||
patterns.push(value);
|
||||
}
|
||||
QSelectValueEditor::Choice(patterns)
|
||||
}
|
||||
PropValue::BoolToggle { .. } => {
|
||||
QSelectValueEditor::Choice(vec![
|
||||
"false".into(),
|
||||
"true".into(),
|
||||
])
|
||||
}
|
||||
PropValue::AttrText { .. } => QSelectValueEditor::Text,
|
||||
PropValue::Stepper { .. }
|
||||
| PropValue::ColorVaries
|
||||
| PropValue::LwVaries => continue,
|
||||
}
|
||||
};
|
||||
out.push(choice(prop.field, prop.label, editor));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for property in &mut out {
|
||||
let QSelectValueEditor::Choice(options) = &mut property.editor else {
|
||||
continue;
|
||||
};
|
||||
let common_choice = matches!(
|
||||
property.field.as_str(),
|
||||
"color"
|
||||
| "layer"
|
||||
| "linetype"
|
||||
| "plot_style"
|
||||
| "lineweight"
|
||||
| "transparency"
|
||||
| "material"
|
||||
);
|
||||
if !common_choice && !options.is_empty() {
|
||||
continue;
|
||||
}
|
||||
for handle in &candidate_handles {
|
||||
let Some(entity) = self.document.get_entity(*handle) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(value) = self.entity_property_value(entity, &property.field) {
|
||||
if !options.iter().any(|option| option.eq_ignore_ascii_case(&value)) {
|
||||
options.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
options.sort_by_key(|value| value.to_lowercase());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -283,10 +511,53 @@ impl Scene {
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
use crate::scene::model::object::PropValue;
|
||||
match field {
|
||||
"handle" => Some(entity.common().handle.value().to_string()),
|
||||
"layer" => Some(entity.common().layer.clone()),
|
||||
"color" => Some(Self::format_color(entity.common().color)),
|
||||
"linetype" => Some(entity.common().linetype.clone()),
|
||||
"linetype" => Some(if entity.common().linetype.is_empty() {
|
||||
"ByLayer".to_string()
|
||||
} else {
|
||||
entity.common().linetype.clone()
|
||||
}),
|
||||
"linetype_scale" => Some(format!("{:.4}", entity.common().linetype_scale)),
|
||||
"plot_style" => Some(
|
||||
match entity.common().plotstyle_flags {
|
||||
0 => "ByLayer",
|
||||
1 => "ByBlock",
|
||||
_ => "ByColor",
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
"lineweight" => Some(Self::format_lineweight(entity.common().line_weight)),
|
||||
"transparency" => Some(if entity.common().transparency.alpha() == 0 {
|
||||
"ByLayer".to_string()
|
||||
} else {
|
||||
((entity.common().transparency.alpha() as f64 / 255.0 * 100.0).round() as u32)
|
||||
.to_string()
|
||||
}),
|
||||
"hyperlink" => Some(
|
||||
entity
|
||||
.common()
|
||||
.extended_data
|
||||
.get_record("PE_URL")
|
||||
.and_then(|record| {
|
||||
record.values.iter().find_map(|value| match value {
|
||||
acadrust::xdata::XDataValue::String(text) if !text.is_empty() => {
|
||||
Some(text.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
"material" => Some(
|
||||
match entity.common().material_flags {
|
||||
0 => "ByLayer",
|
||||
1 => "ByBlock",
|
||||
_ => "Custom",
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
_ => {
|
||||
let text_style_names: Vec<String> = self
|
||||
.document
|
||||
@@ -294,8 +565,14 @@ impl Scene {
|
||||
.iter()
|
||||
.map(|s| s.name.clone())
|
||||
.collect();
|
||||
let prop = entity
|
||||
.geometry_properties(&text_style_names)
|
||||
let mut sections = vec![crate::scene::cache::properties::general_section(entity)];
|
||||
if let Some(section) =
|
||||
crate::scene::cache::properties::visualization_section(entity)
|
||||
{
|
||||
sections.push(section);
|
||||
}
|
||||
sections.extend(entity.geometry_properties(&text_style_names));
|
||||
let prop = sections
|
||||
.into_iter()
|
||||
.flat_map(|s| s.props)
|
||||
.find(|p| p.field == field)?;
|
||||
|
||||
Reference in new issue
Block a user