Skip to content

Commit 9f6ea66

Browse files
committed
Filter transform instances with 'Transform Selection' node
1 parent 7fcdad1 commit 9f6ea66

File tree

10 files changed

+357
-2
lines changed

10 files changed

+357
-2
lines changed

editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,6 +1487,113 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
14871487
description: Cow::Borrowed("TODO"),
14881488
properties: None,
14891489
},
1490+
// A modified version of the transform node that filters values based on a selection field
1491+
DocumentNodeDefinition {
1492+
identifier: "Transform Selection",
1493+
category: "Math: Transform",
1494+
node_template: NodeTemplate {
1495+
document_node: DocumentNode {
1496+
inputs: vec![
1497+
NodeInput::value(TaggedValue::DAffine2(DAffine2::default()), true),
1498+
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
1499+
NodeInput::value(TaggedValue::F64(0.), false),
1500+
NodeInput::value(TaggedValue::DVec2(DVec2::ONE), false),
1501+
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
1502+
NodeInput::value(TaggedValue::IndexOperationFilter((0..=1).into()), false),
1503+
],
1504+
implementation: DocumentNodeImplementation::Network(NodeNetwork {
1505+
exports: vec![NodeInput::node(NodeId(1), 0)],
1506+
nodes: [
1507+
DocumentNode {
1508+
inputs: vec![NodeInput::network(generic!(T), 0)],
1509+
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
1510+
manual_composition: Some(generic!(T)),
1511+
skip_deduplication: true,
1512+
..Default::default()
1513+
},
1514+
DocumentNode {
1515+
inputs: vec![
1516+
NodeInput::node(NodeId(0), 0),
1517+
NodeInput::network(concrete!(DVec2), 1),
1518+
NodeInput::network(concrete!(f64), 2),
1519+
NodeInput::network(concrete!(DVec2), 3),
1520+
NodeInput::network(concrete!(DVec2), 4),
1521+
NodeInput::network(fn_type!(Context, bool), 5),
1522+
],
1523+
manual_composition: Some(concrete!(Context)),
1524+
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::transform_two::IDENTIFIER),
1525+
..Default::default()
1526+
},
1527+
]
1528+
.into_iter()
1529+
.enumerate()
1530+
.map(|(id, node)| (NodeId(id as u64), node))
1531+
.collect(),
1532+
..Default::default()
1533+
}),
1534+
..Default::default()
1535+
},
1536+
persistent_node_metadata: DocumentNodePersistentMetadata {
1537+
network_metadata: Some(NodeNetworkMetadata {
1538+
persistent_metadata: NodeNetworkPersistentMetadata {
1539+
node_metadata: [
1540+
DocumentNodeMetadata {
1541+
persistent_metadata: DocumentNodePersistentMetadata {
1542+
display_name: "Monitor".to_string(),
1543+
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
1544+
..Default::default()
1545+
},
1546+
..Default::default()
1547+
},
1548+
DocumentNodeMetadata {
1549+
persistent_metadata: DocumentNodePersistentMetadata {
1550+
display_name: "Transform".to_string(),
1551+
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)),
1552+
..Default::default()
1553+
},
1554+
..Default::default()
1555+
},
1556+
]
1557+
.into_iter()
1558+
.enumerate()
1559+
.map(|(id, node)| (NodeId(id as u64), node))
1560+
.collect(),
1561+
..Default::default()
1562+
},
1563+
..Default::default()
1564+
}),
1565+
input_metadata: vec![
1566+
("Value", "TODO").into(),
1567+
InputMetadata::with_name_description_override(
1568+
"Translation",
1569+
"TODO",
1570+
WidgetOverride::Vec2(Vec2InputSettings {
1571+
x: "X".to_string(),
1572+
y: "Y".to_string(),
1573+
unit: " px".to_string(),
1574+
..Default::default()
1575+
}),
1576+
),
1577+
InputMetadata::with_name_description_override("Rotation", "TODO", WidgetOverride::Custom("transform_rotation".to_string())),
1578+
InputMetadata::with_name_description_override(
1579+
"Scale",
1580+
"TODO",
1581+
WidgetOverride::Vec2(Vec2InputSettings {
1582+
x: "W".to_string(),
1583+
y: "H".to_string(),
1584+
unit: "x".to_string(),
1585+
..Default::default()
1586+
}),
1587+
),
1588+
InputMetadata::with_name_description_override("Skew", "TODO", WidgetOverride::Custom("transform_skew".to_string())),
1589+
],
1590+
output_names: vec!["Data".to_string()],
1591+
..Default::default()
1592+
},
1593+
},
1594+
description: Cow::Borrowed("Transforms only selected instances based on a selection field"),
1595+
properties: None,
1596+
},
14901597
DocumentNodeDefinition {
14911598
identifier: "Boolean Operation",
14921599
category: "Vector",

editor/src/messages/portfolio/document/node_graph/node_properties.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use graphene_std::raster::{
2020
SelectiveColorChoice,
2121
};
2222
use graphene_std::raster_types::{CPU, GPU, Raster};
23+
use graphene_std::selection::IndexOperationFilter;
2324
use graphene_std::table::Table;
2425
use graphene_std::text::{Font, TextAlign};
2526
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
@@ -180,6 +181,7 @@ pub(crate) fn property_from_type(
180181
// ==========================
181182
Some(x) if x == TypeId::of::<Vec<f64>>() => array_of_number_widget(default_info, TextInput::default()).into(),
182183
Some(x) if x == TypeId::of::<Vec<DVec2>>() => array_of_vec2_widget(default_info, TextInput::default()).into(),
184+
Some(x) if x == TypeId::of::<IndexOperationFilter>() => array_of_ranges(default_info, TextInput::default()).into(),
183185
// ====================
184186
// GRAPHICAL DATA TYPES
185187
// ====================
@@ -757,6 +759,77 @@ pub fn array_of_vec2_widget(parameter_widgets_info: ParameterWidgetsInfo, text_p
757759
widgets
758760
}
759761

762+
pub fn array_of_ranges(parameter_widgets_info: ParameterWidgetsInfo, text_props: TextInput) -> Vec<WidgetHolder> {
763+
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
764+
765+
let mut widgets = start_widgets(parameter_widgets_info);
766+
767+
let from_string = |string: &str| {
768+
let mut result = Vec::new();
769+
let mut start: Option<usize> = None;
770+
let mut number: Option<usize> = None;
771+
let mut seen_continue = false;
772+
for c in string.chars() {
773+
// any string containing a '*' gets all
774+
if c == '*' {
775+
return Some(TaggedValue::IndexOperationFilter(IndexOperationFilter::All));
776+
}
777+
778+
if let Some(digit) = c.to_digit(10) {
779+
if !seen_continue {
780+
if let Some(start) = start.take() {
781+
result.push(start..=start);
782+
}
783+
}
784+
let mut value = number.unwrap_or_default();
785+
value *= 10;
786+
value += digit as usize;
787+
number = Some(value);
788+
} else {
789+
if let Some(number) = number.take() {
790+
if let Some(start) = start.take() {
791+
result.push(start.min(number)..=start.max(number));
792+
} else {
793+
start = Some(number);
794+
}
795+
seen_continue = false;
796+
}
797+
if c == '=' || c == '-' || c == '.' {
798+
seen_continue = true;
799+
}
800+
}
801+
}
802+
if let Some(number) = number.take() {
803+
if let Some(start) = start.take() {
804+
result.push(start.min(number)..=start.max(number));
805+
} else {
806+
result.push(number..=number);
807+
}
808+
}
809+
if let Some(start) = start.take() {
810+
result.push(start..=start);
811+
}
812+
813+
Some(TaggedValue::IndexOperationFilter(result.into()))
814+
};
815+
816+
let Some(document_node) = document_node else { return Vec::new() };
817+
let Some(input) = document_node.inputs.get(index) else {
818+
log::warn!("A widget failed to be built because its node's input index is invalid.");
819+
return vec![];
820+
};
821+
if let Some(TaggedValue::IndexOperationFilter(x)) = &input.as_non_exposed_value() {
822+
widgets.extend_from_slice(&[
823+
Separator::new(SeparatorType::Unrelated).widget_holder(),
824+
text_props
825+
.value(x.to_string())
826+
.on_update(optionally_update_value(move |x: &TextInput| from_string(&x.value), node_id, index))
827+
.widget_holder(),
828+
])
829+
}
830+
widgets
831+
}
832+
760833
pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetHolder>, Option<Vec<WidgetHolder>>) {
761834
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
762835

editor/src/node_graph_executor/runtime.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ impl NodeRuntime {
240240

241241
async fn update_network(&mut self, mut graph: NodeNetwork) -> Result<ResolvedDocumentNodeTypesDelta, String> {
242242
preprocessor::expand_network(&mut graph, &self.substitutions);
243+
preprocessor::evaluate_index_operation_filter(&mut graph);
243244

244245
let scoped_network = wrap_network_in_scope(graph, self.editor_api.clone());
245246

frontend/wasm/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ wasm-opt = ["-Os", "-g"]
6262
[package.metadata.wasm-pack.profile.profiling.wasm-bindgen]
6363
debug-js-glue = true
6464
demangle-name-section = true
65-
dwarf-debug-info = true
65+
dwarf-debug-info = false
6666

6767
[lints.rust]
6868
unexpected_cfgs = { level = "warn", check-cfg = [

node-graph/gcore/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub mod raster;
2121
pub mod raster_types;
2222
pub mod registry;
2323
pub mod render_complexity;
24+
pub mod selection;
2425
pub mod structural;
2526
pub mod table;
2627
pub mod text;

node-graph/gcore/src/selection.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use crate::{Ctx, ExtractIndex};
2+
3+
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Hash, dyn_any::DynAny, Default)]
4+
pub enum IndexOperationFilter {
5+
Range(Vec<core::ops::RangeInclusive<usize>>),
6+
#[default]
7+
All,
8+
}
9+
10+
impl IndexOperationFilter {
11+
pub fn contains(&self, index: usize) -> bool {
12+
match self {
13+
Self::Range(range) => range.iter().any(|range| range.contains(&index)),
14+
Self::All => true,
15+
}
16+
}
17+
}
18+
19+
impl From<Vec<core::ops::RangeInclusive<usize>>> for IndexOperationFilter {
20+
fn from(values: Vec<core::ops::RangeInclusive<usize>>) -> Self {
21+
Self::Range(values)
22+
}
23+
}
24+
25+
impl From<core::ops::RangeInclusive<usize>> for IndexOperationFilter {
26+
fn from(value: core::ops::RangeInclusive<usize>) -> Self {
27+
Self::Range(vec![value])
28+
}
29+
}
30+
31+
impl core::fmt::Display for IndexOperationFilter {
32+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33+
match self {
34+
Self::All => {
35+
write!(f, "*")?;
36+
}
37+
Self::Range(range) => {
38+
let mut started = false;
39+
for value in range {
40+
if started {
41+
write!(f, ", ")?;
42+
}
43+
started = true;
44+
if value.start() == value.end() {
45+
write!(f, "{}", value.start())?;
46+
} else {
47+
write!(f, "{}..={}", value.start(), value.end())?;
48+
}
49+
}
50+
}
51+
}
52+
53+
Ok(())
54+
}
55+
}
56+
57+
#[node_macro::node(category("Filtering"), path(graphene_core::vector))]
58+
async fn evaluate_index_operation_filter(ctx: impl Ctx + ExtractIndex, filter: IndexOperationFilter) -> bool {
59+
let index = ctx.try_index().and_then(|indexes| indexes.last().copied()).unwrap_or_default();
60+
filter.contains(index)
61+
}

node-graph/gcore/src/transform_nodes.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,89 @@
11
use crate::raster_types::{CPU, GPU, Raster};
22
use crate::table::Table;
3-
use crate::transform::{ApplyTransform, Footprint, Transform};
3+
use crate::transform::{ApplyTransform, Footprint, Transform, TransformMut};
44
use crate::vector::VectorData;
55
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, GraphicElement, OwnedContextImpl};
66
use core::f64;
77
use glam::{DAffine2, DVec2};
88

9+
/// An updated version of the transform node supporting selecting which instances/rows are transformed
10+
#[node_macro::node(category(""))]
11+
async fn transform_two<T: ApplyTransform2>(
12+
ctx: impl Ctx + CloneVarArgs + ExtractAll,
13+
#[implementations(
14+
Context -> DAffine2,
15+
Context -> DVec2,
16+
Context -> Table<VectorData>,
17+
Context -> Table<GraphicElement>,
18+
Context -> Table<Raster<CPU>>,
19+
Context -> Table<Raster<GPU>>,
20+
)]
21+
value: impl Node<Context<'static>, Output = T>,
22+
translate: DVec2,
23+
rotate: f64,
24+
scale: DVec2,
25+
skew: DVec2,
26+
selection: impl Node<Context<'static>, Output = bool>,
27+
) -> T {
28+
let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., skew.y, skew.x, 1., 0., 0.]);
29+
30+
let footprint = ctx.try_footprint().copied();
31+
32+
let mut transform_target = {
33+
let mut new_ctx = OwnedContextImpl::from(ctx.clone());
34+
if let Some(mut footprint) = footprint {
35+
footprint.apply_transform(&matrix);
36+
new_ctx = new_ctx.with_footprint(footprint);
37+
}
38+
value.eval(new_ctx.into_context()).await
39+
};
40+
41+
transform_target.apply_transformation(matrix, &ctx, selection).await;
42+
43+
transform_target
44+
}
45+
46+
/// A trait facilitating applying transforms with a particular selection field.
47+
trait ApplyTransform2 {
48+
async fn apply_transformation<'n>(&mut self, matrix: DAffine2, ctx: &(impl Ctx + ExtractAll + CloneVarArgs), selection: &'n impl crate::Node<'n, Context<'n>, Output = impl Future<Output = bool>>);
49+
}
50+
51+
/// Implementations of applying transforms for a table that implement the filtering based on the selection field.
52+
impl<T> ApplyTransform2 for Table<T> {
53+
async fn apply_transformation<'n>(
54+
&mut self,
55+
matrix: DAffine2,
56+
ctx: &(impl Ctx + ExtractAll + CloneVarArgs),
57+
selection: &'n impl crate::Node<'n, Context<'n>, Output = impl Future<Output = bool>>,
58+
) {
59+
for (index, row) in self.iter_mut().enumerate() {
60+
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index);
61+
62+
let should_eval = selection.eval(new_ctx.into_context()).await;
63+
if should_eval {
64+
info!("Applying to {index}");
65+
*row.transform = matrix * *row.transform;
66+
} else {
67+
info!("Skipping index {index}");
68+
}
69+
}
70+
}
71+
}
72+
73+
/// An implementation for a non-table which ignores the selection
74+
impl<T: TransformMut> ApplyTransform2 for T {
75+
async fn apply_transformation<'n>(&mut self, matrix: DAffine2, _: &(impl Ctx + ExtractAll + CloneVarArgs), _: &'n impl crate::Node<'n, Context<'n>, Output = impl Future<Output = bool>>) {
76+
*self.transform_mut() = matrix * self.transform();
77+
}
78+
}
79+
80+
/// An implementation for a point which ignores the selection
81+
impl ApplyTransform2 for DVec2 {
82+
async fn apply_transformation<'n>(&mut self, matrix: DAffine2, _: &(impl Ctx + ExtractAll + CloneVarArgs), _: &'n impl crate::Node<'n, Context<'n>, Output = impl Future<Output = bool>>) {
83+
*self = matrix.transform_point2(*self);
84+
}
85+
}
86+
987
#[node_macro::node(category(""))]
1088
async fn transform<T: ApplyTransform + 'n + 'static>(
1189
ctx: impl Ctx + CloneVarArgs + ExtractAll,

0 commit comments

Comments
 (0)