diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index c8df5618ea..bf0fc101e0 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -1487,6 +1487,113 @@ fn static_nodes() -> Vec { 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", diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 078a8fd375..f4a1c5bad7 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -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::>() => array_of_number_widget(default_info, TextInput::default()).into(), Some(x) if x == TypeId::of::>() => array_of_vec2_widget(default_info, TextInput::default()).into(), + Some(x) if x == TypeId::of::() => 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 { + 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 = None; + let mut number: Option = 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, Option>) { let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index e34b32b746..217faabd55 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -240,6 +240,7 @@ impl NodeRuntime { async fn update_network(&mut self, mut graph: NodeNetwork) -> Result { 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()); diff --git a/frontend/wasm/Cargo.toml b/frontend/wasm/Cargo.toml index 90b8ebe6ba..861a177efe 100644 --- a/frontend/wasm/Cargo.toml +++ b/frontend/wasm/Cargo.toml @@ -62,7 +62,7 @@ wasm-opt = ["-Os", "-g"] [package.metadata.wasm-pack.profile.profiling.wasm-bindgen] debug-js-glue = true demangle-name-section = true -dwarf-debug-info = true +dwarf-debug-info = false [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = [ diff --git a/node-graph/gcore/src/lib.rs b/node-graph/gcore/src/lib.rs index 6e2bc65530..03cd1f7589 100644 --- a/node-graph/gcore/src/lib.rs +++ b/node-graph/gcore/src/lib.rs @@ -21,6 +21,7 @@ pub mod raster; pub mod raster_types; pub mod registry; pub mod render_complexity; +pub mod selection; pub mod structural; pub mod table; pub mod text; diff --git a/node-graph/gcore/src/selection.rs b/node-graph/gcore/src/selection.rs new file mode 100644 index 0000000000..066d9707c4 --- /dev/null +++ b/node-graph/gcore/src/selection.rs @@ -0,0 +1,61 @@ +use crate::{Ctx, ExtractIndex}; + +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Hash, dyn_any::DynAny, Default)] +pub enum IndexOperationFilter { + Range(Vec>), + #[default] + All, +} + +impl IndexOperationFilter { + pub fn contains(&self, index: usize) -> bool { + match self { + Self::Range(range) => range.iter().any(|range| range.contains(&index)), + Self::All => true, + } + } +} + +impl From>> for IndexOperationFilter { + fn from(values: Vec>) -> Self { + Self::Range(values) + } +} + +impl From> for IndexOperationFilter { + fn from(value: core::ops::RangeInclusive) -> Self { + Self::Range(vec![value]) + } +} + +impl core::fmt::Display for IndexOperationFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::All => { + write!(f, "*")?; + } + Self::Range(range) => { + let mut started = false; + for value in range { + if started { + write!(f, ", ")?; + } + started = true; + if value.start() == value.end() { + write!(f, "{}", value.start())?; + } else { + write!(f, "{}..={}", value.start(), value.end())?; + } + } + } + } + + Ok(()) + } +} + +#[node_macro::node(category("Filtering"), path(graphene_core::vector))] +async fn evaluate_index_operation_filter(ctx: impl Ctx + ExtractIndex, filter: IndexOperationFilter) -> bool { + let index = ctx.try_index().and_then(|indexes| indexes.last().copied()).unwrap_or_default(); + filter.contains(index) +} diff --git a/node-graph/gcore/src/transform_nodes.rs b/node-graph/gcore/src/transform_nodes.rs index f407cb8998..e80c9333a8 100644 --- a/node-graph/gcore/src/transform_nodes.rs +++ b/node-graph/gcore/src/transform_nodes.rs @@ -1,11 +1,89 @@ use crate::raster_types::{CPU, GPU, Raster}; use crate::table::Table; -use crate::transform::{ApplyTransform, Footprint, Transform}; +use crate::transform::{ApplyTransform, Footprint, Transform, TransformMut}; use crate::vector::Vector; use crate::{CloneVarArgs, Context, Ctx, ExtractAll, Graphic, OwnedContextImpl}; use core::f64; use glam::{DAffine2, DVec2}; +/// An updated version of the transform node supporting selecting which instances/rows are transformed +#[node_macro::node(category(""))] +async fn transform_two( + ctx: impl Ctx + CloneVarArgs + ExtractAll, + #[implementations( + Context -> DAffine2, + Context -> DVec2, + Context -> Table, + Context -> Table, + Context -> Table>, + Context -> Table>, + )] + value: impl Node, Output = T>, + translate: DVec2, + rotate: f64, + scale: DVec2, + skew: DVec2, + selection: impl Node, Output = bool>, +) -> T { + let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., skew.y, skew.x, 1., 0., 0.]); + + let footprint = ctx.try_footprint().copied(); + + let mut transform_target = { + let mut new_ctx = OwnedContextImpl::from(ctx.clone()); + if let Some(mut footprint) = footprint { + footprint.apply_transform(&matrix); + new_ctx = new_ctx.with_footprint(footprint); + } + value.eval(new_ctx.into_context()).await + }; + + transform_target.apply_transformation(matrix, &ctx, selection).await; + + transform_target +} + +/// A trait facilitating applying transforms with a particular selection field. +trait ApplyTransform2 { + 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>); +} + +/// Implementations of applying transforms for a table that implement the filtering based on the selection field. +impl ApplyTransform2 for Table { + 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>, + ) { + for (index, row) in self.iter_mut().enumerate() { + let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index); + + let should_eval = selection.eval(new_ctx.into_context()).await; + if should_eval { + info!("Applying to {index}"); + *row.transform = matrix * *row.transform; + } else { + info!("Skipping index {index}"); + } + } + } +} + +/// An implementation for a non-table which ignores the selection +impl ApplyTransform2 for T { + async fn apply_transformation<'n>(&mut self, matrix: DAffine2, _: &(impl Ctx + ExtractAll + CloneVarArgs), _: &'n impl crate::Node<'n, Context<'n>, Output = impl Future>) { + *self.transform_mut() = matrix * self.transform(); + } +} + +/// An implementation for a point which ignores the selection +impl ApplyTransform2 for DVec2 { + async fn apply_transformation<'n>(&mut self, matrix: DAffine2, _: &(impl Ctx + ExtractAll + CloneVarArgs), _: &'n impl crate::Node<'n, Context<'n>, Output = impl Future>) { + *self = matrix.transform_point2(*self); + } +} + #[node_macro::node(category(""))] async fn transform( ctx: impl Ctx + CloneVarArgs + ExtractAll, diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index f5603d16fc..1f221c0322 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -9,6 +9,7 @@ use graphene_brush::brush_cache::BrushCache; use graphene_brush::brush_stroke::BrushStroke; use graphene_core::raster::Image; use graphene_core::raster_types::{CPU, Raster}; +use graphene_core::selection::IndexOperationFilter; use graphene_core::table::Table; use graphene_core::transform::ReferencePoint; use graphene_core::uuid::NodeId; @@ -246,6 +247,7 @@ tagged_value! { CentroidType(graphene_core::vector::misc::CentroidType), BooleanOperation(graphene_path_bool::BooleanOperation), TextAlign(graphene_core::text::TextAlign), + IndexOperationFilter(IndexOperationFilter), } impl TaggedValue { diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index 049878cc3b..7093ef638a 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -187,6 +187,8 @@ fn compile_graph(document_string: String, editor_api: Arc) -> Res let substitutions = preprocessor::generate_node_substitutions(); preprocessor::expand_network(&mut network, &substitutions); + preprocessor::evaluate_index_operation_filter(&mut network); + let wrapped_network = wrap_network_in_scope(network.clone(), editor_api); let compiler = Compiler {}; diff --git a/node-graph/preprocessor/src/lib.rs b/node-graph/preprocessor/src/lib.rs index e0b4a01685..3ae82e4080 100644 --- a/node-graph/preprocessor/src/lib.rs +++ b/node-graph/preprocessor/src/lib.rs @@ -24,6 +24,36 @@ pub fn expand_network(network: &mut NodeNetwork, substitutions: &HashMap evaluate_index_operation_filter(node_network), + _ => {} + } + } + for (id, range_input) in new_nodes { + network.nodes.insert( + id, + DocumentNode { + inputs: vec![range_input], + manual_composition: Some(concrete!(Context)), + implementation: DocumentNodeImplementation::ProtoNode(graphene_core::selection::evaluate_index_operation_filter::IDENTIFIER), + ..Default::default() + }, + ); + } +} + pub fn generate_node_substitutions() -> HashMap { let mut custom = HashMap::new(); let node_registry = graphene_core::registry::NODE_REGISTRY.lock().unwrap();