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"
|
"*.graphite": "json"
|
||||||
},
|
},
|
||||||
"editor.renderWhitespace": "boundary",
|
"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::BlendMode;
|
||||||
use graphene_std::raster_types::Image;
|
use graphene_std::raster_types::Image;
|
||||||
use graphene_std::subpath::Subpath;
|
use graphene_std::subpath::Subpath;
|
||||||
use graphene_std::table::Table;
|
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
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};
|
use graphene_std::vector::{GradientStops, PointId, VectorModificationType};
|
||||||
|
|
||||||
#[impl_message(Message, DocumentMessage, GraphOperation)]
|
#[impl_message(Message, DocumentMessage, GraphOperation)]
|
||||||
|
|
@ -25,9 +24,22 @@ pub enum GraphOperationMessage {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
fill: f64,
|
fill: f64,
|
||||||
},
|
},
|
||||||
GradientTableSet {
|
GradientStopsSet {
|
||||||
layer: LayerNodeIdentifier,
|
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 {
|
OpacitySet {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,24 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||||
modify_inputs.blending_fill_set(fill);
|
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) {
|
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 } => {
|
GraphOperationMessage::OpacitySet { layer, opacity } => {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use graphene_std::raster_types::Image;
|
||||||
use graphene_std::subpath::Subpath;
|
use graphene_std::subpath::Subpath;
|
||||||
use graphene_std::table::Table;
|
use graphene_std::table::Table;
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
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::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType};
|
||||||
use graphene_std::{Color, Graphic, NodeInputDecleration};
|
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);
|
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 {
|
let Some(gradient_node_id) = self.existing_proto_node_id(graphene_std::math_nodes::gradient_value::IDENTIFIER, true) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let input_connector = InputConnector::node(gradient_node_id, graphene_std::math_nodes::gradient_value::GradientInput::INDEX);
|
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>) {
|
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 super::utility_types::FrontendGraphDataType;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
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::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::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface};
|
||||||
use crate::messages::portfolio::utility_types::{CachedData, FontCatalogStyle};
|
use crate::messages::portfolio::utility_types::{CachedData, FontCatalogStyle};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||||
use choice::enum_choice;
|
use choice::enum_choice;
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DVec2};
|
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::<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::<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::<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::<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(),
|
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))
|
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
|
/// Fill Node Widgets LayoutGroup
|
||||||
pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
use graphene_std::vector::fill::*;
|
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) {
|
let document_node = match get_document_node(node_id, context) {
|
||||||
Ok(document_node) => document_node,
|
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_color_fill: Fill = backup_color.clone().into();
|
||||||
let backup_gradient_fill: Fill = backup_gradient.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(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||||
widgets_first_row.push(
|
widgets_first_row.push(
|
||||||
ColorInput::default()
|
ColorInput::default()
|
||||||
|
|
@ -2089,32 +2126,12 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||||
.on_commit(commit_value)
|
.on_commit(commit_value)
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut widgets = vec![LayoutGroup::row(widgets_first_row)];
|
let mut widgets = vec![LayoutGroup::row(widgets_first_row)];
|
||||||
|
|
||||||
let fill_type_switch = {
|
let fill_type_switch = {
|
||||||
let mut row = vec![TextLabel::new("").widget_instance()];
|
let mut row = vec![TextLabel::new("").widget_instance()];
|
||||||
match fill {
|
add_blank_assist(&mut row);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let entries = vec![
|
let entries = vec![
|
||||||
RadioEntryData::new("solid")
|
RadioEntryData::new("solid")
|
||||||
|
|
@ -2137,34 +2154,9 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||||
widgets.push(fill_type_switch);
|
widgets.push(fill_type_switch);
|
||||||
|
|
||||||
if let Fill::Gradient(gradient) = fill.clone() {
|
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()];
|
let mut row = vec![TextLabel::new("").widget_instance()];
|
||||||
match gradient.gradient_type {
|
add_blank_assist(&mut row);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let gradient_for_closure = gradient.clone();
|
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));
|
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]
|
let spread_method_entries = [GradientSpreadMethod::Pad, GradientSpreadMethod::Reflect, GradientSpreadMethod::Repeat]
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -2247,8 +2265,10 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
add_blank_assist(&mut spread_methods_row);
|
spread_methods_row.extend_from_slice(&[
|
||||||
spread_methods_row.extend_from_slice(&[RadioInput::new(spread_method_entries).selected_index(Some(gradient.spread_method as u32)).widget_instance()]);
|
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));
|
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())
|
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.
|
/// Get the current fill of a layer from the closest "Fill" node.
|
||||||
pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Color> {
|
pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Color> {
|
||||||
let fill_index = 1;
|
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::node_graph::document_node_definitions::DefinitionIdentifier;
|
||||||
use crate::messages::portfolio::document::overlays::utility_types::{GizmoEmphasis, OverlayContext};
|
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::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::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 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::raster::color::Color;
|
||||||
use graphene_std::table::{Table, TableRow};
|
use graphene_std::vector::style::{Fill, FillChoice, Gradient, GradientSpreadMethod, GradientStops, GradientType};
|
||||||
use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStops, GradientType};
|
|
||||||
use graphene_std::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD, ATTR_TRANSFORM};
|
|
||||||
|
|
||||||
#[derive(Default, ExtractField)]
|
#[derive(Default, ExtractField)]
|
||||||
pub struct GradientTool {
|
pub struct GradientTool {
|
||||||
|
|
@ -49,6 +48,7 @@ pub enum GradientToolMessage {
|
||||||
CommitTransactionForColorStop,
|
CommitTransactionForColorStop,
|
||||||
CloseStopColorPicker,
|
CloseStopColorPicker,
|
||||||
UpdateStopColor { color: Color },
|
UpdateStopColor { color: Color },
|
||||||
|
UpdateStops { stops: GradientStops },
|
||||||
UpdateOptions { options: GradientOptionsUpdate },
|
UpdateOptions { options: GradientOptionsUpdate },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,6 +118,9 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
||||||
responses.add(PropertiesPanelMessage::Refresh);
|
responses.add(PropertiesPanelMessage::Refresh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ToolMessage::Gradient(GradientToolMessage::UpdateStops { stops }) => {
|
||||||
|
apply_stops_update(&mut self.data, context, responses, stops);
|
||||||
|
}
|
||||||
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
|
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
|
||||||
if self.data.color_picker_transaction_open {
|
if self.data.color_picker_transaction_open {
|
||||||
responses.add(DocumentMessage::EndTransaction);
|
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);
|
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 {
|
if has_gradient != self.data.has_selected_gradient {
|
||||||
self.data.has_selected_gradient = has_gradient;
|
self.data.has_selected_gradient = has_gradient;
|
||||||
responses.add(ToolMessage::RefreshToolOptions);
|
needs_refresh = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync tool options with the selected layer's gradient
|
let new_stops = current_gradient.as_ref().map(|gradient| gradient.stops.clone());
|
||||||
if has_gradient && let Some(gradient) = get_gradient_on_selected_layer(context.document) {
|
if self.data.current_gradient_stops != new_stops {
|
||||||
let type_differs = self.options.gradient_type != gradient.gradient_type;
|
self.data.current_gradient_stops = new_stops;
|
||||||
let spread_method_differs = self.options.spread_method != gradient.spread_method;
|
needs_refresh = true;
|
||||||
|
}
|
||||||
|
|
||||||
if type_differs {
|
let new_orientation = match (current_layer, ¤t_gradient) {
|
||||||
self.options.gradient_type = gradient.gradient_type;
|
(Some(layer), Some(gradient)) => {
|
||||||
}
|
let transform = gradient_space_transform(layer, context.document);
|
||||||
if spread_method_differs {
|
graph_modification_utils::gradient_orientation_rightward(gradient.start, gradient.end, transform)
|
||||||
self.options.spread_method = gradient.spread_method;
|
|
||||||
}
|
|
||||||
if type_differs || spread_method_differs {
|
|
||||||
responses.add(ToolMessage::RefreshToolOptions);
|
|
||||||
}
|
}
|
||||||
|
_ => 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))
|
.selected_index(Some((self.options.gradient_type == GradientType::Radial) as u32))
|
||||||
.widget_instance();
|
.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)
|
let reverse_stops = IconButton::new("Reverse", 24)
|
||||||
.tooltip_label("Reverse Stops")
|
.tooltip_label("Reverse Stops")
|
||||||
|
|
@ -221,24 +261,12 @@ impl LayoutHolder for GradientTool {
|
||||||
.selected_index(Some(self.options.spread_method as u32))
|
.selected_index(Some(self.options.spread_method as u32))
|
||||||
.widget_instance();
|
.widget_instance();
|
||||||
|
|
||||||
widgets.extend([spread_method, Separator::new(SeparatorStyle::Unrelated).widget_instance(), reverse_stops]);
|
let reverse_direction_icon = if self.data.gradient_orientation_rightward {
|
||||||
|
"ReverseRadialGradientToRight"
|
||||||
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
|
|
||||||
} else {
|
} else {
|
||||||
(start.x + start.y) < (end.x + end.y)
|
"ReverseRadialGradientToLeft"
|
||||||
}
|
};
|
||||||
})
|
let reverse_direction = IconButton::new(reverse_direction_icon, 24)
|
||||||
.unwrap_or(true);
|
|
||||||
|
|
||||||
let reverse_direction = IconButton::new(if orientation { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24)
|
|
||||||
.tooltip_label("Reverse Direction")
|
.tooltip_label("Reverse Direction")
|
||||||
.tooltip_description("Reverse which end the gradient radiates from.")
|
.tooltip_description("Reverse which end the gradient radiates from.")
|
||||||
.disabled(!self.data.has_selected_gradient)
|
.disabled(!self.data.has_selected_gradient)
|
||||||
|
|
@ -250,9 +278,17 @@ impl LayoutHolder for GradientTool {
|
||||||
})
|
})
|
||||||
.widget_instance();
|
.widget_instance();
|
||||||
|
|
||||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
widgets.extend([
|
||||||
widgets.push(reverse_direction);
|
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)])
|
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 {
|
fn gradient_space_transform(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> DAffine2 {
|
||||||
// TODO: Drop the `is_gradient_table` branch once all gradients are `Table<GradientStops>`
|
graph_modification_utils::gradient_space_transform(layer, &document.network_interface)
|
||||||
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])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove this whole function once all gradients are `Table<GradientStops>`
|
// TODO: Remove this whole function once all gradients are `Table<GradientStops>`
|
||||||
fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
|
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)) {
|
if let Some(stops_table) = get_gradient_table(layer, network_interface) {
|
||||||
(Some(gradient_graphic), _) => {
|
let stops = stops_table.element(0).cloned().unwrap_or_default();
|
||||||
let stops = gradient_graphic.element(0)?.clone();
|
let GradientChainState {
|
||||||
let transform: DAffine2 = gradient_graphic.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
transform,
|
||||||
let spread_method: GradientSpreadMethod = gradient_graphic.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0);
|
gradient_type,
|
||||||
let gradient_type: GradientType = gradient_graphic.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0);
|
spread_method,
|
||||||
let gradient = Gradient {
|
} = read_gradient_chain_state(layer, network_interface);
|
||||||
|
return Some(Gradient {
|
||||||
stops,
|
stops,
|
||||||
gradient_type,
|
gradient_type,
|
||||||
spread_method,
|
spread_method,
|
||||||
start: transform.transform_point2(DVec2::ZERO),
|
start: transform.transform_point2(DVec2::ZERO),
|
||||||
end: transform.transform_point2(DVec2::X),
|
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.
|
/// 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 {
|
if let Some(layer) = self.layer {
|
||||||
// TODO: Drop the `Fill::Gradient` branch when all gradients become `Table<GradientStops>`
|
// TODO: Drop the `Fill::Gradient` branch when all gradients become `Table<GradientStops>`
|
||||||
if self.is_gradient_table {
|
if self.is_gradient_table {
|
||||||
let gradient_table = Table::new_from_row(
|
dispatch_gradient_writes(layer, &self.gradient, responses);
|
||||||
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 });
|
|
||||||
} else {
|
} else {
|
||||||
responses.add(GraphOperationMessage::FillSet {
|
responses.add(GraphOperationMessage::FillSet {
|
||||||
layer,
|
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 {
|
impl GradientTool {
|
||||||
/// Get the gradient type of the selected gradient (if it exists)
|
/// Get the gradient type of the selected gradient (if it exists)
|
||||||
pub fn selected_gradient(&self) -> Option<GradientType> {
|
pub fn selected_gradient(&self) -> Option<GradientType> {
|
||||||
|
|
@ -669,6 +764,12 @@ struct GradientToolData {
|
||||||
auto_pan_shift: DVec2,
|
auto_pan_shift: DVec2,
|
||||||
gradient_angle: f64,
|
gradient_angle: f64,
|
||||||
has_selected_gradient: bool,
|
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_editing_color_stop: Option<usize>,
|
||||||
color_picker_transaction_open: bool,
|
color_picker_transaction_open: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -1251,7 +1352,15 @@ impl Fsm for GradientToolFsmState {
|
||||||
GradientToolFsmState::Drawing { drag_hint: hint }
|
GradientToolFsmState::Drawing { drag_hint: hint }
|
||||||
} else {
|
} else {
|
||||||
let document_mouse = document.metadata().document_to_viewport.inverse().transform_point2(mouse);
|
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
|
// Apply the gradient to the selected layer
|
||||||
if let Some(layer) = 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
|
// 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>`
|
// TODO: Drop the `Fill::Gradient` branch when all gradients become `Table<GradientStops>`
|
||||||
if get_gradient_table(layer, &context.document.network_interface).is_some() {
|
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
|
dispatch_gradient_writes(layer, &gradient, responses);
|
||||||
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 });
|
|
||||||
} else {
|
} else {
|
||||||
responses.add(GraphOperationMessage::FillSet {
|
responses.add(GraphOperationMessage::FillSet {
|
||||||
layer,
|
layer,
|
||||||
|
|
@ -1646,6 +1748,50 @@ fn apply_gradient_update(
|
||||||
responses.add(ToolMessage::RefreshToolOptions);
|
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> {
|
fn get_gradient_on_selected_layer(document: &DocumentMessageHandler) -> Option<Gradient> {
|
||||||
document
|
document
|
||||||
.network_interface
|
.network_interface
|
||||||
|
|
@ -2176,12 +2322,7 @@ mod test_gradient {
|
||||||
// Create original transform for the control geometry and apply it
|
// Create original transform for the control geometry and apply it
|
||||||
let initial_start = DVec2::new(10., 50.);
|
let initial_start = DVec2::new(10., 50.);
|
||||||
let initial_end = DVec2::new(200., 50.);
|
let initial_end = DVec2::new(200., 50.);
|
||||||
let initial_item_transform = super::gradient_item_transform(initial_start, initial_end);
|
let stops = GradientStops::new([
|
||||||
editor
|
|
||||||
.handle_message(GraphOperationMessage::GradientTableSet {
|
|
||||||
layer,
|
|
||||||
gradient_table: Table::new_from_row(
|
|
||||||
TableRow::new_from_element(GradientStops::new([
|
|
||||||
GradientStop {
|
GradientStop {
|
||||||
position: 0.,
|
position: 0.,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
|
|
@ -2192,9 +2333,13 @@ mod test_gradient {
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
color: Color::BLUE,
|
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;
|
.await;
|
||||||
|
|
||||||
|
|
@ -2266,11 +2411,12 @@ mod test_gradient {
|
||||||
]);
|
]);
|
||||||
let initial_start = DVec2::new(10., 50.);
|
let initial_start = DVec2::new(10., 50.);
|
||||||
let initial_end = DVec2::new(200., 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
|
editor
|
||||||
.handle_message(GraphOperationMessage::GradientTableSet {
|
.handle_message(GraphOperationMessage::GradientLineSet {
|
||||||
layer,
|
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;
|
.await;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,15 @@ use crate::application_io::PlatformEditorApi;
|
||||||
use crate::proto::{Any as DAny, FutureAny};
|
use crate::proto::{Any as DAny, FutureAny};
|
||||||
use brush_nodes::brush_cache::BrushCache;
|
use brush_nodes::brush_cache::BrushCache;
|
||||||
use brush_nodes::brush_stroke::BrushStroke;
|
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::transform::Footprint;
|
||||||
use core_types::uuid::NodeId;
|
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;
|
use dyn_any::DynAny;
|
||||||
pub use dyn_any::StaticType;
|
pub use dyn_any::StaticType;
|
||||||
use glam::{Affine2, Vec2};
|
use glam::{Affine2, Vec2};
|
||||||
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
|
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
|
||||||
use graphic_types::raster_types::{CPU, Image, Raster};
|
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::style::{Fill, Gradient, GradientStops, Stroke};
|
||||||
use graphic_types::vector_types::vector::{self, ReferencePoint};
|
use graphic_types::vector_types::vector::{self, ReferencePoint};
|
||||||
use graphic_types::{Graphic, Vector};
|
use graphic_types::{Graphic, Vector};
|
||||||
|
|
@ -119,9 +118,7 @@ macro_rules! tagged_value {
|
||||||
x if x == TypeId::of::<()>() => TaggedValue::None,
|
x if x == TypeId::of::<()>() => TaggedValue::None,
|
||||||
// Table-wrapped types need a single-item default with the element's default, not an empty table
|
// 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<Color>>() => TaggedValue::Color(Table::new_from_element(Color::default())),
|
||||||
x if x == TypeId::of::<Table<GradientStops>>() => TaggedValue::GradientTable(Table::new_from_row(
|
x if x == TypeId::of::<Table<GradientStops>>() => TaggedValue::GradientTable(Table::new_from_element(GradientStops::default())),
|
||||||
TableRow::new_from_element(GradientStops::default()).with_attribute(ATTR_TRANSFORM, DAffine2::from_scale(DVec2::splat(GRADIENT_TABLE_DEFAULT_SCALE))),
|
|
||||||
)),
|
|
||||||
$( x if x == TypeId::of::<$ty>() => TaggedValue::$identifier(Default::default()), )*
|
$( x if x == TypeId::of::<$ty>() => TaggedValue::$identifier(Default::default()), )*
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
|
|
@ -256,6 +253,7 @@ tagged_value! {
|
||||||
PaintOrder(vector::style::PaintOrder),
|
PaintOrder(vector::style::PaintOrder),
|
||||||
FillType(vector::style::FillType),
|
FillType(vector::style::FillType),
|
||||||
GradientType(vector::style::GradientType),
|
GradientType(vector::style::GradientType),
|
||||||
|
GradientSpreadMethod(vector::style::GradientSpreadMethod),
|
||||||
ReferencePoint(vector::ReferencePoint),
|
ReferencePoint(vector::ReferencePoint),
|
||||||
CentroidType(vector::misc::CentroidType),
|
CentroidType(vector::misc::CentroidType),
|
||||||
BooleanOperation(vector::misc::BooleanOperation),
|
BooleanOperation(vector::misc::BooleanOperation),
|
||||||
|
|
|
||||||
|
|
@ -1705,9 +1705,11 @@ impl Render for Table<Color> {
|
||||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
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)) {
|
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| {
|
render.leaf_tag("polyline", |attributes| {
|
||||||
// Chrome doesn't like drawing centered rectangles bigger than ~20 million so we draw a polyline quad instead
|
// Stand-in for an infinite background. Chrome's SVG renderer keeps internal coordinates in f32 and loses
|
||||||
let max = u64::MAX;
|
// precision past ~2^24 (~16.7 million), causing tile-boundary artifacts that pop in and out during panning.
|
||||||
attributes.push("points", format!("{max},{max} -{max},{max} -{max},-{max} {max},-{max}"));
|
// 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()));
|
attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma()));
|
||||||
if color.a() < 1. {
|
if color.a() < 1. {
|
||||||
|
|
@ -1779,9 +1781,11 @@ impl Render for Table<GradientStops> {
|
||||||
attributes.push("width", size.x.to_string());
|
attributes.push("width", size.x.to_string());
|
||||||
attributes.push("height", size.y.to_string());
|
attributes.push("height", size.y.to_string());
|
||||||
} else {
|
} else {
|
||||||
// Chrome doesn't like drawing centered rectangles bigger than ~20 million so we draw a polyline quad instead
|
// Stand-in for an infinite background. Chrome's SVG renderer keeps internal coordinates in f32 and loses
|
||||||
let max = u64::MAX;
|
// precision past ~2^24 (~16.7 million), causing tile-boundary artifacts that pop in and out during panning.
|
||||||
attributes.push("points", format!("{max},{max} -{max},{max} -{max},-{max} {max},-{max}"));
|
// 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();
|
let mut stop_string = String::new();
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,6 @@ use core_types::{Color, render_complexity::RenderComplexity};
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DVec2};
|
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))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
|
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[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
|
// Re-export commonly used types at the crate root
|
||||||
pub use core_types as gcore;
|
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 math::{QuadExt, RectExt};
|
||||||
pub use subpath::Subpath;
|
pub use subpath::Subpath;
|
||||||
pub use vector::Vector;
|
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));
|
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
|
// 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
|
// 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);
|
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() {
|
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);
|
*transform = vello_encoding::Transform::from_kurbo(&scaled_infinite_transform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -863,6 +863,24 @@ fn gradient_value(_: impl Ctx, _primary: (), gradient: Table<GradientStops>) ->
|
||||||
gradient
|
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).
|
/// Gets the color at the specified position along the gradient, given a position from 0 (left) to 1 (right).
|
||||||
#[node_macro::node(category("Color"))]
|
#[node_macro::node(category("Color"))]
|
||||||
fn sample_gradient(_: impl Ctx, _primary: (), gradient: Table<GradientStops>, position: Fraction) -> Table<Color> {
|
fn sample_gradient(_: impl Ctx, _primary: (), gradient: Table<GradientStops>, position: Fraction) -> Table<Color> {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue