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:
parent
e686ee9f42
commit
4b2430290c
|
|
@ -70,5 +70,6 @@
|
|||
"*.graphite": "json"
|
||||
},
|
||||
"editor.renderWhitespace": "boundary",
|
||||
"editor.minimap.markSectionHeaderRegex": "// ===+\\n\\s*//\\s*(?<label>[^\\n]{1,18})[^\\n]*(\\n\\s*//[^\\n]*)*\\n\\s*// ===+"
|
||||
"editor.minimap.markSectionHeaderRegex": "// ===+\\n\\s*//\\s*(?<label>[^\\n]{1,18})[^\\n]*(\\n\\s*//[^\\n]*)*\\n\\s*// ===+",
|
||||
"git.addAICoAuthor": "off"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -289,6 +289,39 @@ pub fn get_gradient_table(layer: LayerNodeIdentifier, network_interface: &NodeNe
|
|||
Some(gradient_table.clone())
|
||||
}
|
||||
|
||||
/// Compute the transform from a gradient's local space to viewport space for the given layer. For a `Table<GradientStops>`
|
||||
/// layer this is the layer's incoming footprint transform; for the legacy `Fill::Gradient` path 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 {
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
||||
|
||||
let metadata = network_interface.document_metadata();
|
||||
let is_gradient_table = is_layer_fed_by_node_of_name(layer, network_interface, &DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER));
|
||||
if is_gradient_table {
|
||||
return metadata
|
||||
.upstream_footprints
|
||||
.get(&layer.to_node())
|
||||
.map(|footprint| footprint.transform)
|
||||
.unwrap_or(metadata.document_to_viewport);
|
||||
}
|
||||
let multiplied = metadata.transform_to_viewport(layer);
|
||||
let bounds = metadata.nonzero_bounding_box(layer);
|
||||
let bound_transform = glam::DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
multiplied * bound_transform
|
||||
}
|
||||
|
||||
/// True when start→end (mapped through `transform` into viewport space) points predominantly rightward. For purely
|
||||
/// vertical lines we fall back to a stable tiebreaker on (x + y) so the choice doesn't flicker between equal alternatives.
|
||||
pub fn gradient_orientation_rightward(start: glam::DVec2, end: glam::DVec2, transform: glam::DAffine2) -> bool {
|
||||
let viewport_start = transform.transform_point2(start);
|
||||
let viewport_end = transform.transform_point2(end);
|
||||
if (viewport_end.x - viewport_start.x).abs() > f64::EPSILON * 1e6 {
|
||||
viewport_end.x > viewport_start.x
|
||||
} else {
|
||||
(viewport_start.x + viewport_start.y) < (viewport_end.x + viewport_end.y)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current fill of a layer from the closest "Fill" node.
|
||||
pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Color> {
|
||||
let fill_index = 1;
|
||||
|
|
|
|||
|
|
@ -6,14 +6,13 @@ use crate::consts::{
|
|||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::{GizmoEmphasis, OverlayContext};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
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_gradient_table, is_layer_fed_by_node_of_name};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_gradient_table};
|
||||
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::raster::color::Color;
|
||||
use graphene_std::table::{Table, TableRow};
|
||||
use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStops, GradientType};
|
||||
use graphene_std::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD, ATTR_TRANSFORM};
|
||||
use graphene_std::vector::style::{Fill, FillChoice, Gradient, GradientSpreadMethod, GradientStops, GradientType};
|
||||
|
||||
#[derive(Default, ExtractField)]
|
||||
pub struct GradientTool {
|
||||
|
|
@ -49,6 +48,7 @@ pub enum GradientToolMessage {
|
|||
CommitTransactionForColorStop,
|
||||
CloseStopColorPicker,
|
||||
UpdateStopColor { color: Color },
|
||||
UpdateStops { stops: GradientStops },
|
||||
UpdateOptions { options: GradientOptionsUpdate },
|
||||
}
|
||||
|
||||
|
|
@ -118,6 +118,9 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
|||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
}
|
||||
ToolMessage::Gradient(GradientToolMessage::UpdateStops { stops }) => {
|
||||
apply_stops_update(&mut self.data, context, responses, stops);
|
||||
}
|
||||
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
|
||||
if self.data.color_picker_transaction_open {
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
|
|
@ -128,27 +131,48 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
|||
_ => {
|
||||
self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, false);
|
||||
|
||||
let has_gradient = has_gradient_on_selected_layers(context.document);
|
||||
// Reading from the layer (not from the in-progress drag state) keeps the control bar widgets current across selection changes, not just drags
|
||||
let (current_layer, current_gradient) = current_layer_and_gradient(context.document);
|
||||
|
||||
let mut needs_refresh = false;
|
||||
if let Some(gradient) = ¤t_gradient {
|
||||
if self.options.gradient_type != gradient.gradient_type {
|
||||
self.options.gradient_type = gradient.gradient_type;
|
||||
needs_refresh = true;
|
||||
}
|
||||
if self.options.spread_method != gradient.spread_method {
|
||||
self.options.spread_method = gradient.spread_method;
|
||||
needs_refresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
let has_gradient = current_gradient.is_some();
|
||||
if has_gradient != self.data.has_selected_gradient {
|
||||
self.data.has_selected_gradient = has_gradient;
|
||||
responses.add(ToolMessage::RefreshToolOptions);
|
||||
needs_refresh = true;
|
||||
}
|
||||
|
||||
// Sync tool options with the selected layer's gradient
|
||||
if has_gradient && let Some(gradient) = get_gradient_on_selected_layer(context.document) {
|
||||
let type_differs = self.options.gradient_type != gradient.gradient_type;
|
||||
let spread_method_differs = self.options.spread_method != gradient.spread_method;
|
||||
let new_stops = current_gradient.as_ref().map(|gradient| gradient.stops.clone());
|
||||
if self.data.current_gradient_stops != new_stops {
|
||||
self.data.current_gradient_stops = new_stops;
|
||||
needs_refresh = true;
|
||||
}
|
||||
|
||||
if type_differs {
|
||||
self.options.gradient_type = gradient.gradient_type;
|
||||
}
|
||||
if spread_method_differs {
|
||||
self.options.spread_method = gradient.spread_method;
|
||||
}
|
||||
if type_differs || spread_method_differs {
|
||||
responses.add(ToolMessage::RefreshToolOptions);
|
||||
let new_orientation = match (current_layer, ¤t_gradient) {
|
||||
(Some(layer), Some(gradient)) => {
|
||||
let transform = gradient_space_transform(layer, context.document);
|
||||
graph_modification_utils::gradient_orientation_rightward(gradient.start, gradient.end, transform)
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
if new_orientation != self.data.gradient_orientation_rightward {
|
||||
self.data.gradient_orientation_rightward = new_orientation;
|
||||
needs_refresh = true;
|
||||
}
|
||||
|
||||
if needs_refresh {
|
||||
responses.add(ToolMessage::RefreshToolOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -184,7 +208,23 @@ impl LayoutHolder for GradientTool {
|
|||
.selected_index(Some((self.options.gradient_type == GradientType::Radial) as u32))
|
||||
.widget_instance();
|
||||
|
||||
widgets.extend([gradient_type, Separator::new(SeparatorStyle::Unrelated).widget_instance()]);
|
||||
let stops_value = self
|
||||
.data
|
||||
.current_gradient_stops
|
||||
.clone()
|
||||
.map(FillChoice::Gradient)
|
||||
.unwrap_or(FillChoice::Gradient(GradientStops::default()));
|
||||
let stops_widget = ColorInput::new(stops_value)
|
||||
.allow_none(false)
|
||||
.disabled(!self.data.has_selected_gradient)
|
||||
.tooltip_label("Gradient Stops")
|
||||
.tooltip_description("Edit the gradient's color stops.")
|
||||
.on_update(|input: &ColorInput| {
|
||||
let stops = input.value.as_gradient().cloned().unwrap_or_default();
|
||||
GradientToolMessage::UpdateStops { stops }.into()
|
||||
})
|
||||
.on_commit(|_| DocumentMessage::AddTransaction.into())
|
||||
.widget_instance();
|
||||
|
||||
let reverse_stops = IconButton::new("Reverse", 24)
|
||||
.tooltip_label("Reverse Stops")
|
||||
|
|
@ -221,24 +261,12 @@ impl LayoutHolder for GradientTool {
|
|||
.selected_index(Some(self.options.spread_method as u32))
|
||||
.widget_instance();
|
||||
|
||||
widgets.extend([spread_method, Separator::new(SeparatorStyle::Unrelated).widget_instance(), reverse_stops]);
|
||||
|
||||
if self.options.gradient_type == GradientType::Radial {
|
||||
let orientation = self
|
||||
.data
|
||||
.selected_gradient
|
||||
.as_ref()
|
||||
.map(|selected_gradient| {
|
||||
let (start, end) = (selected_gradient.gradient.start, selected_gradient.gradient.end);
|
||||
if (end.x - start.x).abs() > f64::EPSILON * 1e6 {
|
||||
end.x > start.x
|
||||
let reverse_direction_icon = if self.data.gradient_orientation_rightward {
|
||||
"ReverseRadialGradientToRight"
|
||||
} else {
|
||||
(start.x + start.y) < (end.x + end.y)
|
||||
}
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
let reverse_direction = IconButton::new(if orientation { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24)
|
||||
"ReverseRadialGradientToLeft"
|
||||
};
|
||||
let reverse_direction = IconButton::new(reverse_direction_icon, 24)
|
||||
.tooltip_label("Reverse Direction")
|
||||
.tooltip_description("Reverse which end the gradient radiates from.")
|
||||
.disabled(!self.data.has_selected_gradient)
|
||||
|
|
@ -250,9 +278,17 @@ impl LayoutHolder for GradientTool {
|
|||
})
|
||||
.widget_instance();
|
||||
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
widgets.push(reverse_direction);
|
||||
}
|
||||
widgets.extend([
|
||||
stops_widget,
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
reverse_stops,
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
gradient_type,
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
spread_method,
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
reverse_direction,
|
||||
]);
|
||||
|
||||
Layout(vec![LayoutGroup::row(widgets)])
|
||||
}
|
||||
|
|
@ -273,63 +309,110 @@ impl Default for GradientToolFsmState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Computes the transform from gradient space to viewport space (where gradient space is 0..1)
|
||||
/// Computes the transform from gradient space to viewport space (where gradient space is 0..1).
|
||||
fn gradient_space_transform(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> DAffine2 {
|
||||
// TODO: Drop the `is_gradient_table` branch once all gradients are `Table<GradientStops>`
|
||||
let is_gradient_table = is_layer_fed_by_node_of_name(
|
||||
layer,
|
||||
&document.network_interface,
|
||||
&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER),
|
||||
);
|
||||
|
||||
if is_gradient_table {
|
||||
// Table<GradientStops> layers use the item's transform from gradient space to document space,
|
||||
// so we cannot use `transform_to_viewport` here as it would apply the transform twice.
|
||||
return document
|
||||
.metadata()
|
||||
.upstream_footprints
|
||||
.get(&layer.to_node())
|
||||
.map(|footprint| footprint.transform)
|
||||
.unwrap_or(document.metadata().document_to_viewport);
|
||||
}
|
||||
|
||||
let multiplied = document.metadata().transform_to_viewport(layer);
|
||||
let bounds = document.metadata().nonzero_bounding_box(layer);
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
multiplied * bound_transform
|
||||
}
|
||||
|
||||
/// Builds the item transform that maps the unit gradient line (the +X unit vector in local space) to
|
||||
/// the segment from `start` to `end` in document space. The perpendicular column is forced to the same magnitude
|
||||
/// as the `start`..`end` direction so the matrix stays invertible (linear gradients ignore the perpendicular axis,
|
||||
/// but click detection uses the full inverse).
|
||||
// TODO: Apply a separate scale on the perpendicular axis when we support elliptical gradients
|
||||
fn gradient_item_transform(start: DVec2, end: DVec2) -> DAffine2 {
|
||||
let delta = end - start;
|
||||
let perp = DVec2::new(-delta.y, delta.x);
|
||||
DAffine2::from_cols_array(&[delta.x, delta.y, perp.x, perp.y, start.x, start.y])
|
||||
graph_modification_utils::gradient_space_transform(layer, &document.network_interface)
|
||||
}
|
||||
|
||||
// TODO: Remove this whole function once all gradients are `Table<GradientStops>`
|
||||
fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
|
||||
match (get_gradient_table(layer, network_interface), graph_modification_utils::get_gradient(layer, network_interface)) {
|
||||
(Some(gradient_graphic), _) => {
|
||||
let stops = gradient_graphic.element(0)?.clone();
|
||||
let transform: DAffine2 = gradient_graphic.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||
let spread_method: GradientSpreadMethod = gradient_graphic.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0);
|
||||
let gradient_type: GradientType = gradient_graphic.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0);
|
||||
let gradient = Gradient {
|
||||
if let Some(stops_table) = get_gradient_table(layer, network_interface) {
|
||||
let stops = stops_table.element(0).cloned().unwrap_or_default();
|
||||
let GradientChainState {
|
||||
transform,
|
||||
gradient_type,
|
||||
spread_method,
|
||||
} = read_gradient_chain_state(layer, network_interface);
|
||||
return Some(Gradient {
|
||||
stops,
|
||||
gradient_type,
|
||||
spread_method,
|
||||
start: transform.transform_point2(DVec2::ZERO),
|
||||
end: transform.transform_point2(DVec2::X),
|
||||
});
|
||||
}
|
||||
graph_modification_utils::get_gradient(layer, network_interface)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct GradientChainState {
|
||||
transform: DAffine2,
|
||||
gradient_type: GradientType,
|
||||
spread_method: GradientSpreadMethod,
|
||||
}
|
||||
|
||||
/// Resolve the gradient transform, type, and spread method by walking the chain feeding the layer. Transform composes all
|
||||
/// 'Transform' nodes. Type and spread method come from the closest-to-layer node of each kind, or the type default.
|
||||
fn read_gradient_chain_state(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> GradientChainState {
|
||||
let transform_reference = DefinitionIdentifier::Network("Transform".into());
|
||||
let gradient_type_reference = DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_type::IDENTIFIER);
|
||||
let spread_method_reference = DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::spread_method::IDENTIFIER);
|
||||
|
||||
let mut transforms_downstream_to_upstream: Vec<DAffine2> = Vec::new();
|
||||
let mut gradient_type: Option<GradientType> = None;
|
||||
let mut spread_method: Option<GradientSpreadMethod> = None;
|
||||
|
||||
for node_id in network_interface
|
||||
.upstream_flow_back_from_nodes(vec![layer.to_node()], &[], FlowType::HorizontalFlow)
|
||||
.skip(1)
|
||||
.take_while(|node_id| !network_interface.is_layer(node_id, &[]))
|
||||
{
|
||||
let Some(reference) = network_interface.reference(&node_id, &[]) else { continue };
|
||||
let Some(document_node) = network_interface.document_network().nodes.get(&node_id) else {
|
||||
continue;
|
||||
};
|
||||
Some(gradient)
|
||||
|
||||
if reference == transform_reference {
|
||||
transforms_downstream_to_upstream.push(read_transform_node_value(&document_node.inputs));
|
||||
} else if reference == gradient_type_reference
|
||||
&& gradient_type.is_none()
|
||||
&& let Some(TaggedValue::GradientType(value)) = document_node.inputs.get(1).and_then(|input| input.as_value())
|
||||
{
|
||||
gradient_type = Some(*value);
|
||||
} else if reference == spread_method_reference
|
||||
&& spread_method.is_none()
|
||||
&& let Some(TaggedValue::GradientSpreadMethod(value)) = document_node.inputs.get(1).and_then(|input| input.as_value())
|
||||
{
|
||||
spread_method = Some(*value);
|
||||
}
|
||||
(None, Some(gradient)) => Some(gradient),
|
||||
(None, None) => None,
|
||||
}
|
||||
|
||||
// Iteration order [T_n, ..., T_1] is the matrix-product order, so the fold yields T_n * ... * T_1
|
||||
let composed_transform = transforms_downstream_to_upstream.into_iter().fold(DAffine2::IDENTITY, |acc, matrix| acc * matrix);
|
||||
|
||||
GradientChainState {
|
||||
transform: composed_transform,
|
||||
gradient_type: gradient_type.unwrap_or_default(),
|
||||
spread_method: spread_method.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstruct the `DAffine2` produced by a 'Transform' node from its translation, rotation, scale, and skew inputs.
|
||||
fn read_transform_node_value(inputs: &[graph_craft::document::NodeInput]) -> DAffine2 {
|
||||
let translation = inputs
|
||||
.get(1)
|
||||
.and_then(|input| input.as_value())
|
||||
.and_then(|value| if let TaggedValue::DVec2(v) = value { Some(*v) } else { None })
|
||||
.unwrap_or(DVec2::ZERO);
|
||||
let rotation_degrees = inputs
|
||||
.get(2)
|
||||
.and_then(|input| input.as_value())
|
||||
.and_then(|value| if let TaggedValue::F64(v) = value { Some(*v) } else { None })
|
||||
.unwrap_or(0.);
|
||||
let scale = inputs
|
||||
.get(3)
|
||||
.and_then(|input| input.as_value())
|
||||
.and_then(|value| if let TaggedValue::DVec2(v) = value { Some(*v) } else { None })
|
||||
.unwrap_or(DVec2::ONE);
|
||||
let skew = inputs
|
||||
.get(4)
|
||||
.and_then(|input| input.as_value())
|
||||
.and_then(|value| if let TaggedValue::DVec2(v) = value { Some(*v) } else { None })
|
||||
.unwrap_or(DVec2::ZERO);
|
||||
|
||||
let trs = DAffine2::from_scale_angle_translation(scale, rotation_degrees.to_radians(), translation);
|
||||
let skew_matrix = DAffine2::from_cols_array(&[1., skew.y.to_radians().tan(), skew.x.to_radians().tan(), 1., 0., 0.]);
|
||||
trs * skew_matrix
|
||||
}
|
||||
|
||||
/// Whether two adjacent stops are too closely packed in viewport space for a midpoint diamond to be shown or interacted with.
|
||||
|
|
@ -625,13 +708,7 @@ impl SelectedGradient {
|
|||
if let Some(layer) = self.layer {
|
||||
// TODO: Drop the `Fill::Gradient` branch when all gradients become `Table<GradientStops>`
|
||||
if self.is_gradient_table {
|
||||
let gradient_table = Table::new_from_row(
|
||||
TableRow::new_from_element(self.gradient.stops.clone())
|
||||
.with_attribute(ATTR_TRANSFORM, gradient_item_transform(self.gradient.start, self.gradient.end))
|
||||
.with_attribute(ATTR_SPREAD_METHOD, self.gradient.spread_method)
|
||||
.with_attribute(ATTR_GRADIENT_TYPE, self.gradient.gradient_type),
|
||||
);
|
||||
responses.add(GraphOperationMessage::GradientTableSet { layer, gradient_table });
|
||||
dispatch_gradient_writes(layer, &self.gradient, responses);
|
||||
} else {
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer,
|
||||
|
|
@ -642,6 +719,24 @@ impl SelectedGradient {
|
|||
}
|
||||
}
|
||||
|
||||
/// Send the four per-attribute graph operations that mirror the in-memory `Gradient` onto the chain feeding the layer.
|
||||
fn dispatch_gradient_writes(layer: LayerNodeIdentifier, gradient: &Gradient, responses: &mut VecDeque<Message>) {
|
||||
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: gradient.stops.clone() });
|
||||
responses.add(GraphOperationMessage::GradientLineSet {
|
||||
layer,
|
||||
start: gradient.start,
|
||||
end: gradient.end,
|
||||
});
|
||||
responses.add(GraphOperationMessage::GradientTypeSet {
|
||||
layer,
|
||||
gradient_type: gradient.gradient_type,
|
||||
});
|
||||
responses.add(GraphOperationMessage::GradientSpreadMethodSet {
|
||||
layer,
|
||||
spread_method: gradient.spread_method,
|
||||
});
|
||||
}
|
||||
|
||||
impl GradientTool {
|
||||
/// Get the gradient type of the selected gradient (if it exists)
|
||||
pub fn selected_gradient(&self) -> Option<GradientType> {
|
||||
|
|
@ -669,6 +764,12 @@ struct GradientToolData {
|
|||
auto_pan_shift: DVec2,
|
||||
gradient_angle: f64,
|
||||
has_selected_gradient: bool,
|
||||
/// Cached stops of the currently selected layer's gradient, mirrored into the control-bar widget. Independent of any
|
||||
/// in-progress drag (which uses `selected_gradient`) so it stays current after selection changes too.
|
||||
current_gradient_stops: Option<GradientStops>,
|
||||
/// Cached viewport-space orientation (true = predominantly rightward) of the selected gradient line.
|
||||
/// Used to refresh the control bar's "Reverse Direction" icon only when the line's apparent direction flips.
|
||||
gradient_orientation_rightward: bool,
|
||||
color_picker_editing_color_stop: Option<usize>,
|
||||
color_picker_transaction_open: bool,
|
||||
}
|
||||
|
|
@ -1251,7 +1352,15 @@ impl Fsm for GradientToolFsmState {
|
|||
GradientToolFsmState::Drawing { drag_hint: hint }
|
||||
} else {
|
||||
let document_mouse = document.metadata().document_to_viewport.inverse().transform_point2(mouse);
|
||||
let selected_layer = document.click_based_on_position(document_mouse);
|
||||
// Table-based gradients render no geometry, so a click on empty canvas yields no layer. Fall back to a
|
||||
// selected gradient-table layer so the user can drag a fresh gradient line anywhere.
|
||||
let selected_layer = document.click_based_on_position(document_mouse).or_else(|| {
|
||||
document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_layers(&document.network_interface)
|
||||
.find(|&layer| get_gradient_table(layer, &document.network_interface).is_some())
|
||||
});
|
||||
|
||||
// Apply the gradient to the selected layer
|
||||
if let Some(layer) = selected_layer {
|
||||
|
|
@ -1615,14 +1724,7 @@ fn apply_gradient_update(
|
|||
// Only check for the gradient table once we know we'll write back, since this is a graph traversal per layer
|
||||
// TODO: Drop the `Fill::Gradient` branch when all gradients become `Table<GradientStops>`
|
||||
if get_gradient_table(layer, &context.document.network_interface).is_some() {
|
||||
// Rebuild the item transform from the (possibly mutated) start/end so updates like `ReverseDirection` that only swap endpoints are reflected in the stored attribute
|
||||
let gradient_table = Table::new_from_row(
|
||||
TableRow::new_from_element(gradient.stops.clone())
|
||||
.with_attribute(ATTR_TRANSFORM, gradient_item_transform(gradient.start, gradient.end))
|
||||
.with_attribute(ATTR_SPREAD_METHOD, gradient.spread_method)
|
||||
.with_attribute(ATTR_GRADIENT_TYPE, gradient.gradient_type),
|
||||
);
|
||||
responses.add(GraphOperationMessage::GradientTableSet { layer, gradient_table });
|
||||
dispatch_gradient_writes(layer, &gradient, responses);
|
||||
} else {
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer,
|
||||
|
|
@ -1646,6 +1748,50 @@ fn apply_gradient_update(
|
|||
responses.add(ToolMessage::RefreshToolOptions);
|
||||
}
|
||||
|
||||
/// Set new gradient stops on every selected layer's gradient. Unlike `apply_gradient_update`, this doesn't open its own
|
||||
/// transaction so it can be called repeatedly during a color picker drag and have all the changes coalesced into a
|
||||
/// single undo entry by the surrounding 'on_commit' callback.
|
||||
fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessageContext, responses: &mut VecDeque<Message>, stops: GradientStops) {
|
||||
let selected_layers: Vec<_> = context
|
||||
.document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_layers(&context.document.network_interface)
|
||||
.collect();
|
||||
|
||||
for layer in selected_layers {
|
||||
if NodeGraphLayer::is_raster_layer(layer, &mut context.document.network_interface) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if get_gradient_table(layer, &context.document.network_interface).is_some() {
|
||||
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: stops.clone() });
|
||||
} else if let Some(mut gradient) = get_gradient(layer, &context.document.network_interface) {
|
||||
gradient.stops = stops.clone();
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer,
|
||||
fill: Fill::Gradient(gradient),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(selected_gradient) = &mut data.selected_gradient {
|
||||
selected_gradient.gradient.stops = stops;
|
||||
}
|
||||
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
|
||||
/// Find the first selected visible layer that has a gradient and return both the layer ID and its resolved gradient.
|
||||
fn current_layer_and_gradient(document: &DocumentMessageHandler) -> (Option<LayerNodeIdentifier>, Option<Gradient>) {
|
||||
for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) {
|
||||
if let Some(gradient) = get_gradient(layer, &document.network_interface) {
|
||||
return (Some(layer), Some(gradient));
|
||||
}
|
||||
}
|
||||
(None, None)
|
||||
}
|
||||
|
||||
fn get_gradient_on_selected_layer(document: &DocumentMessageHandler) -> Option<Gradient> {
|
||||
document
|
||||
.network_interface
|
||||
|
|
@ -2176,12 +2322,7 @@ mod test_gradient {
|
|||
// Create original transform for the control geometry and apply it
|
||||
let initial_start = DVec2::new(10., 50.);
|
||||
let initial_end = DVec2::new(200., 50.);
|
||||
let initial_item_transform = super::gradient_item_transform(initial_start, initial_end);
|
||||
editor
|
||||
.handle_message(GraphOperationMessage::GradientTableSet {
|
||||
layer,
|
||||
gradient_table: Table::new_from_row(
|
||||
TableRow::new_from_element(GradientStops::new([
|
||||
let stops = GradientStops::new([
|
||||
GradientStop {
|
||||
position: 0.,
|
||||
midpoint: 0.5,
|
||||
|
|
@ -2192,9 +2333,13 @@ mod test_gradient {
|
|||
midpoint: 0.5,
|
||||
color: Color::BLUE,
|
||||
},
|
||||
]))
|
||||
.with_attribute(ATTR_TRANSFORM, initial_item_transform),
|
||||
),
|
||||
]);
|
||||
editor.handle_message(GraphOperationMessage::GradientStopsSet { layer, stops }).await;
|
||||
editor
|
||||
.handle_message(GraphOperationMessage::GradientLineSet {
|
||||
layer,
|
||||
start: initial_start,
|
||||
end: initial_end,
|
||||
})
|
||||
.await;
|
||||
|
||||
|
|
@ -2266,11 +2411,12 @@ mod test_gradient {
|
|||
]);
|
||||
let initial_start = DVec2::new(10., 50.);
|
||||
let initial_end = DVec2::new(200., 50.);
|
||||
let initial_item_transform = super::gradient_item_transform(initial_start, initial_end);
|
||||
editor.handle_message(GraphOperationMessage::GradientStopsSet { layer, stops: original_stops.clone() }).await;
|
||||
editor
|
||||
.handle_message(GraphOperationMessage::GradientTableSet {
|
||||
.handle_message(GraphOperationMessage::GradientLineSet {
|
||||
layer,
|
||||
gradient_table: Table::new_from_row(TableRow::new_from_element(original_stops.clone()).with_attribute(ATTR_TRANSFORM, initial_item_transform)),
|
||||
start: initial_start,
|
||||
end: initial_end,
|
||||
})
|
||||
.await;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,16 +3,15 @@ use crate::application_io::PlatformEditorApi;
|
|||
use crate::proto::{Any as DAny, FutureAny};
|
||||
use brush_nodes::brush_cache::BrushCache;
|
||||
use brush_nodes::brush_stroke::BrushStroke;
|
||||
use core_types::table::{Table, TableRow};
|
||||
use core_types::table::Table;
|
||||
use core_types::transform::Footprint;
|
||||
use core_types::uuid::NodeId;
|
||||
use core_types::{ATTR_TRANSFORM, CacheHash, Color, ContextFeatures, MemoHash, Node, Type};
|
||||
use core_types::{CacheHash, Color, ContextFeatures, MemoHash, Node, Type};
|
||||
use dyn_any::DynAny;
|
||||
pub use dyn_any::StaticType;
|
||||
use glam::{Affine2, Vec2};
|
||||
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
|
||||
use graphic_types::raster_types::{CPU, Image, Raster};
|
||||
use graphic_types::vector_types::gradient::GRADIENT_TABLE_DEFAULT_SCALE;
|
||||
use graphic_types::vector_types::vector::style::{Fill, Gradient, GradientStops, Stroke};
|
||||
use graphic_types::vector_types::vector::{self, ReferencePoint};
|
||||
use graphic_types::{Graphic, Vector};
|
||||
|
|
@ -119,9 +118,7 @@ macro_rules! tagged_value {
|
|||
x if x == TypeId::of::<()>() => TaggedValue::None,
|
||||
// Table-wrapped types need a single-item default with the element's default, not an empty table
|
||||
x if x == TypeId::of::<Table<Color>>() => TaggedValue::Color(Table::new_from_element(Color::default())),
|
||||
x if x == TypeId::of::<Table<GradientStops>>() => TaggedValue::GradientTable(Table::new_from_row(
|
||||
TableRow::new_from_element(GradientStops::default()).with_attribute(ATTR_TRANSFORM, DAffine2::from_scale(DVec2::splat(GRADIENT_TABLE_DEFAULT_SCALE))),
|
||||
)),
|
||||
x if x == TypeId::of::<Table<GradientStops>>() => TaggedValue::GradientTable(Table::new_from_element(GradientStops::default())),
|
||||
$( x if x == TypeId::of::<$ty>() => TaggedValue::$identifier(Default::default()), )*
|
||||
_ => return None,
|
||||
})
|
||||
|
|
@ -256,6 +253,7 @@ tagged_value! {
|
|||
PaintOrder(vector::style::PaintOrder),
|
||||
FillType(vector::style::FillType),
|
||||
GradientType(vector::style::GradientType),
|
||||
GradientSpreadMethod(vector::style::GradientSpreadMethod),
|
||||
ReferencePoint(vector::ReferencePoint),
|
||||
CentroidType(vector::misc::CentroidType),
|
||||
BooleanOperation(vector::misc::BooleanOperation),
|
||||
|
|
|
|||
|
|
@ -1705,9 +1705,11 @@ impl Render for Table<Color> {
|
|||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
for (color, alpha_blending) in self.iter_element_values().zip(self.iter_attribute_values_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING)) {
|
||||
render.leaf_tag("polyline", |attributes| {
|
||||
// Chrome doesn't like drawing centered rectangles bigger than ~20 million so we draw a polyline quad instead
|
||||
let max = u64::MAX;
|
||||
attributes.push("points", format!("{max},{max} -{max},{max} -{max},-{max} {max},-{max}"));
|
||||
// Stand-in for an infinite background. Chrome's SVG renderer keeps internal coordinates in f32 and loses
|
||||
// precision past ~2^24 (~16.7 million), causing tile-boundary artifacts that pop in and out during panning.
|
||||
// 1e7 stays under that limit while still being far larger than any practical document extent.
|
||||
const MAX: f64 = 1e7;
|
||||
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
|
||||
|
||||
attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma()));
|
||||
if color.a() < 1. {
|
||||
|
|
@ -1779,9 +1781,11 @@ impl Render for Table<GradientStops> {
|
|||
attributes.push("width", size.x.to_string());
|
||||
attributes.push("height", size.y.to_string());
|
||||
} else {
|
||||
// Chrome doesn't like drawing centered rectangles bigger than ~20 million so we draw a polyline quad instead
|
||||
let max = u64::MAX;
|
||||
attributes.push("points", format!("{max},{max} -{max},{max} -{max},-{max} {max},-{max}"));
|
||||
// Stand-in for an infinite background. Chrome's SVG renderer keeps internal coordinates in f32 and loses
|
||||
// precision past ~2^24 (~16.7 million), causing tile-boundary artifacts that pop in and out during panning.
|
||||
// 1e7 stays under that limit while still being far larger than any practical document extent.
|
||||
const MAX: f64 = 1e7;
|
||||
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
|
||||
}
|
||||
|
||||
let mut stop_string = String::new();
|
||||
|
|
|
|||
|
|
@ -2,10 +2,6 @@ use core_types::{Color, render_complexity::RenderComplexity};
|
|||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
/// Default scale applied to a freshly-created `Table<GradientStops>` item's transform.
|
||||
/// Places the unit gradient line (the +X unit vector in local space) inside a 100×100 document-space box.
|
||||
pub const GRADIENT_TABLE_DEFAULT_SCALE: f64 = 100.;
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ pub mod vector;
|
|||
|
||||
// Re-export commonly used types at the crate root
|
||||
pub use core_types as gcore;
|
||||
pub use gradient::{GradientStop, GradientStops, GradientType};
|
||||
pub use gradient::{GradientSpreadMethod, GradientStop, GradientStops, GradientType};
|
||||
pub use math::{QuadExt, RectExt};
|
||||
pub use subpath::Subpath;
|
||||
pub use vector::Vector;
|
||||
|
|
|
|||
|
|
@ -230,11 +230,17 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito
|
|||
|
||||
scene.append(child, Some(footprint_transform_vello));
|
||||
|
||||
// We now replace all transforms which are supposed to be infinite with a transform which covers the entire viewport
|
||||
// See <https://xi.zulipchat.com/#narrow/channel/197075-vello/topic/Full.20screen.20color.2Fgradients/near/538435044> for more detail
|
||||
// We now replace all transforms which are supposed to be infinite with a transform which covers the entire viewport.
|
||||
// See <https://xi.zulipchat.com/#narrow/channel/197075-vello/topic/Full.20screen.20color.2Fgradients/near/538435044> for more detail.
|
||||
//
|
||||
// `!is_finite()` rather than `== f32::INFINITY`: `scene.append` composes the child's `Affine::scale(INFINITY)` with
|
||||
// the viewport rotation, leaving `matrix[0] = cos(θ) * INFINITY`. In the (90°, 270°) tilt range cos is negative so
|
||||
// the result is `-INFINITY`, which the old equality check missed; Vello then rasterized a unit rect with non-finite
|
||||
// vertices, dropping the gradient and tanking performance. `!is_finite()` also covers NaN as a guard against future
|
||||
// code paths where `matrix[0]` could land on `0 * INFINITY`.
|
||||
let scaled_infinite_transform = vello::kurbo::Affine::scale_non_uniform(physical_resolution.x as f64, physical_resolution.y as f64);
|
||||
for transform in scene.encoding_mut().transforms.iter_mut() {
|
||||
if transform.matrix[0] == f32::INFINITY {
|
||||
if !transform.matrix[0].is_finite() {
|
||||
*transform = vello_encoding::Transform::from_kurbo(&scaled_infinite_transform);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -863,6 +863,24 @@ fn gradient_value(_: impl Ctx, _primary: (), gradient: Table<GradientStops>) ->
|
|||
gradient
|
||||
}
|
||||
|
||||
/// Sets the type (linear or radial) of each gradient in the input table.
|
||||
#[node_macro::node(category("Color"))]
|
||||
fn gradient_type(_: impl Ctx, mut gradient: Table<GradientStops>, gradient_type: vector_types::GradientType) -> Table<GradientStops> {
|
||||
for value in gradient.iter_attribute_values_mut_or_default::<vector_types::GradientType>(core_types::ATTR_GRADIENT_TYPE) {
|
||||
*value = gradient_type;
|
||||
}
|
||||
gradient
|
||||
}
|
||||
|
||||
/// Sets how each gradient in the input table extends past its endpoints: Pad, Reflect, or Repeat.
|
||||
#[node_macro::node(category("Color"))]
|
||||
fn spread_method(_: impl Ctx, mut gradient: Table<GradientStops>, spread_method: vector_types::GradientSpreadMethod) -> Table<GradientStops> {
|
||||
for value in gradient.iter_attribute_values_mut_or_default::<vector_types::GradientSpreadMethod>(core_types::ATTR_SPREAD_METHOD) {
|
||||
*value = spread_method;
|
||||
}
|
||||
gradient
|
||||
}
|
||||
|
||||
/// Gets the color at the specified position along the gradient, given a position from 0 (left) to 1 (right).
|
||||
#[node_macro::node(category("Color"))]
|
||||
fn sample_gradient(_: impl Ctx, _primary: (), gradient: Table<GradientStops>, position: Fraction) -> Table<Color> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue