Filter transform instances with 'Transform Selection' node

This commit is contained in:
hypercube
2025-07-25 01:28:09 +01:00
committed by Keavon Chambers
parent 7cb42b9523
commit 0508da13b9
10 changed files with 357 additions and 2 deletions

View File

@@ -1487,6 +1487,113 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
description: Cow::Borrowed("TODO"),
properties: None,
},
// A modified version of the transform node that filters values based on a selection field
DocumentNodeDefinition {
identifier: "Transform Selection",
category: "Math: Transform",
node_template: NodeTemplate {
document_node: DocumentNode {
inputs: vec![
NodeInput::value(TaggedValue::DAffine2(DAffine2::default()), true),
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
NodeInput::value(TaggedValue::F64(0.), false),
NodeInput::value(TaggedValue::DVec2(DVec2::ONE), false),
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
NodeInput::value(TaggedValue::IndexOperationFilter((0..=1).into()), false),
],
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(1), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::network(generic!(T), 0)],
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
manual_composition: Some(generic!(T)),
skip_deduplication: true,
..Default::default()
},
DocumentNode {
inputs: vec![
NodeInput::node(NodeId(0), 0),
NodeInput::network(concrete!(DVec2), 1),
NodeInput::network(concrete!(f64), 2),
NodeInput::network(concrete!(DVec2), 3),
NodeInput::network(concrete!(DVec2), 4),
NodeInput::network(fn_type!(Context, bool), 5),
],
manual_composition: Some(concrete!(Context)),
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::transform_two::IDENTIFIER),
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
}),
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Monitor".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Transform".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)),
..Default::default()
},
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
},
..Default::default()
}),
input_metadata: vec![
("Value", "TODO").into(),
InputMetadata::with_name_description_override(
"Translation",
"TODO",
WidgetOverride::Vec2(Vec2InputSettings {
x: "X".to_string(),
y: "Y".to_string(),
unit: " px".to_string(),
..Default::default()
}),
),
InputMetadata::with_name_description_override("Rotation", "TODO", WidgetOverride::Custom("transform_rotation".to_string())),
InputMetadata::with_name_description_override(
"Scale",
"TODO",
WidgetOverride::Vec2(Vec2InputSettings {
x: "W".to_string(),
y: "H".to_string(),
unit: "x".to_string(),
..Default::default()
}),
),
InputMetadata::with_name_description_override("Skew", "TODO", WidgetOverride::Custom("transform_skew".to_string())),
],
output_names: vec!["Data".to_string()],
..Default::default()
},
},
description: Cow::Borrowed("Transforms only selected instances based on a selection field"),
properties: None,
},
DocumentNodeDefinition {
identifier: "Boolean Operation",
category: "Vector",

View File

@@ -20,6 +20,7 @@ use graphene_std::raster::{
BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
SelectiveColorChoice,
};
use graphene_std::selection::IndexOperationFilter;
use graphene_std::text::{Font, TextAlign};
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
use graphene_std::vector::misc::{ArcType, CentroidType, GridType, MergeByDistanceAlgorithm, PointSpacingType};
@@ -177,6 +178,7 @@ pub(crate) fn property_from_type(
// ==========================
Some(x) if x == TypeId::of::<Vec<f64>>() => array_of_number_widget(default_info, TextInput::default()).into(),
Some(x) if x == TypeId::of::<Vec<DVec2>>() => array_of_vec2_widget(default_info, TextInput::default()).into(),
Some(x) if x == TypeId::of::<IndexOperationFilter>() => array_of_ranges(default_info, TextInput::default()).into(),
// ============
// STRUCT TYPES
// ============
@@ -748,6 +750,77 @@ pub fn array_of_vec2_widget(parameter_widgets_info: ParameterWidgetsInfo, text_p
widgets
}
pub fn array_of_ranges(parameter_widgets_info: ParameterWidgetsInfo, text_props: TextInput) -> Vec<WidgetHolder> {
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
let mut widgets = start_widgets(parameter_widgets_info);
let from_string = |string: &str| {
let mut result = Vec::new();
let mut start: Option<usize> = None;
let mut number: Option<usize> = None;
let mut seen_continue = false;
for c in string.chars() {
// any string containing a '*' gets all
if c == '*' {
return Some(TaggedValue::IndexOperationFilter(IndexOperationFilter::All));
}
if let Some(digit) = c.to_digit(10) {
if !seen_continue {
if let Some(start) = start.take() {
result.push(start..=start);
}
}
let mut value = number.unwrap_or_default();
value *= 10;
value += digit as usize;
number = Some(value);
} else {
if let Some(number) = number.take() {
if let Some(start) = start.take() {
result.push(start.min(number)..=start.max(number));
} else {
start = Some(number);
}
seen_continue = false;
}
if c == '=' || c == '-' || c == '.' {
seen_continue = true;
}
}
}
if let Some(number) = number.take() {
if let Some(start) = start.take() {
result.push(start.min(number)..=start.max(number));
} else {
result.push(number..=number);
}
}
if let Some(start) = start.take() {
result.push(start..=start);
}
Some(TaggedValue::IndexOperationFilter(result.into()))
};
let Some(document_node) = document_node else { return Vec::new() };
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(TaggedValue::IndexOperationFilter(x)) = &input.as_non_exposed_value() {
widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_holder(),
text_props
.value(x.to_string())
.on_update(optionally_update_value(move |x: &TextInput| from_string(&x.value), node_id, index))
.widget_holder(),
])
}
widgets
}
pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetHolder>, Option<Vec<WidgetHolder>>) {
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;

View File

@@ -240,6 +240,7 @@ impl NodeRuntime {
async fn update_network(&mut self, mut graph: NodeNetwork) -> Result<ResolvedDocumentNodeTypesDelta, String> {
preprocessor::expand_network(&mut graph, &self.substitutions);
preprocessor::evaluate_index_operation_filter(&mut graph);
let scoped_network = wrap_network_in_scope(graph, self.editor_api.clone());