mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 23:28:12 +08:00
New nodes: 'Gradient Type' and 'Spread Method', and add Gradient tool support for controlling these nodes (#4084)
* Use 'Transform', 'Gradient Type', and 'Spread Method' nodes for table gradients * Add gradient widget to the tool's control bar and update where the two swap buttons go * Fix gradient rendering * Format * Code review
This commit is contained in:
@@ -9,9 +9,8 @@ use graphene_std::color::Color;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::Image;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::style::{Fill, Stroke};
|
||||
use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientType, Stroke};
|
||||
use graphene_std::vector::{GradientStops, PointId, VectorModificationType};
|
||||
|
||||
#[impl_message(Message, DocumentMessage, GraphOperation)]
|
||||
@@ -25,9 +24,22 @@ pub enum GraphOperationMessage {
|
||||
layer: LayerNodeIdentifier,
|
||||
fill: f64,
|
||||
},
|
||||
GradientTableSet {
|
||||
GradientStopsSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
gradient_table: Table<GradientStops>,
|
||||
stops: GradientStops,
|
||||
},
|
||||
GradientLineSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
start: DVec2,
|
||||
end: DVec2,
|
||||
},
|
||||
GradientTypeSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
gradient_type: GradientType,
|
||||
},
|
||||
GradientSpreadMethodSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
spread_method: GradientSpreadMethod,
|
||||
},
|
||||
OpacitySet {
|
||||
layer: LayerNodeIdentifier,
|
||||
|
||||
+17
-2
@@ -45,9 +45,24 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
modify_inputs.blending_fill_set(fill);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientTableSet { layer, gradient_table } => {
|
||||
GraphOperationMessage::GradientStopsSet { layer, stops } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_table_set(gradient_table);
|
||||
modify_inputs.gradient_stops_set(stops);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientLineSet { layer, start, end } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_line_set(start, end);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientTypeSet { layer, gradient_type } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_type_set(gradient_type);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientSpreadMethodSet { layer, spread_method } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_spread_method_set(spread_method);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::OpacitySet { layer, opacity } => {
|
||||
|
||||
@@ -13,7 +13,7 @@ use graphene_std::raster_types::Image;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::table::Table;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::style::{Fill, Stroke};
|
||||
use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientType, Stroke};
|
||||
use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType};
|
||||
use graphene_std::{Color, Graphic, NodeInputDecleration};
|
||||
|
||||
@@ -460,13 +460,98 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(fill * 100.), false), false);
|
||||
}
|
||||
|
||||
pub fn gradient_table_set(&mut self, gradient_table: Table<GradientStops>) {
|
||||
/// Set the stops table on the 'Gradient Value' node, creating it if necessary.
|
||||
pub fn gradient_stops_set(&mut self, stops: GradientStops) {
|
||||
let Some(gradient_node_id) = self.existing_proto_node_id(graphene_std::math_nodes::gradient_value::IDENTIFIER, true) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let input_connector = InputConnector::node(gradient_node_id, graphene_std::math_nodes::gradient_value::GradientInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientTable(gradient_table), false), false);
|
||||
let stops_table = Table::new_from_element(stops);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientTable(stops_table), false), false);
|
||||
}
|
||||
|
||||
/// Update the gradient line so its endpoints are at `new_start` and `new_end`.
|
||||
/// With multiple `Transform` nodes the last one (closest to the layer) is modified so the chain still composes to the target.
|
||||
/// With none, one is inserted unless the target is the identity.
|
||||
pub fn gradient_line_set(&mut self, new_start: DVec2, new_end: DVec2) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
|
||||
let transform_reference = DefinitionIdentifier::Network("Transform".into());
|
||||
let upstream_transforms: Vec<NodeId> = self
|
||||
.network_interface
|
||||
.upstream_flow_back_from_nodes(vec![output_layer.to_node()], &[], network_interface::FlowType::HorizontalFlow)
|
||||
.skip(1)
|
||||
.take_while(|node_id| !self.network_interface.is_layer(node_id, &[]))
|
||||
.filter(|node_id| self.network_interface.reference(node_id, &[]).as_ref() == Some(&transform_reference))
|
||||
.collect();
|
||||
|
||||
// Upstream walk yields downstream-to-upstream order, so the first hit is the chain's last `Transform`
|
||||
let (last_transform_node_id, prior_transforms) = match upstream_transforms.split_first() {
|
||||
Some((last, prior)) => (Some(*last), prior),
|
||||
None => (None, [].as_slice()),
|
||||
};
|
||||
|
||||
// `composed_old` = T_n * T_{n-1} * ... * T_1, `prior_combined` = same product without T_n
|
||||
let compose = |ids: &[_]| {
|
||||
ids.iter().fold(DAffine2::IDENTITY, |acc, transform_id| {
|
||||
self.network_interface
|
||||
.document_network()
|
||||
.nodes
|
||||
.get(transform_id)
|
||||
.map_or(acc, |document_node| acc * transform_utils::get_current_transform(&document_node.inputs))
|
||||
})
|
||||
};
|
||||
let composed_old = compose(&upstream_transforms);
|
||||
let prior_combined = compose(prior_transforms);
|
||||
|
||||
// Rebuild the y-axis from the new x-axis using the old (parallel, perpendicular) decomposition and length ratio,
|
||||
// so the gradient's aspect ratio and skew survive an endpoint drag (so an ellipse stays the same ellipse) instead of
|
||||
// the old y-axis vector remaining fixed while x changes
|
||||
let new_x_axis = new_end - new_start;
|
||||
let preserved_y_axis = scale_y_axis_to_match_new_x(composed_old.matrix2.x_axis, composed_old.matrix2.y_axis, new_x_axis);
|
||||
let new_composed = DAffine2 {
|
||||
matrix2: glam::DMat2::from_cols(new_x_axis, preserved_y_axis),
|
||||
translation: new_start,
|
||||
};
|
||||
|
||||
let last_transform_value = new_composed * prior_combined.inverse();
|
||||
|
||||
let transform_node_id = if let Some(id) = last_transform_node_id {
|
||||
id
|
||||
} else {
|
||||
// Don't pollute the graph with an identity 'Transform' node
|
||||
if last_transform_value.abs_diff_eq(DAffine2::IDENTITY, 1e-6) {
|
||||
return;
|
||||
}
|
||||
let Some(id) = self.existing_network_node_id("Transform", true) else { return };
|
||||
id
|
||||
};
|
||||
|
||||
transform_utils::update_transform(self.network_interface, &transform_node_id, last_transform_value);
|
||||
self.responses.add(PropertiesPanelMessage::Refresh);
|
||||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
/// Write the gradient type to the last 'Gradient Type' node in the chain, inserting one only when the value differs
|
||||
/// from the default (`Linear`).
|
||||
pub fn gradient_type_set(&mut self, gradient_type: GradientType) {
|
||||
let identifier = graphene_std::math_nodes::gradient_type::IDENTIFIER;
|
||||
let create_if_nonexistent = gradient_type != GradientType::default();
|
||||
let Some(node_id) = self.existing_proto_node_id(identifier, create_if_nonexistent) else { return };
|
||||
|
||||
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::gradient_type::GradientTypeInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientType(gradient_type), false), false);
|
||||
}
|
||||
|
||||
/// Write the spread method to the last 'Spread Method' node in the chain, inserting one only when the value differs
|
||||
/// from the default (`Pad`).
|
||||
pub fn gradient_spread_method_set(&mut self, spread_method: GradientSpreadMethod) {
|
||||
let identifier = graphene_std::math_nodes::spread_method::IDENTIFIER;
|
||||
let create_if_nonexistent = spread_method != GradientSpreadMethod::default();
|
||||
let Some(node_id) = self.existing_proto_node_id(identifier, create_if_nonexistent) else { return };
|
||||
|
||||
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::spread_method::SpreadMethodInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientSpreadMethod(spread_method), false), false);
|
||||
}
|
||||
|
||||
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
|
||||
@@ -621,3 +706,29 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both
|
||||
/// rescaled by `|new_x| / |old_x|`. This holds the (x, y) parallelogram's aspect ratio and skew fixed across an endpoint
|
||||
/// drag, so a radial ellipse stays the same shape (just rotated and resized) instead of distorting as x grows or shrinks.
|
||||
/// Falls back to a +90° rotation of `new_x` when `old_x` is degenerate.
|
||||
fn scale_y_axis_to_match_new_x(old_x: DVec2, old_y: DVec2, new_x: DVec2) -> DVec2 {
|
||||
let old_x_length = old_x.length();
|
||||
if old_x_length < 1e-9 {
|
||||
return DVec2::new(-new_x.y, new_x.x);
|
||||
}
|
||||
let ex_old = old_x / old_x_length;
|
||||
let ey_old = DVec2::new(-ex_old.y, ex_old.x);
|
||||
|
||||
let new_x_length = new_x.length();
|
||||
if new_x_length < 1e-9 {
|
||||
return DVec2::ZERO;
|
||||
}
|
||||
let ex_new = new_x / new_x_length;
|
||||
let ey_new = DVec2::new(-ex_new.y, ex_new.x);
|
||||
|
||||
let parallel = old_y.dot(ex_old);
|
||||
let perpendicular = old_y.dot(ey_old);
|
||||
let scale = new_x_length / old_x_length;
|
||||
|
||||
scale * (parallel * ex_new + perpendicular * ey_new)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ use super::document_node_definitions::{NODE_OVERRIDES, NodePropertiesContext};
|
||||
use super::utility_types::FrontendGraphDataType;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface};
|
||||
use crate::messages::portfolio::utility_types::{CachedData, FontCatalogStyle};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use choice::enum_choice;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
@@ -235,6 +237,7 @@ pub(crate) fn property_from_type(
|
||||
// =========================
|
||||
Some(x) if x == TypeId::of::<FillType>() => enum_choice::<FillType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<GradientType>() => enum_choice::<GradientType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<GradientSpreadMethod>() => enum_choice::<GradientSpreadMethod>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<RealTimeMode>() => enum_choice::<RealTimeMode>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<RedGreenBlue>() => enum_choice::<RedGreenBlue>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<RedGreenBlueAlpha>() => enum_choice::<RedGreenBlueAlpha>().for_socket(default_info).property_row(),
|
||||
@@ -2025,11 +2028,21 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
|
||||
LayoutGroup::section(name, description, visible, pinned, node_id.0, Layout(layout))
|
||||
}
|
||||
|
||||
/// Resolve the viewport-space orientation of a Fill node's gradient by walking downstream to its owning layer
|
||||
/// and reusing the same helper the Gradient tool uses, so canvas tilt and layer transforms behave identically.
|
||||
fn gradient_orientation_in_fill_node(node_id: NodeId, gradient: &graphene_std::vector::style::Gradient, context: &mut NodePropertiesContext) -> Option<bool> {
|
||||
let layer_node = context.network_interface.downstream_layer_for_chain_node(&node_id, context.selection_network_path)?;
|
||||
let layer = LayerNodeIdentifier::new(layer_node, context.network_interface);
|
||||
let transform = graph_modification_utils::gradient_space_transform(layer, context.network_interface);
|
||||
Some(graph_modification_utils::gradient_orientation_rightward(gradient.start, gradient.end, transform))
|
||||
}
|
||||
|
||||
/// Fill Node Widgets LayoutGroup
|
||||
pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::vector::fill::*;
|
||||
|
||||
let mut widgets_first_row = start_widgets(ParameterWidgetsInfo::new(node_id, FillInput::<Color>::INDEX, true, context));
|
||||
// Pass blank_assist=false because the assist slot is filled below ("Reverse Stops" button when in gradient mode)
|
||||
let mut widgets_first_row = start_widgets(ParameterWidgetsInfo::new(node_id, FillInput::<Color>::INDEX, false, context));
|
||||
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
@@ -2052,6 +2065,30 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
let backup_color_fill: Fill = backup_color.clone().into();
|
||||
let backup_gradient_fill: Fill = backup_gradient.clone().into();
|
||||
|
||||
match fill {
|
||||
Fill::Gradient(gradient) => {
|
||||
let reverse_button = IconButton::new("Reverse", 24)
|
||||
.tooltip_label("Reverse Stops")
|
||||
.tooltip_description("Reverse the gradient color stops.")
|
||||
.on_update(update_value(
|
||||
{
|
||||
let gradient = gradient.clone();
|
||||
move |_| {
|
||||
let mut gradient = gradient.clone();
|
||||
gradient.stops = gradient.stops.reversed();
|
||||
TaggedValue::Fill(Fill::Gradient(gradient))
|
||||
}
|
||||
},
|
||||
node_id,
|
||||
FillInput::<Color>::INDEX,
|
||||
))
|
||||
.widget_instance();
|
||||
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets_first_row.push(reverse_button);
|
||||
}
|
||||
_ => add_blank_assist(&mut widgets_first_row),
|
||||
}
|
||||
|
||||
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets_first_row.push(
|
||||
ColorInput::default()
|
||||
@@ -2089,32 +2126,12 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
.on_commit(commit_value)
|
||||
.widget_instance(),
|
||||
);
|
||||
|
||||
let mut widgets = vec![LayoutGroup::row(widgets_first_row)];
|
||||
|
||||
let fill_type_switch = {
|
||||
let mut row = vec![TextLabel::new("").widget_instance()];
|
||||
match fill {
|
||||
Fill::Solid(_) | Fill::None => add_blank_assist(&mut row),
|
||||
Fill::Gradient(gradient) => {
|
||||
let reverse_button = IconButton::new("Reverse", 24)
|
||||
.tooltip_description("Reverse the gradient color stops.")
|
||||
.on_update(update_value(
|
||||
{
|
||||
let gradient = gradient.clone();
|
||||
move |_| {
|
||||
let mut gradient = gradient.clone();
|
||||
gradient.stops = gradient.stops.reversed();
|
||||
TaggedValue::Fill(Fill::Gradient(gradient))
|
||||
}
|
||||
},
|
||||
node_id,
|
||||
FillInput::<Color>::INDEX,
|
||||
))
|
||||
.widget_instance();
|
||||
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
row.push(reverse_button);
|
||||
}
|
||||
}
|
||||
add_blank_assist(&mut row);
|
||||
|
||||
let entries = vec![
|
||||
RadioEntryData::new("solid")
|
||||
@@ -2137,34 +2154,9 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
widgets.push(fill_type_switch);
|
||||
|
||||
if let Fill::Gradient(gradient) = fill.clone() {
|
||||
// Linear/Radial radio: blank assist (the "Reverse Direction" button has been moved down to the spread method row)
|
||||
let mut row = vec![TextLabel::new("").widget_instance()];
|
||||
match gradient.gradient_type {
|
||||
GradientType::Linear => add_blank_assist(&mut row),
|
||||
GradientType::Radial => {
|
||||
let orientation = if (gradient.end.x - gradient.start.x).abs() > f64::EPSILON * 1e6 {
|
||||
gradient.end.x > gradient.start.x
|
||||
} else {
|
||||
(gradient.start.x + gradient.start.y) < (gradient.end.x + gradient.end.y)
|
||||
};
|
||||
let reverse_radial_gradient_button = IconButton::new(if orientation { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24)
|
||||
.tooltip_description("Reverse which end the gradient radiates from.")
|
||||
.on_update(update_value(
|
||||
{
|
||||
let gradient = gradient.clone();
|
||||
move |_| {
|
||||
let mut gradient = gradient.clone();
|
||||
std::mem::swap(&mut gradient.start, &mut gradient.end);
|
||||
TaggedValue::Fill(Fill::Gradient(gradient))
|
||||
}
|
||||
},
|
||||
node_id,
|
||||
FillInput::<Color>::INDEX,
|
||||
))
|
||||
.widget_instance();
|
||||
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
row.push(reverse_radial_gradient_button);
|
||||
}
|
||||
}
|
||||
add_blank_assist(&mut row);
|
||||
|
||||
let gradient_for_closure = gradient.clone();
|
||||
|
||||
@@ -2203,7 +2195,33 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
|
||||
widgets.push(LayoutGroup::row(row));
|
||||
|
||||
let mut spread_methods_row: Vec<WidgetInstance> = vec![TextLabel::new("").widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance()];
|
||||
// "Reverse Direction" button (assist) plus the Pad/Reflect/Repeat radio. Icon orientation is resolved in viewport
|
||||
// space so canvas tilt and layer transforms behave the same as in the Gradient tool's control bar.
|
||||
let mut spread_methods_row = vec![TextLabel::new("").widget_instance()];
|
||||
|
||||
let orientation_rightward = gradient_orientation_in_fill_node(node_id, &gradient, context).unwrap_or(true);
|
||||
let reverse_direction_button = IconButton::new(if orientation_rightward { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24)
|
||||
.tooltip_label("Reverse Direction")
|
||||
.tooltip_description(if gradient.gradient_type == GradientType::Radial {
|
||||
"Reverse which end the gradient radiates from."
|
||||
} else {
|
||||
"Swap the start and end points of the gradient line."
|
||||
})
|
||||
.on_update(update_value(
|
||||
{
|
||||
let gradient = gradient.clone();
|
||||
move |_| {
|
||||
let mut gradient = gradient.clone();
|
||||
std::mem::swap(&mut gradient.start, &mut gradient.end);
|
||||
TaggedValue::Fill(Fill::Gradient(gradient))
|
||||
}
|
||||
},
|
||||
node_id,
|
||||
FillInput::<Color>::INDEX,
|
||||
))
|
||||
.widget_instance();
|
||||
spread_methods_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
spread_methods_row.push(reverse_direction_button);
|
||||
|
||||
let spread_method_entries = [GradientSpreadMethod::Pad, GradientSpreadMethod::Reflect, GradientSpreadMethod::Repeat]
|
||||
.iter()
|
||||
@@ -2247,8 +2265,10 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
})
|
||||
.collect();
|
||||
|
||||
add_blank_assist(&mut spread_methods_row);
|
||||
spread_methods_row.extend_from_slice(&[RadioInput::new(spread_method_entries).selected_index(Some(gradient.spread_method as u32)).widget_instance()]);
|
||||
spread_methods_row.extend_from_slice(&[
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
RadioInput::new(spread_method_entries).selected_index(Some(gradient.spread_method as u32)).widget_instance(),
|
||||
]);
|
||||
|
||||
widgets.push(LayoutGroup::row(spread_methods_row));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user