mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
Filter transform instances with 'Transform Selection' node
This commit is contained in:
parent
7cb42b9523
commit
0508da13b9
10 changed files with 357 additions and 2 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
61
node-graph/gcore/src/selection.rs
Normal file
61
node-graph/gcore/src/selection.rs
Normal file
|
|
@ -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<core::ops::RangeInclusive<usize>>),
|
||||
#[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<Vec<core::ops::RangeInclusive<usize>>> for IndexOperationFilter {
|
||||
fn from(values: Vec<core::ops::RangeInclusive<usize>>) -> Self {
|
||||
Self::Range(values)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<core::ops::RangeInclusive<usize>> for IndexOperationFilter {
|
||||
fn from(value: core::ops::RangeInclusive<usize>) -> 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)
|
||||
}
|
||||
|
|
@ -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<T: ApplyTransform2>(
|
||||
ctx: impl Ctx + CloneVarArgs + ExtractAll,
|
||||
#[implementations(
|
||||
Context -> DAffine2,
|
||||
Context -> DVec2,
|
||||
Context -> Table<Graphic>,
|
||||
Context -> Table<Vector>,
|
||||
Context -> Table<Raster<CPU>>,
|
||||
Context -> Table<Raster<GPU>>,
|
||||
)]
|
||||
value: impl Node<Context<'static>, Output = T>,
|
||||
translate: DVec2,
|
||||
rotate: f64,
|
||||
scale: DVec2,
|
||||
skew: DVec2,
|
||||
selection: impl Node<Context<'static>, 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<Output = bool>>);
|
||||
}
|
||||
|
||||
/// Implementations of applying transforms for a table that implement the filtering based on the selection field.
|
||||
impl<T> ApplyTransform2 for Table<T> {
|
||||
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>>,
|
||||
) {
|
||||
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<T: TransformMut> 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<Output = bool>>) {
|
||||
*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<Output = bool>>) {
|
||||
*self = matrix.transform_point2(*self);
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
async fn transform<T: ApplyTransform + 'n + 'static>(
|
||||
ctx: impl Ctx + CloneVarArgs + ExtractAll,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -187,6 +187,8 @@ fn compile_graph(document_string: String, editor_api: Arc<WasmEditorApi>) -> 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 {};
|
||||
|
|
|
|||
|
|
@ -24,6 +24,36 @@ pub fn expand_network(network: &mut NodeNetwork, substitutions: &HashMap<ProtoNo
|
|||
}
|
||||
}
|
||||
|
||||
/// Referencing the TaggedValue::IndexOperationFilter will expand to a node that contains the evaluate_index_operation_filter
|
||||
pub fn evaluate_index_operation_filter(network: &mut NodeNetwork) {
|
||||
let mut new_nodes = Vec::new();
|
||||
for node in network.nodes.values_mut() {
|
||||
for input in &mut node.inputs {
|
||||
if !matches!(input.as_value(), Some(TaggedValue::IndexOperationFilter(_)),) {
|
||||
continue;
|
||||
}
|
||||
let node_id = NodeId::new();
|
||||
let range_input = std::mem::replace(input, NodeInput::node(node_id, 0));
|
||||
new_nodes.push((node_id, range_input));
|
||||
}
|
||||
match &mut node.implementation {
|
||||
DocumentNodeImplementation::Network(node_network) => 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<ProtoNodeIdentifier, DocumentNode> {
|
||||
let mut custom = HashMap::new();
|
||||
let node_registry = graphene_core::registry::NODE_REGISTRY.lock().unwrap();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue