Add the "Perceptual" and "Classic" families of gradient spaces with OkLab as the new default (#4415)

* Add an OkLab gradient interpolation space and make it the new default

* Fix the Properties panel fill row resetting the gradient interpolation space to the default

* Add OkLch, Lab, LCh, and HSL gradient interpolation spaces

* Add a hue direction attribute and picker choice for polar gradient interpolation spaces

* Add an HSV gradient interpolation space

* Rename the sRGB Linear and sRGB Gamma gradient spaces to RGB Linear and RGB Gamma

* Divide the gradient interpolation dropdown between absolute and relative color spaces

* Rename the OkLch gradient interpolation space to OkLCh for channel-notation capitalization

* Shorten the color picker popover's hue direction row label to Arc

* Call the Gradient Interpolation node's parameter Space and extend the picker tooltip title to match

* Rename the gradient interpolation attribute and type family to gradient space

* Rename the gradient tool's gradient space transform to gradient to viewport transform

* Add serde aliases and a node replacement covering the gradient space renames

* Give the gradient space choices artist-facing labels and reorder the dropdown sections

* Add a Gradient Hue Direction node and its attribute read node

* Say interpolate instead of blend for gradient stop color traversal in docs and tooltips

* Label the polar perceptual gradient spaces as Perceptual Hue and group the dropdown sections by geometry

* Add a migration alias covering the gradient space attribute reader rename

* Carry the gradient hue direction attribute through boolean operations

* Register GradientHueDirection wire types in the node registry
This commit is contained in:
Keavon Chambers
2026-08-06 02:40:25 -07:00
committed by Dennis Kobert
parent 11dfc27645
commit a35143586a
29 changed files with 886 additions and 410 deletions

View File

@@ -1,6 +1,6 @@
use crate::messages::layout::utility_types::widgets::input_widgets::{SpectrumInputUpdate, VisualColorPickersInputUpdate};
use crate::messages::prelude::*;
use graphene_std::vector::style::{FillChoice, GradientInterpolation, GradientSpread};
use graphene_std::vector::style::{FillChoice, GradientHueDirection, GradientSpace, GradientSpread};
/// Identifies which RGB channel a numeric input change targets.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -49,8 +49,10 @@ pub enum ColorPickerMessage {
GradientUpdate { update: SpectrumInputUpdate },
/// Gradient spread choice from the gradient "Ends" selection.
SetGradientSpread { gradient_spread: GradientSpread },
/// Gradient interpolation choice: the color space the stops blend in, from the "Space" dropdown.
SetGradientInterpolation { gradient_interpolation: GradientInterpolation },
/// Gradient space choice: the color space the stops interpolate in, from the "Space" dropdown.
SetGradientSpace { gradient_space: GradientSpace },
/// Gradient hue direction choice: which way around the hue wheel the stops interpolate in a polar space, from the "Arc" dropdown.
SetGradientHueDirection { gradient_hue_direction: GradientHueDirection },
/// Tell the frontend to start an undo transaction (forwarded as a `FrontendMessage` it bridges out to the picker's parent).
StartTransaction,

View File

@@ -5,7 +5,7 @@ use crate::messages::prelude::*;
use graphene_std::Color;
use graphene_std::color::SRGBA8;
use graphene_std::core_types::misc::parse_css_color;
use graphene_std::vector::style::{FillChoice, Gradient, GradientInterpolation, GradientRamp, GradientSpread, GradientStops};
use graphene_std::vector::style::{FillChoice, Gradient, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, GradientStops};
/// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops).
const MIN_MIDPOINT: f64 = 0.01;
@@ -30,7 +30,8 @@ pub struct ColorPickerMessageHandler {
// When set, the picker is editing a gradient: the visual pickers and inputs target the active stop's color.
gradient: Option<Gradient>,
gradient_spread: GradientSpread,
gradient_interpolation: GradientInterpolation,
gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
active_marker_index: Option<u32>,
active_marker_is_midpoint: bool,
@@ -53,7 +54,8 @@ impl Default for ColorPickerMessageHandler {
old_is_none: true,
gradient: None,
gradient_spread: GradientSpread::default(),
gradient_interpolation: GradientInterpolation::default(),
gradient_space: GradientSpace::default(),
gradient_hue_direction: GradientHueDirection::default(),
active_marker_index: None,
active_marker_is_midpoint: false,
allow_none: true,
@@ -75,14 +77,16 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
self.set_new_hsva(0., 0., 0., 1., true);
self.gradient = None;
self.gradient_spread = GradientSpread::default();
self.gradient_interpolation = GradientInterpolation::default();
self.gradient_space = GradientSpace::default();
self.gradient_hue_direction = GradientHueDirection::default();
self.active_marker_index = None;
self.active_marker_is_midpoint = false;
}
FillChoice::Solid(color) => {
self.gradient = None;
self.gradient_spread = GradientSpread::default();
self.gradient_interpolation = GradientInterpolation::default();
self.gradient_space = GradientSpace::default();
self.gradient_hue_direction = GradientHueDirection::default();
self.active_marker_index = None;
self.active_marker_is_midpoint = false;
self.adopt_color(color);
@@ -91,7 +95,8 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
self.active_marker_index = Some(0);
self.active_marker_is_midpoint = false;
self.gradient_spread = ramp.gradient_spread;
self.gradient_interpolation = ramp.gradient_interpolation;
self.gradient_space = ramp.gradient_space;
self.gradient_hue_direction = ramp.gradient_hue_direction;
let gradient = Gradient::from(ramp);
let first_color = gradient.color(0).unwrap_or(Color::BLACK);
self.gradient = Some(gradient);
@@ -204,20 +209,36 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp {
gradient_spread,
gradient_interpolation: self.gradient_interpolation,
gradient_space: self.gradient_space,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(gradient)
}),
});
self.send_layouts(responses);
}
ColorPickerMessage::SetGradientInterpolation { gradient_interpolation } => {
ColorPickerMessage::SetGradientSpace { gradient_space } => {
let Some(gradient) = &self.gradient else { return };
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
self.gradient_interpolation = gradient_interpolation;
self.gradient_space = gradient_space;
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp {
gradient_spread: self.gradient_spread,
gradient_interpolation,
gradient_space,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(gradient)
}),
});
self.send_layouts(responses);
}
ColorPickerMessage::SetGradientHueDirection { gradient_hue_direction } => {
let Some(gradient) = &self.gradient else { return };
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
self.gradient_hue_direction = gradient_hue_direction;
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp {
gradient_spread: self.gradient_spread,
gradient_space: self.gradient_space,
gradient_hue_direction,
..GradientRamp::from(gradient)
}),
});
@@ -309,7 +330,8 @@ impl ColorPickerMessageHandler {
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp {
gradient_spread: self.gradient_spread,
gradient_interpolation: self.gradient_interpolation,
gradient_space: self.gradient_space,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(&*gradient)
}),
});
@@ -368,7 +390,7 @@ impl ColorPickerMessageHandler {
gradient.set_midpoint(index as usize, position.clamp(MIN_MIDPOINT, MAX_MIDPOINT));
}
SpectrumInputUpdate::InsertMarker { position } => {
let new_index = gradient.insert_stop(position, self.gradient_interpolation);
let new_index = gradient.insert_stop(position, self.gradient_space, self.gradient_hue_direction);
self.active_marker_index = Some(new_index as u32);
self.active_marker_is_midpoint = false;
if let Some(color) = gradient.color(new_index) {
@@ -440,7 +462,8 @@ impl ColorPickerMessageHandler {
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp {
gradient_spread: self.gradient_spread,
gradient_interpolation: self.gradient_interpolation,
gradient_space: self.gradient_space,
gradient_hue_direction: self.gradient_hue_direction,
..GradientRamp::from(&gradient)
}),
});
@@ -471,7 +494,8 @@ impl ColorPickerMessageHandler {
let markers = gradient.iter().map(|stop| SpectrumMarker::new(stop.position, stop.midpoint, stop.color)).collect();
let mut row_widgets = vec![
SpectrumInput::new(GradientStops::from(gradient))
.track_interpolation(self.gradient_interpolation)
.track_space(self.gradient_space)
.track_hue_direction(self.gradient_hue_direction)
.markers(markers)
.active_marker_index(self.active_marker_index)
.active_marker_is_midpoint(self.active_marker_is_midpoint)
@@ -654,15 +678,29 @@ impl ColorPickerMessageHandler {
]));
}
// Gradient interpolation color space (only present when the picker is in gradient mode)
// Gradient color space (only present when the picker is in gradient mode)
if self.gradient.is_some() {
let entries = MenuListEntry::sections_from_choice_type(|gradient_interpolation| ColorPickerMessage::SetGradientInterpolation { gradient_interpolation }.into());
let entries = MenuListEntry::sections_from_choice_type(|gradient_space| ColorPickerMessage::SetGradientSpace { gradient_space }.into());
groups.push(LayoutGroup::row(vec![
TextLabel::new("Space").tooltip_label("Gradient Interpolation").tooltip_description(SPACE_DESCRIPTION).widget_instance(),
TextLabel::new("Space").tooltip_label("Gradient Space").tooltip_description(SPACE_DESCRIPTION).widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
DropdownInput::new(entries).selected_index(Some(self.gradient_space as u32)).disabled(self.disabled).widget_instance(),
]));
}
// Gradient hue direction (only present when the chosen space is polar)
if self.gradient.is_some() && self.gradient_space.is_polar() {
let entries = MenuListEntry::sections_from_choice_type(|gradient_hue_direction| ColorPickerMessage::SetGradientHueDirection { gradient_hue_direction }.into());
groups.push(LayoutGroup::row(vec![
TextLabel::new("Arc")
.tooltip_label("Gradient Hue Direction")
.tooltip_description(HUE_DIRECTION_DESCRIPTION)
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
DropdownInput::new(entries)
.selected_index(Some(self.gradient_interpolation as u32))
.selected_index(Some(self.gradient_hue_direction as u32))
.disabled(self.disabled)
.widget_instance(),
]));
@@ -722,7 +760,8 @@ const SATURATION_DESCRIPTION: &str = "The vividness from grayscale to full color
const VALUE_DESCRIPTION: &str = "The brightness from black to full color.";
const ALPHA_DESCRIPTION: &str = "The level of translucency, from transparent (0%) to opaque (100%).";
const ENDS_DESCRIPTION: &str = "The method for how the gradient continues beyond its ends.";
const SPACE_DESCRIPTION: &str = "The color space where stops blend into their neighbors.";
const SPACE_DESCRIPTION: &str = "The color space where stops interpolate toward their neighbors.";
const HUE_DIRECTION_DESCRIPTION: &str = "Which way around the hue wheel the stops interpolate.";
/// The popover's background color as sRGB gamma-encoded channels (the `--color-2-mildblack` design token, `#222`).
/// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background.

View File

@@ -531,7 +531,7 @@ fn populate_computed_display_fields(layout: &mut Layout) {
color_input.chosen_gradient = color_input.value.to_css_background_image();
}
Widget::SpectrumInput(spectrum_input) => {
spectrum_input.track_css = spectrum_input.track.to_css_linear_gradient(spectrum_input.track_interpolation);
spectrum_input.track_css = spectrum_input.track.to_css_linear_gradient(spectrum_input.track_space, spectrum_input.track_hue_direction);
spectrum_input.track_start_css = spectrum_input.track.color.first().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string());
spectrum_input.track_end_css = spectrum_input.track.color.last().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string());
}

View File

@@ -7,7 +7,7 @@ use derivative::*;
use graphene_std::Color;
use graphene_std::color::SRGBA8;
use graphene_std::transform::ReferencePoint;
use graphene_std::vector::style::{FillChoice, GradientInterpolation, GradientStops};
use graphene_std::vector::style::{FillChoice, GradientHueDirection, GradientSpace, GradientStops};
use graphite_proc_macros::WidgetBuilder;
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
@@ -586,9 +586,12 @@ pub struct SpectrumInput {
/// The colored gradient drawn behind the markers (display-only, caller-owned).
#[widget_builder(constructor)]
pub track: GradientStops<SRGBA8>,
/// The interpolation color space the track's stops blend in, used to compute `track_css`. Not sent to the frontend.
/// The color space the track's stops interpolate in, used to compute `track_css`. Not sent to the frontend.
#[serde(skip)]
pub track_interpolation: GradientInterpolation,
pub track_space: GradientSpace,
/// The hue direction the track's stops interpolate with in a polar space, used to compute `track_css`. Not sent to the frontend.
#[serde(skip)]
pub track_hue_direction: GradientHueDirection,
/// CSS `linear-gradient(...)` string for the track strip's `background-image`. Auto-populated from `track` at layout-send time.
#[serde(rename = "trackCSS")]
#[widget_builder(skip)]

View File

@@ -23,7 +23,7 @@ use graphene_std::transform::{ReferencePoint, ScaleType};
use graphene_std::vector::misc::{
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
};
use graphene_std::vector::style::{DashPattern, FillChoice, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::style::{DashPattern, FillChoice, GradientForm, GradientRamp, GradientSpace, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector};
use graphene_std::{Artboard, Color, Graphic};
use std::any::Any;
@@ -218,7 +218,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
List<BlendMode>,
List<GradientForm>,
List<GradientSpread>,
List<GradientInterpolation>,
List<GradientSpace>,
List<DashPattern>,
List<BoxCorners>,
List<StrokeJoin>,
@@ -271,7 +271,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
DAffine2,
BlendMode,
GradientForm,
GradientInterpolation,
GradientSpace,
GradientSpread,
DashPattern,
BoxCorners,
@@ -1011,7 +1011,7 @@ impl_table_item_layout_for_choice_enum!(
BlendMode,
GradientForm,
GradientSpread,
GradientInterpolation,
GradientSpace,
StrokeJoin,
StrokeAlign,
StrokeCap,
@@ -1224,7 +1224,7 @@ macro_rules! known_item_types {
BlendMode,
GradientForm,
GradientSpread,
GradientInterpolation,
GradientSpace,
StrokeJoin,
StrokeAlign,
StrokeCap,

View File

@@ -10,7 +10,7 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image;
use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{GradientForm, GradientInterpolation, GradientSpread, Stroke};
use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientSpace, GradientSpread, Stroke};
use graphene_std::vector::{Gradient, PointId, VectorModificationType};
#[impl_message(Message, DocumentMessage, GraphOperation)]
@@ -30,7 +30,8 @@ pub enum GraphOperationMessage {
gradient: Gradient,
gradient_form: GradientForm,
gradient_spread: GradientSpread,
gradient_interpolation: GradientInterpolation,
gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
transform: DAffine2,
},
BlendingFillSet {
@@ -62,9 +63,13 @@ pub enum GraphOperationMessage {
layer: LayerNodeIdentifier,
gradient_spread: GradientSpread,
},
GradientInterpolationSet {
GradientSpaceSet {
layer: LayerNodeIdentifier,
gradient_interpolation: GradientInterpolation,
gradient_space: GradientSpace,
},
GradientHueDirectionSet {
layer: LayerNodeIdentifier,
gradient_hue_direction: GradientHueDirection,
},
OpacitySet {
layer: LayerNodeIdentifier,

View File

@@ -14,7 +14,7 @@ use graph_craft::document::{NodeId, NodeInput};
use graphene_std::list::List;
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Gradient, GradientForm, GradientInterpolation, GradientSpread, GradientStop, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::style::{Gradient, GradientForm, GradientSpace, GradientSpread, GradientStop, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::{Artboard, Color};
#[derive(ExtractField)]
@@ -50,11 +50,12 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
gradient,
gradient_form,
gradient_spread,
gradient_interpolation,
gradient_space,
gradient_hue_direction,
transform,
} => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_interpolation, transform);
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, gradient_hue_direction, transform);
}
}
GraphOperationMessage::BlendingFillSet { layer, fill } => {
@@ -92,9 +93,14 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
modify_inputs.gradient_spread_set(gradient_spread);
}
}
GraphOperationMessage::GradientInterpolationSet { layer, gradient_interpolation } => {
GraphOperationMessage::GradientSpaceSet { layer, gradient_space } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.gradient_interpolation_set(gradient_interpolation);
modify_inputs.gradient_space_set(gradient_space);
}
}
GraphOperationMessage::GradientHueDirectionSet { layer, gradient_hue_direction } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.gradient_hue_direction_set(gradient_hue_direction);
}
}
GraphOperationMessage::OpacitySet { layer, opacity } => {
@@ -489,7 +495,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
let gradient_info = SvgGradientInfo {
graphite_stops: extract_graphite_gradient_stops(&svg),
interpolations: extract_gradient_interpolations(&svg),
spaces: extract_gradient_spaces(&svg),
};
// Pass identity so each leaf layer receives only its SVG-native transform from `abs_transform`.
@@ -530,14 +536,14 @@ const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
struct SvgGradientInfo {
/// Real stops, keyed by gradient element `id`, for gradients Graphite exported with midpoint curve data.
graphite_stops: HashMap<String, Gradient>,
/// Interpolation spaces, keyed by gradient element `id`, resolved from the `color-interpolation` property.
interpolations: HashMap<String, GradientInterpolation>,
/// Gradient spaces, keyed by gradient element `id`, resolved from the `color-interpolation` property.
spaces: HashMap<String, GradientSpace>,
}
/// Pre-parses the raw SVG XML to resolve each gradient's inherited `color-interpolation` property, which usvg's
/// tree does not carry. Only `linearRGB` selects linear interpolation; `auto` and `sRGB` (browsers treat the
/// tree does not carry. Only `linearRGB` selects the linear space; `auto` and `sRGB` (browsers treat the
/// user-agent-defined `auto` as `sRGB`) mean gamma, as does any unrecognized value.
fn extract_gradient_interpolations(svg: &str) -> HashMap<String, GradientInterpolation> {
fn extract_gradient_spaces(svg: &str) -> HashMap<String, GradientSpace> {
let mut result = HashMap::new();
// Quick check: gradients in an SVG that never mentions `color-interpolation` all take the sRGB default
@@ -568,9 +574,9 @@ fn extract_gradient_interpolations(svg: &str) -> HashMap<String, GradientInterpo
}
if let Some(gradient_id) = node.attribute("id")
&& let Some(gradient_interpolation) = resolve_color_interpolation(node, &stylesheet)
&& let Some(gradient_space) = resolve_color_interpolation(node, &stylesheet)
{
result.insert(gradient_id.to_string(), gradient_interpolation);
result.insert(gradient_id.to_string(), gradient_space);
}
}
@@ -579,15 +585,15 @@ fn extract_gradient_interpolations(svg: &str) -> HashMap<String, GradientInterpo
/// The `color-interpolation` in effect for an element: the nearest self-or-ancestor declaration, taking each
/// element's own winning declaration per [`declared_color_interpolation`]'s cascade order.
fn resolve_color_interpolation(element: usvg::roxmltree::Node, stylesheet: &simplecss::StyleSheet) -> Option<GradientInterpolation> {
fn resolve_color_interpolation(element: usvg::roxmltree::Node, stylesheet: &simplecss::StyleSheet) -> Option<GradientSpace> {
let mut next = Some(element);
while let Some(element) = next {
match declared_color_interpolation(element, stylesheet) {
Some("linearRGB") => return Some(GradientInterpolation::SrgbLinear),
Some("linearRGB") => return Some(GradientSpace::RgbLinear),
// `inherit` defers to the ancestors like an undeclared element
Some("inherit") | None => {}
Some(_) => return Some(GradientInterpolation::SrgbGamma),
Some(_) => return Some(GradientSpace::RgbGamma),
}
next = element.parent_element();
@@ -971,8 +977,8 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
};
let gradient_spread = convert_gradient_spread(linear.spread_method());
// SVG interpolates between stops in gamma sRGB unless `color-interpolation` opts into linearRGB, carried explicitly rather than as the linear default
let gradient_interpolation = gradient_info.interpolations.get(linear.id()).copied().unwrap_or(GradientInterpolation::SrgbGamma);
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_interpolation, transform);
let gradient_space = gradient_info.spaces.get(linear.id()).copied().unwrap_or(GradientSpace::RgbGamma);
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, Default::default(), transform);
}
usvg::Paint::RadialGradient(radial) => {
let gradient_transform = usvg_transform(radial.transform());
@@ -996,9 +1002,9 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
}
};
let gradient_spread = convert_gradient_spread(radial.spread_method());
let gradient_interpolation = gradient_info.interpolations.get(radial.id()).copied().unwrap_or(GradientInterpolation::SrgbGamma);
let gradient_space = gradient_info.spaces.get(radial.id()).copied().unwrap_or(GradientSpace::RgbGamma);
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_interpolation, transform);
modify_inputs.fill_gradient_set(gradient, gradient_form, gradient_spread, gradient_space, Default::default(), transform);
}
usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"),
};
@@ -1019,25 +1025,13 @@ mod tests {
</defs>
</svg>"##;
let interpolations = extract_gradient_interpolations(svg);
let spaces = extract_gradient_spaces(svg);
assert_eq!(spaces.get("inherited"), Some(&GradientSpace::RgbLinear), "an undeclared gradient should inherit from its ancestors");
assert_eq!(spaces.get("attribute"), Some(&GradientSpace::RgbGamma), "an sRGB declaration should beat the inherited linearRGB");
assert_eq!(spaces.get("styled"), Some(&GradientSpace::RgbLinear), "the inline style should beat the presentation attribute");
assert_eq!(
interpolations.get("inherited"),
Some(&GradientInterpolation::SrgbLinear),
"an undeclared gradient should inherit from its ancestors"
);
assert_eq!(
interpolations.get("attribute"),
Some(&GradientInterpolation::SrgbGamma),
"an sRGB declaration should beat the inherited linearRGB"
);
assert_eq!(
interpolations.get("styled"),
Some(&GradientInterpolation::SrgbLinear),
"the inline style should beat the presentation attribute"
);
assert_eq!(
interpolations.get("auto"),
Some(&GradientInterpolation::SrgbGamma),
spaces.get("auto"),
Some(&GradientSpace::RgbGamma),
"auto should mean gamma like browsers treat it, not defer to ancestors"
);
}
@@ -1059,26 +1053,18 @@ mod tests {
</defs>
</svg>"##;
let interpolations = extract_gradient_interpolations(svg);
let spaces = extract_gradient_spaces(svg);
assert_eq!(spaces.get("from-type-rule"), Some(&GradientSpace::RgbLinear), "a type rule in a style block should reach the gradient");
assert_eq!(
interpolations.get("from-type-rule"),
Some(&GradientInterpolation::SrgbLinear),
"a type rule in a style block should reach the gradient"
);
assert_eq!(
interpolations.get("from-class-rule"),
Some(&GradientInterpolation::SrgbGamma),
spaces.get("from-class-rule"),
Some(&GradientSpace::RgbGamma),
"the class rule should outrank the type rule by specificity"
);
assert_eq!(interpolations.get("exact"), Some(&GradientInterpolation::SrgbLinear), "the ID rule should outrank the class rule");
assert_eq!(spaces.get("exact"), Some(&GradientSpace::RgbLinear), "the ID rule should outrank the class rule");
assert_eq!(spaces.get("inline-beats-rules"), Some(&GradientSpace::RgbLinear), "the inline style should beat every style block rule");
assert_eq!(
interpolations.get("inline-beats-rules"),
Some(&GradientInterpolation::SrgbLinear),
"the inline style should beat every style block rule"
);
assert_eq!(
interpolations.get("rule-beats-attribute"),
Some(&GradientInterpolation::SrgbGamma),
spaces.get("rule-beats-attribute"),
Some(&GradientSpace::RgbGamma),
"a style block rule should beat the presentation attribute"
);
}
@@ -1092,20 +1078,20 @@ mod tests {
<linearGradient id="important-rule-beats-inline" class="forced" style="color-interpolation: sRGB"/>
</svg>"##;
let interpolations = extract_gradient_interpolations(svg);
let spaces = extract_gradient_spaces(svg);
assert_eq!(
interpolations.get("last-declaration-wins"),
Some(&GradientInterpolation::SrgbLinear),
spaces.get("last-declaration-wins"),
Some(&GradientSpace::RgbLinear),
"the last of repeated inline declarations should win"
);
assert_eq!(
interpolations.get("important-beats-later"),
Some(&GradientInterpolation::SrgbLinear),
spaces.get("important-beats-later"),
Some(&GradientSpace::RgbLinear),
"an `!important` declaration should beat a later normal one"
);
assert_eq!(
interpolations.get("important-rule-beats-inline"),
Some(&GradientInterpolation::SrgbLinear),
spaces.get("important-rule-beats-inline"),
Some(&GradientSpace::RgbLinear),
"an `!important` style block rule should beat the inline style"
);
}
@@ -1115,7 +1101,7 @@ mod tests {
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg"><linearGradient id="plain"/></svg>"##;
assert!(
extract_gradient_interpolations(svg).is_empty(),
extract_gradient_spaces(svg).is_empty(),
"gradients without any declaration should fall back to the caller's gamma default"
);
}

