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:
Keavon Chambers
2026-04-30 20:04:04 -07:00
committed by GitHub
parent 4b2430290c
commit 86134c26b4
9 changed files with 272 additions and 76 deletions
@@ -6,15 +6,16 @@ use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::{Affine2, DAffine2, Vec2};
use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::Context;
use graphene_std::Graphic;
use graphene_std::blending::BlendMode;
use graphene_std::gradient::GradientStops;
use graphene_std::memo::IORecord;
use graphene_std::raster_types::{CPU, GPU, Raster};
use graphene_std::table::Table;
use graphene_std::vector::Vector;
use graphene_std::vector::style::{Fill, FillChoice};
use graphene_std::{AlphaBlending, Color};
use std::any::Any;
use std::sync::Arc;
@@ -283,7 +284,7 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
for key in &attribute_keys {
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 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()
});
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`,
/// 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 {
@@ -899,7 +885,6 @@ macro_rules! known_table_row_types {
GradientStops,
Color,
NodeId,
AlphaBlending,
DAffine2,
DVec2,
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.
/// 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
@@ -1032,14 +1027,3 @@ fn format_dvec2(value: DVec2) -> String {
let round = |x: f64| (x * 1e3).round() / 1e3;
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 } => {
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 } => {
@@ -437,7 +437,7 @@ impl<'a> ModifyInputsContext<'a> {
}
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;
};
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) {
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;
};
let input_connector = InputConnector::node(blend_node_id, 2);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(opacity * 100.), false), false);
// Enable the `has_opacity` checkbox so the value is applied
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) {
let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blending::IDENTIFIER, true) else {
pub fn opacity_fill_set(&mut self, fill: f64) {
// 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;
};
let input_connector = InputConnector::node(blend_node_id, 3);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(fill * 100.), false), false);
// Disable the opacity component on a freshly-created node so the slider only affects fill, mirroring the opacity-slider case
// (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.
@@ -556,10 +567,10 @@ impl<'a> ModifyInputsContext<'a> {
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
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;
};
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);
}
@@ -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(
"vec2".to_string(),
Box::new(|node_id, index, context| {