Add Table<GradientStops> gradient rendering (#3989)
* Add Table<GradientStops> gradient rendering * Add SVG and Vello renderers for Table<GradientStops> * Add thumbnail rendering for Table<GradientStops> * Use row transform to map (0,0), (1,0) unit line to document space * Set 100px width for the initially created gradient * Add support of table gradients for the gradient tool * Fix after review * Thumbnail rendering of artboard with infinite gradient layer * Hide radial gradient's reverse direction button for gradient table * Remove unused imports * Format * Fix conflict with spread method * Code review * Fix thumbnails * Connect up gradient_type and spread_method to attributes --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
8cadafa063
commit
e686ee9f42
|
|
@ -4441,6 +4441,7 @@ dependencies = [
|
||||||
"usvg",
|
"usvg",
|
||||||
"vector-types",
|
"vector-types",
|
||||||
"vello",
|
"vello",
|
||||||
|
"vello_encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
// Bumped past the default 128 because the deeply-generic message-passing types pull in wgpu/naga
|
||||||
|
// trait chains that overflow the trait resolver under `--tests`. Set to the same value the compiler suggests.
|
||||||
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
extern crate graphite_proc_macros;
|
extern crate graphite_proc_macros;
|
||||||
|
|
||||||
// `macro_use` puts these macros into scope for all descendant code files
|
// `macro_use` puts these macros into scope for all descendant code files
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,10 @@ 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::PointId;
|
|
||||||
use graphene_std::vector::VectorModificationType;
|
|
||||||
use graphene_std::vector::style::{Fill, Stroke};
|
use graphene_std::vector::style::{Fill, Stroke};
|
||||||
|
use graphene_std::vector::{GradientStops, PointId, VectorModificationType};
|
||||||
|
|
||||||
#[impl_message(Message, DocumentMessage, GraphOperation)]
|
#[impl_message(Message, DocumentMessage, GraphOperation)]
|
||||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
|
@ -25,6 +25,10 @@ pub enum GraphOperationMessage {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
fill: f64,
|
fill: f64,
|
||||||
},
|
},
|
||||||
|
GradientTableSet {
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
gradient_table: Table<GradientStops>,
|
||||||
|
},
|
||||||
OpacitySet {
|
OpacitySet {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
opacity: f64,
|
opacity: f64,
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||||
modify_inputs.blending_fill_set(fill);
|
modify_inputs.blending_fill_set(fill);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GraphOperationMessage::GradientTableSet { layer, gradient_table } => {
|
||||||
|
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||||
|
modify_inputs.gradient_table_set(gradient_table);
|
||||||
|
}
|
||||||
|
}
|
||||||
GraphOperationMessage::OpacitySet { layer, opacity } => {
|
GraphOperationMessage::OpacitySet { layer, opacity } => {
|
||||||
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.opacity_set(opacity);
|
modify_inputs.opacity_set(opacity);
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,8 @@ 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::Vector;
|
|
||||||
use graphene_std::vector::style::{Fill, Stroke};
|
use graphene_std::vector::style::{Fill, Stroke};
|
||||||
use graphene_std::vector::{PointId, VectorModification, VectorModificationType};
|
use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType};
|
||||||
use graphene_std::{Color, Graphic, NodeInputDecleration};
|
use graphene_std::{Color, Graphic, NodeInputDecleration};
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
|
@ -461,6 +460,15 @@ 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>) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
|
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
|
||||||
let clip = !clip_mode.unwrap_or(false);
|
let clip = !clip_mode.unwrap_or(false);
|
||||||
let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blending::IDENTIFIER, true) else {
|
let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blending::IDENTIFIER, true) else {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use glam::{DAffine2, DVec2};
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
||||||
use graph_craft::{Type, concrete};
|
use graph_craft::{Type, concrete};
|
||||||
|
use graphene_std::ATTR_TRANSFORM;
|
||||||
use graphene_std::NodeInputDecleration;
|
use graphene_std::NodeInputDecleration;
|
||||||
use graphene_std::animation::RealTimeMode;
|
use graphene_std::animation::RealTimeMode;
|
||||||
use graphene_std::extract_xy::XY;
|
use graphene_std::extract_xy::XY;
|
||||||
|
|
@ -1154,20 +1155,33 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
|
||||||
.on_commit(commit_value)
|
.on_commit(commit_value)
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
),
|
),
|
||||||
TaggedValue::GradientTable(gradient_table) => widgets.push(
|
TaggedValue::GradientTable(gradient_table) => {
|
||||||
color_button
|
let existing_transform: DAffine2 = gradient_table.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||||
.value(match gradient_table.element(0) {
|
|
||||||
Some(gradient) => FillChoice::Gradient(gradient.clone()),
|
widgets.push(
|
||||||
None => FillChoice::Gradient(GradientStops::default()),
|
color_button
|
||||||
})
|
.value(match gradient_table.element(0) {
|
||||||
.on_update(update_value(
|
Some(gradient) => FillChoice::Gradient(gradient.clone()),
|
||||||
|input: &ColorInput| TaggedValue::GradientTable(input.value.as_gradient().iter().map(|&gradient| TableRow::new_from_element(gradient.clone())).collect()),
|
None => FillChoice::Gradient(GradientStops::default()),
|
||||||
node_id,
|
})
|
||||||
index,
|
.on_update(update_value(
|
||||||
))
|
move |input: &ColorInput| {
|
||||||
.on_commit(commit_value)
|
TaggedValue::GradientTable(
|
||||||
.widget_instance(),
|
input
|
||||||
),
|
.value
|
||||||
|
.as_gradient()
|
||||||
|
.iter()
|
||||||
|
.map(|&gradient| TableRow::new_from_element(gradient.clone()).with_attribute(ATTR_TRANSFORM, existing_transform))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
node_id,
|
||||||
|
index,
|
||||||
|
))
|
||||||
|
.on_commit(commit_value)
|
||||||
|
.widget_instance(),
|
||||||
|
)
|
||||||
|
}
|
||||||
x => warn!("Color {x:?}"),
|
x => warn!("Color {x:?}"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ use graphene_std::table::Table;
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
use graphene_std::text::{Font, TypesettingConfig};
|
||||||
use graphene_std::vector::misc::ManipulatorPointId;
|
use graphene_std::vector::misc::ManipulatorPointId;
|
||||||
use graphene_std::vector::style::{Fill, Gradient};
|
use graphene_std::vector::style::{Fill, Gradient};
|
||||||
use graphene_std::vector::{PointId, SegmentId, VectorModificationType};
|
use graphene_std::vector::{GradientStops, PointId, SegmentId, VectorModificationType};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
/// Returns the ID of the first Spline node in the horizontal flow which is not followed by a `Path` node, or `None` if none exists.
|
/// Returns the ID of the first Spline node in the horizontal flow which is not followed by a `Path` node, or `None` if none exists.
|
||||||
|
|
@ -280,6 +280,15 @@ pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
|
||||||
Some(gradient.clone())
|
Some(gradient.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the gradient table of a layer.
|
||||||
|
pub fn get_gradient_table(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Table<GradientStops>> {
|
||||||
|
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER))?;
|
||||||
|
let TaggedValue::GradientTable(gradient_table) = inputs.get(graphene_std::math_nodes::gradient_value::GradientInput::INDEX)?.as_value()? else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(gradient_table.clone())
|
||||||
|
}
|
||||||
|
|
||||||
/// 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;
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,17 @@ use crate::consts::{
|
||||||
COLOR_OVERLAY_BLUE, DRAG_THRESHOLD, GRADIENT_MIDPOINT_DIAMOND_RADIUS, GRADIENT_MIDPOINT_MAX, GRADIENT_MIDPOINT_MIN, GRADIENT_STOP_MIN_VIEWPORT_GAP, LINE_ROTATE_SNAP_ANGLE,
|
COLOR_OVERLAY_BLUE, DRAG_THRESHOLD, GRADIENT_MIDPOINT_DIAMOND_RADIUS, GRADIENT_MIDPOINT_MAX, GRADIENT_MIDPOINT_MIN, GRADIENT_STOP_MIN_VIEWPORT_GAP, LINE_ROTATE_SNAP_ANGLE,
|
||||||
MANIPULATOR_GROUP_MARKER_SIZE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD,
|
MANIPULATOR_GROUP_MARKER_SIZE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD,
|
||||||
};
|
};
|
||||||
|
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::tool::common_functionality::auto_panning::AutoPanning;
|
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_gradient};
|
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::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
||||||
use graphene_std::raster::color::Color;
|
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::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 {
|
||||||
|
|
@ -131,7 +135,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync tool options with the selected layer's gradient
|
// Sync tool options with the selected layer's gradient
|
||||||
if has_gradient && let Some(gradient) = get_gradient_on_selected_layer(&context.document) {
|
if has_gradient && let Some(gradient) = get_gradient_on_selected_layer(context.document) {
|
||||||
let type_differs = self.options.gradient_type != gradient.gradient_type;
|
let type_differs = self.options.gradient_type != gradient.gradient_type;
|
||||||
let spread_method_differs = self.options.spread_method != gradient.spread_method;
|
let spread_method_differs = self.options.spread_method != gradient.spread_method;
|
||||||
|
|
||||||
|
|
@ -161,6 +165,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
||||||
|
|
||||||
impl LayoutHolder for GradientTool {
|
impl LayoutHolder for GradientTool {
|
||||||
fn layout(&self) -> Layout {
|
fn layout(&self) -> Layout {
|
||||||
|
let mut widgets: Vec<WidgetInstance> = Vec::new();
|
||||||
|
|
||||||
let gradient_type = RadioInput::new(vec![
|
let gradient_type = RadioInput::new(vec![
|
||||||
RadioEntryData::new("Linear").label("Linear").tooltip_label("Linear Gradient").on_update(move |_| {
|
RadioEntryData::new("Linear").label("Linear").tooltip_label("Linear Gradient").on_update(move |_| {
|
||||||
GradientToolMessage::UpdateOptions {
|
GradientToolMessage::UpdateOptions {
|
||||||
|
|
@ -178,6 +184,8 @@ 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 reverse_stops = IconButton::new("Reverse", 24)
|
let reverse_stops = IconButton::new("Reverse", 24)
|
||||||
.tooltip_label("Reverse Stops")
|
.tooltip_label("Reverse Stops")
|
||||||
.tooltip_description("Reverse the gradient color stops.")
|
.tooltip_description("Reverse the gradient color stops.")
|
||||||
|
|
@ -191,19 +199,19 @@ impl LayoutHolder for GradientTool {
|
||||||
.widget_instance();
|
.widget_instance();
|
||||||
|
|
||||||
let spread_method = RadioInput::new(vec![
|
let spread_method = RadioInput::new(vec![
|
||||||
RadioEntryData::new("Pad").label("Pad").tooltip_label("Pad").on_update(move |_| {
|
RadioEntryData::new("Pad").label("Pad").tooltip_label("Pad Spread Method").on_update(move |_| {
|
||||||
GradientToolMessage::UpdateOptions {
|
GradientToolMessage::UpdateOptions {
|
||||||
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Pad),
|
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Pad),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
RadioEntryData::new("Reflect").label("Reflect").tooltip_label("Reflect").on_update(move |_| {
|
RadioEntryData::new("Reflect").label("Reflect").tooltip_label("Reflect Spread Method").on_update(move |_| {
|
||||||
GradientToolMessage::UpdateOptions {
|
GradientToolMessage::UpdateOptions {
|
||||||
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Reflect),
|
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Reflect),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
RadioEntryData::new("Repeat").label("Repeat").tooltip_label("Repeat").on_update(move |_| {
|
RadioEntryData::new("Repeat").label("Repeat").tooltip_label("Repeat Spread Method").on_update(move |_| {
|
||||||
GradientToolMessage::UpdateOptions {
|
GradientToolMessage::UpdateOptions {
|
||||||
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Repeat),
|
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Repeat),
|
||||||
}
|
}
|
||||||
|
|
@ -213,13 +221,7 @@ 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();
|
||||||
|
|
||||||
let mut widgets = vec![
|
widgets.extend([spread_method, Separator::new(SeparatorStyle::Unrelated).widget_instance(), reverse_stops]);
|
||||||
gradient_type,
|
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
|
||||||
spread_method,
|
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
|
||||||
reverse_stops,
|
|
||||||
];
|
|
||||||
|
|
||||||
if self.options.gradient_type == GradientType::Radial {
|
if self.options.gradient_type == GradientType::Radial {
|
||||||
let orientation = self
|
let orientation = self
|
||||||
|
|
@ -273,14 +275,63 @@ 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 {
|
||||||
let bounds = document.metadata().nonzero_bounding_box(layer);
|
// TODO: Drop the `is_gradient_table` branch once all gradients are `Table<GradientStops>`
|
||||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
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 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
|
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>`
|
||||||
|
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 {
|
||||||
|
stops,
|
||||||
|
gradient_type,
|
||||||
|
spread_method,
|
||||||
|
start: transform.transform_point2(DVec2::ZERO),
|
||||||
|
end: transform.transform_point2(DVec2::X),
|
||||||
|
};
|
||||||
|
Some(gradient)
|
||||||
|
}
|
||||||
|
(None, Some(gradient)) => Some(gradient),
|
||||||
|
(None, None) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
fn midpoint_hidden_by_proximity(left_stop_pos: f64, right_stop_pos: f64, viewport_line_length: f64) -> bool {
|
fn midpoint_hidden_by_proximity(left_stop_pos: f64, right_stop_pos: f64, viewport_line_length: f64) -> bool {
|
||||||
(right_stop_pos - left_stop_pos) * viewport_line_length < GRADIENT_STOP_MIN_VIEWPORT_GAP * 2.
|
(right_stop_pos - left_stop_pos) * viewport_line_length < GRADIENT_STOP_MIN_VIEWPORT_GAP * 2.
|
||||||
|
|
@ -304,6 +355,8 @@ struct SelectedGradient {
|
||||||
gradient: Gradient,
|
gradient: Gradient,
|
||||||
dragging: GradientDragTarget,
|
dragging: GradientDragTarget,
|
||||||
initial_gradient: Gradient,
|
initial_gradient: Gradient,
|
||||||
|
// TODO: Remove (and the matching branches in `render_gradient` / pointer-up) once `Table<GradientStops>` replaces legacy `Fill::Gradient`
|
||||||
|
is_gradient_table: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: DVec2) -> Option<f64> {
|
fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: DVec2) -> Option<f64> {
|
||||||
|
|
@ -347,12 +400,14 @@ fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: D
|
||||||
impl SelectedGradient {
|
impl SelectedGradient {
|
||||||
pub fn new(gradient: Gradient, layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Self {
|
pub fn new(gradient: Gradient, layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Self {
|
||||||
let transform = gradient_space_transform(layer, document);
|
let transform = gradient_space_transform(layer, document);
|
||||||
|
let is_gradient_table = get_gradient_table(layer, &document.network_interface).is_some();
|
||||||
Self {
|
Self {
|
||||||
layer: Some(layer),
|
layer: Some(layer),
|
||||||
transform,
|
transform,
|
||||||
gradient: gradient.clone(),
|
gradient: gradient.clone(),
|
||||||
dragging: GradientDragTarget::End,
|
dragging: GradientDragTarget::End,
|
||||||
initial_gradient: gradient,
|
initial_gradient: gradient,
|
||||||
|
is_gradient_table,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -568,10 +623,21 @@ impl SelectedGradient {
|
||||||
/// Update the layer fill to the current gradient
|
/// Update the layer fill to the current gradient
|
||||||
pub fn render_gradient(&mut self, responses: &mut VecDeque<Message>) {
|
pub fn render_gradient(&mut self, responses: &mut VecDeque<Message>) {
|
||||||
if let Some(layer) = self.layer {
|
if let Some(layer) = self.layer {
|
||||||
responses.add(GraphOperationMessage::FillSet {
|
// TODO: Drop the `Fill::Gradient` branch when all gradients become `Table<GradientStops>`
|
||||||
layer,
|
if self.is_gradient_table {
|
||||||
fill: Fill::Gradient(self.gradient.clone()),
|
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 });
|
||||||
|
} else {
|
||||||
|
responses.add(GraphOperationMessage::FillSet {
|
||||||
|
layer,
|
||||||
|
fill: Fill::Gradient(self.gradient.clone()),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -952,8 +1018,11 @@ impl Fsm for GradientToolFsmState {
|
||||||
};
|
};
|
||||||
|
|
||||||
// The gradient has only one point and so should become a fill
|
// The gradient has only one point and so should become a fill
|
||||||
|
// TODO: Drop the legacy `Fill::Solid` branch when all gradients become `Table<GradientStops>`
|
||||||
if selected_gradient.gradient.stops.len() == 1 {
|
if selected_gradient.gradient.stops.len() == 1 {
|
||||||
if let Some(layer) = selected_gradient.layer {
|
if selected_gradient.is_gradient_table {
|
||||||
|
selected_gradient.render_gradient(responses);
|
||||||
|
} else if let Some(layer) = selected_gradient.layer {
|
||||||
responses.add(GraphOperationMessage::FillSet {
|
responses.add(GraphOperationMessage::FillSet {
|
||||||
layer,
|
layer,
|
||||||
fill: Fill::Solid(selected_gradient.gradient.stops.color[0]),
|
fill: Fill::Solid(selected_gradient.gradient.stops.color[0]),
|
||||||
|
|
@ -1047,6 +1116,7 @@ impl Fsm for GradientToolFsmState {
|
||||||
for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) {
|
for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) {
|
||||||
let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue };
|
let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue };
|
||||||
let transform = gradient_space_transform(layer, document);
|
let transform = gradient_space_transform(layer, document);
|
||||||
|
let is_gradient_table = get_gradient_table(layer, &document.network_interface).is_some();
|
||||||
|
|
||||||
// Check for dragging a midpoint diamond
|
// Check for dragging a midpoint diamond
|
||||||
if drag_hint.is_none() {
|
if drag_hint.is_none() {
|
||||||
|
|
@ -1074,6 +1144,7 @@ impl Fsm for GradientToolFsmState {
|
||||||
gradient: gradient.clone(),
|
gradient: gradient.clone(),
|
||||||
dragging: GradientDragTarget::Midpoint(i),
|
dragging: GradientDragTarget::Midpoint(i),
|
||||||
initial_gradient: gradient.clone(),
|
initial_gradient: gradient.clone(),
|
||||||
|
is_gradient_table,
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
@ -1114,6 +1185,7 @@ impl Fsm for GradientToolFsmState {
|
||||||
gradient: gradient.clone(),
|
gradient: gradient.clone(),
|
||||||
dragging: drag_target,
|
dragging: drag_target,
|
||||||
initial_gradient: gradient.clone(),
|
initial_gradient: gradient.clone(),
|
||||||
|
is_gradient_table,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1130,6 +1202,7 @@ impl Fsm for GradientToolFsmState {
|
||||||
gradient: gradient.clone(),
|
gradient: gradient.clone(),
|
||||||
dragging: dragging_target,
|
dragging: dragging_target,
|
||||||
initial_gradient: gradient.clone(),
|
initial_gradient: gradient.clone(),
|
||||||
|
is_gradient_table,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1538,10 +1611,24 @@ fn apply_gradient_update(
|
||||||
transaction_started = true;
|
transaction_started = true;
|
||||||
}
|
}
|
||||||
update(&mut gradient);
|
update(&mut gradient);
|
||||||
responses.add(GraphOperationMessage::FillSet {
|
|
||||||
layer,
|
// Only check for the gradient table once we know we'll write back, since this is a graph traversal per layer
|
||||||
fill: Fill::Gradient(gradient),
|
// 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 });
|
||||||
|
} else {
|
||||||
|
responses.add(GraphOperationMessage::FillSet {
|
||||||
|
layer,
|
||||||
|
fill: Fill::Gradient(gradient),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1623,11 +1710,14 @@ mod test_gradient {
|
||||||
use crate::messages::input_mapper::utility_types::input_mouse::ScrollDelta;
|
use crate::messages::input_mapper::utility_types::input_mouse::ScrollDelta;
|
||||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||||
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
||||||
|
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector};
|
||||||
pub use crate::test_utils::test_prelude::*;
|
pub use crate::test_utils::test_prelude::*;
|
||||||
use glam::DAffine2;
|
use glam::DAffine2;
|
||||||
use graphene_std::vector::fill;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graphene_std::vector::style::Fill;
|
use graphene_std::ATTR_TRANSFORM;
|
||||||
use graphene_std::vector::style::Gradient;
|
use graphene_std::table::{Table, TableRow};
|
||||||
|
use graphene_std::vector::style::{Fill, Gradient};
|
||||||
|
use graphene_std::vector::{GradientStop, GradientStops, fill};
|
||||||
|
|
||||||
use super::gradient_space_transform;
|
use super::gradient_space_transform;
|
||||||
|
|
||||||
|
|
@ -1672,6 +1762,45 @@ mod test_gradient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_gradient_table_layer(editor: &mut EditorTestUtils) -> LayerNodeIdentifier {
|
||||||
|
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||||
|
let document = editor.active_document();
|
||||||
|
let layer = document.metadata().all_layers().next().unwrap();
|
||||||
|
|
||||||
|
let gradient_node_id = editor.create_node_by_name(DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER)).await;
|
||||||
|
|
||||||
|
editor
|
||||||
|
.handle_message(NodeGraphMessage::CreateWire {
|
||||||
|
output_connector: OutputConnector::node(gradient_node_id, 0),
|
||||||
|
input_connector: InputConnector::node(layer.to_node(), 1),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
editor
|
||||||
|
.handle_message(NodeGraphMessage::SetInputValue {
|
||||||
|
node_id: gradient_node_id,
|
||||||
|
input_index: 1,
|
||||||
|
value: TaggedValue::GradientTable(Table::new_from_row(
|
||||||
|
TableRow::new_from_element(GradientStops::new([
|
||||||
|
GradientStop {
|
||||||
|
position: 0.,
|
||||||
|
midpoint: 0.5,
|
||||||
|
color: Color::RED,
|
||||||
|
},
|
||||||
|
GradientStop {
|
||||||
|
position: 1.,
|
||||||
|
midpoint: 0.5,
|
||||||
|
color: Color::BLUE,
|
||||||
|
},
|
||||||
|
]))
|
||||||
|
.with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY),
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
layer
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn ignore_artboard() {
|
async fn ignore_artboard() {
|
||||||
let mut editor = EditorTestUtils::create();
|
let mut editor = EditorTestUtils::create();
|
||||||
|
|
@ -2037,4 +2166,146 @@ mod test_gradient {
|
||||||
let (gradient, _) = get_gradient(&mut editor).await;
|
let (gradient, _) = get_gradient(&mut editor).await;
|
||||||
assert_eq!(gradient.spread_method, GradientSpreadMethod::Reflect);
|
assert_eq!(gradient.spread_method, GradientSpreadMethod::Reflect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn gradient_table_drag_endpoint() {
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
let layer = create_gradient_table_layer(&mut editor).await;
|
||||||
|
|
||||||
|
// 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([
|
||||||
|
GradientStop {
|
||||||
|
position: 0.,
|
||||||
|
midpoint: 0.5,
|
||||||
|
color: Color::RED,
|
||||||
|
},
|
||||||
|
GradientStop {
|
||||||
|
position: 1.,
|
||||||
|
midpoint: 0.5,
|
||||||
|
color: Color::BLUE,
|
||||||
|
},
|
||||||
|
]))
|
||||||
|
.with_attribute(ATTR_TRANSFORM, initial_item_transform),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
|
||||||
|
|
||||||
|
let document = editor.active_document();
|
||||||
|
let space_transform = gradient_space_transform(layer, document);
|
||||||
|
let gradient = super::get_gradient(layer, &document.network_interface).unwrap();
|
||||||
|
let viewport_start = space_transform.transform_point2(gradient.start);
|
||||||
|
let viewport_end = space_transform.transform_point2(gradient.end);
|
||||||
|
|
||||||
|
// Drag target of the end point, move 80px down
|
||||||
|
let new_viewport_end = viewport_end + DVec2::new(0., 80.);
|
||||||
|
editor.select_tool(ToolType::Gradient).await;
|
||||||
|
editor.move_mouse(viewport_end.x, viewport_end.y, ModifierKeys::empty(), MouseKeys::empty()).await;
|
||||||
|
editor.left_mousedown(viewport_end.x, viewport_end.y, ModifierKeys::empty()).await;
|
||||||
|
editor.move_mouse(new_viewport_end.x, new_viewport_end.y, ModifierKeys::empty(), MouseKeys::LEFT).await;
|
||||||
|
editor
|
||||||
|
.mouseup(
|
||||||
|
EditorMouseState {
|
||||||
|
editor_position: new_viewport_end,
|
||||||
|
mouse_keys: MouseKeys::empty(),
|
||||||
|
scroll_delta: ScrollDelta::default(),
|
||||||
|
},
|
||||||
|
ModifierKeys::empty(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Verify if the gradient position is updated correctly
|
||||||
|
let document = editor.active_document();
|
||||||
|
let updated = super::get_gradient(layer, &document.network_interface).expect("Gradient should exist after drag");
|
||||||
|
let updated_space_transform = gradient_space_transform(layer, document);
|
||||||
|
let updated_viewport_start = updated_space_transform.transform_point2(updated.start);
|
||||||
|
let updated_viewport_end = updated_space_transform.transform_point2(updated.end);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
updated_viewport_start.abs_diff_eq(viewport_start, 1.),
|
||||||
|
"Start should not move. Expected {viewport_start:?}, got {updated_viewport_start:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
updated_viewport_end.abs_diff_eq(new_viewport_end, 1.),
|
||||||
|
"End should move to new position. Expected {new_viewport_end:?}, got {updated_viewport_end:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn gradient_table_preserves_stops() {
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
let layer = create_gradient_table_layer(&mut editor).await;
|
||||||
|
|
||||||
|
// Set up a 3-stop gradient with distinct colors
|
||||||
|
let original_stops = GradientStops::new([
|
||||||
|
GradientStop {
|
||||||
|
position: 0.,
|
||||||
|
midpoint: 0.5,
|
||||||
|
color: Color::RED,
|
||||||
|
},
|
||||||
|
GradientStop {
|
||||||
|
position: 0.5,
|
||||||
|
midpoint: 0.5,
|
||||||
|
color: Color::GREEN,
|
||||||
|
},
|
||||||
|
GradientStop {
|
||||||
|
position: 1.,
|
||||||
|
midpoint: 0.5,
|
||||||
|
color: Color::BLUE,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
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(original_stops.clone()).with_attribute(ATTR_TRANSFORM, initial_item_transform)),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
|
||||||
|
|
||||||
|
let document = editor.active_document();
|
||||||
|
let space_transform = gradient_space_transform(layer, document);
|
||||||
|
let gradient = super::get_gradient(layer, &document.network_interface).unwrap();
|
||||||
|
let viewport_end = space_transform.transform_point2(gradient.end);
|
||||||
|
|
||||||
|
// Drag the end point 80px down
|
||||||
|
let new_viewport_end = viewport_end + DVec2::new(0., 80.);
|
||||||
|
editor.select_tool(ToolType::Gradient).await;
|
||||||
|
editor.move_mouse(viewport_end.x, viewport_end.y, ModifierKeys::empty(), MouseKeys::empty()).await;
|
||||||
|
editor.left_mousedown(viewport_end.x, viewport_end.y, ModifierKeys::empty()).await;
|
||||||
|
editor.move_mouse(new_viewport_end.x, new_viewport_end.y, ModifierKeys::empty(), MouseKeys::LEFT).await;
|
||||||
|
editor
|
||||||
|
.mouseup(
|
||||||
|
EditorMouseState {
|
||||||
|
editor_position: new_viewport_end,
|
||||||
|
mouse_keys: MouseKeys::empty(),
|
||||||
|
scroll_delta: ScrollDelta::default(),
|
||||||
|
},
|
||||||
|
ModifierKeys::empty(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Verify stops are preserved after dragging
|
||||||
|
let document = editor.active_document();
|
||||||
|
let updated = super::get_gradient(layer, &document.network_interface).expect("Gradient should exist after drag");
|
||||||
|
|
||||||
|
assert_eq!(updated.stops.len(), 3, "Stop count should be preserved");
|
||||||
|
assert_stops_at_positions(&updated.stops.position, &[0., 0.5, 1.], 1e-10);
|
||||||
|
assert_eq!(updated.stops.color[0].to_rgba8_srgb(), Color::RED.to_rgba8_srgb(), "First stop color should be preserved");
|
||||||
|
assert_eq!(updated.stops.color[1].to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb(), "Middle stop color should be preserved");
|
||||||
|
assert_eq!(updated.stops.color[2].to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb(), "Last stop color should be preserved");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
||||||
use glam::{DAffine2, UVec2};
|
use glam::{DAffine2, DVec2, UVec2};
|
||||||
use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi};
|
use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi};
|
||||||
use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue};
|
use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue};
|
||||||
use graph_craft::document::{NodeId, NodeNetwork};
|
use graph_craft::document::{NodeId, NodeNetwork};
|
||||||
|
|
@ -8,7 +8,7 @@ use graph_craft::graphene_compiler::Compiler;
|
||||||
use graph_craft::proto::GraphErrors;
|
use graph_craft::proto::GraphErrors;
|
||||||
use graph_craft::{ProtoNodeIdentifier, concrete};
|
use graph_craft::{ProtoNodeIdentifier, concrete};
|
||||||
use graphene_std::application_io::{ApplicationIo, ExportFormat, ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
|
use graphene_std::application_io::{ApplicationIo, ExportFormat, ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
|
||||||
use graphene_std::bounds::RenderBoundingBox;
|
use graphene_std::bounds::{BoundingBox, RenderBoundingBox};
|
||||||
use graphene_std::memo::IORecord;
|
use graphene_std::memo::IORecord;
|
||||||
use graphene_std::ops::Convert;
|
use graphene_std::ops::Convert;
|
||||||
#[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))]
|
#[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))]
|
||||||
|
|
@ -435,13 +435,16 @@ impl NodeRuntime {
|
||||||
// Graphic table: thumbnail
|
// Graphic table: thumbnail
|
||||||
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Graphic>>>() {
|
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Graphic>>>() {
|
||||||
if update_thumbnails {
|
if update_thumbnails {
|
||||||
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses)
|
let bounds = io.output.thumbnail_bounding_box(DAffine2::IDENTITY, true);
|
||||||
|
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Artboard table: thumbnail
|
// Artboard thumbnail bounds come from the clipping rectangles, not the content union, since the renderer
|
||||||
|
// clips content to those rectangles so anything outside isn't visible
|
||||||
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Table<Graphic>>>>() {
|
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Table<Graphic>>>>() {
|
||||||
if update_thumbnails {
|
if update_thumbnails {
|
||||||
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses)
|
let bounds = artboard_clip_bounds(&io.output);
|
||||||
|
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Vector table: vector modifications
|
// Vector table: vector modifications
|
||||||
|
|
@ -457,7 +460,13 @@ impl NodeRuntime {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If this is `Graphic` data, regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI.
|
/// If this is `Graphic` data, regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI.
|
||||||
fn render_thumbnail(thumbnail_renders: &mut HashMap<NodeId, Vec<SvgSegment>>, parent_network_node_id: NodeId, graphic: &impl Render, responses: &mut VecDeque<FrontendMessage>) {
|
fn render_thumbnail(
|
||||||
|
thumbnail_renders: &mut HashMap<NodeId, Vec<SvgSegment>>,
|
||||||
|
parent_network_node_id: NodeId,
|
||||||
|
graphic: &impl Render,
|
||||||
|
bounds: RenderBoundingBox,
|
||||||
|
responses: &mut VecDeque<FrontendMessage>,
|
||||||
|
) {
|
||||||
// Skip thumbnails if the layer is too complex (for performance)
|
// Skip thumbnails if the layer is too complex (for performance)
|
||||||
if graphic.render_complexity() > 1000 {
|
if graphic.render_complexity() > 1000 {
|
||||||
let old = thumbnail_renders.insert(parent_network_node_id, Vec::new());
|
let old = thumbnail_renders.insert(parent_network_node_id, Vec::new());
|
||||||
|
|
@ -471,12 +480,13 @@ impl NodeRuntime {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let bounds = match graphic.bounding_box(DAffine2::IDENTITY, true) {
|
// Fall back to a 1×1 rectangle if no caller offered finite bounds, then aspect-correct to the panel's 3:2 ratio
|
||||||
RenderBoundingBox::None => None,
|
let raw_bounds = match bounds {
|
||||||
RenderBoundingBox::Infinite => Some([DVec2::ZERO, DVec2::new(300., 200.)]),
|
RenderBoundingBox::Rectangle(bounds) if (bounds[1] - bounds[0]) != DVec2::ZERO => bounds,
|
||||||
RenderBoundingBox::Rectangle(bounds) => Some(bounds),
|
_ => [DVec2::ZERO, DVec2::ONE],
|
||||||
};
|
};
|
||||||
let new_thumbnail_svg = if let Some(bounds) = bounds {
|
let bounds = expand_to_thumbnail_aspect(raw_bounds);
|
||||||
|
let new_thumbnail_svg = {
|
||||||
let footprint = Footprint {
|
let footprint = Footprint {
|
||||||
transform: DAffine2::from_translation(DVec2::new(bounds[0].x, bounds[0].y)),
|
transform: DAffine2::from_translation(DVec2::new(bounds[0].x, bounds[0].y)),
|
||||||
resolution: UVec2::new((bounds[1].x - bounds[0].x).abs() as u32, (bounds[1].y - bounds[0].y).abs() as u32),
|
resolution: UVec2::new((bounds[1].x - bounds[0].x).abs() as u32, (bounds[1].y - bounds[0].y).abs() as u32),
|
||||||
|
|
@ -496,8 +506,6 @@ impl NodeRuntime {
|
||||||
render.format_svg(bounds[0], bounds[1]);
|
render.format_svg(bounds[0], bounds[1]);
|
||||||
|
|
||||||
render.svg
|
render.svg
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update frontend thumbnail
|
// Update frontend thumbnail
|
||||||
|
|
@ -512,6 +520,41 @@ impl NodeRuntime {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the union of the artboards' clipping rectangles, used as the thumbnail bounds for an artboard layer so the
|
||||||
|
/// framing matches what's actually visible after clipping rather than the unclipped content extents.
|
||||||
|
fn artboard_clip_bounds(artboards: &Table<Table<Graphic>>) -> RenderBoundingBox {
|
||||||
|
let mut combined: Option<[DVec2; 2]> = None;
|
||||||
|
for index in 0..artboards.len() {
|
||||||
|
let location: DVec2 = artboards.attribute_cloned_or_default(graphene_std::ATTR_LOCATION, index);
|
||||||
|
let dimensions: DVec2 = artboards.attribute_cloned_or_default(graphene_std::ATTR_DIMENSIONS, index);
|
||||||
|
let bounds = [location, location + dimensions];
|
||||||
|
combined = Some(match combined {
|
||||||
|
Some(existing) => [existing[0].min(bounds[0]), existing[1].max(bounds[1])],
|
||||||
|
None => bounds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
match combined {
|
||||||
|
Some(bounds) => RenderBoundingBox::Rectangle(bounds),
|
||||||
|
None => RenderBoundingBox::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expands an AABB outward (centered) to match the Layers panel thumbnail's 3:2 aspect ratio, padding the smaller axis
|
||||||
|
/// so the input's extent is always preserved.
|
||||||
|
fn expand_to_thumbnail_aspect(bounds: [DVec2; 2]) -> [DVec2; 2] {
|
||||||
|
const THUMBNAIL_ASPECT_RATIO: f64 = 1.5;
|
||||||
|
|
||||||
|
let size = bounds[1] - bounds[0];
|
||||||
|
let center = (bounds[0] + bounds[1]) / 2.;
|
||||||
|
let (width, height) = if size.x >= size.y * THUMBNAIL_ASPECT_RATIO {
|
||||||
|
(size.x, size.x / THUMBNAIL_ASPECT_RATIO)
|
||||||
|
} else {
|
||||||
|
(size.y * THUMBNAIL_ASPECT_RATIO, size.y)
|
||||||
|
};
|
||||||
|
let half = DVec2::new(width, height) / 2.;
|
||||||
|
[center - half, center + half]
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn introspect_node(path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> {
|
pub async fn introspect_node(path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> {
|
||||||
let runtime = NODE_RUNTIME.lock();
|
let runtime = NODE_RUNTIME.lock();
|
||||||
if let Some(ref mut runtime) = runtime.as_ref() {
|
if let Some(ref mut runtime) = runtime.as_ref() {
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,16 @@ 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;
|
use core_types::table::{Table, TableRow};
|
||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
use core_types::uuid::NodeId;
|
use core_types::uuid::NodeId;
|
||||||
use core_types::{CacheHash, Color, ContextFeatures, MemoHash, Node, Type};
|
use core_types::{ATTR_TRANSFORM, 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};
|
||||||
|
|
@ -118,7 +119,9 @@ 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_element(GradientStops::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::<$ty>() => TaggedValue::$identifier(Default::default()), )*
|
$( x if x == TypeId::of::<$ty>() => TaggedValue::$identifier(Default::default()), )*
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,15 @@ pub enum RenderBoundingBox {
|
||||||
|
|
||||||
pub trait BoundingBox {
|
pub trait BoundingBox {
|
||||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox;
|
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox;
|
||||||
|
|
||||||
|
/// Returns the bounding box to use when sizing this value's thumbnail in the Layers panel.
|
||||||
|
///
|
||||||
|
/// Diverges from `bounding_box` for types where the rendering bounds wouldn't make a useful thumbnail frame.
|
||||||
|
/// For instance, `GradientStops` is `Infinite` for rendering but returns the line's AABB here, so a `Table<Graphic>`
|
||||||
|
/// group of a gradient and a vector frames around the vector's geometry rather than infinity.
|
||||||
|
/// Types with no meaningful contribution (e.g., `Color`) return `Infinite` from both; the runtime substitutes a
|
||||||
|
/// small fallback rectangle at the end if no finite bounds remain after combining.
|
||||||
|
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! none_impl {
|
macro_rules! none_impl {
|
||||||
|
|
@ -19,6 +28,10 @@ macro_rules! none_impl {
|
||||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
|
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
|
||||||
RenderBoundingBox::None
|
RenderBoundingBox::None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn thumbnail_bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
|
||||||
|
RenderBoundingBox::None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -32,4 +45,9 @@ impl BoundingBox for Color {
|
||||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
|
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
|
||||||
RenderBoundingBox::Infinite
|
RenderBoundingBox::Infinite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn thumbnail_bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
|
||||||
|
// A solid color has no intrinsic extent, so its container's other content frames the thumbnail
|
||||||
|
RenderBoundingBox::Infinite
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ use std::any::TypeId;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
pub use table::{
|
pub use table::{
|
||||||
ATTR_ALPHA_BLENDING, ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_END, ATTR_LOCATION, ATTR_NAME, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE,
|
ATTR_ALPHA_BLENDING, ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_END, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME, ATTR_SPREAD_METHOD,
|
||||||
|
ATTR_START, ATTR_TRANSFORM, ATTR_TYPE,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "wasm")]
|
#[cfg(feature = "wasm")]
|
||||||
pub use tsify;
|
pub use tsify;
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,14 @@ pub const ATTR_BACKGROUND: &str = "background";
|
||||||
/// Attribute key for an artboard row's `bool` flag indicating whether content is clipped to the artboard bounds.
|
/// Attribute key for an artboard row's `bool` flag indicating whether content is clipped to the artboard bounds.
|
||||||
pub const ATTR_CLIP: &str = "clip";
|
pub const ATTR_CLIP: &str = "clip";
|
||||||
|
|
||||||
|
/// Attribute key for a `Table<GradientStops>` row's `GradientSpreadMethod`, controlling the gradient's behavior
|
||||||
|
/// outside the start/end stops (`Pad` clamps to the boundary colors, `Reflect` mirrors, `Repeat` tiles).
|
||||||
|
pub const ATTR_SPREAD_METHOD: &str = "spread_method";
|
||||||
|
|
||||||
|
/// Attribute key for a `Table<GradientStops>` row's `GradientType`, choosing between a linear gradient (color
|
||||||
|
/// transitions along the gradient line) or a radial gradient (color transitions outward from the line's start).
|
||||||
|
pub const ATTR_GRADIENT_TYPE: &str = "gradient_type";
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// TRAIT: AttributeValue
|
// TRAIT: AttributeValue
|
||||||
// =====================
|
// =====================
|
||||||
|
|
@ -824,12 +832,12 @@ impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Table<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: BoundingBox> BoundingBox for Table<T> {
|
impl<T: BoundingBox> BoundingBox for Table<T> {
|
||||||
/// Computes the combined bounding box of all rows, composing each row's transform attribute with the given transform.
|
/// Computes the combined bounding box of all items, composing each item's transform attribute with the given transform.
|
||||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
let mut combined_bounds = None;
|
let mut combined_bounds = None;
|
||||||
|
|
||||||
for (element, row_transform) in self.iter_element_values().zip(self.iter_attribute_values_or_default::<DAffine2>(ATTR_TRANSFORM)) {
|
for (element, item_transform) in self.iter_element_values().zip(self.iter_attribute_values_or_default::<DAffine2>(ATTR_TRANSFORM)) {
|
||||||
match element.bounding_box(transform * row_transform, include_stroke) {
|
match element.bounding_box(transform * item_transform, include_stroke) {
|
||||||
RenderBoundingBox::None => continue,
|
RenderBoundingBox::None => continue,
|
||||||
RenderBoundingBox::Infinite => return RenderBoundingBox::Infinite,
|
RenderBoundingBox::Infinite => return RenderBoundingBox::Infinite,
|
||||||
RenderBoundingBox::Rectangle(bounds) => match combined_bounds {
|
RenderBoundingBox::Rectangle(bounds) => match combined_bounds {
|
||||||
|
|
@ -844,6 +852,29 @@ impl<T: BoundingBox> BoundingBox for Table<T> {
|
||||||
None => RenderBoundingBox::None,
|
None => RenderBoundingBox::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
|
// `Infinite` items are skipped here (rather than propagating outward as in `bounding_box`) so a finite sibling in a mixed group dictates the framing
|
||||||
|
let mut combined_bounds = None;
|
||||||
|
let mut any_infinite = false;
|
||||||
|
|
||||||
|
for (element, item_transform) in self.iter_element_values().zip(self.iter_attribute_values_or_default::<DAffine2>(ATTR_TRANSFORM)) {
|
||||||
|
match element.thumbnail_bounding_box(transform * item_transform, include_stroke) {
|
||||||
|
RenderBoundingBox::None => continue,
|
||||||
|
RenderBoundingBox::Infinite => any_infinite = true,
|
||||||
|
RenderBoundingBox::Rectangle(bounds) => match combined_bounds {
|
||||||
|
Some(existing) => combined_bounds = Some(Quad::combine_bounds(existing, bounds)),
|
||||||
|
None => combined_bounds = Some(bounds),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match (combined_bounds, any_infinite) {
|
||||||
|
(Some(bounds), _) => RenderBoundingBox::Rectangle(bounds),
|
||||||
|
(None, true) => RenderBoundingBox::Infinite,
|
||||||
|
(None, false) => RenderBoundingBox::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> IntoIterator for Table<T> {
|
impl<T> IntoIterator for Table<T> {
|
||||||
|
|
@ -897,14 +928,14 @@ impl<T: PartialEq> PartialEq for Table<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> ApplyTransform for Table<T> {
|
impl<T> ApplyTransform for Table<T> {
|
||||||
/// Right-multiplies the modification into each row's transform attribute.
|
/// Right-multiplies the modification into each item's transform attribute.
|
||||||
fn apply_transform(&mut self, modification: &DAffine2) {
|
fn apply_transform(&mut self, modification: &DAffine2) {
|
||||||
for transform in self.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
for transform in self.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||||
*transform *= *modification;
|
*transform *= *modification;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Left-multiplies the modification into each row's transform attribute.
|
/// Left-multiplies the modification into each item's transform attribute.
|
||||||
fn left_apply_transform(&mut self, modification: &DAffine2) {
|
fn left_apply_transform(&mut self, modification: &DAffine2) {
|
||||||
for transform in self.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
for transform in self.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||||
*transform = *modification * *transform;
|
*transform = *modification * *transform;
|
||||||
|
|
|
||||||
|
|
@ -357,6 +357,17 @@ impl BoundingBox for Graphic {
|
||||||
Graphic::Gradient(table) => table.bounding_box(transform, include_stroke),
|
Graphic::Gradient(table) => table.bounding_box(transform, include_stroke),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
|
match self {
|
||||||
|
Graphic::Vector(vector) => vector.thumbnail_bounding_box(transform, include_stroke),
|
||||||
|
Graphic::RasterCPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
||||||
|
Graphic::RasterGPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
||||||
|
Graphic::Graphic(graphic) => graphic.thumbnail_bounding_box(transform, include_stroke),
|
||||||
|
Graphic::Color(color) => color.thumbnail_bounding_box(transform, include_stroke),
|
||||||
|
Graphic::Gradient(gradient) => gradient.thumbnail_bounding_box(transform, include_stroke),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TableConvert<Graphic> for Vector {
|
impl TableConvert<Graphic> for Vector {
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,10 @@ where
|
||||||
let unit_rectangle = Quad::from_box([DVec2::ZERO, DVec2::ONE]);
|
let unit_rectangle = Quad::from_box([DVec2::ZERO, DVec2::ONE]);
|
||||||
RenderBoundingBox::Rectangle((transform * unit_rectangle).bounding_box())
|
RenderBoundingBox::Rectangle((transform * unit_rectangle).bounding_box())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
|
self.bounding_box(transform, include_stroke)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderComplexity trait implementations
|
// RenderComplexity trait implementations
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ kurbo = { workspace = true }
|
||||||
vector-types = { workspace = true }
|
vector-types = { workspace = true }
|
||||||
graphic-types = { workspace = true }
|
graphic-types = { workspace = true }
|
||||||
vello = { workspace = true }
|
vello = { workspace = true }
|
||||||
|
vello_encoding = { workspace = true }
|
||||||
|
|
||||||
# Optional workspace dependencies
|
# Optional workspace dependencies
|
||||||
serde = { workspace = true, optional = true }
|
serde = { workspace = true, optional = true }
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ use core_types::render_complexity::RenderComplexity;
|
||||||
use core_types::table::{Table, TableRow};
|
use core_types::table::{Table, TableRow};
|
||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
use core_types::uuid::{NodeId, generate_uuid};
|
use core_types::uuid::{NodeId, generate_uuid};
|
||||||
use core_types::{ATTR_ALPHA_BLENDING, ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_LOCATION, ATTR_TRANSFORM};
|
use core_types::{
|
||||||
|
ATTR_ALPHA_BLENDING, ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_SPREAD_METHOD, ATTR_TRANSFORM,
|
||||||
|
};
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphene_hash::CacheHashWrapper;
|
use graphene_hash::CacheHashWrapper;
|
||||||
|
|
@ -287,6 +289,10 @@ pub fn to_transform(transform: DAffine2) -> usvg::Transform {
|
||||||
usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32)
|
usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_point(p: DVec2) -> kurbo::Point {
|
||||||
|
kurbo::Point::new(p.x, p.y)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_outline_styles(render_params: &RenderParams) -> (kurbo::Stroke, peniko::Color) {
|
fn get_outline_styles(render_params: &RenderParams) -> (kurbo::Stroke, peniko::Color) {
|
||||||
use core_types::consts::LAYER_OUTLINE_STROKE_WEIGHT;
|
use core_types::consts::LAYER_OUTLINE_STROKE_WEIGHT;
|
||||||
|
|
||||||
|
|
@ -1088,7 +1094,6 @@ impl Render for Table<Vector> {
|
||||||
}
|
}
|
||||||
let layer_bounds = element.bounding_box().unwrap_or_default();
|
let layer_bounds = element.bounding_box().unwrap_or_default();
|
||||||
|
|
||||||
let to_point = |p: DVec2| kurbo::Point::new(p.x, p.y);
|
|
||||||
let mut path = kurbo::BezPath::new();
|
let mut path = kurbo::BezPath::new();
|
||||||
for mut bezpath in element.stroke_bezpath_iter() {
|
for mut bezpath in element.stroke_bezpath_iter() {
|
||||||
bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array()));
|
bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array()));
|
||||||
|
|
@ -1749,16 +1754,35 @@ impl Render for Table<Color> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for Table<GradientStops> {
|
impl Render for Table<GradientStops> {
|
||||||
// TODO: Fix infinite gradient rendering
|
|
||||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||||
|
// For thumbnails the gradient fills a finite rect at the footprint's document space bounds, with a 1-unit margin to cover the `as u32` truncation of `Footprint::resolution`.
|
||||||
|
// The viewBox crops the overshoot. Canvas rendering keeps the polyline path since Chrome rejects rects larger than ~20 million.
|
||||||
|
let thumbnail_rect = if render_params.thumbnail {
|
||||||
|
let truncated_size = render_params.footprint.resolution.as_dvec2();
|
||||||
|
let margin = DVec2::ONE;
|
||||||
|
Some((render_params.footprint.transform.translation - margin / 2., truncated_size + margin))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
let Some(gradient) = self.element(index) else { continue };
|
let Some(gradient) = self.element(index) else { continue };
|
||||||
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, index);
|
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, index);
|
||||||
render.leaf_tag("rect", |attributes| {
|
let spread_method: GradientSpreadMethod = self.attribute_cloned_or_default(ATTR_SPREAD_METHOD, index);
|
||||||
// Chrome doesn't like drawing centered rectangles bigger than ~20 million so we draw a polyline quad instead
|
let gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, index);
|
||||||
let max = u64::MAX;
|
let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" };
|
||||||
attributes.push("points", format!("{max},{max} -{max},{max} -{max},-{max} {max},-{max}"));
|
render.leaf_tag(tag, |attributes| {
|
||||||
|
if let Some((min, size)) = thumbnail_rect {
|
||||||
|
attributes.push("x", min.x.to_string());
|
||||||
|
attributes.push("y", min.y.to_string());
|
||||||
|
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}"));
|
||||||
|
}
|
||||||
|
|
||||||
let mut stop_string = String::new();
|
let mut stop_string = String::new();
|
||||||
for (position, color, original_midpoint) in gradient.interpolated_samples() {
|
for (position, color, original_midpoint) in gradient.interpolated_samples() {
|
||||||
|
|
@ -1772,7 +1796,8 @@ impl Render for Table<GradientStops> {
|
||||||
stop_string.push_str(" />");
|
stop_string.push_str(" />");
|
||||||
}
|
}
|
||||||
|
|
||||||
let gradient_transform = render_params.footprint.transform * transform;
|
// render_thumbnail already added the footprint transform
|
||||||
|
let gradient_transform = if render_params.thumbnail { transform } else { render_params.footprint.transform * transform };
|
||||||
let gradient_transform_matrix = format_transform_matrix(gradient_transform);
|
let gradient_transform_matrix = format_transform_matrix(gradient_transform);
|
||||||
let gradient_transform_attribute = if gradient_transform_matrix.is_empty() {
|
let gradient_transform_attribute = if gradient_transform_matrix.is_empty() {
|
||||||
String::new()
|
String::new()
|
||||||
|
|
@ -1781,24 +1806,24 @@ impl Render for Table<GradientStops> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let gradient_id = generate_uuid();
|
let gradient_id = generate_uuid();
|
||||||
let start = DVec2::ZERO;
|
let spread_method_attribute = if spread_method == GradientSpreadMethod::Pad {
|
||||||
let end = DVec2::X;
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(r#" spreadMethod="{}""#, spread_method.svg_name())
|
||||||
|
};
|
||||||
|
|
||||||
match GradientType::Radial {
|
// The unit gradient line is the +X unit vector in local space, before the item's transform is applied
|
||||||
|
match gradient_type {
|
||||||
GradientType::Linear => {
|
GradientType::Linear => {
|
||||||
let (x1, y1) = (start.x, start.y);
|
|
||||||
let (x2, y2) = (end.x, end.y);
|
|
||||||
let _ = write!(
|
let _ = write!(
|
||||||
&mut attributes.0.svg_defs,
|
&mut attributes.0.svg_defs,
|
||||||
r#"<linearGradient id="{gradient_id}" gradientUnits="userSpaceOnUse" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}"{gradient_transform_attribute}>{stop_string}</linearGradient>"#
|
r#"<linearGradient id="{gradient_id}" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0"{spread_method_attribute}{gradient_transform_attribute}>{stop_string}</linearGradient>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
GradientType::Radial => {
|
GradientType::Radial => {
|
||||||
let (cx, cy) = (start.x, start.y);
|
|
||||||
let r = start.distance(end);
|
|
||||||
let _ = write!(
|
let _ = write!(
|
||||||
&mut attributes.0.svg_defs,
|
&mut attributes.0.svg_defs,
|
||||||
r#"<radialGradient id="{gradient_id}" gradientUnits="userSpaceOnUse" cx="{cx}" cy="{cy}" r="{r}"{gradient_transform_attribute}>{stop_string}</radialGradient>"#
|
r#"<radialGradient id="{gradient_id}" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="1"{spread_method_attribute}{gradient_transform_attribute}>{stop_string}</radialGradient>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1817,28 +1842,82 @@ impl Render for Table<GradientStops> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Fix infinite gradient rendering
|
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
|
||||||
fn render_to_vello(&self, scene: &mut Scene, _parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
|
|
||||||
use vello::peniko;
|
use vello::peniko;
|
||||||
|
|
||||||
for (gradient, alpha_blending) in self.iter_element_values().zip(self.iter_attribute_values_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING)) {
|
if let RenderMode::Outline = render_params.render_mode {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((((gradient, transform), alpha_blending), spread_method), gradient_type) in self
|
||||||
|
.iter_element_values()
|
||||||
|
.zip(self.iter_attribute_values_or_default::<DAffine2>(ATTR_TRANSFORM))
|
||||||
|
.zip(self.iter_attribute_values_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING))
|
||||||
|
.zip(self.iter_attribute_values_or_default::<GradientSpreadMethod>(ATTR_SPREAD_METHOD))
|
||||||
|
.zip(self.iter_attribute_values_or_default::<GradientType>(ATTR_GRADIENT_TYPE))
|
||||||
|
{
|
||||||
|
let gradient_transform = parent_transform * transform;
|
||||||
|
|
||||||
let blend_mode = alpha_blending.blend_mode.to_peniko();
|
let blend_mode = alpha_blending.blend_mode.to_peniko();
|
||||||
let opacity = alpha_blending.opacity(render_params.for_mask);
|
let opacity = alpha_blending.opacity(render_params.for_mask);
|
||||||
|
|
||||||
let color = gradient.color.first().copied().unwrap_or(Color::MAGENTA);
|
let mut stops: peniko::ColorStops = peniko::ColorStops::new();
|
||||||
let vello_color = peniko::Color::new([color.r(), color.g(), color.b(), color.a()]);
|
for (position, color, _) in gradient.interpolated_samples() {
|
||||||
|
stops.push(peniko::ColorStop {
|
||||||
|
offset: position as f32,
|
||||||
|
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let extend = match spread_method {
|
||||||
|
GradientSpreadMethod::Pad => peniko::Extend::Pad,
|
||||||
|
GradientSpreadMethod::Reflect => peniko::Extend::Reflect,
|
||||||
|
GradientSpreadMethod::Repeat => peniko::Extend::Repeat,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The unit gradient line is the +X unit vector in local space, before the item's transform is applied.
|
||||||
|
// For radial, the unit-radius circle at the origin scales out to the line's length once the brush transform applies.
|
||||||
|
let kind = match gradient_type {
|
||||||
|
GradientType::Linear => peniko::LinearGradientPosition {
|
||||||
|
start: to_point(DVec2::ZERO),
|
||||||
|
end: to_point(DVec2::X),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
GradientType::Radial => peniko::RadialGradientPosition {
|
||||||
|
start_center: to_point(DVec2::ZERO),
|
||||||
|
start_radius: 0.,
|
||||||
|
end_center: to_point(DVec2::ZERO),
|
||||||
|
end_radius: 1.,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let fill = peniko::Brush::Gradient(peniko::Gradient {
|
||||||
|
kind,
|
||||||
|
stops,
|
||||||
|
extend,
|
||||||
|
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let brush_transform = kurbo::Affine::new((gradient_transform).to_cols_array());
|
||||||
let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.));
|
let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.));
|
||||||
|
|
||||||
let mut layer = false;
|
let mut layer = false;
|
||||||
if opacity < 1. || alpha_blending.blend_mode != BlendMode::default() {
|
if opacity < 1. || alpha_blending.blend_mode != BlendMode::default() {
|
||||||
let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver);
|
let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver);
|
||||||
// See implemenation in `Table<Color>` for more detail
|
// See implementation in `Table<Color>` for more detail
|
||||||
scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::scale(f64::INFINITY), &rect);
|
scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::scale(f64::INFINITY), &rect);
|
||||||
layer = true;
|
layer = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::scale(f64::INFINITY), vello_color, None, &rect);
|
// Encode shape and brush manually instead of Scene.fill(), which would multiply brush_transform by the path transform
|
||||||
|
scene.encoding_mut().encode_transform(vello_encoding::Transform::from_kurbo(&kurbo::Affine::scale(f64::INFINITY)));
|
||||||
|
scene.encoding_mut().encode_fill_style(peniko::Fill::NonZero);
|
||||||
|
scene.encoding_mut().encode_shape(&rect, true);
|
||||||
|
|
||||||
|
scene.encoding_mut().encode_transform(vello_encoding::Transform::from_kurbo(&brush_transform));
|
||||||
|
scene.encoding_mut().swap_last_path_tags();
|
||||||
|
scene.encoding_mut().encode_brush(&fill, 1.);
|
||||||
|
|
||||||
if layer {
|
if layer {
|
||||||
scene.pop_layer();
|
scene.pop_layer();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ 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))]
|
||||||
|
|
@ -488,4 +492,12 @@ impl core_types::bounds::BoundingBox for GradientStops {
|
||||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> core_types::bounds::RenderBoundingBox {
|
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> core_types::bounds::RenderBoundingBox {
|
||||||
core_types::bounds::RenderBoundingBox::Infinite
|
core_types::bounds::RenderBoundingBox::Infinite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn thumbnail_bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> core_types::bounds::RenderBoundingBox {
|
||||||
|
// AABB of the gradient line itself, leaving aspect padding and sub-pixel fallbacks to the runtime so this stays
|
||||||
|
// a clean per-item geometric bound that combines naturally with siblings
|
||||||
|
let start = transform.transform_point2(DVec2::ZERO);
|
||||||
|
let end = transform.transform_point2(DVec2::X);
|
||||||
|
core_types::bounds::RenderBoundingBox::Rectangle([start.min(end), start.max(end)])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -468,6 +468,10 @@ impl BoundingBox for Vector {
|
||||||
None => RenderBoundingBox::None,
|
None => RenderBoundingBox::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
|
BoundingBox::bounding_box(self, transform, include_stroke)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderComplexity for Vector {
|
impl RenderComplexity for Vector {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue