mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-01 14:28:11 +08:00
Refactor the 'Fill' and 'Stroke' nodes to set "fill" and "stroke" attributes for paints (#4257)
* Allow using any graphics type for fill * Adapt gradient/fill property panels and tools to handle List<T> * Make the initial gradient transform covers the target's bounding box * Introduce AnyGraphicListDyn to avoid combinatorial explosion * Allow using any graphics type for stroke paint * Add FIll node migration * Add `for_each_vector_list_mut` instead of `set_paint_attribute` * Adapt paint flow to read and write attributes instead of legacy Fill/Stroke.color * Fix responsibilities of paint related node input setters * Fix Morph by storing List<Graphic> for attributes rather than the concrete types Store fill/stroke paints as List<Graphic> so Color/Gradient transitions do not hit set_attribute_value_dyn's type-mismatch fallback to default paint. * Preserve paint attributes in vector editing ops * Enhance the clarity between direct and chained fill gradients * Update demo arts * Consolidate Fill node gradient appearance inputs * Fix after the cubic review * Revert "Consolidate Fill node gradient appearance inputs" This reverts commit 9622feb20196e2c4da99e98ca95dcc2e34c2c98e. * Replace AnyGraphicListDyn with generic paint connectors on the Fill and Stroke nodes * Canonicalize paint attribute storage to List<Graphic> with a single write helper * Fix Solidify Stroke missing fills stored in the legacy style * Fix Solidify Stroke producing invisible strokes for legacy-only stroke colors * Fix the initial gradient transform ignoring the bounding box's vertical extent * Step paint at the morph midpoint instead of dropping it for unmixable pairings * Remove migration-stage comments * Clarify the initial gradient transform helper's doc and name * Correct the bake_paint_transforms doc and prune dead tolerance arms * Delete the unused Gradient::lerp * Fix a comment typo * Thread network paths through the legacy gradient bake so nested fills migrate * Restore the legacy fill fallback in Expand Fill and Stroke * Read gradient stops from the node's own input in the Fill properties panel * Use the transform input constant instead of a hardcoded index * Remove the unreachable wired color fallback in the Fill properties solid branch * Narrow the fill overlay redraw check to actual fill inputs * Coalesce the fill setter's graph runs into a single dispatch * Position the Gradient Value node inserted for gradient stops * Bake backup gradient placement during migration * Persist pending gradient bakes so unfinished migrations retry on reopen * Migrate the backup gradient's type and spread method * Harden the gradient-migration pass against document switches and stale bakes * Keep the Fill properties UI for layerless and nested Fill nodes * Leave a wired gradient transform input connected instead of overwriting it * Refuse to start a gradient chain ahead of existing layer content * Decode a Fill node's gradient through one shared reader * Nudge a degenerate bounding box so the Fill gradient transform stays invertible * Broadcast Fill and Stroke paint with a single attribute-column pass * Fix Morph stepping the target's stroke in near the source instead of the target * Tidy conventions: clippy get_first, comment periods, sentence-case test messages * Reattach the gradient orientation doc to its function * Re-save demo artwork --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
f82b0a8fca
commit
9f9899cfd0
@@ -37,6 +37,7 @@ use graph_craft::application_io::wgpu_available;
|
||||
use graph_craft::descriptor;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
|
||||
use graphene_std::graphic::is_paint_present;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::path_bool_nodes::boolean_intersect;
|
||||
use graphene_std::raster::BlendMode;
|
||||
@@ -44,7 +45,7 @@ use graphene_std::subpath::Subpath;
|
||||
use graphene_std::vector::PointId;
|
||||
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
||||
use graphene_std::vector::misc::dvec2_to_point;
|
||||
use graphene_std::vector::style::{Fill, RenderMode};
|
||||
use graphene_std::vector::style::{Fill, Gradient, RenderMode};
|
||||
use kurbo::{Affine, BezPath, Line, PathSeg};
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
@@ -119,6 +120,12 @@ pub struct DocumentMessageHandler {
|
||||
pub graph_view_overlay_open: bool,
|
||||
/// The current opacity of the faded node graph background that covers up the artwork.
|
||||
pub graph_fade_artwork_percentage: f64,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// Fill nodes whose decomposed legacy gradient still awaits its bounding box measurement, each recorded as its enclosing
|
||||
/// network path, the node itself, and its original relative gradient. The deferred migration removes each entry as its bake lands.
|
||||
/// Transient migration state, but persisted in the saved document so unfinished bakes retry on the next open instead of losing placement.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub(crate) pending_gradient_bbox_bake: Vec<(Vec<NodeId>, NodeId, Gradient)>,
|
||||
|
||||
// =============================================
|
||||
// Fields omitted from the saved document format
|
||||
@@ -130,11 +137,6 @@ 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>,
|
||||
@@ -185,13 +187,13 @@ impl Default for DocumentMessageHandler {
|
||||
graph_view_overlay_open: false,
|
||||
snapping_state: SnappingState::default(),
|
||||
graph_fade_artwork_percentage: 80.,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
pending_gradient_bbox_bake: Vec::new(),
|
||||
// =============================================
|
||||
// Fields omitted from the saved document format
|
||||
// =============================================
|
||||
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(),
|
||||
history: DocumentHistory::default(),
|
||||
@@ -2770,8 +2772,9 @@ impl DocumentMessageHandler {
|
||||
let fill_graphic_list = self.network_interface.document_metadata().layer_fill_attributes.get(&layer);
|
||||
let stroke_graphic_list = self.network_interface.document_metadata().layer_stroke_attributes.get(&layer);
|
||||
|
||||
// `ATTR_FILL` is the source of truth when set; fall back to the legacy `style.fill` only when no attribute is present
|
||||
let has_fill = if let Some(list) = fill_graphic_list {
|
||||
list.element(0).is_some()
|
||||
is_paint_present(list)
|
||||
} else {
|
||||
!matches!(style.fill, Fill::None)
|
||||
};
|
||||
@@ -4011,6 +4014,21 @@ mod document_message_handler_tests {
|
||||
use super::*;
|
||||
use crate::test_utils::test_prelude::*;
|
||||
|
||||
#[test]
|
||||
fn pending_gradient_bakes_round_trip_through_serialization() {
|
||||
let document = DocumentMessageHandler {
|
||||
pending_gradient_bbox_bake: vec![(vec![NodeId(7)], NodeId(42), Gradient::default())],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let serialized = document.serialize_document();
|
||||
let deserialized = DocumentMessageHandler::deserialize_document(&serialized).expect("Document with pending gradient bakes should deserialize");
|
||||
assert_eq!(deserialized.pending_gradient_bbox_bake, document.pending_gradient_bbox_bake);
|
||||
|
||||
// The common empty case must not add the field to saved files
|
||||
assert!(!DocumentMessageHandler::default().serialize_document().contains("pending_gradient_bbox_bake"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_layer_selection_with_shift_and_ctrl() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, OutputConnector};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{get_fill_input_node_id, get_upstream_gradient_value_node_id, gradient_chain_target_input};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::application_io::resource::ResourceId;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
@@ -15,7 +15,7 @@ use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::Image;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientType, Stroke};
|
||||
use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientType, Stroke, build_transform_with_y_preservation};
|
||||
use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType};
|
||||
use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration};
|
||||
|
||||
@@ -454,29 +454,73 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
}
|
||||
|
||||
pub fn fill_set(&mut self, fill: Fill) {
|
||||
let fill_index = 1;
|
||||
let backup_color_index = 2;
|
||||
let backup_gradient_index = 3;
|
||||
|
||||
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
|
||||
return;
|
||||
};
|
||||
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX);
|
||||
|
||||
match &fill {
|
||||
Fill::None => {
|
||||
let input_connector = InputConnector::node(fill_node_id, backup_color_index);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(None), false), true);
|
||||
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX);
|
||||
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(None), false), true);
|
||||
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(None), false), false);
|
||||
}
|
||||
Fill::Solid(color) => {
|
||||
let input_connector = InputConnector::node(fill_node_id, backup_color_index);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Some(*color)), false), true);
|
||||
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX);
|
||||
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(Some(*color)), false), true);
|
||||
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Some(*color)), false), false);
|
||||
}
|
||||
Fill::Gradient(gradient) => {
|
||||
let input_connector = InputConnector::node(fill_node_id, backup_gradient_index);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::FillGradient(gradient.clone()), false), true);
|
||||
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput::INDEX);
|
||||
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Gradient(gradient.stops.clone()), false), true);
|
||||
|
||||
// Skip the rerender on all but the last input so the whole update triggers a single graph run
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX),
|
||||
NodeInput::value(TaggedValue::Gradient(gradient.stops.clone()), false),
|
||||
true,
|
||||
);
|
||||
|
||||
// Reposition the gradient only when the transform is a plain value, leaving a wired transform source connected
|
||||
let old_transform: Option<DAffine2> = self
|
||||
.network_interface
|
||||
.document_network()
|
||||
.nodes
|
||||
.get(&fill_node_id)
|
||||
.and_then(|node| node.inputs.get(graphene_std::vector::fill::TransformInput::INDEX))
|
||||
.and_then(|input| input.as_value())
|
||||
.map(|value| {
|
||||
if let TaggedValue::OptionalDAffine2(transform) = value {
|
||||
transform.unwrap_or(DAffine2::IDENTITY)
|
||||
} else {
|
||||
DAffine2::IDENTITY
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(old_transform) = old_transform {
|
||||
let new_transform = build_transform_with_y_preservation(old_transform, gradient.start, gradient.end);
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX),
|
||||
NodeInput::value(TaggedValue::OptionalDAffine2(Some(new_transform)), false),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientTypeInput::INDEX),
|
||||
NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false),
|
||||
true,
|
||||
);
|
||||
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::SpreadMethodInput::INDEX),
|
||||
NodeInput::value(TaggedValue::GradientSpreadMethod(gradient.spread_method), false),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
let input_connector = InputConnector::node(fill_node_id, fill_index);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Fill(fill), false), false);
|
||||
}
|
||||
|
||||
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
|
||||
@@ -536,8 +580,42 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
/// Write the gradient stops to the 'Gradient Value' node feeding the layer.
|
||||
pub fn gradient_stops_set(&mut self, stops: GradientStops) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else {
|
||||
return;
|
||||
|
||||
let gradient_value_id = match get_upstream_gradient_value_node_id(output_layer, self.network_interface) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
let target = gradient_chain_target_input(output_layer, self.network_interface);
|
||||
let starts_layer_chain = target == InputConnector::node(output_layer.to_node(), 1);
|
||||
|
||||
// The Gradient Value node discards its primary input, so starting a chain ahead of existing layer content would drop that content; refuse instead
|
||||
if starts_layer_chain && self.network_interface.upstream_output_connector(&target, &[]).is_some() {
|
||||
log::error!("Refusing to start a gradient chain ahead of existing layer content");
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(node_definition) = resolve_proto_node_type(graphene_std::math_nodes::gradient_value::IDENTIFIER) else {
|
||||
return;
|
||||
};
|
||||
let node_id = NodeId::new();
|
||||
self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]);
|
||||
|
||||
if starts_layer_chain {
|
||||
// No Fill node: the new node starts the layer's chain
|
||||
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[], self.import);
|
||||
} else {
|
||||
// Feeding a Fill node's paint input: wire it up and place it one chain-width left and a step below the Fill
|
||||
self.network_interface.set_input(&target, NodeInput::node(node_id, 0), &[]);
|
||||
if let Some(target_node_id) = target.node_id()
|
||||
&& let Some(target_position) = self.network_interface.position(&target_node_id, &[])
|
||||
{
|
||||
let node_position = self.network_interface.position(&node_id, &[]).unwrap_or_default();
|
||||
let desired_position = target_position + IVec2::new(-crate::consts::NODE_CHAIN_WIDTH, 2);
|
||||
self.network_interface.shift_absolute_node_position(&node_id, desired_position - node_position, &[]);
|
||||
}
|
||||
}
|
||||
|
||||
node_id
|
||||
}
|
||||
};
|
||||
|
||||
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput::INDEX);
|
||||
@@ -615,6 +693,7 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
/// from the default (`Linear`).
|
||||
pub fn gradient_type_set(&mut self, gradient_type: GradientType) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
|
||||
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||
let identifier = graphene_std::math_nodes::gradient_type::IDENTIFIER;
|
||||
let create_if_nonexistent = gradient_type != GradientType::default();
|
||||
@@ -630,6 +709,7 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
/// from the default (`Pad`).
|
||||
pub fn gradient_spread_method_set(&mut self, spread_method: GradientSpreadMethod) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
|
||||
let target_input = gradient_chain_target_input(output_layer, self.network_interface);
|
||||
let identifier = graphene_std::math_nodes::spread_method::IDENTIFIER;
|
||||
let create_if_nonexistent = spread_method != GradientSpreadMethod::default();
|
||||
@@ -655,7 +735,7 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
return;
|
||||
};
|
||||
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::INDEX);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintInput::<List<Graphic>>::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke.color), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
|
||||
@@ -804,40 +884,3 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both
|
||||
/// rescaled by `|new_x| / |old_x|`. This holds the (x, y) parallelogram's aspect ratio and skew fixed across an endpoint
|
||||
/// drag, so a radial ellipse stays the same shape (just rotated and resized) instead of distorting as x grows or shrinks.
|
||||
/// Falls back to a +90° rotation of `new_x` when `old_x` is degenerate.
|
||||
fn scale_y_axis_to_match_new_x(old_x: DVec2, old_y: DVec2, new_x: DVec2) -> DVec2 {
|
||||
let old_x_length = old_x.length();
|
||||
if old_x_length < 1e-9 {
|
||||
return DVec2::new(-new_x.y, new_x.x);
|
||||
}
|
||||
let ex_old = old_x / old_x_length;
|
||||
let ey_old = DVec2::new(-ex_old.y, ex_old.x);
|
||||
|
||||
let new_x_length = new_x.length();
|
||||
if new_x_length < 1e-9 {
|
||||
return DVec2::ZERO;
|
||||
}
|
||||
let ex_new = new_x / new_x_length;
|
||||
let ey_new = DVec2::new(-ex_new.y, ex_new.x);
|
||||
|
||||
let parallel = old_y.dot(ex_old);
|
||||
let perpendicular = old_y.dot(ey_old);
|
||||
let scale = new_x_length / old_x_length;
|
||||
|
||||
scale * (parallel * ex_new + perpendicular * ey_new)
|
||||
}
|
||||
|
||||
/// Build a new affine that maps canonical (0,0) -> (1,0) to (new_start, new_end), preserving the y-axis
|
||||
/// shape of `old` proportionally to the x-axis length change.
|
||||
fn build_transform_with_y_preservation(old: DAffine2, new_start: DVec2, new_end: DVec2) -> DAffine2 {
|
||||
let new_x_axis = new_end - new_start;
|
||||
let preserved_y_axis = scale_y_axis_to_match_new_x(old.matrix2.x_axis, old.matrix2.y_axis, new_x_axis);
|
||||
DAffine2 {
|
||||
matrix2: glam::DMat2::from_cols(new_x_axis, preserved_y_axis),
|
||||
translation: new_start,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1765,11 +1765,11 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
NodeGraphMessage::SetInputValue { node_id, input_index, value } => {
|
||||
use graphene_std::vector::generator_nodes::*;
|
||||
|
||||
let is_fill = matches!(value, TaggedValue::Fill(_));
|
||||
let reference = network_interface.reference(&node_id, selection_network_path);
|
||||
let is_text_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER));
|
||||
let is_stroke_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER));
|
||||
let is_fill_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER));
|
||||
let is_fill_input = is_fill_node && input_index == graphene_std::vector::fill::FillInput::<graphene_std::list::List<Graphic>>::INDEX;
|
||||
let is_shape_generator_node = reference.as_ref().is_some_and(|r| {
|
||||
[regular_polygon::IDENTIFIER, star::IDENTIFIER, arc::IDENTIFIER, spiral::IDENTIFIER, grid::IDENTIFIER, arrow::IDENTIFIER]
|
||||
.into_iter()
|
||||
@@ -1782,7 +1782,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
input,
|
||||
});
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
if is_fill {
|
||||
if is_fill_input {
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
if is_stroke_node || is_fill_node || is_shape_generator_node || is_text_node {
|
||||
|
||||
@@ -16,9 +16,11 @@ use graph_craft::application_io::resource::ResourceId;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
||||
use graph_craft::{Type, concrete};
|
||||
use graphene_std::Graphic;
|
||||
use graphene_std::NodeInputDecleration;
|
||||
use graphene_std::animation::RealTimeMode;
|
||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
||||
use graphene_std::color::SRGBA8;
|
||||
use graphene_std::extract_xy::XY;
|
||||
use graphene_std::list::List;
|
||||
use graphene_std::raster::{
|
||||
@@ -31,7 +33,9 @@ use graphene_std::text_nodes::StringCapitalization;
|
||||
use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
|
||||
use graphene_std::vector::misc::BooleanOperation;
|
||||
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
|
||||
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI, GradientSpreadMethod, GradientStops, GradientStopsUI, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphene_std::vector::style::{
|
||||
FillChoice, FillChoiceUI, GradientSpreadMethod, GradientStops, GradientStopsUI, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation,
|
||||
};
|
||||
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
|
||||
|
||||
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
|
||||
@@ -2412,60 +2416,111 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
|
||||
LayoutGroup::section(name, description, visible, pinned, expanded, node_id.0, Layout(layout))
|
||||
}
|
||||
|
||||
/// The layer that a chain node ultimately feeds, if any. Returns `None` in a nested network since the layer metadata structure
|
||||
/// is only loaded for the root document network, so a `LayerNodeIdentifier` can't be constructed there.
|
||||
fn root_layer_for_chain_node(node_id: NodeId, context: &mut NodePropertiesContext) -> Option<LayerNodeIdentifier> {
|
||||
if !context.selection_network_path.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let layer_node = context.network_interface.downstream_layer_for_chain_node(&node_id, context.selection_network_path)?;
|
||||
Some(LayerNodeIdentifier::new(layer_node, context.network_interface))
|
||||
}
|
||||
|
||||
/// Resolve the viewport-space orientation of a Fill node's gradient by walking downstream to its owning layer
|
||||
/// and reusing the same helper the Gradient tool uses, so canvas tilt and layer transforms behave identically.
|
||||
fn gradient_orientation_in_fill_node(node_id: NodeId, gradient: &graphene_std::vector::style::Gradient, context: &mut NodePropertiesContext) -> Option<bool> {
|
||||
let layer_node = context.network_interface.downstream_layer_for_chain_node(&node_id, context.selection_network_path)?;
|
||||
let layer = LayerNodeIdentifier::new(layer_node, context.network_interface);
|
||||
fn gradient_orientation_in_fill_node(node_id: NodeId, start: DVec2, end: DVec2, context: &mut NodePropertiesContext) -> Option<bool> {
|
||||
let layer = root_layer_for_chain_node(node_id, context)?;
|
||||
let transform = graph_modification_utils::gradient_space_transform(layer, context.network_interface);
|
||||
Some(graph_modification_utils::gradient_orientation_rightward(gradient.start, gradient.end, transform))
|
||||
Some(graph_modification_utils::gradient_orientation_rightward(start, end, transform))
|
||||
}
|
||||
|
||||
/// Fill Node Widgets LayoutGroup
|
||||
pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::vector::fill::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ResolvedFill {
|
||||
Solid(Option<Color>),
|
||||
Gradient {
|
||||
gradient: GradientStops,
|
||||
gradient_type: GradientType,
|
||||
spread_method: GradientSpreadMethod,
|
||||
transform: DAffine2,
|
||||
/// Whether the transform input holds a plain value (so the "Reverse Direction" button may write to it) rather than a wire.
|
||||
transform_is_value: bool,
|
||||
},
|
||||
Other,
|
||||
}
|
||||
|
||||
let connector = InputConnector::node(node_id, FillInput::<List<Graphic>>::INDEX);
|
||||
let input_type = context.network_interface.input_type(&connector, context.selection_network_path);
|
||||
|
||||
// Pass blank_assist=false because the assist slot is filled below ("Reverse Stops" button when in gradient mode)
|
||||
let mut widgets_first_row = start_widgets(ParameterWidgetsInfo::new(node_id, FillInput::<Color>::INDEX, false, context));
|
||||
let mut widgets_first_row = start_widgets(ParameterWidgetsInfo::new(node_id, FillInput::<List<Graphic>>::INDEX, false, context));
|
||||
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
Err(err) => {
|
||||
log::error!("Could not get document node in fill_properties: {err}");
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
|
||||
let (fill, backup_color, backup_gradient) = if let (Some(TaggedValue::Fill(fill)), Some(TaggedValue::Color(backup_color)), Some(TaggedValue::FillGradient(backup_gradient))) = (
|
||||
&document_node.inputs[FillInput::<Color>::INDEX].as_value(),
|
||||
&document_node.inputs[BackupColorInput::INDEX].as_value(),
|
||||
&document_node.inputs[BackupGradientInput::INDEX].as_value(),
|
||||
) {
|
||||
(fill, backup_color, backup_gradient)
|
||||
} else {
|
||||
if get_document_node(node_id, context).is_ok_and(|node| node.inputs.get(FillInput::<List<Graphic>>::INDEX).is_some_and(|input| input.is_exposed())) {
|
||||
return vec![LayoutGroup::row(widgets_first_row)];
|
||||
}
|
||||
|
||||
// A Fill node not attached to a layer (or living in a nested network) still shows its full fill UI; only the gradient's
|
||||
// bounding-box default transform needs the layer, and it falls back to a unit box when there isn't one.
|
||||
let layer = root_layer_for_chain_node(node_id, context);
|
||||
|
||||
let fill = match input_type.compiled_nested_type() {
|
||||
Some(ty) if ty == &concrete!(List<Color>) => {
|
||||
if let Ok(document_node) = get_document_node(node_id, context) {
|
||||
let color = match document_node.inputs[FillInput::<List<Graphic>>::INDEX].as_value() {
|
||||
Some(&TaggedValue::Color(c)) => c,
|
||||
_ => None,
|
||||
};
|
||||
ResolvedFill::Solid(color)
|
||||
} else {
|
||||
ResolvedFill::Other
|
||||
}
|
||||
}
|
||||
Some(ty) if ty == &concrete!(List<GradientStops>) => {
|
||||
// Read this node's own inputs rather than the layer's nearest Fill, which may be a different node when Fills are chained
|
||||
if let Ok(document_node) = get_document_node(node_id, context)
|
||||
&& let Some(gradient) = graph_modification_utils::read_fill_node_gradient(document_node, || {
|
||||
layer.map_or([DVec2::ZERO, DVec2::ONE], |layer| context.network_interface.document_metadata().nonzero_bounding_box(layer))
|
||||
}) {
|
||||
ResolvedFill::Gradient {
|
||||
gradient: gradient.stops,
|
||||
gradient_type: gradient.gradient_type,
|
||||
spread_method: gradient.spread_method,
|
||||
transform: gradient.transform,
|
||||
transform_is_value: gradient.transform_is_value,
|
||||
}
|
||||
} else {
|
||||
ResolvedFill::Other
|
||||
}
|
||||
}
|
||||
_ => ResolvedFill::Other,
|
||||
};
|
||||
let fill2 = fill.clone();
|
||||
let backup_color_fill: Fill = (*backup_color).into();
|
||||
let backup_gradient_fill: Fill = backup_gradient.clone().into();
|
||||
|
||||
let (backup_color, backup_gradient) = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => {
|
||||
let backup_color = match document_node.inputs[BackupColorInput::INDEX].as_value() {
|
||||
Some(&TaggedValue::Color(color)) => color,
|
||||
_ => None,
|
||||
};
|
||||
let backup_stops = match document_node.inputs[BackupGradientInput::INDEX].as_value() {
|
||||
Some(TaggedValue::Gradient(stops)) => stops.clone(),
|
||||
_ => GradientStops::default(),
|
||||
};
|
||||
(backup_color, backup_stops)
|
||||
}
|
||||
Err(_) => (None, GradientStops::default()),
|
||||
};
|
||||
|
||||
match &fill {
|
||||
ResolvedFill::Gradient { gradient: stops, .. } => {
|
||||
let stops = stops.clone();
|
||||
|
||||
match fill {
|
||||
Fill::Gradient(gradient) => {
|
||||
let reverse_button = IconButton::new("Reverse", 24)
|
||||
.tooltip_label("Reverse Stops")
|
||||
.tooltip_description("Reverse the gradient color stops.")
|
||||
.on_update(update_value(
|
||||
{
|
||||
let gradient = gradient.clone();
|
||||
move |_| {
|
||||
let mut gradient = gradient.clone();
|
||||
gradient.stops = gradient.stops.reversed();
|
||||
TaggedValue::Fill(Fill::Gradient(gradient))
|
||||
}
|
||||
},
|
||||
node_id,
|
||||
FillInput::<Color>::INDEX,
|
||||
))
|
||||
.on_update(update_value(move |_| TaggedValue::Gradient(stops.reversed()), node_id, FillInput::<List<Graphic>>::INDEX))
|
||||
.widget_instance();
|
||||
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets_first_row.push(reverse_button);
|
||||
@@ -2473,32 +2528,65 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
_ => add_blank_assist(&mut widgets_first_row),
|
||||
}
|
||||
|
||||
let fill_choice_ui = match &fill {
|
||||
ResolvedFill::Solid(color) => {
|
||||
if let Some(color) = color {
|
||||
FillChoiceUI::Solid(SRGBA8::from(*color))
|
||||
} else {
|
||||
FillChoiceUI::None
|
||||
}
|
||||
}
|
||||
ResolvedFill::Gradient { gradient: stops, .. } => FillChoiceUI::Gradient(GradientStopsUI::from(stops)),
|
||||
ResolvedFill::Other => FillChoiceUI::None,
|
||||
};
|
||||
|
||||
let solid_set_messages = move |color: Option<Color>| Message::Batched {
|
||||
messages: Box::new([
|
||||
NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: FillInput::<List<Graphic>>::INDEX,
|
||||
value: TaggedValue::Color(color),
|
||||
}
|
||||
.into(),
|
||||
NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: BackupColorInput::INDEX,
|
||||
value: TaggedValue::Color(color),
|
||||
}
|
||||
.into(),
|
||||
]),
|
||||
};
|
||||
|
||||
let gradient_set_messages = move |gradient: GradientStops| Message::Batched {
|
||||
messages: Box::new([
|
||||
NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: FillInput::<List<Graphic>>::INDEX,
|
||||
value: TaggedValue::Gradient(gradient.clone()),
|
||||
}
|
||||
.into(),
|
||||
NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: BackupGradientInput::INDEX,
|
||||
value: TaggedValue::Gradient(gradient),
|
||||
}
|
||||
.into(),
|
||||
]),
|
||||
};
|
||||
|
||||
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets_first_row.push(
|
||||
ColorInput::default()
|
||||
.value(FillChoiceUI::from(&FillChoice::from(fill.clone())))
|
||||
.on_update(move |x: &ColorInput| {
|
||||
let new_fill = FillChoice::from(&x.value).to_fill(fill2.as_gradient());
|
||||
let (backup_index, backup_value) = match &new_fill {
|
||||
Fill::None => (BackupColorInput::INDEX, TaggedValue::Color(None)),
|
||||
Fill::Solid(color) => (BackupColorInput::INDEX, TaggedValue::Color(Some(*color))),
|
||||
Fill::Gradient(gradient) => (BackupGradientInput::INDEX, TaggedValue::FillGradient(gradient.clone())),
|
||||
};
|
||||
Message::Batched {
|
||||
messages: Box::new([
|
||||
NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: backup_index,
|
||||
value: backup_value,
|
||||
}
|
||||
.into(),
|
||||
NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: FillInput::<Color>::INDEX,
|
||||
value: TaggedValue::Fill(new_fill),
|
||||
}
|
||||
.into(),
|
||||
]),
|
||||
.value(fill_choice_ui)
|
||||
.on_update(move |x: &ColorInput| match &x.value {
|
||||
FillChoiceUI::None => solid_set_messages(None),
|
||||
FillChoiceUI::Solid(srgba8) => {
|
||||
let color = Some(Color::from(*srgba8));
|
||||
solid_set_messages(color)
|
||||
}
|
||||
FillChoiceUI::Gradient(gradient_stops_ui) => {
|
||||
let gradient = GradientStops::from(gradient_stops_ui);
|
||||
gradient_set_messages(gradient)
|
||||
}
|
||||
})
|
||||
.on_commit(commit_value)
|
||||
@@ -2514,61 +2602,50 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
let entries = vec![
|
||||
RadioEntryData::new("solid")
|
||||
.label("Solid")
|
||||
.on_update(update_value(move |_| TaggedValue::Fill(backup_color_fill.clone()), node_id, FillInput::<Color>::INDEX))
|
||||
.on_update(update_value(move |_| TaggedValue::Color(backup_color), node_id, FillInput::<List<Graphic>>::INDEX))
|
||||
.on_commit(commit_value),
|
||||
RadioEntryData::new("gradient")
|
||||
.label("Gradient")
|
||||
.on_update(update_value(move |_| TaggedValue::Fill(backup_gradient_fill.clone()), node_id, FillInput::<Color>::INDEX))
|
||||
.on_update(update_value(move |_| TaggedValue::Gradient(backup_gradient.clone()), node_id, FillInput::<List<Graphic>>::INDEX))
|
||||
.on_commit(commit_value),
|
||||
];
|
||||
|
||||
row.extend_from_slice(&[
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
RadioInput::new(entries).selected_index(Some(if fill.as_gradient().is_some() { 1 } else { 0 })).widget_instance(),
|
||||
RadioInput::new(entries)
|
||||
.selected_index(Some(if matches!(fill, ResolvedFill::Gradient { .. }) { 1 } else { 0 }))
|
||||
.widget_instance(),
|
||||
]);
|
||||
|
||||
LayoutGroup::row(row)
|
||||
};
|
||||
widgets.push(fill_type_switch);
|
||||
|
||||
if let Fill::Gradient(gradient) = fill.clone() {
|
||||
if let ResolvedFill::Gradient {
|
||||
gradient_type,
|
||||
spread_method,
|
||||
transform,
|
||||
transform_is_value,
|
||||
..
|
||||
} = fill.clone()
|
||||
{
|
||||
// Linear/Radial radio: blank assist (the "Reverse Direction" button has been moved down to the spread method row)
|
||||
let mut row = vec![TextLabel::new("").widget_instance()];
|
||||
add_blank_assist(&mut row);
|
||||
|
||||
let gradient_for_closure = gradient.clone();
|
||||
|
||||
let entries = [GradientType::Linear, GradientType::Radial]
|
||||
.iter()
|
||||
.map(|&grad_type| {
|
||||
let gradient = gradient_for_closure.clone();
|
||||
let set_input_value = update_value(
|
||||
move |_: &()| {
|
||||
let mut new_gradient = gradient.clone();
|
||||
new_gradient.gradient_type = grad_type;
|
||||
TaggedValue::Fill(Fill::Gradient(new_gradient))
|
||||
},
|
||||
node_id,
|
||||
FillInput::<Color>::INDEX,
|
||||
);
|
||||
RadioEntryData::new(format!("{:?}", grad_type))
|
||||
.label(format!("{:?}", grad_type))
|
||||
.on_update(move |_| Message::Batched {
|
||||
messages: Box::new([
|
||||
set_input_value(&()),
|
||||
GradientToolMessage::UpdateOptions {
|
||||
options: GradientOptionsUpdate::Type(grad_type),
|
||||
}
|
||||
.into(),
|
||||
]),
|
||||
})
|
||||
.on_update(update_value(move |_| TaggedValue::GradientType(grad_type), node_id, GradientTypeInput::INDEX))
|
||||
.on_commit(commit_value)
|
||||
})
|
||||
.collect();
|
||||
|
||||
row.extend_from_slice(&[
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
RadioInput::new(entries).selected_index(Some(gradient.gradient_type as u32)).widget_instance(),
|
||||
RadioInput::new(entries).selected_index(Some(gradient_type as u32)).widget_instance(),
|
||||
]);
|
||||
|
||||
widgets.push(LayoutGroup::row(row));
|
||||
@@ -2577,75 +2654,41 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
// space so canvas tilt and layer transforms behave the same as in the Gradient tool's control bar.
|
||||
let mut spread_methods_row = vec![TextLabel::new("").widget_instance()];
|
||||
|
||||
let orientation_rightward = gradient_orientation_in_fill_node(node_id, &gradient, context).unwrap_or(true);
|
||||
let reverse_direction_button = IconButton::new(if orientation_rightward { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24)
|
||||
.tooltip_label("Reverse Direction")
|
||||
.tooltip_description(if gradient.gradient_type == GradientType::Radial {
|
||||
"Reverse which end the gradient radiates from."
|
||||
} else {
|
||||
"Swap the start and end points of the gradient line."
|
||||
})
|
||||
.on_update(update_value(
|
||||
{
|
||||
let gradient = gradient.clone();
|
||||
move |_| {
|
||||
let mut gradient = gradient.clone();
|
||||
std::mem::swap(&mut gradient.start, &mut gradient.end);
|
||||
TaggedValue::Fill(Fill::Gradient(gradient))
|
||||
}
|
||||
},
|
||||
node_id,
|
||||
FillInput::<Color>::INDEX,
|
||||
))
|
||||
.widget_instance();
|
||||
spread_methods_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
spread_methods_row.push(reverse_direction_button);
|
||||
// The button writes a value into the transform input, so only offer it when the input isn't wired
|
||||
if transform_is_value {
|
||||
let start = transform.transform_point2(DVec2::ZERO);
|
||||
let end = transform.transform_point2(DVec2::X);
|
||||
let new_transform = build_transform_with_y_preservation(transform, end, start);
|
||||
let orientation_rightward = gradient_orientation_in_fill_node(node_id, start, end, context).unwrap_or(true);
|
||||
|
||||
let reverse_direction_button = IconButton::new(if orientation_rightward { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24)
|
||||
.tooltip_label("Reverse Direction")
|
||||
.tooltip_description(if gradient_type == GradientType::Radial {
|
||||
"Reverse which end the gradient radiates from."
|
||||
} else {
|
||||
"Swap the start and end points of the gradient line."
|
||||
})
|
||||
.on_update(update_value(move |_| TaggedValue::OptionalDAffine2(Some(new_transform)), node_id, TransformInput::INDEX))
|
||||
.widget_instance();
|
||||
spread_methods_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
spread_methods_row.push(reverse_direction_button);
|
||||
} else {
|
||||
add_blank_assist(&mut spread_methods_row);
|
||||
}
|
||||
|
||||
let spread_method_entries = [GradientSpreadMethod::Pad, GradientSpreadMethod::Reflect, GradientSpreadMethod::Repeat]
|
||||
.iter()
|
||||
.map(|&spread_method| {
|
||||
let gradient_for_input = gradient_for_closure.clone();
|
||||
let gradient_for_backup = gradient_for_closure.clone();
|
||||
|
||||
let set_input_value = update_value(
|
||||
move |_: &()| {
|
||||
let mut new_gradient = gradient_for_input.clone();
|
||||
new_gradient.spread_method = spread_method;
|
||||
TaggedValue::Fill(Fill::Gradient(new_gradient))
|
||||
},
|
||||
node_id,
|
||||
FillInput::<Color>::INDEX,
|
||||
);
|
||||
|
||||
let set_backup_value = update_value(
|
||||
move |_: &()| {
|
||||
let mut new_gradient = gradient_for_backup.clone();
|
||||
new_gradient.spread_method = spread_method;
|
||||
TaggedValue::FillGradient(new_gradient)
|
||||
},
|
||||
node_id,
|
||||
BackupGradientInput::INDEX,
|
||||
);
|
||||
|
||||
RadioEntryData::new(format!("{:?}", spread_method))
|
||||
.label(format!("{:?}", spread_method))
|
||||
.on_update(move |_| Message::Batched {
|
||||
messages: Box::new([
|
||||
set_input_value(&()),
|
||||
set_backup_value(&()),
|
||||
GradientToolMessage::UpdateOptions {
|
||||
options: GradientOptionsUpdate::SetSpreadMethod(spread_method),
|
||||
}
|
||||
.into(),
|
||||
]),
|
||||
})
|
||||
.on_update(update_value(move |_| TaggedValue::GradientSpreadMethod(spread_method), node_id, SpreadMethodInput::INDEX))
|
||||
.on_commit(commit_value)
|
||||
})
|
||||
.collect();
|
||||
|
||||
spread_methods_row.extend_from_slice(&[
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
RadioInput::new(spread_method_entries).selected_index(Some(gradient.spread_method as u32)).widget_instance(),
|
||||
RadioInput::new(spread_method_entries).selected_index(Some(spread_method as u32)).widget_instance(),
|
||||
]);
|
||||
|
||||
widgets.push(LayoutGroup::row(spread_methods_row));
|
||||
@@ -2660,7 +2703,7 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
Err(err) => {
|
||||
log::error!("Could not get document node in fill_properties: {err}");
|
||||
log::error!("Could not get document node in stroke_properties: {err}");
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
@@ -2676,7 +2719,7 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
|
||||
let miter_limit_disabled = join_value != &StrokeJoin::Miter;
|
||||
|
||||
let color = color_widget(
|
||||
ParameterWidgetsInfo::new(node_id, ColorInput::INDEX, true, context),
|
||||
ParameterWidgetsInfo::new(node_id, PaintInput::<List<Graphic>>::INDEX, true, context),
|
||||
crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default(),
|
||||
);
|
||||
let weight = number_widget(ParameterWidgetsInfo::new(node_id, WeightInput::INDEX, true, context), NumberInput::default().unit(" px").min(0.));
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector};
|
||||
use crate::messages::prelude::DocumentMessageHandler;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use glam::{DVec2, IVec2};
|
||||
use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId};
|
||||
use graph_craft::descriptor;
|
||||
@@ -15,7 +14,7 @@ use graphene_std::ProtoNodeIdentifier;
|
||||
use graphene_std::text::{TextAlign, TypesettingConfig};
|
||||
use graphene_std::transform::ScaleType;
|
||||
use graphene_std::uuid::NodeId;
|
||||
use graphene_std::vector::style::{PaintOrder, StrokeAlign};
|
||||
use graphene_std::vector::style::{Fill, PaintOrder, StrokeAlign};
|
||||
use std::collections::HashMap;
|
||||
use std::f64::consts::PI;
|
||||
use std::ops::Range;
|
||||
@@ -1122,9 +1121,6 @@ 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 pre-pass that measures each fill's geometry
|
||||
document.pending_gradient_migration = !graph_modification_utils::legacy_gradient_fill_nodes(&document.network_interface).is_empty();
|
||||
|
||||
let network = document.network_interface.document_network().clone();
|
||||
|
||||
// Apply string and node replacements to each node
|
||||
@@ -1575,6 +1571,96 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
inputs_count = 5;
|
||||
}
|
||||
|
||||
// Upgrade the legacy 4-input Fill node (content, fill: Fill, _backup_color, _backup_gradient: Gradient) to the
|
||||
// value-model 7-input shape (content, fill: generic paint list, _backup_color, _backup_gradient, _gradient_type, _spread_method, _transform).
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::fill::IDENTIFIER) && inputs_count == 4 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||
|
||||
// Content: no change
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
|
||||
// Fill: a literal Fill value is decomposed, and a wired input (`List<GradientStops> / List<Color>`) is kept as-is
|
||||
match old_inputs[1].as_value() {
|
||||
Some(TaggedValue::Fill(old_fill)) => {
|
||||
let exposed = old_inputs[1].is_exposed();
|
||||
let fill_value = match old_fill {
|
||||
Fill::None => TaggedValue::Color(None),
|
||||
Fill::Solid(color) => TaggedValue::Color(Some(*color)),
|
||||
Fill::Gradient(gradient) => TaggedValue::Gradient(gradient.stops.clone()),
|
||||
};
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(fill_value, exposed), network_path);
|
||||
|
||||
// Gradient metadata (4, 5, 6): applies only to a literal gradient, solids/none keep the template defaults
|
||||
if let Fill::Gradient(gradient) = old_fill {
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node(*node_id, 4),
|
||||
NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false),
|
||||
network_path,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node(*node_id, 5),
|
||||
NodeInput::value(TaggedValue::GradientSpreadMethod(gradient.spread_method), false),
|
||||
network_path,
|
||||
);
|
||||
|
||||
let transform = if gradient.absolute {
|
||||
Some(gradient.transform * gradient.to_transform())
|
||||
} else {
|
||||
// Baking a legacy bounding-box-relative gradient is deferred until the measurement pre-pass can supply the paint target's bounds
|
||||
document.pending_gradient_bbox_bake.push((network_path.to_vec(), *node_id, gradient.clone()));
|
||||
None
|
||||
};
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::OptionalDAffine2(transform), false), network_path);
|
||||
}
|
||||
}
|
||||
// Wired/exposed fill keeps the connection.
|
||||
// The generic paint connector accepts `List<Color>`/`List<GradientStops>` sources directly, and there were no other nodes which can generate output type that implements `From` for `Fill`.
|
||||
_ => {
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Color backup: no change
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||
|
||||
// Gradient backup: extract stops
|
||||
if let Some(TaggedValue::FillGradient(g)) = old_inputs[3].as_value() {
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Gradient(g.stops.clone()), false), network_path);
|
||||
|
||||
// A solid/no-fill node leaves the gradient metadata inputs unused, so seed them from the backup gradient for a later Solid -> Gradient toggle to restore
|
||||
if matches!(old_inputs[1].as_value(), Some(TaggedValue::Fill(Fill::None | Fill::Solid(_)))) {
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::GradientType(g.gradient_type), false), network_path);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node(*node_id, 5),
|
||||
NodeInput::value(TaggedValue::GradientSpreadMethod(g.spread_method), false),
|
||||
network_path,
|
||||
);
|
||||
|
||||
let transform = if g.absolute {
|
||||
Some(g.transform * g.to_transform())
|
||||
} else {
|
||||
document.pending_gradient_bbox_bake.push((network_path.to_vec(), *node_id, g.clone()));
|
||||
None
|
||||
};
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::OptionalDAffine2(transform), false), network_path);
|
||||
}
|
||||
}
|
||||
|
||||
inputs_count = 7;
|
||||
}
|
||||
|
||||
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 8 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
|
||||
@@ -1491,9 +1491,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
let physical_resolution = viewport.size().to_physical().into_dvec2().round().as_uvec2();
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// A freshly-opened document with legacy gradients runs a one-time measurement pre-pass instead of rendering, until every gradient is converted to absolute space
|
||||
if document.pending_gradient_migration {
|
||||
self.executor.drive_gradient_migration(document, document_id, physical_resolution, scale, responses);
|
||||
// A freshly-opened document with legacy gradients (whether newly decomposed or persisted from a save made before every bake landed) runs a
|
||||
// one-time measurement pre-pass instead of rendering, until every gradient's transform is baked into absolute space
|
||||
if !document.pending_gradient_bbox_bake.is_empty() && self.executor.drive_gradient_migration(document, document_id, physical_resolution, scale, responses) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user