View File

@@ -19,7 +19,7 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image;
use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{GradientForm, GradientInterpolation, GradientSpread, Stroke};
use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientSpace, GradientSpread, Stroke};
use graphene_std::vector::{Gradient, GradientRamp, PointId, Vector, VectorModification, VectorModificationType};
use graphene_std::{Artboard, Color, Graphic};
@@ -433,7 +433,15 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh(input_connector, NodeInput::value(fill_value, false), false);
}
pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_form: GradientForm, gradient_spread: GradientSpread, gradient_interpolation: GradientInterpolation, transform: DAffine2) {
pub fn fill_gradient_set(
&mut self,
gradient: Gradient,
gradient_form: GradientForm,
gradient_spread: GradientSpread,
gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
transform: DAffine2,
) {
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
return;
};
@@ -442,7 +450,8 @@ impl<'a> ModifyInputsContext<'a> {
let ramp = GradientRamp::from(gradient);
let ramp = GradientRamp {
gradient_spread,
gradient_interpolation,
gradient_space,
gradient_hue_direction,
..ramp
};
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp.clone()), false), true);
@@ -769,16 +778,29 @@ impl<'a> ModifyInputsContext<'a> {
Some(ramp.clone())
}
/// Set the interpolation on the chain's gradient value, which is where the ramp carries it. Never touches a
/// 'Gradient Interpolation' node: that one is a user-authored procedural override, not something the tools manage.
pub fn gradient_interpolation_set(&mut self, gradient_interpolation: GradientInterpolation) {
/// Set the space on the chain's gradient value, which is where the ramp carries it. Never touches a
/// 'Gradient Space' node: that one is a user-authored procedural override, not something the tools manage.
pub fn gradient_space_set(&mut self, gradient_space: GradientSpace) {
let Some(output_layer) = self.get_output_layer() else { return };
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
return;
};
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
let ramp = GradientRamp { gradient_interpolation, ..ramp };
let ramp = GradientRamp { gradient_space, ..ramp };
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
}
/// Set the hue direction on the chain's gradient value, which is where the ramp carries it.
pub fn gradient_hue_direction_set(&mut self, gradient_hue_direction: GradientHueDirection) {
let Some(output_layer) = self.get_output_layer() else { return };
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
return;
};
let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return };
let ramp = GradientRamp { gradient_hue_direction, ..ramp };
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false);
}

