Remove the 'Blending' node, moving clip into a new 'Clipping Mask' node and fill into the 'Opacity' node (#4085)
* Separate 'Clip' into its own node out from the removed 'Blending' node * Code review * Rename to Clipping Mask * Update Opacity node in demo art that use it * Use DIsplay not Debug for printing blend modes
This commit is contained in:
parent
4b2430290c
commit
86134c26b4
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -6,15 +6,16 @@ use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||||
use glam::{Affine2, DAffine2, Vec2};
|
use glam::{Affine2, DAffine2, Vec2};
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
|
use graphene_std::Color;
|
||||||
use graphene_std::Context;
|
use graphene_std::Context;
|
||||||
use graphene_std::Graphic;
|
use graphene_std::Graphic;
|
||||||
|
use graphene_std::blending::BlendMode;
|
||||||
use graphene_std::gradient::GradientStops;
|
use graphene_std::gradient::GradientStops;
|
||||||
use graphene_std::memo::IORecord;
|
use graphene_std::memo::IORecord;
|
||||||
use graphene_std::raster_types::{CPU, GPU, Raster};
|
use graphene_std::raster_types::{CPU, GPU, Raster};
|
||||||
use graphene_std::table::Table;
|
use graphene_std::table::Table;
|
||||||
use graphene_std::vector::Vector;
|
use graphene_std::vector::Vector;
|
||||||
use graphene_std::vector::style::{Fill, FillChoice};
|
use graphene_std::vector::style::{Fill, FillChoice};
|
||||||
use graphene_std::{AlphaBlending, Color};
|
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -283,7 +284,7 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
|
||||||
for key in &attribute_keys {
|
for key in &attribute_keys {
|
||||||
let target = PathStep::Attribute { row: index, key: key.clone() };
|
let target = PathStep::Attribute { row: index, key: key.clone() };
|
||||||
let widget = self.attribute_any(key, index).and_then(|any| dispatch_value_widget(any, target, data)).unwrap_or_else(|| {
|
let widget = self.attribute_any(key, index).and_then(|any| dispatch_value_widget(any, target, data)).unwrap_or_else(|| {
|
||||||
let text = self.attribute_display_value(key, index, |_| None).unwrap_or_else(|| "-".to_string());
|
let text = self.attribute_display_value(key, index, display_value_override).unwrap_or_else(|| "-".to_string());
|
||||||
TextLabel::new(text).narrow(true).widget_instance()
|
TextLabel::new(text).narrow(true).widget_instance()
|
||||||
});
|
});
|
||||||
values.push(widget);
|
values.push(widget);
|
||||||
|
|
@ -742,21 +743,6 @@ impl TableRowLayout for Affine2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TableRowLayout for AlphaBlending {
|
|
||||||
fn type_name() -> &'static str {
|
|
||||||
"AlphaBlending"
|
|
||||||
}
|
|
||||||
fn identifier(&self) -> String {
|
|
||||||
format_alpha_blending(*self)
|
|
||||||
}
|
|
||||||
fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance {
|
|
||||||
TextLabel::new(format_alpha_blending(*self)).narrow(true).widget_instance()
|
|
||||||
}
|
|
||||||
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
|
||||||
vec![LayoutGroup::row(vec![self.value_widget(PathStep::Element(0), _data)])]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolves the value/breadcrumb label for a `NodeId` against `network_interface` at the given `network_path`,
|
/// Resolves the value/breadcrumb label for a `NodeId` against `network_interface` at the given `network_path`,
|
||||||
/// falling back to "Node {id}" if the node isn't present (e.g. an ID that no longer maps to a real node).
|
/// falling back to "Node {id}" if the node isn't present (e.g. an ID that no longer maps to a real node).
|
||||||
fn node_id_display_label(node_id: NodeId, network_interface: &NodeNetworkInterface, network_path: &[NodeId]) -> String {
|
fn node_id_display_label(node_id: NodeId, network_interface: &NodeNetworkInterface, network_path: &[NodeId]) -> String {
|
||||||
|
|
@ -899,7 +885,6 @@ macro_rules! known_table_row_types {
|
||||||
GradientStops,
|
GradientStops,
|
||||||
Color,
|
Color,
|
||||||
NodeId,
|
NodeId,
|
||||||
AlphaBlending,
|
|
||||||
DAffine2,
|
DAffine2,
|
||||||
DVec2,
|
DVec2,
|
||||||
Affine2,
|
Affine2,
|
||||||
|
|
@ -919,6 +904,16 @@ macro_rules! known_table_row_types {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Override hook for [`Table::attribute_display_value`] that prefers `Display` over `Debug` for select
|
||||||
|
/// attribute types. The underlying storage is generic and can only see a `Debug` bound, so types whose
|
||||||
|
/// nicer `Display` rendering matters in the data panel are listed here explicitly.
|
||||||
|
fn display_value_override(any: &dyn Any) -> Option<String> {
|
||||||
|
if let Some(value) = any.downcast_ref::<BlendMode>() {
|
||||||
|
return Some(value.to_string());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Type-dispatched widget for displaying an attribute value in a `Table<T>` item.
|
/// Type-dispatched widget for displaying an attribute value in a `Table<T>` item.
|
||||||
/// Delegates to [`TableRowLayout::value_widget`] so the same widget code is shared between
|
/// Delegates to [`TableRowLayout::value_widget`] so the same widget code is shared between
|
||||||
/// element-column rendering and attribute-column rendering. Returns `None` for unrecognized types so the
|
/// element-column rendering and attribute-column rendering. Returns `None` for unrecognized types so the
|
||||||
|
|
@ -1032,14 +1027,3 @@ fn format_dvec2(value: DVec2) -> String {
|
||||||
let round = |x: f64| (x * 1e3).round() / 1e3;
|
let round = |x: f64| (x * 1e3).round() / 1e3;
|
||||||
format!("({} px, {} px)", round(value.x), round(value.y))
|
format!("({} px, {} px)", round(value.x), round(value.y))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_alpha_blending(value: AlphaBlending) -> String {
|
|
||||||
let round = |x: f32| (x * 1e3).round() / 1e3;
|
|
||||||
format!(
|
|
||||||
"Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}",
|
|
||||||
value.blend_mode,
|
|
||||||
round(value.opacity * 100.),
|
|
||||||
round(value.fill * 100.),
|
|
||||||
if value.clip { "Yes" } else { "No" }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||||
}
|
}
|
||||||
GraphOperationMessage::BlendingFillSet { layer, fill } => {
|
GraphOperationMessage::BlendingFillSet { layer, fill } => {
|
||||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
modify_inputs.blending_fill_set(fill);
|
modify_inputs.opacity_fill_set(fill);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GraphOperationMessage::GradientStopsSet { layer, stops } => {
|
GraphOperationMessage::GradientStopsSet { layer, stops } => {
|
||||||
|
|
|
||||||
|
|
@ -437,7 +437,7 @@ impl<'a> ModifyInputsContext<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
|
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
|
||||||
let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blending::IDENTIFIER, true) else {
|
let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blend_mode::IDENTIFIER, true) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let input_connector = InputConnector::node(blend_node_id, 1);
|
let input_connector = InputConnector::node(blend_node_id, 1);
|
||||||
|
|
@ -445,19 +445,30 @@ impl<'a> ModifyInputsContext<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn opacity_set(&mut self, opacity: f64) {
|
pub fn opacity_set(&mut self, opacity: f64) {
|
||||||
let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blending::IDENTIFIER, true) else {
|
let Some(opacity_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::opacity::IDENTIFIER, true) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let input_connector = InputConnector::node(blend_node_id, 2);
|
// Enable the `has_opacity` checkbox so the value is applied
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(opacity * 100.), false), false);
|
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 1), NodeInput::value(TaggedValue::Bool(true), false), false);
|
||||||
|
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 2), NodeInput::value(TaggedValue::F64(opacity * 100.), false), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn blending_fill_set(&mut self, fill: f64) {
|
pub fn opacity_fill_set(&mut self, fill: f64) {
|
||||||
let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blending::IDENTIFIER, true) else {
|
// Reuse the Opacity node if already present (saving a chain walk on slider drags), otherwise let the next call create it
|
||||||
|
let identifier = graphene_std::blending_nodes::opacity::IDENTIFIER;
|
||||||
|
let existing = self.existing_proto_node_id(identifier.clone(), false);
|
||||||
|
let existed = existing.is_some();
|
||||||
|
let Some(opacity_node_id) = existing.or_else(|| self.existing_proto_node_id(identifier, true)) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let input_connector = InputConnector::node(blend_node_id, 3);
|
// Disable the opacity component on a freshly-created node so the slider only affects fill, mirroring the opacity-slider case
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(fill * 100.), false), false);
|
// (where the node's default `has_fill = false` already keeps fill out of the picture)
|
||||||
|
if !existed {
|
||||||
|
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 1), NodeInput::value(TaggedValue::Bool(false), false), false);
|
||||||
|
}
|
||||||
|
// Enable the `has_fill` checkbox so the value is applied
|
||||||
|
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 3), NodeInput::value(TaggedValue::Bool(true), false), false);
|
||||||
|
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 4), NodeInput::value(TaggedValue::F64(fill * 100.), false), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the stops table on the 'Gradient Value' node, creating it if necessary.
|
/// Set the stops table on the 'Gradient Value' node, creating it if necessary.
|
||||||
|
|
@ -556,10 +567,10 @@ impl<'a> ModifyInputsContext<'a> {
|
||||||
|
|
||||||
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
|
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
|
||||||
let clip = !clip_mode.unwrap_or(false);
|
let clip = !clip_mode.unwrap_or(false);
|
||||||
let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blending::IDENTIFIER, true) else {
|
let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::clipping_mask::IDENTIFIER, true) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let input_connector = InputConnector::node(clip_node_id, 4);
|
let input_connector = InputConnector::node(clip_node_id, 1);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false);
|
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2036,6 +2036,19 @@ fn static_input_properties() -> InputProperties {
|
||||||
))])
|
))])
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
map.insert(
|
||||||
|
// Like `optional_f64`, but the number input is configured as a percentage with a 0-100 range.
|
||||||
|
// As with `optional_f64`, the bool input must be at the input index directly before the f64 input.
|
||||||
|
"optional_percentage".to_string(),
|
||||||
|
Box::new(|node_id, index, context| {
|
||||||
|
let number_input = NumberInput::default().percentage().min(0.).max(100.);
|
||||||
|
Ok(vec![LayoutGroup::row(node_properties::optional_f64_widget(
|
||||||
|
ParameterWidgetsInfo::new(node_id, index, false, context),
|
||||||
|
index - 1,
|
||||||
|
number_input,
|
||||||
|
))])
|
||||||
|
}),
|
||||||
|
);
|
||||||
map.insert(
|
map.insert(
|
||||||
"vec2".to_string(),
|
"vec2".to_string(),
|
||||||
Box::new(|node_id, index, context| {
|
Box::new(|node_id, index, context| {
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,19 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||||
// ================================
|
// ================================
|
||||||
// blending
|
// blending
|
||||||
// ================================
|
// ================================
|
||||||
NodeReplacement {
|
// The legacy combined "Blending" node was split into separate Blend Mode, Opacity (now also covers fill), and Clip nodes.
|
||||||
node: graphene_std::blending_nodes::blending::IDENTIFIER,
|
// Old Blending references are remapped here to `blend_mode::IDENTIFIER` so the per-node migration in `migrate_node` can
|
||||||
aliases: &["graphene_core::raster::BlendingNode", "graphene_core::blending_nodes::BlendingNode"],
|
// detect a 5-input Blend Mode node and rewrite it as a chain (skipping any sub-node whose value is at the default and
|
||||||
},
|
// not exposed/wired up).
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::blending_nodes::blend_mode::IDENTIFIER,
|
node: graphene_std::blending_nodes::blend_mode::IDENTIFIER,
|
||||||
aliases: &["graphene_core::raster::BlendModeNode", "graphene_core::blending_nodes::BlendModeNode"],
|
aliases: &[
|
||||||
|
"graphene_core::raster::BlendModeNode",
|
||||||
|
"graphene_core::blending_nodes::BlendModeNode",
|
||||||
|
"graphene_core::raster::BlendingNode",
|
||||||
|
"graphene_core::blending_nodes::BlendingNode",
|
||||||
|
"blending_nodes::BlendingNode",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
NodeReplacement {
|
NodeReplacement {
|
||||||
node: graphene_std::blending_nodes::opacity::IDENTIFIER,
|
node: graphene_std::blending_nodes::opacity::IDENTIFIER,
|
||||||
|
|
@ -1090,6 +1096,175 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||||
|
|
||||||
let mut inputs_count = node.inputs.len();
|
let mut inputs_count = node.inputs.len();
|
||||||
|
|
||||||
|
// Split the legacy combined "Blending" node into a chain of separate Blend Mode, Opacity (now also covers fill), and Clip nodes.
|
||||||
|
// `NODE_REPLACEMENTS` rewrites the old `Blending` proto identifier (and its older aliases) to `blend_mode::IDENTIFIER`, so a leftover
|
||||||
|
// 5-input shape on a Blend Mode node identifies an old Blending node that still needs structural splitting. Sub-nodes whose values
|
||||||
|
// were at the default and weren't exposed/wired up are skipped, and the existing node ID is reused for the first kept sub-node so
|
||||||
|
// any references elsewhere in the document survive. If everything would be skipped, a no-op default Blend Mode node is left behind
|
||||||
|
// to preserve the chain structure (the user can delete it manually).
|
||||||
|
if reference == DefinitionIdentifier::ProtoNode(graphene_std::blending_nodes::blend_mode::IDENTIFIER) && inputs_count == 5 {
|
||||||
|
use graphene_std::blending::BlendMode;
|
||||||
|
|
||||||
|
// Snapshot the old inputs before any rewriting
|
||||||
|
let old_inputs = node.inputs.clone();
|
||||||
|
let content_input = old_inputs[0].clone();
|
||||||
|
let blend_mode_input = old_inputs[1].clone();
|
||||||
|
let opacity_input = old_inputs[2].clone();
|
||||||
|
let fill_input = old_inputs[3].clone();
|
||||||
|
let clip_input = old_inputs[4].clone();
|
||||||
|
|
||||||
|
// A sub-node is kept if its input is exposed/wired (so the user can drive it from the graph) OR if its value differs from the default
|
||||||
|
let keep_blend_mode = blend_mode_input.is_exposed() || !matches!(blend_mode_input.as_value(), Some(TaggedValue::BlendMode(BlendMode::Normal)));
|
||||||
|
let keep_opacity = opacity_input.is_exposed() || !matches!(opacity_input.as_value(), Some(TaggedValue::F64(v)) if (*v - 100.).abs() < f64::EPSILON);
|
||||||
|
let keep_fill = fill_input.is_exposed() || !matches!(fill_input.as_value(), Some(TaggedValue::F64(v)) if (*v - 100.).abs() < f64::EPSILON);
|
||||||
|
let keep_clip = clip_input.is_exposed() || !matches!(clip_input.as_value(), Some(TaggedValue::Bool(false)));
|
||||||
|
|
||||||
|
// Find the downstream connection so we can chain new nodes between this node and downstream
|
||||||
|
let downstream = document
|
||||||
|
.network_interface
|
||||||
|
.outward_wires(network_path)
|
||||||
|
.and_then(|wires| wires.get(&OutputConnector::node(*node_id, 0)))
|
||||||
|
.and_then(|connections| connections.first().cloned());
|
||||||
|
|
||||||
|
// The position of the existing node; subsequent chain nodes are placed to the right
|
||||||
|
let base_position = document.network_interface.position(node_id, network_path).unwrap_or_default();
|
||||||
|
|
||||||
|
// Decide which sub-node the existing ID becomes (the first one in the chain order: Blend Mode, Opacity, Clip)
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
enum SubNode {
|
||||||
|
BlendMode,
|
||||||
|
Opacity,
|
||||||
|
Clip,
|
||||||
|
}
|
||||||
|
let first_kind = if keep_blend_mode {
|
||||||
|
SubNode::BlendMode
|
||||||
|
} else if keep_opacity || keep_fill {
|
||||||
|
SubNode::Opacity
|
||||||
|
} else if keep_clip {
|
||||||
|
SubNode::Clip
|
||||||
|
} else {
|
||||||
|
// Everything was at default and not exposed: leave a no-op Blend Mode node so the chain structure is preserved
|
||||||
|
SubNode::BlendMode
|
||||||
|
};
|
||||||
|
|
||||||
|
let identifier_for = |kind: SubNode| match kind {
|
||||||
|
SubNode::BlendMode => graphene_std::blending_nodes::blend_mode::IDENTIFIER,
|
||||||
|
SubNode::Opacity => graphene_std::blending_nodes::opacity::IDENTIFIER,
|
||||||
|
SubNode::Clip => graphene_std::blending_nodes::clipping_mask::IDENTIFIER,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replace the existing node's implementation with the first kept sub-node's template
|
||||||
|
let first_reference = DefinitionIdentifier::ProtoNode(identifier_for(first_kind));
|
||||||
|
let mut first_template = resolve_document_node_type(&first_reference)?.default_node_template();
|
||||||
|
document.network_interface.replace_implementation(node_id, network_path, &mut first_template);
|
||||||
|
let _ = document.network_interface.replace_inputs(node_id, network_path, &mut first_template);
|
||||||
|
|
||||||
|
// Wire the existing node's inputs based on its new role (input 0 is always `content`)
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 0), content_input, network_path);
|
||||||
|
match first_kind {
|
||||||
|
SubNode::BlendMode => {
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 1), blend_mode_input.clone(), network_path);
|
||||||
|
}
|
||||||
|
SubNode::Opacity => {
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Bool(keep_opacity), false), network_path);
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 2), opacity_input.clone(), network_path);
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(keep_fill), false), network_path);
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 4), fill_input.clone(), network_path);
|
||||||
|
}
|
||||||
|
SubNode::Clip => {
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 1), clip_input.clone(), network_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the list of remaining sub-nodes to insert (after the first one), in chain order. Blend Mode is never
|
||||||
|
// reinserted because if it was kept it would already be the `first_kind`.
|
||||||
|
let mut remaining: Vec<SubNode> = Vec::new();
|
||||||
|
if first_kind != SubNode::Opacity && (keep_opacity || keep_fill) {
|
||||||
|
remaining.push(SubNode::Opacity);
|
||||||
|
}
|
||||||
|
if first_kind != SubNode::Clip && keep_clip {
|
||||||
|
remaining.push(SubNode::Clip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert each remaining sub-node into the chain between the previous node and the original downstream
|
||||||
|
let mut chain_x_offset = 7;
|
||||||
|
for sub in remaining {
|
||||||
|
let identifier = identifier_for(sub);
|
||||||
|
let new_id = NodeId::new();
|
||||||
|
let definition = match resolve_document_node_type(&DefinitionIdentifier::ProtoNode(identifier.clone())) {
|
||||||
|
Some(definition) => definition,
|
||||||
|
None => {
|
||||||
|
log::error!("Could not resolve `{identifier:?}` while migrating Blending node");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let template = definition.default_node_template();
|
||||||
|
document.network_interface.insert_node(new_id, template, network_path);
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.shift_absolute_node_position(&new_id, base_position + IVec2::new(chain_x_offset, 0), network_path);
|
||||||
|
chain_x_offset += 7;
|
||||||
|
|
||||||
|
// Splice this node in just before the original downstream input (so each iteration appends to the chain end)
|
||||||
|
if let Some(downstream_input) = &downstream {
|
||||||
|
document.network_interface.insert_node_between(&new_id, downstream_input, 0, network_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire the parameter inputs for the inserted node
|
||||||
|
match sub {
|
||||||
|
SubNode::BlendMode => {
|
||||||
|
document.network_interface.set_input(&InputConnector::node(new_id, 1), blend_mode_input.clone(), network_path);
|
||||||
|
}
|
||||||
|
SubNode::Opacity => {
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(new_id, 1), NodeInput::value(TaggedValue::Bool(keep_opacity), false), network_path);
|
||||||
|
document.network_interface.set_input(&InputConnector::node(new_id, 2), opacity_input.clone(), network_path);
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(new_id, 3), NodeInput::value(TaggedValue::Bool(keep_fill), false), network_path);
|
||||||
|
document.network_interface.set_input(&InputConnector::node(new_id, 4), fill_input.clone(), network_path);
|
||||||
|
}
|
||||||
|
SubNode::Clip => {
|
||||||
|
document.network_interface.set_input(&InputConnector::node(new_id, 1), clip_input.clone(), network_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs_count = match first_kind {
|
||||||
|
SubNode::BlendMode => 2,
|
||||||
|
SubNode::Opacity => 5,
|
||||||
|
SubNode::Clip => 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade old single-purpose Opacity node (content, opacity) to the new shape that also covers fill:
|
||||||
|
// (content, has_opacity, opacity, has_fill, fill). The original opacity input is preserved with `has_opacity` enabled,
|
||||||
|
// while `has_fill` is left disabled and the fill value defaults to 100% (no change to fill behavior).
|
||||||
|
if reference == DefinitionIdentifier::ProtoNode(graphene_std::blending_nodes::opacity::IDENTIFIER) && inputs_count == 2 {
|
||||||
|
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||||
|
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||||
|
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||||
|
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Bool(true), false), network_path);
|
||||||
|
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[1].clone(), network_path);
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(false), false), network_path);
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::F64(100.), false), network_path);
|
||||||
|
|
||||||
|
inputs_count = 5;
|
||||||
|
}
|
||||||
|
|
||||||
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
|
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
|
||||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 8 {
|
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 8 {
|
||||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||||
|
|
|
||||||
|
|
@ -333,25 +333,28 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
|
||||||
Some(color.to_linear_srgb())
|
Some(color.to_linear_srgb())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current blend mode of a layer from the closest "Blending" node.
|
/// Get the current blend mode of a layer from the closest upstream "Blend Mode" node.
|
||||||
pub fn get_blend_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<BlendMode> {
|
pub fn get_blend_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<BlendMode> {
|
||||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::blending_nodes::blending::IDENTIFIER))?;
|
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::blending_nodes::blend_mode::IDENTIFIER))?;
|
||||||
let TaggedValue::BlendMode(blend_mode) = inputs.get(1)?.as_value()? else {
|
let TaggedValue::BlendMode(blend_mode) = inputs.get(1)?.as_value()? else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
Some(*blend_mode)
|
Some(*blend_mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current opacity of a layer from the closest "Blending" node.
|
/// Get the current opacity of a layer from the closest upstream "Opacity" node, only when the node's `has_opacity` checkbox is enabled.
|
||||||
/// This may differ from the actual opacity contained within the data type reaching this layer, because that actual opacity may be:
|
/// This may differ from the actual opacity contained within the data type reaching this layer, because that actual opacity may be:
|
||||||
/// - Multiplied with additional opacity nodes earlier in the chain
|
/// - Multiplied with additional Opacity nodes earlier in the chain
|
||||||
/// - Set by an Opacity node with an exposed input value driven by another node
|
/// - Set by an Opacity node with an exposed input value driven by another node
|
||||||
/// - Already factored into the pixel alpha channel of an image
|
/// - Already factored into the pixel alpha channel of an image
|
||||||
/// - The default value of 100% if no Opacity node is present, but this function returns None in that case
|
/// - The default value of 100% if no Opacity node has its checkbox enabled (this function returns `None` in that case)
|
||||||
///
|
///
|
||||||
/// With those limitations in mind, the intention of this function is to show just the value already present in an upstream Opacity node so that value can be directly edited.
|
/// With those limitations in mind, the intention of this function is to show just the value already present in an upstream Opacity node so that value can be directly edited.
|
||||||
pub fn get_opacity(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
|
pub fn get_opacity(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
|
||||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::blending_nodes::blending::IDENTIFIER))?;
|
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::blending_nodes::opacity::IDENTIFIER))?;
|
||||||
|
let TaggedValue::Bool(true) = inputs.get(1)?.as_value()? else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
let TaggedValue::F64(opacity) = inputs.get(2)?.as_value()? else {
|
let TaggedValue::F64(opacity) = inputs.get(2)?.as_value()? else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
@ -359,16 +362,20 @@ pub fn get_opacity(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_clip_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<bool> {
|
pub fn get_clip_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<bool> {
|
||||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::blending_nodes::blending::IDENTIFIER))?;
|
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::blending_nodes::clipping_mask::IDENTIFIER))?;
|
||||||
let TaggedValue::Bool(clip) = inputs.get(4)?.as_value()? else {
|
let TaggedValue::Bool(clip) = inputs.get(1)?.as_value()? else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
Some(*clip)
|
Some(*clip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the current fill of a layer from the closest upstream "Opacity" node, only when the node's `has_fill` checkbox is enabled.
|
||||||
pub fn get_fill(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
|
pub fn get_fill(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
|
||||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::blending_nodes::blending::IDENTIFIER))?;
|
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::blending_nodes::opacity::IDENTIFIER))?;
|
||||||
let TaggedValue::F64(fill) = inputs.get(3)?.as_value()? else {
|
let TaggedValue::Bool(true) = inputs.get(3)?.as_value()? else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let TaggedValue::F64(fill) = inputs.get(4)?.as_value()? else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
Some(*fill)
|
Some(*fill)
|
||||||
|
|
|
||||||
|
|
@ -197,10 +197,11 @@ fn blend_mode<T: SetBlendMode>(
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modifies the opacity of the input graphics by multiplying the existing opacity by this percentage.
|
/// Modifies the opacity and/or fill of the input graphics by multiplying the existing values by these percentages.
|
||||||
/// This affects the transparency of the content (together with anything above which is clipped to it).
|
/// Opacity affects the transparency of the content (together with anything above which is clipped to it).
|
||||||
|
/// Fill affects the transparency of the content itself, independent of any content clipped to it.
|
||||||
#[node_macro::node(category("Blending"))]
|
#[node_macro::node(category("Blending"))]
|
||||||
fn opacity<T: MultiplyAlpha>(
|
fn opacity<T: MultiplyAlpha + MultiplyFill>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The layer stack that will be composited when rendering.
|
/// The layer stack that will be composited when rendering.
|
||||||
#[implementations(
|
#[implementations(
|
||||||
|
|
@ -211,19 +212,37 @@ fn opacity<T: MultiplyAlpha>(
|
||||||
Table<GradientStops>,
|
Table<GradientStops>,
|
||||||
)]
|
)]
|
||||||
mut content: T,
|
mut content: T,
|
||||||
|
/// Whether the *Opacity* property is enabled, multiplying the existing opacity by the chosen percentage.
|
||||||
|
#[widget(ParsedWidgetOverride::Hidden)]
|
||||||
|
#[default(true)]
|
||||||
|
has_opacity: bool,
|
||||||
/// How visible the content should be, including any content clipped to it.
|
/// How visible the content should be, including any content clipped to it.
|
||||||
/// Ranges from the default of 100% (fully opaque) to 0% (fully transparent).
|
/// Ranges from the default of 100% (fully opaque) to 0% (fully transparent).
|
||||||
|
#[widget(ParsedWidgetOverride::Custom = "optional_percentage")]
|
||||||
#[default(100.)]
|
#[default(100.)]
|
||||||
opacity: Percentage,
|
opacity: Percentage,
|
||||||
|
/// Whether the *Fill* property is enabled, multiplying the existing fill by the chosen percentage.
|
||||||
|
#[widget(ParsedWidgetOverride::Hidden)]
|
||||||
|
has_fill: bool,
|
||||||
|
/// How visible the content should be, independent of any content clipped to it.
|
||||||
|
/// Ranges from 0% (fully transparent) to the default of 100% (fully opaque).
|
||||||
|
#[widget(ParsedWidgetOverride::Custom = "optional_percentage")]
|
||||||
|
#[default(100.)]
|
||||||
|
fill: Percentage,
|
||||||
) -> T {
|
) -> T {
|
||||||
// TODO: Find a way to make this apply once to the table's parent (i.e. its item in its parent table or TableRow<T>) rather than applying to each item in its own table, which produces the undesired result
|
// TODO: Find a way to make this apply once to the table's parent (i.e. its item in its parent table or TableRow<T>) rather than applying to each item in its own table, which produces the undesired result
|
||||||
|
if has_opacity {
|
||||||
content.multiply_alpha(opacity / 100.);
|
content.multiply_alpha(opacity / 100.);
|
||||||
|
}
|
||||||
|
if has_fill {
|
||||||
|
content.multiply_fill(fill / 100.);
|
||||||
|
}
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets each of the blending properties at once. The blend mode determines how overlapping content is composited together. The opacity affects the transparency of the content (together with anything above which is clipped to it). The fill affects the transparency of the content itself, without affecting that of content clipped to it. The clip property determines whether the content inherits the alpha of the content beneath it.
|
/// Sets whether the input graphics inherit the alpha of the content beneath them, "clipping" them to that content.
|
||||||
#[node_macro::node(category("Blending"))]
|
#[node_macro::node(category("Blending"))]
|
||||||
fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
|
fn clipping_mask<T: SetClip>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The layer stack that will be composited when rendering.
|
/// The layer stack that will be composited when rendering.
|
||||||
#[implementations(
|
#[implementations(
|
||||||
|
|
@ -234,23 +253,10 @@ fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
|
||||||
Table<GradientStops>,
|
Table<GradientStops>,
|
||||||
)]
|
)]
|
||||||
mut content: T,
|
mut content: T,
|
||||||
/// The choice of equation that controls how brightness and color blends between overlapping pixels.
|
|
||||||
blend_mode: BlendMode,
|
|
||||||
/// How visible the content should be, including any content clipped to it.
|
|
||||||
/// Ranges from the default of 100% (fully opaque) to 0% (fully transparent).
|
|
||||||
#[default(100.)]
|
|
||||||
opacity: Percentage,
|
|
||||||
/// How visible the content should be, independent of any content clipped to it.
|
|
||||||
/// Ranges from 0% (fully transparent) to 100% (fully opaque).
|
|
||||||
#[default(100.)]
|
|
||||||
fill: Percentage,
|
|
||||||
/// Whether the content inherits the alpha of the content beneath it.
|
/// Whether the content inherits the alpha of the content beneath it.
|
||||||
clip: bool,
|
clip: bool,
|
||||||
) -> T {
|
) -> T {
|
||||||
// TODO: Find a way to make this apply once to the table's parent (i.e. its item in its parent table or TableRow<T>) rather than applying to each item in its own table, which produces the undesired result
|
// TODO: Find a way to make this apply once to the table's parent (i.e. its item in its parent table or TableRow<T>) rather than applying to each item in its own table, which produces the undesired result
|
||||||
content.set_blend_mode(blend_mode);
|
|
||||||
content.multiply_alpha(opacity / 100.);
|
|
||||||
content.multiply_fill(fill / 100.);
|
|
||||||
content.set_clip(clip);
|
content.set_clip(clip);
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue