Skip to content

Commit 4b24302

Browse files
authored
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
1 parent e686ee9 commit 4b24302

13 files changed

Lines changed: 579 additions & 219 deletions

File tree

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,6 @@
7070
"*.graphite": "json"
7171
},
7272
"editor.renderWhitespace": "boundary",
73-
"editor.minimap.markSectionHeaderRegex": "// ===+\\n\\s*//\\s*(?<label>[^\\n]{1,18})[^\\n]*(\\n\\s*//[^\\n]*)*\\n\\s*// ===+"
73+
"editor.minimap.markSectionHeaderRegex": "// ===+\\n\\s*//\\s*(?<label>[^\\n]{1,18})[^\\n]*(\\n\\s*//[^\\n]*)*\\n\\s*// ===+",
74+
"git.addAICoAuthor": "off"
7475
}

editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ use graphene_std::color::Color;
99
use graphene_std::raster::BlendMode;
1010
use graphene_std::raster_types::Image;
1111
use graphene_std::subpath::Subpath;
12-
use graphene_std::table::Table;
1312
use graphene_std::text::{Font, TypesettingConfig};
14-
use graphene_std::vector::style::{Fill, Stroke};
13+
use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientType, Stroke};
1514
use graphene_std::vector::{GradientStops, PointId, VectorModificationType};
1615

1716
#[impl_message(Message, DocumentMessage, GraphOperation)]
@@ -25,9 +24,22 @@ pub enum GraphOperationMessage {
2524
layer: LayerNodeIdentifier,
2625
fill: f64,
2726
},
28-
GradientTableSet {
27+
GradientStopsSet {
2928
layer: LayerNodeIdentifier,
30-
gradient_table: Table<GradientStops>,
29+
stops: GradientStops,
30+
},
31+
GradientLineSet {
32+
layer: LayerNodeIdentifier,
33+
start: DVec2,
34+
end: DVec2,
35+
},
36+
GradientTypeSet {
37+
layer: LayerNodeIdentifier,
38+
gradient_type: GradientType,
39+
},
40+
GradientSpreadMethodSet {
41+
layer: LayerNodeIdentifier,
42+
spread_method: GradientSpreadMethod,
3143
},
3244
OpacitySet {
3345
layer: LayerNodeIdentifier,

editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,24 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
4545
modify_inputs.blending_fill_set(fill);
4646
}
4747
}
48-
GraphOperationMessage::GradientTableSet { layer, gradient_table } => {
48+
GraphOperationMessage::GradientStopsSet { layer, stops } => {
4949
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
50-
modify_inputs.gradient_table_set(gradient_table);
50+
modify_inputs.gradient_stops_set(stops);
51+
}
52+
}
53+
GraphOperationMessage::GradientLineSet { layer, start, end } => {
54+
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
55+
modify_inputs.gradient_line_set(start, end);
56+
}
57+
}
58+
GraphOperationMessage::GradientTypeSet { layer, gradient_type } => {
59+
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
60+
modify_inputs.gradient_type_set(gradient_type);
61+
}
62+
}
63+
GraphOperationMessage::GradientSpreadMethodSet { layer, spread_method } => {
64+
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
65+
modify_inputs.gradient_spread_method_set(spread_method);
5166
}
5267
}
5368
GraphOperationMessage::OpacitySet { layer, opacity } => {

editor/src/messages/portfolio/document/graph_operation/utility_types.rs

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use graphene_std::raster_types::Image;
1313
use graphene_std::subpath::Subpath;
1414
use graphene_std::table::Table;
1515
use graphene_std::text::{Font, TypesettingConfig};
16-
use graphene_std::vector::style::{Fill, Stroke};
16+
use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientType, Stroke};
1717
use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType};
1818
use graphene_std::{Color, Graphic, NodeInputDecleration};
1919

@@ -460,13 +460,98 @@ impl<'a> ModifyInputsContext<'a> {
460460
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(fill * 100.), false), false);
461461
}
462462

463-
pub fn gradient_table_set(&mut self, gradient_table: Table<GradientStops>) {
463+
/// Set the stops table on the 'Gradient Value' node, creating it if necessary.
464+
pub fn gradient_stops_set(&mut self, stops: GradientStops) {
464465
let Some(gradient_node_id) = self.existing_proto_node_id(graphene_std::math_nodes::gradient_value::IDENTIFIER, true) else {
465466
return;
466467
};
467-
468468
let input_connector = InputConnector::node(gradient_node_id, graphene_std::math_nodes::gradient_value::GradientInput::INDEX);
469-
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientTable(gradient_table), false), false);
469+
let stops_table = Table::new_from_element(stops);
470+
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientTable(stops_table), false), false);
471+
}
472+
473+
/// Update the gradient line so its endpoints are at `new_start` and `new_end`.
474+
/// With multiple `Transform` nodes the last one (closest to the layer) is modified so the chain still composes to the target.
475+
/// With none, one is inserted unless the target is the identity.
476+
pub fn gradient_line_set(&mut self, new_start: DVec2, new_end: DVec2) {
477+
let Some(output_layer) = self.get_output_layer() else { return };
478+
479+
let transform_reference = DefinitionIdentifier::Network("Transform".into());
480+
let upstream_transforms: Vec<NodeId> = self
481+
.network_interface
482+
.upstream_flow_back_from_nodes(vec![output_layer.to_node()], &[], network_interface::FlowType::HorizontalFlow)
483+
.skip(1)
484+
.take_while(|node_id| !self.network_interface.is_layer(node_id, &[]))
485+
.filter(|node_id| self.network_interface.reference(node_id, &[]).as_ref() == Some(&transform_reference))
486+
.collect();
487+
488+
// Upstream walk yields downstream-to-upstream order, so the first hit is the chain's last `Transform`
489+
let (last_transform_node_id, prior_transforms) = match upstream_transforms.split_first() {
490+
Some((last, prior)) => (Some(*last), prior),
491+
None => (None, [].as_slice()),
492+
};
493+
494+
// `composed_old` = T_n * T_{n-1} * ... * T_1, `prior_combined` = same product without T_n
495+
let compose = |ids: &[_]| {
496+
ids.iter().fold(DAffine2::IDENTITY, |acc, transform_id| {
497+
self.network_interface
498+
.document_network()
499+
.nodes
500+
.get(transform_id)
501+
.map_or(acc, |document_node| acc * transform_utils::get_current_transform(&document_node.inputs))
502+
})
503+
};
504+
let composed_old = compose(&upstream_transforms);
505+
let prior_combined = compose(prior_transforms);
506+
507+
// Rebuild the y-axis from the new x-axis using the old (parallel, perpendicular) decomposition and length ratio,
508+
// so the gradient's aspect ratio and skew survive an endpoint drag (so an ellipse stays the same ellipse) instead of
509+
// the old y-axis vector remaining fixed while x changes
510+
let new_x_axis = new_end - new_start;
511+
let preserved_y_axis = scale_y_axis_to_match_new_x(composed_old.matrix2.x_axis, composed_old.matrix2.y_axis, new_x_axis);
512+
let new_composed = DAffine2 {
513+
matrix2: glam::DMat2::from_cols(new_x_axis, preserved_y_axis),
514+
translation: new_start,
515+
};
516+
517+
let last_transform_value = new_composed * prior_combined.inverse();
518+
519+
let transform_node_id = if let Some(id) = last_transform_node_id {
520+
id
521+
} else {
522+
// Don't pollute the graph with an identity 'Transform' node
523+
if last_transform_value.abs_diff_eq(DAffine2::IDENTITY, 1e-6) {
524+
return;
525+
}
526+
let Some(id) = self.existing_network_node_id("Transform", true) else { return };
527+
id
528+
};
529+
530+
transform_utils::update_transform(self.network_interface, &transform_node_id, last_transform_value);
531+
self.responses.add(PropertiesPanelMessage::Refresh);
532+
self.responses.add(NodeGraphMessage::RunDocumentGraph);
533+
}
534+
535+
/// Write the gradient type to the last 'Gradient Type' node in the chain, inserting one only when the value differs
536+
/// from the default (`Linear`).
537+
pub fn gradient_type_set(&mut self, gradient_type: GradientType) {
538+
let identifier = graphene_std::math_nodes::gradient_type::IDENTIFIER;
539+
let create_if_nonexistent = gradient_type != GradientType::default();
540+
let Some(node_id) = self.existing_proto_node_id(identifier, create_if_nonexistent) else { return };
541+
542+
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::gradient_type::GradientTypeInput::INDEX);
543+
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientType(gradient_type), false), false);
544+
}
545+
546+
/// Write the spread method to the last 'Spread Method' node in the chain, inserting one only when the value differs
547+
/// from the default (`Pad`).
548+
pub fn gradient_spread_method_set(&mut self, spread_method: GradientSpreadMethod) {
549+
let identifier = graphene_std::math_nodes::spread_method::IDENTIFIER;
550+
let create_if_nonexistent = spread_method != GradientSpreadMethod::default();
551+
let Some(node_id) = self.existing_proto_node_id(identifier, create_if_nonexistent) else { return };
552+
553+
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::spread_method::SpreadMethodInput::INDEX);
554+
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientSpreadMethod(spread_method), false), false);
470555
}
471556

472557
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
@@ -621,3 +706,29 @@ impl<'a> ModifyInputsContext<'a> {
621706
}
622707
}
623708
}
709+
710+
/// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both
711+
/// rescaled by `|new_x| / |old_x|`. This holds the (x, y) parallelogram's aspect ratio and skew fixed across an endpoint
712+
/// drag, so a radial ellipse stays the same shape (just rotated and resized) instead of distorting as x grows or shrinks.
713+
/// Falls back to a +90° rotation of `new_x` when `old_x` is degenerate.
714+
fn scale_y_axis_to_match_new_x(old_x: DVec2, old_y: DVec2, new_x: DVec2) -> DVec2 {
715+
let old_x_length = old_x.length();
716+
if old_x_length < 1e-9 {
717+
return DVec2::new(-new_x.y, new_x.x);
718+
}
719+
let ex_old = old_x / old_x_length;
720+
let ey_old = DVec2::new(-ex_old.y, ex_old.x);
721+
722+
let new_x_length = new_x.length();
723+
if new_x_length < 1e-9 {
724+
return DVec2::ZERO;
725+
}
726+
let ex_new = new_x / new_x_length;
727+
let ey_new = DVec2::new(-ex_new.y, ex_new.x);
728+
729+
let parallel = old_y.dot(ex_old);
730+
let perpendicular = old_y.dot(ey_old);
731+
let scale = new_x_length / old_x_length;
732+
733+
scale * (parallel * ex_new + perpendicular * ey_new)
734+
}

0 commit comments

Comments
 (0)