mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Transition gradients from bbox-relative to document-absolute space (#4241)
* Use userSpaceOnUse to render gradient on SVG * Allow non-uniform transform to radial gradient on vello * Position gradients absolutely instead of by bbox * Fix SVG import by removing multiplication of bound's inverse transform * Represent sheared linear gradients via their equivalent gradient line * Migration * Fix migration for radial (elliptical) gradients * Fix boolean operation gradients --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -423,6 +423,10 @@ impl TableItemLayout for Vector {
|
||||
TextLabel::new("Fill Gradient End").narrow(true).widget_instance(),
|
||||
TextLabel::new(format_dvec2(gradient.end)).narrow(true).widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Fill Gradient Transform").narrow(true).widget_instance(),
|
||||
TextLabel::new(format_transform_matrix(gradient.transform)).narrow(true).widget_instance(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -225,6 +225,8 @@ pub enum DocumentMessage {
|
||||
UpdateClickTargets {
|
||||
click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>>,
|
||||
},
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
MigrateLegacyGradients,
|
||||
UpdateOutlines {
|
||||
outlines: HashMap<NodeId, Vec<Arc<ClickTarget>>>,
|
||||
},
|
||||
|
||||
@@ -124,6 +124,11 @@ pub struct DocumentMessageHandler {
|
||||
/// The path of the to the document file.
|
||||
#[serde(skip)]
|
||||
pub(crate) path: Option<PathBuf>,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// Set when a freshly-opened document still has legacy bounding-box-relative gradients; the deferred gradient
|
||||
/// migration converts them to absolute after the first graph run (when geometry bounds are available) and clears this.
|
||||
#[serde(skip)]
|
||||
pub(crate) pending_gradient_migration: bool,
|
||||
/// Path to network currently viewed in the node graph overlay. This will eventually be stored in each panel, so that multiple panels can refer to different networks
|
||||
#[serde(skip)]
|
||||
breadcrumb_network_path: Vec<NodeId>,
|
||||
@@ -181,6 +186,8 @@ impl Default for DocumentMessageHandler {
|
||||
// =============================================
|
||||
name: DEFAULT_DOCUMENT_NAME.to_string(),
|
||||
path: None,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
pending_gradient_migration: false,
|
||||
breadcrumb_network_path: Vec::new(),
|
||||
selection_network_path: Vec::new(),
|
||||
document_undo_history: VecDeque::new(),
|
||||
@@ -1365,6 +1372,38 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
.collect();
|
||||
self.network_interface.update_click_targets(layer_click_targets);
|
||||
}
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
DocumentMessage::MigrateLegacyGradients => {
|
||||
if self.pending_gradient_migration {
|
||||
self.pending_gradient_migration = false;
|
||||
|
||||
// Read each layer's legacy gradient and compute its absolute form from the now-available local bounds
|
||||
let layers: Vec<_> = self.metadata().all_layers().collect();
|
||||
let conversions: Vec<(NodeId, Fill)> = layers
|
||||
.into_iter()
|
||||
.filter_map(|layer| {
|
||||
let gradient = graph_modification_utils::get_gradient(layer, &self.network_interface)?;
|
||||
if gradient.absolute {
|
||||
return None;
|
||||
}
|
||||
let fill_node_id = graph_modification_utils::get_fill_node_id(layer, &self.network_interface)?;
|
||||
let bounds = self.metadata().nonzero_bounding_box(layer);
|
||||
let bounding_box = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
let layer_transform = self.metadata().upstream_transform(layer.to_node());
|
||||
Some((fill_node_id, Fill::Gradient(gradient.to_absolute(bounding_box, layer_transform))))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let converted_any = !conversions.is_empty();
|
||||
for (fill_node_id, fill) in conversions {
|
||||
self.network_interface
|
||||
.set_input(&InputConnector::node(fill_node_id, 1), NodeInput::value(TaggedValue::Fill(fill), false), &[]);
|
||||
}
|
||||
if converted_any {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
}
|
||||
DocumentMessage::UpdateOutlines { outlines } => {
|
||||
let layer_outlines = outlines
|
||||
.into_iter()
|
||||
|
||||
@@ -11,7 +11,6 @@ use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::descriptor;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_std::list::List;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
@@ -684,7 +683,6 @@ fn import_usvg_node_inner(
|
||||
/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
|
||||
fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, graphite_gradient_stops: &HashMap<String, GradientStops>) {
|
||||
let subpaths = convert_usvg_path(path);
|
||||
let bounds = subpaths.iter().filter_map(|subpath| subpath.bounding_box()).reduce(Quad::combine_bounds).unwrap_or_default();
|
||||
|
||||
// Skip creating a Transform node entirely when the SVG-native transform is identity.
|
||||
let node_transform = usvg_transform(node.abs_transform());
|
||||
@@ -697,8 +695,7 @@ fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
|
||||
}
|
||||
|
||||
if let Some(fill) = path.fill() {
|
||||
let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
apply_usvg_fill(fill, modify_inputs, bounds_transform, graphite_gradient_stops);
|
||||
apply_usvg_fill(fill, modify_inputs, graphite_gradient_stops);
|
||||
}
|
||||
if let Some(stroke) = path.stroke() {
|
||||
apply_usvg_stroke(stroke, modify_inputs, node_transform);
|
||||
@@ -797,14 +794,13 @@ fn convert_spread_method(spread_method: usvg::SpreadMethod) -> GradientSpreadMet
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, bounds_transform: DAffine2, graphite_gradient_stops: &HashMap<String, GradientStops>) {
|
||||
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, graphite_gradient_stops: &HashMap<String, GradientStops>) {
|
||||
modify_inputs.fill_set(match &fill.paint() {
|
||||
usvg::Paint::Color(color) => Fill::solid(usvg_color(*color, fill.opacity().get())),
|
||||
usvg::Paint::LinearGradient(linear) => {
|
||||
let gradient_transform = usvg_transform(linear.transform());
|
||||
let (start, end) = (DVec2::new(linear.x1() as f64, linear.y1() as f64), DVec2::new(linear.x2() as f64, linear.y2() as f64));
|
||||
let (start, end) = (gradient_transform.transform_point2(start), gradient_transform.transform_point2(end));
|
||||
let (start, end) = (bounds_transform.inverse().transform_point2(start), bounds_transform.inverse().transform_point2(end));
|
||||
|
||||
let gradient_type = GradientType::Linear;
|
||||
|
||||
@@ -827,6 +823,9 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b
|
||||
gradient_type,
|
||||
stops,
|
||||
spread_method,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
absolute: true,
|
||||
transform: DAffine2::IDENTITY,
|
||||
})
|
||||
}
|
||||
usvg::Paint::RadialGradient(radial) => {
|
||||
@@ -834,7 +833,6 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b
|
||||
let center = DVec2::new(radial.cx() as f64, radial.cy() as f64);
|
||||
let edge = center + DVec2::X * radial.r().get() as f64;
|
||||
let (start, end) = (gradient_transform.transform_point2(center), gradient_transform.transform_point2(edge));
|
||||
let (start, end) = (bounds_transform.inverse().transform_point2(start), bounds_transform.inverse().transform_point2(end));
|
||||
|
||||
let gradient_type = GradientType::Radial;
|
||||
|
||||
@@ -857,6 +855,9 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b
|
||||
gradient_type,
|
||||
stops,
|
||||
spread_method,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
absolute: true,
|
||||
transform: DAffine2::IDENTITY,
|
||||
})
|
||||
}
|
||||
usvg::Paint::Pattern(_) => {
|
||||
|
||||
@@ -1121,6 +1121,9 @@ pub fn document_migration_replace_resources_referenced_by_hash(document_serializ
|
||||
pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) {
|
||||
document.network_interface.migrate_path_modify_node();
|
||||
|
||||
// Legacy `Fill::Gradient`s are converted to absolute by the deferred migration pass once the first graph run yields geometry bounds
|
||||
document.pending_gradient_migration = true;
|
||||
|
||||
let network = document.network_interface.document_network().clone();
|
||||
|
||||
// Apply string and node replacements to each node
|
||||
|
||||
@@ -292,6 +292,12 @@ pub fn get_upstream_gradient_value_node_id(layer: LayerNodeIdentifier, network_i
|
||||
.find(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER)))
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// Get the layer's "Fill" node itself (whose `fill` input holds the paint value), not the node feeding that input.
|
||||
pub fn get_fill_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))
|
||||
}
|
||||
|
||||
/// Get the node connected to Fill's fill input, if any.
|
||||
pub fn get_fill_input_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||
let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
|
||||
@@ -337,10 +343,16 @@ pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: &
|
||||
.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
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// Only an existing legacy `Fill::Gradient` is in (0, 0)..(1, 1) bounding-box space; migrated and newly-created gradients are absolute (layer space).
|
||||
if get_gradient(layer, network_interface).is_some_and(|gradient| !gradient.absolute) {
|
||||
let bounds = metadata.nonzero_bounding_box(layer);
|
||||
let bound_transform = glam::DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
return metadata.transform_to_viewport(layer) * bound_transform;
|
||||
}
|
||||
|
||||
metadata.transform_to_viewport(layer)
|
||||
}
|
||||
|
||||
/// True when start→end (mapped through `transform` into viewport space) points predominantly rightward. For purely
|
||||
|
||||
@@ -363,6 +363,9 @@ fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
|
||||
spread_method: chain_state.spread_method,
|
||||
start: chain_state.transform.transform_point2(DVec2::ZERO),
|
||||
end: chain_state.transform.transform_point2(DVec2::X),
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
absolute: true,
|
||||
transform: DAffine2::IDENTITY,
|
||||
})
|
||||
} else {
|
||||
// Try to find a legacy Fill::Gradient that is selected in a Fill node
|
||||
|
||||
@@ -458,6 +458,11 @@ impl NodeGraphExecutor {
|
||||
first_element_source_id,
|
||||
});
|
||||
responses.add(DocumentMessage::UpdateClickTargets { click_targets });
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// Runs after click targets land (this graph run's geometry bounds) so the deferred gradient migration can use them.
|
||||
responses.add(DocumentMessage::MigrateLegacyGradients);
|
||||
|
||||
responses.add(DocumentMessage::UpdateOutlines { outlines });
|
||||
responses.add(DocumentMessage::UpdateTextFrames { text_frames });
|
||||
responses.add(DocumentMessage::UpdateClipTargets { clip_targets });
|
||||
|
||||
Reference in New Issue
Block a user