mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 12:08:13 +08:00
Rework Gradient into a newtype of List<Color> with optional position and midpoint attributes (#4397)
* Rework Gradient into a newtype of List<Color> with optional position and midpoint attributes * Fix Vello stopless-gradient fallback coverage, empty legacy gradient tables, the node docs gradient swatch, NaN position elision, and wired setter input overwrites
This commit is contained in:
@@ -35,6 +35,14 @@ pub enum GraphOperationMessage {
|
||||
layer: LayerNodeIdentifier,
|
||||
stops: Gradient,
|
||||
},
|
||||
GradientPositionsSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
positions: Vec<f64>,
|
||||
},
|
||||
GradientMidpointsSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
midpoints: Vec<f64>,
|
||||
},
|
||||
GradientTransformSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
transform: DAffine2,
|
||||
|
||||
+10
@@ -61,6 +61,16 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
modify_inputs.gradient_stops_set(stops);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientPositionsSet { layer, positions } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_positions_set(positions);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientMidpointsSet { layer, midpoints } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_midpoints_set(midpoints);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::GradientTransformSet { layer, transform } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.gradient_transform_set(transform);
|
||||
|
||||
@@ -552,6 +552,57 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(stops), false), false);
|
||||
}
|
||||
|
||||
/// Update the last 'Gradient Positions' node in the chain when one exists, so on-canvas stop drags stay live even
|
||||
/// though that node would otherwise override the stops value's own placement. Never inserts one: the stops value
|
||||
/// carries placement itself, and these setter nodes are user-authored procedural overrides. A wired input is
|
||||
/// procedural authorship too, so it is likewise left untouched.
|
||||
pub fn gradient_positions_set(&mut self, positions: Vec<f64>) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
|
||||
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||
let identifier = graphene_std::math_nodes::gradient_positions::IDENTIFIER;
|
||||
let Some(node_id) = self.existing_proto_node_id_at(&target_input, identifier, false) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let current_input = self
|
||||
.network_interface
|
||||
.document_network()
|
||||
.nodes
|
||||
.get(&node_id)
|
||||
.and_then(|node| node.input(graphene_std::math_nodes::gradient_positions::PositionsInput));
|
||||
if !current_input.is_some_and(|input| input.as_value().is_some()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::gradient_positions::PositionsInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64Array(positions), false), false);
|
||||
}
|
||||
|
||||
/// The 'Gradient Midpoints' counterpart of [`Self::gradient_positions_set`], likewise update-only.
|
||||
pub fn gradient_midpoints_set(&mut self, midpoints: Vec<f64>) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
|
||||
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||
let identifier = graphene_std::math_nodes::gradient_midpoints::IDENTIFIER;
|
||||
let Some(node_id) = self.existing_proto_node_id_at(&target_input, identifier, false) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let current_input = self
|
||||
.network_interface
|
||||
.document_network()
|
||||
.nodes
|
||||
.get(&node_id)
|
||||
.and_then(|node| node.input(graphene_std::math_nodes::gradient_midpoints::MidpointsInput));
|
||||
if !current_input.is_some_and(|input| input.as_value().is_some()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::gradient_midpoints::MidpointsInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64Array(midpoints), false), false);
|
||||
}
|
||||
|
||||
/// Update the transform to map the unit gradient ((0,0), (1, 0)) to the geometry's local space.
|
||||
/// 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.
|
||||
|
||||
@@ -33,7 +33,7 @@ use graphene_std::vector::misc::BooleanOperation;
|
||||
use graphene_std::vector::misc::{
|
||||
ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
||||
};
|
||||
use graphene_std::vector::style::{FillChoiceUI, Gradient, GradientSpreadMethod, GradientType, GradientUI, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation};
|
||||
use graphene_std::vector::style::{FillChoiceUI, Gradient, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation};
|
||||
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
|
||||
use graphene_std::{NodeParameter, ParameterRef};
|
||||
|
||||
@@ -1158,7 +1158,7 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
|
||||
// Add the color input
|
||||
let widget_value = match &**tagged_value {
|
||||
TaggedValue::Color(color) => FillChoiceUI::Solid(SRGBA8::from(*color)),
|
||||
TaggedValue::Gradient(stops) => FillChoiceUI::Gradient(GradientUI::from(stops)),
|
||||
TaggedValue::Gradient(stops) => FillChoiceUI::Gradient(GradientStops::from(stops)),
|
||||
value if value.is_no_paint() => FillChoiceUI::None,
|
||||
x => {
|
||||
warn!("Color {x:?}");
|
||||
@@ -1175,7 +1175,7 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
|
||||
FillChoiceUI::Gradient(gradient_ui) => TaggedValue::Gradient(Gradient::from(gradient_ui)),
|
||||
}
|
||||
} else if matches!(&**tagged_value, TaggedValue::Gradient(_)) {
|
||||
|input| TaggedValue::Gradient(input.value.as_gradient().map(Gradient::from).unwrap_or_default())
|
||||
|input| TaggedValue::Gradient(input.value.as_gradient().map(Gradient::from).unwrap_or_else(Gradient::black_to_white))
|
||||
} else {
|
||||
|input| TaggedValue::Color(input.value.as_solid().map(Color::from).unwrap_or(Color::TRANSPARENT))
|
||||
};
|
||||
@@ -1256,20 +1256,12 @@ pub fn query_assign_colors_randomize(node_id: NodeId, context: &NodePropertiesCo
|
||||
|
||||
/// 2-stop black-to-white gradient track for spectrum sliders that map a value to a grayscale axis.
|
||||
fn bw_track() -> Gradient {
|
||||
Gradient {
|
||||
position: vec![0., 1.],
|
||||
midpoint: vec![0.5, 0.5],
|
||||
color: vec![Color::BLACK, Color::WHITE],
|
||||
}
|
||||
Gradient::from(vec![Color::BLACK, Color::WHITE])
|
||||
}
|
||||
|
||||
/// 3-stop black-to-color-to-white gradient track for spectrum sliders that map a value to a hue's full luminance range.
|
||||
fn color_track(color: Color) -> Gradient {
|
||||
Gradient {
|
||||
position: vec![0., 0.5, 1.],
|
||||
midpoint: vec![0.5; 3],
|
||||
color: vec![Color::BLACK, color, Color::WHITE],
|
||||
}
|
||||
Gradient::from(vec![Color::BLACK, color, Color::WHITE])
|
||||
}
|
||||
|
||||
pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
@@ -1300,11 +1292,8 @@ pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut Node
|
||||
|
||||
let contrast_min = if use_classic_value { -100. } else { -50. };
|
||||
let zero_position = -contrast_min / (100. - contrast_min);
|
||||
let contrast_track = Gradient {
|
||||
position: vec![0., zero_position, 1.],
|
||||
midpoint: vec![0.5; 3],
|
||||
color: vec![Color::from_rgbf32_unchecked(0.5, 0.5, 0.5), Color::BLACK, Color::from_rgbf32_unchecked(0.5, 0.5, 0.5)],
|
||||
};
|
||||
let mut contrast_track = Gradient::from(vec![Color::MIDDLE_GRAY, Color::BLACK, Color::MIDDLE_GRAY]);
|
||||
contrast_track.set_positions(&[0., zero_position, 1.]);
|
||||
let contrast = spectrum_slider_row(
|
||||
node_id,
|
||||
context,
|
||||
@@ -1333,7 +1322,7 @@ pub(crate) fn levels_properties(node_id: NodeId, context: &mut NodePropertiesCon
|
||||
// (parameter, marker handle color, default percentage for double-click reset)
|
||||
let input_range_params = [
|
||||
(ShadowsInput.into(), Color::BLACK, 0.),
|
||||
(MidtonesInput.into(), Color::from_rgbf32_unchecked(0.5, 0.5, 0.5), 50.),
|
||||
(MidtonesInput.into(), Color::MIDDLE_GRAY, 50.),
|
||||
(HighlightsInput.into(), Color::WHITE, 100.),
|
||||
];
|
||||
let output_range_params = [(OutputMinimumsInput.into(), Color::BLACK, 0.), (OutputMaximumsInput.into(), Color::WHITE, 100.)];
|
||||
@@ -1397,7 +1386,7 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
|
||||
|
||||
// Build the shared spectrum widget (placed on the first non-exposed row)
|
||||
let spectrum_widget = (!spectrum_markers.is_empty()).then(|| {
|
||||
SpectrumInput::new(GradientUI::from(&bw_track()))
|
||||
SpectrumInput::new(GradientStops::from(&bw_track()))
|
||||
.markers(spectrum_markers)
|
||||
.show_midpoints(false)
|
||||
.allow_insert(false)
|
||||
@@ -1495,17 +1484,9 @@ pub(crate) fn hue_saturation_properties(node_id: NodeId, context: &mut NodePrope
|
||||
let saturated_current_hue = Color::from_hsva(marker_hue, 1., 1., 1.);
|
||||
|
||||
// Hue: cyclic rainbow
|
||||
let hue_track = Gradient {
|
||||
position: vec![0., 1. / 6., 2. / 6., 3. / 6., 4. / 6., 5. / 6., 1.],
|
||||
midpoint: vec![0.5; 7],
|
||||
color: vec![Color::RED, Color::YELLOW, Color::GREEN, Color::CYAN, Color::BLUE, Color::MAGENTA, Color::RED],
|
||||
};
|
||||
let hue_track = Gradient::from(vec![Color::RED, Color::YELLOW, Color::GREEN, Color::CYAN, Color::BLUE, Color::MAGENTA, Color::RED]);
|
||||
// Saturation: gray to the fully saturated current hue
|
||||
let saturation_track = Gradient {
|
||||
position: vec![0., 1.],
|
||||
midpoint: vec![0.5, 0.5],
|
||||
color: vec![Color::from_rgbf32_unchecked(0.5, 0.5, 0.5), saturated_current_hue],
|
||||
};
|
||||
let saturation_track = Gradient::from(vec![Color::MIDDLE_GRAY, saturated_current_hue]);
|
||||
// Lightness: black to white
|
||||
let lightness_track = bw_track();
|
||||
|
||||
@@ -1577,7 +1558,7 @@ fn spectrum_slider_row(
|
||||
|
||||
let position_to_value = move |position: f64| value_min + position * value_range;
|
||||
row.push(
|
||||
SpectrumInput::new(GradientUI::from(&track))
|
||||
SpectrumInput::new(GradientStops::from(&track))
|
||||
.markers(vec![SpectrumMarker::new(position, 0.5, handle_color)])
|
||||
.show_midpoints(false)
|
||||
.allow_insert(false)
|
||||
@@ -1641,11 +1622,7 @@ pub(crate) fn threshold_properties(node_id: NodeId, context: &mut NodeProperties
|
||||
pub(crate) fn vibrance_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::raster::vibrance::*;
|
||||
|
||||
let track = Gradient {
|
||||
position: vec![0., 1.],
|
||||
midpoint: vec![0.5, 0.5],
|
||||
color: vec![Color::from_rgbf32_unchecked(0.5, 0.5, 0.5), Color::RED],
|
||||
};
|
||||
let track = Gradient::from(vec![Color::MIDDLE_GRAY, Color::RED]);
|
||||
vec![spectrum_slider_row(
|
||||
node_id,
|
||||
context,
|
||||
@@ -2474,11 +2451,11 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
};
|
||||
let backup_stops = match document_node.input_value(BackupGradientInput) {
|
||||
Some(TaggedValue::Gradient(stops)) => stops.clone(),
|
||||
_ => Gradient::default(),
|
||||
_ => Gradient::black_to_white(),
|
||||
};
|
||||
(backup_color, backup_stops)
|
||||
}
|
||||
Err(_) => (None, Gradient::default()),
|
||||
Err(_) => (None, Gradient::black_to_white()),
|
||||
};
|
||||
|
||||
match &fill {
|
||||
@@ -2504,7 +2481,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
FillChoiceUI::None
|
||||
}
|
||||
}
|
||||
ResolvedFill::Gradient { gradient: stops, .. } => FillChoiceUI::Gradient(GradientUI::from(stops)),
|
||||
ResolvedFill::Gradient { gradient: stops, .. } => FillChoiceUI::Gradient(GradientStops::from(stops)),
|
||||
ResolvedFill::Other => FillChoiceUI::None,
|
||||
};
|
||||
|
||||
|
||||
@@ -800,4 +800,13 @@ async fn legacy_four_input_fill_migrates_to_the_split_transform_shape() {
|
||||
matches!(transform, Some(TaggedValue::DAffine2(_))),
|
||||
"the transform input should hold a matrix, but became {transform:?}"
|
||||
);
|
||||
|
||||
// The Sample Gradient parameter held the tuple-form stops, which parse as the stops value with even positions elided
|
||||
let sample_gradient_node = &network.nodes[&graph_craft::document::NodeId(2)];
|
||||
let stops = sample_gradient_node.input_value(graphene_std::math_nodes::sample_gradient::GradientInput);
|
||||
let Some(TaggedValue::Gradient(stops)) = stops else {
|
||||
panic!("the legacy stops parameter should become a gradient stops value, but became {stops:?}");
|
||||
};
|
||||
assert_eq!(stops.len(), 2);
|
||||
assert!(!stops.has_position_attribute(), "even legacy tuple positions should elide rather than materialize");
|
||||
}
|
||||
|
||||
@@ -1814,7 +1814,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 9), old_inputs[4].clone(), network_path);
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// A legacy "no color" on a plain color connector (`TaggedValue::no_paint()` restored by the deserializer) becomes a color,
|
||||
// since only paint connectors keep the no-paint choice
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user