View File

@@ -34,7 +34,8 @@ use graphene_std::vector::misc::{
ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
};
use graphene_std::vector::style::{
FillChoice, Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, GradientStops, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation,
FillChoice, Gradient, GradientForm, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, GradientStops, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin,
build_transform_with_y_preservation,
};
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
use graphene_std::{NodeParameter, ParameterRef};
@@ -294,7 +295,8 @@ pub(crate) fn property_from_type(
// =========================
Some(x) if id_is::<GradientForm>(x) => enum_choice::<GradientForm>().for_socket(default_info).property_row(),
Some(x) if id_is::<GradientSpread>(x) => enum_choice::<GradientSpread>().for_socket(default_info).property_row(),
Some(x) if id_is::<GradientInterpolation>(x) => enum_choice::<GradientInterpolation>().for_socket(default_info).property_row(),
Some(x) if id_is::<GradientSpace>(x) => enum_choice::<GradientSpace>().for_socket(default_info).property_row(),
Some(x) if id_is::<GradientHueDirection>(x) => enum_choice::<GradientHueDirection>().for_socket(default_info).property_row(),
Some(x) if id_is::<RealTimeMode>(x) => enum_choice::<RealTimeMode>().for_socket(default_info).property_row(),
Some(x) if id_is::<RedGreenBlue>(x) => enum_choice::<RedGreenBlue>().for_socket(default_info).property_row(),
Some(x) if id_is::<RedGreenBlueAlpha>(x) => enum_choice::<RedGreenBlueAlpha>().for_socket(default_info).property_row(),
@@ -1390,7 +1392,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(GradientStops::from(&bw_track()))
.track_interpolation(GradientInterpolation::SrgbGamma)
.track_space(GradientSpace::RgbGamma)
.markers(spectrum_markers)
.show_midpoints(false)
.allow_insert(false)
@@ -1563,7 +1565,7 @@ fn spectrum_slider_row(
let position_to_value = move |position: f64| value_min + position * value_range;
row.push(
SpectrumInput::new(GradientStops::from(&track))
.track_interpolation(GradientInterpolation::SrgbGamma)
.track_space(GradientSpace::RgbGamma)
.markers(vec![SpectrumMarker::new(position, 0.5, handle_color)])
.show_midpoints(false)
.allow_insert(false)
@@ -2392,7 +2394,7 @@ fn root_layer_for_chain_node(node_id: NodeId, context: &mut NodePropertiesContex
/// 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_transform: DAffine2, context: &mut NodePropertiesContext) -> Option<bool> {
let layer = root_layer_for_chain_node(node_id, context)?;
let transform = graph_modification_utils::gradient_space_transform(layer, context.network_interface);
let transform = graph_modification_utils::gradient_to_viewport_transform(layer, context.network_interface);
Some(graph_modification_utils::gradient_orientation_rightward(transform * gradient_transform))
}
@@ -2407,6 +2409,8 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
gradient: Gradient,
gradient_form: GradientForm,
gradient_spread: GradientSpread,
gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
transform: DAffine2,
/// Whether the transform input holds a plain value (so the "Reverse Direction" button may write to it) rather than a wire.
transform_is_value: bool,
@@ -2437,6 +2441,8 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
gradient: gradient.stops,
gradient_form: gradient.gradient_form,
gradient_spread: gradient.gradient_spread,
gradient_space: gradient.gradient_space,
gradient_hue_direction: gradient.gradient_hue_direction,
transform: gradient.transform,
transform_is_value: gradient.transform_is_value,
},
@@ -2464,9 +2470,17 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
};
match &fill {
ResolvedFill::Gradient { gradient: stops, gradient_spread, .. } => {
ResolvedFill::Gradient {
gradient: stops,
gradient_spread,
gradient_space,
gradient_hue_direction,
..
} => {
let stops = stops.clone();
let gradient_spread = *gradient_spread;
let gradient_space = *gradient_space;
let gradient_hue_direction = *gradient_hue_direction;
let reverse_button = IconButton::new("Reverse", 24)
.tooltip_label("Reverse Stops")
@@ -2475,6 +2489,8 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
move |_| {
TaggedValue::GradientRamp(GradientRamp {
gradient_spread,
gradient_space,
gradient_hue_direction,
..GradientRamp::from(stops.reversed())
})
},
@@ -2496,8 +2512,16 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
FillChoice::<SRGBA8>::None
}
}
ResolvedFill::Gradient { gradient: stops, gradient_spread, .. } => FillChoice::<SRGBA8>::Gradient(GradientRamp {
ResolvedFill::Gradient {
gradient: stops,
gradient_spread,
gradient_space,
gradient_hue_direction,
..
} => FillChoice::<SRGBA8>::Gradient(GradientRamp {
gradient_spread: *gradient_spread,
gradient_space: *gradient_space,
gradient_hue_direction: *gradient_hue_direction,
..GradientRamp::from(stops)
}),
ResolvedFill::Other => FillChoice::<SRGBA8>::None,

View File

@@ -808,8 +808,8 @@ async fn legacy_four_input_fill_migrates_to_the_split_transform_shape() {
panic!("the legacy stops parameter should become a gradient ramp value, but became {stops:?}");
};
assert_eq!(
ramp.gradient_interpolation,
graphene_std::vector::style::GradientInterpolation::SrgbGamma,
ramp.gradient_space,
graphene_std::vector::style::GradientSpace::RgbGamma,
"a legacy document's ramps should deserialize with the explicit gamma interpolation"
);
let stops = graphene_std::vector::Gradient::from(ramp);
@@ -850,8 +850,8 @@ async fn eight_input_fill_migrates_the_spread_input_into_the_ramp() {
};
assert_eq!(ramp.gradient_spread, GradientSpread::Repeat, "the spread input's value should fold into the fill ramp");
assert_eq!(
ramp.gradient_interpolation,
graphene_std::vector::style::GradientInterpolation::SrgbGamma,
ramp.gradient_space,
graphene_std::vector::style::GradientSpace::RgbGamma,
"a legacy document's ramps should deserialize with the explicit gamma interpolation"
);
@@ -861,8 +861,8 @@ async fn eight_input_fill_migrates_the_spread_input_into_the_ramp() {
};
assert_eq!(backup_ramp.gradient_spread, GradientSpread::Repeat, "the spread input's value should fold into the backup ramp");
assert_eq!(
backup_ramp.gradient_interpolation,
graphene_std::vector::style::GradientInterpolation::SrgbGamma,
backup_ramp.gradient_space,
graphene_std::vector::style::GradientSpace::RgbGamma,
"the backup ramp should carry the explicit gamma interpolation too"
);

View File

@@ -172,6 +172,13 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::graphic::read_gradient_spread_attribute::IDENTIFIER,
aliases: &["graphic_nodes::graphic::ReadAttributeSpreadMethodNode", "graphic_nodes::graphic::ReadSpreadMethodAttributeNode"],
},
NodeReplacement {
node: graphene_std::graphic::read_gradient_space_attribute::IDENTIFIER,
aliases: &[
"graphic_nodes::graphic::ReadAttributeGradientInterpolationNode",
"graphic_nodes::graphic::ReadGradientInterpolationAttributeNode",
],
},
NodeReplacement {
node: graphene_std::list::remove_at_index::IDENTIFIER,
aliases: &["graphic_nodes::graphic::OmitElementNode", "graphic_nodes::graphic::RemoveAtIndexNode"],
@@ -273,6 +280,10 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::math_nodes::gradient_form::IDENTIFIER,
aliases: &["math_nodes::GradientTypeNode"],
},
NodeReplacement {
node: graphene_std::math_nodes::gradient_space::IDENTIFIER,
aliases: &["math_nodes::GradientInterpolationNode"],
},
NodeReplacement {
node: graphene_std::math_nodes::gradient_spread::IDENTIFIER,
aliases: &["math_nodes::SpreadMethodNode"],

View File

@@ -14,7 +14,7 @@ use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box};
use graphene_std::vector::{Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, PointId, SegmentId, VectorModificationType};
use graphene_std::vector::{Gradient, GradientForm, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, PointId, SegmentId, VectorModificationType};
use graphene_std::{NodeParameter, ParameterRef};
use std::collections::VecDeque;
@@ -402,9 +402,14 @@ pub fn get_chain_source_gradient_spread(layer: LayerNodeIdentifier, network_inte
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_spread)
}
/// The interpolation baked into the 'Gradient Value' node feeding a layer's chain.
pub fn get_chain_source_gradient_interpolation(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientInterpolation> {
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_interpolation)
/// The space baked into the 'Gradient Value' node feeding a layer's chain.
pub fn get_chain_source_gradient_space(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientSpace> {
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_space)
}
/// The hue direction baked into the 'Gradient Value' node feeding a layer's chain.
pub fn get_chain_source_gradient_hue_direction(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientHueDirection> {
Some(get_chain_source_gradient_ramp(layer, network_interface)?.gradient_hue_direction)
}
/// Get the gradient stops of a layer, if any.
@@ -466,7 +471,7 @@ pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNe
/// Compute the transform from a gradient's local space to viewport space for the given layer. For a `List<Gradient>`
/// layer this is the layer's incoming footprint transform; for a Fill-owned gradient value it composes the layer's viewport
/// transform with the [0,1]² → bounding-box mapping.
pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> glam::DAffine2 {
pub fn gradient_to_viewport_transform(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> glam::DAffine2 {
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
let metadata = network_interface.document_metadata();
@@ -762,7 +767,8 @@ pub struct FillNodeGradient {
pub stops: Gradient,
pub gradient_form: GradientForm,
pub gradient_spread: GradientSpread,
pub gradient_interpolation: GradientInterpolation,
pub gradient_space: GradientSpace,
pub gradient_hue_direction: GradientHueDirection,
pub transform: DAffine2,
/// Whether the transform input holds a plain value (so it may be written to) rather than a wire.
pub transform_is_value: bool,
@@ -776,7 +782,8 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn
return None;
};
let gradient_spread = ramp.gradient_spread;
let gradient_interpolation = ramp.gradient_interpolation;
let gradient_space = ramp.gradient_space;
let gradient_hue_direction = ramp.gradient_hue_direction;
let stops = Gradient::from(ramp);
let gradient_form = match fill_node.input(fill::GradientFormInput).and_then(|input| input.as_value()) {
Some(&TaggedValue::GradientForm(value)) => value,
@@ -794,7 +801,8 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn
stops,
gradient_form,
gradient_spread,
gradient_interpolation,
gradient_space,
gradient_hue_direction,
transform,
transform_is_value: transform_input.is_some(),
})
@@ -944,7 +952,8 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document
gradient: Gradient::from(ramp),
gradient_form,
gradient_spread: ramp.gradient_spread,
gradient_interpolation: ramp.gradient_interpolation,
gradient_space: ramp.gradient_space,
gradient_hue_direction: ramp.gradient_hue_direction,
transform,
});
}

View File

@@ -9,15 +9,15 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface};
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::graph_modification_utils::{
self, NodeGraphLayer, get_chain_source_gradient_interpolation, get_chain_source_gradient_spread, get_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id,
gradient_chain_target_input, replaceable_paint_chain, reverse_direction_tooltip_description,
self, NodeGraphLayer, get_chain_source_gradient_hue_direction, get_chain_source_gradient_space, get_chain_source_gradient_spread, get_fill_node_id_with_direct_fill_input, get_gradient_stops,
get_upstream_gradient_value_node_id, gradient_chain_target_input, replaceable_paint_chain, reverse_direction_tooltip_description,
};
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use glam::DMat2;
use graph_craft::document::value::TaggedValue;
use graphene_std::color::SRGBA8;
use graphene_std::raster::color::Color;
use graphene_std::vector::style::{FillChoice, Gradient, GradientForm, GradientInterpolation, GradientRamp, GradientSpread, GradientStop, build_transform_with_y_preservation};
use graphene_std::vector::style::{FillChoice, Gradient, GradientForm, GradientHueDirection, GradientRamp, GradientSpace, GradientSpread, GradientStop, build_transform_with_y_preservation};
#[derive(Default, ExtractField)]
pub struct GradientTool {
@@ -30,7 +30,8 @@ pub struct GradientTool {
pub struct GradientOptions {
gradient_form: GradientForm,
gradient_spread: GradientSpread,
gradient_interpolation: GradientInterpolation,
gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
}
#[impl_message(Message, ToolMessage, Gradient)]
@@ -139,8 +140,17 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
ToolMessage::Gradient(GradientToolMessage::UpdateRamp { ramp }) => {
let ramp = GradientRamp::from(&ramp);
self.options.gradient_spread = ramp.gradient_spread;
self.options.gradient_interpolation = ramp.gradient_interpolation;
apply_stops_update(&mut self.data, context, responses, Gradient::from(&ramp), ramp.gradient_spread, ramp.gradient_interpolation);
self.options.gradient_space = ramp.gradient_space;
self.options.gradient_hue_direction = ramp.gradient_hue_direction;
apply_stops_update(
&mut self.data,
context,
responses,
Gradient::from(&ramp),
ramp.gradient_spread,
ramp.gradient_space,
ramp.gradient_hue_direction,
);
}
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
if self.data.color_picker_transaction_open {
@@ -178,8 +188,12 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
self.options.gradient_spread = appearance.gradient_spread;
needs_refresh = true;
}
if self.options.gradient_interpolation != appearance.gradient_interpolation {
self.options.gradient_interpolation = appearance.gradient_interpolation;
if self.options.gradient_space != appearance.gradient_space {
self.options.gradient_space = appearance.gradient_space;
needs_refresh = true;
}
if self.options.gradient_hue_direction != appearance.gradient_hue_direction {
self.options.gradient_hue_direction = appearance.gradient_hue_direction;
needs_refresh = true;
}
}
@@ -198,7 +212,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
let new_orientation = match (current_layer, &current_gradient) {
(Some(layer), Some((_gradient, appearance))) => {
let transform = gradient_space_transform(layer, context.document) * appearance.transform;
let transform = gradient_to_viewport_transform(layer, context.document) * appearance.transform;
graph_modification_utils::gradient_orientation_rightward(transform)
}
_ => true,
@@ -262,7 +276,8 @@ impl LayoutHolder for GradientTool {
});
let stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp {
gradient_spread: self.options.gradient_spread,
gradient_interpolation: self.options.gradient_interpolation,
gradient_space: self.options.gradient_space,
gradient_hue_direction: self.options.gradient_hue_direction,
..GradientRamp::from(&stops_value)
}))
.allow_none(false)
@@ -335,8 +350,8 @@ impl Default for GradientToolFsmState {
}
/// Computes the transform from gradient space to viewport space.
fn gradient_space_transform(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> DAffine2 {
graph_modification_utils::gradient_space_transform(layer, &document.network_interface)
fn gradient_to_viewport_transform(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> DAffine2 {
graph_modification_utils::gradient_to_viewport_transform(layer, &document.network_interface)
}
/// Viewport positions of the gradient's start (unit param 0) and end (unit param 1) handles.
@@ -363,7 +378,8 @@ fn resolve_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
GradientAppearance {
gradient_form: gradient.gradient_form,
gradient_spread: gradient.gradient_spread,
gradient_interpolation: gradient.gradient_interpolation,
gradient_space: gradient.gradient_space,
gradient_hue_direction: gradient.gradient_hue_direction,
transform: gradient.transform,
},
GradientSource::Direct,
@@ -383,7 +399,8 @@ struct GradientAppearance {
transform: DAffine2,
gradient_form: GradientForm,
gradient_spread: GradientSpread,
gradient_interpolation: GradientInterpolation,
gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
}
/// Resolve the gradient transform, form, and spread by walking the chain feeding the layer.
@@ -424,7 +441,8 @@ fn read_gradient_chain_state(layer: LayerNodeIdentifier, network_interface: &Nod
transform: composed_transform,
gradient_form: gradient_form.unwrap_or_default(),
gradient_spread: get_chain_source_gradient_spread(layer, network_interface).unwrap_or_default(),
gradient_interpolation: get_chain_source_gradient_interpolation(layer, network_interface).unwrap_or_default(),
gradient_space: get_chain_source_gradient_space(layer, network_interface).unwrap_or_default(),
gradient_hue_direction: get_chain_source_gradient_hue_direction(layer, network_interface).unwrap_or_default(),
}
}
@@ -477,7 +495,7 @@ struct SelectedGradient {
layer: Option<LayerNodeIdentifier>,
dragging: GradientDragTarget,
/// Transform from the geometry's local gradient space to viewport space.
gradient_space_transform: DAffine2,
gradient_to_viewport_transform: DAffine2,
gradient: Gradient,
appearance: GradientAppearance,
initial_gradient: Gradient,
@@ -526,10 +544,10 @@ fn calculate_insertion(start: DVec2, end: DVec2, stops: &Gradient, mouse: DVec2)
impl SelectedGradient {
pub fn new(gradient: Gradient, appearance: GradientAppearance, source: GradientSource, layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Self {
let gradient_space_transform = gradient_space_transform(layer, document);
let gradient_to_viewport_transform = gradient_to_viewport_transform(layer, document);
Self {
layer: Some(layer),
gradient_space_transform,
gradient_to_viewport_transform,
gradient: gradient.clone(),
appearance,
dragging: GradientDragTarget::End,
@@ -615,7 +633,7 @@ impl SelectedGradient {
snap_manager.update_indicator(snapped);
}
let local_mouse = self.gradient_space_transform.inverse().transform_point2(mouse);
let local_mouse = self.gradient_to_viewport_transform.inverse().transform_point2(mouse);
let local_start = self.appearance.transform.transform_point2(DVec2::ZERO);
let local_end = self.appearance.transform.transform_point2(DVec2::X);
@@ -630,7 +648,7 @@ impl SelectedGradient {
self.appearance.transform = create_new_gradient_transform(local_start, local_mouse);
}
GradientDragTarget::New => {
self.appearance.transform = create_new_gradient_transform(self.gradient_space_transform.inverse().transform_point2(drag_start), local_mouse);
self.appearance.transform = create_new_gradient_transform(self.gradient_to_viewport_transform.inverse().transform_point2(drag_start), local_mouse);
}
GradientDragTarget::Stop(stop) => {
let document_to_viewport = snap_data.document.metadata().document_to_viewport;
@@ -761,7 +779,8 @@ impl SelectedGradient {
gradient: self.gradient.clone(),
gradient_form: self.appearance.gradient_form,
gradient_spread: self.appearance.gradient_spread,
gradient_interpolation: self.appearance.gradient_interpolation,
gradient_space: self.appearance.gradient_space,
gradient_hue_direction: self.appearance.gradient_hue_direction,
transform: self.appearance.transform,
});
}
@@ -769,7 +788,7 @@ impl SelectedGradient {
}
fn unit_to_viewport_transform(&self) -> DAffine2 {
self.gradient_space_transform * self.appearance.transform
self.gradient_to_viewport_transform * self.appearance.transform
}
fn viewport_handle_positions(&self) -> (DVec2, DVec2) {
@@ -800,9 +819,13 @@ fn dispatch_gradient_chain_writes(layer: LayerNodeIdentifier, gradient: &Gradien
layer,
gradient_spread: appearance.gradient_spread,
});
responses.add(GraphOperationMessage::GradientInterpolationSet {
responses.add(GraphOperationMessage::GradientSpaceSet {
layer,
gradient_interpolation: appearance.gradient_interpolation,
gradient_space: appearance.gradient_space,
});
responses.add(GraphOperationMessage::GradientHueDirectionSet {
layer,
gradient_hue_direction: appearance.gradient_hue_direction,
});
}
@@ -883,7 +906,7 @@ impl Fsm for GradientToolFsmState {
let Some((gradient, appearance, _source)) = resolve_gradient(layer, &document.network_interface) else {
continue;
};
let unit_to_viewport = gradient_space_transform(layer, document) * appearance.transform;
let unit_to_viewport = gradient_to_viewport_transform(layer, document) * appearance.transform;
let dragging = selected
.filter(|selected| selected.layer.is_some_and(|selected_layer| selected_layer == layer))
.map(|selected| selected.dragging);
@@ -1065,8 +1088,8 @@ impl Fsm for GradientToolFsmState {
&& let Some(selected_gradient) = tool_data.selected_gradient.as_ref()
&& let Some(layer) = selected_gradient.layer
{
// The gradient space transform has be recalculated as the saved transform in SelectedGradient may become stale by panning/zooming during the rendering of the overlay.
let transform = gradient_space_transform(layer, document) * selected_gradient.appearance.transform;
// The gradient-to-viewport transform has to be recalculated as the saved transform in SelectedGradient may become stale by panning/zooming during the rendering of the overlay.
let transform = gradient_to_viewport_transform(layer, document) * selected_gradient.appearance.transform;
let gradient = &selected_gradient.gradient;
if stop_index < gradient.len() {
let color = gradient.color(stop_index).unwrap_or(Color::BLACK);
@@ -1244,7 +1267,7 @@ impl Fsm for GradientToolFsmState {
continue;
};
// TODO: This transform is incorrect. I think this is since it is based on the Footprint which has not been updated yet
let unit_to_viewport = gradient_space_transform(layer, document) * appearance.transform;
let unit_to_viewport = gradient_to_viewport_transform(layer, document) * appearance.transform;
let mouse = input.mouse.position;
let (start, end) = gradient_handle_positions(unit_to_viewport);
@@ -1254,7 +1277,7 @@ impl Fsm for GradientToolFsmState {
// If click is on the line then insert point
if distance < (SELECTION_THRESHOLD * 2.) {
// Try and insert the new stop
if let Some(index) = insert_stop_at_point(&mut gradient, mouse, unit_to_viewport, appearance.gradient_interpolation) {
if let Some(index) = insert_stop_at_point(&mut gradient, mouse, unit_to_viewport, appearance.gradient_space, appearance.gradient_hue_direction) {
responses.add(DocumentMessage::StartTransaction);
let mut selected_gradient = SelectedGradient::new(gradient, appearance, source, layer, document);
@@ -1298,8 +1321,8 @@ impl Fsm for GradientToolFsmState {
let Some((gradient, appearance, source)) = resolve_gradient(layer, &document.network_interface) else {
continue;
};
let gradient_space_transform = gradient_space_transform(layer, document);
let unit_to_viewport = gradient_space_transform * appearance.transform;
let gradient_to_viewport_transform = gradient_to_viewport_transform(layer, document);
let unit_to_viewport = gradient_to_viewport_transform * appearance.transform;
let is_gradient_chain = source == GradientSource::Chain;
let (start, end) = gradient_handle_positions(unit_to_viewport);
@@ -1324,7 +1347,7 @@ impl Fsm for GradientToolFsmState {
tool_data.selected_gradient = Some(SelectedGradient {
layer: Some(layer),
gradient_space_transform,
gradient_to_viewport_transform,
gradient: gradient.clone(),
appearance,
initial_gradient_transform: appearance.transform,
@@ -1368,7 +1391,7 @@ impl Fsm for GradientToolFsmState {
tool_data.selected_gradient = Some(SelectedGradient {
layer: Some(layer),
dragging: drag_target,
gradient_space_transform,
gradient_to_viewport_transform,
gradient: gradient.clone(),
appearance,
initial_gradient: gradient.clone(),
@@ -1386,7 +1409,7 @@ impl Fsm for GradientToolFsmState {
tool_data.selected_gradient = Some(SelectedGradient {
layer: Some(layer),
dragging: dragging_target,
gradient_space_transform,
gradient_to_viewport_transform,
gradient: gradient.clone(),
appearance,
initial_gradient: gradient.clone(),
@@ -1404,7 +1427,7 @@ impl Fsm for GradientToolFsmState {
if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) {
let mut new_gradient = gradient.clone();
if let Some(index) = insert_stop_at_point(&mut new_gradient, mouse, unit_to_viewport, appearance.gradient_interpolation) {
if let Some(index) = insert_stop_at_point(&mut new_gradient, mouse, unit_to_viewport, appearance.gradient_space, appearance.gradient_hue_direction) {
responses.add(DocumentMessage::StartTransaction);
transaction_started = true;
@@ -1483,7 +1506,8 @@ impl Fsm for GradientToolFsmState {
transform: DAffine2::IDENTITY,
gradient_form: tool_options.gradient_form,
gradient_spread: tool_options.gradient_spread,
gradient_interpolation: tool_options.gradient_interpolation,
gradient_space: tool_options.gradient_space,
gradient_hue_direction: tool_options.gradient_hue_direction,
},
// A blank layer, or one holding only the other tool's paint, starts a whole-expanse gradient chain; a layer with content gets its Fill painted
if replaceable_paint_chain(layer, &document.network_interface).is_some() {
@@ -1524,8 +1548,8 @@ impl Fsm for GradientToolFsmState {
// Recompute the gradient-to-viewport transform fresh each frame so zoom/pan mid-drag works correctly
if let Some(layer) = selected_gradient.layer {
selected_gradient.gradient_space_transform = gradient_space_transform(layer, document);
selected_gradient.gradient_space_transform.translation += tool_data.auto_pan_shift;
selected_gradient.gradient_to_viewport_transform = gradient_to_viewport_transform(layer, document);
selected_gradient.gradient_to_viewport_transform.translation += tool_data.auto_pan_shift;
}
// Convert drag_start from document space to effective viewport space
@@ -1715,10 +1739,10 @@ impl Fsm for GradientToolFsmState {
}
}
fn insert_stop_at_point(gradient: &mut Gradient, point: DVec2, unit_to_viewport: DAffine2, gradient_interpolation: GradientInterpolation) -> Option<usize> {
fn insert_stop_at_point(gradient: &mut Gradient, point: DVec2, unit_to_viewport: DAffine2, gradient_space: GradientSpace, gradient_hue_direction: GradientHueDirection) -> Option<usize> {
let (start, end) = gradient_handle_positions(unit_to_viewport);
let t = ((end - start).angle_to(point - start)).cos() * start.distance(point) / start.distance(end);
(0. ..=1.).contains(&t).then(|| gradient.insert_stop(t, gradient_interpolation))
(0. ..=1.).contains(&t).then(|| gradient.insert_stop(t, gradient_space, gradient_hue_direction))
}
fn dismiss_color_stop_color_picker(tool_data: &mut GradientToolData, responses: &mut VecDeque<Message>) {
@@ -1739,8 +1763,8 @@ fn detect_hover_target(mouse: DVec2, document: &DocumentMessageHandler) -> Gradi
let Some((gradient, appearance, _source)) = resolve_gradient(layer, &document.network_interface) else {
continue;
};
let gradient_space_transform = gradient_space_transform(layer, document);
let unit_to_viewport = gradient_space_transform * appearance.transform;
let gradient_to_viewport_transform = gradient_to_viewport_transform(layer, document);
let unit_to_viewport = gradient_to_viewport_transform * appearance.transform;
let (start, end) = gradient_handle_positions(unit_to_viewport);
let line_length = start.distance(end);
@@ -1842,7 +1866,8 @@ fn apply_gradient_update(
gradient,
gradient_form: appearance.gradient_form,
gradient_spread: appearance.gradient_spread,
gradient_interpolation: appearance.gradient_interpolation,
gradient_space: appearance.gradient_space,
gradient_hue_direction: appearance.gradient_hue_direction,
transform: appearance.transform,
});
}
@@ -1872,7 +1897,8 @@ fn apply_stops_update(
responses: &mut VecDeque<Message>,
new_gradient: Gradient,
gradient_spread: GradientSpread,
gradient_interpolation: GradientInterpolation,
gradient_space: GradientSpace,
gradient_hue_direction: GradientHueDirection,
) {
let selected_layers: Vec<_> = context
.document
@@ -1890,7 +1916,8 @@ fn apply_stops_update(
if get_upstream_gradient_value_node_id(layer, &context.document.network_interface).is_some() {
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: new_gradient.clone() });
responses.add(GraphOperationMessage::GradientSpreadSet { layer, gradient_spread });
responses.add(GraphOperationMessage::GradientInterpolationSet { layer, gradient_interpolation });
responses.add(GraphOperationMessage::GradientSpaceSet { layer, gradient_space });
responses.add(GraphOperationMessage::GradientHueDirectionSet { layer, gradient_hue_direction });
updated_any_layer = true;
} else if let Some((_gradient, appearance, _source)) = resolve_gradient(layer, &context.document.network_interface) {
responses.add(GraphOperationMessage::FillGradientSet {
@@ -1898,7 +1925,8 @@ fn apply_stops_update(
gradient: new_gradient.clone(),
gradient_form: appearance.gradient_form,
gradient_spread,
gradient_interpolation,
gradient_space,
gradient_hue_direction,
transform: appearance.transform,
});
updated_any_layer = true;
@@ -1908,7 +1936,8 @@ fn apply_stops_update(
if let Some(selected_gradient) = &mut data.selected_gradient {
selected_gradient.gradient = new_gradient.clone();
selected_gradient.appearance.gradient_spread = gradient_spread;
selected_gradient.appearance.gradient_interpolation = gradient_interpolation;
selected_gradient.appearance.gradient_space = gradient_space;
selected_gradient.appearance.gradient_hue_direction = gradient_hue_direction;
}
// When no selected layer had a gradient to update, the user is editing the tool's default gradient instead.
@@ -2008,7 +2037,7 @@ mod test_gradient {
use graphene_std::vector::style::{GradientForm, GradientSpread, build_transform_with_y_preservation};
use graphene_std::vector::{Gradient, GradientRamp, GradientStop, fill};
use super::gradient_space_transform;
use super::gradient_to_viewport_transform;
struct ResolvedGradient {
stops: Gradient,
@@ -2065,7 +2094,7 @@ mod test_gradient {
transform: local_transform,
};
let transform = gradient_space_transform(layer, document);
let transform = gradient_to_viewport_transform(layer, document);
Some((gradient, transform))
})
.collect()
@@ -2082,7 +2111,7 @@ mod test_gradient {
let (gradient, appearance, _) = super::resolve_gradient(layer, &document.network_interface)?;
let gradient = ResolvedGradient::new(gradient, appearance);
let transform = gradient_space_transform(layer, document);
let transform = gradient_to_viewport_transform(layer, document);
Some((gradient, transform))
})
.collect()
@@ -2770,11 +2799,11 @@ mod test_gradient {
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
let document = editor.active_document();
let space_transform = gradient_space_transform(layer, document);
let to_viewport_transform = gradient_to_viewport_transform(layer, document);
let (gradient, appearance, _) = super::resolve_gradient(layer, &document.network_interface).unwrap();
let gradient = ResolvedGradient::new(gradient, appearance);
let viewport_start = space_transform.transform_point2(gradient.start());
let viewport_end = space_transform.transform_point2(gradient.end());
let viewport_start = to_viewport_transform.transform_point2(gradient.start());
let viewport_end = to_viewport_transform.transform_point2(gradient.end());
// Drag target of the end point, move 80px down
let new_viewport_end = viewport_end + DVec2::new(0., 80.);
@@ -2797,9 +2826,9 @@ mod test_gradient {
let document = editor.active_document();
let (updated, appearance, _) = super::resolve_gradient(layer, &document.network_interface).expect("Gradient should exist after drag");
let updated = ResolvedGradient::new(updated, appearance);
let updated_space_transform = gradient_space_transform(layer, document);
let updated_viewport_start = updated_space_transform.transform_point2(updated.start());
let updated_viewport_end = updated_space_transform.transform_point2(updated.end());
let updated_to_viewport_transform = gradient_to_viewport_transform(layer, document);
let updated_viewport_start = updated_to_viewport_transform.transform_point2(updated.start());
let updated_viewport_end = updated_to_viewport_transform.transform_point2(updated.end());
assert!(
updated_viewport_start.abs_diff_eq(viewport_start, 1.),
@@ -2848,10 +2877,10 @@ mod test_gradient {
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
let document = editor.active_document();
let space_transform = gradient_space_transform(layer, document);
let to_viewport_transform = gradient_to_viewport_transform(layer, document);
let (gradient, appearance, _) = super::resolve_gradient(layer, &document.network_interface).unwrap();
let gradient = ResolvedGradient::new(gradient, appearance);
let viewport_end = space_transform.transform_point2(gradient.end());
let viewport_end = to_viewport_transform.transform_point2(gradient.end());
// Drag the end point 80px down
let new_viewport_end = viewport_end + DVec2::new(0., 80.);